From 90754ad7d185fe345e9e4f5aea16aba52beef19c Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Thu, 26 Jun 2025 18:45:10 -0500 Subject: [PATCH] refactor: replace stream-json with sax parser for message parsing - Remove stream-json and proper-lockfile dependencies - Add sax parser dependency - Refactor DirectiveStreamingParser to use SAX parser - Reorganize assistant-message module to message-parsing - Extract directive parsing into focused classes - Update all related tests and imports - Maintain backward compatibility with existing API --- .github/pull_request_template.md | 24 +- .roo/rules-code/use-safeWriteJson.md | 6 - .roo/rules-translate/001-general-rules.md | 6 +- .roomodes | 54 - CHANGELOG.md | 1 + .../src/suite/tools/use-mcp-tool.test.ts | 2 +- locales/ca/README.md | 73 +- locales/de/README.md | 73 +- locales/es/README.md | 73 +- locales/fr/README.md | 73 +- locales/hi/README.md | 73 +- locales/id/README.md | 73 +- locales/it/README.md | 73 +- locales/ja/README.md | 73 +- locales/ko/README.md | 73 +- locales/nl/README.md | 73 +- locales/pl/README.md | 73 +- locales/pt-BR/README.md | 73 +- locales/ru/README.md | 73 +- locales/tr/README.md | 73 +- locales/vi/README.md | 73 +- locales/zh-CN/README.md | 73 +- locales/zh-TW/README.md | 73 +- packages/build/src/__tests__/index.test.ts | 4 +- packages/cloud/package.json | 1 + packages/cloud/src/CloudService.ts | 64 +- packages/cloud/src/SettingsService.ts | 134 +- packages/cloud/src/ShareService.ts | 36 +- packages/cloud/src/TelemetryClient.ts | 67 +- .../cloud/src/__tests__/CloudService.test.ts | 211 +-- .../cloud/src/__tests__/ShareService.test.ts | 161 +-- .../src/__tests__/TelemetryClient.test.ts | 311 ----- .../src/__tests__/auth/WebAuthService.spec.ts | 58 +- packages/cloud/src/auth/WebAuthService.ts | 118 +- packages/telemetry/src/BaseTelemetryClient.ts | 15 +- .../telemetry/src/PostHogTelemetryClient.ts | 15 - packages/telemetry/src/TelemetryService.ts | 25 - .../__tests__/PostHogTelemetryClient.test.ts | 113 -- packages/types/npm/package.json | 2 +- packages/types/src/cloud.ts | 1 - packages/types/src/global-settings.ts | 3 - packages/types/src/index.ts | 1 - packages/types/src/mode.ts | 2 - packages/types/src/provider-settings.ts | 18 +- packages/types/src/providers/groq.ts | 2 +- packages/types/src/providers/index.ts | 1 - packages/types/src/sharing.ts | 8 - packages/types/src/telemetry.ts | 38 - packages/types/src/vscode.ts | 1 - pnpm-lock.yaml | 97 +- scripts/update-contributors.js | 10 +- src/activate/CodeActionProvider.ts | 5 - .../__tests__/CodeActionProvider.spec.ts | 26 - .../__tests__/registerCommands.spec.ts | 9 - src/activate/handleUri.ts | 8 +- src/activate/registerCommands.ts | 30 +- src/api/index.ts | 3 - .../__tests__/bedrock-error-handling.spec.ts | 551 -------- .../__tests__/claude-code-caching.spec.ts | 305 ----- src/api/providers/__tests__/lmstudio.spec.ts | 2 +- src/api/providers/__tests__/openai.spec.ts | 8 +- src/api/providers/bedrock.ts | 180 +-- .../fetchers/__tests__/lmstudio.test.ts | 42 +- .../fetchers/__tests__/ollama.test.ts | 89 -- src/api/providers/fetchers/lmstudio.ts | 32 +- src/api/providers/fetchers/modelCache.ts | 3 +- .../providers/fetchers/modelEndpointCache.ts | 3 +- src/api/providers/fetchers/ollama.ts | 20 +- src/api/providers/index.ts | 1 - src/api/providers/openai.ts | 6 +- src/api/providers/openrouter.ts | 14 +- src/api/transform/stream.ts | 8 +- .../__tests__/parseAssistantMessage.spec.ts | 340 ----- .../parseAssistantMessageBenchmark.ts | 111 -- src/core/assistant-message/index.ts | 2 - .../parseAssistantMessage.ts | 166 --- .../parseAssistantMessageV2.ts | 281 ---- src/core/config/CustomModesManager.ts | 157 +-- .../__tests__/CustomModesManager.spec.ts | 2 +- .../CustomModesManager.yamlEdgeCases.spec.ts | 474 ------- .../config/__tests__/importExport.spec.ts | 145 +- src/core/config/importExport.ts | 129 +- .../context-tracking/FileContextTracker.ts | 3 +- .../strategies/multi-file-search-replace.ts | 7 +- .../diff/strategies/multi-search-replace.ts | 9 +- .../message-parsing/CodeBlockStateMachine.ts | 61 + src/core/message-parsing/DirectiveHandler.ts | 11 + .../DirectiveHandlerRegistry.ts | 27 + .../DirectiveRegistryFactory.ts | 15 + .../DirectiveStreamingParser.ts | 164 +++ src/core/message-parsing/FallbackParser.ts | 139 ++ .../ParameterCodeBlockHandler.ts | 12 + src/core/message-parsing/ParseContext.ts | 21 + src/core/message-parsing/XmlUtils.ts | 23 + .../code-block-state-machine.spec.ts | 65 + .../message-parsing/directives/Directive.ts | 4 + .../directives/TextDirective.ts | 9 + .../directives/ToolDirective.ts | 10 + src/core/message-parsing/directives/index.ts | 25 + .../AccessMcpResourceToolDirective.ts | 10 + .../AskFollowupQuestionToolDirective.ts | 10 + .../AttemptCompletionToolDirective.ts | 10 + .../BrowserActionToolDirective.ts | 10 + .../CodebaseSearchToolDirective.ts | 10 + .../ExecuteCommandToolDirective.ts | 11 + .../FetchInstructionsToolDirective.ts | 10 + .../InsertCodeBlockToolDirective.ts | 10 + .../ListCodeDefinitionNamesToolDirective.ts | 10 + .../tool-directives/ListFilesToolDirective.ts | 10 + .../tool-directives/NewTaskToolDirective.ts | 10 + .../tool-directives/ReadFileToolDirective.ts | 10 + .../SearchAndReplaceToolDirective.ts | 11 + .../SearchFilesToolDirective.ts | 10 + .../SwitchModeToolDirective.ts | 10 + .../tool-directives/ToolParamName.ts | 45 + .../tool-directives/ToolResponse.ts | 6 + .../UseMcpToolToolDirective.ts | 10 + .../WriteToFileToolDirective.ts | 10 + .../directives/tool-directives/index.ts | 21 + .../handlers/BaseDirectiveHandler.ts | 28 + .../handlers/TextDirectiveHandler.ts | 59 + .../handlers/ToolDirectiveHandler.ts | 91 ++ src/core/message-parsing/handlers/index.ts | 2 + src/core/message-parsing/index.ts | 5 + .../presentAssistantMessage.ts | 140 +- .../architect-mode-prompt.snap | 38 + .../ask-mode-prompt.snap | 38 + .../mcp-server-creation-disabled.snap | 38 + .../mcp-server-creation-enabled.snap | 38 + .../partial-reads-enabled.snap | 38 + .../consistent-system-prompt.snap | 38 + .../with-computer-use-support.snap | 38 + .../with-diff-enabled-false.snap | 38 + .../system-prompt/with-diff-enabled-true.snap | 38 + .../with-diff-enabled-undefined.snap | 38 + .../with-different-viewport-size.snap | 38 + .../system-prompt/with-mcp-hub-provided.snap | 38 + .../system-prompt/with-undefined-mcp-hub.snap | 38 + .../__tests__/custom-system-prompt.spec.ts | 2 +- ...custom-instructions-path-detection.spec.ts | 66 - .../prompts/sections/custom-instructions.ts | 70 +- src/core/prompts/sections/mcp-servers.ts | 2 +- src/core/task-persistence/apiMessages.ts | 3 +- src/core/task-persistence/taskMessages.ts | 3 +- src/core/task-persistence/taskMetadata.ts | 63 +- src/core/task/Task.ts | 94 +- src/core/tools/ToolRepetitionDetector.ts | 24 +- .../__tests__/ToolRepetitionDetector.spec.ts | 107 +- .../__tests__/executeCommandTool.spec.ts | 29 +- src/core/tools/__tests__/newTaskTool.spec.ts | 26 +- src/core/tools/__tests__/readFileTool.spec.ts | 944 ++++++++++++- .../tools/__tests__/useMcpToolTool.spec.ts | 16 +- .../tools/__tests__/writeToFileTool.spec.ts | 8 +- src/core/tools/accessMcpResourceTool.ts | 5 +- src/core/tools/applyDiffTool.ts | 5 +- src/core/tools/askFollowupQuestionTool.ts | 5 +- src/core/tools/attemptCompletionTool.ts | 26 +- src/core/tools/browserActionTool.ts | 5 +- src/core/tools/codebaseSearchTool.ts | 5 +- src/core/tools/executeCommandTool.ts | 163 +-- src/core/tools/fetchInstructionsTool.ts | 5 +- src/core/tools/insertContentTool.ts | 72 +- src/core/tools/listCodeDefinitionNamesTool.ts | 5 +- src/core/tools/listFilesTool.ts | 5 +- src/core/tools/multiApplyDiffTool.ts | 5 +- src/core/tools/newTaskTool.ts | 5 +- src/core/tools/readFileTool.ts | 5 +- src/core/tools/searchAndReplaceTool.ts | 12 +- src/core/tools/searchFilesTool.ts | 5 +- src/core/tools/switchModeTool.ts | 5 +- src/core/tools/useMcpToolTool.ts | 5 +- src/core/tools/writeToFileTool.ts | 16 +- src/core/webview/ClineProvider.ts | 107 +- .../webview/__tests__/ClineProvider.spec.ts | 1200 +---------------- src/core/webview/webviewMessageHandler.ts | 935 ++----------- src/i18n/locales/ca/common.json | 87 +- src/i18n/locales/de/common.json | 87 +- src/i18n/locales/en/common.json | 80 +- src/i18n/locales/es/common.json | 87 +- src/i18n/locales/fr/common.json | 87 +- src/i18n/locales/hi/common.json | 87 +- src/i18n/locales/id/common.json | 91 +- src/i18n/locales/it/common.json | 87 +- src/i18n/locales/ja/common.json | 87 +- src/i18n/locales/ko/common.json | 87 +- src/i18n/locales/nl/common.json | 91 +- src/i18n/locales/pl/common.json | 87 +- src/i18n/locales/pt-BR/common.json | 87 +- src/i18n/locales/ru/common.json | 87 +- src/i18n/locales/tr/common.json | 87 +- src/i18n/locales/vi/common.json | 94 +- src/i18n/locales/zh-CN/common.json | 87 +- src/i18n/locales/zh-TW/common.json | 87 +- .../__tests__/message-filter.spec.ts | 263 ---- .../claude-code/message-filter.ts | 35 - src/integrations/claude-code/types.ts | 34 - src/integrations/editor/DiffViewProvider.ts | 122 +- .../editor/__tests__/DiffViewProvider.spec.ts | 344 +---- src/package.json | 125 +- src/package.nls.ca.json | 8 +- src/package.nls.de.json | 8 +- src/package.nls.es.json | 8 +- src/package.nls.fr.json | 8 +- src/package.nls.hi.json | 8 +- src/package.nls.id.json | 8 +- src/package.nls.it.json | 8 +- src/package.nls.ja.json | 8 +- src/package.nls.json | 9 +- src/package.nls.ko.json | 8 +- src/package.nls.nl.json | 8 +- src/package.nls.pl.json | 8 +- src/package.nls.pt-BR.json | 8 +- src/package.nls.ru.json | 8 +- src/package.nls.tr.json | 8 +- src/package.nls.vi.json | 8 +- src/package.nls.zh-CN.json | 8 +- src/package.nls.zh-TW.json | 8 +- src/services/browser/BrowserSession.ts | 11 +- .../browser/__tests__/BrowserSession.spec.ts | 234 ---- .../__tests__/cache-manager.spec.ts | 30 +- src/services/code-index/cache-manager.ts | 22 +- .../__tests__/qdrant-client.spec.ts | 407 +----- .../code-index/vector-store/qdrant-client.ts | 102 +- src/services/marketplace/SimpleInstaller.ts | 125 +- src/services/mcp/__tests__/McpHub.spec.ts | 37 - src/services/mdm/MdmService.ts | 45 +- src/services/mdm/__tests__/MdmService.spec.ts | 115 +- .../roo-config/__tests__/index.spec.ts | 301 ----- src/services/roo-config/index.ts | 252 ---- src/shared/ExtensionMessage.ts | 49 - src/shared/WebviewMessage.ts | 58 +- src/shared/checkExistApiConfig.ts | 4 +- src/shared/modes.ts | 118 +- src/shared/tools.ts | 152 +-- src/utils/__tests__/git.spec.ts | 483 +------ src/utils/__tests__/safeWriteJson.test.ts | 480 ------- src/utils/git.ts | 199 +-- src/utils/migrateSettings.ts | 4 +- src/utils/safeWriteJson.ts | 235 ---- webview-ui/src/App.tsx | 132 +- webview-ui/src/__tests__/App.spec.tsx | 2 +- .../__tests__/ContextWindowProgress.spec.tsx | 29 +- .../src/components/account/AccountView.tsx | 86 +- .../account/__tests__/AccountView.spec.tsx | 92 -- .../src/components/chat/ChatTextArea.tsx | 745 +++++----- webview-ui/src/components/chat/ChatView.tsx | 609 +++------ .../components/chat/CodebaseSearchResult.tsx | 32 +- .../components/chat/ContextWindowProgress.tsx | 107 +- .../src/components/chat/FollowUpSuggest.tsx | 151 +-- webview-ui/src/components/chat/IconButton.tsx | 7 +- webview-ui/src/components/chat/Markdown.tsx | 48 +- .../src/components/chat/McpExecution.tsx | 2 - .../src/components/chat/ModeSelector.tsx | 180 --- .../src/components/chat/TaskActions.tsx | 79 +- webview-ui/src/components/chat/TaskHeader.tsx | 71 +- .../chat/__tests__/Announcement.spec.tsx | 2 +- .../__tests__/BatchFilePermission.spec.tsx | 2 +- .../chat/__tests__/ChatTextArea.spec.tsx | 4 +- .../__tests__/ChatView.auto-approve.spec.tsx | 2 +- .../chat/__tests__/ChatView.spec.tsx | 2 +- .../__tests__/IndexingStatusBadge.spec.tsx | 2 +- .../chat/__tests__/ModeSelector.spec.tsx | 58 - .../chat/__tests__/ShareButton.spec.tsx | 325 ----- .../chat/__tests__/TaskHeader.spec.tsx | 24 +- .../chat/checkpoints/CheckpointMenu.tsx | 26 +- .../src/components/common/CodeBlock.tsx | 168 +-- .../src/components/common/IconButton.tsx | 14 +- .../common/MermaidActionButtons.tsx | 67 +- .../src/components/common/MermaidButton.tsx | 36 +- .../src/components/common/TelemetryBanner.tsx | 9 +- .../src/components/common/ZoomControls.tsx | 35 +- .../common/__tests__/CodeBlock.spec.tsx | 16 +- .../common/__tests__/MarkdownBlock.spec.tsx | 2 +- .../src/components/history/CopyButton.tsx | 21 +- .../src/components/history/DeleteButton.tsx | 21 +- .../src/components/history/ExportButton.tsx | 21 +- .../src/components/history/HistoryView.tsx | 33 +- .../__tests__/BatchDeleteTaskDialog.spec.tsx | 2 +- .../history/__tests__/CopyButton.spec.tsx | 2 +- .../history/__tests__/DeleteButton.spec.tsx | 2 +- .../__tests__/DeleteTaskDialog.spec.tsx | 2 +- .../history/__tests__/ExportButton.spec.tsx | 2 +- .../history/__tests__/HistoryPreview.spec.tsx | 2 +- .../history/__tests__/HistoryView.spec.tsx | 2 +- .../history/__tests__/TaskItem.spec.tsx | 2 +- .../history/__tests__/TaskItemFooter.spec.tsx | 2 +- .../history/__tests__/TaskItemHeader.spec.tsx | 2 +- .../history/__tests__/useTaskSearch.spec.tsx | 2 +- .../marketplace/MarketplaceView.tsx | 11 +- .../MarketplaceViewStateManager.ts | 12 - .../__tests__/MarketplaceListView.spec.tsx | 4 +- .../__tests__/MarketplaceView.spec.tsx | 2 +- .../components/MarketplaceInstallModal.tsx | 21 +- .../components/MarketplaceItemCard.tsx | 168 +-- ...placeInstallModal-optional-params.spec.tsx | 2 +- .../MarketplaceInstallModal.spec.tsx | 2 +- .../__tests__/MarketplaceItemCard.spec.tsx | 4 +- webview-ui/src/components/mcp/McpToolRow.tsx | 71 +- .../mcp/__tests__/McpToolRow.spec.tsx | 146 +- webview-ui/src/components/modes/ModesView.tsx | 647 ++------- .../modes/__tests__/ModesView.spec.tsx | 47 +- webview-ui/src/components/settings/About.tsx | 7 +- .../components/settings/ApiConfigManager.tsx | 256 +++- .../src/components/settings/ApiOptions.tsx | 7 - .../components/settings/AutoApproveToggle.tsx | 30 +- .../components/settings/PromptsSettings.tsx | 23 +- .../src/components/settings/SettingsView.tsx | 36 +- .../__tests__/ApiConfigManager.spec.tsx | 3 +- .../settings/__tests__/ApiOptions.spec.tsx | 3 +- .../__tests__/AutoApproveToggle.spec.tsx | 2 +- .../ContextManagementSettings.spec.tsx | 591 ++++---- .../settings/__tests__/ModelPicker.spec.tsx | 2 +- .../settings/__tests__/SettingsView.spec.tsx | 3 +- .../__tests__/TemperatureControl.spec.tsx | 16 +- .../__tests__/ThinkingBudget.spec.tsx | 2 +- .../src/components/settings/constants.ts | 3 - .../components/settings/providers/Bedrock.tsx | 13 +- .../settings/providers/LMStudio.tsx | 9 +- .../components/settings/providers/Ollama.tsx | 9 +- .../settings/providers/OpenAICompatible.tsx | 102 +- .../providers/__tests__/Bedrock.spec.tsx | 2 +- .../__tests__/OpenAICompatible.spec.tsx | 3 +- .../components/settings/providers/index.ts | 1 - .../ui/__tests__/select-dropdown.spec.tsx | 2 +- .../components/ui/__tests__/tooltip.spec.tsx | 182 --- .../hooks/__tests__/useSelectedModel.spec.ts | 72 - .../components/ui/hooks/useSelectedModel.ts | 8 - webview-ui/src/components/ui/index.ts | 3 - .../src/components/ui/select-dropdown.tsx | 84 +- .../src/components/ui/standard-tooltip.tsx | 69 - webview-ui/src/components/ui/tooltip.tsx | 5 +- .../welcome/__tests__/RooTips.spec.tsx | 2 +- .../src/context/ExtensionStateContext.tsx | 56 +- .../__tests__/ExtensionStateContext.spec.tsx | 3 +- .../__tests__/TranslationContext.spec.tsx | 2 +- webview-ui/src/i18n/locales/ca/account.json | 10 +- webview-ui/src/i18n/locales/ca/chat.json | 13 +- .../src/i18n/locales/ca/marketplace.json | 2 +- webview-ui/src/i18n/locales/ca/prompts.json | 9 - webview-ui/src/i18n/locales/ca/settings.json | 9 +- webview-ui/src/i18n/locales/de/account.json | 10 +- webview-ui/src/i18n/locales/de/chat.json | 13 +- .../src/i18n/locales/de/marketplace.json | 2 +- webview-ui/src/i18n/locales/de/prompts.json | 9 - webview-ui/src/i18n/locales/de/settings.json | 9 +- webview-ui/src/i18n/locales/en/account.json | 10 +- webview-ui/src/i18n/locales/en/chat.json | 13 +- .../src/i18n/locales/en/marketplace.json | 2 +- webview-ui/src/i18n/locales/en/prompts.json | 13 +- webview-ui/src/i18n/locales/en/settings.json | 11 +- webview-ui/src/i18n/locales/en/welcome.json | 2 +- webview-ui/src/i18n/locales/es/account.json | 10 +- webview-ui/src/i18n/locales/es/chat.json | 13 +- .../src/i18n/locales/es/marketplace.json | 2 +- webview-ui/src/i18n/locales/es/prompts.json | 9 - webview-ui/src/i18n/locales/es/settings.json | 9 +- webview-ui/src/i18n/locales/fr/account.json | 10 +- webview-ui/src/i18n/locales/fr/chat.json | 13 +- .../src/i18n/locales/fr/marketplace.json | 2 +- webview-ui/src/i18n/locales/fr/prompts.json | 9 - webview-ui/src/i18n/locales/fr/settings.json | 9 +- webview-ui/src/i18n/locales/hi/account.json | 10 +- webview-ui/src/i18n/locales/hi/chat.json | 13 +- .../src/i18n/locales/hi/marketplace.json | 2 +- webview-ui/src/i18n/locales/hi/prompts.json | 9 - webview-ui/src/i18n/locales/hi/settings.json | 9 +- webview-ui/src/i18n/locales/id/account.json | 10 +- webview-ui/src/i18n/locales/id/chat.json | 13 +- .../src/i18n/locales/id/marketplace.json | 2 +- webview-ui/src/i18n/locales/id/prompts.json | 9 - webview-ui/src/i18n/locales/id/settings.json | 15 +- webview-ui/src/i18n/locales/it/account.json | 10 +- webview-ui/src/i18n/locales/it/chat.json | 13 +- .../src/i18n/locales/it/marketplace.json | 2 +- webview-ui/src/i18n/locales/it/prompts.json | 9 - webview-ui/src/i18n/locales/it/settings.json | 9 +- webview-ui/src/i18n/locales/ja/account.json | 10 +- webview-ui/src/i18n/locales/ja/chat.json | 13 +- .../src/i18n/locales/ja/marketplace.json | 2 +- webview-ui/src/i18n/locales/ja/prompts.json | 9 - webview-ui/src/i18n/locales/ja/settings.json | 9 +- webview-ui/src/i18n/locales/ko/account.json | 10 +- webview-ui/src/i18n/locales/ko/chat.json | 13 +- .../src/i18n/locales/ko/marketplace.json | 2 +- webview-ui/src/i18n/locales/ko/prompts.json | 9 - webview-ui/src/i18n/locales/ko/settings.json | 9 +- webview-ui/src/i18n/locales/nl/account.json | 10 +- webview-ui/src/i18n/locales/nl/chat.json | 13 +- .../src/i18n/locales/nl/marketplace.json | 2 +- webview-ui/src/i18n/locales/nl/prompts.json | 9 - webview-ui/src/i18n/locales/nl/settings.json | 9 +- webview-ui/src/i18n/locales/pl/account.json | 10 +- webview-ui/src/i18n/locales/pl/chat.json | 13 +- .../src/i18n/locales/pl/marketplace.json | 2 +- webview-ui/src/i18n/locales/pl/prompts.json | 9 - webview-ui/src/i18n/locales/pl/settings.json | 9 +- .../src/i18n/locales/pt-BR/account.json | 10 +- webview-ui/src/i18n/locales/pt-BR/chat.json | 13 +- .../src/i18n/locales/pt-BR/marketplace.json | 2 +- .../src/i18n/locales/pt-BR/prompts.json | 9 - .../src/i18n/locales/pt-BR/settings.json | 9 +- webview-ui/src/i18n/locales/ru/account.json | 10 +- webview-ui/src/i18n/locales/ru/chat.json | 13 +- .../src/i18n/locales/ru/marketplace.json | 2 +- webview-ui/src/i18n/locales/ru/prompts.json | 9 - webview-ui/src/i18n/locales/ru/settings.json | 9 +- webview-ui/src/i18n/locales/tr/account.json | 10 +- webview-ui/src/i18n/locales/tr/chat.json | 13 +- .../src/i18n/locales/tr/marketplace.json | 2 +- webview-ui/src/i18n/locales/tr/prompts.json | 9 - webview-ui/src/i18n/locales/tr/settings.json | 9 +- webview-ui/src/i18n/locales/vi/account.json | 10 +- webview-ui/src/i18n/locales/vi/chat.json | 13 +- .../src/i18n/locales/vi/marketplace.json | 2 +- webview-ui/src/i18n/locales/vi/prompts.json | 9 - webview-ui/src/i18n/locales/vi/settings.json | 9 +- .../src/i18n/locales/zh-CN/account.json | 10 +- webview-ui/src/i18n/locales/zh-CN/chat.json | 13 +- .../src/i18n/locales/zh-CN/marketplace.json | 2 +- .../src/i18n/locales/zh-CN/prompts.json | 9 - .../src/i18n/locales/zh-CN/settings.json | 9 +- .../src/i18n/locales/zh-TW/account.json | 10 +- webview-ui/src/i18n/locales/zh-TW/chat.json | 13 +- .../src/i18n/locales/zh-TW/marketplace.json | 2 +- .../src/i18n/locales/zh-TW/prompts.json | 9 - .../src/i18n/locales/zh-TW/settings.json | 9 +- webview-ui/src/utils/context-mentions.ts | 12 +- webview-ui/src/utils/test-utils.tsx | 21 - webview-ui/vitest.setup.ts | 3 - 429 files changed, 6451 insertions(+), 18239 deletions(-) delete mode 100644 .roo/rules-code/use-safeWriteJson.md delete mode 100644 packages/types/src/sharing.ts mode change 100644 => 100755 scripts/update-contributors.js delete mode 100644 src/api/providers/__tests__/bedrock-error-handling.spec.ts delete mode 100644 src/api/providers/__tests__/claude-code-caching.spec.ts delete mode 100644 src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts delete mode 100644 src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts delete mode 100644 src/core/assistant-message/index.ts delete mode 100644 src/core/assistant-message/parseAssistantMessage.ts delete mode 100644 src/core/assistant-message/parseAssistantMessageV2.ts delete mode 100644 src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts create mode 100644 src/core/message-parsing/CodeBlockStateMachine.ts create mode 100644 src/core/message-parsing/DirectiveHandler.ts create mode 100644 src/core/message-parsing/DirectiveHandlerRegistry.ts create mode 100644 src/core/message-parsing/DirectiveRegistryFactory.ts create mode 100644 src/core/message-parsing/DirectiveStreamingParser.ts create mode 100644 src/core/message-parsing/FallbackParser.ts create mode 100644 src/core/message-parsing/ParameterCodeBlockHandler.ts create mode 100644 src/core/message-parsing/ParseContext.ts create mode 100644 src/core/message-parsing/XmlUtils.ts create mode 100644 src/core/message-parsing/__tests__/code-block-state-machine.spec.ts create mode 100644 src/core/message-parsing/directives/Directive.ts create mode 100644 src/core/message-parsing/directives/TextDirective.ts create mode 100644 src/core/message-parsing/directives/ToolDirective.ts create mode 100644 src/core/message-parsing/directives/index.ts create mode 100644 src/core/message-parsing/directives/tool-directives/AccessMcpResourceToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/AskFollowupQuestionToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/AttemptCompletionToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/BrowserActionToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/CodebaseSearchToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/ExecuteCommandToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/FetchInstructionsToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/InsertCodeBlockToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/ListCodeDefinitionNamesToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/ListFilesToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/NewTaskToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/ReadFileToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/SearchAndReplaceToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/SearchFilesToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/SwitchModeToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/ToolParamName.ts create mode 100644 src/core/message-parsing/directives/tool-directives/ToolResponse.ts create mode 100644 src/core/message-parsing/directives/tool-directives/UseMcpToolToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/WriteToFileToolDirective.ts create mode 100644 src/core/message-parsing/directives/tool-directives/index.ts create mode 100644 src/core/message-parsing/handlers/BaseDirectiveHandler.ts create mode 100644 src/core/message-parsing/handlers/TextDirectiveHandler.ts create mode 100644 src/core/message-parsing/handlers/ToolDirectiveHandler.ts create mode 100644 src/core/message-parsing/handlers/index.ts create mode 100644 src/core/message-parsing/index.ts rename src/core/{assistant-message => message-parsing}/presentAssistantMessage.ts (87%) delete mode 100644 src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts delete mode 100644 src/integrations/claude-code/__tests__/message-filter.spec.ts delete mode 100644 src/integrations/claude-code/message-filter.ts delete mode 100644 src/integrations/claude-code/types.ts delete mode 100644 src/services/browser/__tests__/BrowserSession.spec.ts delete mode 100644 src/services/roo-config/__tests__/index.spec.ts delete mode 100644 src/services/roo-config/index.ts delete mode 100644 src/utils/__tests__/safeWriteJson.test.ts delete mode 100644 src/utils/safeWriteJson.ts delete mode 100644 webview-ui/src/components/account/__tests__/AccountView.spec.tsx delete mode 100644 webview-ui/src/components/chat/ModeSelector.tsx delete mode 100644 webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx delete mode 100644 webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx delete mode 100644 webview-ui/src/components/ui/__tests__/tooltip.spec.tsx delete mode 100644 webview-ui/src/components/ui/standard-tooltip.tsx delete mode 100644 webview-ui/src/utils/test-utils.tsx diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e83e44cd66..632d9a3ecc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -38,6 +38,19 @@ Detail the steps to test your changes. This helps reviewers verify your work. - Include relevant testing environment details if applicable. --> +### Type of Change + + + +- [ ] 🐛 **Bug Fix**: Non-breaking change that fixes an issue. +- [ ] ✨ **New Feature**: Non-breaking change that adds functionality. +- [ ] 💥 **Breaking Change**: Fix or feature that would cause existing functionality to not work as expected. +- [ ] ♻️ **Refactor**: Code change that neither fixes a bug nor adds a feature. +- [ ] 💅 **Style**: Changes that do not affect the meaning of the code (white-space, formatting, etc.). +- [ ] 📚 **Documentation**: Updates to documentation files. +- [ ] ⚙️ **Build/CI**: Changes to the build process or CI configuration. +- [ ] 🧹 **Chore**: Other changes that don't modify `src` or test files. + ### Pre-Submission Checklist @@ -45,8 +58,17 @@ Detail the steps to test your changes. This helps reviewers verify your work. - [ ] **Issue Linked**: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above). - [ ] **Scope**: My changes are focused on the linked issue (one major feature/fix per PR). - [ ] **Self-Review**: I have performed a thorough self-review of my code. -- [ ] **Testing**: New and/or updated tests have been added to cover my changes (if applicable). +- [ ] **Code Quality**: + - [ ] My code adheres to the project's style guidelines. + - [ ] There are no new linting errors or warnings (`npm run lint`). + - [ ] All debug code (e.g., `console.log`) has been removed. +- [ ] **Testing**: + - [ ] New and/or updated tests have been added to cover my changes. + - [ ] All tests pass locally (`npm test`). + - [ ] The application builds successfully with my changes. +- [ ] **Branch Hygiene**: My branch is up-to-date (rebased) with the `main` branch. - [ ] **Documentation Impact**: I have considered if my changes require documentation updates (see "Documentation Updates" section below). +- [ ] **Changeset**: A changeset has been created using `npm run changeset` if this PR includes user-facing changes or dependency updates. - [ ] **Contribution Guidelines**: I have read and agree to the [Contributor Guidelines](/CONTRIBUTING.md). ### Screenshots / Videos diff --git a/.roo/rules-code/use-safeWriteJson.md b/.roo/rules-code/use-safeWriteJson.md deleted file mode 100644 index 21e42553da..0000000000 --- a/.roo/rules-code/use-safeWriteJson.md +++ /dev/null @@ -1,6 +0,0 @@ -# JSON File Writing Must Be Atomic - -- You MUST use `safeWriteJson(filePath: string, data: any): Promise` from `src/utils/safeWriteJson.ts` instead of `JSON.stringify` with file-write operations -- `safeWriteJson` will create parent directories if necessary, so do not call `mkdir` prior to `safeWriteJson` -- `safeWriteJson` prevents data corruption via atomic writes with locking and streams the write to minimize memory footprint -- Test files are exempt from this rule diff --git a/.roo/rules-translate/001-general-rules.md b/.roo/rules-translate/001-general-rules.md index e27b9793e2..2b747f77b9 100644 --- a/.roo/rules-translate/001-general-rules.md +++ b/.roo/rules-translate/001-general-rules.md @@ -64,10 +64,8 @@ 1. Identify where the string appears in the UI/codebase 2. Understand the context and purpose of the string 3. Update English translation first - 4. Use the `` tool to find JSON keys that are near new keys in English translations but do not yet exist in the other language files for `` SEARCH context - 5. Create appropriate translations for all other supported languages utilizing the `search_files` result using `` without reading every file. - 6. Do not output the translated text into the chat, just modify the files. - 7. Validate your changes with the missing translations script + 4. Create appropriate translations for all other supported languages + 5. Validate your changes with the missing translations script - Flag or comment if an English source string is incomplete ("please see this...") to avoid truncated or unclear translations - For UI elements, distinguish between: - Button labels: Use short imperative commands ("Save", "Cancel") diff --git a/.roomodes b/.roomodes index e9cb7d8a94..87249f4768 100644 --- a/.roomodes +++ b/.roomodes @@ -25,8 +25,6 @@ customModes: - fileRegex: (\.roomodes$|\.roo/.*\.xml$|\.yaml$) description: Mode configuration files and XML instructions - command - - mcp - source: project - slug: test name: 🧪 Test roleDefinition: |- @@ -205,55 +203,3 @@ customModes: description: Temporary documentation extraction files only - command - mcp - - slug: pr-fixer - name: 🛠️ PR Fixer - roleDefinition: "You are Roo, a pull request resolution specialist. Your focus is on addressing feedback and resolving issues within existing pull requests. Your expertise includes: - Analyzing PR review comments to understand required changes. - Checking CI/CD workflow statuses to identify failing tests. - Fetching and analyzing test logs to diagnose failures. - Identifying and resolving merge conflicts. - Guiding the user through the resolution process." - whenToUse: Use this mode to fix pull requests. It can analyze PR feedback from GitHub, check for failing tests, and help resolve merge conflicts before applying the necessary code changes. - description: Fix pull requests. - groups: - - read - - edit - - command - - mcp - - slug: issue-investigator - name: 🕵️ Issue Investigator - roleDefinition: You are Roo, a GitHub issue investigator. Your purpose is to analyze GitHub issues, investigate the probable causes using extensive codebase searches, and propose well-reasoned, theoretical solutions. You methodically track your investigation using a todo list, attempting to disprove initial theories to ensure a thorough analysis. Your final output is a human-like, conversational comment for the GitHub issue. - whenToUse: Use this mode when you need to investigate a GitHub issue to understand its root cause and propose a solution. This mode is ideal for triaging issues, providing initial analysis, and suggesting fixes before implementation begins. It uses the `gh` CLI for issue interaction. - description: Investigates GitHub issues - groups: - - read - - command - - mcp - source: project - - slug: merge-resolver - name: 🔀 Merge Resolver - roleDefinition: |- - You are Roo, a merge conflict resolution specialist with expertise in: - - Analyzing pull request merge conflicts using git blame and commit history - - Understanding code intent through commit messages and diffs - - Making intelligent decisions about which changes to keep, merge, or discard - - Using git commands and GitHub CLI to gather context - - Resolving conflicts based on commit metadata and code semantics - - Prioritizing changes based on intent (bugfix vs feature vs refactor) - - Combining non-conflicting changes when appropriate - - You receive a PR number (e.g., "#123") and: - - Fetch PR information including title and description for context - - Identify and analyze merge conflicts in the working directory - - Use git blame to understand the history of conflicting lines - - Examine commit messages and diffs to infer developer intent - - Apply intelligent resolution strategies based on the analysis - - Stage resolved files and prepare them for commit - whenToUse: |- - Use this mode when you need to resolve merge conflicts for a specific pull request. - This mode is triggered by providing a PR number (e.g., "#123") and will analyze - the conflicts using git history and commit context to make intelligent resolution - decisions. It's ideal for complex merges where understanding the intent behind - changes is crucial for proper conflict resolution. - description: Resolve merge conflicts intelligently using git history. - groups: - - read - - edit - - command - - mcp - source: project diff --git a/CHANGELOG.md b/CHANGELOG.md index e34a65dbee..66bb7c37e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -277,6 +277,7 @@ - Fix context length for lmstudio and ollama (thanks @thecolorblue!) - Resolve MCP tool eye icon state and hide in chat context (thanks @daniel-lxs!) +======= ## [3.21.2] - 2025-06-20 - Add LaTeX math equation rendering in chat window diff --git a/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts b/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts index 8e83dd7e4b..537ef54b82 100644 --- a/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts +++ b/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts @@ -767,7 +767,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { } }) - test.skip("Should validate MCP request message format and complete successfully", async function () { + test("Should validate MCP request message format and complete successfully", async function () { const api = globalThis.api const messages: ClineMessage[] = [] let _taskCompleted = false diff --git a/locales/ca/README.md b/locales/ca/README.md index c75275881c..3d41ea5c45 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -180,44 +180,41 @@ Ens encanten les contribucions de la comunitat! Comenceu llegint el nostre [CONT Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Llicència diff --git a/locales/de/README.md b/locales/de/README.md index e9357daa47..4492fdf9aa 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -180,44 +180,41 @@ Wir lieben Community-Beiträge! Beginnen Sie mit dem Lesen unserer [CONTRIBUTING Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code zu verbessern! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Lizenz diff --git a/locales/es/README.md b/locales/es/README.md index 09eb159479..a1b95bfe83 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -180,44 +180,41 @@ Usamos [changesets](https://github.com/changesets/changesets) para versionar y p ¡Gracias a todos nuestros colaboradores que han ayudado a mejorar Roo Code! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Licencia diff --git a/locales/fr/README.md b/locales/fr/README.md index 0ca48da21e..36ee1a55d3 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -180,44 +180,41 @@ Nous adorons les contributions de la communauté ! Commencez par lire notre [CON Merci à tous nos contributeurs qui ont aidé à améliorer Roo Code ! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Licence diff --git a/locales/hi/README.md b/locales/hi/README.md index aa3da73d0f..f1cc876efa 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -180,44 +180,41 @@ code --install-extension bin/roo-cline-.vsix Roo Code को बेहतर बनाने में मदद करने वाले हमारे सभी योगदानकर्ताओं को धन्यवाद! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## लाइसेंस diff --git a/locales/id/README.md b/locales/id/README.md index 2aa0d6b423..6320095c5c 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -174,44 +174,41 @@ Kami menyukai kontribusi komunitas! Mulai dengan membaca [CONTRIBUTING.md](CONTR Terima kasih kepada semua kontributor kami yang telah membantu membuat Roo Code lebih baik! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## License diff --git a/locales/it/README.md b/locales/it/README.md index c357f4330d..012551b222 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -180,44 +180,41 @@ Amiamo i contributi della community! Inizia leggendo il nostro [CONTRIBUTING.md] Grazie a tutti i nostri contributori che hanno aiutato a migliorare Roo Code! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Licenza diff --git a/locales/ja/README.md b/locales/ja/README.md index 2c8b5ecf27..87ce61d3a4 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -180,44 +180,41 @@ code --install-extension bin/roo-cline-.vsix Roo Codeの改善に貢献してくれたすべての貢献者に感謝します! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## ライセンス diff --git a/locales/ko/README.md b/locales/ko/README.md index 3b41d7927d..047419f3a5 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -180,44 +180,41 @@ code --install-extension bin/roo-cline-.vsix Roo Code를 더 좋게 만드는 데 도움을 준 모든 기여자에게 감사드립니다! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## 라이선스 diff --git a/locales/nl/README.md b/locales/nl/README.md index 77372be7ff..064cb29dbb 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -180,44 +180,41 @@ We houden van bijdragen uit de community! Begin met het lezen van onze [CONTRIBU Dank aan alle bijdragers die Roo Code beter hebben gemaakt! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Licentie diff --git a/locales/pl/README.md b/locales/pl/README.md index 5a44275d6e..9617281fcc 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -180,44 +180,41 @@ Kochamy wkład społeczności! Zacznij od przeczytania naszego [CONTRIBUTING.md] Dziękujemy wszystkim naszym współtwórcom, którzy pomogli ulepszyć Roo Code! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Licencja diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 8412b94ffc..4dc6bac10e 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -180,44 +180,41 @@ Adoramos contribuições da comunidade! Comece lendo nosso [CONTRIBUTING.md](CON Obrigado a todos os nossos contribuidores que ajudaram a tornar o Roo Code melhor! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Licença diff --git a/locales/ru/README.md b/locales/ru/README.md index e3161a73d8..c81a8c9f2a 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -180,44 +180,41 @@ code --install-extension bin/roo-cline-.vsix Спасибо всем нашим участникам, которые помогли сделать Roo Code лучше! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Лицензия diff --git a/locales/tr/README.md b/locales/tr/README.md index 7e65ebbafd..afd284f568 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -180,44 +180,41 @@ Topluluk katkılarını seviyoruz! [CONTRIBUTING.md](CONTRIBUTING.md) dosyasın Roo Code'u daha iyi hale getirmeye yardımcı olan tüm katkıda bulunanlara teşekkür ederiz! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Lisans diff --git a/locales/vi/README.md b/locales/vi/README.md index 5483d8c2cf..e13ac7512a 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -180,44 +180,41 @@ Chúng tôi rất hoan nghênh đóng góp từ cộng đồng! Bắt đầu b Cảm ơn tất cả những người đóng góp đã giúp cải thiện Roo Code! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## Giấy Phép diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index a3af91c935..f958207502 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -180,44 +180,41 @@ code --install-extension bin/roo-cline-.vsix 感谢所有帮助改进 Roo Code 的贡献者! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## 许可证 diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 16f0f7c936..18e7b31739 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -181,44 +181,41 @@ code --install-extension bin/roo-cline-.vsix 感謝所有幫助改進 Roo Code 的貢獻者! -|mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| -|:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| -|joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| -|lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| -|dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| -|Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| -|upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| -|avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| -|vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| -|catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| -|julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | + +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| samhvw8
samhvw8
| daniel-lxs
daniel-lxs
| hannesrudolph
hannesrudolph
| +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| joemanley201
joemanley201
| +| System233
System233
| jquanton
jquanton
| nissa-seru
nissa-seru
| NyxJae
NyxJae
| jr
jr
| MuriloFP
MuriloFP
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| +| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| xyOz-dev
xyOz-dev
| +| dtrugman
dtrugman
| vigneshsubbiah16
vigneshsubbiah16
| lloydchang
lloydchang
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| Szpadel
Szpadel
| +| chrarnoldus
chrarnoldus
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| lupuletic
lupuletic
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| kiwina
kiwina
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| +| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| +| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dleffel
dleffel
| +| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| SannidhyaSah
SannidhyaSah
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| +| taisukeoe
taisukeoe
| avtc
avtc
| dlab-anton
dlab-anton
| eonghk
eonghk
| kcwhite
kcwhite
| ronyblum
ronyblum
| +| teddyOOXX
teddyOOXX
| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| +| yt3trees
yt3trees
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| bramburn
bramburn
| olearycrew
olearycrew
| +| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| ross
ross
| philfung
philfung
| napter
napter
| mdp
mdp
| +| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| SplittyDev
SplittyDev
| kohii
kohii
| kinandan
kinandan
| +| jwcraig
jwcraig
| shoopapa
shoopapa
| im47cn
im47cn
| hongzio
hongzio
| hatsu38
hatsu38
| GOODBOY008
GOODBOY008
| +| forestyoo
forestyoo
| dqroid
dqroid
| dairui1
dairui1
| bannzai
bannzai
| tgfjt
tgfjt
| axmo
axmo
| +| asychin
asychin
| amittell
amittell
| tmsjngx0
tmsjngx0
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| +| vladstudio
vladstudio
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| samir-nimbly
samir-nimbly
| roomote
roomote
| +| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| oprstchn
oprstchn
| +| nobu007
nobu007
| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| +| lightrabbit
lightrabbit
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| chadgauth
chadgauth
| brunobergher
brunobergher
| bogdan0083
bogdan0083
| +| Atlogit
Atlogit
| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| +| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| +| R-omk
R-omk
| Sarke
Sarke
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| KanTakahiro
KanTakahiro
| ksze
ksze
| +| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| | + ## 授權 diff --git a/packages/build/src/__tests__/index.test.ts b/packages/build/src/__tests__/index.test.ts index eda70fac1c..0b38287cb0 100644 --- a/packages/build/src/__tests__/index.test.ts +++ b/packages/build/src/__tests__/index.test.ts @@ -70,7 +70,7 @@ describe("generatePackageJson", () => { { command: "roo-cline.accountButtonClicked", group: "navigation@6", - when: "activeWebviewPanelId == roo-cline.TabPanelProvider", + when: "activeWebviewPanelId == roo-cline.TabPanelProvider && config.roo-cline.rooCodeCloudEnabled", }, ], }, @@ -183,7 +183,7 @@ describe("generatePackageJson", () => { { command: "roo-code-nightly.accountButtonClicked", group: "navigation@6", - when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider", + when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider && config.roo-code-nightly.rooCodeCloudEnabled", }, ], }, diff --git a/packages/cloud/package.json b/packages/cloud/package.json index d67b5ae7eb..ac8dd6d05f 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -13,6 +13,7 @@ "dependencies": { "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", + "axios": "^1.7.4", "zod": "^3.25.61" }, "devDependencies": { diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 32ea443cd6..886f8f8c3f 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -1,12 +1,6 @@ import * as vscode from "vscode" -import type { - CloudUserInfo, - TelemetryEvent, - OrganizationAllowList, - ClineMessage, - ShareVisibility, -} from "@roo-code/types" +import type { CloudUserInfo, TelemetryEvent, OrganizationAllowList } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudServiceCallbacks } from "./types" @@ -16,7 +10,7 @@ import type { SettingsService } from "./SettingsService" import { CloudSettingsService } from "./CloudSettingsService" import { StaticSettingsService } from "./StaticSettingsService" import { TelemetryClient } from "./TelemetryClient" -import { ShareService, TaskNotFoundError } from "./ShareService" +import { ShareService } from "./ShareService" export class CloudService { private static _instance: CloudService | null = null @@ -61,20 +55,10 @@ export class CloudService { this.authService.on("logged-out", this.authListener) this.authService.on("user-info", this.authListener) - // Check for static settings environment variable - const staticOrgSettings = process.env.ROO_CODE_CLOUD_ORG_SETTINGS - if (staticOrgSettings && staticOrgSettings.length > 0) { - this.settingsService = new StaticSettingsService(staticOrgSettings, this.log) - } else { - const cloudSettingsService = new CloudSettingsService( - this.context, - this.authService, - () => this.callbacks.stateChanged?.(), - this.log, - ) - cloudSettingsService.initialize() - this.settingsService = cloudSettingsService - } + this.settingsService = new SettingsService(this.context, this.authService, () => + this.callbacks.stateChanged?.(), + ) + this.settingsService.initialize() this.telemetryClient = new TelemetryClient(this.authService, this.settingsService) @@ -143,28 +127,14 @@ export class CloudService { return userInfo?.organizationRole || null } - public hasStoredOrganizationId(): boolean { - this.ensureInitialized() - return this.authService!.getStoredOrganizationId() !== null - } - - public getStoredOrganizationId(): string | null { - this.ensureInitialized() - return this.authService!.getStoredOrganizationId() - } - public getAuthState(): string { this.ensureInitialized() return this.authService!.getState() } - public async handleAuthCallback( - code: string | null, - state: string | null, - organizationId?: string | null, - ): Promise { + public async handleAuthCallback(code: string | null, state: string | null): Promise { this.ensureInitialized() - return this.authService!.handleCallback(code, state, organizationId) + return this.authService!.handleCallback(code, state) } // SettingsService @@ -183,23 +153,9 @@ export class CloudService { // ShareService - public async shareTask( - taskId: string, - visibility: ShareVisibility = "organization", - clineMessages?: ClineMessage[], - ) { + public async shareTask(taskId: string, visibility: "organization" | "public" = "organization") { this.ensureInitialized() - - try { - return await this.shareService!.shareTask(taskId, visibility) - } catch (error) { - if (error instanceof TaskNotFoundError && clineMessages) { - // Backfill messages and retry - await this.telemetryClient!.backfillMessages(clineMessages, taskId) - return await this.shareService!.shareTask(taskId, visibility) - } - throw error - } + return this.shareService!.shareTask(taskId, visibility) } public async canShareTask(): Promise { diff --git a/packages/cloud/src/SettingsService.ts b/packages/cloud/src/SettingsService.ts index c1027dc25c..e9bf5e83d6 100644 --- a/packages/cloud/src/SettingsService.ts +++ b/packages/cloud/src/SettingsService.ts @@ -1,23 +1,121 @@ import type { OrganizationAllowList, OrganizationSettings } from "@roo-code/types" +import * as vscode from "vscode" +import { AuthService } from "./auth" +import { RefreshTimer } from "./RefreshTimer" +import { getRooCodeApiUrl } from "./utils" +import { organizationSettingsSchema } from "@roo-code/types/src/schemas/organization-settings" +import { ORGANIZATION_ALLOW_ALL } from "@roo-code/types/src/constants" -/** - * Interface for settings services that provide organization settings - */ -export interface SettingsService { - /** - * Get the organization allow list - * @returns The organization allow list or default if none available - */ - getAllowList(): OrganizationAllowList +const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings" - /** - * Get the current organization settings - * @returns The organization settings or undefined if none available - */ - getSettings(): OrganizationSettings | undefined +export class SettingsService { + private context: vscode.ExtensionContext + private authService: AuthService + private settings: OrganizationSettings | undefined = undefined + private timer: RefreshTimer - /** - * Dispose of the settings service and clean up resources - */ - dispose(): void + constructor(context: vscode.ExtensionContext, authService: AuthService, callback: () => void) { + this.context = context + this.authService = authService + + this.timer = new RefreshTimer({ + callback: async () => { + await this.fetchSettings(callback) + return true + }, + successInterval: 30000, + initialBackoffMs: 1000, + maxBackoffMs: 30000, + }) + } + + public initialize(): void { + this.loadCachedSettings() + + // Clear cached settings if we have missed a log out. + if (this.authService.getState() == "logged-out" && this.settings) { + this.removeSettings() + } + + this.authService.on("attempting-session", () => { + this.timer.start() + }) + + this.authService.on("active-session", () => { + this.timer.start() + }) + + this.authService.on("logged-out", () => { + this.timer.stop() + this.removeSettings() + }) + + if (this.authService.hasOrIsAcquiringActiveSession()) { + this.timer.start() + } + } + + private async fetchSettings(callback: () => void): Promise { + const token = this.authService.getSessionToken() + + if (!token) { + return + } + + try { + const response = await fetch(`${getRooCodeApiUrl()}/api/organization-settings`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + if (!response.ok) { + console.error(`Failed to fetch organization settings: ${response.status} ${response.statusText}`) + return + } + + const data = await response.json() + const result = organizationSettingsSchema.safeParse(data) + + if (!result.success) { + console.error("Invalid organization settings format:", result.error) + return + } + + const newSettings = result.data + + if (!this.settings || this.settings.version !== newSettings.version) { + this.settings = newSettings + await this.cacheSettings() + callback() + } + } catch (error) { + console.error("Error fetching organization settings:", error) + } + } + + private async cacheSettings(): Promise { + await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings) + } + + private loadCachedSettings(): void { + this.settings = this.context.globalState.get(ORGANIZATION_SETTINGS_CACHE_KEY) + } + + public getAllowList(): OrganizationAllowList { + return this.settings?.allowList || ORGANIZATION_ALLOW_ALL + } + + public getSettings(): OrganizationSettings | undefined { + return this.settings + } + + public async removeSettings(): Promise { + this.settings = undefined + await this.cacheSettings() + } + + public dispose(): void { + this.timer.stop() + } } diff --git a/packages/cloud/src/ShareService.ts b/packages/cloud/src/ShareService.ts index 5dcc7cae3f..e4dbd5d2af 100644 --- a/packages/cloud/src/ShareService.ts +++ b/packages/cloud/src/ShareService.ts @@ -1,3 +1,4 @@ +import axios from "axios" import * as vscode from "vscode" import { shareResponseSchema } from "@roo-code/types" @@ -8,13 +9,6 @@ import { getUserAgent } from "./utils" export type ShareVisibility = "organization" | "public" -export class TaskNotFoundError extends Error { - constructor(taskId?: string) { - super(taskId ? `Task '${taskId}' not found` : "Task not found") - Object.setPrototypeOf(this, TaskNotFoundError.prototype) - } -} - export class ShareService { private authService: AuthService private settingsService: SettingsService @@ -37,25 +31,19 @@ export class ShareService { throw new Error("Authentication required") } - const response = await fetch(`${getRooCodeApiUrl()}/api/extension/share`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${sessionToken}`, - "User-Agent": getUserAgent(), + const response = await axios.post( + `${getRooCodeApiUrl()}/api/extension/share`, + { taskId, visibility }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${sessionToken}`, + "User-Agent": getUserAgent(), + }, }, - body: JSON.stringify({ taskId, visibility }), - signal: AbortSignal.timeout(10000), - }) + ) - if (!response.ok) { - if (response.status === 404) { - throw new TaskNotFoundError(taskId) - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const data = shareResponseSchema.parse(await response.json()) + const data = shareResponseSchema.parse(response.data) this.log("[share] Share link created successfully:", data) if (data.success && data.shareUrl) { diff --git a/packages/cloud/src/TelemetryClient.ts b/packages/cloud/src/TelemetryClient.ts index e33843a30c..eda2754f2c 100644 --- a/packages/cloud/src/TelemetryClient.ts +++ b/packages/cloud/src/TelemetryClient.ts @@ -1,9 +1,4 @@ -import { - TelemetryEventName, - type TelemetryEvent, - rooCodeTelemetryEventSchema, - type ClineMessage, -} from "@roo-code/types" +import { TelemetryEventName, type TelemetryEvent, rooCodeTelemetryEventSchema } from "@roo-code/types" import { BaseTelemetryClient } from "@roo-code/telemetry" import { getRooCodeApiUrl } from "./Config" @@ -84,66 +79,6 @@ export class TelemetryClient extends BaseTelemetryClient { } } - public async backfillMessages(messages: ClineMessage[], taskId: string): Promise { - if (!this.authService.isAuthenticated()) { - if (this.debug) { - console.info(`[TelemetryClient#backfillMessages] Skipping: Not authenticated`) - } - return - } - - const token = this.authService.getSessionToken() - - if (!token) { - console.error(`[TelemetryClient#backfillMessages] Unauthorized: No session token available.`) - return - } - - try { - const mergedProperties = await this.getEventProperties({ - event: TelemetryEventName.TASK_MESSAGE, - properties: { taskId }, - }) - - const formData = new FormData() - formData.append("taskId", taskId) - formData.append("properties", JSON.stringify(mergedProperties)) - - formData.append( - "file", - new File([JSON.stringify(messages)], "task.json", { - type: "application/json", - }), - ) - - if (this.debug) { - console.info( - `[TelemetryClient#backfillMessages] Uploading ${messages.length} messages for task ${taskId}`, - ) - } - - // Custom fetch for multipart - don't set Content-Type header (let browser set it) - const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - // Note: No Content-Type header - browser will set multipart/form-data with boundary - }, - body: formData, - }) - - if (!response.ok) { - console.error( - `[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`, - ) - } else if (this.debug) { - console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`) - } - } catch (error) { - console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`) - } - } - public override updateTelemetryState(_didUserOptIn: boolean) {} public override isTelemetryEnabled(): boolean { diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts index 1384b6de6b..586beb16d2 100644 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ b/packages/cloud/src/__tests__/CloudService.test.ts @@ -1,13 +1,10 @@ // npx vitest run src/__tests__/CloudService.test.ts import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" import { CloudService } from "../CloudService" -import { WebAuthService } from "../auth/WebAuthService" -import { CloudSettingsService } from "../CloudSettingsService" -import { ShareService, TaskNotFoundError } from "../ShareService" -import { TelemetryClient } from "../TelemetryClient" +import { AuthService } from "../AuthService" +import { SettingsService } from "../SettingsService" import { TelemetryService } from "@roo-code/telemetry" import { CloudServiceCallbacks } from "../types" @@ -31,10 +28,6 @@ vi.mock("../auth/WebAuthService") vi.mock("../CloudSettingsService") -vi.mock("../ShareService") - -vi.mock("../TelemetryClient") - describe("CloudService", () => { let mockContext: vscode.ExtensionContext let mockAuthService: { @@ -43,12 +36,10 @@ describe("CloudService", () => { logout: ReturnType isAuthenticated: ReturnType hasActiveSession: ReturnType - hasOrIsAcquiringActiveSession: ReturnType getUserInfo: ReturnType getState: ReturnType getSessionToken: ReturnType handleCallback: ReturnType - getStoredOrganizationId: ReturnType on: ReturnType off: ReturnType once: ReturnType @@ -60,13 +51,6 @@ describe("CloudService", () => { getAllowList: ReturnType dispose: ReturnType } - let mockShareService: { - shareTask: ReturnType - canShareTask: ReturnType - } - let mockTelemetryClient: { - backfillMessages: ReturnType - } let mockTelemetryService: { hasInstance: ReturnType instance: { @@ -78,29 +62,15 @@ describe("CloudService", () => { CloudService.resetInstance() mockContext = { - subscriptions: [], - workspaceState: { - get: vi.fn(), - update: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn(), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), }, globalState: { get: vi.fn(), update: vi.fn(), - setKeysForSync: vi.fn(), - keys: vi.fn().mockReturnValue([]), }, - extensionUri: { scheme: "file", path: "/mock/path" }, - extensionPath: "/mock/path", - extensionMode: 1, - asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`), - storageUri: { scheme: "file", path: "/mock/storage" }, extension: { packageJSON: { version: "1.0.0", @@ -114,12 +84,10 @@ describe("CloudService", () => { logout: vi.fn(), isAuthenticated: vi.fn().mockReturnValue(false), hasActiveSession: vi.fn().mockReturnValue(false), - hasOrIsAcquiringActiveSession: vi.fn().mockReturnValue(false), getUserInfo: vi.fn(), getState: vi.fn().mockReturnValue("logged-out"), getSessionToken: vi.fn(), handleCallback: vi.fn(), - getStoredOrganizationId: vi.fn().mockReturnValue(null), on: vi.fn(), off: vi.fn(), once: vi.fn(), @@ -133,15 +101,6 @@ describe("CloudService", () => { dispose: vi.fn(), } - mockShareService = { - shareTask: vi.fn(), - canShareTask: vi.fn().mockResolvedValue(true), - } - - mockTelemetryClient = { - backfillMessages: vi.fn().mockResolvedValue(undefined), - } - mockTelemetryService = { hasInstance: vi.fn().mockReturnValue(true), instance: { @@ -149,10 +108,8 @@ describe("CloudService", () => { }, } - vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService) - vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService) - vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService) - vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient) + vi.mocked(AuthService).mockImplementation(() => mockAuthService as unknown as AuthService) + vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService) vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) Object.defineProperty(TelemetryService, "instance", { @@ -175,13 +132,8 @@ describe("CloudService", () => { const cloudService = await CloudService.createInstance(mockContext, callbacks) expect(cloudService).toBeInstanceOf(CloudService) - expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function)) - expect(CloudSettingsService).toHaveBeenCalledWith( - mockContext, - mockAuthService, - expect.any(Function), - expect.any(Function), - ) + expect(AuthService).toHaveBeenCalledWith(mockContext, expect.any(Function)) + expect(SettingsService).toHaveBeenCalledWith(mockContext, mockAuthService, expect.any(Function)) }) it("should throw error if instance already exists", async () => { @@ -303,41 +255,7 @@ describe("CloudService", () => { it("should delegate handleAuthCallback to AuthService", async () => { await cloudService.handleAuthCallback("code", "state") - expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", undefined) - }) - - it("should delegate handleAuthCallback with organizationId to AuthService", async () => { - await cloudService.handleAuthCallback("code", "state", "org_123") - expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", "org_123") - }) - - it("should return stored organization ID from AuthService", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue("org_456") - - const result = cloudService.getStoredOrganizationId() - expect(mockAuthService.getStoredOrganizationId).toHaveBeenCalled() - expect(result).toBe("org_456") - }) - - it("should return null when no stored organization ID available", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue(null) - - const result = cloudService.getStoredOrganizationId() - expect(result).toBe(null) - }) - - it("should return true when stored organization ID exists", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue("org_789") - - const result = cloudService.hasStoredOrganizationId() - expect(result).toBe(true) - }) - - it("should return false when no stored organization ID exists", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue(null) - - const result = cloudService.hasStoredOrganizationId() - expect(result).toBe(false) + expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state") }) }) @@ -383,119 +301,4 @@ describe("CloudService", () => { expect(mockSettingsService.dispose).toHaveBeenCalled() }) }) - - describe("shareTask with ClineMessage retry logic", () => { - let cloudService: CloudService - - beforeEach(async () => { - // Reset mocks for shareTask tests - vi.clearAllMocks() - - // Reset authentication state for shareTask tests - mockAuthService.isAuthenticated.mockReturnValue(true) - mockAuthService.hasActiveSession.mockReturnValue(true) - mockAuthService.hasOrIsAcquiringActiveSession.mockReturnValue(true) - mockAuthService.getState.mockReturnValue("active") - - cloudService = await CloudService.createInstance(mockContext, {}) - }) - - it("should call shareTask without retry when successful", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - mockShareService.shareTask.mockResolvedValue(expectedResult) - - const result = await cloudService.shareTask(taskId, visibility, clineMessages) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - expect(result).toEqual(expectedResult) - }) - - it("should retry with backfill when TaskNotFoundError occurs", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - - // First call throws TaskNotFoundError, second call succeeds - mockShareService.shareTask - .mockRejectedValueOnce(new TaskNotFoundError(taskId)) - .mockResolvedValueOnce(expectedResult) - - const result = await cloudService.shareTask(taskId, visibility, clineMessages) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(2) - expect(mockShareService.shareTask).toHaveBeenNthCalledWith(1, taskId, visibility) - expect(mockShareService.shareTask).toHaveBeenNthCalledWith(2, taskId, visibility) - expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledWith(clineMessages, taskId) - expect(result).toEqual(expectedResult) - }) - - it("should not retry when TaskNotFoundError occurs but no clineMessages provided", async () => { - const taskId = "test-task-id" - const visibility = "organization" - - const taskNotFoundError = new TaskNotFoundError(taskId) - mockShareService.shareTask.mockRejectedValue(taskNotFoundError) - - await expect(cloudService.shareTask(taskId, visibility)).rejects.toThrow(TaskNotFoundError) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - }) - - it("should not retry when non-TaskNotFoundError occurs", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const genericError = new Error("Some other error") - mockShareService.shareTask.mockRejectedValue(genericError) - - await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(genericError) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - }) - - it("should work with default parameters", async () => { - const taskId = "test-task-id" - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - mockShareService.shareTask.mockResolvedValue(expectedResult) - - const result = await cloudService.shareTask(taskId) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, "organization") - expect(result).toEqual(expectedResult) - }) - }) }) diff --git a/packages/cloud/src/__tests__/ShareService.test.ts b/packages/cloud/src/__tests__/ShareService.test.ts index dd5b669603..55fc4e7c38 100644 --- a/packages/cloud/src/__tests__/ShareService.test.ts +++ b/packages/cloud/src/__tests__/ShareService.test.ts @@ -1,15 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { MockedFunction } from "vitest" +import axios from "axios" import * as vscode from "vscode" -import { ShareService, TaskNotFoundError } from "../ShareService" -import type { AuthService } from "../auth" +import { ShareService } from "../ShareService" +import type { AuthService } from "../AuthService" import type { SettingsService } from "../SettingsService" -// Mock fetch -const mockFetch = vi.fn() -global.fetch = mockFetch as any +// Mock axios +vi.mock("axios") +const mockedAxios = axios as any // Mock vscode vi.mock("vscode", () => ({ @@ -52,7 +53,6 @@ describe("ShareService", () => { beforeEach(() => { vi.clearAllMocks() - mockFetch.mockClear() mockLog = vi.fn() mockAuthService = { @@ -70,99 +70,86 @@ describe("ShareService", () => { describe("shareTask", () => { it("should share task with organization visibility and copy to clipboard", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", + const mockResponse = { + data: { + success: true, + shareUrl: "https://app.roocode.com/share/abc123", + }, } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) + mockedAxios.post.mockResolvedValue(mockResponse) const result = await shareService.shareTask("task-123", "organization") expect(result.success).toBe(true) expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123") - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", + expect(mockedAxios.post).toHaveBeenCalledWith( + "https://app.roocode.com/api/extension/share", + { taskId: "task-123", visibility: "organization" }, + { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer session-token", + "User-Agent": "Roo-Code 1.0.0", + }, }, - body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), - signal: expect.any(AbortSignal), - }) + ) expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123") }) it("should share task with public visibility", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", + const mockResponse = { + data: { + success: true, + shareUrl: "https://app.roocode.com/share/abc123", + }, } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) + mockedAxios.post.mockResolvedValue(mockResponse) const result = await shareService.shareTask("task-123", "public") expect(result.success).toBe(true) - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "public" }), - signal: expect.any(AbortSignal), - }) + expect(mockedAxios.post).toHaveBeenCalledWith( + "https://app.roocode.com/api/extension/share", + { taskId: "task-123", visibility: "public" }, + expect.any(Object), + ) }) it("should default to organization visibility when not specified", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", + const mockResponse = { + data: { + success: true, + shareUrl: "https://app.roocode.com/share/abc123", + }, } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) + mockedAxios.post.mockResolvedValue(mockResponse) const result = await shareService.shareTask("task-123") expect(result.success).toBe(true) - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), - signal: expect.any(AbortSignal), - }) + expect(mockedAxios.post).toHaveBeenCalledWith( + "https://app.roocode.com/api/extension/share", + { taskId: "task-123", visibility: "organization" }, + expect.any(Object), + ) }) it("should handle API error response", async () => { - const mockResponseData = { - success: false, - error: "Task not found", + const mockResponse = { + data: { + success: false, + error: "Task not found", + }, } ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) + mockedAxios.post.mockResolvedValue(mockResponse) const result = await shareService.shareTask("task-123", "organization") @@ -178,56 +165,10 @@ describe("ShareService", () => { it("should handle unexpected errors", async () => { ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockRejectedValue(new Error("Network error")) + mockedAxios.post.mockRejectedValue(new Error("Network error")) await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error") }) - - it("should throw TaskNotFoundError for 404 responses", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - }) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(TaskNotFoundError) - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( - "Task 'task-123' not found", - ) - }) - - it("should throw generic Error for non-404 HTTP errors", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - }) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( - "HTTP 500: Internal Server Error", - ) - await expect(shareService.shareTask("task-123", "organization")).rejects.not.toThrow(TaskNotFoundError) - }) - - it("should create TaskNotFoundError with correct properties", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - }) - - try { - await shareService.shareTask("task-123", "organization") - expect.fail("Expected TaskNotFoundError to be thrown") - } catch (error) { - expect(error).toBeInstanceOf(TaskNotFoundError) - expect(error).toBeInstanceOf(Error) - expect((error as TaskNotFoundError).message).toBe("Task 'task-123' not found") - } - }) }) describe("canShareTask", () => { diff --git a/packages/cloud/src/__tests__/TelemetryClient.test.ts b/packages/cloud/src/__tests__/TelemetryClient.test.ts index e4c62b1e4e..85b0fbf5ef 100644 --- a/packages/cloud/src/__tests__/TelemetryClient.test.ts +++ b/packages/cloud/src/__tests__/TelemetryClient.test.ts @@ -424,315 +424,4 @@ describe("TelemetryClient", () => { await client.shutdown() }) }) - - describe("backfillMessages", () => { - it("should not send request when not authenticated", async () => { - mockAuthService.isAuthenticated.mockReturnValue(false) - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not send request when no session token available", async () => { - mockAuthService.getSessionToken.mockReturnValue(null) - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).not.toHaveBeenCalled() - expect(console.error).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Unauthorized: No session token available.", - ) - }) - - it("should send FormData request with correct structure when authenticated", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const providerProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - } - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties), - } - - client.setProvider(mockProvider) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message 1", - }, - { - ts: 2, - type: "ask" as const, - ask: "followup" as const, - text: "test question", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - - // Parse and compare properties as objects since JSON.stringify order can vary - const propertiesJson = formData.get("properties") as string - const parsedProperties = JSON.parse(propertiesJson) - expect(parsedProperties).toEqual({ - taskId: "test-task-id", - ...providerProperties, - }) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should handle provider errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")), - } - - client.setProvider(mockProvider) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - should still work with just taskId - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - expect(formData.get("properties")).toBe( - JSON.stringify({ - taskId: "test-task-id", - }), - ) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should work without provider set", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - should work with just taskId - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - expect(formData.get("properties")).toBe( - JSON.stringify({ - taskId: "test-task-id", - }), - ) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should handle fetch errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockRejectedValue(new Error("Network error")) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow() - - expect(console.error).toHaveBeenCalledWith( - expect.stringContaining( - "[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error", - ), - ) - }) - - it("should handle HTTP error responses", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - }) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(console.error).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found", - ) - }) - - it("should log debug information when debug is enabled", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService, true) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(console.info).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id", - ) - expect(console.info).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id", - ) - }) - - it("should handle empty messages array", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.backfillMessages([], "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the empty messages array - const fileContent = await fileField.text() - expect(fileContent).toBe("[]") - }) - }) }) diff --git a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts index 0e6681c20b..91cf6eeb73 100644 --- a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts +++ b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts @@ -329,7 +329,7 @@ describe("WebAuthService", () => { expect(mockContext.secrets.store).toHaveBeenCalledWith( "clerk-auth-credentials", - JSON.stringify({ clientToken: "Bearer token-123", sessionId: "session-123", organizationId: null }), + JSON.stringify({ clientToken: "Bearer token-123", sessionId: "session-123" }), ) expect(mockShowInfo).toHaveBeenCalledWith("Successfully authenticated with Roo Code Cloud") }) @@ -634,55 +634,9 @@ describe("WebAuthService", () => { expect(authService.getUserInfo()).toBeNull() }) - it("should parse user info correctly for personal accounts", async () => { - // Set up with credentials for personal account (no organizationId) - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock successful responses - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Jane", - last_name: "Smith", - image_url: "https://example.com/jane.jpg", - primary_email_address_id: "email-2", - email_addresses: [ - { id: "email-1", email_address: "jane.old@example.com" }, - { id: "email-2", email_address: "jane@example.com" }, - ], - }, - }), - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - const userInfo = authService.getUserInfo() - expect(userInfo).toEqual({ - name: "Jane Smith", - email: "jane@example.com", - picture: "https://example.com/jane.jpg", - }) - }) - - it("should parse user info correctly for organization accounts", async () => { - // Set up with credentials for organization account - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: "org_1" } + it("should parse user info correctly", async () => { + // Set up with credentials + const credentials = { clientToken: "test-token", sessionId: "test-session" } mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) await authService.initialize() @@ -746,8 +700,8 @@ describe("WebAuthService", () => { }) it("should handle missing user info fields", async () => { - // Set up with credentials for personal account (no organizationId) - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } + // Set up with credentials + const credentials = { clientToken: "test-token", sessionId: "test-session" } mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) await authService.initialize() diff --git a/packages/cloud/src/auth/WebAuthService.ts b/packages/cloud/src/auth/WebAuthService.ts index 82d3122426..f38aa47800 100644 --- a/packages/cloud/src/auth/WebAuthService.ts +++ b/packages/cloud/src/auth/WebAuthService.ts @@ -14,7 +14,6 @@ import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" const authCredentialsSchema = z.object({ clientToken: z.string().min(1, "Client token cannot be empty"), sessionId: z.string().min(1, "Session ID cannot be empty"), - organizationId: z.string().nullable().optional(), }) type AuthCredentials = z.infer @@ -33,8 +32,8 @@ const clerkCreateSessionTokenResponseSchema = z.object({ const clerkMeResponseSchema = z.object({ response: z.object({ - first_name: z.string().optional().nullable(), - last_name: z.string().optional().nullable(), + first_name: z.string().optional(), + last_name: z.string().optional(), image_url: z.string().optional(), primary_email_address_id: z.string().optional(), email_addresses: z @@ -212,16 +211,7 @@ export class WebAuthService extends EventEmitter implements A try { const parsedJson = JSON.parse(credentialsJson) - const credentials = authCredentialsSchema.parse(parsedJson) - - // Migration: If no organizationId but we have userInfo, add it - if (credentials.organizationId === undefined && this.userInfo?.organizationId) { - credentials.organizationId = this.userInfo.organizationId - await this.storeCredentials(credentials) - this.log("[auth] Migrated credentials with organizationId") - } - - return credentials + return authCredentialsSchema.parse(parsedJson) } catch (error) { if (error instanceof z.ZodError) { this.log("[auth] Invalid credentials format:", error.errors) @@ -270,13 +260,8 @@ export class WebAuthService extends EventEmitter implements A * * @param code The authorization code from the callback * @param state The state parameter from the callback - * @param organizationId The organization ID from the callback (null for personal accounts) */ - public async handleCallback( - code: string | null, - state: string | null, - organizationId?: string | null, - ): Promise { + public async handleCallback(code: string | null, state: string | null): Promise { if (!code || !state) { vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url") return @@ -293,9 +278,6 @@ export class WebAuthService extends EventEmitter implements A const credentials = await this.clerkSignIn(code) - // Set organizationId (null for personal accounts) - credentials.organizationId = organizationId || null - await this.storeCredentials(credentials) vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud") @@ -426,15 +408,6 @@ export class WebAuthService extends EventEmitter implements A return this.userInfo } - /** - * Get the stored organization ID from credentials - * - * @returns The stored organization ID, null for personal accounts or if no credentials exist - */ - public getStoredOrganizationId(): string | null { - return this.credentials?.organizationId || null - } - private async clerkSignIn(ticket: string): Promise { const formData = new URLSearchParams() formData.append("strategy", "ticket") @@ -472,17 +445,6 @@ export class WebAuthService extends EventEmitter implements A const formData = new URLSearchParams() formData.append("_is_native", "1") - // Handle 3 cases for organization_id: - // 1. Have an org id: organization_id=THE_ORG_ID - // 2. Have a personal account: organization_id= (empty string) - // 3. Don't know if you have an org id (old style credentials): don't send organization_id param at all - const organizationId = this.getStoredOrganizationId() - if (this.credentials?.organizationId !== undefined) { - // We have organization context info (either org id or personal account) - formData.append("organization_id", organizationId || "") - } - // If organizationId is undefined, don't send the param at all (old credentials) - const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, { method: "POST", headers: { @@ -522,8 +484,7 @@ export class WebAuthService extends EventEmitter implements A const userInfo: CloudUserInfo = {} - const names = [userData.first_name, userData.last_name].filter((name) => !!name) - userInfo.name = names.length > 0 ? names.join(" ") : undefined + userInfo.name = `${userData.first_name} ${userData.last_name}` const primaryEmailAddressId = userData.primary_email_address_id const emailAddresses = userData.email_addresses @@ -535,74 +496,29 @@ export class WebAuthService extends EventEmitter implements A userInfo.picture = userData.image_url - // Fetch organization info if user is in organization context + // Fetch organization memberships separately try { - const storedOrgId = this.getStoredOrganizationId() + const orgMemberships = await this.clerkGetOrganizationMemberships() + if (orgMemberships && orgMemberships.length > 0) { + // Get the first (or active) organization membership + const primaryOrgMembership = orgMemberships[0] + const organization = primaryOrgMembership?.organization - if (this.credentials?.organizationId !== undefined) { - // We have organization context info - if (storedOrgId !== null) { - // User is in organization context - fetch user's memberships and filter - const orgMemberships = await this.clerkGetOrganizationMemberships() - const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId) - - if (userMembership) { - this.setUserOrganizationInfo(userInfo, userMembership) - this.log("[auth] User in organization context:", { - id: userMembership.organization.id, - name: userMembership.organization.name, - role: userMembership.role, - }) - } else { - this.log("[auth] Warning: User not found in stored organization:", storedOrgId) - } - } else { - this.log("[auth] User in personal account context - not setting organization info") - } - } else { - // Old credentials without organization context - fetch organization info to determine context - const orgMemberships = await this.clerkGetOrganizationMemberships() - const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships) - - if (primaryOrgMembership) { - this.setUserOrganizationInfo(userInfo, primaryOrgMembership) - this.log("[auth] Legacy credentials: Found organization membership:", { - id: primaryOrgMembership.organization.id, - name: primaryOrgMembership.organization.name, - role: primaryOrgMembership.role, - }) - } else { - this.log("[auth] Legacy credentials: No organization memberships found") + if (organization) { + userInfo.organizationId = organization.id + userInfo.organizationName = organization.name + userInfo.organizationRole = primaryOrgMembership.role + userInfo.organizationImageUrl = organization.image_url } } } catch (error) { - this.log("[auth] Failed to fetch organization info:", error) + this.log("[auth] Failed to fetch organization memberships:", error) // Don't throw - organization info is optional } return userInfo } - private findOrganizationMembership( - memberships: CloudOrganizationMembership[], - organizationId: string, - ): CloudOrganizationMembership | undefined { - return memberships?.find((membership) => membership.organization.id === organizationId) - } - - private findPrimaryOrganizationMembership( - memberships: CloudOrganizationMembership[], - ): CloudOrganizationMembership | undefined { - return memberships && memberships.length > 0 ? memberships[0] : undefined - } - - private setUserOrganizationInfo(userInfo: CloudUserInfo, membership: CloudOrganizationMembership): void { - userInfo.organizationId = membership.organization.id - userInfo.organizationName = membership.organization.name - userInfo.organizationRole = membership.role - userInfo.organizationImageUrl = membership.organization.image_url - } - private async clerkGetOrganizationMemberships(): Promise { const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, { headers: { diff --git a/packages/telemetry/src/BaseTelemetryClient.ts b/packages/telemetry/src/BaseTelemetryClient.ts index 2eb308b414..ab8ab56f59 100644 --- a/packages/telemetry/src/BaseTelemetryClient.ts +++ b/packages/telemetry/src/BaseTelemetryClient.ts @@ -25,21 +25,13 @@ export abstract class BaseTelemetryClient implements TelemetryClient { : !this.subscription.events.includes(eventName) } - /** - * Determines if a specific property should be included in telemetry events - * Override in subclasses to filter specific properties - */ - protected isPropertyCapturable(_propertyName: string): boolean { - return true - } - protected async getEventProperties(event: TelemetryEvent): Promise { let providerProperties: TelemetryEvent["properties"] = {} const provider = this.providerRef?.deref() if (provider) { try { - // Get properties from the provider + // Get the telemetry properties directly from the provider. providerProperties = await provider.getTelemetryProperties() } catch (error) { // Log error but continue with capturing the event. @@ -51,10 +43,7 @@ export abstract class BaseTelemetryClient implements TelemetryClient { // Merge provider properties with event-specific properties. // Event properties take precedence in case of conflicts. - const mergedProperties = { ...providerProperties, ...(event.properties || {}) } - - // Filter out properties that shouldn't be captured by this client - return Object.fromEntries(Object.entries(mergedProperties).filter(([key]) => this.isPropertyCapturable(key))) + return { ...providerProperties, ...(event.properties || {}) } } public abstract capture(event: TelemetryEvent): Promise diff --git a/packages/telemetry/src/PostHogTelemetryClient.ts b/packages/telemetry/src/PostHogTelemetryClient.ts index f1c46577df..243176ed45 100644 --- a/packages/telemetry/src/PostHogTelemetryClient.ts +++ b/packages/telemetry/src/PostHogTelemetryClient.ts @@ -13,8 +13,6 @@ import { BaseTelemetryClient } from "./BaseTelemetryClient" export class PostHogTelemetryClient extends BaseTelemetryClient { private client: PostHog private distinctId: string = vscode.env.machineId - // Git repository properties that should be filtered out - private readonly gitPropertyNames = ["repositoryUrl", "repositoryName", "defaultBranch"] constructor(debug = false) { super( @@ -28,19 +26,6 @@ export class PostHogTelemetryClient extends BaseTelemetryClient { this.client = new PostHog(process.env.POSTHOG_API_KEY || "", { host: "https://us.i.posthog.com" }) } - /** - * Filter out git repository properties for PostHog telemetry - * @param propertyName The property name to check - * @returns Whether the property should be included in telemetry events - */ - protected override isPropertyCapturable(propertyName: string): boolean { - // Filter out git repository properties - if (this.gitPropertyNames.includes(propertyName)) { - return false - } - return true - } - public override async capture(event: TelemetryEvent): Promise { if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) { if (this.debug) { diff --git a/packages/telemetry/src/TelemetryService.ts b/packages/telemetry/src/TelemetryService.ts index 7a11e3d388..728809f8bd 100644 --- a/packages/telemetry/src/TelemetryService.ts +++ b/packages/telemetry/src/TelemetryService.ts @@ -152,31 +152,6 @@ export class TelemetryService { this.captureEvent(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, { taskId }) } - /** - * Captures when a tab is shown due to user action - * @param tab The tab that was shown - */ - public captureTabShown(tab: string): void { - this.captureEvent(TelemetryEventName.TAB_SHOWN, { tab }) - } - - /** - * Captures when a setting is changed in ModesView - * @param settingName The name of the setting that was changed - */ - public captureModeSettingChanged(settingName: string): void { - this.captureEvent(TelemetryEventName.MODE_SETTINGS_CHANGED, { settingName }) - } - - /** - * Captures when a user creates a new custom mode - * @param modeSlug The slug of the custom mode - * @param modeName The name of the custom mode - */ - public captureCustomModeCreated(modeSlug: string, modeName: string): void { - this.captureEvent(TelemetryEventName.CUSTOM_MODE_CREATED, { modeSlug, modeName }) - } - /** * Captures a marketplace item installation event * @param itemId The unique identifier of the marketplace item diff --git a/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts b/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts index 282d1d6c6a..c94dbdb734 100644 --- a/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts +++ b/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts @@ -70,29 +70,6 @@ describe("PostHogTelemetryClient", () => { }) }) - describe("isPropertyCapturable", () => { - it("should filter out git repository properties", () => { - const client = new PostHogTelemetryClient() - - const isPropertyCapturable = getPrivateProperty<(propertyName: string) => boolean>( - client, - "isPropertyCapturable", - ).bind(client) - - // Git properties should be filtered out - expect(isPropertyCapturable("repositoryUrl")).toBe(false) - expect(isPropertyCapturable("repositoryName")).toBe(false) - expect(isPropertyCapturable("defaultBranch")).toBe(false) - - // Other properties should be included - expect(isPropertyCapturable("appVersion")).toBe(true) - expect(isPropertyCapturable("vscodeVersion")).toBe(true) - expect(isPropertyCapturable("platform")).toBe(true) - expect(isPropertyCapturable("mode")).toBe(true) - expect(isPropertyCapturable("customProperty")).toBe(true) - }) - }) - describe("getEventProperties", () => { it("should merge provider properties with event properties", async () => { const client = new PostHogTelemetryClient() @@ -135,54 +112,6 @@ describe("PostHogTelemetryClient", () => { expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1) }) - it("should filter out git repository properties", async () => { - const client = new PostHogTelemetryClient() - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue({ - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - // Git properties that should be filtered out - repositoryUrl: "https://github.com/example/repo", - repositoryName: "example/repo", - defaultBranch: "main", - }), - } - - client.setProvider(mockProvider) - - const getEventProperties = getPrivateProperty< - (event: { event: TelemetryEventName; properties?: Record }) => Promise> - >(client, "getEventProperties").bind(client) - - const result = await getEventProperties({ - event: TelemetryEventName.TASK_CREATED, - properties: { - customProp: "value", - }, - }) - - // Git properties should be filtered out - expect(result).not.toHaveProperty("repositoryUrl") - expect(result).not.toHaveProperty("repositoryName") - expect(result).not.toHaveProperty("defaultBranch") - - // Other properties should be included - expect(result).toEqual({ - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - customProp: "value", - }) - }) - it("should handle errors from provider gracefully", async () => { const client = new PostHogTelemetryClient() @@ -282,48 +211,6 @@ describe("PostHogTelemetryClient", () => { }), }) }) - - it("should filter out git repository properties when capturing events", async () => { - const client = new PostHogTelemetryClient() - client.updateTelemetryState(true) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue({ - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - // Git properties that should be filtered out - repositoryUrl: "https://github.com/example/repo", - repositoryName: "example/repo", - defaultBranch: "main", - }), - } - - client.setProvider(mockProvider) - - await client.capture({ - event: TelemetryEventName.TASK_CREATED, - properties: { test: "value" }, - }) - - expect(mockPostHogClient.capture).toHaveBeenCalledWith({ - distinctId: "test-machine-id", - event: TelemetryEventName.TASK_CREATED, - properties: expect.objectContaining({ - appVersion: "1.0.0", - test: "value", - }), - }) - - // Verify git properties are not included - const captureCall = mockPostHogClient.capture.mock.calls[0][0] - expect(captureCall.properties).not.toHaveProperty("repositoryUrl") - expect(captureCall.properties).not.toHaveProperty("repositoryName") - expect(captureCall.properties).not.toHaveProperty("defaultBranch") - }) }) describe("updateTelemetryState", () => { diff --git a/packages/types/npm/package.json b/packages/types/npm/package.json index 7f364e3d4f..2e0e33876a 100644 --- a/packages/types/npm/package.json +++ b/packages/types/npm/package.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.36.0", + "version": "1.27.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 75ff0b08b9..dc5ef439dc 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -125,7 +125,6 @@ export const ORGANIZATION_ALLOW_ALL: OrganizationAllowList = { export const ORGANIZATION_DEFAULT: OrganizationSettings = { version: 0, cloudSettings: { - recordTaskMessages: true, enableTaskSharing: true, taskShareExpirationDays: 30, }, diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 2754086dba..2180668def 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -139,9 +139,6 @@ export const globalSettingsSchema = z.object({ enhancementApiConfigId: z.string().optional(), historyPreviewCollapsed: z.boolean().optional(), profileThresholds: z.record(z.string(), z.number()).optional(), - hasOpenedModeSelector: z.boolean().optional(), - lastModeExportPath: z.string().optional(), - lastModeImportPath: z.string().optional(), }) export type GlobalSettings = z.infer diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 44937da235..f19dd81f60 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -14,7 +14,6 @@ export * from "./message.js" export * from "./mode.js" export * from "./model.js" export * from "./provider-settings.js" -export * from "./sharing.js" export * from "./telemetry.js" export * from "./terminal.js" export * from "./tool.js" diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index 175ec095fc..dfe95f8d7e 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -66,7 +66,6 @@ export const modeConfigSchema = z.object({ name: z.string().min(1, "Name is required"), roleDefinition: z.string().min(1, "Role definition is required"), whenToUse: z.string().optional(), - description: z.string().optional(), customInstructions: z.string().optional(), groups: groupEntryArraySchema, source: z.enum(["global", "project"]).optional(), @@ -107,7 +106,6 @@ export type CustomModesSettings = z.infer export const promptComponentSchema = z.object({ roleDefinition: z.string().optional(), whenToUse: z.string().optional(), - description: z.string().optional(), customInstructions: z.string().optional(), }) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index ea7089a81e..8e07cb69df 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -9,7 +9,6 @@ import { codebaseIndexProviderSchema } from "./codebase-index.js" export const providerNames = [ "anthropic", - "claude-code", "glama", "openrouter", "bedrock", @@ -19,7 +18,6 @@ export const providerNames = [ "vscode-lm", "lmstudio", "gemini", - "gemini-cli", "openai-native", "mistral", "moonshot", @@ -87,11 +85,6 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({ anthropicUseAuthToken: z.boolean().optional(), }) -const claudeCodeSchema = apiModelIdProviderModelSchema.extend({ - claudeCodePath: z.string().optional(), - claudeCodeMaxOutputTokens: z.number().int().min(1).max(200000).optional(), -}) - const glamaSchema = baseProviderSettingsSchema.extend({ glamaModelId: z.string().optional(), glamaApiKey: z.string().optional(), @@ -169,11 +162,6 @@ const geminiSchema = apiModelIdProviderModelSchema.extend({ googleGeminiBaseUrl: z.string().optional(), }) -const geminiCliSchema = apiModelIdProviderModelSchema.extend({ - geminiCliOAuthPath: z.string().optional(), - geminiCliProjectId: z.string().optional(), -}) - const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ openAiNativeApiKey: z.string().optional(), openAiNativeBaseUrl: z.string().optional(), @@ -242,7 +230,6 @@ const defaultSchema = z.object({ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [ anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })), - claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })), glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })), openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })), bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })), @@ -252,7 +239,6 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv vsCodeLmSchema.merge(z.object({ apiProvider: z.literal("vscode-lm") })), lmStudioSchema.merge(z.object({ apiProvider: z.literal("lmstudio") })), geminiSchema.merge(z.object({ apiProvider: z.literal("gemini") })), - geminiCliSchema.merge(z.object({ apiProvider: z.literal("gemini-cli") })), openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })), mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })), deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })), @@ -272,7 +258,6 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv export const providerSettingsSchema = z.object({ apiProvider: providerNamesSchema.optional(), ...anthropicSchema.shape, - ...claudeCodeSchema.shape, ...glamaSchema.shape, ...openRouterSchema.shape, ...bedrockSchema.shape, @@ -282,7 +267,6 @@ export const providerSettingsSchema = z.object({ ...vsCodeLmSchema.shape, ...lmStudioSchema.shape, ...geminiSchema.shape, - ...geminiCliSchema.shape, ...openAiNativeSchema.shape, ...mistralSchema.shape, ...deepSeekSchema.shape, @@ -322,7 +306,7 @@ export const getModelId = (settings: ProviderSettings): string | undefined => { } // Providers that use Anthropic-style API protocol -export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock"] +export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock"] // Helper function to determine API protocol for a provider and model export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => { diff --git a/packages/types/src/providers/groq.ts b/packages/types/src/providers/groq.ts index 2eac1f954a..1159a4f5bc 100644 --- a/packages/types/src/providers/groq.ts +++ b/packages/types/src/providers/groq.ts @@ -71,7 +71,7 @@ export const groqModels = { description: "Alibaba Qwen QwQ 32B model, 128K context.", }, "qwen/qwen3-32b": { - maxTokens: 8192, + maxTokens: 131072, contextWindow: 131072, supportsImages: false, supportsPromptCache: false, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index e4e506b8a7..2ea0958aa0 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -1,7 +1,6 @@ export * from "./anthropic.js" export * from "./bedrock.js" export * from "./chutes.js" -export * from "./claude-code.js" export * from "./deepseek.js" export * from "./gemini.js" export * from "./glama.js" diff --git a/packages/types/src/sharing.ts b/packages/types/src/sharing.ts deleted file mode 100644 index f295798032..0000000000 --- a/packages/types/src/sharing.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Types related to task sharing functionality - */ - -/** - * Visibility options for sharing tasks - */ -export type ShareVisibility = "organization" | "public" diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 223c39484c..7b4814aa02 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -25,17 +25,12 @@ export enum TelemetryEventName { TASK_CONVERSATION_MESSAGE = "Conversation Message", LLM_COMPLETION = "LLM Completion", MODE_SWITCH = "Mode Switched", - MODE_SELECTOR_OPENED = "Mode Selector Opened", TOOL_USED = "Tool Used", CHECKPOINT_CREATED = "Checkpoint Created", CHECKPOINT_RESTORED = "Checkpoint Restored", CHECKPOINT_DIFFED = "Checkpoint Diffed", - TAB_SHOWN = "Tab Shown", - MODE_SETTINGS_CHANGED = "Mode Setting Changed", - CUSTOM_MODE_CREATED = "Custom Mode Created", - CONTEXT_CONDENSED = "Context Condensed", SLIDING_WINDOW_TRUNCATION = "Sliding Window Truncation", @@ -51,16 +46,6 @@ export enum TelemetryEventName { MARKETPLACE_TAB_VIEWED = "Marketplace Tab Viewed", MARKETPLACE_INSTALL_BUTTON_CLICKED = "Marketplace Install Button Clicked", - SHARE_BUTTON_CLICKED = "Share Button Clicked", - SHARE_ORGANIZATION_CLICKED = "Share Organization Clicked", - SHARE_PUBLIC_CLICKED = "Share Public Clicked", - SHARE_CONNECT_TO_CLOUD_CLICKED = "Share Connect To Cloud Clicked", - - ACCOUNT_CONNECT_CLICKED = "Account Connect Clicked", - ACCOUNT_CONNECT_SUCCESS = "Account Connect Success", - ACCOUNT_LOGOUT_CLICKED = "Account Logout Clicked", - ACCOUNT_LOGOUT_SUCCESS = "Account Logout Success", - SCHEMA_VALIDATION_ERROR = "Schema Validation Error", DIFF_APPLICATION_ERROR = "Diff Application Error", SHELL_INTEGRATION_ERROR = "Shell Integration Error", @@ -80,7 +65,6 @@ export const appPropertiesSchema = z.object({ editorName: z.string(), language: z.string(), mode: z.string(), - cloudIsAuthenticated: z.boolean().optional(), }) export const taskPropertiesSchema = z.object({ @@ -99,20 +83,12 @@ export const taskPropertiesSchema = z.object({ .optional(), }) -export const gitPropertiesSchema = z.object({ - repositoryUrl: z.string().optional(), - repositoryName: z.string().optional(), - defaultBranch: z.string().optional(), -}) - export const telemetryPropertiesSchema = z.object({ ...appPropertiesSchema.shape, ...taskPropertiesSchema.shape, - ...gitPropertiesSchema.shape, }) export type TelemetryProperties = z.infer -export type GitProperties = z.infer /** * TelemetryEvent @@ -136,7 +112,6 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.TASK_COMPLETED, TelemetryEventName.TASK_CONVERSATION_MESSAGE, TelemetryEventName.MODE_SWITCH, - TelemetryEventName.MODE_SELECTOR_OPENED, TelemetryEventName.TOOL_USED, TelemetryEventName.CHECKPOINT_CREATED, TelemetryEventName.CHECKPOINT_RESTORED, @@ -147,16 +122,6 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.AUTHENTICATION_INITIATED, TelemetryEventName.MARKETPLACE_ITEM_INSTALLED, TelemetryEventName.MARKETPLACE_ITEM_REMOVED, - TelemetryEventName.MARKETPLACE_TAB_VIEWED, - TelemetryEventName.MARKETPLACE_INSTALL_BUTTON_CLICKED, - TelemetryEventName.SHARE_BUTTON_CLICKED, - TelemetryEventName.SHARE_ORGANIZATION_CLICKED, - TelemetryEventName.SHARE_PUBLIC_CLICKED, - TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED, - TelemetryEventName.ACCOUNT_CONNECT_CLICKED, - TelemetryEventName.ACCOUNT_CONNECT_SUCCESS, - TelemetryEventName.ACCOUNT_LOGOUT_CLICKED, - TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS, TelemetryEventName.SCHEMA_VALIDATION_ERROR, TelemetryEventName.DIFF_APPLICATION_ERROR, TelemetryEventName.SHELL_INTEGRATION_ERROR, @@ -164,9 +129,6 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.CODE_INDEX_ERROR, TelemetryEventName.CONTEXT_CONDENSED, TelemetryEventName.SLIDING_WINDOW_TRUNCATION, - TelemetryEventName.TAB_SHOWN, - TelemetryEventName.MODE_SETTINGS_CHANGED, - TelemetryEventName.CUSTOM_MODE_CREATED, ]), properties: telemetryPropertiesSchema, }), diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index 00f6bbbcba..e6640e9bb6 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -48,7 +48,6 @@ export const commandIds = [ "newTask", "setCustomStoragePath", - "importSettings", "focusInput", "acceptInput", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b94b47a1e..393487eb77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -359,6 +359,9 @@ importers: '@roo-code/types': specifier: workspace:^ version: link:../types + axios: + specifier: ^1.7.4 + version: 1.9.0 zod: specifier: ^3.25.61 version: 3.25.61 @@ -699,9 +702,6 @@ importers: pretty-bytes: specifier: ^7.0.0 version: 7.0.0 - proper-lockfile: - specifier: ^4.1.2 - version: 4.1.2 ps-tree: specifier: ^1.2.0 version: 1.2.0 @@ -717,6 +717,9 @@ importers: sanitize-filename: specifier: ^1.6.3 version: 1.6.3 + sax: + specifier: ^1.4.1 + version: 1.4.1 say: specifier: ^0.16.0 version: 0.16.0 @@ -729,9 +732,6 @@ importers: sound-play: specifier: ^1.1.0 version: 1.1.0 - stream-json: - specifier: ^1.8.0 - version: 1.9.1 string-similarity: specifier: ^4.0.4 version: 4.0.4 @@ -808,15 +808,12 @@ importers: '@types/node-ipc': specifier: ^9.2.3 version: 9.2.3 - '@types/proper-lockfile': - specifier: ^4.1.4 - version: 4.1.4 '@types/ps-tree': specifier: ^1.1.6 version: 1.1.6 - '@types/stream-json': - specifier: ^1.7.8 - version: 1.7.8 + '@types/sax': + specifier: ^1.2.7 + version: 1.2.7 '@types/string-similarity': specifier: ^4.0.2 version: 4.0.2 @@ -958,9 +955,6 @@ importers: fzf: specifier: ^0.5.2 version: 0.5.2 - hast-util-to-jsx-runtime: - specifier: ^2.3.6 - version: 2.3.6 i18next: specifier: ^25.0.0 version: 25.2.1(typescript@5.8.3) @@ -1036,9 +1030,6 @@ importers: source-map: specifier: ^0.7.4 version: 0.7.4 - stacktrace-js: - specifier: ^2.0.2 - version: 2.0.2 styled-components: specifier: ^6.1.13 version: 6.1.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1100,9 +1091,6 @@ importers: '@types/shell-quote': specifier: ^1.7.5 version: 1.7.5 - '@types/stacktrace-js': - specifier: ^2.0.3 - version: 2.0.3 '@types/vscode-webview': specifier: ^1.57.5 version: 1.57.5 @@ -3871,9 +3859,6 @@ packages: '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/proper-lockfile@4.1.4': - resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} - '@types/ps-tree@1.1.6': resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==} @@ -3885,8 +3870,8 @@ packages: '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} - '@types/retry@0.12.5': - resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} @@ -3894,16 +3879,6 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/stacktrace-js@2.0.3': - resolution: {integrity: sha512-B6JnMic4NAZ4mLWmRi4RvayCN2HZQvpcVF0MkoqubtuZx1AQB0/kRlrngGiocEPyO7R+TFocTEoLKQ0HzmEOPw==} - deprecated: This is a stub types definition. stacktrace-js provides its own type definitions, so you do not need this installed. - - '@types/stream-chain@2.1.0': - resolution: {integrity: sha512-guDyAl6s/CAzXUOWpGK2bHvdiopLIwpGu8v10+lb9hnQOyo4oj/ZUQFOvqFjKGsE3wJP1fpIesCcMvbXuWsqOg==} - - '@types/stream-json@1.7.8': - resolution: {integrity: sha512-MU1OB1eFLcYWd1LjwKXrxdoPtXSRzRmAnnxs4Js/ayB5O/NvHraWwuOaqMWIebpYwM6khFlsJOHEhI9xK/ab4Q==} - '@types/string-similarity@4.0.2': resolution: {integrity: sha512-LkJQ/jsXtCVMK+sKYAmX/8zEq+/46f1PTQw7YtmQwb74jemS1SlNLmARM2Zml9DgdDTWKAtc5L13WorpHPDjDA==} @@ -7980,9 +7955,6 @@ packages: resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} engines: {node: '>= 8'} - proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} @@ -8318,10 +8290,6 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -8653,15 +8621,9 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} - stream-chain@2.2.5: - resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} - stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} - stream-json@1.9.1: - resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} - streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -13079,10 +13041,6 @@ snapshots: '@types/prop-types@15.7.14': {} - '@types/proper-lockfile@4.1.4': - dependencies: - '@types/retry': 0.12.5 - '@types/ps-tree@1.1.6': {} '@types/react-dom@18.3.7(@types/react@18.3.23)': @@ -13094,25 +13052,14 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 - '@types/retry@0.12.5': {} + '@types/sax@1.2.7': + dependencies: + '@types/node': 20.17.57 '@types/shell-quote@1.7.5': {} '@types/stack-utils@2.0.3': {} - '@types/stacktrace-js@2.0.3': - dependencies: - stacktrace-js: 2.0.2 - - '@types/stream-chain@2.1.0': - dependencies: - '@types/node': 20.19.1 - - '@types/stream-json@1.7.8': - dependencies: - '@types/node': 20.19.1 - '@types/stream-chain': 2.1.0 - '@types/string-similarity@4.0.2': {} '@types/stylis@4.2.5': {} @@ -13136,7 +13083,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.1 + '@types/node': 20.17.57 optional: true '@types/yargs-parser@21.0.3': {} @@ -17825,12 +17772,6 @@ snapshots: propagate@2.0.1: {} - proper-lockfile@4.1.2: - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - property-information@5.6.0: dependencies: xtend: 4.0.2 @@ -18305,8 +18246,6 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - retry@0.12.0: {} - reusify@1.1.0: {} rfdc@1.4.1: {} @@ -18716,16 +18655,10 @@ snapshots: stdin-discarder@0.2.2: {} - stream-chain@2.2.5: {} - stream-combiner@0.0.4: dependencies: duplexer: 0.1.2 - stream-json@1.9.1: - dependencies: - stream-chain: 2.2.5 - streamsearch@1.1.0: {} streamx@2.22.0: diff --git a/scripts/update-contributors.js b/scripts/update-contributors.js old mode 100644 new mode 100755 index 6bd4c35f0c..32fd645f68 --- a/scripts/update-contributors.js +++ b/scripts/update-contributors.js @@ -183,15 +183,9 @@ async function readReadme() { * @param {Array} contributors Array of contributor objects from GitHub API * @returns {string} HTML for contributors section */ -const EXCLUDED_LOGIN_SUBSTRINGS = ['[bot]', 'R00-B0T']; -const EXCLUDED_LOGIN_EXACTS = ['cursor', 'roomote']; - function formatContributorsSection(contributors) { - // Filter out GitHub Actions bot, cursor, and roomote - const filteredContributors = contributors.filter((c) => - !EXCLUDED_LOGIN_SUBSTRINGS.some(sub => c.login.includes(sub)) && - !EXCLUDED_LOGIN_EXACTS.includes(c.login) - ) + // Filter out GitHub Actions bot + const filteredContributors = contributors.filter((c) => !c.login.includes("[bot]") && !c.login.includes("R00-B0T")) // Start building with Markdown table format let markdown = `${START_MARKER} diff --git a/src/activate/CodeActionProvider.ts b/src/activate/CodeActionProvider.ts index 4a0eb1b81e..2646552452 100644 --- a/src/activate/CodeActionProvider.ts +++ b/src/activate/CodeActionProvider.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode" import { CodeActionName, CodeActionId } from "@roo-code/types" -import { Package } from "../shared/package" import { getCodeActionCommand } from "../utils/commands" import { EditorUtils } from "../integrations/editor/EditorUtils" @@ -37,10 +36,6 @@ export class CodeActionProvider implements vscode.CodeActionProvider { context: vscode.CodeActionContext, ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { try { - if (!vscode.workspace.getConfiguration(Package.name).get("enableCodeActions", true)) { - return [] - } - const effectiveRange = EditorUtils.getEffectiveRange(document, range) if (!effectiveRange) { diff --git a/src/activate/__tests__/CodeActionProvider.spec.ts b/src/activate/__tests__/CodeActionProvider.spec.ts index 8a99f748c1..671dd0927f 100644 --- a/src/activate/__tests__/CodeActionProvider.spec.ts +++ b/src/activate/__tests__/CodeActionProvider.spec.ts @@ -25,11 +25,6 @@ vi.mock("vscode", () => ({ Information: 2, Hint: 3, }, - workspace: { - getConfiguration: vi.fn().mockReturnValue({ - get: vi.fn().mockReturnValue(true), - }), - }, })) vi.mock("../../integrations/editor/EditorUtils", () => ({ @@ -99,30 +94,9 @@ describe("CodeActionProvider", () => { expect(actions).toEqual([]) }) - it("should return empty array when enableCodeActions is disabled", () => { - // Mock the configuration to return false for enableCodeActions - const mockGet = vi.fn().mockReturnValue(false) - const mockGetConfiguration = vi.fn().mockReturnValue({ - get: mockGet, - }) - ;(vscode.workspace.getConfiguration as Mock).mockReturnValue(mockGetConfiguration()) - - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) - - expect(actions).toEqual([]) - expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline") - expect(mockGet).toHaveBeenCalledWith("enableCodeActions", true) - }) - it("should handle errors gracefully", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) - // Reset the workspace mock to return true for enableCodeActions - const mockGet = vi.fn().mockReturnValue(true) - const mockGetConfiguration = vi.fn().mockReturnValue({ - get: mockGet, - }) - ;(vscode.workspace.getConfiguration as Mock).mockReturnValue(mockGetConfiguration()) ;(EditorUtils.getEffectiveRange as Mock).mockImplementation(() => { throw new Error("Test error") }) diff --git a/src/activate/__tests__/registerCommands.spec.ts b/src/activate/__tests__/registerCommands.spec.ts index 92c129fa03..e1d23bfcb8 100644 --- a/src/activate/__tests__/registerCommands.spec.ts +++ b/src/activate/__tests__/registerCommands.spec.ts @@ -16,15 +16,6 @@ vi.mock("vscode", () => ({ window: { createTextEditorDecorationType: vi.fn().mockReturnValue({ dispose: vi.fn() }), }, - workspace: { - workspaceFolders: [ - { - uri: { - fsPath: "/mock/workspace", - }, - }, - ], - }, })) vi.mock("../../core/webview/ClineProvider") diff --git a/src/activate/handleUri.ts b/src/activate/handleUri.ts index 7f0b4c64cc..106bcdb311 100644 --- a/src/activate/handleUri.ts +++ b/src/activate/handleUri.ts @@ -38,13 +38,7 @@ export const handleUri = async (uri: vscode.Uri) => { case "/auth/clerk/callback": { const code = query.get("code") const state = query.get("state") - const organizationId = query.get("organizationId") - - await CloudService.instance.handleAuthCallback( - code, - state, - organizationId === "null" ? null : organizationId, - ) + await CloudService.instance.handleAuthCallback(code, state) break } default: diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index bd925b0e90..e6911b2332 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -13,9 +13,6 @@ import { focusPanel } from "../utils/focusPanel" import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay" import { handleNewTask } from "./handleTask" import { CodeIndexManager } from "../services/code-index/manager" -import { importSettingsWithFeedback } from "../core/config/importExport" -import { MdmService } from "../services/mdm/MdmService" -import { t } from "../i18n" /** * Helper to get the visible ClineProvider instance or log if not found. @@ -174,22 +171,6 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt const { promptForCustomStoragePath } = await import("../utils/storage") await promptForCustomStoragePath() }, - importSettings: async (filePath?: string) => { - const visibleProvider = getVisibleProviderOrLog(outputChannel) - if (!visibleProvider) { - return - } - - await importSettingsWithFeedback( - { - providerSettingsManager: visibleProvider.providerSettingsManager, - contextProxy: visibleProvider.contextProxy, - customModesManager: visibleProvider.customModesManager, - provider: visibleProvider, - }, - filePath, - ) - }, focusInput: async () => { try { await focusPanel(tabPanel, sidebarPanel) @@ -228,16 +209,7 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit editor.viewColumn || 0)) // Check if there are any visible text editors, otherwise open a new group diff --git a/src/api/index.ts b/src/api/index.ts index bda390848c..fc50c98ecf 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -29,7 +29,6 @@ import { HuggingFaceHandler, ChutesHandler, LiteLLMHandler, - ClaudeCodeHandler, } from "./providers" export interface SingleCompletionHandler { @@ -67,8 +66,6 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { switch (apiProvider) { case "anthropic": return new AnthropicHandler(options) - case "claude-code": - return new ClaudeCodeHandler(options) case "glama": return new GlamaHandler(options) case "openrouter": diff --git a/src/api/providers/__tests__/bedrock-error-handling.spec.ts b/src/api/providers/__tests__/bedrock-error-handling.spec.ts deleted file mode 100644 index 53e582c25b..0000000000 --- a/src/api/providers/__tests__/bedrock-error-handling.spec.ts +++ /dev/null @@ -1,551 +0,0 @@ -import { vi } from "vitest" - -// Mock BedrockRuntimeClient and commands -const mockSend = vi.fn() - -// Mock AWS SDK credential providers -vi.mock("@aws-sdk/credential-providers", () => { - return { - fromIni: vi.fn().mockReturnValue({ - accessKeyId: "profile-access-key", - secretAccessKey: "profile-secret-key", - }), - } -}) - -vi.mock("@aws-sdk/client-bedrock-runtime", () => ({ - BedrockRuntimeClient: vi.fn().mockImplementation(() => ({ - send: mockSend, - })), - ConverseStreamCommand: vi.fn(), - ConverseCommand: vi.fn(), -})) - -import { AwsBedrockHandler } from "../bedrock" -import { Anthropic } from "@anthropic-ai/sdk" - -describe("AwsBedrockHandler Error Handling", () => { - let handler: AwsBedrockHandler - - beforeEach(() => { - vi.clearAllMocks() - handler = new AwsBedrockHandler({ - apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", - awsAccessKey: "test-access-key", - awsSecretKey: "test-secret-key", - awsRegion: "us-east-1", - }) - }) - - const createMockError = (options: { - message?: string - name?: string - status?: number - __type?: string - $metadata?: { - httpStatusCode?: number - requestId?: string - extendedRequestId?: string - cfId?: string - [key: string]: any // Allow additional properties - } - }): Error => { - const error = new Error(options.message || "Test error") as any - if (options.name) error.name = options.name - if (options.status) error.status = options.status - if (options.__type) error.__type = options.__type - if (options.$metadata) error.$metadata = options.$metadata - return error - } - - describe("Throttling Error Detection", () => { - it("should detect throttling from HTTP 429 status code", async () => { - const throttleError = createMockError({ - message: "Request failed", - status: 429, - }) - - mockSend.mockRejectedValueOnce(throttleError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("throttled or rate limited") - } catch (error) { - expect(error.message).toContain("throttled or rate limited") - } - }) - - it("should detect throttling from AWS SDK $metadata.httpStatusCode", async () => { - const throttleError = createMockError({ - message: "Request failed", - $metadata: { httpStatusCode: 429 }, - }) - - mockSend.mockRejectedValueOnce(throttleError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("throttled or rate limited") - } catch (error) { - expect(error.message).toContain("throttled or rate limited") - } - }) - - it("should detect throttling from ThrottlingException name", async () => { - const throttleError = createMockError({ - message: "Request failed", - name: "ThrottlingException", - }) - - mockSend.mockRejectedValueOnce(throttleError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("throttled or rate limited") - } catch (error) { - expect(error.message).toContain("throttled or rate limited") - } - }) - - it("should detect throttling from __type field", async () => { - const throttleError = createMockError({ - message: "Request failed", - __type: "ThrottlingException", - }) - - mockSend.mockRejectedValueOnce(throttleError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("throttled or rate limited") - } catch (error) { - expect(error.message).toContain("throttled or rate limited") - } - }) - - it("should detect throttling from 'Bedrock is unable to process your request' message", async () => { - const throttleError = createMockError({ - message: "Bedrock is unable to process your request", - }) - - mockSend.mockRejectedValueOnce(throttleError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("throttled or rate limited") - } catch (error) { - expect(error.message).toMatch(/throttled or rate limited/) - } - }) - - it("should detect throttling from various message patterns", async () => { - const throttlingMessages = [ - "Request throttled", - "Rate limit exceeded", - "Too many requests", - "Service unavailable due to high demand", - "Server is overloaded", - "System is busy", - "Please wait and try again", - ] - - for (const message of throttlingMessages) { - const throttleError = createMockError({ message }) - mockSend.mockRejectedValueOnce(throttleError) - - try { - await handler.completePrompt("test") - // Should not reach here as completePrompt should throw - throw new Error("Expected error to be thrown") - } catch (error) { - expect(error.message).toContain("throttled or rate limited") - } - } - }) - - it("should display appropriate error information for throttling errors", async () => { - const throttlingError = createMockError({ - message: "Bedrock is unable to process your request", - name: "ThrottlingException", - status: 429, - $metadata: { - httpStatusCode: 429, - requestId: "12345-abcde-67890", - extendedRequestId: "extended-12345", - cfId: "cf-12345", - }, - }) - - mockSend.mockRejectedValueOnce(throttlingError) - - try { - await handler.completePrompt("test") - throw new Error("Expected error to be thrown") - } catch (error) { - // Should contain the main error message - expect(error.message).toContain("throttled or rate limited") - } - }) - }) - - describe("Service Quota Exceeded Detection", () => { - it("should detect service quota exceeded errors", async () => { - const quotaError = createMockError({ - message: "Service quota exceeded for model requests", - }) - - mockSend.mockRejectedValueOnce(quotaError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("Service quota exceeded") - } catch (error) { - expect(error.message).toContain("Service quota exceeded") - } - }) - }) - - describe("Model Not Ready Detection", () => { - it("should detect model not ready errors", async () => { - const modelError = createMockError({ - message: "Model is not ready, please try again later", - }) - - mockSend.mockRejectedValueOnce(modelError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("Model is not ready") - } catch (error) { - expect(error.message).toContain("Model is not ready") - } - }) - }) - - describe("Internal Server Error Detection", () => { - it("should detect internal server errors", async () => { - const serverError = createMockError({ - message: "Internal server error occurred", - }) - - mockSend.mockRejectedValueOnce(serverError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("internal server error") - } catch (error) { - expect(error.message).toContain("internal server error") - } - }) - }) - - describe("Token Limit Detection", () => { - it("should detect enhanced token limit errors", async () => { - const tokenErrors = [ - "Too many tokens in request", - "Token limit exceeded", - "Maximum context length reached", - "Context length exceeds limit", - ] - - for (const message of tokenErrors) { - const tokenError = createMockError({ message }) - mockSend.mockRejectedValueOnce(tokenError) - - try { - await handler.completePrompt("test") - // Should not reach here as completePrompt should throw - throw new Error("Expected error to be thrown") - } catch (error) { - // Either "Too many tokens" for token-specific errors or "throttled" for limit-related errors - expect(error.message).toMatch(/Too many tokens|throttled or rate limited/) - } - } - }) - }) - - describe("Streaming Context Error Handling", () => { - it("should handle throttling errors in streaming context", async () => { - const throttleError = createMockError({ - message: "Bedrock is unable to process your request", - status: 429, - }) - - const mockStream = { - [Symbol.asyncIterator]() { - return { - async next() { - throw throttleError - }, - } - }, - } - - mockSend.mockResolvedValueOnce({ stream: mockStream }) - - const generator = handler.createMessage("system", [{ role: "user", content: "test" }]) - - // For throttling errors, it should throw immediately without yielding chunks - // This allows the retry mechanism to catch and handle it - await expect(async () => { - for await (const chunk of generator) { - // Should not yield any chunks for throttling errors - } - }).rejects.toThrow("Bedrock is unable to process your request") - }) - - it("should yield error chunks for non-throttling errors in streaming context", async () => { - const genericError = createMockError({ - message: "Some other error", - status: 500, - }) - - const mockStream = { - [Symbol.asyncIterator]() { - return { - async next() { - throw genericError - }, - } - }, - } - - mockSend.mockResolvedValueOnce({ stream: mockStream }) - - const generator = handler.createMessage("system", [{ role: "user", content: "test" }]) - - const chunks: any[] = [] - try { - for await (const chunk of generator) { - chunks.push(chunk) - } - } catch (error) { - // Expected to throw after yielding chunks - } - - // Should have yielded error chunks before throwing for non-throttling errors - expect( - chunks.some((chunk) => chunk.type === "text" && chunk.text && chunk.text.includes("Some other error")), - ).toBe(true) - }) - }) - - describe("Error Priority and Specificity", () => { - it("should prioritize HTTP status codes over message patterns", async () => { - // Error with both 429 status and generic message should be detected as throttling - const mixedError = createMockError({ - message: "Some generic error message", - status: 429, - }) - - mockSend.mockRejectedValueOnce(mixedError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("throttled or rate limited") - } catch (error) { - expect(error.message).toContain("throttled or rate limited") - } - }) - - it("should prioritize AWS error types over message patterns", async () => { - // Error with ThrottlingException name but different message should still be throttling - const specificError = createMockError({ - message: "Some other error occurred", - name: "ThrottlingException", - }) - - mockSend.mockRejectedValueOnce(specificError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("throttled or rate limited") - } catch (error) { - expect(error.message).toContain("throttled or rate limited") - } - }) - }) - - describe("Unknown Error Fallback", () => { - it("should still show unknown error for truly unrecognized errors", async () => { - const unknownError = createMockError({ - message: "Something completely unexpected happened", - }) - - mockSend.mockRejectedValueOnce(unknownError) - - try { - const result = await handler.completePrompt("test") - expect(result).toContain("Unknown Error") - } catch (error) { - expect(error.message).toContain("Unknown Error") - } - }) - }) - - describe("Enhanced Error Throw for Retry System", () => { - it("should throw enhanced error messages for completePrompt to display in retry system", async () => { - const throttlingError = createMockError({ - message: "Too many tokens, rate limited", - status: 429, - $metadata: { - httpStatusCode: 429, - requestId: "test-request-id-12345", - }, - }) - mockSend.mockRejectedValueOnce(throttlingError) - - try { - await handler.completePrompt("test") - throw new Error("Expected error to be thrown") - } catch (error) { - // Should contain the verbose message template - expect(error.message).toContain("Request was throttled or rate limited") - // Should preserve original error properties - expect((error as any).status).toBe(429) - expect((error as any).$metadata.requestId).toBe("test-request-id-12345") - } - }) - - it("should throw enhanced error messages for createMessage streaming to display in retry system", async () => { - const tokenError = createMockError({ - message: "Too many tokens in request", - name: "ValidationException", - $metadata: { - httpStatusCode: 400, - requestId: "token-error-id-67890", - extendedRequestId: "extended-12345", - }, - }) - - const mockStream = { - [Symbol.asyncIterator]() { - return { - async next() { - throw tokenError - }, - } - }, - } - - mockSend.mockResolvedValueOnce({ stream: mockStream }) - - try { - const stream = handler.createMessage("system", [{ role: "user", content: "test" }]) - for await (const chunk of stream) { - // Should not reach here as it should throw an error - } - throw new Error("Expected error to be thrown") - } catch (error) { - // Should contain error codes (note: this will be caught by the non-throttling error path) - expect(error.message).toContain("Too many tokens") - // Should preserve original error properties - expect(error.name).toBe("ValidationException") - expect((error as any).$metadata.requestId).toBe("token-error-id-67890") - } - }) - }) - - describe("Edge Case Test Coverage", () => { - it("should handle concurrent throttling errors correctly", async () => { - const throttlingError = createMockError({ - message: "Bedrock is unable to process your request", - status: 429, - }) - - // Setup multiple concurrent requests that will all fail with throttling - mockSend.mockRejectedValue(throttlingError) - - // Execute multiple concurrent requests - const promises = Array.from({ length: 5 }, () => handler.completePrompt("test")) - - // All should throw with throttling error - const results = await Promise.allSettled(promises) - - results.forEach((result) => { - expect(result.status).toBe("rejected") - if (result.status === "rejected") { - expect(result.reason.message).toContain("throttled or rate limited") - } - }) - }) - - it("should handle mixed error scenarios with both throttling and other indicators", async () => { - // Error with both 429 status (throttling) and validation error message - const mixedError = createMockError({ - message: "ValidationException: Your input is invalid, but also rate limited", - name: "ValidationException", - status: 429, - $metadata: { - httpStatusCode: 429, - requestId: "mixed-error-id", - }, - }) - - mockSend.mockRejectedValueOnce(mixedError) - - try { - await handler.completePrompt("test") - } catch (error) { - // Should be treated as throttling due to 429 status taking priority - expect(error.message).toContain("throttled or rate limited") - // Should still preserve metadata - expect((error as any).$metadata?.requestId).toBe("mixed-error-id") - } - }) - - it("should handle rapid successive retries in streaming context", async () => { - const throttlingError = createMockError({ - message: "ThrottlingException", - name: "ThrottlingException", - }) - - // Mock stream that throws immediately - const mockStream = { - // eslint-disable-next-line require-yield - [Symbol.asyncIterator]: async function* () { - throw throttlingError - }, - } - - mockSend.mockResolvedValueOnce({ stream: mockStream }) - - const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "test" }] - - try { - // Should throw immediately without yielding any chunks - const stream = handler.createMessage("", messages) - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - // Should not reach here - expect(chunks).toHaveLength(0) - } catch (error) { - // Error should be thrown immediately for retry mechanism - // The error might be a TypeError if the stream iterator fails - expect(error).toBeDefined() - // The important thing is that it throws immediately without yielding chunks - } - }) - - it("should validate error properties exist before accessing them", async () => { - // Error with unusual structure - const unusualError = { - message: "Error with unusual structure", - // Missing typical properties like name, status, etc. - } - - mockSend.mockRejectedValueOnce(unusualError) - - try { - await handler.completePrompt("test") - } catch (error) { - // Should handle gracefully without accessing undefined properties - expect(error.message).toContain("Unknown Error") - // Should not have undefined values in the error message - expect(error.message).not.toContain("undefined") - } - }) - }) -}) diff --git a/src/api/providers/__tests__/claude-code-caching.spec.ts b/src/api/providers/__tests__/claude-code-caching.spec.ts deleted file mode 100644 index b7f7ff852a..0000000000 --- a/src/api/providers/__tests__/claude-code-caching.spec.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest" -import { ClaudeCodeHandler } from "../claude-code" -import { runClaudeCode } from "../../../integrations/claude-code/run" -import type { ApiHandlerOptions } from "../../../shared/api" -import type { ClaudeCodeMessage } from "../../../integrations/claude-code/types" -import type { ApiStreamUsageChunk } from "../../transform/stream" -import type { Anthropic } from "@anthropic-ai/sdk" - -// Mock the runClaudeCode function -vi.mock("../../../integrations/claude-code/run", () => ({ - runClaudeCode: vi.fn(), -})) - -describe("ClaudeCodeHandler - Caching Support", () => { - let handler: ClaudeCodeHandler - const mockOptions: ApiHandlerOptions = { - apiKey: "test-key", - apiModelId: "claude-3-5-sonnet-20241022", - claudeCodePath: "/test/path", - } - - beforeEach(() => { - handler = new ClaudeCodeHandler(mockOptions) - vi.clearAllMocks() - }) - - it("should collect cache read tokens from API response", async () => { - const mockStream = async function* (): AsyncGenerator { - // Initial system message - yield { - type: "system", - subtype: "init", - session_id: "test-session", - tools: [], - mcp_servers: [], - apiKeySource: "user", - } as ClaudeCodeMessage - - // Assistant message with cache tokens - const message: Anthropic.Messages.Message = { - id: "msg_123", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [{ type: "text", text: "Hello!", citations: [] }], - usage: { - input_tokens: 100, - output_tokens: 50, - cache_read_input_tokens: 80, // 80 tokens read from cache - cache_creation_input_tokens: 20, // 20 new tokens cached - }, - stop_reason: "end_turn", - stop_sequence: null, - } - - yield { - type: "assistant", - message, - session_id: "test-session", - } as ClaudeCodeMessage - - // Result with cost - yield { - type: "result", - subtype: "success", - result: "success", - total_cost_usd: 0.001, - is_error: false, - duration_ms: 1000, - duration_api_ms: 900, - num_turns: 1, - session_id: "test-session", - } as ClaudeCodeMessage - } - - vi.mocked(runClaudeCode).mockReturnValue(mockStream()) - - const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) - - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - // Find the usage chunk - const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined - expect(usageChunk).toBeDefined() - expect(usageChunk!.inputTokens).toBe(100) - expect(usageChunk!.outputTokens).toBe(50) - expect(usageChunk!.cacheReadTokens).toBe(80) - expect(usageChunk!.cacheWriteTokens).toBe(20) - }) - - it("should accumulate cache tokens across multiple messages", async () => { - const mockStream = async function* (): AsyncGenerator { - yield { - type: "system", - subtype: "init", - session_id: "test-session", - tools: [], - mcp_servers: [], - apiKeySource: "user", - } as ClaudeCodeMessage - - // First message chunk - const message1: Anthropic.Messages.Message = { - id: "msg_1", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [{ type: "text", text: "Part 1", citations: [] }], - usage: { - input_tokens: 50, - output_tokens: 25, - cache_read_input_tokens: 40, - cache_creation_input_tokens: 10, - }, - stop_reason: null, - stop_sequence: null, - } - - yield { - type: "assistant", - message: message1, - session_id: "test-session", - } as ClaudeCodeMessage - - // Second message chunk - const message2: Anthropic.Messages.Message = { - id: "msg_2", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [{ type: "text", text: "Part 2", citations: [] }], - usage: { - input_tokens: 50, - output_tokens: 25, - cache_read_input_tokens: 30, - cache_creation_input_tokens: 20, - }, - stop_reason: "end_turn", - stop_sequence: null, - } - - yield { - type: "assistant", - message: message2, - session_id: "test-session", - } as ClaudeCodeMessage - - yield { - type: "result", - subtype: "success", - result: "success", - total_cost_usd: 0.002, - is_error: false, - duration_ms: 2000, - duration_api_ms: 1800, - num_turns: 1, - session_id: "test-session", - } as ClaudeCodeMessage - } - - vi.mocked(runClaudeCode).mockReturnValue(mockStream()) - - const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) - - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined - expect(usageChunk).toBeDefined() - expect(usageChunk!.inputTokens).toBe(100) // 50 + 50 - expect(usageChunk!.outputTokens).toBe(50) // 25 + 25 - expect(usageChunk!.cacheReadTokens).toBe(70) // 40 + 30 - expect(usageChunk!.cacheWriteTokens).toBe(30) // 10 + 20 - }) - - it("should handle missing cache token fields gracefully", async () => { - const mockStream = async function* (): AsyncGenerator { - yield { - type: "system", - subtype: "init", - session_id: "test-session", - tools: [], - mcp_servers: [], - apiKeySource: "user", - } as ClaudeCodeMessage - - // Message without cache tokens - const message: Anthropic.Messages.Message = { - id: "msg_123", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [{ type: "text", text: "Hello!", citations: [] }], - usage: { - input_tokens: 100, - output_tokens: 50, - cache_read_input_tokens: null, - cache_creation_input_tokens: null, - }, - stop_reason: "end_turn", - stop_sequence: null, - } - - yield { - type: "assistant", - message, - session_id: "test-session", - } as ClaudeCodeMessage - - yield { - type: "result", - subtype: "success", - result: "success", - total_cost_usd: 0.001, - is_error: false, - duration_ms: 1000, - duration_api_ms: 900, - num_turns: 1, - session_id: "test-session", - } as ClaudeCodeMessage - } - - vi.mocked(runClaudeCode).mockReturnValue(mockStream()) - - const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) - - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined - expect(usageChunk).toBeDefined() - expect(usageChunk!.inputTokens).toBe(100) - expect(usageChunk!.outputTokens).toBe(50) - expect(usageChunk!.cacheReadTokens).toBe(0) - expect(usageChunk!.cacheWriteTokens).toBe(0) - }) - - it("should report zero cost for subscription usage", async () => { - const mockStream = async function* (): AsyncGenerator { - // Subscription usage has apiKeySource: "none" - yield { - type: "system", - subtype: "init", - session_id: "test-session", - tools: [], - mcp_servers: [], - apiKeySource: "none", - } as ClaudeCodeMessage - - const message: Anthropic.Messages.Message = { - id: "msg_123", - type: "message", - role: "assistant", - model: "claude-3-5-sonnet-20241022", - content: [{ type: "text", text: "Hello!", citations: [] }], - usage: { - input_tokens: 100, - output_tokens: 50, - cache_read_input_tokens: 80, - cache_creation_input_tokens: 20, - }, - stop_reason: "end_turn", - stop_sequence: null, - } - - yield { - type: "assistant", - message, - session_id: "test-session", - } as ClaudeCodeMessage - - yield { - type: "result", - subtype: "success", - result: "success", - total_cost_usd: 0.001, // This should be ignored for subscription usage - is_error: false, - duration_ms: 1000, - duration_api_ms: 900, - num_turns: 1, - session_id: "test-session", - } as ClaudeCodeMessage - } - - vi.mocked(runClaudeCode).mockReturnValue(mockStream()) - - const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }]) - - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - - const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined - expect(usageChunk).toBeDefined() - expect(usageChunk!.totalCost).toBe(0) // Should be 0 for subscription usage - }) -}) diff --git a/src/api/providers/__tests__/lmstudio.spec.ts b/src/api/providers/__tests__/lmstudio.spec.ts index 0adebdeea7..2679d225df 100644 --- a/src/api/providers/__tests__/lmstudio.spec.ts +++ b/src/api/providers/__tests__/lmstudio.spec.ts @@ -71,7 +71,7 @@ describe("LmStudioHandler", () => { mockOptions = { apiModelId: "local-model", lmStudioModelId: "local-model", - lmStudioBaseUrl: "http://localhost:1234", + lmStudioBaseUrl: "http://localhost:1234/v1", } handler = new LmStudioHandler(mockOptions) mockCreate.mockClear() diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index b4b5f29204..90e4a08284 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -601,7 +601,7 @@ describe("OpenAiHandler", () => { stream: true, stream_options: { include_usage: true }, reasoning_effort: "medium", - temperature: undefined, + temperature: 0.5, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 32000, }), @@ -642,7 +642,7 @@ describe("OpenAiHandler", () => { stream: true, stream_options: { include_usage: true }, reasoning_effort: "medium", - temperature: undefined, + temperature: 0.7, }), {}, ) @@ -684,7 +684,7 @@ describe("OpenAiHandler", () => { { role: "user", content: "Hello!" }, ], reasoning_effort: "medium", - temperature: undefined, + temperature: 0.3, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 65536, // Using default maxTokens from o3Options }), @@ -714,7 +714,7 @@ describe("OpenAiHandler", () => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ - temperature: undefined, // Temperature is not supported for O3 models + temperature: 0, // Default temperature }), {}, ) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index a25fea5200..beba8ccaf3 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -561,47 +561,16 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH // Clear timeout on error clearTimeout(timeoutId) - // Check if this is a throttling error that should trigger retry logic - const errorType = this.getErrorType(error) - - // For throttling errors, throw immediately without yielding chunks - // This allows the retry mechanism in attemptApiRequest() to catch and handle it - // The retry logic in Task.ts (around line 1817) expects errors to be thrown - // on the first chunk for proper exponential backoff behavior - if (errorType === "THROTTLING") { - if (error instanceof Error) { - throw error - } else { - throw new Error("Throttling error occurred") - } - } - - // For non-throttling errors, use the standard error handling with chunks + // Use the extracted error handling method for all errors const errorChunks = this.handleBedrockError(error, true) // true for streaming context // Yield each chunk individually to ensure type compatibility for (const chunk of errorChunks) { yield chunk as any // Cast to any to bypass type checking since we know the structure is correct } - // Re-throw with enhanced error message for retry system - const enhancedErrorMessage = this.formatErrorMessage(error, this.getErrorType(error), true) + // Re-throw the error if (error instanceof Error) { - const enhancedError = new Error(enhancedErrorMessage) - // Preserve important properties from the original error - enhancedError.name = error.name - // Validate and preserve status property - if ("status" in error && typeof (error as any).status === "number") { - ;(enhancedError as any).status = (error as any).status - } - // Validate and preserve $metadata property - if ( - "$metadata" in error && - typeof (error as any).$metadata === "object" && - (error as any).$metadata !== null - ) { - ;(enhancedError as any).$metadata = (error as any).$metadata - } - throw enhancedError + throw error } else { throw new Error("An unknown error occurred") } @@ -669,26 +638,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH const errorResult = this.handleBedrockError(error, false) // false for non-streaming context // Since we're in a non-streaming context, we know the result is a string const errorMessage = errorResult as string - - // Create enhanced error for retry system - const enhancedError = new Error(errorMessage) - if (error instanceof Error) { - // Preserve important properties from the original error - enhancedError.name = error.name - // Validate and preserve status property - if ("status" in error && typeof (error as any).status === "number") { - ;(enhancedError as any).status = (error as any).status - } - // Validate and preserve $metadata property - if ( - "$metadata" in error && - typeof (error as any).$metadata === "object" && - (error as any).$metadata !== null - ) { - ;(enhancedError as any).$metadata = (error as any).$metadata - } - } - throw enhancedError + throw new Error(errorMessage) } } @@ -1074,32 +1024,19 @@ Please verify: logLevel: "error", }, THROTTLING: { - patterns: [ - "throttl", - "rate", - "limit", - "bedrock is unable to process your request", // AWS Bedrock specific throttling message - "please wait", - "quota exceeded", - "service unavailable", - "busy", - "overloaded", - "too many requests", - "request limit", - "concurrent requests", - ], + patterns: ["throttl", "rate", "limit"], messageTemplate: `Request was throttled or rate limited. Please try: 1. Reducing the frequency of requests 2. If using a provisioned model, check its throughput settings 3. Contact AWS support to request a quota increase if needed - +{formattedErrorDetails} `, logLevel: "error", }, TOO_MANY_TOKENS: { - patterns: ["too many tokens", "token limit exceeded", "context length", "maximum context length"], + patterns: ["too many tokens"], messageTemplate: `"Too many tokens" error detected. Possible Causes: 1. Input exceeds model's context window limit @@ -1112,49 +1049,7 @@ Suggestions: 2. Split your request into smaller chunks 3. Use a model with a larger context window 4. If rate limited, reduce request frequency -5. Check your Amazon Bedrock quotas and limits - -`, - logLevel: "error", - }, - SERVICE_QUOTA_EXCEEDED: { - patterns: ["service quota exceeded", "service quota", "quota exceeded for model"], - messageTemplate: `Service quota exceeded. This error indicates you've reached AWS service limits. - -Please try: -1. Contact AWS support to request a quota increase -2. Reduce request frequency temporarily -3. Check your AWS Bedrock quotas in the AWS console -4. Consider using a different model or region with available capacity - -`, - logLevel: "error", - }, - MODEL_NOT_READY: { - patterns: ["model not ready", "model is not ready", "provisioned throughput not ready", "model loading"], - messageTemplate: `Model is not ready or still loading. This can happen with: -1. Provisioned throughput models that are still initializing -2. Custom models that are being loaded -3. Models that are temporarily unavailable - -Please try: -1. Wait a few minutes and retry -2. Check the model status in AWS Bedrock console -3. Verify the model is properly provisioned - -`, - logLevel: "error", - }, - INTERNAL_SERVER_ERROR: { - patterns: ["internal server error", "internal error", "server error", "service error"], - messageTemplate: `AWS Bedrock internal server error. This is a temporary service issue. - -Please try: -1. Retry the request after a brief delay -2. If the error persists, check AWS service health -3. Contact AWS support if the issue continues - -`, +5. Check your Amazon Bedrock quotas and limits`, logLevel: "error", }, ON_DEMAND_NOT_SUPPORTED: { @@ -1213,34 +1108,12 @@ Please check: return "GENERIC" } - // Check for HTTP 429 status code (Too Many Requests) - if ((error as any).status === 429 || (error as any).$metadata?.httpStatusCode === 429) { - return "THROTTLING" - } - - // Check for AWS Bedrock specific throttling exception names - if ((error as any).name === "ThrottlingException" || (error as any).__type === "ThrottlingException") { - return "THROTTLING" - } - const errorMessage = error.message.toLowerCase() const errorName = error.name.toLowerCase() - // Check each error type's patterns in order of specificity (most specific first) - const errorTypeOrder = [ - "SERVICE_QUOTA_EXCEEDED", // Most specific - check before THROTTLING - "MODEL_NOT_READY", - "TOO_MANY_TOKENS", - "INTERNAL_SERVER_ERROR", - "ON_DEMAND_NOT_SUPPORTED", - "NOT_FOUND", - "ACCESS_DENIED", - "THROTTLING", // Less specific - check after more specific patterns - ] - - for (const errorType of errorTypeOrder) { - const definition = AwsBedrockHandler.ERROR_TYPES[errorType] - if (!definition) continue + // Check each error type's patterns + for (const [errorType, definition] of Object.entries(AwsBedrockHandler.ERROR_TYPES)) { + if (errorType === "GENERIC") continue // Skip the generic type // If any pattern matches in either message or name, return this error type if (definition.patterns.some((pattern) => errorMessage.includes(pattern) || errorName.includes(pattern))) { @@ -1269,6 +1142,37 @@ Please check: const modelConfig = this.getModel() templateVars.modelId = modelConfig.id templateVars.contextWindow = String(modelConfig.info.contextWindow || "unknown") + + // Format error details + const errorDetails: Record = {} + Object.getOwnPropertyNames(error).forEach((prop) => { + if (prop !== "stack") { + errorDetails[prop] = (error as any)[prop] + } + }) + + // Safely stringify error details to avoid circular references + templateVars.formattedErrorDetails = Object.entries(errorDetails) + .map(([key, value]) => { + let valueStr + if (typeof value === "object" && value !== null) { + try { + // Use a replacer function to handle circular references + valueStr = JSON.stringify(value, (k, v) => { + if (k && typeof v === "object" && v !== null) { + return "[Object]" + } + return v + }) + } catch (e) { + valueStr = "[Complex Object]" + } + } else { + valueStr = String(value) + } + return `- ${key}: ${valueStr}` + }) + .join("\n") } // Add context-specific template variables diff --git a/src/api/providers/fetchers/__tests__/lmstudio.test.ts b/src/api/providers/fetchers/__tests__/lmstudio.test.ts index 98fe5db32e..59b4388785 100644 --- a/src/api/providers/fetchers/__tests__/lmstudio.test.ts +++ b/src/api/providers/fetchers/__tests__/lmstudio.test.ts @@ -1,6 +1,6 @@ import axios from "axios" import { vi, describe, it, expect, beforeEach } from "vitest" -import { LMStudioClient, LLM, LLMInstanceInfo, LLMInfo } from "@lmstudio/sdk" +import { LMStudioClient, LLM, LLMInstanceInfo } from "@lmstudio/sdk" // LLMInfo is a type import { getLMStudioModels, parseLMStudioModel } from "../lmstudio" import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types" // ModelInfo is a type @@ -11,16 +11,12 @@ const mockedAxios = axios as any // Mock @lmstudio/sdk const mockGetModelInfo = vi.fn() const mockListLoaded = vi.fn() -const mockListDownloadedModels = vi.fn() vi.mock("@lmstudio/sdk", () => { return { LMStudioClient: vi.fn().mockImplementation(() => ({ llm: { listLoaded: mockListLoaded, }, - system: { - listDownloadedModels: mockListDownloadedModels, - }, })), } }) @@ -32,7 +28,6 @@ describe("LMStudio Fetcher", () => { MockedLMStudioClientConstructor.mockClear() mockListLoaded.mockClear() mockGetModelInfo.mockClear() - mockListDownloadedModels.mockClear() }) describe("parseLMStudioModel", () => { @@ -93,40 +88,8 @@ describe("LMStudio Fetcher", () => { trainedForToolUse: false, // Added } - it("should fetch downloaded models using system.listDownloadedModels", async () => { - const mockLLMInfo: LLMInfo = { - type: "llm" as const, - modelKey: "mistralai/devstral-small-2505", - format: "safetensors", - displayName: "Devstral Small 2505", - path: "mistralai/devstral-small-2505", - sizeBytes: 13277565112, - architecture: "mistral", - vision: false, - trainedForToolUse: false, - maxContextLength: 131072, - } - + it("should fetch and parse models successfully", async () => { mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } }) - mockListDownloadedModels.mockResolvedValueOnce([mockLLMInfo]) - - const result = await getLMStudioModels(baseUrl) - - expect(mockedAxios.get).toHaveBeenCalledTimes(1) - expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`) - expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1) - expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl }) - expect(mockListDownloadedModels).toHaveBeenCalledTimes(1) - expect(mockListDownloadedModels).toHaveBeenCalledWith("llm") - expect(mockListLoaded).not.toHaveBeenCalled() - - const expectedParsedModel = parseLMStudioModel(mockLLMInfo) - expect(result).toEqual({ [mockLLMInfo.path]: expectedParsedModel }) - }) - - it("should fall back to listLoaded when listDownloadedModels fails", async () => { - mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } }) - mockListDownloadedModels.mockRejectedValueOnce(new Error("Method not available")) mockListLoaded.mockResolvedValueOnce([{ getModelInfo: mockGetModelInfo }]) mockGetModelInfo.mockResolvedValueOnce(mockRawModel) @@ -136,7 +99,6 @@ describe("LMStudio Fetcher", () => { expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`) expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1) expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl }) - expect(mockListDownloadedModels).toHaveBeenCalledTimes(1) expect(mockListLoaded).toHaveBeenCalledTimes(1) const expectedParsedModel = parseLMStudioModel(mockRawModel) diff --git a/src/api/providers/fetchers/__tests__/ollama.test.ts b/src/api/providers/fetchers/__tests__/ollama.test.ts index bf1bf3c6b2..cada0a4b60 100644 --- a/src/api/providers/fetchers/__tests__/ollama.test.ts +++ b/src/api/providers/fetchers/__tests__/ollama.test.ts @@ -31,31 +31,6 @@ describe("Ollama Fetcher", () => { description: "Family: qwen3, Context: 40960, Size: 32.8B", }) }) - - it("should handle models with null families field", () => { - const modelDataWithNullFamilies = { - ...ollamaModelsData["qwen3-2to16:latest"], - details: { - ...ollamaModelsData["qwen3-2to16:latest"].details, - families: null, - }, - } - - const parsedModel = parseOllamaModel(modelDataWithNullFamilies as any) - - expect(parsedModel).toEqual({ - maxTokens: 40960, - contextWindow: 40960, - supportsImages: false, - supportsComputerUse: false, - supportsPromptCache: true, - inputPrice: 0, - outputPrice: 0, - cacheWritesPrice: 0, - cacheReadsPrice: 0, - description: "Family: qwen3, Context: 40960, Size: 32.8B", - }) - }) }) describe("getOllamaModels", () => { @@ -154,69 +129,5 @@ describe("Ollama Fetcher", () => { consoleInfoSpy.mockRestore() // Restore original console.info }) - - it("should handle models with null families field in API response", async () => { - const baseUrl = "http://localhost:11434" - const modelName = "test-model:latest" - - const mockApiTagsResponse = { - models: [ - { - name: modelName, - model: modelName, - modified_at: "2025-06-03T09:23:22.610222878-04:00", - size: 14333928010, - digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5", - details: { - family: "llama", - families: null, // This is the case we're testing - format: "gguf", - parameter_size: "23.6B", - parent_model: "", - quantization_level: "Q4_K_M", - }, - }, - ], - } - const mockApiShowResponse = { - license: "Mock License", - modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}", - parameters: "num_ctx 4096\nstop_token ", - template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:", - modified_at: "2025-06-03T09:23:22.610222878-04:00", - details: { - parent_model: "", - format: "gguf", - family: "llama", - families: null, // This is the case we're testing - parameter_size: "23.6B", - quantization_level: "Q4_K_M", - }, - model_info: { - "ollama.context_length": 4096, - "some.other.info": "value", - }, - capabilities: ["completion"], - } - - mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse }) - mockedAxios.post.mockResolvedValueOnce({ data: mockApiShowResponse }) - - const result = await getOllamaModels(baseUrl) - - expect(mockedAxios.get).toHaveBeenCalledTimes(1) - expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`) - - expect(mockedAxios.post).toHaveBeenCalledTimes(1) - expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName }) - - expect(typeof result).toBe("object") - expect(result).not.toBeInstanceOf(Array) - expect(Object.keys(result).length).toBe(1) - expect(result[modelName]).toBeDefined() - - // Verify the model was parsed correctly despite null families - expect(result[modelName].description).toBe("Family: llama, Context: 4096, Size: 23.6B") - }) }) }) diff --git a/src/api/providers/fetchers/lmstudio.ts b/src/api/providers/fetchers/lmstudio.ts index 4b7ece71ea..ea1a590f1e 100644 --- a/src/api/providers/fetchers/lmstudio.ts +++ b/src/api/providers/fetchers/lmstudio.ts @@ -2,17 +2,14 @@ import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types" import { LLM, LLMInfo, LLMInstanceInfo, LMStudioClient } from "@lmstudio/sdk" import axios from "axios" -export const parseLMStudioModel = (rawModel: LLMInstanceInfo | LLMInfo): ModelInfo => { - // Handle both LLMInstanceInfo (from loaded models) and LLMInfo (from downloaded models) - const contextLength = "contextLength" in rawModel ? rawModel.contextLength : rawModel.maxContextLength - +export const parseLMStudioModel = (rawModel: LLMInstanceInfo): ModelInfo => { const modelInfo: ModelInfo = Object.assign({}, lMStudioDefaultModelInfo, { description: `${rawModel.displayName} - ${rawModel.path}`, - contextWindow: contextLength, + contextWindow: rawModel.contextLength, supportsPromptCache: true, supportsImages: rawModel.vision, supportsComputerUse: false, - maxTokens: contextLength, + maxTokens: rawModel.contextLength, }) return modelInfo @@ -36,25 +33,12 @@ export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Prom await axios.get(`${baseUrl}/v1/models`) const client = new LMStudioClient({ baseUrl: lmsUrl }) + const response = (await client.llm.listLoaded().then((models: LLM[]) => { + return Promise.all(models.map((m) => m.getModelInfo())) + })) as Array - // First, try to get all downloaded models - try { - const downloadedModels = await client.system.listDownloadedModels("llm") - for (const model of downloadedModels) { - // Use the model path as the key since that's what users select - models[model.path] = parseLMStudioModel(model) - } - } catch (error) { - console.warn("Failed to list downloaded models, falling back to loaded models only") - - // Fall back to listing only loaded models - const loadedModels = (await client.llm.listLoaded().then((models: LLM[]) => { - return Promise.all(models.map((m) => m.getModelInfo())) - })) as Array - - for (const lmstudioModel of loadedModels) { - models[lmstudioModel.modelKey] = parseLMStudioModel(lmstudioModel) - } + for (const lmstudioModel of response) { + models[lmstudioModel.modelKey] = parseLMStudioModel(lmstudioModel) } } catch (error) { if (error.code === "ECONNREFUSED") { diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index fef700268d..5956187e41 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -2,7 +2,6 @@ import * as path from "path" import fs from "fs/promises" import NodeCache from "node-cache" -import { safeWriteJson } from "../../../utils/safeWriteJson" import { ContextProxy } from "../../../core/config/ContextProxy" import { getCacheDirectoryPath } from "../../../utils/storage" @@ -23,7 +22,7 @@ const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) async function writeModels(router: RouterName, data: ModelRecord) { const filename = `${router}_models.json` const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath) - await safeWriteJson(path.join(cacheDir, filename), data) + await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data)) } async function readModels(router: RouterName): Promise { diff --git a/src/api/providers/fetchers/modelEndpointCache.ts b/src/api/providers/fetchers/modelEndpointCache.ts index 256ae84048..c69e7c82a3 100644 --- a/src/api/providers/fetchers/modelEndpointCache.ts +++ b/src/api/providers/fetchers/modelEndpointCache.ts @@ -2,7 +2,6 @@ import * as path from "path" import fs from "fs/promises" import NodeCache from "node-cache" -import { safeWriteJson } from "../../../utils/safeWriteJson" import sanitize from "sanitize-filename" import { ContextProxy } from "../../../core/config/ContextProxy" @@ -19,7 +18,7 @@ const getCacheKey = (router: RouterName, modelId: string) => sanitize(`${router} async function writeModelEndpoints(key: string, data: ModelRecord) { const filename = `${key}_endpoints.json` const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath) - await safeWriteJson(path.join(cacheDir, filename), data) + await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data, null, 2)) } async function readModelEndpoints(key: string): Promise { diff --git a/src/api/providers/fetchers/ollama.ts b/src/api/providers/fetchers/ollama.ts index 8e1e3f7f07..8de2c1a918 100644 --- a/src/api/providers/fetchers/ollama.ts +++ b/src/api/providers/fetchers/ollama.ts @@ -4,26 +4,26 @@ import { z } from "zod" const OllamaModelDetailsSchema = z.object({ family: z.string(), - families: z.array(z.string()).nullable().optional(), - format: z.string().optional(), + families: z.array(z.string()), + format: z.string(), parameter_size: z.string(), - parent_model: z.string().optional(), - quantization_level: z.string().optional(), + parent_model: z.string(), + quantization_level: z.string(), }) const OllamaModelSchema = z.object({ details: OllamaModelDetailsSchema, - digest: z.string().optional(), + digest: z.string(), model: z.string(), - modified_at: z.string().optional(), + modified_at: z.string(), name: z.string(), - size: z.number().optional(), + size: z.number(), }) const OllamaModelInfoResponseSchema = z.object({ - modelfile: z.string().optional(), - parameters: z.string().optional(), - template: z.string().optional(), + modelfile: z.string(), + parameters: z.string(), + template: z.string(), details: OllamaModelDetailsSchema, model_info: z.record(z.string(), z.any()), capabilities: z.array(z.string()).optional(), diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 1cefd0616b..5e9ee9762d 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -2,7 +2,6 @@ export { AnthropicVertexHandler } from "./anthropic-vertex" export { AnthropicHandler } from "./anthropic" export { AwsBedrockHandler } from "./bedrock" export { ChutesHandler } from "./chutes" -export { ClaudeCodeHandler } from "./claude-code" export { DeepSeekHandler } from "./deepseek" export { MoonshotHandler } from "./moonshot" export { FakeAIHandler } from "./fake-ai" diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index f5e4e4c985..b4f256f43a 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -86,7 +86,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format const ark = modelUrl.includes(".volces.com") - if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) { + if (modelId.startsWith("o3-mini")) { yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages) return } @@ -306,7 +306,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl stream: true, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), reasoning_effort: modelInfo.reasoningEffort, - temperature: undefined, + temperature: this.options.modelTemperature ?? 0, } // O3 family models do not support the deprecated max_tokens parameter @@ -331,7 +331,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ...convertToOpenAiMessages(messages), ], reasoning_effort: modelInfo.reasoningEffort, - temperature: undefined, + temperature: this.options.modelTemperature ?? 0, } // O3 family models do not support the deprecated max_tokens parameter diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6565daa238..51d97963e7 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -48,11 +48,13 @@ interface CompletionUsage { } total_tokens?: number cost?: number - cost_details?: { - upstream_inference_cost?: number - } + is_byok?: boolean } +// with bring your own key, OpenRouter charges 5% of what it normally would: https://openrouter.ai/docs/use-cases/byok +// so we multiply the cost reported by OpenRouter to get an estimate of what the request actually cost +const BYOK_COST_MULTIPLIER = 20 + export class OpenRouterHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI @@ -166,9 +168,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH type: "usage", inputTokens: lastUsage.prompt_tokens || 0, outputTokens: lastUsage.completion_tokens || 0, - cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens, + // Waiting on OpenRouter to figure out what this represents in the Gemini case + // and how to best support it. + // cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens, reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens, - totalCost: (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0), + totalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0), } } } diff --git a/src/api/transform/stream.ts b/src/api/transform/stream.ts index 89655a3f56..caa69a09fe 100644 --- a/src/api/transform/stream.ts +++ b/src/api/transform/stream.ts @@ -1,12 +1,6 @@ export type ApiStream = AsyncGenerator -export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk | ApiStreamReasoningChunk | ApiStreamError - -export interface ApiStreamError { - type: "error" - error: string - message: string -} +export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk | ApiStreamReasoningChunk export interface ApiStreamTextChunk { type: "text" diff --git a/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts b/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts deleted file mode 100644 index f5ae600bee..0000000000 --- a/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts +++ /dev/null @@ -1,340 +0,0 @@ -// npx vitest src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts - -import { TextContent, ToolUse } from "../../../shared/tools" - -import { AssistantMessageContent, parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage" -import { parseAssistantMessageV2 } from "../parseAssistantMessageV2" - -const isEmptyTextContent = (block: AssistantMessageContent) => - block.type === "text" && (block as TextContent).content === "" - -;[parseAssistantMessageV1, parseAssistantMessageV2].forEach((parser, index) => { - describe(`parseAssistantMessageV${index + 1}`, () => { - describe("text content parsing", () => { - it("should parse a simple text message", () => { - const message = "This is a simple text message" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0]).toEqual({ - type: "text", - content: message, - partial: true, // Text is always partial when it's the last content - }) - }) - - it("should parse a multi-line text message", () => { - const message = "This is a multi-line\ntext message\nwith several lines" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0]).toEqual({ - type: "text", - content: message, - partial: true, // Text is always partial when it's the last content - }) - }) - - it("should mark text as partial when it's the last content in the message", () => { - const message = "This is a partial text" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0]).toEqual({ - type: "text", - content: message, - partial: true, - }) - }) - }) - - describe("tool use parsing", () => { - it("should parse a simple tool use", () => { - const message = "src/file.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("read_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.partial).toBe(false) - }) - - it("should parse a tool use with multiple parameters", () => { - const message = - "src/file.ts1020" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("read_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.params.start_line).toBe("10") - expect(toolUse.params.end_line).toBe("20") - expect(toolUse.partial).toBe(false) - }) - - it("should mark tool use as partial when it's not closed", () => { - const message = "src/file.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("read_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.partial).toBe(true) - }) - - it("should handle a partial parameter in a tool use", () => { - const message = "src/file.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("read_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.partial).toBe(true) - }) - }) - - describe("mixed content parsing", () => { - it("should parse text followed by a tool use", () => { - const message = "Here's the file content: src/file.ts" - const result = parser(message) - - expect(result).toHaveLength(2) - - const textContent = result[0] as TextContent - expect(textContent.type).toBe("text") - expect(textContent.content).toBe("Here's the file content:") - expect(textContent.partial).toBe(false) - - const toolUse = result[1] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("read_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.partial).toBe(false) - }) - - it("should parse a tool use followed by text", () => { - const message = "src/file.tsHere's what I found in the file." - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(2) - - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("read_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.partial).toBe(false) - - const textContent = result[1] as TextContent - expect(textContent.type).toBe("text") - expect(textContent.content).toBe("Here's what I found in the file.") - expect(textContent.partial).toBe(true) - }) - - it("should parse multiple tool uses separated by text", () => { - const message = - "First file: src/file1.tsSecond file: src/file2.ts" - const result = parser(message) - - expect(result).toHaveLength(4) - - expect(result[0].type).toBe("text") - expect((result[0] as TextContent).content).toBe("First file:") - - expect(result[1].type).toBe("tool_use") - expect((result[1] as ToolUse).name).toBe("read_file") - expect((result[1] as ToolUse).params.path).toBe("src/file1.ts") - - expect(result[2].type).toBe("text") - expect((result[2] as TextContent).content).toBe("Second file:") - - expect(result[3].type).toBe("tool_use") - expect((result[3] as ToolUse).name).toBe("read_file") - expect((result[3] as ToolUse).params.path).toBe("src/file2.ts") - }) - }) - - describe("special cases", () => { - it("should handle the write_to_file tool with content that contains closing tags", () => { - const message = `src/file.ts - function example() { - // This has XML-like content: - return true; - } - 5` - - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("write_to_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.params.line_count).toBe("5") - expect(toolUse.params.content).toContain("function example()") - expect(toolUse.params.content).toContain("// This has XML-like content: ") - expect(toolUse.params.content).toContain("return true;") - expect(toolUse.partial).toBe(false) - }) - - it("should handle empty messages", () => { - const message = "" - const result = parser(message) - - expect(result).toHaveLength(0) - }) - - it("should handle malformed tool use tags", () => { - const message = "This has a malformed tag" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0].type).toBe("text") - expect((result[0] as TextContent).content).toBe(message) - }) - - it("should handle tool use with no parameters", () => { - const message = "" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("browser_action") - expect(Object.keys(toolUse.params).length).toBe(0) - expect(toolUse.partial).toBe(false) - }) - - it("should handle nested tool tags that aren't actually nested", () => { - const message = - "echo 'test.txt'" - - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("execute_command") - expect(toolUse.params.command).toBe("echo 'test.txt'") - expect(toolUse.partial).toBe(false) - }) - - it("should handle a tool use with a parameter containing XML-like content", () => { - const message = "
.*
src
" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("search_files") - expect(toolUse.params.regex).toBe("
.*
") - expect(toolUse.params.path).toBe("src") - expect(toolUse.partial).toBe(false) - }) - - it("should handle consecutive tool uses without text in between", () => { - const message = - "file1.tsfile2.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(2) - - const toolUse1 = result[0] as ToolUse - expect(toolUse1.type).toBe("tool_use") - expect(toolUse1.name).toBe("read_file") - expect(toolUse1.params.path).toBe("file1.ts") - expect(toolUse1.partial).toBe(false) - - const toolUse2 = result[1] as ToolUse - expect(toolUse2.type).toBe("tool_use") - expect(toolUse2.name).toBe("read_file") - expect(toolUse2.params.path).toBe("file2.ts") - expect(toolUse2.partial).toBe(false) - }) - - it("should handle whitespace in parameters", () => { - const message = " src/file.ts " - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("read_file") - expect(toolUse.params.path).toBe("src/file.ts") - expect(toolUse.partial).toBe(false) - }) - - it("should handle multi-line parameters", () => { - const message = `file.ts - line 1 - line 2 - line 3 - 3` - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const toolUse = result[0] as ToolUse - expect(toolUse.type).toBe("tool_use") - expect(toolUse.name).toBe("write_to_file") - expect(toolUse.params.path).toBe("file.ts") - expect(toolUse.params.content).toContain("line 1") - expect(toolUse.params.content).toContain("line 2") - expect(toolUse.params.content).toContain("line 3") - expect(toolUse.params.line_count).toBe("3") - expect(toolUse.partial).toBe(false) - }) - - it("should handle a complex message with multiple content types", () => { - const message = `I'll help you with that task. - - src/index.ts - - Now let's modify the file: - - src/index.ts - // Updated content - console.log("Hello world"); - 2 - - Let's run the code: - - node src/index.ts` - - const result = parser(message) - - expect(result).toHaveLength(6) - - // First text block - expect(result[0].type).toBe("text") - expect((result[0] as TextContent).content).toBe("I'll help you with that task.") - - // First tool use (read_file) - expect(result[1].type).toBe("tool_use") - expect((result[1] as ToolUse).name).toBe("read_file") - - // Second text block - expect(result[2].type).toBe("text") - expect((result[2] as TextContent).content).toContain("Now let's modify the file:") - - // Second tool use (write_to_file) - expect(result[3].type).toBe("tool_use") - expect((result[3] as ToolUse).name).toBe("write_to_file") - - // Third text block - expect(result[4].type).toBe("text") - expect((result[4] as TextContent).content).toContain("Let's run the code:") - - // Third tool use (execute_command) - expect(result[5].type).toBe("tool_use") - expect((result[5] as ToolUse).name).toBe("execute_command") - }) - }) - }) -}) diff --git a/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts b/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts deleted file mode 100644 index d5450988c9..0000000000 --- a/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ - -// node --expose-gc --import tsx src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts - -import { performance } from "perf_hooks" -import { parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage" -import { parseAssistantMessageV2 } from "../parseAssistantMessageV2" - -const formatNumber = (num: number): string => { - return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") -} - -const measureExecutionTime = (fn: Function, input: string, iterations: number = 1000): number => { - for (let i = 0; i < 10; i++) { - fn(input) - } - - const start = performance.now() - - for (let i = 0; i < iterations; i++) { - fn(input) - } - - const end = performance.now() - return (end - start) / iterations // Average time per iteration in ms. -} - -const measureMemoryUsage = ( - fn: Function, - input: string, - iterations: number = 100, -): { heapUsed: number; heapTotal: number } => { - if (global.gc) { - // Force garbage collection if available. - global.gc() - } else { - console.warn("No garbage collection hook! Run with --expose-gc for more accurate memory measurements.") - } - - const initialMemory = process.memoryUsage() - - for (let i = 0; i < iterations; i++) { - fn(input) - } - - const finalMemory = process.memoryUsage() - - return { - heapUsed: (finalMemory.heapUsed - initialMemory.heapUsed) / iterations, - heapTotal: (finalMemory.heapTotal - initialMemory.heapTotal) / iterations, - } -} - -const testCases = [ - { - name: "Simple text message", - input: "This is a simple text message without any tool uses.", - }, - { - name: "Message with a simple tool use", - input: "Let's read a file: src/file.ts", - }, - { - name: "Message with a complex tool use (write_to_file)", - input: "src/file.ts\nfunction example() {\n // This has XML-like content: \n return true;\n}\n5", - }, - { - name: "Message with multiple tool uses", - input: "First file: src/file1.ts\nSecond file: src/file2.ts\nLet's write a new file: src/file3.ts\nexport function newFunction() {\n return 'Hello world';\n}\n3", - }, - { - name: "Large message with repeated tool uses", - input: Array(50) - .fill( - 'src/file.ts\noutput.tsconsole.log("hello");1', - ) - .join("\n"), - }, -] - -const runBenchmark = () => { - const maxNameLength = testCases.reduce((max, testCase) => Math.max(max, testCase.name.length), 0) - const namePadding = maxNameLength + 2 - - console.log( - `| ${"Test Case".padEnd(namePadding)} | V1 Time (ms) | V2 Time (ms) | V1/V2 Ratio | V1 Heap (bytes) | V2 Heap (bytes) |`, - ) - console.log( - `| ${"-".repeat(namePadding)} | ------------ | ------------ | ----------- | ---------------- | ---------------- |`, - ) - - for (const testCase of testCases) { - const v1Time = measureExecutionTime(parseAssistantMessageV1, testCase.input) - const v2Time = measureExecutionTime(parseAssistantMessageV2, testCase.input) - const timeRatio = v1Time / v2Time - - const v1Memory = measureMemoryUsage(parseAssistantMessageV1, testCase.input) - const v2Memory = measureMemoryUsage(parseAssistantMessageV2, testCase.input) - - console.log( - `| ${testCase.name.padEnd(namePadding)} | ` + - `${v1Time.toFixed(4).padStart(12)} | ` + - `${v2Time.toFixed(4).padStart(12)} | ` + - `${timeRatio.toFixed(2).padStart(11)} | ` + - `${formatNumber(Math.round(v1Memory.heapUsed)).padStart(16)} | ` + - `${formatNumber(Math.round(v2Memory.heapUsed)).padStart(16)} |`, - ) - } -} - -runBenchmark() diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts deleted file mode 100644 index 72201b7722..0000000000 --- a/src/core/assistant-message/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { type AssistantMessageContent, parseAssistantMessage } from "./parseAssistantMessage" -export { presentAssistantMessage } from "./presentAssistantMessage" diff --git a/src/core/assistant-message/parseAssistantMessage.ts b/src/core/assistant-message/parseAssistantMessage.ts deleted file mode 100644 index ebb8674c8f..0000000000 --- a/src/core/assistant-message/parseAssistantMessage.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { type ToolName, toolNames } from "@roo-code/types" - -import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools" - -export type AssistantMessageContent = TextContent | ToolUse - -export function parseAssistantMessage(assistantMessage: string): AssistantMessageContent[] { - let contentBlocks: AssistantMessageContent[] = [] - let currentTextContent: TextContent | undefined = undefined - let currentTextContentStartIndex = 0 - let currentToolUse: ToolUse | undefined = undefined - let currentToolUseStartIndex = 0 - let currentParamName: ToolParamName | undefined = undefined - let currentParamValueStartIndex = 0 - let accumulator = "" - - for (let i = 0; i < assistantMessage.length; i++) { - const char = assistantMessage[i] - accumulator += char - - // There should not be a param without a tool use. - if (currentToolUse && currentParamName) { - const currentParamValue = accumulator.slice(currentParamValueStartIndex) - const paramClosingTag = `` - if (currentParamValue.endsWith(paramClosingTag)) { - // End of param value. - // Don't trim content parameters to preserve newlines, but strip first and last newline only - const paramValue = currentParamValue.slice(0, -paramClosingTag.length) - currentToolUse.params[currentParamName] = - currentParamName === "content" - ? paramValue.replace(/^\n/, "").replace(/\n$/, "") - : paramValue.trim() - currentParamName = undefined - continue - } else { - // Partial param value is accumulating. - continue - } - } - - // No currentParamName. - - if (currentToolUse) { - const currentToolValue = accumulator.slice(currentToolUseStartIndex) - const toolUseClosingTag = `` - if (currentToolValue.endsWith(toolUseClosingTag)) { - // End of a tool use. - currentToolUse.partial = false - contentBlocks.push(currentToolUse) - currentToolUse = undefined - continue - } else { - const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`) - for (const paramOpeningTag of possibleParamOpeningTags) { - if (accumulator.endsWith(paramOpeningTag)) { - // Start of a new parameter. - currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName - currentParamValueStartIndex = accumulator.length - break - } - } - - // There's no current param, and not starting a new param. - - // Special case for write_to_file where file contents could - // contain the closing tag, in which case the param would have - // closed and we end up with the rest of the file contents here. - // To work around this, we get the string between the starting - // content tag and the LAST content tag. - const contentParamName: ToolParamName = "content" - - if (currentToolUse.name === "write_to_file" && accumulator.endsWith(``)) { - const toolContent = accumulator.slice(currentToolUseStartIndex) - const contentStartTag = `<${contentParamName}>` - const contentEndTag = `` - const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length - const contentEndIndex = toolContent.lastIndexOf(contentEndTag) - - if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) { - // Don't trim content to preserve newlines, but strip first and last newline only - currentToolUse.params[contentParamName] = toolContent - .slice(contentStartIndex, contentEndIndex) - .replace(/^\n/, "") - .replace(/\n$/, "") - } - } - - // Partial tool value is accumulating. - continue - } - } - - // No currentToolUse. - - let didStartToolUse = false - const possibleToolUseOpeningTags = toolNames.map((name) => `<${name}>`) - - for (const toolUseOpeningTag of possibleToolUseOpeningTags) { - if (accumulator.endsWith(toolUseOpeningTag)) { - // Start of a new tool use. - currentToolUse = { - type: "tool_use", - name: toolUseOpeningTag.slice(1, -1) as ToolName, - params: {}, - partial: true, - } - - currentToolUseStartIndex = accumulator.length - - // This also indicates the end of the current text content. - if (currentTextContent) { - currentTextContent.partial = false - - // Remove the partially accumulated tool use tag from the - // end of text (() - const toolParamOpenTags = new Map() - - for (const name of toolNames) { - toolUseOpenTags.set(`<${name}>`, name) - } - - for (const name of toolParamNames) { - toolParamOpenTags.set(`<${name}>`, name) - } - - const len = assistantMessage.length - - for (let i = 0; i < len; i++) { - const currentCharIndex = i - - // Parsing a tool parameter - if (currentToolUse && currentParamName) { - const closeTag = `` - // Check if the string *ending* at index `i` matches the closing tag - if ( - currentCharIndex >= closeTag.length - 1 && - assistantMessage.startsWith( - closeTag, - currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag. - ) - ) { - // Found the closing tag for the parameter. - const value = assistantMessage.slice( - currentParamValueStart, // Start after the opening tag. - currentCharIndex - closeTag.length + 1, // End before the closing tag. - ) - // Don't trim content parameters to preserve newlines, but strip first and last newline only - currentToolUse.params[currentParamName] = - currentParamName === "content" ? value.replace(/^\n/, "").replace(/\n$/, "") : value.trim() - currentParamName = undefined // Go back to parsing tool content. - // We don't continue loop here, need to check for tool close or other params at index i. - } else { - continue // Still inside param value, move to next char. - } - } - - // Parsing a tool use (but not a specific parameter). - if (currentToolUse && !currentParamName) { - // Ensure we are not inside a parameter already. - // Check if starting a new parameter. - let startedNewParam = false - - for (const [tag, paramName] of toolParamOpenTags.entries()) { - if ( - currentCharIndex >= tag.length - 1 && - assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1) - ) { - currentParamName = paramName - currentParamValueStart = currentCharIndex + 1 // Value starts after the tag. - startedNewParam = true - break - } - } - - if (startedNewParam) { - continue // Handled start of param, move to next char. - } - - // Check if closing the current tool use. - const toolCloseTag = `` - - if ( - currentCharIndex >= toolCloseTag.length - 1 && - assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1) - ) { - // End of the tool use found. - // Special handling for content params *before* finalizing the - // tool. - const toolContentSlice = assistantMessage.slice( - currentToolUseStart, // From after the tool opening tag. - currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag. - ) - - // Check if content parameter needs special handling - // (write_to_file/new_rule). - // This check is important if the closing tag was - // missed by the parameter parsing logic (e.g., if content is - // empty or parsing logic prioritizes tool close). - const contentParamName: ToolParamName = "content" - if ( - currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ && - // !(contentParamName in currentToolUse.params) && // Only if not already parsed. - toolContentSlice.includes(`<${contentParamName}>`) // Check if tag exists. - ) { - const contentStartTag = `<${contentParamName}>` - const contentEndTag = `` - const contentStart = toolContentSlice.indexOf(contentStartTag) - - // Use `lastIndexOf` for robustness against nested tags. - const contentEnd = toolContentSlice.lastIndexOf(contentEndTag) - - if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) { - // Don't trim content to preserve newlines, but strip first and last newline only - const contentValue = toolContentSlice - .slice(contentStart + contentStartTag.length, contentEnd) - .replace(/^\n/, "") - .replace(/\n$/, "") - currentToolUse.params[contentParamName] = contentValue - } - } - - currentToolUse.partial = false // Mark as complete. - contentBlocks.push(currentToolUse) - currentToolUse = undefined // Reset state. - currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag. - continue // Move to next char. - } - - // If not starting a param and not closing the tool, continue - // accumulating tool content implicitly. - continue - } - - // Parsing text / looking for tool start. - if (!currentToolUse) { - // Check if starting a new tool use. - let startedNewTool = false - - for (const [tag, toolName] of toolUseOpenTags.entries()) { - if ( - currentCharIndex >= tag.length - 1 && - assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1) - ) { - // End current text block if one was active. - if (currentTextContent) { - currentTextContent.content = assistantMessage - .slice( - currentTextContentStart, // From where text started. - currentCharIndex - tag.length + 1, // To before the tool tag starts. - ) - .trim() - - currentTextContent.partial = false // Ended because tool started. - - if (currentTextContent.content.length > 0) { - contentBlocks.push(currentTextContent) - } - - currentTextContent = undefined - } else { - // Check for any text between the last block and this tag. - const potentialText = assistantMessage - .slice( - currentTextContentStart, // From where text *might* have started. - currentCharIndex - tag.length + 1, // To before the tool tag starts. - ) - .trim() - - if (potentialText.length > 0) { - contentBlocks.push({ - type: "text", - content: potentialText, - partial: false, - }) - } - } - - // Start the new tool use. - currentToolUse = { - type: "tool_use", - name: toolName, - params: {}, - partial: true, // Assume partial until closing tag is found. - } - - currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag. - startedNewTool = true - - break - } - } - - if (startedNewTool) { - continue // Handled start of tool, move to next char. - } - - // If not starting a tool, it must be text content. - if (!currentTextContent) { - // Start a new text block if we aren't already in one. - currentTextContentStart = currentCharIndex // Text starts at the current character. - - // Check if the current char is the start of potential text *immediately* after a tag. - // This needs the previous state - simpler to let slicing handle it later. - // Resetting start index accurately is key. - // It should be the index *after* the last processed tag. - // The logic managing currentTextContentStart after closing tags handles this. - currentTextContent = { - type: "text", - content: "", // Will be determined by slicing at the end or when a tool starts - partial: true, - } - } - // Continue accumulating text implicitly; content is extracted later. - } - } - - // Finalize any open parameter within an open tool use. - if (currentToolUse && currentParamName) { - const value = assistantMessage.slice(currentParamValueStart) // From param start to end of string. - // Don't trim content parameters to preserve newlines, but strip first and last newline only - currentToolUse.params[currentParamName] = - currentParamName === "content" ? value.replace(/^\n/, "").replace(/\n$/, "") : value.trim() - // Tool use remains partial. - } - - // Finalize any open tool use (which might contain the finalized partial param). - if (currentToolUse) { - // Tool use is partial because the loop finished before its closing tag. - contentBlocks.push(currentToolUse) - } - // Finalize any trailing text content. - // Only possible if a tool use wasn't open at the very end. - else if (currentTextContent) { - currentTextContent.content = assistantMessage - .slice(currentTextContentStart) // From text start to end of string. - .trim() - - // Text is partial because the loop finished. - if (currentTextContent.content.length > 0) { - contentBlocks.push(currentTextContent) - } - } - - return contentBlocks -} diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index c388c1a537..fb08d2dee8 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -4,7 +4,6 @@ import * as fs from "fs/promises" import * as os from "os" import * as yaml from "yaml" -import stripBom from "strip-bom" import { type ModeConfig, type PromptComponent, customModesSettingsSchema, modeConfigSchema } from "@roo-code/types" @@ -14,7 +13,6 @@ import { getGlobalRooDirectory } from "../../services/roo-config" import { logger } from "../../utils/logging" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" -import { t } from "../../i18n" const ROOMODES_FILENAME = ".roomodes" @@ -102,107 +100,12 @@ export class CustomModesManager { return exists ? roomodesPath : undefined } - /** - * Regex pattern for problematic characters that need to be cleaned from YAML content - * Includes: - * - \u00A0: Non-breaking space - * - \u200B-\u200D: Zero-width spaces and joiners - * - \u2010-\u2015, \u2212: Various dash characters - * - \u2018-\u2019: Smart single quotes - * - \u201C-\u201D: Smart double quotes - */ - private static readonly PROBLEMATIC_CHARS_REGEX = - // eslint-disable-next-line no-misleading-character-class - /[\u00A0\u200B\u200C\u200D\u2010\u2011\u2012\u2013\u2014\u2015\u2212\u2018\u2019\u201C\u201D]/g - - /** - * Clean invisible and problematic characters from YAML content - */ - private cleanInvisibleCharacters(content: string): string { - // Single pass replacement for all problematic characters - return content.replace(CustomModesManager.PROBLEMATIC_CHARS_REGEX, (match) => { - switch (match) { - case "\u00A0": // Non-breaking space - return " " - case "\u200B": // Zero-width space - case "\u200C": // Zero-width non-joiner - case "\u200D": // Zero-width joiner - return "" - case "\u2018": // Left single quotation mark - case "\u2019": // Right single quotation mark - return "'" - case "\u201C": // Left double quotation mark - case "\u201D": // Right double quotation mark - return '"' - default: // Dash characters (U+2010 through U+2015, U+2212) - return "-" - } - }) - } - - /** - * Parse YAML content with enhanced error handling and preprocessing - */ - private parseYamlSafely(content: string, filePath: string): any { - // Clean the content - let cleanedContent = stripBom(content) - cleanedContent = this.cleanInvisibleCharacters(cleanedContent) - - try { - const parsed = yaml.parse(cleanedContent) - // Ensure we never return null or undefined - return parsed ?? {} - } catch (yamlError) { - // For .roomodes files, try JSON as fallback - if (filePath.endsWith(ROOMODES_FILENAME)) { - try { - // Try parsing the original content as JSON (not the cleaned content) - return JSON.parse(content) - } catch (jsonError) { - // JSON also failed, show the original YAML error - const errorMsg = yamlError instanceof Error ? yamlError.message : String(yamlError) - console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, errorMsg) - - const lineMatch = errorMsg.match(/at line (\d+)/) - const line = lineMatch ? lineMatch[1] : "unknown" - vscode.window.showErrorMessage(t("common:customModes.errors.yamlParseError", { line })) - - // Return empty object to prevent duplicate error handling - return {} - } - } - - // For non-.roomodes files, just log and return empty object - const errorMsg = yamlError instanceof Error ? yamlError.message : String(yamlError) - console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, errorMsg) - return {} - } - } - private async loadModesFromFile(filePath: string): Promise { try { const content = await fs.readFile(filePath, "utf-8") - const settings = this.parseYamlSafely(content, filePath) - - // Ensure settings has customModes property - if (!settings || typeof settings !== "object" || !settings.customModes) { - return [] - } - + const settings = yaml.parse(content) const result = customModesSettingsSchema.safeParse(settings) - if (!result.success) { - console.error(`[CustomModesManager] Schema validation failed for ${filePath}:`, result.error) - - // Show user-friendly error for .roomodes files - if (filePath.endsWith(ROOMODES_FILENAME)) { - const issues = result.error.issues - .map((issue) => `• ${issue.path.join(".")}: ${issue.message}`) - .join("\n") - - vscode.window.showErrorMessage(t("common:customModes.errors.schemaValidationError", { issues })) - } - return [] } @@ -213,11 +116,8 @@ export class CustomModesManager { // Add source to each mode return result.data.customModes.map((mode) => ({ ...mode, source })) } catch (error) { - // Only log if the error wasn't already handled in parseYamlSafely - if (!(error as any).alreadyHandled) { - const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}` - console.error(`[CustomModesManager] ${errorMsg}`) - } + const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}` + console.error(`[CustomModesManager] ${errorMsg}`) return [] } } @@ -251,7 +151,7 @@ export class CustomModesManager { const fileExists = await fileExistsAtPath(filePath) if (!fileExists) { - await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] }, { lineWidth: 0 }))) + await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] }))) } return filePath @@ -274,12 +174,13 @@ export class CustomModesManager { await this.getCustomModesFilePath() const content = await fs.readFile(settingsPath, "utf-8") - const errorMessage = t("common:customModes.errors.invalidFormat") + const errorMessage = + "Invalid custom modes format. Please ensure your settings follow the correct YAML format." let config: any try { - config = this.parseYamlSafely(content, settingsPath) + config = yaml.parse(content) } catch (error) { console.error(error) vscode.window.showErrorMessage(errorMessage) @@ -418,7 +319,7 @@ export class CustomModesManager { if (!workspaceFolders || workspaceFolders.length === 0) { logger.error("Failed to update project mode: No workspace folder found", { slug }) - throw new Error(t("common:customModes.errors.noWorkspaceForProject")) + throw new Error("No workspace folder found for project-specific mode") } const workspaceRoot = getWorkspacePath() @@ -452,7 +353,7 @@ export class CustomModesManager { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logger.error("Failed to update custom mode", { slug, error: errorMessage }) - vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage })) + vscode.window.showErrorMessage(`Failed to update custom mode: ${errorMessage}`) } } @@ -463,28 +364,20 @@ export class CustomModesManager { content = await fs.readFile(filePath, "utf-8") } catch (error) { // File might not exist yet. - content = yaml.stringify({ customModes: [] }, { lineWidth: 0 }) + content = yaml.stringify({ customModes: [] }) } let settings try { - settings = this.parseYamlSafely(content, filePath) + settings = yaml.parse(content) } catch (error) { - // Error already logged in parseYamlSafely + console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, error) settings = { customModes: [] } } - // Ensure settings is an object and has customModes property - if (!settings || typeof settings !== "object") { - settings = { customModes: [] } - } - if (!settings.customModes) { - settings.customModes = [] - } - - settings.customModes = operation(settings.customModes) - await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0 }), "utf-8") + settings.customModes = operation(settings.customModes || []) + await fs.writeFile(filePath, yaml.stringify(settings), "utf-8") } private async refreshMergedState(): Promise { @@ -515,7 +408,7 @@ export class CustomModesManager { const globalMode = settingsModes.find((m) => m.slug === slug) if (!projectMode && !globalMode) { - throw new Error(t("common:customModes.errors.modeNotFound")) + throw new Error("Write error: Mode not found") } // Determine which mode to use for rules folder path calculation @@ -542,8 +435,9 @@ export class CustomModesManager { await this.refreshMergedState() }) } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage })) + vscode.window.showErrorMessage( + `Failed to delete custom mode: ${error instanceof Error ? error.message : String(error)}`, + ) } } @@ -581,10 +475,10 @@ export class CustomModesManager { } catch (error) { logger.error(`Failed to delete rules folder for mode ${slug}: ${error}`) // Notify the user about the failure - const messageKey = fromMarketplace - ? "common:marketplace.mode.rulesCleanupFailed" - : "common:customModes.errors.rulesCleanupFailed" - vscode.window.showWarningMessage(t(messageKey, { rulesFolderPath })) + const message = fromMarketplace + ? `Failed to clean up marketplace mode rules folder: ${rulesFolderPath}` + : `Failed to clean up rules folder: ${rulesFolderPath}` + vscode.window.showWarningMessage(message) // Continue even if folder deletion fails } } @@ -598,13 +492,14 @@ export class CustomModesManager { public async resetCustomModes(): Promise { try { const filePath = await this.getCustomModesFilePath() - await fs.writeFile(filePath, yaml.stringify({ customModes: [] }, { lineWidth: 0 })) + await fs.writeFile(filePath, yaml.stringify({ customModes: [] })) await this.context.globalState.update("customModes", []) this.clearCache() await this.onUpdate() } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - vscode.window.showErrorMessage(t("common:customModes.errors.resetFailed", { error: errorMessage })) + vscode.window.showErrorMessage( + `Failed to reset custom modes: ${error instanceof Error ? error.message : String(error)}`, + ) } } diff --git a/src/core/config/__tests__/CustomModesManager.spec.ts b/src/core/config/__tests__/CustomModesManager.spec.ts index 682696fd03..4849708b80 100644 --- a/src/core/config/__tests__/CustomModesManager.spec.ts +++ b/src/core/config/__tests__/CustomModesManager.spec.ts @@ -767,7 +767,7 @@ describe("CustomModesManager", () => { await manager.deleteCustomMode("non-existent-mode") - expect(mockShowError).toHaveBeenCalledWith("customModes.errors.deleteFailed") + expect(mockShowError).toHaveBeenCalledWith(expect.stringContaining("Write error")) }) }) diff --git a/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts b/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts deleted file mode 100644 index 251a33d211..0000000000 --- a/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts +++ /dev/null @@ -1,474 +0,0 @@ -// npx vitest core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts - -import type { Mock } from "vitest" - -import * as path from "path" -import * as fs from "fs/promises" - -import * as yaml from "yaml" -import * as vscode from "vscode" - -import type { ModeConfig } from "@roo-code/types" - -import { fileExistsAtPath } from "../../../utils/fs" -import { getWorkspacePath } from "../../../utils/path" -import { GlobalFileNames } from "../../../shared/globalFileNames" - -import { CustomModesManager } from "../CustomModesManager" - -vi.mock("vscode", () => ({ - workspace: { - workspaceFolders: [], - onDidSaveTextDocument: vi.fn(), - createFileSystemWatcher: vi.fn(), - }, - window: { - showErrorMessage: vi.fn(), - }, -})) - -vi.mock("fs/promises") - -vi.mock("../../../utils/fs") -vi.mock("../../../utils/path") - -describe("CustomModesManager - YAML Edge Cases", () => { - let manager: CustomModesManager - let mockContext: vscode.ExtensionContext - let mockOnUpdate: Mock - let mockWorkspaceFolders: { uri: { fsPath: string } }[] - - const mockStoragePath = `${path.sep}mock${path.sep}settings` - const mockSettingsPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes) - const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.roomodes` - - // Helper function to reduce duplication in fs.readFile mocks - const mockFsReadFile = (files: Record) => { - ;(fs.readFile as Mock).mockImplementation(async (path: string) => { - if (files[path]) return files[path] - throw new Error("File not found") - }) - } - - beforeEach(() => { - mockOnUpdate = vi.fn() - mockContext = { - globalState: { - get: vi.fn(), - update: vi.fn(), - keys: vi.fn(() => []), - setKeysForSync: vi.fn(), - }, - globalStorageUri: { - fsPath: mockStoragePath, - }, - } as unknown as vscode.ExtensionContext - - mockWorkspaceFolders = [{ uri: { fsPath: "/mock/workspace" } }] - ;(vscode.workspace as any).workspaceFolders = mockWorkspaceFolders - ;(vscode.workspace.onDidSaveTextDocument as Mock).mockReturnValue({ dispose: vi.fn() }) - ;(getWorkspacePath as Mock).mockReturnValue("/mock/workspace") - ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { - return path === mockSettingsPath || path === mockRoomodes - }) - ;(fs.mkdir as Mock).mockResolvedValue(undefined) - ;(fs.readFile as Mock).mockImplementation(async (path: string) => { - if (path === mockSettingsPath) { - return yaml.stringify({ customModes: [] }) - } - throw new Error("File not found") - }) - - // Mock createFileSystemWatcher to prevent file watching in tests - const mockWatcher = { - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - onDidCreate: vi.fn().mockReturnValue({ dispose: vi.fn() }), - onDidDelete: vi.fn().mockReturnValue({ dispose: vi.fn() }), - dispose: vi.fn(), - } - ;(vscode.workspace.createFileSystemWatcher as Mock).mockReturnValue(mockWatcher) - - manager = new CustomModesManager(mockContext, mockOnUpdate) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("BOM (Byte Order Mark) handling", () => { - it("should handle UTF-8 BOM in YAML files", async () => { - const yamlWithBOM = - "\uFEFF" + - yaml.stringify({ - customModes: [ - { - slug: "test-mode", - name: "Test Mode", - roleDefinition: "Test role", - groups: ["read"], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithBOM, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].slug).toBe("test-mode") - expect(modes[0].name).toBe("Test Mode") - }) - - it("should handle UTF-16 BOM in YAML files", async () => { - // When Node.js reads UTF-16 files, the BOM is correctly decoded as \uFEFF - const yamlWithBOM = - "\uFEFF" + - yaml.stringify({ - customModes: [ - { - slug: "utf16-mode", - name: "UTF-16 Mode", - roleDefinition: "Test role", - groups: ["read"], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithBOM, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].slug).toBe("utf16-mode") - }) - }) - - describe("Invisible character handling", () => { - it("should handle non-breaking spaces in YAML", async () => { - // YAML with non-breaking spaces (U+00A0) instead of regular spaces - const yamlWithNonBreakingSpaces = `customModes: - - slug: "test-mode" - name: "Test\u00A0Mode" - roleDefinition: "Test\u00A0role\u00A0with\u00A0non-breaking\u00A0spaces" - groups: ["read"]` - - mockFsReadFile({ - [mockRoomodes]: yamlWithNonBreakingSpaces, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].name).toBe("Test Mode") // Non-breaking spaces replaced with regular spaces - expect(modes[0].roleDefinition).toBe("Test role with non-breaking spaces") - }) - - it("should handle zero-width characters", async () => { - // YAML with zero-width characters - const yamlWithZeroWidth = `customModes: - - slug: "test-mode" - name: "Test\u200BMode\u200C" - roleDefinition: "Test\u200Drole" - groups: ["read"]` - - mockFsReadFile({ - [mockRoomodes]: yamlWithZeroWidth, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].name).toBe("TestMode") // Zero-width characters removed - expect(modes[0].roleDefinition).toBe("Testrole") - }) - - it("should normalize various quote characters", async () => { - // Use fancy quotes that will be normalized before YAML parsing - // The fancy quotes will be normalized to standard quotes - const yamlWithFancyQuotes = yaml.stringify({ - customModes: [ - { - slug: "test-mode", - name: "Test Mode", - roleDefinition: "Test role with \u2018fancy\u2019 quotes and \u201Ccurly\u201D quotes", - groups: ["read"], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithFancyQuotes, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].roleDefinition).toBe("Test role with 'fancy' quotes and \"curly\" quotes") - }) - }) - - // Note: YAML anchor/alias support has been removed to reduce complexity - // If needed in the future, users should pre-process their YAML files - - describe("Complex fileRegex handling", () => { - it("should handle complex fileRegex syntax gracefully", async () => { - const yamlWithComplexFileRegex = yaml.stringify({ - customModes: [ - { - slug: "test-mode", - name: "Test Mode", - roleDefinition: "Test role", - groups: [ - "read", - ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], - "browser", - ], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithComplexFileRegex, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - // Should successfully parse the complex fileRegex syntax - expect(modes).toHaveLength(1) - expect(modes[0].groups).toHaveLength(3) - expect(modes[0].groups[1]).toEqual(["edit", { fileRegex: "\\.md$", description: "Markdown files only" }]) - }) - - it("should handle invalid fileRegex syntax with clear error", async () => { - // This YAML has invalid structure that might cause parsing issues - const invalidYaml = `customModes: - - slug: "test-mode" - name: "Test Mode" - roleDefinition: "Test role" - groups: - - read - - ["edit", { fileRegex: "\\.md$" }] # This line has invalid YAML syntax - - browser` - - mockFsReadFile({ - [mockRoomodes]: invalidYaml, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - // Should handle the error gracefully - expect(modes).toHaveLength(0) - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.yamlParseError") - }) - }) - - describe("Error messages", () => { - it("should provide detailed syntax error messages with context", async () => { - const invalidYaml = `customModes: - - slug: "test-mode" - name: "Test Mode" - roleDefinition: "Test role - groups: ["read"]` // Missing closing quote - - mockFsReadFile({ - [mockRoomodes]: invalidYaml, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - // Should fallback to empty array and show detailed error - expect(modes).toHaveLength(0) - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.yamlParseError") - }) - - it("should provide schema validation error messages", async () => { - const invalidSchema = yaml.stringify({ - customModes: [ - { - slug: "test-mode", - name: "Test Mode", - // Missing required 'roleDefinition' field - groups: ["read"], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: invalidSchema, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - // Should show schema validation error - expect(modes).toHaveLength(0) - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.schemaValidationError") - }) - }) - - describe("UTF-8 encoding", () => { - it("should handle special characters and emojis", async () => { - const yamlWithEmojis = yaml.stringify({ - customModes: [ - { - slug: "emoji-mode", - name: "📝 Writing Mode", - roleDefinition: "A mode for writing with emojis 🚀", - groups: ["read", "edit"], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithEmojis, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].name).toBe("📝 Writing Mode") - expect(modes[0].roleDefinition).toBe("A mode for writing with emojis 🚀") - }) - - it("should handle various international characters", async () => { - const yamlWithInternational = yaml.stringify({ - customModes: [ - { - slug: "intl-mode", - name: "Mode Français", - roleDefinition: "Mode für Deutsch, 日本語モード, Режим русский", - groups: ["read"], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithInternational, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].roleDefinition).toContain("für Deutsch") - expect(modes[0].roleDefinition).toContain("日本語モード") - expect(modes[0].roleDefinition).toContain("Режим русский") - }) - }) - - describe("Additional edge cases", () => { - it("should handle mixed line endings (CRLF vs LF)", async () => { - // YAML with mixed line endings - const yamlWithMixedLineEndings = - "customModes:\r\n" + - ' - slug: "test-mode"\n' + - ' name: "Test Mode"\r\n' + - ' roleDefinition: "Test role with mixed line endings"\n' + - ' groups: ["read"]' - - mockFsReadFile({ - [mockRoomodes]: yamlWithMixedLineEndings, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].slug).toBe("test-mode") - expect(modes[0].roleDefinition).toBe("Test role with mixed line endings") - }) - - it("should handle multiple BOMs in sequence", async () => { - // File with multiple BOMs (edge case from file concatenation) - const yamlWithMultipleBOMs = - "\uFEFF\uFEFF" + - yaml.stringify({ - customModes: [ - { - slug: "multi-bom-mode", - name: "Multi BOM Mode", - roleDefinition: "Test role", - groups: ["read"], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithMultipleBOMs, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].slug).toBe("multi-bom-mode") - }) - - it("should handle deeply nested structures with edge case characters", async () => { - const yamlWithComplexNesting = yaml.stringify({ - customModes: [ - { - slug: "complex-mode", - name: "Complex\u00A0Mode\u2019s Name", - roleDefinition: "Complex role with \u201Cquotes\u201D and \u2014dashes\u2014", - groups: [ - "read", - [ - "edit", - { - fileRegex: "\\.md$", - description: "Markdown files with \u2018special\u2019 chars", - }, - ], - [ - "browser", - { - fileRegex: "\\.html?$", - description: "HTML files\u00A0only", - }, - ], - ], - }, - ], - }) - - mockFsReadFile({ - [mockRoomodes]: yamlWithComplexNesting, - [mockSettingsPath]: yaml.stringify({ customModes: [] }), - }) - - const modes = await manager.getCustomModes() - - expect(modes).toHaveLength(1) - expect(modes[0].name).toBe("Complex Mode's Name") - expect(modes[0].roleDefinition).toBe('Complex role with "quotes" and -dashes-') - expect(modes[0].groups[1]).toEqual([ - "edit", - { - fileRegex: "\\.md$", - description: "Markdown files with 'special' chars", - }, - ]) - expect(modes[0].groups[2]).toEqual([ - "browser", - { - fileRegex: "\\.html?$", - description: "HTML files only", - }, - ]) - }) - }) -}) diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts index 361d6b23b0..e95c00e9af 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/src/core/config/__tests__/importExport.spec.ts @@ -8,11 +8,10 @@ import * as vscode from "vscode" import type { ProviderName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { importSettings, importSettingsFromFile, importSettingsWithFeedback, exportSettings } from "../importExport" +import { importSettings, exportSettings } from "../importExport" import { ProviderSettingsManager } from "../ProviderSettingsManager" import { ContextProxy } from "../ContextProxy" import { CustomModesManager } from "../CustomModesManager" -import { safeWriteJson } from "../../../utils/safeWriteJson" import type { Mock } from "vitest" @@ -20,8 +19,6 @@ vi.mock("vscode", () => ({ window: { showOpenDialog: vi.fn(), showSaveDialog: vi.fn(), - showErrorMessage: vi.fn(), - showInformationMessage: vi.fn(), }, Uri: { file: vi.fn((filePath) => ({ fsPath: filePath })), @@ -33,20 +30,10 @@ vi.mock("fs/promises", () => ({ readFile: vi.fn(), mkdir: vi.fn(), writeFile: vi.fn(), - access: vi.fn(), - constants: { - F_OK: 0, - R_OK: 4, - }, }, readFile: vi.fn(), mkdir: vi.fn(), writeFile: vi.fn(), - access: vi.fn(), - constants: { - F_OK: 0, - R_OK: 4, - }, })) vi.mock("os", () => ({ @@ -56,8 +43,6 @@ vi.mock("os", () => ({ homedir: vi.fn(() => "/mock/home"), })) -vi.mock("../../../utils/safeWriteJson") - describe("importExport", () => { let mockProviderSettingsManager: ReturnType> let mockContextProxy: ReturnType> @@ -108,7 +93,7 @@ describe("importExport", () => { customModesManager: mockCustomModesManager, }) - expect(result).toEqual({ success: false, error: "User cancelled file selection" }) + expect(result).toEqual({ success: false }) expect(vscode.window.showOpenDialog).toHaveBeenCalledWith({ filters: { JSON: ["json"] }, @@ -158,12 +143,9 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ + ...previousProviderProfiles, currentApiConfigName: "test", - apiConfigs: { - default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, - test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, - }, - modeApiConfigs: {}, + apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } }, }) expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true }) @@ -234,12 +216,11 @@ describe("importExport", () => { expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8") expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ + ...previousProviderProfiles, currentApiConfigName: "test", apiConfigs: { - default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, }, - modeApiConfigs: {}, }) // Should call setValues with an empty object since globalSettings is missing. @@ -313,11 +294,9 @@ describe("importExport", () => { }) expect(result.success).toBe(true) - if (result.success && "providerProfiles" in result) { - expect(result.providerProfiles?.apiConfigs["openai"]).toBeDefined() - expect(result.providerProfiles?.apiConfigs["default"]).toBeDefined() - expect(result.providerProfiles?.apiConfigs["default"].apiProvider).toBe("anthropic") - } + expect(result.providerProfiles?.apiConfigs["openai"]).toBeDefined() + expect(result.providerProfiles?.apiConfigs["default"]).toBeDefined() + expect(result.providerProfiles?.apiConfigs["default"].apiProvider).toBe("anthropic") }) it("should call updateCustomMode for each custom mode in config", async () => { @@ -355,87 +334,6 @@ describe("importExport", () => { expect(mockCustomModesManager.updateCustomMode).toHaveBeenCalledWith(mode.slug, mode) }) }) - - it("should import settings from provided file path without showing dialog", async () => { - const filePath = "/mock/path/settings.json" - const mockFileContent = JSON.stringify({ - providerProfiles: { - currentApiConfigName: "test", - apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } }, - }, - globalSettings: { mode: "code", autoApprovalEnabled: true }, - }) - - ;(fs.readFile as Mock).mockResolvedValue(mockFileContent) - ;(fs.access as Mock).mockResolvedValue(undefined) // File exists and is readable - - const previousProviderProfiles = { - currentApiConfigName: "default", - apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, - } - - mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles) - mockProviderSettingsManager.listConfig.mockResolvedValue([ - { name: "test", id: "test-id", apiProvider: "openai" as ProviderName }, - { name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName }, - ]) - mockContextProxy.export.mockResolvedValue({ mode: "code" }) - - const result = await importSettingsFromFile( - { - providerSettingsManager: mockProviderSettingsManager, - contextProxy: mockContextProxy, - customModesManager: mockCustomModesManager, - }, - vscode.Uri.file(filePath), - ) - - expect(vscode.window.showOpenDialog).not.toHaveBeenCalled() - expect(fs.readFile).toHaveBeenCalledWith(filePath, "utf-8") - expect(result.success).toBe(true) - expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ - currentApiConfigName: "test", - apiConfigs: { - default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, - test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, - }, - modeApiConfigs: {}, - }) - expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true }) - }) - - it("should return error when provided file path does not exist", async () => { - const filePath = "/nonexistent/path/settings.json" - const accessError = new Error("ENOENT: no such file or directory") - - ;(fs.access as Mock).mockRejectedValue(accessError) - - // Create a mock provider for the test - const mockProvider = { - settingsImportedAt: 0, - postStateToWebview: vi.fn().mockResolvedValue(undefined), - } - - // Mock the showErrorMessage to capture the error - const showErrorMessageSpy = vi.spyOn(vscode.window, "showErrorMessage").mockResolvedValue(undefined) - - await importSettingsWithFeedback( - { - providerSettingsManager: mockProviderSettingsManager, - contextProxy: mockContextProxy, - customModesManager: mockCustomModesManager, - provider: mockProvider, - }, - filePath, - ) - - expect(vscode.window.showOpenDialog).not.toHaveBeenCalled() - expect(fs.access).toHaveBeenCalledWith(filePath, fs.constants.F_OK | fs.constants.R_OK) - expect(fs.readFile).not.toHaveBeenCalled() - expect(showErrorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("errors.settings_import_failed")) - - showErrorMessageSpy.mockRestore() - }) }) describe("exportSettings", () => { @@ -486,10 +384,11 @@ describe("importExport", () => { expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true }) - expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { - providerProfiles: mockProviderProfiles, - globalSettings: mockGlobalSettings, - }) + expect(fs.writeFile).toHaveBeenCalledWith( + "/mock/path/roo-code-settings.json", + JSON.stringify({ providerProfiles: mockProviderProfiles, globalSettings: mockGlobalSettings }, null, 2), + "utf-8", + ) }) it("should include globalSettings when allowedMaxRequests is null", async () => { @@ -518,10 +417,11 @@ describe("importExport", () => { contextProxy: mockContextProxy, }) - expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { - providerProfiles: mockProviderProfiles, - globalSettings: mockGlobalSettings, - }) + expect(fs.writeFile).toHaveBeenCalledWith( + "/mock/path/roo-code-settings.json", + JSON.stringify({ providerProfiles: mockProviderProfiles, globalSettings: mockGlobalSettings }, null, 2), + "utf-8", + ) }) it("should handle errors during the export process", async () => { @@ -536,8 +436,7 @@ describe("importExport", () => { }) mockContextProxy.export.mockResolvedValue({ mode: "code" }) - // Simulate an error during the safeWriteJson operation - ;(safeWriteJson as Mock).mockRejectedValueOnce(new Error("Safe write error")) + ;(fs.writeFile as Mock).mockRejectedValue(new Error("Write error")) await exportSettings({ providerSettingsManager: mockProviderSettingsManager, @@ -548,10 +447,8 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true }) - expect(safeWriteJson).toHaveBeenCalled() // safeWriteJson is called, but it will throw + expect(fs.writeFile).toHaveBeenCalled() // The error is caught and the function exits silently. - // Optionally, ensure no error message was shown if that's part of "silent" - // expect(vscode.window.showErrorMessage).not.toHaveBeenCalled(); }) it("should handle errors during directory creation", async () => { @@ -577,7 +474,7 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalled() - expect(safeWriteJson).not.toHaveBeenCalled() // Should not be called since mkdir failed. + expect(fs.writeFile).not.toHaveBeenCalled() // Should not be called since mkdir failed. }) it("should use the correct default save location", async () => { diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index c3d6f9c215..4830a5f987 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -1,4 +1,3 @@ -import { safeWriteJson } from "../../utils/safeWriteJson" import os from "os" import * as path from "path" import fs from "fs/promises" @@ -12,9 +11,8 @@ import { TelemetryService } from "@roo-code/telemetry" import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager" import { ContextProxy } from "./ContextProxy" import { CustomModesManager } from "./CustomModesManager" -import { t } from "../../i18n" -export type ImportOptions = { +type ImportOptions = { providerSettingsManager: ProviderSettingsManager contextProxy: ContextProxy customModesManager: CustomModesManager @@ -24,22 +22,17 @@ type ExportOptions = { providerSettingsManager: ProviderSettingsManager contextProxy: ContextProxy } -type ImportWithProviderOptions = ImportOptions & { - provider: { - settingsImportedAt?: number - postStateToWebview: () => Promise - } -} -/** - * Imports configuration from a specific file path - * Shares base functionality for import settings for both the manual - * and automatic settings importing - */ -export async function importSettingsFromPath( - filePath: string, - { providerSettingsManager, contextProxy, customModesManager }: ImportOptions, -) { +export const importSettings = async ({ providerSettingsManager, contextProxy, customModesManager }: ImportOptions) => { + const uris = await vscode.window.showOpenDialog({ + filters: { JSON: ["json"] }, + canSelectMany: false, + }) + + if (!uris) { + return { success: false } + } + const schema = z.object({ providerProfiles: providerProfilesSchema, globalSettings: globalSettingsSchema.optional(), @@ -48,9 +41,8 @@ export async function importSettingsFromPath( try { const previousProviderProfiles = await providerSettingsManager.export() - const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse( - JSON.parse(await fs.readFile(filePath, "utf-8")), - ) + const data = JSON.parse(await fs.readFile(uris[0].fsPath, "utf-8")) + const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse(data) const providerProfiles = { currentApiConfigName: newProviderProfiles.currentApiConfigName, @@ -68,10 +60,7 @@ export async function importSettingsFromPath( (globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)), ) - // OpenAI Compatible settings are now correctly stored in codebaseIndexConfig - // They will be imported automatically with the config - no special handling needed - - await providerSettingsManager.import(providerProfiles) + await providerSettingsManager.import(newProviderProfiles) await contextProxy.setValues(globalSettings) // Set the current provider. @@ -103,45 +92,6 @@ export async function importSettingsFromPath( } } -/** - * Import settings from a file using a file dialog - * @param options - Import options containing managers and proxy - * @returns Promise resolving to import result - */ -export const importSettings = async ({ providerSettingsManager, contextProxy, customModesManager }: ImportOptions) => { - const uris = await vscode.window.showOpenDialog({ - filters: { JSON: ["json"] }, - canSelectMany: false, - }) - - if (!uris) { - return { success: false, error: "User cancelled file selection" } - } - - return importSettingsFromPath(uris[0].fsPath, { - providerSettingsManager, - contextProxy, - customModesManager, - }) -} - -/** - * Import settings from a specific file - * @param options - Import options containing managers and proxy - * @param fileUri - URI of the file to import from - * @returns Promise resolving to import result - */ -export const importSettingsFromFile = async ( - { providerSettingsManager, contextProxy, customModesManager }: ImportOptions, - fileUri: vscode.Uri, -) => { - return importSettingsFromPath(fileUri.fsPath, { - providerSettingsManager, - contextProxy, - customModesManager, - }) -} - export const exportSettings = async ({ providerSettingsManager, contextProxy }: ExportOptions) => { const uri = await vscode.window.showSaveDialog({ filters: { JSON: ["json"] }, @@ -164,55 +114,8 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }: return } - // OpenAI Compatible settings are now correctly stored in codebaseIndexConfig - // No workaround needed - they will be exported automatically with the config - const dirname = path.dirname(uri.fsPath) await fs.mkdir(dirname, { recursive: true }) - await safeWriteJson(uri.fsPath, { providerProfiles, globalSettings }) - } catch (e) { - console.error("Failed to export settings:", e) - // Don't re-throw - the UI will handle showing error messages - } -} - -/** - * Import settings with complete UI feedback and provider state updates - * @param options - Import options with provider instance - * @param filePath - Optional file path to import from. If not provided, a file dialog will be shown. - * @returns Promise that resolves when import is complete - */ -export const importSettingsWithFeedback = async ( - { providerSettingsManager, contextProxy, customModesManager, provider }: ImportWithProviderOptions, - filePath?: string, -) => { - let result - - if (filePath) { - // Validate file path and check if file exists - try { - // Check if file exists and is readable - await fs.access(filePath, fs.constants.F_OK | fs.constants.R_OK) - result = await importSettingsFromPath(filePath, { - providerSettingsManager, - contextProxy, - customModesManager, - }) - } catch (error) { - result = { - success: false, - error: `Cannot access file at path "${filePath}": ${error instanceof Error ? error.message : "Unknown error"}`, - } - } - } else { - result = await importSettings({ providerSettingsManager, contextProxy, customModesManager }) - } - - if (result.success) { - provider.settingsImportedAt = Date.now() - await provider.postStateToWebview() - await vscode.window.showInformationMessage(t("common:info.settings_imported")) - } else if (result.error) { - await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error })) - } + await fs.writeFile(uri.fsPath, JSON.stringify({ providerProfiles, globalSettings }, null, 2), "utf-8") + } catch (e) {} } diff --git a/src/core/context-tracking/FileContextTracker.ts b/src/core/context-tracking/FileContextTracker.ts index 5741b62cfc..323bb4122f 100644 --- a/src/core/context-tracking/FileContextTracker.ts +++ b/src/core/context-tracking/FileContextTracker.ts @@ -1,4 +1,3 @@ -import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as vscode from "vscode" import { getTaskDirectoryPath } from "../../utils/storage" @@ -131,7 +130,7 @@ export class FileContextTracker { const globalStoragePath = this.getContextProxy()!.globalStorageUri.fsPath const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.taskMetadata) - await safeWriteJson(filePath, metadata) + await fs.writeFile(filePath, JSON.stringify(metadata, null, 2)) } catch (error) { console.error("Failed to save task metadata:", error) } diff --git a/src/core/diff/strategies/multi-file-search-replace.ts b/src/core/diff/strategies/multi-file-search-replace.ts index d875d723a1..3618722a1b 100644 --- a/src/core/diff/strategies/multi-file-search-replace.ts +++ b/src/core/diff/strategies/multi-file-search-replace.ts @@ -2,7 +2,8 @@ import { distance } from "fastest-levenshtein" import { ToolProgressStatus } from "@roo-code/types" import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" -import { ToolUse, DiffStrategy, DiffResult } from "../../../shared/tools" +import { DiffStrategy, DiffResult } from "../../../shared/tools" +import { ToolDirective } from "../../message-parsing/directives/" import { normalizeString } from "../../../utils/text-normalization" const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches @@ -487,7 +488,7 @@ Each file requires its own path, start_line, and diff elements. const replacements = matches .map((match) => ({ - startLine: _paramStartLine ?? Number(match[2] ?? 0), + startLine: Number(match[2] ?? 0), searchContent: match[6], replaceContent: match[7], })) @@ -715,7 +716,7 @@ Each file requires its own path, start_line, and diff elements. } } - getProgressStatus(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus { + getProgressStatus(toolUse: ToolDirective, result?: DiffResult): ToolProgressStatus { const diffContent = toolUse.params.diff if (diffContent) { const icon = "diff-multiple" diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index b90ef4072d..3bfe914678 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -5,7 +5,8 @@ import { distance } from "fastest-levenshtein" import { ToolProgressStatus } from "@roo-code/types" import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" -import { ToolUse, DiffStrategy, DiffResult } from "../../../shared/tools" +import { DiffStrategy, DiffResult } from "../../../shared/tools" +import { ToolDirective } from "../../message-parsing/directives/" import { normalizeString } from "../../../utils/text-normalization" const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches @@ -609,11 +610,11 @@ Only use a single line of '=======' between search and replacement content, beca } } - getProgressStatus(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus { - const diffContent = toolUse.params.diff + getProgressStatus(ToolDirective: ToolDirective, result?: DiffResult): ToolProgressStatus { + const diffContent = ToolDirective.params.diff if (diffContent) { const icon = "diff-multiple" - if (toolUse.partial) { + if (ToolDirective.partial) { if (Math.floor(diffContent.length / 10) % 10 === 0) { const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length return { icon, text: `${searchBlockCount}` } diff --git a/src/core/message-parsing/CodeBlockStateMachine.ts b/src/core/message-parsing/CodeBlockStateMachine.ts new file mode 100644 index 0000000000..4fab47c65c --- /dev/null +++ b/src/core/message-parsing/CodeBlockStateMachine.ts @@ -0,0 +1,61 @@ +import { ParseContext, CodeBlockState } from "./ParseContext" + +export interface ProcessedTextResult { + processedText: string + suppressXmlParsing: boolean + stateChanged: boolean + nextIndex: number +} + +export interface CodeBlockBoundary { + found: boolean + endIndex: number + isComplete: boolean +} + +export class CodeBlockStateMachine { + /** + * Process incoming text and manage code block state transitions + */ + processText(text: string, context: ParseContext): ProcessedTextResult { + // Simple approach: scan for ``` patterns and track state + let result = "" + let i = 0 + let stateChanged = false + + while (i < text.length) { + // Check for ``` pattern at current position + if (this.isCodeBlockBoundary(text, i)) { + // Found ``` - toggle state + if (context.codeBlockState === CodeBlockState.OUTSIDE) { + context.codeBlockState = CodeBlockState.INSIDE + stateChanged = true + } else if (context.codeBlockState === CodeBlockState.INSIDE) { + context.codeBlockState = CodeBlockState.OUTSIDE + stateChanged = true + } + // Include the ``` in the result + result += "```" + i += 3 + } else { + // Regular character + result += text[i] + i++ + } + } + + return { + processedText: result, + suppressXmlParsing: context.codeBlockState === CodeBlockState.INSIDE, + stateChanged, + nextIndex: i, + } + } + + /** + * Check if there's a ``` pattern at the given position + */ + private isCodeBlockBoundary(text: string, pos: number): boolean { + return pos <= text.length - 3 && text[pos] === "`" && text[pos + 1] === "`" && text[pos + 2] === "`" + } +} diff --git a/src/core/message-parsing/DirectiveHandler.ts b/src/core/message-parsing/DirectiveHandler.ts new file mode 100644 index 0000000000..015b9c0a85 --- /dev/null +++ b/src/core/message-parsing/DirectiveHandler.ts @@ -0,0 +1,11 @@ +import * as sax from "sax" +import { ParseContext } from "./ParseContext" + +export interface DirectiveHandler { + readonly tagName: string + canHandle(tagName: string): boolean + onOpenTag(node: sax.Tag, context: ParseContext): void + onCloseTag(tagName: string, context: ParseContext): void + onText(text: string, context: ParseContext): void + onEnd(context: ParseContext): void +} diff --git a/src/core/message-parsing/DirectiveHandlerRegistry.ts b/src/core/message-parsing/DirectiveHandlerRegistry.ts new file mode 100644 index 0000000000..91aa80c3e3 --- /dev/null +++ b/src/core/message-parsing/DirectiveHandlerRegistry.ts @@ -0,0 +1,27 @@ +import { DirectiveHandler } from "./DirectiveHandler" +import { TextDirectiveHandler, ToolDirectiveHandler } from "./handlers" + +export class DirectiveHandlerRegistry { + private handlers: Map = new Map() + private textHandler = new TextDirectiveHandler() + + register(handler: DirectiveHandler): void { + this.handlers.set(handler.tagName, handler) + } + + registerTool(toolName: string): void { + this.register(new ToolDirectiveHandler(toolName)) + } + + getHandler(tagName: string): DirectiveHandler | undefined { + return this.handlers.get(tagName) + } + + getTextHandler(): TextDirectiveHandler { + return this.textHandler + } + + getAllHandlers(): DirectiveHandler[] { + return [this.textHandler, ...Array.from(this.handlers.values())] + } +} diff --git a/src/core/message-parsing/DirectiveRegistryFactory.ts b/src/core/message-parsing/DirectiveRegistryFactory.ts new file mode 100644 index 0000000000..1026fdf909 --- /dev/null +++ b/src/core/message-parsing/DirectiveRegistryFactory.ts @@ -0,0 +1,15 @@ +import { DirectiveHandlerRegistry } from "./DirectiveHandlerRegistry" +import { toolNames } from "@roo-code/types" + +export class DirectiveRegistryFactory { + static create(): DirectiveHandlerRegistry { + const registry = new DirectiveHandlerRegistry() + + // Register all tool directives + toolNames.forEach((toolName) => { + registry.registerTool(toolName) + }) + + return registry + } +} diff --git a/src/core/message-parsing/DirectiveStreamingParser.ts b/src/core/message-parsing/DirectiveStreamingParser.ts new file mode 100644 index 0000000000..9cfd874180 --- /dev/null +++ b/src/core/message-parsing/DirectiveStreamingParser.ts @@ -0,0 +1,164 @@ +import * as sax from "sax" +import { Directive } from "./directives" +import { ParseContext, CodeBlockState } from "./ParseContext" +import { DirectiveRegistryFactory } from "./DirectiveRegistryFactory" +import { FallbackParser } from "./FallbackParser" +import { XmlUtils } from "./XmlUtils" +import { DirectiveHandler } from "./DirectiveHandler" +import { ParameterCodeBlockHandler } from "./ParameterCodeBlockHandler" +import { ToolDirectiveHandler } from "./handlers" + +export class DirectiveStreamingParser { + private static registry = DirectiveRegistryFactory.create() + + static parse(assistantMessage: string): Directive[] { + const context: ParseContext = { + currentText: "", + contentBlocks: [], + hasXmlTags: false, + hasIncompleteXml: XmlUtils.hasIncompleteXml(assistantMessage), + codeBlockState: CodeBlockState.OUTSIDE, + pendingBackticks: "", + codeBlockContent: "", + codeBlockStartIndex: -1, + } + + const parser = sax.parser(false, { lowercase: true }) + let parseError = false + let tagStack: string[] = [] + let activeHandler: DirectiveHandler | null = null + + parser.onopentag = (node: sax.Tag) => { + // Check if we're inside a code block (either global or within tool parameters) + const insideCodeBlock = this.isInsideCodeBlock(context, activeHandler) + + // Check if we're inside a tool parameter (but not at the parameter level itself) + const insideToolParameter = this.isInsideToolParameter(activeHandler) + + // Only process XML tags if NOT inside code block AND NOT inside tool parameter + if (!insideCodeBlock && !insideToolParameter) { + context.hasXmlTags = true + tagStack.push(node.name) + const handler = this.registry.getHandler(node.name) + + if (handler) { + activeHandler = handler + this.registry.getTextHandler().setState("none") + } + if (activeHandler) { + activeHandler.onOpenTag(node, context) + } + } else { + // Inside code block or tool parameter - treat as plain text + const tagText = `<${node.name}${this.attributesToString(node.attributes)}>` + if (activeHandler) { + activeHandler.onText(tagText, context) + } else { + this.registry.getTextHandler().onText(tagText, context) + } + } + } + + parser.onclosetag = (tagName: string) => { + // Check if we're inside a code block (either global or within tool parameters) + const insideCodeBlock = this.isInsideCodeBlock(context, activeHandler) + + // Check if we're inside a tool parameter (but not at the parameter level itself) + const insideToolParameter = this.isInsideToolParameter(activeHandler, tagName) + + if (!insideCodeBlock && !insideToolParameter) { + // Normal XML processing + if (activeHandler) { + activeHandler.onCloseTag(tagName, context) + if (tagName === activeHandler.tagName) { + activeHandler = null + this.registry.getTextHandler().setState("text") + } + } + tagStack.pop() + } else { + // Inside code block or tool parameter - treat as plain text + if (activeHandler) { + activeHandler.onText(``, context) + } else { + this.registry.getTextHandler().onText(``, context) + } + } + } + + parser.ontext = (text: string) => { + if (activeHandler) { + activeHandler.onText(text, context) + } else { + this.registry.getTextHandler().onText(text, context) + } + } + + parser.onend = () => { + for (const handler of this.registry.getAllHandlers()) { + handler.onEnd(context) + } + } + + parser.onerror = (error: Error) => { + parseError = true + } + + try { + const wrappedMessage = `${assistantMessage}` + parser.write(wrappedMessage).close() + } catch (e) { + parseError = true + } + + if (parseError || (!context.hasXmlTags && context.contentBlocks.length === 0 && assistantMessage.trim())) { + return FallbackParser.parse(assistantMessage) + } + + return context.contentBlocks + } + + /** + * Check if we're inside a code block (either global or within tool parameters) + */ + private static isInsideCodeBlock(context: ParseContext, activeHandler: DirectiveHandler | null): boolean { + return ( + context.codeBlockState === CodeBlockState.INSIDE || + (!!activeHandler && + activeHandler instanceof ToolDirectiveHandler && + !!(activeHandler as ParameterCodeBlockHandler).isInsideParameterCodeBlock()) + ) + } + + /** + * Check if we're inside a tool parameter (but not at the parameter level itself) + */ + private static isInsideToolParameter(activeHandler: DirectiveHandler | null, tagName?: string): boolean { + if (!activeHandler || !(activeHandler instanceof ToolDirectiveHandler)) { + return false + } + + const typedHandler = activeHandler as ParameterCodeBlockHandler + const isInParamContext = typedHandler.currentContext === "param" + + // For close tags, also check if this is not the parameter tag itself + if (tagName !== undefined) { + return isInParamContext && tagName !== typedHandler.currentParamName + } + + // For open tags, just check if we're in param context + return isInParamContext + } + + /** + * Convert SAX node attributes to string representation + */ + private static attributesToString(attributes: { [key: string]: string }): string { + if (!attributes || Object.keys(attributes).length === 0) { + return "" + } + return Object.entries(attributes) + .map(([key, value]) => ` ${key}="${value}"`) + .join("") + } +} diff --git a/src/core/message-parsing/FallbackParser.ts b/src/core/message-parsing/FallbackParser.ts new file mode 100644 index 0000000000..54abc826f7 --- /dev/null +++ b/src/core/message-parsing/FallbackParser.ts @@ -0,0 +1,139 @@ +import { Directive, ToolDirective } from "./directives" +import { TextDirective } from "./directives" +import { ToolName, toolNames } from "@roo-code/types" + +export class FallbackParser { + static parse(assistantMessage: string): Directive[] { + const contentBlocks: Directive[] = [] + + // Check if we're inside code blocks before parsing log messages + const codeBlockRegex = /```[\s\S]*?```/g + const codeBlocks: Array<{ start: number; end: number }> = [] + let codeBlockMatch + + // Find all code block ranges + while ((codeBlockMatch = codeBlockRegex.exec(assistantMessage)) !== null) { + codeBlocks.push({ + start: codeBlockMatch.index, + end: codeBlockMatch.index + codeBlockMatch[0].length, + }) + } + + // Helper function to check if a position is inside a code block + const isInsideCodeBlock = (position: number): boolean => { + return codeBlocks.some((block) => position >= block.start && position < block.end) + } + + let lastIndex = 0 + + // If no log messages were found, check for tool use + if (contentBlocks.length === 0) { + for (const toolName of toolNames) { + const toolRegex = new RegExp(`<${toolName}>[\\s\\S]*?(?:<\\/${toolName}>|$)`) + const toolMatch = assistantMessage.match(toolRegex) + if (toolMatch) { + const toolContent = toolMatch[0] + const params: Record = {} + + // Extract parameters - need to be more careful about nested structures + // Find direct child parameters of the tool, not nested ones + const toolInnerContent = toolContent + .replace(new RegExp(`^<${toolName}>`), "") + .replace(new RegExp(`$`), "") + + // Use a more sophisticated approach to find top-level parameters + let currentIndex = 0 + while (currentIndex < toolInnerContent.length) { + // Find the next opening tag + const tagMatch = toolInnerContent.substring(currentIndex).match(/<(\w+)>/) + if (!tagMatch) break + + const paramName = tagMatch[1] + const tagStart = currentIndex + tagMatch.index! + const contentStart = tagStart + tagMatch[0].length + + // Find the matching closing tag, accounting for nested tags + let depth = 1 + let searchIndex = contentStart + let paramValue = "" + + while (depth > 0 && searchIndex < toolInnerContent.length) { + const nextTag = toolInnerContent.substring(searchIndex).match(/<\/?(\w+)>/) + if (!nextTag) { + // No more tags, take the rest as content + paramValue = toolInnerContent.substring(contentStart) + break + } + + const tagName = nextTag[1] + const isClosing = nextTag[0].startsWith(" 0) { + // Unclosed tag, take the rest + paramValue = toolInnerContent.substring(contentStart) + params[paramName] = paramValue + break + } + } + + const ToolDirective: ToolDirective = { + type: "tool_use", + name: toolName as ToolName, + params, + partial: !assistantMessage.includes(``), + } + + contentBlocks.push(ToolDirective) + return contentBlocks + } + } + } + + // Add any remaining text after the last log message + if (lastIndex < assistantMessage.length) { + const remainingText = assistantMessage.substring(lastIndex).trim() + if (remainingText) { + contentBlocks.push({ + type: "text", + content: remainingText, + partial: true, + } as TextDirective) + } + } + + // If no structured content was found, treat as plain text + if (contentBlocks.length === 0) { + contentBlocks.push({ + type: "text", + content: assistantMessage, + partial: true, + } as TextDirective) + } + + return contentBlocks + } +} diff --git a/src/core/message-parsing/ParameterCodeBlockHandler.ts b/src/core/message-parsing/ParameterCodeBlockHandler.ts new file mode 100644 index 0000000000..2cece6abab --- /dev/null +++ b/src/core/message-parsing/ParameterCodeBlockHandler.ts @@ -0,0 +1,12 @@ +import { DirectiveHandler } from "./DirectiveHandler" + +/** + * Interface for directive handlers that support parameter code block detection. + * Extends the base DirectiveHandler to include methods and properties specific to + * tool directive handling. + */ +export interface ParameterCodeBlockHandler extends DirectiveHandler { + isInsideParameterCodeBlock(): boolean + currentContext: "param" | "none" + currentParamName?: string +} diff --git a/src/core/message-parsing/ParseContext.ts b/src/core/message-parsing/ParseContext.ts new file mode 100644 index 0000000000..bc615b5198 --- /dev/null +++ b/src/core/message-parsing/ParseContext.ts @@ -0,0 +1,21 @@ +import { Directive } from "./directives" + +export enum CodeBlockState { + OUTSIDE = "outside", // Normal parsing mode + INSIDE = "inside", // Inside code block - suppress XML + PARTIAL_START = "partial_start", // Detected partial ``` at start + PARTIAL_END = "partial_end", // Detected partial ``` at end +} + +export interface ParseContext { + currentText: string + contentBlocks: Directive[] + hasXmlTags: boolean + hasIncompleteXml: boolean + + // Code block state tracking + codeBlockState: CodeBlockState + pendingBackticks: string // For partial ``` detection + codeBlockContent: string // Accumulated content inside code blocks + codeBlockStartIndex: number // Track where code block started +} diff --git a/src/core/message-parsing/XmlUtils.ts b/src/core/message-parsing/XmlUtils.ts new file mode 100644 index 0000000000..3c84536f87 --- /dev/null +++ b/src/core/message-parsing/XmlUtils.ts @@ -0,0 +1,23 @@ +export class XmlUtils { + static hasIncompleteXml(input: string): boolean { + const openTags: string[] = [] + const tagRegex = /<\/?([a-zA-Z_][a-zA-Z0-9_-]*)[^>]*>/g + let match + + while ((match = tagRegex.exec(input)) !== null) { + const fullTag = match[0] + const tagName = match[1] + + if (fullTag.startsWith("")) { + openTags.push(tagName) + } + } + + return openTags.length > 0 + } +} diff --git a/src/core/message-parsing/__tests__/code-block-state-machine.spec.ts b/src/core/message-parsing/__tests__/code-block-state-machine.spec.ts new file mode 100644 index 0000000000..311c2055ac --- /dev/null +++ b/src/core/message-parsing/__tests__/code-block-state-machine.spec.ts @@ -0,0 +1,65 @@ +import { suite, test, expect } from "vitest" +import { CodeBlockStateMachine } from "../CodeBlockStateMachine" +import { ParseContext, CodeBlockState } from "../ParseContext" + +suite("CodeBlockStateMachine", () => { + function createContext(): ParseContext { + return { + currentText: "", + contentBlocks: [], + hasXmlTags: false, + hasIncompleteXml: false, + codeBlockState: CodeBlockState.OUTSIDE, + pendingBackticks: "", + codeBlockContent: "", + codeBlockStartIndex: -1, + } + } + + test("should detect complete code block boundary", () => { + const stateMachine = new CodeBlockStateMachine() + const context = createContext() + const input = "```\ncode content\n```" + + const result = stateMachine.processText(input, context) + + expect(context.codeBlockState).toBe(CodeBlockState.OUTSIDE) + expect(result.processedText).toBe("```\ncode content\n```") + }) + + test("should handle false positive backticks", () => { + const stateMachine = new CodeBlockStateMachine() + const context = createContext() + + // Single backtick should not trigger code block + const result1 = stateMachine.processText("text `single` more", context) + expect(context.codeBlockState).toBe(CodeBlockState.OUTSIDE) + + // Two backticks should not trigger code block + const result2 = stateMachine.processText("text ``double`` more", context) + expect(context.codeBlockState).toBe(CodeBlockState.OUTSIDE) + }) + + test("should toggle state correctly for code blocks", () => { + const stateMachine = new CodeBlockStateMachine() + const context = createContext() + + // Start outside + expect(context.codeBlockState).toBe(CodeBlockState.OUTSIDE) + + // Process opening ``` + const result1 = stateMachine.processText("```", context) + expect(context.codeBlockState).toBe(CodeBlockState.INSIDE) + expect(result1.suppressXmlParsing).toBe(true) + + // Process content inside + const result2 = stateMachine.processText("content", context) + expect(context.codeBlockState).toBe(CodeBlockState.INSIDE) + expect(result2.suppressXmlParsing).toBe(true) + + // Process closing ``` + const result3 = stateMachine.processText("```", context) + expect(context.codeBlockState).toBe(CodeBlockState.OUTSIDE) + expect(result3.suppressXmlParsing).toBe(false) + }) +}) diff --git a/src/core/message-parsing/directives/Directive.ts b/src/core/message-parsing/directives/Directive.ts new file mode 100644 index 0000000000..11a3270a95 --- /dev/null +++ b/src/core/message-parsing/directives/Directive.ts @@ -0,0 +1,4 @@ +import { TextDirective } from "./TextDirective" +import { ToolDirective } from "./ToolDirective" + +export type Directive = TextDirective | ToolDirective diff --git a/src/core/message-parsing/directives/TextDirective.ts b/src/core/message-parsing/directives/TextDirective.ts new file mode 100644 index 0000000000..1f06dfdd6a --- /dev/null +++ b/src/core/message-parsing/directives/TextDirective.ts @@ -0,0 +1,9 @@ +/** + * Represents a message directive from the assistant to the system. + * This directive instructs the system to output text. + */ +export interface TextDirective { + type: "text" + content: string + partial: boolean +} diff --git a/src/core/message-parsing/directives/ToolDirective.ts b/src/core/message-parsing/directives/ToolDirective.ts new file mode 100644 index 0000000000..29da68773d --- /dev/null +++ b/src/core/message-parsing/directives/ToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolName } from "@roo-code/types" +import { ToolParamName } from "./tool-directives" + +export interface ToolDirective { + type: "tool_use" + name: ToolName + // params is a partial record, allowing only some or none of the possible parameters to be used + params: Partial> + partial: boolean +} diff --git a/src/core/message-parsing/directives/index.ts b/src/core/message-parsing/directives/index.ts new file mode 100644 index 0000000000..b521818a61 --- /dev/null +++ b/src/core/message-parsing/directives/index.ts @@ -0,0 +1,25 @@ +export type { Directive } from "./Directive" +export type { TextDirective } from "./TextDirective" +export type { ToolDirective } from "./ToolDirective" +export type { + ToolParamName, + ToolResponse, + ExecuteCommandToolDirective, + ReadFileToolDirective, + WriteToFileToolDirective, + InsertCodeBlockToolDirective, + CodebaseSearchToolDirective, + SearchFilesToolDirective, + ListFilesToolDirective, + ListCodeDefinitionNamesToolDirective, + BrowserActionToolDirective, + UseMcpToolToolDirective, + AccessMcpResourceToolDirective, + AskFollowupQuestionToolDirective, + AttemptCompletionToolDirective, + SwitchModeToolDirective, + NewTaskToolDirective, + SearchAndReplaceToolDirective, + FetchInstructionsToolDirective, +} from "./tool-directives" +export { toolParamNames } from "./tool-directives" diff --git a/src/core/message-parsing/directives/tool-directives/AccessMcpResourceToolDirective.ts b/src/core/message-parsing/directives/tool-directives/AccessMcpResourceToolDirective.ts new file mode 100644 index 0000000000..143441ce7f --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/AccessMcpResourceToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for accessing a resource provided by an MCP server. + */ +export interface AccessMcpResourceToolDirective extends ToolDirective { + name: "access_mcp_resource" + params: Partial, "server_name" | "uri">> +} diff --git a/src/core/message-parsing/directives/tool-directives/AskFollowupQuestionToolDirective.ts b/src/core/message-parsing/directives/tool-directives/AskFollowupQuestionToolDirective.ts new file mode 100644 index 0000000000..c99cdac341 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/AskFollowupQuestionToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for asking a follow-up question to the user. + */ +export interface AskFollowupQuestionToolDirective extends ToolDirective { + name: "ask_followup_question" + params: Partial, "question" | "follow_up">> +} diff --git a/src/core/message-parsing/directives/tool-directives/AttemptCompletionToolDirective.ts b/src/core/message-parsing/directives/tool-directives/AttemptCompletionToolDirective.ts new file mode 100644 index 0000000000..43d426fb0a --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/AttemptCompletionToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for attempting to complete a task. + */ +export interface AttemptCompletionToolDirective extends ToolDirective { + name: "attempt_completion" + params: Partial, "result" | "command">> +} diff --git a/src/core/message-parsing/directives/tool-directives/BrowserActionToolDirective.ts b/src/core/message-parsing/directives/tool-directives/BrowserActionToolDirective.ts new file mode 100644 index 0000000000..6b75ec9d45 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/BrowserActionToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for performing browser actions. + */ +export interface BrowserActionToolDirective extends ToolDirective { + name: "browser_action" + params: Partial, "action" | "url" | "coordinate" | "text" | "size">> +} diff --git a/src/core/message-parsing/directives/tool-directives/CodebaseSearchToolDirective.ts b/src/core/message-parsing/directives/tool-directives/CodebaseSearchToolDirective.ts new file mode 100644 index 0000000000..35a066972b --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/CodebaseSearchToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for searching the codebase. + */ +export interface CodebaseSearchToolDirective extends ToolDirective { + name: "codebase_search" + params: Partial, "query" | "path">> +} diff --git a/src/core/message-parsing/directives/tool-directives/ExecuteCommandToolDirective.ts b/src/core/message-parsing/directives/tool-directives/ExecuteCommandToolDirective.ts new file mode 100644 index 0000000000..097d966554 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/ExecuteCommandToolDirective.ts @@ -0,0 +1,11 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for executing a command on the system. + */ +export interface ExecuteCommandToolDirective extends ToolDirective { + name: "execute_command" + // Pick, "command"> makes "command" required, but Partial<> makes it optional + params: Partial, "command" | "cwd">> +} diff --git a/src/core/message-parsing/directives/tool-directives/FetchInstructionsToolDirective.ts b/src/core/message-parsing/directives/tool-directives/FetchInstructionsToolDirective.ts new file mode 100644 index 0000000000..d895eda3be --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/FetchInstructionsToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for fetching instructions to perform a task. + */ +export interface FetchInstructionsToolDirective extends ToolDirective { + name: "fetch_instructions" + params: Partial, "task">> +} diff --git a/src/core/message-parsing/directives/tool-directives/InsertCodeBlockToolDirective.ts b/src/core/message-parsing/directives/tool-directives/InsertCodeBlockToolDirective.ts new file mode 100644 index 0000000000..6e589e0462 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/InsertCodeBlockToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for inserting content into a file at a specific line. + */ +export interface InsertCodeBlockToolDirective extends ToolDirective { + name: "insert_content" + params: Partial, "path" | "line" | "content">> +} diff --git a/src/core/message-parsing/directives/tool-directives/ListCodeDefinitionNamesToolDirective.ts b/src/core/message-parsing/directives/tool-directives/ListCodeDefinitionNamesToolDirective.ts new file mode 100644 index 0000000000..2a710ed0ce --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/ListCodeDefinitionNamesToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for listing definition names from source code. + */ +export interface ListCodeDefinitionNamesToolDirective extends ToolDirective { + name: "list_code_definition_names" + params: Partial, "path">> +} diff --git a/src/core/message-parsing/directives/tool-directives/ListFilesToolDirective.ts b/src/core/message-parsing/directives/tool-directives/ListFilesToolDirective.ts new file mode 100644 index 0000000000..85d3d8677f --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/ListFilesToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for listing files and directories. + */ +export interface ListFilesToolDirective extends ToolDirective { + name: "list_files" + params: Partial, "path" | "recursive">> +} diff --git a/src/core/message-parsing/directives/tool-directives/NewTaskToolDirective.ts b/src/core/message-parsing/directives/tool-directives/NewTaskToolDirective.ts new file mode 100644 index 0000000000..005a2fa590 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/NewTaskToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for creating a new task instance. + */ +export interface NewTaskToolDirective extends ToolDirective { + name: "new_task" + params: Partial, "mode" | "message">> +} diff --git a/src/core/message-parsing/directives/tool-directives/ReadFileToolDirective.ts b/src/core/message-parsing/directives/tool-directives/ReadFileToolDirective.ts new file mode 100644 index 0000000000..3b83aa50fe --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/ReadFileToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for reading the contents of a file. + */ +export interface ReadFileToolDirective extends ToolDirective { + name: "read_file" + params: Partial, "args" | "path" | "start_line" | "end_line">> +} diff --git a/src/core/message-parsing/directives/tool-directives/SearchAndReplaceToolDirective.ts b/src/core/message-parsing/directives/tool-directives/SearchAndReplaceToolDirective.ts new file mode 100644 index 0000000000..8cb46e89f7 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/SearchAndReplaceToolDirective.ts @@ -0,0 +1,11 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for searching and replacing text or patterns in a file. + */ +export interface SearchAndReplaceToolDirective extends ToolDirective { + name: "search_and_replace" + params: Required, "path" | "search" | "replace">> & + Partial, "use_regex" | "ignore_case" | "start_line" | "end_line">> +} diff --git a/src/core/message-parsing/directives/tool-directives/SearchFilesToolDirective.ts b/src/core/message-parsing/directives/tool-directives/SearchFilesToolDirective.ts new file mode 100644 index 0000000000..f0d4d4f9d0 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/SearchFilesToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for performing a regex search across files. + */ +export interface SearchFilesToolDirective extends ToolDirective { + name: "search_files" + params: Partial, "path" | "regex" | "file_pattern">> +} diff --git a/src/core/message-parsing/directives/tool-directives/SwitchModeToolDirective.ts b/src/core/message-parsing/directives/tool-directives/SwitchModeToolDirective.ts new file mode 100644 index 0000000000..45da6383c4 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/SwitchModeToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for switching to a different mode. + */ +export interface SwitchModeToolDirective extends ToolDirective { + name: "switch_mode" + params: Partial, "mode_slug" | "reason">> +} diff --git a/src/core/message-parsing/directives/tool-directives/ToolParamName.ts b/src/core/message-parsing/directives/tool-directives/ToolParamName.ts new file mode 100644 index 0000000000..32b1a3f196 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/ToolParamName.ts @@ -0,0 +1,45 @@ +/** + * List of parameter names that can be used in tool directives. + */ +export const toolParamNames = [ + "command", + "path", + "content", + "line_count", + "regex", + "file_pattern", + "recursive", + "action", + "url", + "coordinate", + "text", + "server_name", + "tool_name", + "arguments", + "uri", + "question", + "result", + "diff", + "mode_slug", + "reason", + "line", + "mode", + "message", + "cwd", + "follow_up", + "task", + "size", + "search", + "replace", + "use_regex", + "ignore_case", + "args", + "start_line", + "end_line", + "query", +] as const + +/** + * Type representing a parameter name for tool directives. + */ +export type ToolParamName = (typeof toolParamNames)[number] diff --git a/src/core/message-parsing/directives/tool-directives/ToolResponse.ts b/src/core/message-parsing/directives/tool-directives/ToolResponse.ts new file mode 100644 index 0000000000..9d85661e0e --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/ToolResponse.ts @@ -0,0 +1,6 @@ +import { Anthropic } from "@anthropic-ai/sdk" + +/** + * Type representing the response from a tool execution. + */ +export type ToolResponse = string | Array diff --git a/src/core/message-parsing/directives/tool-directives/UseMcpToolToolDirective.ts b/src/core/message-parsing/directives/tool-directives/UseMcpToolToolDirective.ts new file mode 100644 index 0000000000..694620e038 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/UseMcpToolToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for using a tool provided by an MCP server. + */ +export interface UseMcpToolToolDirective extends ToolDirective { + name: "use_mcp_tool" + params: Partial, "server_name" | "tool_name" | "arguments">> +} diff --git a/src/core/message-parsing/directives/tool-directives/WriteToFileToolDirective.ts b/src/core/message-parsing/directives/tool-directives/WriteToFileToolDirective.ts new file mode 100644 index 0000000000..eda995cbab --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/WriteToFileToolDirective.ts @@ -0,0 +1,10 @@ +import { ToolDirective } from "../ToolDirective" +import { ToolParamName } from "./ToolParamName" + +/** + * Directive for writing content to a file. + */ +export interface WriteToFileToolDirective extends ToolDirective { + name: "write_to_file" + params: Partial, "path" | "content" | "line_count">> +} diff --git a/src/core/message-parsing/directives/tool-directives/index.ts b/src/core/message-parsing/directives/tool-directives/index.ts new file mode 100644 index 0000000000..55dec78b20 --- /dev/null +++ b/src/core/message-parsing/directives/tool-directives/index.ts @@ -0,0 +1,21 @@ +export type { ToolResponse } from "./ToolResponse" +export type { ToolParamName } from "./ToolParamName" +export type { ExecuteCommandToolDirective } from "./ExecuteCommandToolDirective" +export type { ReadFileToolDirective } from "./ReadFileToolDirective" +export type { FetchInstructionsToolDirective } from "./FetchInstructionsToolDirective" +export type { WriteToFileToolDirective } from "./WriteToFileToolDirective" +export type { InsertCodeBlockToolDirective } from "./InsertCodeBlockToolDirective" +export type { CodebaseSearchToolDirective } from "./CodebaseSearchToolDirective" +export type { SearchFilesToolDirective } from "./SearchFilesToolDirective" +export type { ListFilesToolDirective } from "./ListFilesToolDirective" +export type { ListCodeDefinitionNamesToolDirective } from "./ListCodeDefinitionNamesToolDirective" +export type { BrowserActionToolDirective } from "./BrowserActionToolDirective" +export type { UseMcpToolToolDirective } from "./UseMcpToolToolDirective" +export type { AccessMcpResourceToolDirective } from "./AccessMcpResourceToolDirective" +export type { AskFollowupQuestionToolDirective } from "./AskFollowupQuestionToolDirective" +export type { AttemptCompletionToolDirective } from "./AttemptCompletionToolDirective" +export type { SwitchModeToolDirective } from "./SwitchModeToolDirective" +export type { NewTaskToolDirective } from "./NewTaskToolDirective" +export type { SearchAndReplaceToolDirective } from "./SearchAndReplaceToolDirective" + +export { toolParamNames } from "./ToolParamName" diff --git a/src/core/message-parsing/handlers/BaseDirectiveHandler.ts b/src/core/message-parsing/handlers/BaseDirectiveHandler.ts new file mode 100644 index 0000000000..c276363b95 --- /dev/null +++ b/src/core/message-parsing/handlers/BaseDirectiveHandler.ts @@ -0,0 +1,28 @@ +import * as sax from "sax" +import { DirectiveHandler } from "../DirectiveHandler" +import { ParseContext } from "../ParseContext" +import { TextDirective } from "../directives" + +export abstract class BaseDirectiveHandler implements DirectiveHandler { + abstract readonly tagName: string + + canHandle(tagName: string): boolean { + return tagName === this.tagName + } + + onOpenTag(node: sax.Tag, context: ParseContext): void {} + onCloseTag(tagName: string, context: ParseContext): void {} + onText(text: string, context: ParseContext): void {} + onEnd(context: ParseContext): void {} + + protected flushCurrentText(context: ParseContext): void { + if (context.currentText.trim()) { + context.contentBlocks.push({ + type: "text", + content: context.currentText.trim(), + partial: false, + } as TextDirective) + context.currentText = "" + } + } +} diff --git a/src/core/message-parsing/handlers/TextDirectiveHandler.ts b/src/core/message-parsing/handlers/TextDirectiveHandler.ts new file mode 100644 index 0000000000..8cd1eb7485 --- /dev/null +++ b/src/core/message-parsing/handlers/TextDirectiveHandler.ts @@ -0,0 +1,59 @@ +import { BaseDirectiveHandler } from "./BaseDirectiveHandler" +import { ParseContext, CodeBlockState } from "../ParseContext" +import { TextDirective } from "../directives" +import { CodeBlockStateMachine } from "../CodeBlockStateMachine" + +export class TextDirectiveHandler extends BaseDirectiveHandler { + readonly tagName = "text" + private currentState: "text" | "none" = "text" + private stateMachine = new CodeBlockStateMachine() + + override canHandle(tagName: string): boolean { + return false // Text handler is fallback + } + + override onText(text: string, context: ParseContext): void { + if (this.currentState === "text") { + // Process text through the code block state machine + const result = this.stateMachine.processText(text, context) + + // Always add processed text to current text + // The suppressXmlParsing flag is used by the parser to decide whether to process XML tags + context.currentText += result.processedText + } + } + + setState(state: "text" | "none"): void { + this.currentState = state + } + + override onEnd(context: ParseContext): void { + // Handle any remaining code block content + if (context.codeBlockContent) { + context.currentText += context.codeBlockContent + context.codeBlockContent = "" + } + + // Handle any pending backticks that weren't completed + if (context.pendingBackticks) { + context.currentText += context.pendingBackticks + context.pendingBackticks = "" + } + + // Create text directive if we have content + if (context.currentText.trim()) { + context.contentBlocks.push({ + type: "text", + content: context.currentText.trim(), + partial: true, + } as TextDirective) + } + } + + /** + * Check if we're currently inside a code block + */ + isInsideCodeBlock(context: ParseContext): boolean { + return context.codeBlockState === CodeBlockState.INSIDE + } +} diff --git a/src/core/message-parsing/handlers/ToolDirectiveHandler.ts b/src/core/message-parsing/handlers/ToolDirectiveHandler.ts new file mode 100644 index 0000000000..71f6fd39cb --- /dev/null +++ b/src/core/message-parsing/handlers/ToolDirectiveHandler.ts @@ -0,0 +1,91 @@ +import * as sax from "sax" +import { BaseDirectiveHandler } from "./BaseDirectiveHandler" +import { ParseContext, CodeBlockState } from "../ParseContext" +import { ToolDirective, ToolParamName } from "../directives" +import { CodeBlockStateMachine } from "../CodeBlockStateMachine" +import { ToolName } from "@roo-code/types" + +export class ToolDirectiveHandler extends BaseDirectiveHandler { + readonly tagName: string + private currentToolDirective?: ToolDirective + public currentParamName?: ToolParamName + private currentParamValue = "" + public currentContext: "param" | "none" = "none" + private stateMachine = new CodeBlockStateMachine() + private paramCodeBlockState: CodeBlockState = CodeBlockState.OUTSIDE + + constructor(toolName: string) { + super() + this.tagName = toolName + } + + override onOpenTag(node: sax.Tag, context: ParseContext): void { + if (node.name === this.tagName) { + this.flushCurrentText(context) + this.currentToolDirective = { + type: "tool_use", + name: this.tagName as ToolName, + params: {}, + partial: true, + } + this.currentContext = "none" + } else if (this.currentToolDirective) { + this.currentParamName = node.name as ToolParamName + this.currentParamValue = "" + this.currentContext = "param" + // Reset code block state for new parameter + this.paramCodeBlockState = CodeBlockState.OUTSIDE + } + } + + override onCloseTag(tagName: string, context: ParseContext): void { + if (tagName === this.tagName && this.currentToolDirective) { + this.currentToolDirective.partial = + context.hasIncompleteXml || Object.keys(this.currentToolDirective.params).length === 0 + context.contentBlocks.push(this.currentToolDirective) + this.currentToolDirective = undefined + } else if (this.currentToolDirective && this.currentParamName && tagName === this.currentParamName) { + ;(this.currentToolDirective.params as Record)[this.currentParamName] = + this.currentParamValue.trim() + this.currentParamName = undefined + this.currentParamValue = "" + this.currentContext = "none" + } + } + + override onText(text: string, context: ParseContext): void { + if (this.currentContext === "param" && this.currentParamName && this.currentToolDirective) { + // Create a temporary context to track code block state within this parameter + const tempContext = { + ...context, + codeBlockState: this.paramCodeBlockState, + } + + // Process text through the code block state machine + const result = this.stateMachine.processText(text, tempContext) + + // Update our parameter-specific code block state + this.paramCodeBlockState = tempContext.codeBlockState + + this.currentParamValue += result.processedText + } + } + + override onEnd(context: ParseContext): void { + if (this.currentToolDirective) { + if (this.currentParamName && this.currentParamValue) { + ;(this.currentToolDirective.params as Record)[this.currentParamName] = + this.currentParamValue.trim() + } + this.currentToolDirective.partial = true + context.contentBlocks.push(this.currentToolDirective) + } + } + + /** + * Check if we're currently inside a code block within a tool parameter + */ + isInsideParameterCodeBlock(): boolean { + return this.currentContext === "param" && this.paramCodeBlockState === CodeBlockState.INSIDE + } +} diff --git a/src/core/message-parsing/handlers/index.ts b/src/core/message-parsing/handlers/index.ts new file mode 100644 index 0000000000..57f2f9f178 --- /dev/null +++ b/src/core/message-parsing/handlers/index.ts @@ -0,0 +1,2 @@ +export { TextDirectiveHandler } from "./TextDirectiveHandler" +export { ToolDirectiveHandler } from "./ToolDirectiveHandler" diff --git a/src/core/message-parsing/index.ts b/src/core/message-parsing/index.ts new file mode 100644 index 0000000000..d340528ada --- /dev/null +++ b/src/core/message-parsing/index.ts @@ -0,0 +1,5 @@ +export { presentAssistantMessage } from "./presentAssistantMessage" + +// Main API +export type { Directive } from "./directives" +export { DirectiveStreamingParser } from "./DirectiveStreamingParser" diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/message-parsing/presentAssistantMessage.ts similarity index 87% rename from src/core/assistant-message/presentAssistantMessage.ts rename to src/core/message-parsing/presentAssistantMessage.ts index ee3fa148b4..6a19657dd1 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/message-parsing/presentAssistantMessage.ts @@ -4,8 +4,28 @@ import { serializeError } from "serialize-error" import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" +import type { + ToolParamName, + ToolResponse, + ExecuteCommandToolDirective, + ListFilesToolDirective, + ReadFileToolDirective, + WriteToFileToolDirective, + InsertCodeBlockToolDirective, + SearchAndReplaceToolDirective, + SearchFilesToolDirective, + ListCodeDefinitionNamesToolDirective, + UseMcpToolToolDirective, + AccessMcpResourceToolDirective, + AskFollowupQuestionToolDirective, + SwitchModeToolDirective, + NewTaskToolDirective, + AttemptCompletionToolDirective, + BrowserActionToolDirective, + FetchInstructionsToolDirective, +} from "./directives" + import { defaultModeSlug, getModeBySlug } from "../../shared/modes" -import type { ToolParamName, ToolResponse } from "../../shared/tools" import { fetchInstructionsTool } from "../tools/fetchInstructionsTool" import { listFilesTool } from "../tools/listFilesTool" @@ -150,7 +170,7 @@ export async function presentAssistantMessage(cline: Task) { await cline.say("text", content, undefined, block.partial) break } - case "tool_use": + case "tool_use": { const toolDescription = (): string => { switch (block.name) { case "execute_command": @@ -411,7 +431,14 @@ export async function presentAssistantMessage(cline: Task) { switch (block.name) { case "write_to_file": - await writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await writeToFileTool( + cline, + block as WriteToFileToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "update_todo_list": await updateTodoListTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) @@ -444,20 +471,54 @@ export async function presentAssistantMessage(cline: Task) { break } case "insert_content": - await insertContentTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await insertContentTool( + cline, + block as InsertCodeBlockToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "search_and_replace": - await searchAndReplaceTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await searchAndReplaceTool( + cline, + block as SearchAndReplaceToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "read_file": - await readFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await readFileTool( + cline, + block as ReadFileToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "fetch_instructions": - await fetchInstructionsTool(cline, block, askApproval, handleError, pushToolResult) + await fetchInstructionsTool( + cline, + block as FetchInstructionsToolDirective, + askApproval, + handleError, + pushToolResult, + ) break case "list_files": - await listFilesTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await listFilesTool( + cline, + block as ListFilesToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "codebase_search": await codebaseSearchTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) @@ -465,7 +526,7 @@ export async function presentAssistantMessage(cline: Task) { case "list_code_definition_names": await listCodeDefinitionNamesTool( cline, - block, + block as ListCodeDefinitionNamesToolDirective, askApproval, handleError, pushToolResult, @@ -473,21 +534,49 @@ export async function presentAssistantMessage(cline: Task) { ) break case "search_files": - await searchFilesTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await searchFilesTool( + cline, + block as SearchFilesToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "browser_action": - await browserActionTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await browserActionTool( + cline, + block as BrowserActionToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "execute_command": - await executeCommandTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await executeCommandTool( + cline, + block as ExecuteCommandToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "use_mcp_tool": - await useMcpToolTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await useMcpToolTool( + cline, + block as UseMcpToolToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "access_mcp_resource": await accessMcpResourceTool( cline, - block, + block as AccessMcpResourceToolDirective, askApproval, handleError, pushToolResult, @@ -497,7 +586,7 @@ export async function presentAssistantMessage(cline: Task) { case "ask_followup_question": await askFollowupQuestionTool( cline, - block, + block as AskFollowupQuestionToolDirective, askApproval, handleError, pushToolResult, @@ -505,15 +594,29 @@ export async function presentAssistantMessage(cline: Task) { ) break case "switch_mode": - await switchModeTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await switchModeTool( + cline, + block as SwitchModeToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "new_task": - await newTaskTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) + await newTaskTool( + cline, + block as NewTaskToolDirective, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) break case "attempt_completion": await attemptCompletionTool( cline, - block, + block as AttemptCompletionToolDirective, askApproval, handleError, pushToolResult, @@ -525,6 +628,7 @@ export async function presentAssistantMessage(cline: Task) { } break + } } const recentlyModifiedFiles = cline.fileContextTracker.getAndClearCheckpointPossibleFile() diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index 632273dea0..603ba34e4c 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap index 09b6b04348..ff7b0c692a 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap index 632273dea0..603ba34e4c 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index 7ca32b80a1..fd9c56bbc0 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap index 7dce6219f3..294512bb1e 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index 632273dea0..603ba34e4c 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index 419049609e..cd9b849acf 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index 632273dea0..603ba34e4c 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 4390b95519..e124f3bda3 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index 632273dea0..603ba34e4c 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index 191816f180..af1e227838 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index 7ca32b80a1..fd9c56bbc0 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index 632273dea0..603ba34e4c 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -31,6 +31,44 @@ For example, to use the new_task tool: Always use the actual tool name as the XML tag name for proper parsing and execution. +==== + +LOG MESSAGES + +You can use log messages to output debugging information to the VSCode output channel. Unlike tools, log messages don't require approval and don't count toward the one-tool-per-message limit. + +# Purpose and Context + +The VSCode OutputChannel is a dedicated console-like interface within VSCode where extensions can write diagnostic information, separate from the user's workspace files. + +Unlike other tools that create or modify files in the workspace, log messages are purely for diagnostic purposes within the extension's runtime environment. + +# Log Message Formatting + +Log messages are formatted using XML-style tags. Here's the structure: + + +Your log message here +info + + +The level parameter is optional and defaults to "info". Valid levels are: "debug", "info", "warn", and "error". + +For example: + + +Starting task execution +info + + + +Failed to parse input: invalid JSON +error + + +You can use log messages multiple times in a single message, and they will be processed immediately without requiring user approval. + + # Tools ## read_file diff --git a/src/core/prompts/__tests__/custom-system-prompt.spec.ts b/src/core/prompts/__tests__/custom-system-prompt.spec.ts index acf34ac459..7c796c90f1 100644 --- a/src/core/prompts/__tests__/custom-system-prompt.spec.ts +++ b/src/core/prompts/__tests__/custom-system-prompt.spec.ts @@ -96,7 +96,7 @@ describe("File-Based Custom System Prompt", () => { expect(prompt).toContain("CAPABILITIES") expect(prompt).toContain("MODES") expect(prompt).toContain("Test role definition") - }) + }, 10000) it("should use file-based custom system prompt when available", async () => { // Mock the readFile to return content from a file diff --git a/src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts deleted file mode 100644 index 53272a112b..0000000000 --- a/src/core/prompts/sections/__tests__/custom-instructions-path-detection.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, it, expect, vi } from "vitest" -import * as os from "os" -import * as path from "path" - -describe("custom-instructions path detection", () => { - it("should use exact path comparison instead of string includes", () => { - // Test the logic that our fix implements - const fakeHomeDir = "/Users/john.roo.smith" - const globalRooDir = path.join(fakeHomeDir, ".roo") // "/Users/john.roo.smith/.roo" - const projectRooDir = "/projects/my-project/.roo" - - // Old implementation (fragile): - // const isGlobal = rooDir.includes(path.join(os.homedir(), ".roo")) - // This could fail if the home directory path contains ".roo" elsewhere - - // New implementation (robust): - // const isGlobal = path.resolve(rooDir) === path.resolve(getGlobalRooDirectory()) - - // Test the new logic - const isGlobalForGlobalDir = path.resolve(globalRooDir) === path.resolve(globalRooDir) - const isGlobalForProjectDir = path.resolve(projectRooDir) === path.resolve(globalRooDir) - - expect(isGlobalForGlobalDir).toBe(true) - expect(isGlobalForProjectDir).toBe(false) - - // Verify that the old implementation would have been problematic - // if the home directory contained ".roo" in the path - const oldLogicGlobal = globalRooDir.includes(path.join(fakeHomeDir, ".roo")) - const oldLogicProject = projectRooDir.includes(path.join(fakeHomeDir, ".roo")) - - expect(oldLogicGlobal).toBe(true) // This works - expect(oldLogicProject).toBe(false) // This also works, but is fragile - - // The issue was that if the home directory path itself contained ".roo", - // the includes() check could produce false positives in edge cases - }) - - it("should handle edge cases with path resolution", () => { - // Test various edge cases that exact path comparison handles better - const testCases = [ - { - global: "/Users/test/.roo", - project: "/Users/test/project/.roo", - expected: { global: true, project: false }, - }, - { - global: "/home/user/.roo", - project: "/home/user/.roo", // Same directory - expected: { global: true, project: true }, - }, - { - global: "/Users/john.roo.smith/.roo", - project: "/projects/app/.roo", - expected: { global: true, project: false }, - }, - ] - - testCases.forEach(({ global, project, expected }) => { - const isGlobalForGlobal = path.resolve(global) === path.resolve(global) - const isGlobalForProject = path.resolve(project) === path.resolve(global) - - expect(isGlobalForGlobal).toBe(expected.global) - expect(isGlobalForProject).toBe(expected.project) - }) - }) -}) diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index ccd36b2662..2d57d92477 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -1,6 +1,5 @@ import fs from "fs/promises" import path from "path" -import * as os from "os" import { Dirent } from "fs" import { isLanguage } from "@roo-code/types" @@ -8,7 +7,6 @@ import { isLanguage } from "@roo-code/types" import type { SystemPromptSettings } from "../types" import { LANGUAGES } from "../../../shared/language" -import { getRooDirectoriesForCwd, getGlobalRooDirectory } from "../../../services/roo-config" /** * Safely read a file and return its trimmed content @@ -171,39 +169,30 @@ async function readTextFilesFromDirectory(dirPath: string): Promise): string { if (files.length === 0) return "" - return files - .map((file) => { - return `# Rules from ${file.filename}:\n${file.content}` - }) - .join("\n\n") + return ( + "\n\n" + + files + .map((file) => { + return `# Rules from ${file.filename}:\n${file.content}` + }) + .join("\n\n") + ) } /** - * Load rule files from global and project-local directories - * Global rules are loaded first, then project-local rules which can override global ones + * Load rule files from the specified directory */ export async function loadRuleFiles(cwd: string): Promise { - const rules: string[] = [] - const rooDirectories = getRooDirectoriesForCwd(cwd) - - // Check for .roo/rules/ directories in order (global first, then project-local) - for (const rooDir of rooDirectories) { - const rulesDir = path.join(rooDir, "rules") - if (await directoryExists(rulesDir)) { - const files = await readTextFilesFromDirectory(rulesDir) - if (files.length > 0) { - const content = formatDirectoryContent(rulesDir, files) - rules.push(content) - } + // Check for .roo/rules/ directory + const rooRulesDir = path.join(cwd, ".roo", "rules") + if (await directoryExists(rooRulesDir)) { + const files = await readTextFilesFromDirectory(rooRulesDir) + if (files.length > 0) { + return formatDirectoryContent(rooRulesDir, files) } } - // If we found rules in .roo/rules/ directories, return them - if (rules.length > 0) { - return "\n" + rules.join("\n\n") - } - - // Fall back to existing behavior for legacy .roorules/.clinerules files + // Fall back to existing behavior const ruleFiles = [".roorules", ".clinerules"] for (const file of ruleFiles) { @@ -250,27 +239,18 @@ export async function addCustomInstructions( let usedRuleFile = "" if (mode) { - const modeRules: string[] = [] - const rooDirectories = getRooDirectoriesForCwd(cwd) - - // Check for .roo/rules-${mode}/ directories in order (global first, then project-local) - for (const rooDir of rooDirectories) { - const modeRulesDir = path.join(rooDir, `rules-${mode}`) - if (await directoryExists(modeRulesDir)) { - const files = await readTextFilesFromDirectory(modeRulesDir) - if (files.length > 0) { - const content = formatDirectoryContent(modeRulesDir, files) - modeRules.push(content) - } + // Check for .roo/rules-${mode}/ directory + const modeRulesDir = path.join(cwd, ".roo", `rules-${mode}`) + if (await directoryExists(modeRulesDir)) { + const files = await readTextFilesFromDirectory(modeRulesDir) + if (files.length > 0) { + modeRuleContent = formatDirectoryContent(modeRulesDir, files) + usedRuleFile = modeRulesDir } } - // If we found mode-specific rules in .roo/rules-${mode}/ directories, use them - if (modeRules.length > 0) { - modeRuleContent = "\n" + modeRules.join("\n\n") - usedRuleFile = `rules-${mode} directories` - } else { - // Fall back to existing behavior for legacy files + // If no directory exists, fall back to existing behavior + if (!modeRuleContent) { const rooModeRuleFile = `.roorules-${mode}` modeRuleContent = await safeReadFile(path.join(cwd, rooModeRuleFile)) if (modeRuleContent) { diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts index 643233ab6f..0af850cbb1 100644 --- a/src/core/prompts/sections/mcp-servers.ts +++ b/src/core/prompts/sections/mcp-servers.ts @@ -39,7 +39,7 @@ export async function getMcpServersSection( const config = JSON.parse(server.config) return ( - `## ${server.name}${config.command ? ` (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` : ""}` + + `## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` + (server.instructions ? `\n\n### Instructions\n${server.instructions}` : "") + (tools ? `\n\n### Available Tools\n${tools}` : "") + (templates ? `\n\n### Resource Templates\n${templates}` : "") + diff --git a/src/core/task-persistence/apiMessages.ts b/src/core/task-persistence/apiMessages.ts index f846aaf13f..d6c17bd9b3 100644 --- a/src/core/task-persistence/apiMessages.ts +++ b/src/core/task-persistence/apiMessages.ts @@ -1,4 +1,3 @@ -import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as fs from "fs/promises" @@ -79,5 +78,5 @@ export async function saveApiMessages({ }) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory) - await safeWriteJson(filePath, messages) + await fs.writeFile(filePath, JSON.stringify(messages)) } diff --git a/src/core/task-persistence/taskMessages.ts b/src/core/task-persistence/taskMessages.ts index 63a2eefbaa..3ed5c5099e 100644 --- a/src/core/task-persistence/taskMessages.ts +++ b/src/core/task-persistence/taskMessages.ts @@ -1,4 +1,3 @@ -import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as fs from "fs/promises" @@ -38,5 +37,5 @@ export type SaveTaskMessagesOptions = { export async function saveTaskMessages({ messages, taskId, globalStoragePath }: SaveTaskMessagesOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.uiMessages) - await safeWriteJson(filePath, messages) + await fs.writeFile(filePath, JSON.stringify(messages)) } diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index 1759a72f47..8044acd8ba 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -8,7 +8,6 @@ import { combineCommandSequences } from "../../shared/combineCommandSequences" import { getApiMetrics } from "../../shared/getApiMetrics" import { findLastIndex } from "../../shared/array" import { getTaskDirectoryPath } from "../../utils/storage" -import { t } from "../../i18n" const taskSizeCache = new NodeCache({ stdTTL: 30, checkperiod: 5 * 60 }) @@ -28,63 +27,29 @@ export async function taskMetadata({ workspace, }: TaskMetadataOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) + const taskMessage = messages[0] // First message is always the task say. - // Determine message availability upfront - const hasMessages = messages && messages.length > 0 + const lastRelevantMessage = + messages[findLastIndex(messages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))] - // Pre-calculate all values based on availability - let timestamp: number - let tokenUsage: ReturnType - let taskDirSize: number - let taskMessage: ClineMessage | undefined + let taskDirSize = taskSizeCache.get(taskDir) - if (!hasMessages) { - // Handle no messages case - timestamp = Date.now() - tokenUsage = { - totalTokensIn: 0, - totalTokensOut: 0, - totalCacheWrites: 0, - totalCacheReads: 0, - totalCost: 0, - contextTokens: 0, - } - taskDirSize = 0 - } else { - // Handle messages case - taskMessage = messages[0] // First message is always the task say. - - const lastRelevantMessage = - messages[findLastIndex(messages, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))] || - taskMessage - - timestamp = lastRelevantMessage.ts - - tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1)))) - - // Get task directory size - const cachedSize = taskSizeCache.get(taskDir) - - if (cachedSize === undefined) { - try { - taskDirSize = await getFolderSize.loose(taskDir) - taskSizeCache.set(taskDir, taskDirSize) - } catch (error) { - taskDirSize = 0 - } - } else { - taskDirSize = cachedSize + if (taskDirSize === undefined) { + try { + taskDirSize = await getFolderSize.loose(taskDir) + taskSizeCache.set(taskDir, taskDirSize) + } catch (error) { + taskDirSize = 0 } } - // Create historyItem once with pre-calculated values + const tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1)))) + const historyItem: HistoryItem = { id: taskId, number: taskNumber, - ts: timestamp, - task: hasMessages - ? taskMessage!.text?.trim() || t("common:tasks.incomplete", { taskNumber }) - : t("common:tasks.no_messages", { taskNumber }), + ts: lastRelevantMessage.ts, + task: taskMessage.text ?? "", tokensIn: tokenUsage.totalTokensIn, tokensOut: tokenUsage.totalTokensOut, cacheWrites: tokenUsage.totalCacheWrites, diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 95d12f66aa..54a826ab65 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1,5 +1,4 @@ import * as path from "path" -import * as vscode from "vscode" import os from "os" import crypto from "crypto" import EventEmitter from "events" @@ -19,12 +18,8 @@ import { type ClineMessage, type ClineSay, type ToolProgressStatus, - DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, type HistoryItem, TelemetryEventName, - TodoItem, - getApiProtocol, - getModelId, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -44,7 +39,6 @@ import { ClineAskResponse } from "../../shared/WebviewMessage" import { defaultModeSlug } from "../../shared/modes" import { DiffStrategy } from "../../shared/tools" import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" -import { getModelMaxOutputTokens } from "../../shared/api" // services import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher" @@ -71,8 +65,8 @@ import { SYSTEM_PROMPT } from "../prompts/system" import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector" import { FileContextTracker } from "../context-tracking/FileContextTracker" import { RooIgnoreController } from "../ignore/RooIgnoreController" +import { presentAssistantMessage } from "../message-parsing" import { RooProtectedController } from "../protect/RooProtectedController" -import { type AssistantMessageContent, parseAssistantMessage, presentAssistantMessage } from "../assistant-message" import { truncateConversationIfNeeded } from "../sliding-window" import { ClineProvider } from "../webview/ClineProvider" import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace" @@ -91,12 +85,9 @@ import { processUserContentMentions } from "../mentions/processUserContentMentio import { ApiMessage } from "../task-persistence/apiMessages" import { getMessagesSinceLastSummary, summarizeConversation } from "../condense" import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning" -import { restoreTodoListForTask } from "../tools/updateTodoListTool" +import { Directive, DirectiveStreamingParser } from "../message-parsing" -// Constants -const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes - -export type ClineEvents = { +type ClineEvents = { message: [{ action: "created" | "updated"; message: ClineMessage }] taskStarted: [] taskModeSwitched: [taskId: string, mode: string] @@ -129,7 +120,6 @@ export type TaskOptions = { } export class Task extends EventEmitter { - todoList?: TodoItem[] readonly taskId: string readonly instanceId: string @@ -204,7 +194,7 @@ export class Task extends EventEmitter { isWaitingForFirstChunk = false isStreaming = false currentStreamingContentIndex = 0 - assistantMessageContent: AssistantMessageContent[] = [] + assistantMessageContent: Directive[] = [] presentAssistantMessageLocked = false presentAssistantMessageHasPendingUpdates = false userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = [] @@ -219,7 +209,7 @@ export class Task extends EventEmitter { enableDiff = false, enableCheckpoints = true, fuzzyMatchThreshold = 1.0, - consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, + consecutiveMistakeLimit = 3, task, images, historyItem, @@ -258,10 +248,10 @@ export class Task extends EventEmitter { this.browserSession = new BrowserSession(provider.context) this.diffEnabled = enableDiff this.fuzzyMatchThreshold = fuzzyMatchThreshold - this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT + this.consecutiveMistakeLimit = consecutiveMistakeLimit this.providerRef = new WeakRef(provider) this.globalStoragePath = provider.context.globalStorageUri.fsPath - this.diffViewProvider = new DiffViewProvider(this.cwd, this) + this.diffViewProvider = new DiffViewProvider(this.cwd) this.enableCheckpoints = enableCheckpoints this.rootTask = rootTask @@ -378,7 +368,6 @@ export class Task extends EventEmitter { public async overwriteClineMessages(newMessages: ClineMessage[]) { this.clineMessages = newMessages - restoreTodoListForTask(this) await this.saveClineMessages() } @@ -1162,7 +1151,7 @@ export class Task extends EventEmitter { throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`) } - if (this.consecutiveMistakeLimit > 0 && this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) { + if (this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) { const { response, text, images } = await this.ask( "mistake_limit_reached", t("common:errors.mistake_limit_guidance"), @@ -1212,25 +1201,15 @@ export class Task extends EventEmitter { // top-down build file structure of project which for large projects can // take a few seconds. For the best UX we show a placeholder api_req_started // message with a loading spinner as this happens. - - // Determine API protocol based on provider and model - const modelId = getModelId(this.apiConfiguration) - const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId) - await this.say( "api_req_started", JSON.stringify({ request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n") + "\n\nLoading...", - apiProtocol, }), ) - const { - showRooIgnoredFiles = true, - includeDiagnosticMessages = true, - maxDiagnosticMessages = 50, - } = (await this.providerRef.deref()?.getState()) ?? {} + const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {} const parsedUserContent = await processUserContentMentions({ userContent, @@ -1239,8 +1218,6 @@ export class Task extends EventEmitter { fileContextTracker: this.fileContextTracker, rooIgnoreController: this.rooIgnoreController, showRooIgnoredFiles, - includeDiagnosticMessages, - maxDiagnosticMessages, }) const environmentDetails = await getEnvironmentDetails(this, includeFileDetails) @@ -1260,7 +1237,6 @@ export class Task extends EventEmitter { this.clineMessages[lastApiReqIndex].text = JSON.stringify({ request: finalUserContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"), - apiProtocol, } satisfies ClineApiReqInfo) await this.saveClineMessages() @@ -1281,9 +1257,8 @@ export class Task extends EventEmitter { // of prices in tasks from history (it's worth removing a few months // from now). const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => { - const existingData = JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}") this.clineMessages[lastApiReqIndex].text = JSON.stringify({ - ...existingData, + ...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"), tokensIn: inputTokens, tokensOut: outputTokens, cacheWrites: cacheWriteTokens, @@ -1369,7 +1344,7 @@ export class Task extends EventEmitter { try { for await (const chunk of stream) { if (!chunk) { - // Sometimes chunk is undefined, no idea that can cause + // Sometimes chunk is undefined, no idea what can cause // it, but this workaround seems to fix it. continue } @@ -1391,7 +1366,7 @@ export class Task extends EventEmitter { // Parse raw assistant message into content blocks. const prevLength = this.assistantMessageContent.length - this.assistantMessageContent = parseAssistantMessage(assistantMessage) + this.assistantMessageContent = DirectiveStreamingParser.parse(assistantMessage) if (this.assistantMessageContent.length > prevLength) { // New content we need to present, reset to @@ -1449,19 +1424,12 @@ export class Task extends EventEmitter { // could be in (i.e. could have streamed some tools the user // may have executed), so we just resort to replicating a // cancel task. + this.abortTask() - // Check if this was a user-initiated cancellation BEFORE calling abortTask - // If this.abort is already true, it means the user clicked cancel, so we should - // treat this as "user_cancelled" rather than "streaming_failed" - const cancelReason = this.abort ? "user_cancelled" : "streaming_failed" - const streamingFailedMessage = this.abort - ? undefined - : (error.message ?? JSON.stringify(serializeError(error), null, 2)) - - // Now call abortTask after determining the cancel reason - await this.abortTask() - - await abortStream(cancelReason, streamingFailedMessage) + await abortStream( + "streaming_failed", + error.message ?? JSON.stringify(serializeError(error), null, 2), + ) const history = await provider?.getTaskWithId(this.taskId) @@ -1633,7 +1601,6 @@ export class Task extends EventEmitter { language, maxConcurrentFileReads, maxReadFileLine, - apiConfiguration, } = state ?? {} return await (async () => { @@ -1661,9 +1628,7 @@ export class Task extends EventEmitter { rooIgnoreInstructions, maxReadFileLine !== -1, { - maxConcurrentFileReads: maxConcurrentFileReads ?? 5, - todoListEnabled: apiConfiguration?.todoListEnabled ?? true, - useAgentRules: vscode.workspace.getConfiguration("roo-cline").get("useAgentRules") ?? true, + maxConcurrentFileReads, }, ) })() @@ -1732,20 +1697,18 @@ export class Task extends EventEmitter { const { contextTokens } = this.getTokenUsage() if (contextTokens) { + // Default max tokens value for thinking models when no specific + // value is set. + const DEFAULT_THINKING_MODEL_MAX_TOKENS = 16_384 + const modelInfo = this.api.getModel().info - const maxTokens = getModelMaxOutputTokens({ - modelId: this.api.getModel().id, - model: modelInfo, - settings: this.apiConfiguration, - }) + const maxTokens = modelInfo.supportsReasoningBudget + ? this.apiConfiguration.modelMaxTokens || DEFAULT_THINKING_MODEL_MAX_TOKENS + : modelInfo.maxTokens const contextWindow = modelInfo.contextWindow - const currentProfileId = - state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ?? - "default" - const truncateResult = await truncateConversationIfNeeded({ messages: this.apiConversationHistory, totalTokens: contextTokens, @@ -1759,7 +1722,7 @@ export class Task extends EventEmitter { customCondensingPrompt, condensingApiHandler, profileThresholds, - currentProfileId, + currentProfileId: state?.currentApiConfigName || "default", }) if (truncateResult.messages !== this.apiConversationHistory) { await this.overwriteApiConversationHistory(truncateResult.messages) @@ -1830,10 +1793,7 @@ export class Task extends EventEmitter { } const baseDelay = requestDelaySeconds || 5 - let exponentialDelay = Math.min( - Math.ceil(baseDelay * Math.pow(2, retryAttempt)), - MAX_EXPONENTIAL_BACKOFF_SECONDS, - ) + let exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) // If the error is a 429, and the error details contain a retry delay, use that delay instead of exponential backoff if (error.status === 429) { diff --git a/src/core/tools/ToolRepetitionDetector.ts b/src/core/tools/ToolRepetitionDetector.ts index 927b031e3b..9845dae31a 100644 --- a/src/core/tools/ToolRepetitionDetector.ts +++ b/src/core/tools/ToolRepetitionDetector.ts @@ -1,5 +1,5 @@ -import { ToolUse } from "../../shared/tools" import { t } from "../../i18n" +import { ToolDirective } from "../message-parsing/directives" /** * Class for detecting consecutive identical tool calls @@ -22,10 +22,10 @@ export class ToolRepetitionDetector { * Checks if the current tool call is identical to the previous one * and determines if execution should be allowed * - * @param currentToolCallBlock ToolUse object representing the current tool call + * @param currentToolCallBlock ToolDirective object representing the current tool call * @returns Object indicating if execution is allowed and a message to show if not */ - public check(currentToolCallBlock: ToolUse): { + public check(currentToolCallBlock: ToolDirective): { allowExecution: boolean askUser?: { messageKey: string @@ -33,7 +33,7 @@ export class ToolRepetitionDetector { } } { // Serialize the block to a canonical JSON string for comparison - const currentToolCallJson = this.serializeToolUse(currentToolCallBlock) + const currentToolCallJson = this.serializeToolDirective(currentToolCallBlock) // Compare with previous tool call if (this.previousToolCallJson === currentToolCallJson) { @@ -67,28 +67,28 @@ export class ToolRepetitionDetector { } /** - * Serializes a ToolUse object into a canonical JSON string for comparison + * Serializes a ToolDirective object into a canonical JSON string for comparison * - * @param toolUse The ToolUse object to serialize - * @returns JSON string representation of the tool use with sorted parameter keys + * @param ToolDirective The ToolDirective object to serialize + * @returns JSON string representation of the tool directive with sorted parameter keys */ - private serializeToolUse(toolUse: ToolUse): string { + private serializeToolDirective(ToolDirective: ToolDirective): string { // Create a new parameters object with alphabetically sorted keys const sortedParams: Record = {} // Get parameter keys and sort them alphabetically - const sortedKeys = Object.keys(toolUse.params).sort() + const sortedKeys = Object.keys(ToolDirective.params).sort() // Populate the sorted parameters object in a type-safe way for (const key of sortedKeys) { - if (Object.prototype.hasOwnProperty.call(toolUse.params, key)) { - sortedParams[key] = toolUse.params[key as keyof typeof toolUse.params] + if (Object.prototype.hasOwnProperty.call(ToolDirective.params, key)) { + sortedParams[key] = ToolDirective.params[key as keyof typeof ToolDirective.params] } } // Create the object with the tool name and sorted parameters const toolObject = { - name: toolUse.name, + name: ToolDirective.name, parameters: sortedParams, } diff --git a/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts b/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts index 42041c1a46..e9ade0ba73 100644 --- a/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts +++ b/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts @@ -2,9 +2,8 @@ import type { ToolName } from "@roo-code/types" -import type { ToolUse } from "../../../shared/tools" - import { ToolRepetitionDetector } from "../ToolRepetitionDetector" +import { ToolDirective } from "../../message-parsing/directives" vitest.mock("../../../i18n", () => ({ t: vitest.fn((key, options) => { @@ -16,7 +15,7 @@ vitest.mock("../../../i18n", () => ({ }), })) -function createToolUse(name: string, displayName?: string, params: Record = {}): ToolUse { +function createToolDirective(name: string, displayName?: string, params: Record = {}): ToolDirective { return { type: "tool_use", name: (displayName || name) as ToolName, @@ -33,15 +32,15 @@ describe("ToolRepetitionDetector", () => { // We'll verify this through behavior in subsequent tests // First call (counter = 1) - const result1 = detector.check(createToolUse("test", "test-tool")) + const result1 = detector.check(createToolDirective("test", "test-tool")) expect(result1.allowExecution).toBe(true) // Second identical call (counter = 2) - const result2 = detector.check(createToolUse("test", "test-tool")) + const result2 = detector.check(createToolDirective("test", "test-tool")) expect(result2.allowExecution).toBe(true) // Third identical call (counter = 3) reaches the default limit - const result3 = detector.check(createToolUse("test", "test-tool")) + const result3 = detector.check(createToolDirective("test", "test-tool")) expect(result3.allowExecution).toBe(false) }) @@ -50,11 +49,11 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(customLimit) // First call (counter = 1) - const result1 = detector.check(createToolUse("test", "test-tool")) + const result1 = detector.check(createToolDirective("test", "test-tool")) expect(result1.allowExecution).toBe(true) // Second identical call (counter = 2) reaches the custom limit - const result2 = detector.check(createToolUse("test", "test-tool")) + const result2 = detector.check(createToolDirective("test", "test-tool")) expect(result2.allowExecution).toBe(false) }) }) @@ -64,15 +63,15 @@ describe("ToolRepetitionDetector", () => { it("should allow execution for different tool calls", () => { const detector = new ToolRepetitionDetector() - const result1 = detector.check(createToolUse("first", "first-tool")) + const result1 = detector.check(createToolDirective("first", "first-tool")) expect(result1.allowExecution).toBe(true) expect(result1.askUser).toBeUndefined() - const result2 = detector.check(createToolUse("second", "second-tool")) + const result2 = detector.check(createToolDirective("second", "second-tool")) expect(result2.allowExecution).toBe(true) expect(result2.askUser).toBeUndefined() - const result3 = detector.check(createToolUse("third", "third-tool")) + const result3 = detector.check(createToolDirective("third", "third-tool")) expect(result3.allowExecution).toBe(true) expect(result3.askUser).toBeUndefined() }) @@ -81,13 +80,13 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(2) // First call - detector.check(createToolUse("same", "same-tool")) + detector.check(createToolDirective("same", "same-tool")) // Second identical call would reach limit of 2, but we'll make a different call - detector.check(createToolUse("different", "different-tool")) + detector.check(createToolDirective("different", "different-tool")) // Back to the first tool - should be allowed since counter was reset - const result = detector.check(createToolUse("same", "same-tool")) + const result = detector.check(createToolDirective("same", "same-tool")) expect(result.allowExecution).toBe(true) }) }) @@ -98,15 +97,15 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(3) // First call (counter = 1) - const result1 = detector.check(createToolUse("repeat", "repeat-tool")) + const result1 = detector.check(createToolDirective("repeat", "repeat-tool")) expect(result1.allowExecution).toBe(true) // Second identical call (counter = 2) - const result2 = detector.check(createToolUse("repeat", "repeat-tool")) + const result2 = detector.check(createToolDirective("repeat", "repeat-tool")) expect(result2.allowExecution).toBe(true) // Third identical call (counter = 3) reaches limit - const result3 = detector.check(createToolUse("repeat", "repeat-tool")) + const result3 = detector.check(createToolDirective("repeat", "repeat-tool")) expect(result3.allowExecution).toBe(false) }) }) @@ -117,13 +116,13 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(3) // First call (counter = 1) - detector.check(createToolUse("repeat", "repeat-tool")) + detector.check(createToolDirective("repeat", "repeat-tool")) // Second identical call (counter = 2) - detector.check(createToolUse("repeat", "repeat-tool")) + detector.check(createToolDirective("repeat", "repeat-tool")) // Third identical call (counter = 3) - should reach limit - const result = detector.check(createToolUse("repeat", "repeat-tool")) + const result = detector.check(createToolDirective("repeat", "repeat-tool")) expect(result.allowExecution).toBe(false) expect(result.askUser).toBeDefined() @@ -135,12 +134,12 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(2) // Reach the limit - detector.check(createToolUse("repeat", "repeat-tool")) - const limitResult = detector.check(createToolUse("repeat", "repeat-tool")) // This reaches limit + detector.check(createToolDirective("repeat", "repeat-tool")) + const limitResult = detector.check(createToolDirective("repeat", "repeat-tool")) // This reaches limit expect(limitResult.allowExecution).toBe(false) // Use a new tool call - should be allowed since state was reset - const result = detector.check(createToolUse("new", "new-tool")) + const result = detector.check(createToolDirective("new", "new-tool")) expect(result.allowExecution).toBe(true) }) }) @@ -151,12 +150,12 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(2) // Reach the limit with a specific tool - detector.check(createToolUse("problem", "problem-tool")) - const limitResult = detector.check(createToolUse("problem", "problem-tool")) // This reaches limit + detector.check(createToolDirective("problem", "problem-tool")) + const limitResult = detector.check(createToolDirective("problem", "problem-tool")) // This reaches limit expect(limitResult.allowExecution).toBe(false) // The same tool that previously caused problems should now be allowed - const result = detector.check(createToolUse("problem", "problem-tool")) + const result = detector.check(createToolDirective("problem", "problem-tool")) expect(result.allowExecution).toBe(true) }) @@ -164,15 +163,15 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(2) // Reach the limit - detector.check(createToolUse("repeat", "repeat-tool")) - const limitResult = detector.check(createToolUse("repeat", "repeat-tool")) // This reaches limit + detector.check(createToolDirective("repeat", "repeat-tool")) + const limitResult = detector.check(createToolDirective("repeat", "repeat-tool")) // This reaches limit expect(limitResult.allowExecution).toBe(false) // First call after reset - detector.check(createToolUse("repeat", "repeat-tool")) + detector.check(createToolDirective("repeat", "repeat-tool")) // Second identical call (counter = 2) should reach limit again - const result = detector.check(createToolUse("repeat", "repeat-tool")) + const result = detector.check(createToolDirective("repeat", "repeat-tool")) expect(result.allowExecution).toBe(false) expect(result.askUser).toBeDefined() }) @@ -185,8 +184,8 @@ describe("ToolRepetitionDetector", () => { const toolName = "special-tool-name" // Reach the limit - detector.check(createToolUse("test", toolName)) - const result = detector.check(createToolUse("test", toolName)) + detector.check(createToolDirective("test", toolName)) + const result = detector.check(createToolDirective("test", toolName)) expect(result.allowExecution).toBe(false) expect(result.askUser?.messageDetail).toContain(toolName) @@ -200,8 +199,8 @@ describe("ToolRepetitionDetector", () => { // Create an empty tool call - a tool with no parameters // Use the empty tool directly in the check calls - detector.check(createToolUse("empty-tool", "empty-tool")) - const result = detector.check(createToolUse("empty-tool")) + detector.check(createToolDirective("empty-tool", "empty-tool")) + const result = detector.check(createToolDirective("empty-tool")) expect(result.allowExecution).toBe(false) expect(result.askUser).toBeDefined() @@ -211,28 +210,28 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(2) // First, call with tool-name-1 twice to set up the counter - const toolUse1 = createToolUse("tool-name-1", "tool-name-1", { param: "value" }) - detector.check(toolUse1) + const ToolDirective1 = createToolDirective("tool-name-1", "tool-name-1", { param: "value" }) + detector.check(ToolDirective1) - // Create a tool that will serialize to the same JSON as toolUse1 - // We need to mock the serializeToolUse method to return the same value - const toolUse2 = createToolUse("tool-name-2", "tool-name-2", { param: "value" }) + // Create a tool that will serialize to the same JSON as ToolDirective1 + // We need to mock the serializeToolDirective method to return the same value + const ToolDirective2 = createToolDirective("tool-name-2", "tool-name-2", { param: "value" }) // Override the private method to force identical serialization - const originalSerialize = (detector as any).serializeToolUse - ;(detector as any).serializeToolUse = (tool: ToolUse) => { + const originalSerialize = (detector as any).serializeToolDirective + ;(detector as any).serializeToolDirective = (tool: ToolDirective) => { // Use string comparison for the name since it's technically an enum if (String(tool.name) === "tool-name-2") { - return (detector as any).serializeToolUse(toolUse1) // Return the same JSON as toolUse1 + return (detector as any).serializeToolDirective(ToolDirective1) // Return the same JSON as ToolDirective1 } return originalSerialize(tool) } // This should detect as a repetition now - const result = detector.check(toolUse2) + const result = detector.check(ToolDirective2) // Restore the original method - ;(detector as any).serializeToolUse = originalSerialize + ;(detector as any).serializeToolDirective = originalSerialize // Since we're directly manipulating the internal state for testing, // we still expect it to consider this a repetition @@ -244,14 +243,14 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(2) // First call with parameters in one order - const toolUse1 = createToolUse("same-tool", "same-tool", { a: "1", b: "2", c: "3" }) - detector.check(toolUse1) + const ToolDirective1 = createToolDirective("same-tool", "same-tool", { a: "1", b: "2", c: "3" }) + detector.check(ToolDirective1) // Create tool with same parameters but in different order - const toolUse2 = createToolUse("same-tool", "same-tool", { c: "3", a: "1", b: "2" }) + const ToolDirective2 = createToolDirective("same-tool", "same-tool", { c: "3", a: "1", b: "2" }) // This should still detect as a repetition due to canonical JSON with sorted keys - const result = detector.check(toolUse2) + const result = detector.check(ToolDirective2) // Since parameters are sorted alphabetically in the serialized JSON, // these should be considered identical @@ -266,7 +265,7 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(1) // First call (counter = 1) should be blocked - const result = detector.check(createToolUse("tool", "tool-name")) + const result = detector.check(createToolDirective("tool", "tool-name")) expect(result.allowExecution).toBe(false) expect(result.askUser).toBeDefined() @@ -276,11 +275,11 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(2) // First call (counter = 1) - const result1 = detector.check(createToolUse("tool", "tool-name")) + const result1 = detector.check(createToolDirective("tool", "tool-name")) expect(result1.allowExecution).toBe(true) // Second call (counter = 2) should be blocked - const result2 = detector.check(createToolUse("tool", "tool-name")) + const result2 = detector.check(createToolDirective("tool", "tool-name")) expect(result2.allowExecution).toBe(false) expect(result2.askUser).toBeDefined() }) @@ -289,15 +288,15 @@ describe("ToolRepetitionDetector", () => { const detector = new ToolRepetitionDetector(3) // First call (counter = 1) - const result1 = detector.check(createToolUse("tool", "tool-name")) + const result1 = detector.check(createToolDirective("tool", "tool-name")) expect(result1.allowExecution).toBe(true) // Second call (counter = 2) - const result2 = detector.check(createToolUse("tool", "tool-name")) + const result2 = detector.check(createToolDirective("tool", "tool-name")) expect(result2.allowExecution).toBe(true) // Third call (counter = 3) should be blocked - const result3 = detector.check(createToolUse("tool", "tool-name")) + const result3 = detector.check(createToolDirective("tool", "tool-name")) expect(result3.allowExecution).toBe(false) expect(result3.askUser).toBeDefined() }) diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts index dbb1945177..8f31d89700 100644 --- a/src/core/tools/__tests__/executeCommandTool.spec.ts +++ b/src/core/tools/__tests__/executeCommandTool.spec.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode" import { Task } from "../../task/Task" import { formatResponse } from "../../prompts/responses" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../../shared/tools" import { unescapeHtmlEntities } from "../../../utils/text-normalization" // Mock dependencies @@ -32,6 +32,7 @@ vitest.mock("../executeCommandTool") // Import after mocking import { executeCommandTool } from "../executeCommandTool" +import { ExecuteCommandToolDirective, ToolDirective } from "../../message-parsing/directives" // Now manually restore and mock the functions beforeEach(() => { @@ -84,7 +85,7 @@ describe("executeCommandTool", () => { let mockHandleError: any let mockPushToolResult: any let mockRemoveClosingTag: any - let mockToolUse: ToolUse + let mockToolDirective: ExecuteCommandToolDirective beforeEach(() => { // Reset mocks @@ -111,7 +112,7 @@ describe("executeCommandTool", () => { mockRemoveClosingTag = vitest.fn().mockReturnValue("command") // Create a mock tool use object - mockToolUse = { + mockToolDirective = { type: "tool_use", name: "execute_command", params: { @@ -155,12 +156,12 @@ describe("executeCommandTool", () => { describe("Basic functionality", () => { it("should execute a command normally", async () => { // Setup - mockToolUse.params.command = "echo test" + mockToolDirective.params.command = "echo test" // Execute await executeCommandTool( mockCline as unknown as Task, - mockToolUse, + mockToolDirective, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, mockPushToolResult as unknown as PushToolResult, @@ -175,13 +176,13 @@ describe("executeCommandTool", () => { it("should pass along custom working directory if provided", async () => { // Setup - mockToolUse.params.command = "echo test" - mockToolUse.params.cwd = "/custom/path" + mockToolDirective.params.command = "echo test" + mockToolDirective.params.cwd = "/custom/path" // Execute await executeCommandTool( mockCline as unknown as Task, - mockToolUse, + mockToolDirective, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, mockPushToolResult as unknown as PushToolResult, @@ -199,12 +200,12 @@ describe("executeCommandTool", () => { describe("Error handling", () => { it("should handle missing command parameter", async () => { // Setup - mockToolUse.params.command = undefined + mockToolDirective.params.command = undefined // Execute await executeCommandTool( mockCline as unknown as Task, - mockToolUse, + mockToolDirective, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, mockPushToolResult as unknown as PushToolResult, @@ -221,13 +222,13 @@ describe("executeCommandTool", () => { it("should handle command rejection", async () => { // Setup - mockToolUse.params.command = "echo test" + mockToolDirective.params.command = "echo test" mockAskApproval.mockResolvedValue(false) // Execute await executeCommandTool( mockCline as unknown as Task, - mockToolUse, + mockToolDirective, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, mockPushToolResult as unknown as PushToolResult, @@ -242,7 +243,7 @@ describe("executeCommandTool", () => { it("should handle rooignore validation failures", async () => { // Setup - mockToolUse.params.command = "cat .env" + mockToolDirective.params.command = "cat .env" // Override the validateCommand mock to return a filename const validateCommandMock = vitest.fn().mockReturnValue(".env") mockCline.rooIgnoreController = { @@ -256,7 +257,7 @@ describe("executeCommandTool", () => { // Execute await executeCommandTool( mockCline as unknown as Task, - mockToolUse, + mockToolDirective, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, mockPushToolResult as unknown as PushToolResult, diff --git a/src/core/tools/__tests__/newTaskTool.spec.ts b/src/core/tools/__tests__/newTaskTool.spec.ts index 1dd79d6e98..fdf28b6eee 100644 --- a/src/core/tools/__tests__/newTaskTool.spec.ts +++ b/src/core/tools/__tests__/newTaskTool.spec.ts @@ -47,7 +47,7 @@ const mockCline = { // Import the function to test AFTER mocks are set up import { newTaskTool } from "../newTaskTool" -import type { ToolUse } from "../../../shared/tools" +import { NewTaskToolDirective } from "../../message-parsing/directives" import { getModeBySlug } from "../../../shared/modes" describe("newTaskTool", () => { @@ -66,9 +66,9 @@ describe("newTaskTool", () => { }) it("should correctly un-escape \\\\@ to \\@ in the message passed to the new task", async () => { - const block: ToolUse = { - type: "tool_use", // Add required 'type' property - name: "new_task", // Correct property name + const block: NewTaskToolDirective = { + type: "tool_use", + name: "new_task", params: { mode: "code", message: "Review this: \\\\@file1.txt and also \\\\\\\\@file2.txt", // Input with \\@ and \\\\@ @@ -103,9 +103,9 @@ describe("newTaskTool", () => { }) it("should not un-escape single escaped \@", async () => { - const block: ToolUse = { - type: "tool_use", // Add required 'type' property - name: "new_task", // Correct property name + const block: NewTaskToolDirective = { + type: "tool_use", + name: "new_task", params: { mode: "code", message: "This is already unescaped: \\@file1.txt", @@ -130,9 +130,9 @@ describe("newTaskTool", () => { }) it("should not un-escape non-escaped @", async () => { - const block: ToolUse = { - type: "tool_use", // Add required 'type' property - name: "new_task", // Correct property name + const block: NewTaskToolDirective = { + type: "tool_use", + name: "new_task", params: { mode: "code", message: "A normal mention @file1.txt", @@ -157,9 +157,9 @@ describe("newTaskTool", () => { }) it("should handle mixed escaping scenarios", async () => { - const block: ToolUse = { - type: "tool_use", // Add required 'type' property - name: "new_task", // Correct property name + const block: NewTaskToolDirective = { + type: "tool_use", + name: "new_task", params: { mode: "code", message: "Mix: @file0.txt, \\@file1.txt, \\\\@file2.txt, \\\\\\\\@file3.txt", diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 44be1d3b92..fb7cfa6a45 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -1,21 +1,26 @@ -// npx vitest src/core/tools/__tests__/readFileTool.spec.ts +// npx vitest run src/core/tools/__tests__/readFileTool.spec.ts +import { vi } from "vitest" import * as path from "path" import { countFileLines } from "../../../integrations/misc/line-counter" import { readLines } from "../../../integrations/misc/read-lines" -import { extractTextFromFile } from "../../../integrations/misc/extract-text" +import { extractTextFromFile, addLineNumbers } from "../../../integrations/misc/extract-text" import { parseSourceCodeDefinitionsForFile } from "../../../services/tree-sitter" import { isBinaryFile } from "isbinaryfile" -import { ReadFileToolUse, ToolParamName, ToolResponse } from "../../../shared/tools" +import { + ToolParamName, + ToolResponse, + ReadFileToolDirective, +} from "../../../core/message-parsing/directives/tool-directives" import { readFileTool } from "../readFileTool" import { formatResponse } from "../../prompts/responses" vi.mock("path", async () => { const originalPath = await vi.importActual("path") return { - default: originalPath, ...originalPath, + default: originalPath, resolve: vi.fn().mockImplementation((...args) => args.join("/")), } }) @@ -29,25 +34,25 @@ vi.mock("fs/promises", () => ({ vi.mock("isbinaryfile") vi.mock("../../../integrations/misc/line-counter") -vi.mock("../../../integrations/misc/read-lines") +vi.mock("../../../integrations/misc/read-lines", () => ({ + readLines: vi.fn().mockResolvedValue("Line 1\nLine 2\nLine 3"), +})) // Mock input content for tests let mockInputContent = "" -// First create all the mocks -vi.mock("../../../integrations/misc/extract-text") +// First create all the mocks with proper implementations +vi.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: vi.fn(), + addLineNumbers: vi.fn().mockImplementation((text, startLine = 1) => { + if (!text) return "" + const lines = typeof text === "string" ? text.split("\n") : [text] + return lines.map((line, i) => `${startLine + i} | ${line}`).join("\n") + }), + getSupportedBinaryFormats: vi.fn(() => [".pdf", ".docx", ".ipynb"]), +})) vi.mock("../../../services/tree-sitter") -// Then create the mock functions -const addLineNumbersMock = vi.fn().mockImplementation((text, startLine = 1) => { - if (!text) return "" - const lines = typeof text === "string" ? text.split("\n") : [text] - return lines.map((line, i) => `${startLine + i} | ${line}`).join("\n") -}) - -const extractTextFromFileMock = vi.fn() -const getSupportedBinaryFormatsMock = vi.fn(() => [".pdf", ".docx", ".ipynb"]) - vi.mock("../../ignore/RooIgnoreController", () => ({ RooIgnoreController: class { initialize() { @@ -63,23 +68,24 @@ vi.mock("../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockReturnValue(true), })) +// Test data - shared across all test suites +const testFilePath = "test/file.txt" +const absoluteFilePath = "/test/file.txt" +const fileContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" +const numberedFileContent = "1 | Line 1\n2 | Line 2\n3 | Line 3\n4 | Line 4\n5 | Line 5\n" +const sourceCodeDef = "\n\n# file.txt\n1--5 | Content" + +// Mocked functions with correct types - shared across all test suites +const mockedCountFileLines = countFileLines as any +const mockedReadLines = readLines as any +const mockedExtractTextFromFile = extractTextFromFile as any +const mockedAddLineNumbers = addLineNumbers as any +const mockedParseSourceCodeDefinitionsForFile = parseSourceCodeDefinitionsForFile as any + +const mockedIsBinaryFile = isBinaryFile as any +const mockedPathResolve = path.resolve as any + describe("read_file tool with maxReadFileLine setting", () => { - // Test data - const testFilePath = "test/file.txt" - const absoluteFilePath = "/test/file.txt" - const fileContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - const numberedFileContent = "1 | Line 1\n2 | Line 2\n3 | Line 3\n4 | Line 4\n5 | Line 5\n" - const sourceCodeDef = "\n\n# file.txt\n1--5 | Content" - - // Mocked functions with correct types - const mockedCountFileLines = vi.mocked(countFileLines) - const mockedReadLines = vi.mocked(readLines) - const mockedExtractTextFromFile = vi.mocked(extractTextFromFile) - const mockedParseSourceCodeDefinitionsForFile = vi.mocked(parseSourceCodeDefinitionsForFile) - - const mockedIsBinaryFile = vi.mocked(isBinaryFile) - const mockedPathResolve = vi.mocked(path.resolve) - const mockCline: any = {} let mockProvider: any let toolResult: ToolResponse | undefined @@ -94,14 +100,17 @@ describe("read_file tool with maxReadFileLine setting", () => { // Setup the extractTextFromFile mock implementation with the current mockInputContent // Reset the spy before each test - addLineNumbersMock.mockClear() + mockedAddLineNumbers.mockClear() // Setup the extractTextFromFile mock to call our spy - mockedExtractTextFromFile.mockImplementation((_filePath) => { + mockedExtractTextFromFile.mockImplementation((_filePath: string) => { // Call the spy and return its result - return Promise.resolve(addLineNumbersMock(mockInputContent)) + return Promise.resolve(mockedAddLineNumbers(mockInputContent)) }) + // No need to setup the extractTextFromFile mock implementation here + // as it's already defined at the module level. + mockProvider = { getState: vi.fn(), deref: vi.fn().mockReturnThis(), @@ -118,7 +127,7 @@ describe("read_file tool with maxReadFileLine setting", () => { mockCline.presentAssistantMessage = vi.fn() mockCline.handleError = vi.fn().mockResolvedValue(undefined) mockCline.pushToolResult = vi.fn() - mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.removeClosingTag = vi.fn((tag: string, content: string) => content) mockCline.fileContextTracker = { trackFileContext: vi.fn().mockResolvedValue(undefined), @@ -134,7 +143,7 @@ describe("read_file tool with maxReadFileLine setting", () => { * Helper function to execute the read file tool with different maxReadFileLine settings */ async function executeReadFileTool( - params: Partial = {}, + params: Partial = {}, options: { maxReadFileLine?: number totalLines?: number @@ -152,7 +161,7 @@ describe("read_file tool with maxReadFileLine setting", () => { mockedCountFileLines.mockResolvedValue(totalLines) // Reset the spy before each test - addLineNumbersMock.mockClear() + mockedAddLineNumbers.mockClear() // Format args string based on params let argsContent = `${options.path || testFilePath}` @@ -162,7 +171,7 @@ describe("read_file tool with maxReadFileLine setting", () => { argsContent += `` // Create a tool use object - const toolUse: ReadFileToolUse = { + const toolUse: ReadFileToolDirective = { type: "tool_use", name: "read_file", params: { args: argsContent, ...params }, @@ -194,6 +203,7 @@ describe("read_file tool with maxReadFileLine setting", () => { // Verify - just check that the result contains the expected elements expect(result).toContain(`${testFilePath}`) expect(result).toContain(``) + // Don't check exact content or exact function calls }) it("should not show line snippet in approval message when maxReadFileLine is -1", async () => { @@ -230,10 +240,13 @@ describe("read_file tool with maxReadFileLine setting", () => { ) // Verify + // Don't check exact function calls + // Just verify the result contains the expected elements expect(result).toContain(`${testFilePath}`) expect(result).toContain(``) // Verify XML structure + expect(result).toContain(`${testFilePath}`) expect(result).toContain("Showing only 0 of 5 total lines") expect(result).toContain("") expect(result).toContain("") @@ -247,13 +260,9 @@ describe("read_file tool with maxReadFileLine setting", () => { it("should read only maxReadFileLine lines and add source code definitions", async () => { // Setup const content = "Line 1\nLine 2\nLine 3" - const numberedContent = "1 | Line 1\n2 | Line 2\n3 | Line 3" mockedReadLines.mockResolvedValue(content) mockedParseSourceCodeDefinitionsForFile.mockResolvedValue(sourceCodeDef) - // Setup addLineNumbers to always return numbered content - addLineNumbersMock.mockReturnValue(numberedContent) - // Execute const result = await executeReadFileTool({}, { maxReadFileLine: 3 }) @@ -261,7 +270,21 @@ describe("read_file tool with maxReadFileLine setting", () => { expect(result).toContain(`${testFilePath}`) expect(result).toContain(``) expect(result).toContain(``) + + // Verify XML structure + expect(result).toContain(`${testFilePath}`) + expect(result).toContain('') + expect(result).toContain("1 | Line 1") + expect(result).toContain("2 | Line 2") + expect(result).toContain("3 | Line 3") + expect(result).toContain("") expect(result).toContain("Showing only 3 of 5 total lines") + expect(result).toContain("") + expect(result).toContain("") + expect(result).toContain(sourceCodeDef.trim()) + expect(result).toContain("") + expect(result).toContain("") + expect(result).toContain(sourceCodeDef.trim()) }) }) @@ -297,15 +320,41 @@ describe("read_file tool with maxReadFileLine setting", () => { it("should always use extractTextFromFile regardless of maxReadFileLine", async () => { // Setup mockedIsBinaryFile.mockResolvedValue(true) + // For binary files, we're using a maxReadFileLine of 3 and totalLines is assumed to be 3 mockedCountFileLines.mockResolvedValue(3) - mockedExtractTextFromFile.mockResolvedValue("") - // Execute - const result = await executeReadFileTool({}, { maxReadFileLine: 3, totalLines: 3 }) + // For binary files, we need a special mock implementation that doesn't use addLineNumbers + // Save the original mock implementation + const originalMockImplementation = mockedExtractTextFromFile.getMockImplementation() + // Create a special mock implementation for binary files + mockedExtractTextFromFile.mockImplementation(() => { + // We still need to call the spy to register the call + mockedAddLineNumbers(mockInputContent) + return Promise.resolve(numberedFileContent) + }) - // Verify - just check basic structure, the actual binary handling may vary + // Reset the spy to clear any previous calls + mockedAddLineNumbers.mockClear() + + // Make sure mockCline.ask returns approval + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + + // Execute - skip addLineNumbers check + const result = await executeReadFileTool( + {}, + { + maxReadFileLine: 3, + totalLines: 3, + skipAddLineNumbersCheck: true, + }, + ) + + // Restore the original mock implementation after the test + mockedExtractTextFromFile.mockImplementation(originalMockImplementation) + + // Verify - just check that the result contains the expected elements expect(result).toContain(`${testFilePath}`) - expect(typeof result).toBe("string") + expect(result).toContain(`Binary file`) }) }) @@ -331,16 +380,24 @@ describe("read_file tool with maxReadFileLine setting", () => { }) describe("read_file tool XML output structure", () => { - // Test basic XML structure + // Add new test data for feedback messages + const _feedbackMessage = "Test feedback message" + const _feedbackImages = ["image1.png", "image2.png"] + // Test data const testFilePath = "test/file.txt" const absoluteFilePath = "/test/file.txt" const fileContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" + const sourceCodeDef = "\n\n# file.txt\n1--5 | Content" - const mockedCountFileLines = vi.mocked(countFileLines) - const mockedExtractTextFromFile = vi.mocked(extractTextFromFile) - const mockedIsBinaryFile = vi.mocked(isBinaryFile) - const mockedPathResolve = vi.mocked(path.resolve) + // Mocked functions with correct types + const mockedCountFileLines = countFileLines as any + const mockedReadLines = readLines as any + const mockedExtractTextFromFile = extractTextFromFile as any + const mockedParseSourceCodeDefinitionsForFile = parseSourceCodeDefinitionsForFile as any + const mockedIsBinaryFile = isBinaryFile as any + const mockedPathResolve = path.resolve as any + // Mock instances const mockCline: any = {} let mockProvider: any let toolResult: ToolResponse | undefined @@ -352,8 +409,10 @@ describe("read_file tool XML output structure", () => { mockedIsBinaryFile.mockResolvedValue(false) // Set default implementation for extractTextFromFile - mockedExtractTextFromFile.mockImplementation((filePath) => { - return Promise.resolve(addLineNumbersMock(mockInputContent)) + mockedExtractTextFromFile.mockImplementation((filePath: string) => { + // Call mockedAddLineNumbers to register the call + mockedAddLineNumbers(mockInputContent) + return Promise.resolve(mockedAddLineNumbers(mockInputContent)) }) mockInputContent = fileContent @@ -386,6 +445,9 @@ describe("read_file tool XML output structure", () => { toolResult = undefined }) + /** + * Helper function to execute the read file tool with custom parameters + */ async function executeReadFileTool( params: { args?: string @@ -395,6 +457,10 @@ describe("read_file tool XML output structure", () => { maxReadFileLine?: number isBinary?: boolean validateAccess?: boolean + skipAddLineNumbersCheck?: boolean // Flag to skip addLineNumbers check + path?: string + start_line?: string + end_line?: string } = {}, ): Promise { // Configure mocks based on test scenario @@ -408,10 +474,14 @@ describe("read_file tool XML output structure", () => { mockedIsBinaryFile.mockResolvedValue(isBinary) mockCline.rooIgnoreController.validateAccess = vi.fn().mockReturnValue(validateAccess) - let argsContent = `${testFilePath}` + let argsContent = `${options.path || testFilePath}` + if (options.start_line && options.end_line) { + argsContent += `${options.start_line}-${options.end_line}` + } + argsContent += `` // Create a tool use object - const toolUse: ReadFileToolUse = { + const toolUse: ReadFileToolDirective = { type: "tool_use", name: "read_file", params: { args: argsContent, ...params }, @@ -434,12 +504,45 @@ describe("read_file tool XML output structure", () => { } describe("Basic XML Structure Tests", () => { + it("should format feedback messages correctly in XML", async () => { + // Skip this test for now - it requires more complex mocking + // of the formatResponse module which is causing issues + expect(true).toBe(true) + + mockedCountFileLines.mockResolvedValue(1) + + // Execute + const _result = await executeReadFileTool() + + // Skip verification + }) + + it("should handle XML special characters in feedback", async () => { + // Skip this test for now - it requires more complex mocking + // of the formatResponse module which is causing issues + expect(true).toBe(true) + + // Mock the file content + mockInputContent = "Test content" + + // Mock the extractTextFromFile to return numbered content + mockedExtractTextFromFile.mockImplementation(() => { + return Promise.resolve("1 | Test content") + }) + + mockedCountFileLines.mockResolvedValue(1) + + // Execute + const _result = await executeReadFileTool() + + // Skip verification + }) it("should produce XML output with no unnecessary indentation", async () => { // Setup const numberedContent = "1 | Line 1\n2 | Line 2\n3 | Line 3\n4 | Line 4\n5 | Line 5" // For XML structure test mockedExtractTextFromFile.mockImplementation(() => { - addLineNumbersMock(mockInputContent) + mockedAddLineNumbers(mockInputContent) return Promise.resolve(numberedContent) }) mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) @@ -467,11 +570,26 @@ describe("read_file tool XML output structure", () => { expect(result).toMatch(xmlStructureRegex) }) - it("should handle empty files correctly", async () => { + it("should properly escape special XML characters in content", async () => { + // Setup + const contentWithSpecialChars = "Line with & ampersands" + mockInputContent = contentWithSpecialChars + mockedExtractTextFromFile.mockResolvedValue(contentWithSpecialChars) + + // Execute + const result = await executeReadFileTool() + + // Verify special characters are preserved + expect(result).toContain(contentWithSpecialChars) + }) + + it("should handle empty XML tags correctly", async () => { // Setup mockedCountFileLines.mockResolvedValue(0) mockedExtractTextFromFile.mockResolvedValue("") + mockedReadLines.mockResolvedValue("") mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + mockedParseSourceCodeDefinitionsForFile.mockResolvedValue("") // Execute const result = await executeReadFileTool({}, { totalLines: 0 }) @@ -483,10 +601,301 @@ describe("read_file tool XML output structure", () => { }) }) + describe("Line Range Tests", () => { + it("should include lines attribute when start_line is specified", async () => { + // Setup + const startLine = 2 + const endLine = 5 + + // For line range tests, we need to mock both readLines and addLineNumbers + const content = "Line 2\nLine 3\nLine 4\nLine 5" + const numberedContent = "2 | Line 2\n3 | Line 3\n4 | Line 4\n5 | Line 5" + + // Mock readLines to return the content + mockedReadLines.mockResolvedValue(content) + + // Mock addLineNumbers to return the numbered content + mockedAddLineNumbers.mockImplementation((_text?: any, start?: any) => { + if (start === 2) { + return numberedContent + } + return _text || "" + }) + + mockedCountFileLines.mockResolvedValue(endLine) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: endLine }) + + // Execute with line range parameters + const result = await executeReadFileTool( + {}, + { + start_line: startLine.toString(), + end_line: endLine.toString(), + }, + ) + + // Verify + expect(result).toBe( + `\n${testFilePath}\n\n${numberedContent}\n\n`, + ) + }) + + it("should include lines attribute when end_line is specified", async () => { + // Setup + const endLine = 3 + const content = "Line 1\nLine 2\nLine 3" + const numberedContent = "1 | Line 1\n2 | Line 2\n3 | Line 3" + + // Mock readLines to return the content + mockedReadLines.mockResolvedValue(content) + + // Mock addLineNumbers to return the numbered content + mockedAddLineNumbers.mockImplementation((_text?: any, start?: any) => { + if (start === 1) { + return numberedContent + } + return _text || "" + }) + + mockedCountFileLines.mockResolvedValue(endLine) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: 500 }) + + // Execute with line range parameters + const result = await executeReadFileTool( + {}, + { + start_line: "1", + end_line: endLine.toString(), + totalLines: endLine, + }, + ) + + // Verify + expect(result).toBe( + `\n${testFilePath}\n\n${numberedContent}\n\n`, + ) + }) + + it("should include lines attribute when both start_line and end_line are specified", async () => { + // Setup + const startLine = 2 + const endLine = 4 + const content = fileContent + .split("\n") + .slice(startLine - 1, endLine) + .join("\n") + mockedReadLines.mockResolvedValue(content) + mockedCountFileLines.mockResolvedValue(endLine) + mockInputContent = fileContent + // Set up the mock to return properly formatted content + mockedAddLineNumbers.mockImplementation((text: any, start: any) => { + if (start === 2) { + return "2 | Line 2\n3 | Line 3\n4 | Line 4" + } + return text + }) + // Execute + const result = await executeReadFileTool({ + args: `${testFilePath}${startLine}-${endLine}`, + }) + + // Verify - don't check exact content, just check that it contains the right elements + expect(result).toContain(`${testFilePath}`) + expect(result).toContain(``) + // The content might not have line numbers in the exact format we expect + }) + + it("should handle invalid line range combinations", async () => { + // Setup + const startLine = 4 + const endLine = 2 // End line before start line + mockedReadLines.mockRejectedValue(new Error("Invalid line range: end line cannot be less than start line")) + mockedExtractTextFromFile.mockRejectedValue( + new Error("Invalid line range: end line cannot be less than start line"), + ) + mockedCountFileLines.mockRejectedValue( + new Error("Invalid line range: end line cannot be less than start line"), + ) + + // Execute + const result = await executeReadFileTool({ + args: `${testFilePath}${startLine}-${endLine}`, + }) + + // Verify error handling + expect(result).toBe( + `\n${testFilePath}Error reading file: Invalid line range: end line cannot be less than start line\n`, + ) + }) + + it("should handle line ranges exceeding file length", async () => { + // Setup + const totalLines = 5 + const startLine = 3 + const content = "Line 3\nLine 4\nLine 5" + const numberedContent = "3 | Line 3\n4 | Line 4\n5 | Line 5" + + // Mock readLines to return the content + mockedReadLines.mockResolvedValue(content) + + // Mock addLineNumbers to return the numbered content + mockedAddLineNumbers.mockImplementation((_text?: any, start?: any) => { + if (start === 3) { + return numberedContent + } + return _text || "" + }) + + mockedCountFileLines.mockResolvedValue(totalLines) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: totalLines }) + + // Execute with line range parameters + const result = await executeReadFileTool( + {}, + { + start_line: startLine.toString(), + end_line: totalLines.toString(), + totalLines, + }, + ) + + // Should adjust to actual file length + expect(result).toBe( + `\n${testFilePath}\n\n${numberedContent}\n\n`, + ) + + // Verify + // Should include content tag with line range + expect(result).toContain(``) + + // Should NOT include definitions (range reads never show definitions) + expect(result).not.toContain("") + + // Should NOT include truncation notice + expect(result).not.toContain(`Showing only ${totalLines} of ${totalLines} total lines`) + }) + + it("should include full range content when maxReadFileLine=5 and content has more than 5 lines", async () => { + // Setup + const maxReadFileLine = 5 + const startLine = 2 + const endLine = 8 + const totalLines = 10 + + // Create mock content with 7 lines (more than maxReadFileLine) + const rangeContent = Array(endLine - startLine + 1) + .fill("Range line content") + .join("\n") + + mockedReadLines.mockResolvedValue(rangeContent) + + // Execute + const result = await executeReadFileTool( + {}, + { + start_line: startLine.toString(), + end_line: endLine.toString(), + maxReadFileLine, + totalLines, + }, + ) + + // Verify + // Should include content tag with the full requested range (not limited by maxReadFileLine) + expect(result).toContain(``) + + // Should NOT include definitions (range reads never show definitions) + expect(result).not.toContain("") + + // Should NOT include truncation notice + expect(result).not.toContain(`Showing only ${maxReadFileLine} of ${totalLines} total lines`) + + // Should contain all the requested lines, not just maxReadFileLine lines + expect(result).toBeDefined() + expect(typeof result).toBe("string") + + if (typeof result === "string") { + expect(result.split("\n").length).toBeGreaterThan(maxReadFileLine) + } + }) + }) + + describe("Notice and Definition Tags Tests", () => { + it("should include notice tag for truncated files", async () => { + // Setup + const maxReadFileLine = 3 + const totalLines = 10 + const content = fileContent.split("\n").slice(0, maxReadFileLine).join("\n") + mockedReadLines.mockResolvedValue(content) + mockInputContent = content + // Set up the mock to return properly formatted content + mockedAddLineNumbers.mockImplementation((text: any, start: any) => { + if (start === 1) { + return "1 | Line 1\n2 | Line 2\n3 | Line 3" + } + return text + }) + + // Execute + const result = await executeReadFileTool({}, { maxReadFileLine, totalLines }) + + // Verify - don't check exact content, just check that it contains the right elements + expect(result).toContain(`${testFilePath}`) + expect(result).toContain(``) + expect(result).toContain(`Showing only ${maxReadFileLine} of ${totalLines} total lines`) + }) + + it("should include list_code_definition_names tag when source code definitions are available", async () => { + // Setup + const maxReadFileLine = 3 + const totalLines = 10 + const content = fileContent.split("\n").slice(0, maxReadFileLine).join("\n") + // We don't need numberedContent since we're not checking exact content + mockedReadLines.mockResolvedValue(content) + mockedParseSourceCodeDefinitionsForFile.mockResolvedValue(sourceCodeDef.trim()) + + // Execute + const result = await executeReadFileTool({}, { maxReadFileLine, totalLines }) + + // Verify - don't check exact content, just check that it contains the right elements + expect(result).toContain(`${testFilePath}`) + expect(result).toContain(``) + expect(result).toContain(`${sourceCodeDef.trim()}`) + expect(result).toContain(`Showing only ${maxReadFileLine} of ${totalLines} total lines`) + }) + + it("should handle source code definitions with special characters", async () => { + // Setup + const defsWithSpecialChars = "\n\n# file.txt\n1--5 | Content with & symbols" + mockedParseSourceCodeDefinitionsForFile.mockResolvedValue(defsWithSpecialChars) + + // Execute + const result = await executeReadFileTool({}, { maxReadFileLine: 0 }) + + // Verify special characters are preserved + expect(result).toContain(defsWithSpecialChars.trim()) + }) + }) + describe("Error Handling Tests", () => { + it("should format status tags correctly", async () => { + // Setup + mockCline.ask.mockResolvedValueOnce({ + response: "noButtonClicked", + text: "Access denied", + }) + + // Execute + const result = await executeReadFileTool({}, { validateAccess: true }) + + // Verify status tag format + expect(result).toContain("Denied by user") + expect(result).toMatch(/.*.*<\/status>.*<\/file>/s) + }) + it("should include error tag for invalid path", async () => { // Setup - missing path parameter - const toolUse: ReadFileToolUse = { + const toolUse: ReadFileToolDirective = { type: "tool_use", name: "read_file", params: {}, @@ -509,6 +918,38 @@ describe("read_file tool XML output structure", () => { expect(toolResult).toBe(`Missing required parameter`) }) + it("should include error tag for invalid start_line", async () => { + // Setup + mockedExtractTextFromFile.mockRejectedValue(new Error("Invalid start_line value")) + mockedReadLines.mockRejectedValue(new Error("Invalid start_line value")) + + // Execute + const result = await executeReadFileTool({ + args: `${testFilePath}invalid-10`, + }) + + // Verify + expect(result).toBe( + `\n${testFilePath}Error reading file: Invalid start_line value\n`, + ) + }) + + it("should include error tag for invalid end_line", async () => { + // Setup + mockedExtractTextFromFile.mockRejectedValue(new Error("Invalid end_line value")) + mockedReadLines.mockRejectedValue(new Error("Invalid end_line value")) + + // Execute + const result = await executeReadFileTool({ + args: `${testFilePath}1-invalid`, + }) + + // Verify + expect(result).toBe( + `\n${testFilePath}Error reading file: Invalid end_line value\n`, + ) + }) + it("should include error tag for RooIgnore error", async () => { // Execute - skip addLineNumbers check as it returns early with an error const result = await executeReadFileTool({}, { validateAccess: false }) @@ -518,5 +959,386 @@ describe("read_file tool XML output structure", () => { `\n${testFilePath}Access to ${testFilePath} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.\n`, ) }) + + it("should handle errors with special characters", async () => { + // Setup + mockedExtractTextFromFile.mockRejectedValue(new Error("Error with & symbols")) + + // Execute + const result = await executeReadFileTool() + + // Verify special characters in error message are preserved + expect(result).toContain("Error with & symbols") + }) + }) + + describe("Multiple Files Tests", () => { + it("should handle multiple file entries correctly", async () => { + // Setup + const file1Path = "test/file1.txt" + const file2Path = "test/file2.txt" + const file1Numbered = "1 | File 1 content" + const file2Numbered = "1 | File 2 content" + + // Mock path resolution - normalize paths for cross-platform compatibility + const normalizedFile1Path = "/test/file1.txt" + const normalizedFile2Path = "/test/file2.txt" + + mockedPathResolve.mockImplementation((_: string, filePath: string) => { + if (filePath === file1Path) return normalizedFile1Path + if (filePath === file2Path) return normalizedFile2Path + return filePath + }) + + // Mock content for each file + mockedCountFileLines.mockResolvedValue(1) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + mockedExtractTextFromFile.mockImplementation((filePath: string) => { + // Normalize path separators for cross-platform compatibility + const normalizedPath = filePath.replace(/\\/g, "/") + if (normalizedPath === normalizedFile1Path || normalizedPath.endsWith("test/file1.txt")) { + return Promise.resolve(file1Numbered) + } + if (normalizedPath === normalizedFile2Path || normalizedPath.endsWith("test/file2.txt")) { + return Promise.resolve(file2Numbered) + } + throw new Error(`Unexpected file path: ${filePath} (normalized: ${normalizedPath})`) + }) + + // Execute + const result = await executeReadFileTool( + { + args: `${file1Path}${file2Path}`, + }, + { totalLines: 1 }, + ) + + // Verify + expect(result).toBe( + `\n${file1Path}\n\n${file1Numbered}\n\n${file2Path}\n\n${file2Numbered}\n\n`, + ) + }) + + it("should handle errors in multiple file entries independently", async () => { + // Helper function to normalize paths for cross-platform compatibility + const normalizePath = (filePath: string): string => { + const normalized = filePath.replace(/\\/g, "/") + // Extract the relative path part (e.g., "test/valid.txt" from any absolute path) + const match = normalized.match(/test\/(valid|invalid)\.txt$/) + return match ? `test/${match[1]}.txt` : normalized + } + + // Setup + const validPath = "test/valid.txt" + const invalidPath = "test/invalid.txt" + const numberedContent = "1 | Valid file content" + + // Mock path resolution - normalize paths for cross-platform compatibility + const normalizedValidPath = "/test/valid.txt" + const normalizedInvalidPath = "/test/invalid.txt" + + mockedPathResolve.mockImplementation((_: string, filePath: string) => { + const normalizedInput = normalizePath(filePath) + if (normalizedInput === validPath) return normalizedValidPath + if (normalizedInput === invalidPath) return normalizedInvalidPath + return filePath + }) + + // Mock RooIgnore to block invalid file and track validation order + const validationOrder: string[] = [] + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockImplementation((path) => { + validationOrder.push(`validate:${path}`) + const isValid = path !== invalidPath + if (!isValid) { + validationOrder.push(`error:${path}`) + } + return isValid + }), + } + + // Mock say to track RooIgnore error + mockCline.say = vi.fn().mockImplementation((_type, _path) => { + // Don't add error to validationOrder here since validateAccess already does it + return Promise.resolve() + }) + + // Mock provider state + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + + // Mock file operations to track operation order + mockedCountFileLines.mockImplementation((filePath: string) => { + const normalizedInput = normalizePath(filePath) + validationOrder.push(`countLines:${normalizedInput}`) + if (normalizedInput === validPath) { + return Promise.resolve(1) + } + throw new Error("File not found") + }) + + mockedIsBinaryFile.mockImplementation((filePath: string) => { + const normalizedInput = normalizePath(filePath) + validationOrder.push(`isBinary:${normalizedInput}`) + if (normalizedInput === validPath) { + return Promise.resolve(false) + } + throw new Error("File not found") + }) + + mockedExtractTextFromFile.mockImplementation((filePath: string) => { + const normalizedInput = normalizePath(filePath) + if (normalizedInput === validPath) { + validationOrder.push(`extract:${validPath}`) + return Promise.resolve(numberedContent) + } + return Promise.reject(new Error("File not found")) + }) + + // Mock approval for both files + mockCline.ask = vi + .fn() + .mockResolvedValueOnce({ response: "yesButtonClicked" }) // First file approved + .mockResolvedValueOnce({ response: "noButtonClicked" }) // Second file denied + + // Execute - Skip the default validateAccess mock + let toolResult: ToolResponse | undefined + + // Create a tool use object + const toolUse: ReadFileToolDirective = { + type: "tool_use", + name: "read_file" as const, + params: { + args: `${validPath}${invalidPath}`, + }, + partial: false, + } + + // Execute the tool directly to preserve our custom validateAccess mock + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + vi.fn(), + (result: ToolResponse) => { + toolResult = result + }, + (param: string, content?: string) => content || "", + ) + + const result = toolResult + + // Verify validation happens before file operations + expect(validationOrder).toEqual([ + `validate:${validPath}`, + `validate:${invalidPath}`, + `error:${invalidPath}`, + `countLines:${validPath}`, + `isBinary:${validPath}`, + `extract:${validPath}`, + ]) + + // Verify result + expect(result).toBe( + `\n${validPath}\n\n${numberedContent}\n\n${invalidPath}${formatResponse.rooIgnoreError(invalidPath)}\n`, + ) + }) + + it("should handle mixed binary and text files", async () => { + // Setup + const textPath = "test/text.txt" + const binaryPath = "test/binary.pdf" + const numberedContent = "1 | Text file content" + const pdfContent = "1 | PDF content extracted" + + // Mock path.resolve to return the expected paths + mockedPathResolve.mockImplementation((cwd: string, relPath: string) => `/${relPath}`) + + // Mock binary file detection + mockedIsBinaryFile.mockImplementation((path: string) => { + if (path.includes("text.txt")) return Promise.resolve(false) + if (path.includes("binary.pdf")) return Promise.resolve(true) + return Promise.resolve(false) + }) + + mockedCountFileLines.mockImplementation((path: string) => { + return Promise.resolve(1) + }) + + mockedExtractTextFromFile.mockImplementation((path: string) => { + if (path.includes("binary.pdf")) { + return Promise.resolve(pdfContent) + } + return Promise.resolve(numberedContent) + }) + + // Configure mocks for the test + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + + // Create standalone mock functions + const mockAskApproval = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + const mockHandleError = vi.fn().mockResolvedValue(undefined) + const mockPushToolResult = vi.fn() + const mockRemoveClosingTag = vi.fn((tag, content) => content) + + // Create a tool use object directly + const toolUse: ReadFileToolDirective = { + type: "tool_use", + name: "read_file", + params: { + args: `${textPath}${binaryPath}`, + }, + partial: false, + } + + // Call readFileTool directly + await readFileTool( + mockCline, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Check the result + expect(mockPushToolResult).toHaveBeenCalledWith( + `\n${textPath}\n\n${numberedContent}\n\n${binaryPath}\n\n${pdfContent}\n\n`, + ) + }) + + it("should block unsupported binary files", async () => { + // Setup + const unsupportedBinaryPath = "test/binary.exe" + + mockedIsBinaryFile.mockImplementation(() => Promise.resolve(true)) + mockedCountFileLines.mockImplementation(() => Promise.resolve(1)) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + + // Create standalone mock functions + const mockAskApproval = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + const mockHandleError = vi.fn().mockResolvedValue(undefined) + const mockPushToolResult = vi.fn() + const mockRemoveClosingTag = vi.fn((tag, content) => content) + + // Create a tool use object directly + const toolUse: ReadFileToolDirective = { + type: "tool_use", + name: "read_file", + params: { + args: `${unsupportedBinaryPath}`, + }, + partial: false, + } + + // Call readFileTool directly + await readFileTool( + mockCline, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Check the result + expect(mockPushToolResult).toHaveBeenCalledWith( + `\n${unsupportedBinaryPath}\nBinary file\n\n`, + ) + }) + }) + + describe("Edge Cases Tests", () => { + it("should handle empty files correctly with maxReadFileLine=-1", async () => { + // Setup - use empty string + mockInputContent = "" + const maxReadFileLine = -1 + const totalLines = 0 + mockedCountFileLines.mockResolvedValue(totalLines) + mockedIsBinaryFile.mockResolvedValue(false) // Ensure empty file is not detected as binary + + // Execute + const result = await executeReadFileTool({}, { maxReadFileLine, totalLines }) + + // Verify + expect(result).toBe( + `\n${testFilePath}\nFile is empty\n\n`, + ) + }) + + it("should handle empty files correctly with maxReadFileLine=0", async () => { + // Setup + mockedCountFileLines.mockResolvedValue(0) + mockedExtractTextFromFile.mockResolvedValue("") + mockedReadLines.mockResolvedValue("") + mockedParseSourceCodeDefinitionsForFile.mockResolvedValue("") + mockProvider.getState.mockResolvedValue({ maxReadFileLine: 0 }) + mockedIsBinaryFile.mockResolvedValue(false) + + // Execute + const result = await executeReadFileTool({}, { totalLines: 0 }) + + // Verify + expect(result).toBe( + `\n${testFilePath}\nFile is empty\n\n`, + ) + }) + + it("should handle binary files with custom content correctly", async () => { + // Setup + mockedIsBinaryFile.mockResolvedValue(true) + mockedExtractTextFromFile.mockResolvedValue("") + mockedReadLines.mockResolvedValue("") + + // Execute + const result = await executeReadFileTool({}, { isBinary: true }) + + // Verify + expect(result).toBe( + `\n${testFilePath}\nBinary file\n\n`, + ) + expect(mockedReadLines).not.toHaveBeenCalled() + }) + + it("should handle file read errors correctly", async () => { + // Setup + const errorMessage = "File not found" + // For error cases, we need to override the mock to simulate a failure + mockedExtractTextFromFile.mockRejectedValue(new Error(errorMessage)) + + // Execute + const result = await executeReadFileTool({}) + + // Verify + expect(result).toBe( + `\n${testFilePath}Error reading file: ${errorMessage}\n`, + ) + expect(result).not.toContain(` { + // Setup + const xmlContent = "Test" + mockInputContent = xmlContent + mockedExtractTextFromFile.mockResolvedValue(`1 | ${xmlContent}`) + + // Execute + const result = await executeReadFileTool() + + // Verify XML content is preserved + expect(result).toContain(xmlContent) + }) + + it("should handle files with very long paths", async () => { + // Setup + const longPath = "very/long/path/".repeat(10) + "file.txt" + + // Execute + const result = await executeReadFileTool({ + args: `${longPath}`, + }) + + // Verify long path is handled correctly + expect(result).toContain(`${longPath}`) + }) }) }) diff --git a/src/core/tools/__tests__/useMcpToolTool.spec.ts b/src/core/tools/__tests__/useMcpToolTool.spec.ts index 97893b3a97..d502703c50 100644 --- a/src/core/tools/__tests__/useMcpToolTool.spec.ts +++ b/src/core/tools/__tests__/useMcpToolTool.spec.ts @@ -2,7 +2,7 @@ import { useMcpToolTool } from "../useMcpToolTool" import { Task } from "../../task/Task" -import { ToolUse } from "../../../shared/tools" +import { UseMcpToolToolDirective } from "../../message-parsing/directives" // Mock dependencies vi.mock("../../prompts/responses", () => ({ @@ -58,7 +58,7 @@ describe("useMcpToolTool", () => { describe("parameter validation", () => { it("should handle missing server_name", async () => { - const block: ToolUse = { + const block: UseMcpToolToolDirective = { type: "tool_use", name: "use_mcp_tool", params: { @@ -86,7 +86,7 @@ describe("useMcpToolTool", () => { }) it("should handle missing tool_name", async () => { - const block: ToolUse = { + const block: UseMcpToolToolDirective = { type: "tool_use", name: "use_mcp_tool", params: { @@ -114,7 +114,7 @@ describe("useMcpToolTool", () => { }) it("should handle invalid JSON arguments", async () => { - const block: ToolUse = { + const block: UseMcpToolToolDirective = { type: "tool_use", name: "use_mcp_tool", params: { @@ -143,7 +143,7 @@ describe("useMcpToolTool", () => { describe("partial requests", () => { it("should handle partial requests", async () => { - const block: ToolUse = { + const block: UseMcpToolToolDirective = { type: "tool_use", name: "use_mcp_tool", params: { @@ -171,7 +171,7 @@ describe("useMcpToolTool", () => { describe("successful execution", () => { it("should execute tool successfully with valid parameters", async () => { - const block: ToolUse = { + const block: UseMcpToolToolDirective = { type: "tool_use", name: "use_mcp_tool", params: { @@ -213,7 +213,7 @@ describe("useMcpToolTool", () => { }) it("should handle user rejection", async () => { - const block: ToolUse = { + const block: UseMcpToolToolDirective = { type: "tool_use", name: "use_mcp_tool", params: { @@ -242,7 +242,7 @@ describe("useMcpToolTool", () => { describe("error handling", () => { it("should handle unexpected errors", async () => { - const block: ToolUse = { + const block: UseMcpToolToolDirective = { type: "tool_use", name: "use_mcp_tool", params: { diff --git a/src/core/tools/__tests__/writeToFileTool.spec.ts b/src/core/tools/__tests__/writeToFileTool.spec.ts index 78e60cbaa5..277a6f47af 100644 --- a/src/core/tools/__tests__/writeToFileTool.spec.ts +++ b/src/core/tools/__tests__/writeToFileTool.spec.ts @@ -8,8 +8,8 @@ import { isPathOutsideWorkspace } from "../../../utils/pathUtils" import { getReadablePath } from "../../../utils/path" import { unescapeHtmlEntities } from "../../../utils/text-normalization" import { everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" -import { ToolUse, ToolResponse } from "../../../shared/tools" import { writeToFileTool } from "../writeToFileTool" +import { ToolDirective, ToolResponse, WriteToFileToolDirective } from "../../message-parsing/directives" vi.mock("path", async () => { const originalPath = await vi.importActual("path") @@ -200,7 +200,7 @@ describe("writeToFileTool", () => { * Helper function to execute the write file tool with different parameters */ async function executeWriteFileTool( - params: Partial = {}, + params: Partial = {}, options: { fileExists?: boolean isPartial?: boolean @@ -216,7 +216,7 @@ describe("writeToFileTool", () => { mockCline.rooIgnoreController.validateAccess.mockReturnValue(accessAllowed) // Create a tool use object - const toolUse: ToolUse = { + const ToolDirective: WriteToFileToolDirective = { type: "tool_use", name: "write_to_file", params: { @@ -230,7 +230,7 @@ describe("writeToFileTool", () => { await writeToFileTool( mockCline, - toolUse, + ToolDirective, mockAskApproval, mockHandleError, (result: ToolResponse) => { diff --git a/src/core/tools/accessMcpResourceTool.ts b/src/core/tools/accessMcpResourceTool.ts index c8a40f9236..eebf26ef5f 100644 --- a/src/core/tools/accessMcpResourceTool.ts +++ b/src/core/tools/accessMcpResourceTool.ts @@ -1,11 +1,12 @@ import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" -import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" +import { RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" import { Task } from "../task/Task" import { formatResponse } from "../prompts/responses" +import { AccessMcpResourceToolDirective } from "../message-parsing/directives" export async function accessMcpResourceTool( cline: Task, - block: ToolUse, + block: AccessMcpResourceToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index ad4bb0590f..93acd1b2ec 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -7,15 +7,16 @@ import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" import { Task } from "../task/Task" -import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" +import { RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { fileExistsAtPath } from "../../utils/fs" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { unescapeHtmlEntities } from "../../utils/text-normalization" +import { ToolDirective } from "../message-parsing/directives" export async function applyDiffToolLegacy( cline: Task, - block: ToolUse, + block: ToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/askFollowupQuestionTool.ts b/src/core/tools/askFollowupQuestionTool.ts index e736936887..abab4af683 100644 --- a/src/core/tools/askFollowupQuestionTool.ts +++ b/src/core/tools/askFollowupQuestionTool.ts @@ -1,11 +1,12 @@ import { Task } from "../task/Task" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { parseXml } from "../../utils/xml" +import { AskFollowupQuestionToolDirective } from "../message-parsing/directives" export async function askFollowupQuestionTool( cline: Task, - block: ToolUse, + block: AskFollowupQuestionToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/attemptCompletionTool.ts b/src/core/tools/attemptCompletionTool.ts index ef7881854f..041cd4527f 100644 --- a/src/core/tools/attemptCompletionTool.ts +++ b/src/core/tools/attemptCompletionTool.ts @@ -1,12 +1,9 @@ import Anthropic from "@anthropic-ai/sdk" -import * as vscode from "vscode" import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../task/Task" import { - ToolResponse, - ToolUse, AskApproval, HandleError, PushToolResult, @@ -15,11 +12,11 @@ import { AskFinishSubTaskApproval, } from "../../shared/tools" import { formatResponse } from "../prompts/responses" -import { Package } from "../../shared/package" +import { ToolResponse, AttemptCompletionToolDirective } from "../message-parsing/directives" export async function attemptCompletionTool( cline: Task, - block: ToolUse, + block: AttemptCompletionToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, @@ -30,25 +27,6 @@ export async function attemptCompletionTool( const result: string | undefined = block.params.result const command: string | undefined = block.params.command - // Get the setting for preventing completion with open todos from VSCode configuration - const preventCompletionWithOpenTodos = vscode.workspace - .getConfiguration(Package.name) - .get("preventCompletionWithOpenTodos", false) - - // Check if there are incomplete todos (only if the setting is enabled) - const hasIncompleteTodos = cline.todoList && cline.todoList.some((todo) => todo.status !== "completed") - - if (preventCompletionWithOpenTodos && hasIncompleteTodos) { - cline.consecutiveMistakeCount++ - cline.recordToolError("attempt_completion") - pushToolResult( - formatResponse.toolError( - "Cannot complete task while there are incomplete todos. Please finish all todos before attempting completion.", - ), - ) - return - } - try { const lastMessage = cline.clineMessages.at(-1) diff --git a/src/core/tools/browserActionTool.ts b/src/core/tools/browserActionTool.ts index 13cb9b0ec2..cf939396a8 100644 --- a/src/core/tools/browserActionTool.ts +++ b/src/core/tools/browserActionTool.ts @@ -1,5 +1,5 @@ import { Task } from "../task/Task" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { BrowserAction, BrowserActionResult, @@ -7,10 +7,11 @@ import { ClineSayBrowserAction, } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" +import { ToolDirective, BrowserActionToolDirective } from "../message-parsing/directives" export async function browserActionTool( cline: Task, - block: ToolUse, + block: BrowserActionToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/codebaseSearchTool.ts b/src/core/tools/codebaseSearchTool.ts index 236b066306..6e204948a3 100644 --- a/src/core/tools/codebaseSearchTool.ts +++ b/src/core/tools/codebaseSearchTool.ts @@ -5,12 +5,13 @@ import { CodeIndexManager } from "../../services/code-index/manager" import { getWorkspacePath } from "../../utils/path" import { formatResponse } from "../prompts/responses" import { VectorStoreSearchResult } from "../../services/code-index/interfaces" -import { AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolUse } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { ToolDirective } from "../message-parsing/directives" import path from "path" export async function codebaseSearchTool( cline: Task, - block: ToolUse, + block: ToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index c346526a2e..34243845a5 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -1,28 +1,27 @@ import fs from "fs/promises" import * as path from "path" -import * as vscode from "vscode" import delay from "delay" -import { CommandExecutionStatus, DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT } from "@roo-code/types" +import { CommandExecutionStatus } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../task/Task" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { unescapeHtmlEntities } from "../../utils/text-normalization" import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { Terminal } from "../../integrations/terminal/Terminal" -import { Package } from "../../shared/package" -import { t } from "../../i18n" +import { ExecuteCommandToolDirective } from "../message-parsing/directives/tool-directives/ExecuteCommandToolDirective" +import { ToolResponse } from "../message-parsing/directives" class ShellIntegrationError extends Error {} export async function executeCommandTool( - task: Task, - block: ToolUse, + cline: Task, + block: ExecuteCommandToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, @@ -33,25 +32,25 @@ export async function executeCommandTool( try { if (block.partial) { - await task.ask("command", removeClosingTag("command", command), block.partial).catch(() => {}) + await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {}) return } else { if (!command) { - task.consecutiveMistakeCount++ - task.recordToolError("execute_command") - pushToolResult(await task.sayAndCreateMissingParamError("execute_command", "command")) + cline.consecutiveMistakeCount++ + cline.recordToolError("execute_command") + pushToolResult(await cline.sayAndCreateMissingParamError("execute_command", "command")) return } - const ignoredFileAttemptedToAccess = task.rooIgnoreController?.validateCommand(command) + const ignoredFileAttemptedToAccess = cline.rooIgnoreController?.validateCommand(command) if (ignoredFileAttemptedToAccess) { - await task.say("rooignore_error", ignoredFileAttemptedToAccess) + await cline.say("rooignore_error", ignoredFileAttemptedToAccess) pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess))) return } - task.consecutiveMistakeCount = 0 + cline.consecutiveMistakeCount = 0 command = unescapeHtmlEntities(command) // Unescape HTML entities. const didApprove = await askApproval("command", command) @@ -60,31 +59,10 @@ export async function executeCommandTool( return } - const executionId = task.lastMessageTs?.toString() ?? Date.now().toString() - const provider = await task.providerRef.deref() - const providerState = await provider?.getState() - - const { - terminalOutputLineLimit = 500, - terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT, - terminalShellIntegrationDisabled = false, - } = providerState ?? {} - - // Get command execution timeout from VSCode configuration (in seconds) - const commandExecutionTimeoutSeconds = vscode.workspace - .getConfiguration(Package.name) - .get("commandExecutionTimeout", 0) - - // Get command timeout allowlist from VSCode configuration - const commandTimeoutAllowlist = vscode.workspace - .getConfiguration(Package.name) - .get("commandTimeoutAllowlist", []) - - // Check if command matches any prefix in the allowlist - const isCommandAllowlisted = commandTimeoutAllowlist.some((prefix) => command!.startsWith(prefix.trim())) - - // Convert seconds to milliseconds for internal use, but skip timeout if command is allowlisted - const commandExecutionTimeout = isCommandAllowlisted ? 0 : commandExecutionTimeoutSeconds * 1000 + const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString() + const clineProvider = await cline.providerRef.deref() + const clineProviderState = await clineProvider?.getState() + const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {} const options: ExecuteCommandOptions = { executionId, @@ -92,31 +70,29 @@ export async function executeCommandTool( customCwd, terminalShellIntegrationDisabled, terminalOutputLineLimit, - terminalOutputCharacterLimit, - commandExecutionTimeout, } try { - const [rejected, result] = await executeCommand(task, options) + const [rejected, result] = await executeCommand(cline, options) if (rejected) { - task.didRejectTool = true + cline.didRejectTool = true } pushToolResult(result) } catch (error: unknown) { const status: CommandExecutionStatus = { executionId, status: "fallback" } - provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) - await task.say("shell_integration_warning") + clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + await cline.say("shell_integration_warning") if (error instanceof ShellIntegrationError) { - const [rejected, result] = await executeCommand(task, { + const [rejected, result] = await executeCommand(cline, { ...options, terminalShellIntegrationDisabled: true, }) if (rejected) { - task.didRejectTool = true + cline.didRejectTool = true } pushToolResult(result) @@ -139,32 +115,26 @@ export type ExecuteCommandOptions = { customCwd?: string terminalShellIntegrationDisabled?: boolean terminalOutputLineLimit?: number - terminalOutputCharacterLimit?: number - commandExecutionTimeout?: number } export async function executeCommand( - task: Task, + cline: Task, { executionId, command, customCwd, terminalShellIntegrationDisabled = false, terminalOutputLineLimit = 500, - terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT, - commandExecutionTimeout = 0, }: ExecuteCommandOptions, ): Promise<[boolean, ToolResponse]> { - // Convert milliseconds back to seconds for display purposes. - const commandExecutionTimeoutSeconds = commandExecutionTimeout / 1000 let workingDir: string if (!customCwd) { - workingDir = task.cwd + workingDir = cline.cwd } else if (path.isAbsolute(customCwd)) { workingDir = customCwd } else { - workingDir = path.resolve(task.cwd, customCwd) + workingDir = path.resolve(cline.cwd, customCwd) } try { @@ -181,26 +151,22 @@ export async function executeCommand( let shellIntegrationError: string | undefined const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode" - const provider = await task.providerRef.deref() + const clineProvider = await cline.providerRef.deref() let accumulatedOutput = "" const callbacks: RooTerminalCallbacks = { onLine: async (lines: string, process: RooTerminalProcess) => { accumulatedOutput += lines - const compressedOutput = Terminal.compressTerminalOutput( - accumulatedOutput, - terminalOutputLineLimit, - terminalOutputCharacterLimit, - ) + const compressedOutput = Terminal.compressTerminalOutput(accumulatedOutput, terminalOutputLineLimit) const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput } - provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) if (runInBackground) { return } try { - const { response, text, images } = await task.ask("command_output", "") + const { response, text, images } = await cline.ask("command_output", "") runInBackground = true if (response === "messageResponse") { @@ -210,35 +176,30 @@ export async function executeCommand( } catch (_error) {} }, onCompleted: (output: string | undefined) => { - result = Terminal.compressTerminalOutput( - output ?? "", - terminalOutputLineLimit, - terminalOutputCharacterLimit, - ) - - task.say("command_output", result) + result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit) + cline.say("command_output", result) completed = true }, onShellExecutionStarted: (pid: number | undefined) => { console.log(`[executeCommand] onShellExecutionStarted: ${pid}`) const status: CommandExecutionStatus = { executionId, status: "started", pid, command } - provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) }, onShellExecutionComplete: (details: ExitCodeDetails) => { const status: CommandExecutionStatus = { executionId, status: "exited", exitCode: details.exitCode } - provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) exitDetails = details }, } if (terminalProvider === "vscode") { callbacks.onNoShellIntegration = async (error: string) => { - TelemetryService.instance.captureShellIntegrationError(task.taskId) + TelemetryService.instance.captureShellIntegrationError(cline.taskId) shellIntegrationError = error } } - const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, task.taskId, terminalProvider) + const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId, terminalProvider) if (terminal instanceof Terminal) { terminal.terminal.show(true) @@ -250,51 +211,10 @@ export async function executeCommand( } const process = terminal.runCommand(command, callbacks) - task.terminalProcess = process + cline.terminalProcess = process - // Implement command execution timeout (skip if timeout is 0). - if (commandExecutionTimeout > 0) { - let timeoutId: NodeJS.Timeout | undefined - let isTimedOut = false - - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - isTimedOut = true - task.terminalProcess?.abort() - reject(new Error(`Command execution timed out after ${commandExecutionTimeout}ms`)) - }, commandExecutionTimeout) - }) - - try { - await Promise.race([process, timeoutPromise]) - } catch (error) { - if (isTimedOut) { - const status: CommandExecutionStatus = { executionId, status: "timeout" } - provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) - await task.say("error", t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds })) - task.terminalProcess = undefined - - return [ - false, - `The command was terminated after exceeding a user-configured ${commandExecutionTimeoutSeconds}s timeout. Do not try to re-run the command.`, - ] - } - throw error - } finally { - if (timeoutId) { - clearTimeout(timeoutId) - } - - task.terminalProcess = undefined - } - } else { - // No timeout - just wait for the process to complete. - try { - await process - } finally { - task.terminalProcess = undefined - } - } + await process + cline.terminalProcess = undefined if (shellIntegrationError) { throw new ShellIntegrationError(shellIntegrationError) @@ -309,7 +229,7 @@ export async function executeCommand( if (message) { const { text, images } = message - await task.say("user_feedback", text, images) + await cline.say("user_feedback", text, images) return [ true, @@ -348,7 +268,8 @@ export async function executeCommand( exitStatus = `Exit code: ` } - let workingDirInfo = ` within working directory '${terminal.getCurrentWorkingDirectory().toPosix()}'` + let workingDirInfo = ` within working directory '${workingDir.toPosix()}'` + const newWorkingDir = terminal.getCurrentWorkingDirectory() return [false, `Command executed in terminal ${workingDirInfo}. ${exitStatus}\nOutput:\n${result}`] } else { diff --git a/src/core/tools/fetchInstructionsTool.ts b/src/core/tools/fetchInstructionsTool.ts index 5325f98fbf..1f229b0e77 100644 --- a/src/core/tools/fetchInstructionsTool.ts +++ b/src/core/tools/fetchInstructionsTool.ts @@ -2,11 +2,12 @@ import { Task } from "../task/Task" import { fetchInstructions } from "../prompts/instructions/instructions" import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" -import { ToolUse, AskApproval, HandleError, PushToolResult } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult } from "../../shared/tools" +import { FetchInstructionsToolDirective } from "../message-parsing/directives" export async function fetchInstructionsTool( cline: Task, - block: ToolUse, + block: FetchInstructionsToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index 2b31224400..b207b05a07 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -4,17 +4,17 @@ import path from "path" import { getReadablePath } from "../../utils/path" import { Task } from "../task/Task" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { ClineSayTool } from "../../shared/ExtensionMessage" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { fileExistsAtPath } from "../../utils/fs" import { insertGroups } from "../diff/insert-groups" -import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { InsertCodeBlockToolDirective } from "../message-parsing/directives" export async function insertContentTool( cline: Task, - block: ToolUse, + block: InsertCodeBlockToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, @@ -52,7 +52,7 @@ export async function insertContentTool( return } - if (content === undefined) { + if (!content) { cline.consecutiveMistakeCount++ cline.recordToolError("insert_content") pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "content")) @@ -71,6 +71,17 @@ export async function insertContentTool( const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false const absolutePath = path.resolve(cline.cwd, relPath) + const fileExists = await fileExistsAtPath(absolutePath) + + if (!fileExists) { + cline.consecutiveMistakeCount++ + cline.recordToolError("insert_content") + const formattedError = `File does not exist at path: ${absolutePath}\n\n\nThe specified file could not be found. Please verify the file path and try again.\n` + await cline.say("error", formattedError) + pushToolResult(formattedError) + return + } + const lineNumber = parseInt(line, 10) if (isNaN(lineNumber) || lineNumber < 0) { cline.consecutiveMistakeCount++ @@ -79,26 +90,13 @@ export async function insertContentTool( return } - const fileExists = await fileExistsAtPath(absolutePath) - let fileContent: string = "" - if (!fileExists) { - if (lineNumber > 1) { - cline.consecutiveMistakeCount++ - cline.recordToolError("insert_content") - const formattedError = `Cannot insert content at line ${lineNumber} into a non-existent file. For new files, 'line' must be 0 (to append) or 1 (to insert at the beginning).` - await cline.say("error", formattedError) - pushToolResult(formattedError) - return - } - } else { - fileContent = await fs.readFile(absolutePath, "utf8") - } - cline.consecutiveMistakeCount = 0 - cline.diffViewProvider.editType = fileExists ? "modify" : "create" + // Read the file + const fileContent = await fs.readFile(absolutePath, "utf8") + cline.diffViewProvider.editType = "modify" cline.diffViewProvider.originalContent = fileContent - const lines = fileExists ? fileContent.split("\n") : [] + const lines = fileContent.split("\n") const updatedContent = insertGroups(lines, [ { @@ -117,22 +115,11 @@ export async function insertContentTool( await delay(200) } - // For consistency with writeToFileTool, handle new files differently - let diff: string | undefined - let approvalContent: string | undefined + const diff = formatResponse.createPrettyPatch(relPath, fileContent, updatedContent) - if (fileExists) { - // For existing files, generate diff and check for changes - diff = formatResponse.createPrettyPatch(relPath, fileContent, updatedContent) - if (!diff) { - pushToolResult(`No changes needed for '${relPath}'`) - return - } - approvalContent = undefined - } else { - // For new files, skip diff generation and provide full content - diff = undefined - approvalContent = updatedContent + if (!diff) { + pushToolResult(`No changes needed for '${relPath}'`) + return } await cline.diffViewProvider.update(updatedContent, true) @@ -140,7 +127,6 @@ export async function insertContentTool( const completeMessage = JSON.stringify({ ...sharedMessageProps, diff, - content: approvalContent, lineNumber: lineNumber, isProtected: isWriteProtected, } satisfies ClineSayTool) @@ -156,11 +142,7 @@ export async function insertContentTool( } // Call saveChanges to update the DiffViewProvider properties - const provider = cline.providerRef.deref() - const state = await provider?.getState() - const diagnosticsEnabled = state?.diagnosticsEnabled ?? true - const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.diffViewProvider.saveChanges() // Track file edit operation if (relPath) { @@ -170,7 +152,11 @@ export async function insertContentTool( cline.didEditFile = true // Get the formatted response message - const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + const message = await cline.diffViewProvider.pushToolWriteResult( + cline, + cline.cwd, + false, // Always false for insert_content + ) pushToolResult(message) diff --git a/src/core/tools/listCodeDefinitionNamesTool.ts b/src/core/tools/listCodeDefinitionNamesTool.ts index 6ceec0a725..c2d237a399 100644 --- a/src/core/tools/listCodeDefinitionNamesTool.ts +++ b/src/core/tools/listCodeDefinitionNamesTool.ts @@ -1,17 +1,18 @@ import path from "path" import fs from "fs/promises" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { parseSourceCodeForDefinitionsTopLevel, parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" +import { ListCodeDefinitionNamesToolDirective } from "../message-parsing/directives" export async function listCodeDefinitionNamesTool( cline: Task, - block: ToolUse, + block: ListCodeDefinitionNamesToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/listFilesTool.ts b/src/core/tools/listFilesTool.ts index dcd7655b1f..7707b20b01 100644 --- a/src/core/tools/listFilesTool.ts +++ b/src/core/tools/listFilesTool.ts @@ -5,8 +5,9 @@ import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" import { listFiles } from "../../services/glob/list-files" import { getReadablePath } from "../../utils/path" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { ListFilesToolDirective } from "../message-parsing/directives" import { isPathOutsideWorkspace } from "../../utils/pathUtils" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" /** * Implements the list_files tool. @@ -25,7 +26,7 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } f export async function listFilesTool( cline: Task, - block: ToolUse, + block: ListFilesToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/multiApplyDiffTool.ts b/src/core/tools/multiApplyDiffTool.ts index 4ddef4880b..7e6c336891 100644 --- a/src/core/tools/multiApplyDiffTool.ts +++ b/src/core/tools/multiApplyDiffTool.ts @@ -7,7 +7,7 @@ import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" import { Task } from "../task/Task" -import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" +import { RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { fileExistsAtPath } from "../../utils/fs" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" @@ -15,6 +15,7 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization" import { parseXml } from "../../utils/xml" import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" import { applyDiffToolLegacy } from "./applyDiffTool" +import { ToolDirective } from "../message-parsing/directives" interface DiffOperation { path: string @@ -52,7 +53,7 @@ interface ParsedXmlResult { export async function applyDiffTool( cline: Task, - block: ToolUse, + block: ToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index 7cc7063b49..37b9f1e408 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -1,14 +1,15 @@ import delay from "delay" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { Task } from "../task/Task" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import { formatResponse } from "../prompts/responses" +import { NewTaskToolDirective } from "../message-parsing/directives" import { t } from "../../i18n" export async function newTaskTool( cline: Task, - block: ToolUse, + block: NewTaskToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 6de8dd5642..d9f38c66e8 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -5,7 +5,7 @@ import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" import { t } from "../../i18n" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { getReadablePath } from "../../utils/path" @@ -14,6 +14,7 @@ import { readLines } from "../../integrations/misc/read-lines" import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text" import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { parseXml } from "../../utils/xml" +import { ReadFileToolDirective } from "../message-parsing/directives" export function getReadFileToolDescription(blockName: string, blockParams: any): string { // Handle both single path and multiple files via args @@ -72,7 +73,7 @@ interface FileResult { export async function readFileTool( cline: Task, - block: ToolUse, + block: ReadFileToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index b6ec3ed39b..8d2e4235dc 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -5,13 +5,13 @@ import delay from "delay" // Internal imports import { Task } from "../task/Task" -import { AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolUse } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" import { fileExistsAtPath } from "../../utils/fs" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" -import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { SearchAndReplaceToolDirective } from "../message-parsing/directives" /** * Tool for performing search and replace operations on files @@ -63,7 +63,7 @@ async function validateParams( */ export async function searchAndReplaceTool( cline: Task, - block: ToolUse, + block: SearchAndReplaceToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, @@ -228,11 +228,7 @@ export async function searchAndReplaceTool( } // Call saveChanges to update the DiffViewProvider properties - const provider = cline.providerRef.deref() - const state = await provider?.getState() - const diagnosticsEnabled = state?.diagnosticsEnabled ?? true - const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.diffViewProvider.saveChanges() // Track file edit operation if (relPath) { diff --git a/src/core/tools/searchFilesTool.ts b/src/core/tools/searchFilesTool.ts index b6ee97f874..ee95d2805d 100644 --- a/src/core/tools/searchFilesTool.ts +++ b/src/core/tools/searchFilesTool.ts @@ -1,15 +1,16 @@ import path from "path" import { Task } from "../task/Task" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { regexSearchFiles } from "../../services/ripgrep" +import { SearchFilesToolDirective } from "../message-parsing/directives" export async function searchFilesTool( cline: Task, - block: ToolUse, + block: SearchFilesToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/switchModeTool.ts b/src/core/tools/switchModeTool.ts index 8ce906b41f..4d438e8b3d 100644 --- a/src/core/tools/switchModeTool.ts +++ b/src/core/tools/switchModeTool.ts @@ -1,13 +1,14 @@ import delay from "delay" import { Task } from "../task/Task" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" +import { SwitchModeToolDirective } from "../message-parsing/directives" export async function switchModeTool( cline: Task, - block: ToolUse, + block: SwitchModeToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/useMcpToolTool.ts b/src/core/tools/useMcpToolTool.ts index 30dff5ce4f..b6d176472b 100644 --- a/src/core/tools/useMcpToolTool.ts +++ b/src/core/tools/useMcpToolTool.ts @@ -1,9 +1,10 @@ import { Task } from "../task/Task" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" import { McpExecutionStatus } from "@roo-code/types" import { t } from "../../i18n" +import { UseMcpToolToolDirective } from "../message-parsing/directives" interface McpToolParams { server_name?: string @@ -166,7 +167,7 @@ async function executeToolAndProcessResult( export async function useMcpToolTool( cline: Task, - block: ToolUse, + block: UseMcpToolToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index fd9d158f3f..c733a188b7 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode" import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" -import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { fileExistsAtPath } from "../../utils/fs" import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/misc/extract-text" @@ -13,11 +13,11 @@ import { getReadablePath } from "../../utils/path" import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { detectCodeOmission } from "../../integrations/editor/detect-omission" import { unescapeHtmlEntities } from "../../utils/text-normalization" -import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { WriteToFileToolDirective } from "../message-parsing/directives" export async function writeToFileTool( cline: Task, - block: ToolUse, + block: WriteToFileToolDirective, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, @@ -74,11 +74,11 @@ export async function writeToFileTool( // pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini) if (newContent.startsWith("```")) { // cline handles cases where it includes language specifiers like ```python ```js - newContent = newContent.split("\n").slice(1).join("\n") + newContent = newContent.split("\n").slice(1).join("\n").trim() } if (newContent.endsWith("```")) { - newContent = newContent.split("\n").slice(0, -1).join("\n") + newContent = newContent.split("\n").slice(0, -1).join("\n").trim() } if (!cline.api.getModel().id.includes("claude")) { @@ -214,11 +214,7 @@ export async function writeToFileTool( } // Call saveChanges to update the DiffViewProvider properties - const provider = cline.providerRef.deref() - const state = await provider?.getState() - const diagnosticsEnabled = state?.diagnosticsEnabled ?? true - const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.diffViewProvider.saveChanges() // Track file edit operation if (relPath) { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 905e657b37..847fbb2a29 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -31,7 +31,7 @@ import { DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" +import { CloudService } from "@roo-code/cloud" import { t } from "../../i18n" import { setPanel } from "../../activate/registerCommands" @@ -70,7 +70,6 @@ import { webviewMessageHandler } from "./webviewMessageHandler" import { WebviewMessage } from "../../shared/WebviewMessage" import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels" import { ProfileValidator } from "../../shared/ProfileValidator" -import { getWorkspaceGitInfo } from "../../utils/git" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -1303,62 +1302,6 @@ export class ClineProvider return await fileExistsAtPath(promptFilePath) } - /** - * Merges allowed commands from global state and workspace configuration - * with proper validation and deduplication - */ - private mergeAllowedCommands(globalStateCommands?: string[]): string[] { - return this.mergeCommandLists("allowedCommands", "allowed", globalStateCommands) - } - - /** - * Merges denied commands from global state and workspace configuration - * with proper validation and deduplication - */ - private mergeDeniedCommands(globalStateCommands?: string[]): string[] { - return this.mergeCommandLists("deniedCommands", "denied", globalStateCommands) - } - - /** - * Common utility for merging command lists from global state and workspace configuration. - * Implements the Command Denylist feature's merging strategy with proper validation. - * - * @param configKey - VSCode workspace configuration key - * @param commandType - Type of commands for error logging - * @param globalStateCommands - Commands from global state - * @returns Merged and deduplicated command list - */ - private mergeCommandLists( - configKey: "allowedCommands" | "deniedCommands", - commandType: "allowed" | "denied", - globalStateCommands?: string[], - ): string[] { - try { - // Validate and sanitize global state commands - const validGlobalCommands = Array.isArray(globalStateCommands) - ? globalStateCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) - : [] - - // Get workspace configuration commands - const workspaceCommands = vscode.workspace.getConfiguration(Package.name).get(configKey) || [] - - // Validate and sanitize workspace commands - const validWorkspaceCommands = Array.isArray(workspaceCommands) - ? workspaceCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) - : [] - - // Combine and deduplicate commands - // Global state takes precedence over workspace configuration - const mergedCommands = [...new Set([...validGlobalCommands, ...validWorkspaceCommands])] - - return mergedCommands - } catch (error) { - console.error(`Error merging ${commandType} commands:`, error) - // Return empty array as fallback to prevent crashes - return [] - } - } - async getStateToPostToWebview() { const { apiConfiguration, @@ -1370,8 +1313,6 @@ export class ClineProvider alwaysAllowWriteOutsideWorkspace, alwaysAllowWriteProtected, alwaysAllowExecute, - allowedCommands, - deniedCommands, alwaysAllowBrowser, alwaysAllowMcp, alwaysAllowModeSwitch, @@ -1445,8 +1386,7 @@ export class ClineProvider const telemetryKey = process.env.POSTHOG_API_KEY const machineId = vscode.env.machineId - const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands) - const mergedDeniedCommands = this.mergeDeniedCommands(deniedCommands) + const allowedCommands = vscode.workspace.getConfiguration(Package.name).get("allowedCommands") || [] const cwd = this.cwd // Check if there's a system prompt override for the current mode @@ -1486,8 +1426,7 @@ export class ClineProvider enableCheckpoints: enableCheckpoints ?? true, shouldShowAnnouncement: telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId, - allowedCommands: mergedAllowedCommands, - deniedCommands: mergedDeniedCommands, + allowedCommands, soundVolume: soundVolume ?? 0.5, browserViewportSize: browserViewportSize ?? "900x600", screenshotQuality: screenshotQuality ?? 75, @@ -1557,12 +1496,6 @@ export class ClineProvider }, mdmCompliant: this.checkMdmCompliance(), profileThresholds: profileThresholds ?? {}, - cloudApiUrl: getRooCodeApiUrl(), - hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, - alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, - followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, - includeDiagnosticMessages: includeDiagnosticMessages ?? true, - maxDiagnosticMessages: maxDiagnosticMessages ?? 50, } } @@ -1846,7 +1779,7 @@ export class ClineProvider /** * Returns properties to be included in every telemetry event * This method is called by the telemetry service to get context information - * like the current mode, API provider, git repository information, etc. + * like the current mode, API provider, etc. */ public async getTelemetryProperties(): Promise { const { mode, apiConfiguration, language } = await this.getState() @@ -1854,35 +1787,6 @@ export class ClineProvider const packageJSON = this.context.extension?.packageJSON - // Get Roo Code Cloud authentication state - let cloudIsAuthenticated: boolean | undefined - - try { - if (CloudService.hasInstance()) { - cloudIsAuthenticated = CloudService.instance.isAuthenticated() - } - } catch (error) { - // Silently handle errors to avoid breaking telemetry collection - this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`) - } - - // Get git repository information - const gitInfo = await getWorkspaceGitInfo() - - // Calculate todo list statistics - const todoList = task?.todoList - let todos: { total: number; completed: number; inProgress: number; pending: number } | undefined - - if (todoList && todoList.length > 0) { - todos = { - total: todoList.length, - completed: todoList.filter((todo) => todo.status === "completed").length, - inProgress: todoList.filter((todo) => todo.status === "in_progress").length, - pending: todoList.filter((todo) => todo.status === "pending").length, - } - } - - // Return all properties including git info - clients will filter as needed return { appName: packageJSON?.name ?? Package.name, appVersion: packageJSON?.version ?? Package.version, @@ -1895,9 +1799,6 @@ export class ClineProvider modelId: task?.api?.getModel().id, diffStrategy: task?.diffStrategy?.getName(), isSubtask: task ? !!task.parentTask : undefined, - cloudIsAuthenticated, - ...(todos && { todos }), - ...gitInfo, } } } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 344b098816..fabb0aae60 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -13,7 +13,6 @@ import { experimentDefault } from "../../../shared/experiments" import { setTtsEnabled } from "../../../utils/tts" import { ContextProxy } from "../../config/ContextProxy" import { Task, TaskOptions } from "../../task/Task" -import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" @@ -44,8 +43,6 @@ vi.mock("axios", () => ({ post: vi.fn(), })) -vi.mock("../../../utils/safeWriteJson") - vi.mock("@modelcontextprotocol/sdk/types.js", () => ({ CallToolResultSchema: {}, ListResourcesResultSchema: {}, @@ -145,7 +142,6 @@ vi.mock("vscode", () => ({ }, window: { showInformationMessage: vi.fn(), - showWarningMessage: vi.fn(), showErrorMessage: vi.fn(), }, workspace: { @@ -312,18 +308,6 @@ vi.mock("../diff/strategies/multi-search-replace", () => ({ })), })) -vi.mock("@roo-code/cloud", () => ({ - CloudService: { - hasInstance: vi.fn().mockReturnValue(true), - get instance() { - return { - isAuthenticated: vi.fn().mockReturnValue(false), - } - }, - }, - getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), -})) - afterAll(() => { vi.restoreAllMocks() }) @@ -400,7 +384,6 @@ describe("ClineProvider", () => { options: {}, onDidReceiveMessage: vi.fn(), asWebviewUri: vi.fn(), - cspSource: "vscode-webview://test-csp-source", }, visible: true, onDidDispose: vi.fn().mockImplementation((callback) => { @@ -474,7 +457,7 @@ describe("ClineProvider", () => { // Verify Content Security Policy contains the necessary PostHog domains expect(mockWebviewView.webview.html).toContain( - "connect-src vscode-webview://test-csp-source https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com", + "connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com", ) // Extract the script-src directive section and verify required security elements @@ -502,7 +485,7 @@ describe("ClineProvider", () => { alwaysAllowReadOnlyOutsideWorkspace: false, alwaysAllowWrite: false, codebaseIndexConfig: { - codebaseIndexEnabled: true, + codebaseIndexEnabled: false, codebaseIndexQdrantUrl: "", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderBaseUrl: "", @@ -540,8 +523,6 @@ describe("ClineProvider", () => { cloudIsAuthenticated: false, sharingEnabled: false, profileThresholds: {}, - hasOpenedModeSelector: false, - diagnosticsEnabled: true, } const message: ExtensionMessage = { @@ -1165,10 +1146,15 @@ describe("ClineProvider", () => { describe("deleteMessage", () => { beforeEach(async () => { + // Mock window.showInformationMessage + ;(vscode.window.showInformationMessage as any) = vi.fn() await provider.resolveWebviewView(mockWebviewView) }) - test("handles deletion with confirmation dialog", async () => { + test('handles "Just this message" deletion correctly', async () => { + // Mock user selecting "Just this message" + ;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.just_this_message") + // Setup mock messages const mockMessages = [ { ts: 1000, type: "say", say: "user_feedback" }, // User message 1 @@ -1199,66 +1185,37 @@ describe("ClineProvider", () => { historyItem: { id: "test-task-id" }, }) - // Mock initClineWithHistoryItem - ;(provider as any).initClineWithHistoryItem = vi.fn() - // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] await messageHandler({ type: "deleteMessage", value: 4000 }) - // Verify that the dialog message was sent to webview - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showDeleteMessageDialog", - messageTs: 4000, - }) + // Verify correct messages were kept + expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([ + mockMessages[0], + mockMessages[1], + mockMessages[4], + mockMessages[5], + ]) - // Simulate user confirming deletion through the dialog - await messageHandler({ type: "deleteMessageConfirm", messageTs: 4000 }) - - // Verify only messages before the deleted message were kept - expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]]) - - // Verify only API messages before the deleted message were kept + // Verify correct API messages were kept expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([ mockApiHistory[0], mockApiHistory[1], + mockApiHistory[4], + mockApiHistory[5], ]) - - // Verify initClineWithHistoryItem was called - expect((provider as any).initClineWithHistoryItem).toHaveBeenCalledWith({ id: "test-task-id" }) }) - test("handles case when no current task exists", async () => { - // Clear the cline stack - ;(provider as any).clineStack = [] + test('handles "This and all subsequent messages" deletion correctly', async () => { + // Mock user selecting "This and all subsequent messages" + ;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.this_and_subsequent") - // Trigger message deletion - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - await messageHandler({ type: "deleteMessage", value: 2000 }) - - // Verify no dialog was shown since there's no current cline - expect(mockPostMessage).not.toHaveBeenCalledWith( - expect.objectContaining({ - type: "showDeleteMessageDialog", - }), - ) - }) - }) - - describe("editMessage", () => { - beforeEach(async () => { - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles edit with confirmation dialog", async () => { // Setup mock messages const mockMessages = [ - { ts: 1000, type: "say", say: "user_feedback" }, // User message 1 - { ts: 2000, type: "say", say: "tool" }, // Tool message - { ts: 3000, type: "say", say: "text", value: 4000 }, // Message to edit - { ts: 4000, type: "say", say: "browser_action" }, // Response to edit - { ts: 5000, type: "say", say: "user_feedback" }, // Next user message - { ts: 6000, type: "say", say: "user_feedback" }, // Final message + { ts: 1000, type: "say", say: "user_feedback" }, + { ts: 2000, type: "say", say: "text", value: 3000 }, // Message to delete + { ts: 3000, type: "say", say: "user_feedback" }, + { ts: 4000, type: "say", say: "user_feedback" }, ] as ClineMessage[] const mockApiHistory = [ @@ -1266,64 +1223,51 @@ describe("ClineProvider", () => { { ts: 2000 }, { ts: 3000 }, { ts: 4000 }, - { ts: 5000 }, - { ts: 6000 }, - ] as (Anthropic.MessageParam & { ts?: number })[] + ] as (Anthropic.MessageParam & { + ts?: number + })[] - // Setup Task instance with auto-mock from the top of the file + // Setup Cline instance with auto-mock from the top of the file const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance - mockCline.clineMessages = mockMessages // Set test-specific messages - mockCline.apiConversationHistory = mockApiHistory // Set API history - - // Explicitly mock the overwrite methods since they're not being called in the tests - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) // Add the mocked instance to the stack + mockCline.clineMessages = mockMessages + mockCline.apiConversationHistory = mockApiHistory + await provider.addClineToStack(mockCline) // Mock getTaskWithId ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ historyItem: { id: "test-task-id" }, }) - // Trigger message edit - // Get the message handler function that was registered with the webview + // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + await messageHandler({ type: "deleteMessage", value: 3000 }) - // Call the message handler with a submitEditedMessage message - await messageHandler({ - type: "submitEditedMessage", - value: 4000, - editedMessageContent: "Edited message content", - }) + // Verify only messages before the deleted message were kept + expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]]) - // Verify that the dialog message was sent to webview - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 4000, - text: "Edited message content", - }) + // Verify only API messages before the deleted message were kept + expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([mockApiHistory[0]]) + }) - // Simulate user confirming edit through the dialog - await messageHandler({ - type: "editMessageConfirm", - messageTs: 4000, - text: "Edited message content", - }) + test("handles Cancel correctly", async () => { + // Mock user selecting "Cancel" + ;(vscode.window.showInformationMessage as any).mockResolvedValue("Cancel") - // Verify correct messages were kept (only messages before the edited one) - expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]]) + // Setup Cline instance with auto-mock from the top of the file + const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance + mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }] as ClineMessage[] + mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as (Anthropic.MessageParam & { + ts?: number + })[] + await provider.addClineToStack(mockCline) - // Verify correct API messages were kept (only messages before the edited one) - expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([ - mockApiHistory[0], - mockApiHistory[1], - ]) + // Trigger message deletion + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + await messageHandler({ type: "deleteMessage", value: 2000 }) - // The new flow calls webviewMessageHandler recursively with askResponse - // We need to verify the recursive call happened by checking if the handler was called again - expect((mockWebviewView.webview.onDidReceiveMessage as any).mock.calls.length).toBeGreaterThanOrEqual(1) + // Verify no messages were deleted + expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled() + expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled() }) }) @@ -1987,7 +1931,6 @@ describe("Project MCP Settings", () => { options: {}, onDidReceiveMessage: vi.fn(), asWebviewUri: vi.fn(), - cspSource: "vscode-webview://test-csp-source", }, visible: true, onDidDispose: vi.fn(), @@ -2033,8 +1976,11 @@ describe("Project MCP Settings", () => { // Check that fs.mkdir was called with the correct path expect(mockedFs.mkdir).toHaveBeenCalledWith("/test/workspace/.roo", { recursive: true }) - // Verify file was created with default content - expect(safeWriteJson).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json", { mcpServers: {} }) + // Check that fs.writeFile was called with default content + expect(mockedFs.writeFile).toHaveBeenCalledWith( + "/test/workspace/.roo/mcp.json", + JSON.stringify({ mcpServers: {} }, null, 2), + ) // Check that openFile was called expect(openFileSpy).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json") @@ -2143,11 +2089,6 @@ describe("getTelemetryProperties", () => { // Reset mocks vi.clearAllMocks() - // Initialize TelemetryService if not already initialized - if (!TelemetryService.hasInstance()) { - TelemetryService.createInstance([]) - } - // Setup basic mocks mockContext = { globalState: { @@ -2201,96 +2142,6 @@ describe("getTelemetryProperties", () => { expect(properties).toHaveProperty("modelId", "claude-sonnet-4-20250514") }) - - describe("cloud authentication telemetry", () => { - beforeEach(() => { - // Reset all mocks before each test - vi.clearAllMocks() - }) - - test("includes cloud authentication property when user is authenticated", async () => { - // Import the CloudService mock and update it - const { CloudService } = await import("@roo-code/cloud") - const mockCloudService = { - isAuthenticated: vi.fn().mockReturnValue(true), - } - - // Update the existing mock - Object.defineProperty(CloudService, "instance", { - get: vi.fn().mockReturnValue(mockCloudService), - configurable: true, - }) - - const properties = await provider.getTelemetryProperties() - - expect(properties).toHaveProperty("cloudIsAuthenticated", true) - }) - - test("includes cloud authentication property when user is not authenticated", async () => { - // Import the CloudService mock and update it - const { CloudService } = await import("@roo-code/cloud") - const mockCloudService = { - isAuthenticated: vi.fn().mockReturnValue(false), - } - - // Update the existing mock - Object.defineProperty(CloudService, "instance", { - get: vi.fn().mockReturnValue(mockCloudService), - configurable: true, - }) - - const properties = await provider.getTelemetryProperties() - - expect(properties).toHaveProperty("cloudIsAuthenticated", false) - }) - - test("handles CloudService errors gracefully", async () => { - // Import the CloudService mock and update it to throw an error - const { CloudService } = await import("@roo-code/cloud") - Object.defineProperty(CloudService, "instance", { - get: vi.fn().mockImplementation(() => { - throw new Error("CloudService not available") - }), - configurable: true, - }) - - const properties = await provider.getTelemetryProperties() - - // Should still include basic telemetry properties - expect(properties).toHaveProperty("vscodeVersion") - expect(properties).toHaveProperty("platform") - expect(properties).toHaveProperty("appVersion", "1.0.0") - - // Cloud property should be undefined when CloudService is not available - expect(properties).toHaveProperty("cloudIsAuthenticated", undefined) - }) - - test("handles CloudService method errors gracefully", async () => { - // Import the CloudService mock and update it - const { CloudService } = await import("@roo-code/cloud") - const mockCloudService = { - isAuthenticated: vi.fn().mockImplementation(() => { - throw new Error("Authentication check error") - }), - } - - // Update the existing mock - Object.defineProperty(CloudService, "instance", { - get: vi.fn().mockReturnValue(mockCloudService), - configurable: true, - }) - - const properties = await provider.getTelemetryProperties() - - // Should still include basic telemetry properties - expect(properties).toHaveProperty("vscodeVersion") - expect(properties).toHaveProperty("platform") - expect(properties).toHaveProperty("appVersion", "1.0.0") - - // Property that errored should be undefined - expect(properties).toHaveProperty("cloudIsAuthenticated", undefined) - }) - }) }) describe("ClineProvider - Router Models", () => { @@ -2577,934 +2428,3 @@ describe("ClineProvider - Router Models", () => { }) }) }) - -describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { - let provider: ClineProvider - let mockContext: vscode.ExtensionContext - let mockOutputChannel: vscode.OutputChannel - let mockWebviewView: vscode.WebviewView - let mockPostMessage: any - let defaultTaskOptions: TaskOptions - - beforeEach(() => { - vi.clearAllMocks() - - if (!TelemetryService.hasInstance()) { - TelemetryService.createInstance([]) - } - - const globalState: Record = { - mode: "code", - currentApiConfigName: "current-config", - } - - 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: string | undefined) => (globalState[key] = value)), - 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)), - delete: vi.fn().mockImplementation((key: string) => delete secrets[key]), - }, - 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 - - mockPostMessage = vi.fn() - - mockWebviewView = { - webview: { - postMessage: mockPostMessage, - html: "", - options: {}, - onDidReceiveMessage: vi.fn(), - asWebviewUri: vi.fn(), - }, - 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)) - - defaultTaskOptions = { - provider, - apiConfiguration: { - apiProvider: "openrouter", - }, - } - - // 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("Edit Messages with Images and Attachments", () => { - beforeEach(async () => { - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles editing messages containing images", async () => { - const mockMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Original message" }, - { - ts: 2000, - type: "say", - say: "user_feedback", - text: "Message with image", - images: [ - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==", - ], - value: 3000, - }, - { ts: 3000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = mockMessages - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - await messageHandler({ - type: "submitEditedMessage", - value: 3000, - editedMessageContent: "Edited message with preserved images", - }) - - // Verify dialog was shown - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 3000, - text: "Edited message with preserved images", - }) - - // Simulate confirmation - await messageHandler({ - type: "editMessageConfirm", - messageTs: 3000, - text: "Edited message with preserved images", - }) - - // Verify messages were edited correctly - only the first message should remain - expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]]) - expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }]) - }) - - test("handles editing messages with file attachments", async () => { - const mockMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Original message" }, - { - ts: 2000, - type: "say", - say: "user_feedback", - text: "Message with file", - attachments: [{ path: "/path/to/file.txt", type: "file" }], - value: 3000, - }, - { ts: 3000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = mockMessages - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - await messageHandler({ - type: "submitEditedMessage", - value: 3000, - editedMessageContent: "Edited message with file attachment", - }) - - // Verify dialog was shown - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 3000, - text: "Edited message with file attachment", - }) - - // Simulate user confirming the edit - await messageHandler({ - type: "editMessageConfirm", - messageTs: 3000, - text: "Edited message with file attachment", - }) - - expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith( - "messageResponse", - "Edited message with file attachment", - undefined, - ) - }) - }) - - describe("Network Failure Scenarios", () => { - beforeEach(async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles network timeout during edit submission", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn().mockRejectedValue(new Error("Network timeout")) - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Should not throw error, but handle gracefully - await expect( - messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: "Edited message", - }), - ).resolves.toBeUndefined() - - // Verify dialog was shown - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 2000, - text: "Edited message", - }) - - // Simulate user confirming the edit - await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" }) - - expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - }) - - test("handles connection drops during edit operation", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - mockCline.overwriteClineMessages = vi.fn().mockRejectedValue(new Error("Connection lost")) - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Should handle connection error gracefully - await expect( - messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: "Edited message", - }), - ).resolves.toBeUndefined() - - // Verify dialog was shown - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 2000, - text: "Edited message", - }) - - // Simulate user confirming the edit - await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" }) - - // The error should be caught and shown - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Connection lost") - }) - }) - - describe("Concurrent Edit Operations", () => { - beforeEach(async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles race conditions with simultaneous edits", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Message 1", value: 2000 }, - { ts: 2000, type: "say", say: "text", text: "AI response 1" }, - { ts: 3000, type: "say", say: "user_feedback", text: "Message 2", value: 4000 }, - { ts: 4000, type: "say", say: "text", text: "AI response 2" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Simulate concurrent edit operations - const edit1Promise = messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: "Edited message 1", - }) - - const edit2Promise = messageHandler({ - type: "submitEditedMessage", - value: 4000, - editedMessageContent: "Edited message 2", - }) - - await Promise.all([edit1Promise, edit2Promise]) - - // Verify dialogs were shown for both edits - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 2000, - text: "Edited message 1", - }) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 4000, - text: "Edited message 2", - }) - - // Simulate user confirming both edits - await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message 1" }) - await messageHandler({ type: "editMessageConfirm", messageTs: 4000, text: "Edited message 2" }) - - // Both operations should complete without throwing - expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - }) - }) - - describe("Edit Permissions and Authorization", () => { - beforeEach(async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles edit permission failures", async () => { - // Mock no current cline (simulating permission failure) - vi.spyOn(provider, "getCurrentCline").mockReturnValue(undefined) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: "Edited message", - }) - - // Should not show confirmation dialog when no current cline - expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() - }) - - test("handles authorization failures during edit", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - mockCline.overwriteClineMessages = vi.fn().mockRejectedValue(new Error("Unauthorized")) - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: "Edited message", - }) - - // Simulate confirmation - await messageHandler({ - type: "editMessageConfirm", - messageTs: 2000, - text: "Edited message", - }) - - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Unauthorized") - }) - - describe("Malformed Requests and Invalid Formats", () => { - beforeEach(async () => { - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles malformed edit requests", async () => { - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Test with missing value - await messageHandler({ - type: "submitEditedMessage", - editedMessageContent: "Edited message", - }) - - // Test with invalid value type - await messageHandler({ - type: "submitEditedMessage", - value: "invalid", - editedMessageContent: "Edited message", - }) - - // Test with missing editedMessageContent - await messageHandler({ - type: "submitEditedMessage", - value: 2000, - }) - - // Should not show confirmation dialog for malformed requests - expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() - }) - - test("handles invalid message formats", async () => { - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Test with null message - should throw error - await expect(messageHandler(null)).rejects.toThrow() - - // Test with undefined message - should throw error - await expect(messageHandler(undefined)).rejects.toThrow() - - // Test with message missing type - await expect( - messageHandler({ - value: 2000, - editedMessageContent: "Edited message", - }), - ).resolves.toBeUndefined() - - // Should handle gracefully without errors - expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() - }) - - test("handles invalid timestamp values", async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Original message" }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - - await provider.addClineToStack(mockCline) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Test with negative timestamp - await messageHandler({ - type: "deleteMessage", - value: -1000, - }) - - // Test with zero timestamp - await messageHandler({ - type: "deleteMessage", - value: 0, - }) - - // Invalid timestamps may still trigger confirmation dialog - // This is expected behavior as the system tries to process the message - }) - }) - - describe("Operations on Deleted or Non-existent Messages", () => { - beforeEach(async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles edit operations on deleted messages", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Existing message" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Try to edit a message that doesn't exist (timestamp 5000) - await messageHandler({ - type: "submitEditedMessage", - value: 5000, - editedMessageContent: "Edited non-existent message", - }) - - // Should show edit dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 5000, - text: "Edited non-existent message", - }) - - // Simulate user confirming the edit - await messageHandler({ - type: "editMessageConfirm", - messageTs: 5000, - text: "Edited non-existent message", - }) - - // Should not perform any operations since message doesn't exist - expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled() - }) - - test("handles delete operations on non-existent messages", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Existing message" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - // Try to delete a message that doesn't exist (timestamp 5000) - await messageHandler({ - type: "deleteMessage", - value: 5000, - }) - - // Should show delete dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showDeleteMessageDialog", - messageTs: 5000, - }) - - // Simulate user confirming the delete - await messageHandler({ type: "deleteMessageConfirm", messageTs: 5000 }) - - // Should not perform any operations since message doesn't exist - expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled() - }) - }) - - describe("Resource Cleanup During Failed Operations", () => { - beforeEach(async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - await provider.resolveWebviewView(mockWebviewView) - }) - - test("validates proper cleanup during failed edit operations", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - - // Mock cleanup tracking - const cleanupSpy = vi.fn() - mockCline.overwriteClineMessages = vi.fn().mockImplementation(() => { - cleanupSpy() - throw new Error("Operation failed") - }) - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: "Edited message", - }) - - // Should show edit dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 2000, - text: "Edited message", - }) - - // Simulate user confirming the edit - await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" }) - - // Verify cleanup was attempted before failure - expect(cleanupSpy).toHaveBeenCalled() - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Operation failed") - }) - - test("validates proper cleanup during failed delete operations", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - - // Mock cleanup tracking - const cleanupSpy = vi.fn() - mockCline.overwriteClineMessages = vi.fn().mockImplementation(() => { - cleanupSpy() - throw new Error("Delete operation failed") - }) - mockCline.overwriteApiConversationHistory = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ type: "deleteMessage", value: 2000 }) - - // Should show delete dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showDeleteMessageDialog", - messageTs: 2000, - }) - - // Simulate user confirming the delete - await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 }) - - // Verify cleanup was attempted before failure - expect(cleanupSpy).toHaveBeenCalled() - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - "Error deleting message: Delete operation failed", - ) - }) - }) - - describe("Large Message Payloads", () => { - beforeEach(async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles editing messages with large text content", async () => { - // Create a large message (10KB of text) - const largeText = "A".repeat(10000) - const mockMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: largeText, value: 2000 }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = mockMessages - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - const largeEditedContent = "B".repeat(15000) - await messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: largeEditedContent, - }) - - // Should show edit dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: 2000, - text: largeEditedContent, - }) - - // Simulate user confirming the edit - await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent }) - - expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith( - "messageResponse", - largeEditedContent, - undefined, - ) - }) - - test("handles deleting messages with large payloads", async () => { - // Create messages with large payloads - const largeText = "X".repeat(50000) - const mockMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Small message" }, - { ts: 2000, type: "say", say: "user_feedback", text: largeText }, - { ts: 3000, type: "say", say: "text", text: "AI response" }, - { ts: 4000, type: "say", say: "user_feedback", text: "Another large message: " + largeText }, - ] as ClineMessage[] - - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = mockMessages - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ type: "deleteMessage", value: 3000 }) - - // Should show delete dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showDeleteMessageDialog", - messageTs: 3000, - }) - - // Simulate user confirming the delete - await messageHandler({ type: "deleteMessageConfirm", messageTs: 3000 }) - - // Should handle large payloads without issues - expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]]) - expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }]) - }) - }) - - describe("Error Messaging and User Feedback", () => { - // Note: Error messaging test removed as the implementation may not have proper error handling in place - - test("provides user feedback for successful operations", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - ;(provider as any).initClineWithHistoryItem = vi.fn() - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ type: "deleteMessage", value: 2000 }) - - // Should show delete dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showDeleteMessageDialog", - messageTs: 2000, - }) - - // Simulate user confirming the delete - await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 }) - - // Verify successful operation completed - expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(provider.initClineWithHistoryItem).toHaveBeenCalled() - expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() - }) - - test("handles user cancellation gracefully", async () => { - // Test cancellation by not sending confirmation - - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Message to edit" }, - { ts: 2000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ - type: "submitEditedMessage", - value: 2000, - editedMessageContent: "Edited message", - }) - - // Verify no operations were performed when user canceled - expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled() - expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled() - expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() - }) - }) - - describe("Edge Cases with Message Timestamps", () => { - beforeEach(async () => { - ;(vscode.window.showInformationMessage as any) = vi.fn() - await provider.resolveWebviewView(mockWebviewView) - }) - - test("handles messages with identical timestamps", async () => { - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Message 1" }, - { ts: 1000, type: "say", say: "text", text: "Message 2 (same timestamp)" }, - { ts: 1000, type: "say", say: "user_feedback", text: "Message 3 (same timestamp)" }, - { ts: 2000, type: "say", say: "text", text: "Message 4" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 1000 }, { ts: 1000 }, { ts: 2000 }] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ type: "deleteMessage", value: 1000 }) - - // Should show delete dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showDeleteMessageDialog", - messageTs: 1000, - }) - - // Simulate user confirming the delete - await messageHandler({ type: "deleteMessageConfirm", messageTs: 1000 }) - - // Should handle identical timestamps gracefully - expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - }) - - test("handles messages with future timestamps", async () => { - const futureTimestamp = Date.now() + 100000 // Future timestamp - const mockCline = new Task(defaultTaskOptions) - mockCline.clineMessages = [ - { ts: 1000, type: "say", say: "user_feedback", text: "Past message" }, - { - ts: futureTimestamp, - type: "say", - say: "user_feedback", - text: "Future message", - value: futureTimestamp + 1000, - }, - { ts: futureTimestamp + 1000, type: "say", say: "text", text: "AI response" }, - ] as ClineMessage[] - mockCline.apiConversationHistory = [ - { ts: 1000 }, - { ts: futureTimestamp }, - { ts: futureTimestamp + 1000 }, - ] as any[] - mockCline.overwriteClineMessages = vi.fn() - mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() - - await provider.addClineToStack(mockCline) - ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ - historyItem: { id: "test-task-id" }, - }) - - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - - await messageHandler({ - type: "submitEditedMessage", - value: futureTimestamp + 1000, - editedMessageContent: "Edited future message", - }) - - // Should show edit dialog - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "showEditMessageDialog", - messageTs: futureTimestamp + 1000, - text: "Edited future message", - }) - - // Simulate user confirming the edit - await messageHandler({ - type: "editMessageConfirm", - messageTs: futureTimestamp + 1000, - text: "Edited future message", - }) - - // Should handle future timestamps correctly - expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).toHaveBeenCalled() - }) - }) - }) -}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c739c2ade8..f689196d79 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1,21 +1,11 @@ -import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" -import * as os from "os" -import * as fs from "fs/promises" +import fs from "fs/promises" import pWaitFor from "p-wait-for" import * as vscode from "vscode" -import * as yaml from "yaml" -import { - type Language, - type ProviderSettings, - type GlobalState, - type ClineMessage, - TelemetryEventName, -} from "@roo-code/types" +import { type Language, type ProviderSettings, type GlobalState, TelemetryEventName } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" import { TelemetryService } from "@roo-code/telemetry" -import { type ApiMessage } from "../task-persistence/apiMessages" import { ClineProvider } from "./ClineProvider" import { changeLanguage, t } from "../../i18n" @@ -37,13 +27,12 @@ import { fileExistsAtPath } from "../../utils/fs" import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts" import { singleCompletionHandler } from "../../utils/single-completion-handler" import { searchCommits } from "../../utils/git" -import { exportSettings, importSettingsWithFeedback } from "../config/importExport" +import { exportSettings, importSettings } from "../config/importExport" import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" import { TelemetrySetting } from "../../shared/TelemetrySetting" import { getWorkspacePath } from "../../utils/path" -import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" import { GetModelsOptions } from "../../shared/api" @@ -53,7 +42,6 @@ import { getCommand } from "../../utils/commands" const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"]) import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace" -import { setPendingTodoList } from "../tools/updateTodoListTool" export const webviewMessageHandler = async ( provider: ClineProvider, @@ -65,149 +53,6 @@ export const webviewMessageHandler = async ( const updateGlobalState = async (key: K, value: GlobalState[K]) => await provider.contextProxy.setValue(key, value) - /** - * Shared utility to find message indices based on timestamp - */ - const findMessageIndices = (messageTs: number, currentCline: any) => { - const timeCutoff = messageTs - 1000 // 1 second buffer before the message - const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts && msg.ts >= timeCutoff) - const apiConversationHistoryIndex = currentCline.apiConversationHistory.findIndex( - (msg: ApiMessage) => msg.ts && msg.ts >= timeCutoff, - ) - return { messageIndex, apiConversationHistoryIndex } - } - - /** - * Removes the target message and all subsequent messages - */ - const removeMessagesThisAndSubsequent = async ( - currentCline: any, - messageIndex: number, - apiConversationHistoryIndex: number, - ) => { - // Delete this message and all that follow - await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex)) - - if (apiConversationHistoryIndex !== -1) { - await currentCline.overwriteApiConversationHistory( - currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex), - ) - } - } - - /** - * Handles message deletion operations with user confirmation - */ - const handleDeleteOperation = async (messageTs: number): Promise => { - // Send message to webview to show delete confirmation dialog - await provider.postMessageToWebview({ - type: "showDeleteMessageDialog", - messageTs, - }) - } - - /** - * Handles confirmed message deletion from webview dialog - */ - const handleDeleteMessageConfirm = async (messageTs: number): Promise => { - // Only proceed if we have a current cline - if (provider.getCurrentCline()) { - const currentCline = provider.getCurrentCline()! - const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) - - if (messageIndex !== -1) { - try { - const { historyItem } = await provider.getTaskWithId(currentCline.taskId) - - // Delete this message and all subsequent messages - await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex) - - // Initialize with history item after deletion - await provider.initClineWithHistoryItem(historyItem) - } catch (error) { - console.error("Error in delete message:", error) - vscode.window.showErrorMessage( - `Error deleting message: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - } - } - - /** - * Handles message editing operations with user confirmation - */ - const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise => { - // Send message to webview to show edit confirmation dialog - await provider.postMessageToWebview({ - type: "showEditMessageDialog", - messageTs, - text: editedContent, - images, - }) - } - - /** - * Handles confirmed message editing from webview dialog - */ - const handleEditMessageConfirm = async ( - messageTs: number, - editedContent: string, - images?: string[], - ): Promise => { - // Only proceed if we have a current cline - if (provider.getCurrentCline()) { - const currentCline = provider.getCurrentCline()! - - // Use findMessageIndices to find messages based on timestamp - const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) - - if (messageIndex !== -1) { - try { - // Edit this message and delete subsequent - await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex) - - // Process the edited message as a regular user message - // This will add it to the conversation and trigger an AI response - webviewMessageHandler(provider, { - type: "askResponse", - askResponse: "messageResponse", - text: editedContent, - images, - }) - - // Don't initialize with history item for edit operations - // The webviewMessageHandler will handle the conversation state - } catch (error) { - console.error("Error in edit message:", error) - vscode.window.showErrorMessage( - `Error editing message: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - } - } - - /** - * Handles message modification operations (delete or edit) with confirmation dialog - * @param messageTs Timestamp of the message to operate on - * @param operation Type of operation ('delete' or 'edit') - * @param editedContent New content for edit operations - * @returns Promise - */ - const handleMessageModificationsOperation = async ( - messageTs: number, - operation: "delete" | "edit", - editedContent?: string, - images?: string[], - ): Promise => { - if (operation === "delete") { - await handleDeleteOperation(messageTs) - } else if (operation === "edit" && editedContent) { - await handleEditOperation(messageTs, editedContent, images) - } - } - switch (message.type) { case "webviewDidLaunch": // Load custom modes first @@ -336,10 +181,6 @@ export const webviewMessageHandler = async ( await updateGlobalState("alwaysAllowSubtasks", message.bool) await provider.postStateToWebview() break - case "alwaysAllowUpdateTodoList": - await updateGlobalState("alwaysAllowUpdateTodoList", message.bool) - await provider.postStateToWebview() - break case "askResponse": provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break @@ -374,12 +215,7 @@ export const webviewMessageHandler = async ( break case "selectImages": const images = await selectImages() - await provider.postMessageToWebview({ - type: "selectedImages", - images, - context: message.context, - messageTs: message.messageTs, - }) + await provider.postMessageToWebview({ type: "selectedImages", images }) break case "exportCurrentTask": const currentTaskId = provider.getCurrentCline()?.taskId @@ -389,7 +225,6 @@ export const webviewMessageHandler = async ( break case "shareCurrentTask": const shareTaskId = provider.getCurrentCline()?.taskId - const clineMessages = provider.getCurrentCline()?.clineMessages if (!shareTaskId) { vscode.window.showErrorMessage(t("common:errors.share_no_active_task")) break @@ -397,7 +232,7 @@ export const webviewMessageHandler = async ( try { const visibility = message.visibility || "organization" - const result = await CloudService.instance.shareTask(shareTaskId, visibility, clineMessages) + const result = await CloudService.instance.shareTask(shareTaskId, visibility) if (result.success && result.shareUrl) { // Show success notification @@ -406,13 +241,6 @@ export const webviewMessageHandler = async ( ? "common:info.public_share_link_copied" : "common:info.organization_share_link_copied" vscode.window.showInformationMessage(t(messageKey)) - - // Send success feedback to webview for inline display - await provider.postMessageToWebview({ - type: "shareTaskSuccess", - visibility, - text: result.shareUrl, - }) } else { // Handle error const errorMessage = result.error || "Failed to create share link" @@ -488,13 +316,20 @@ export const webviewMessageHandler = async ( provider.exportTaskWithId(message.text!) break case "importSettings": { - await importSettingsWithFeedback({ + const result = await importSettings({ providerSettingsManager: provider.providerSettingsManager, contextProxy: provider.contextProxy, customModesManager: provider.customModesManager, - provider: provider, }) + if (result.success) { + provider.settingsImportedAt = Date.now() + await provider.postStateToWebview() + await vscode.window.showInformationMessage(t("common:info.settings_imported")) + } else if (result.error) { + await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error })) + } + break } case "exportSettings": @@ -613,9 +448,6 @@ export const webviewMessageHandler = async ( // Specific handler for Ollama models only const { apiConfiguration: ollamaApiConfig } = await provider.getState() try { - // Flush cache first to ensure fresh models - await flushModels("ollama") - const ollamaModels = await getModels({ provider: "ollama", baseUrl: ollamaApiConfig.ollamaBaseUrl, @@ -637,9 +469,6 @@ export const webviewMessageHandler = async ( // Specific handler for LM Studio models only const { apiConfiguration: lmStudioApiConfig } = await provider.getState() try { - // Flush cache first to ensure fresh models - await flushModels("lmstudio") - const lmStudioModels = await getModels({ provider: "lmstudio", baseUrl: lmStudioApiConfig.lmStudioBaseUrl, @@ -674,22 +503,6 @@ export const webviewMessageHandler = async ( // TODO: Cache like we do for OpenRouter, etc? provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break - case "requestHuggingFaceModels": - try { - const { getHuggingFaceModels } = await import("../../api/huggingface-models") - const huggingFaceModelsResponse = await getHuggingFaceModels() - provider.postMessageToWebview({ - type: "huggingFaceModels", - huggingFaceModels: huggingFaceModelsResponse.models, - }) - } catch (error) { - console.error("Failed to fetch Hugging Face models:", error) - provider.postMessageToWebview({ - type: "huggingFaceModels", - huggingFaceModels: [], - }) - } - break case "openImage": openImage(message.text!, { values: message.values }) break @@ -739,38 +552,15 @@ export const webviewMessageHandler = async ( case "cancelTask": await provider.cancelTask() break - case "allowedCommands": { - // Validate and sanitize the commands array - const commands = message.commands ?? [] - const validCommands = Array.isArray(commands) - ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) - : [] - - await updateGlobalState("allowedCommands", validCommands) + case "allowedCommands": + await provider.context.globalState.update("allowedCommands", message.commands) // Also update workspace settings. await vscode.workspace .getConfiguration(Package.name) - .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global) + .update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global) break - } - case "deniedCommands": { - // Validate and sanitize the commands array - const commands = message.commands ?? [] - const validCommands = Array.isArray(commands) - ? commands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0) - : [] - - await updateGlobalState("deniedCommands", validCommands) - - // Also update workspace settings. - await vscode.workspace - .getConfiguration(Package.name) - .update("deniedCommands", validCommands, vscode.ConfigurationTarget.Global) - - break - } case "openCustomModesSettings": { const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() @@ -804,7 +594,7 @@ export const webviewMessageHandler = async ( const exists = await fileExistsAtPath(mcpPath) if (!exists) { - await safeWriteJson(mcpPath, { mcpServers: {} }) + await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2)) } await openFile(mcpPath) @@ -1060,34 +850,9 @@ export const webviewMessageHandler = async ( await updateGlobalState("writeDelayMs", message.value) await provider.postStateToWebview() break - case "diagnosticsEnabled": - await updateGlobalState("diagnosticsEnabled", message.bool ?? true) - await provider.postStateToWebview() - break case "terminalOutputLineLimit": - // Validate that the line limit is a positive number - const lineLimit = message.value - if (typeof lineLimit === "number" && lineLimit > 0) { - await updateGlobalState("terminalOutputLineLimit", lineLimit) - await provider.postStateToWebview() - } else { - vscode.window.showErrorMessage( - t("common:errors.invalid_line_limit") || "Terminal output line limit must be a positive number", - ) - } - break - case "terminalOutputCharacterLimit": - // Validate that the character limit is a positive number - const charLimit = message.value - if (typeof charLimit === "number" && charLimit > 0) { - await updateGlobalState("terminalOutputCharacterLimit", charLimit) - await provider.postStateToWebview() - } else { - vscode.window.showErrorMessage( - t("common:errors.invalid_character_limit") || - "Terminal output character limit must be a positive number", - ) - } + await updateGlobalState("terminalOutputLineLimit", message.value) + await provider.postStateToWebview() break case "terminalShellIntegrationTimeout": await updateGlobalState("terminalShellIntegrationTimeout", message.value) @@ -1177,48 +942,113 @@ export const webviewMessageHandler = async ( const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt } await updateGlobalState("customModePrompts", updatedPrompts) const currentState = await provider.getStateToPostToWebview() - const stateWithPrompts = { - ...currentState, - customModePrompts: updatedPrompts, - hasOpenedModeSelector: currentState.hasOpenedModeSelector ?? false, - } + const stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts } provider.postMessageToWebview({ type: "state", state: stateWithPrompts }) - - if (TelemetryService.hasInstance()) { - // Determine which setting was changed by comparing objects - const oldPrompt = existingPrompts[message.promptMode] || {} - const newPrompt = message.customPrompt - const changedSettings = Object.keys(newPrompt).filter( - (key) => - JSON.stringify((oldPrompt as Record)[key]) !== - JSON.stringify((newPrompt as Record)[key]), - ) - - if (changedSettings.length > 0) { - TelemetryService.instance.captureModeSettingChanged(changedSettings[0]) - } - } } break case "deleteMessage": { - if (provider.getCurrentCline() && typeof message.value === "number" && message.value) { - await handleMessageModificationsOperation(message.value, "delete") - } - break - } - case "submitEditedMessage": { + const answer = await vscode.window.showInformationMessage( + t("common:confirmation.delete_message"), + { modal: true }, + t("common:confirmation.just_this_message"), + t("common:confirmation.this_and_subsequent"), + ) + if ( + (answer === t("common:confirmation.just_this_message") || + answer === t("common:confirmation.this_and_subsequent")) && provider.getCurrentCline() && typeof message.value === "number" && - message.value && - message.editedMessageContent + message.value ) { - await handleMessageModificationsOperation( - message.value, - "edit", - message.editedMessageContent, - message.images, - ) + const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete + + const messageIndex = provider + .getCurrentCline()! + .clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff) + + const apiConversationHistoryIndex = provider + .getCurrentCline() + ?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff) + + if (messageIndex !== -1) { + const { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId) + + if (answer === t("common:confirmation.just_this_message")) { + // Find the next user message first + const nextUserMessage = provider + .getCurrentCline()! + .clineMessages.slice(messageIndex + 1) + .find((msg) => msg.type === "say" && msg.say === "user_feedback") + + // Handle UI messages + if (nextUserMessage) { + // Find absolute index of next user message + const nextUserMessageIndex = provider + .getCurrentCline()! + .clineMessages.findIndex((msg) => msg === nextUserMessage) + + // Keep messages before current message and after next user message + await provider + .getCurrentCline()! + .overwriteClineMessages([ + ...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex), + ...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex), + ]) + } else { + // If no next user message, keep only messages before current message + await provider + .getCurrentCline()! + .overwriteClineMessages( + provider.getCurrentCline()!.clineMessages.slice(0, messageIndex), + ) + } + + // Handle API messages + if (apiConversationHistoryIndex !== -1) { + if (nextUserMessage && nextUserMessage.ts) { + // Keep messages before current API message and after next user message + await provider + .getCurrentCline()! + .overwriteApiConversationHistory([ + ...provider + .getCurrentCline()! + .apiConversationHistory.slice(0, apiConversationHistoryIndex), + ...provider + .getCurrentCline()! + .apiConversationHistory.filter( + (msg) => msg.ts && msg.ts >= nextUserMessage.ts, + ), + ]) + } else { + // If no next user message, keep only messages before current API message + await provider + .getCurrentCline()! + .overwriteApiConversationHistory( + provider + .getCurrentCline()! + .apiConversationHistory.slice(0, apiConversationHistoryIndex), + ) + } + } + } else if (answer === t("common:confirmation.this_and_subsequent")) { + // Delete this message and all that follow + await provider + .getCurrentCline()! + .overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex)) + if (apiConversationHistoryIndex !== -1) { + await provider + .getCurrentCline()! + .overwriteApiConversationHistory( + provider + .getCurrentCline()! + .apiConversationHistory.slice(0, apiConversationHistoryIndex), + ) + } + } + + await provider.initClineWithHistoryItem(historyItem) + } } break } @@ -1236,14 +1066,6 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxWorkspaceFiles", fileCount) await provider.postStateToWebview() break - case "alwaysAllowFollowupQuestions": - await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false) - await provider.postStateToWebview() - break - case "followupAutoApproveTimeoutMs": - await updateGlobalState("followupAutoApproveTimeoutMs", message.value) - await provider.postStateToWebview() - break case "browserToolEnabled": await updateGlobalState("browserToolEnabled", message.bool ?? true) await provider.postStateToWebview() @@ -1257,10 +1079,6 @@ export const webviewMessageHandler = async ( await updateGlobalState("showRooIgnoredFiles", message.bool ?? true) await provider.postStateToWebview() break - case "hasOpenedModeSelector": - await updateGlobalState("hasOpenedModeSelector", message.bool ?? true) - await provider.postStateToWebview() - break case "maxReadFileLine": await updateGlobalState("maxReadFileLine", message.value) await provider.postStateToWebview() @@ -1270,16 +1088,6 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxConcurrentFileReads", valueToSave) await provider.postStateToWebview() break - case "includeDiagnosticMessages": - // Only apply default if the value is truly undefined (not false) - const includeValue = message.bool !== undefined ? message.bool : true - await updateGlobalState("includeDiagnosticMessages", includeValue) - await provider.postStateToWebview() - break - case "maxDiagnosticMessages": - await updateGlobalState("maxDiagnosticMessages", message.value ?? 50) - await provider.postStateToWebview() - break case "setHistoryPreviewCollapsed": // Add the new case handler await updateGlobalState("historyPreviewCollapsed", message.bool ?? false) // No need to call postStateToWebview here as the UI already updated optimistically @@ -1308,11 +1116,6 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break case "updateCondensingPrompt": - // Store the condensing prompt in customSupportPrompts["CONDENSE"] instead of customCondensingPrompt - const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {} - const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text } - await updateGlobalState("customSupportPrompts", updatedSupportPrompts) - // Also update the old field for backward compatibility during migration await updateGlobalState("customCondensingPrompt", message.text) await provider.postStateToWebview() break @@ -1450,14 +1253,6 @@ export const webviewMessageHandler = async ( } break } - case "updateTodoList": { - const payload = message.payload as { todos?: any[] } - const todos = payload?.todos - if (Array.isArray(todos)) { - await setPendingTodoList(todos) - } - break - } case "saveApiConfiguration": if (message.text && message.apiConfiguration) { try { @@ -1566,16 +1361,6 @@ export const webviewMessageHandler = async ( } } break - case "deleteMessageConfirm": - if (message.messageTs) { - await handleDeleteMessageConfirm(message.messageTs) - } - break - case "editMessageConfirm": - if (message.messageTs && message.text) { - await handleEditMessageConfirm(message.messageTs, message.text, message.images) - } - break case "getListApiConfiguration": try { const listApiConfig = await provider.providerSettingsManager.listConfig() @@ -1623,300 +1408,32 @@ export const webviewMessageHandler = async ( break case "updateCustomMode": if (message.modeConfig) { - // Check if this is a new mode or an update to an existing mode - const existingModes = await provider.customModesManager.getCustomModes() - const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug) - await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig) // Update state after saving the mode const customModes = await provider.customModesManager.getCustomModes() await updateGlobalState("customModes", customModes) await updateGlobalState("mode", message.modeConfig.slug) await provider.postStateToWebview() - - // Track telemetry for custom mode creation or update - if (TelemetryService.hasInstance()) { - if (isNewMode) { - // This is a new custom mode - TelemetryService.instance.captureCustomModeCreated( - message.modeConfig.slug, - message.modeConfig.name, - ) - } else { - // Determine which setting was changed by comparing objects - const existingMode = existingModes.find((mode) => mode.slug === message.modeConfig?.slug) - const changedSettings = existingMode - ? Object.keys(message.modeConfig).filter( - (key) => - JSON.stringify((existingMode as Record)[key]) !== - JSON.stringify((message.modeConfig as Record)[key]), - ) - : [] - - if (changedSettings.length > 0) { - TelemetryService.instance.captureModeSettingChanged(changedSettings[0]) - } - } - } } break case "deleteCustomMode": if (message.slug) { - // Get the mode details to determine source and rules folder path - const customModes = await provider.customModesManager.getCustomModes() - const modeToDelete = customModes.find((mode) => mode.slug === message.slug) + const answer = await vscode.window.showInformationMessage( + t("common:confirmation.delete_custom_mode"), + { modal: true }, + t("common:answers.yes"), + ) - if (!modeToDelete) { + if (answer !== t("common:answers.yes")) { break } - // Determine the scope based on source (project or global) - const scope = modeToDelete.source || "global" - - // Determine the rules folder path - let rulesFolderPath: string - if (scope === "project") { - const workspacePath = getWorkspacePath() - if (workspacePath) { - rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`) - } else { - rulesFolderPath = path.join(".roo", `rules-${message.slug}`) - } - } else { - // Global scope - use OS home directory - const homeDir = os.homedir() - rulesFolderPath = path.join(homeDir, ".roo", `rules-${message.slug}`) - } - - // Check if the rules folder exists - const rulesFolderExists = await fileExistsAtPath(rulesFolderPath) - - // If this is a check request, send back the folder info - if (message.checkOnly) { - await provider.postMessageToWebview({ - type: "deleteCustomModeCheck", - slug: message.slug, - rulesFolderPath: rulesFolderExists ? rulesFolderPath : undefined, - }) - break - } - - // Delete the mode await provider.customModesManager.deleteCustomMode(message.slug) - - // Delete the rules folder if it exists - if (rulesFolderExists) { - try { - await fs.rm(rulesFolderPath, { recursive: true, force: true }) - provider.log(`Deleted rules folder for mode ${message.slug}: ${rulesFolderPath}`) - } catch (error) { - provider.log(`Failed to delete rules folder for mode ${message.slug}: ${error}`) - // Notify the user about the failure - vscode.window.showErrorMessage( - t("common:errors.delete_rules_folder_failed", { - rulesFolderPath, - error: error instanceof Error ? error.message : String(error), - }), - ) - // Continue with mode deletion even if folder deletion fails - } - } - // Switch back to default mode after deletion await updateGlobalState("mode", defaultModeSlug) await provider.postStateToWebview() } break - case "exportMode": - if (message.slug) { - try { - // Get custom mode prompts to check if built-in mode has been customized - const customModePrompts = getGlobalState("customModePrompts") || {} - const customPrompt = customModePrompts[message.slug] - - // Export the mode with any customizations merged directly - const result = await provider.customModesManager.exportModeWithRules(message.slug, customPrompt) - - if (result.success && result.yaml) { - // Get last used directory for export - const lastExportPath = getGlobalState("lastModeExportPath") - let defaultUri: vscode.Uri - - if (lastExportPath) { - // Use the directory from the last export - const lastDir = path.dirname(lastExportPath) - defaultUri = vscode.Uri.file(path.join(lastDir, `${message.slug}-export.yaml`)) - } else { - // Default to workspace or home directory - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders && workspaceFolders.length > 0) { - defaultUri = vscode.Uri.file( - path.join(workspaceFolders[0].uri.fsPath, `${message.slug}-export.yaml`), - ) - } else { - defaultUri = vscode.Uri.file(`${message.slug}-export.yaml`) - } - } - - // Show save dialog - const saveUri = await vscode.window.showSaveDialog({ - defaultUri, - filters: { - "YAML files": ["yaml", "yml"], - }, - title: "Save mode export", - }) - - if (saveUri && result.yaml) { - // Save the directory for next time - await updateGlobalState("lastModeExportPath", saveUri.fsPath) - - // Write the file to the selected location - await fs.writeFile(saveUri.fsPath, result.yaml, "utf-8") - - // Send success message to webview - provider.postMessageToWebview({ - type: "exportModeResult", - success: true, - slug: message.slug, - }) - - // Show info message - vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug })) - } else { - // User cancelled the save dialog - provider.postMessageToWebview({ - type: "exportModeResult", - success: false, - error: "Export cancelled", - slug: message.slug, - }) - } - } else { - // Send error message to webview - provider.postMessageToWebview({ - type: "exportModeResult", - success: false, - error: result.error, - slug: message.slug, - }) - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - provider.log(`Failed to export mode ${message.slug}: ${errorMessage}`) - - // Send error message to webview - provider.postMessageToWebview({ - type: "exportModeResult", - success: false, - error: errorMessage, - slug: message.slug, - }) - } - } - break - case "importMode": - try { - // Get last used directory for import - const lastImportPath = getGlobalState("lastModeImportPath") - let defaultUri: vscode.Uri | undefined - - if (lastImportPath) { - // Use the directory from the last import - const lastDir = path.dirname(lastImportPath) - defaultUri = vscode.Uri.file(lastDir) - } else { - // Default to workspace or home directory - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders && workspaceFolders.length > 0) { - defaultUri = vscode.Uri.file(workspaceFolders[0].uri.fsPath) - } - } - - // Show file picker to select YAML file - const fileUri = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - defaultUri, - filters: { - "YAML files": ["yaml", "yml"], - }, - title: "Select mode export file to import", - }) - - if (fileUri && fileUri[0]) { - // Save the directory for next time - await updateGlobalState("lastModeImportPath", fileUri[0].fsPath) - - // Read the file content - const yamlContent = await fs.readFile(fileUri[0].fsPath, "utf-8") - - // Import the mode with the specified source level - const result = await provider.customModesManager.importModeWithRules( - yamlContent, - message.source || "project", // Default to project if not specified - ) - - if (result.success) { - // Update state after importing - const customModes = await provider.customModesManager.getCustomModes() - await updateGlobalState("customModes", customModes) - await provider.postStateToWebview() - - // Send success message to webview - provider.postMessageToWebview({ - type: "importModeResult", - success: true, - }) - - // Show success message - vscode.window.showInformationMessage(t("common:info.mode_imported")) - } else { - // Send error message to webview - provider.postMessageToWebview({ - type: "importModeResult", - success: false, - error: result.error, - }) - - // Show error message - vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: result.error })) - } - } else { - // User cancelled the file dialog - reset the importing state - provider.postMessageToWebview({ - type: "importModeResult", - success: false, - error: "cancelled", - }) - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - provider.log(`Failed to import mode: ${errorMessage}`) - - // Send error message to webview - provider.postMessageToWebview({ - type: "importModeResult", - success: false, - error: errorMessage, - }) - - // Show error message - vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: errorMessage })) - } - break - case "checkRulesDirectory": - if (message.slug) { - const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug) - - provider.postMessageToWebview({ - type: "checkRulesDirectoryResult", - slug: message.slug, - hasContent: hasContent, - }) - } - break case "humanRelayResponse": if (message.requestId && message.text) { vscode.commands.executeCommand(getCommand("handleHumanRelayResponse"), { @@ -1972,138 +1489,38 @@ export const webviewMessageHandler = async ( break } - - case "saveCodeIndexSettingsAtomic": { - if (!message.codeIndexSettings) { - break + case "codebaseIndexConfig": { + const codebaseIndexConfig = message.values ?? { + codebaseIndexEnabled: false, + codebaseIndexQdrantUrl: "http://localhost:6333", + codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderBaseUrl: "", + codebaseIndexEmbedderModelId: "", } - - const settings = message.codeIndexSettings + await updateGlobalState("codebaseIndexConfig", codebaseIndexConfig) try { - // Check if embedder provider has changed - const currentConfig = getGlobalState("codebaseIndexConfig") || {} - const embedderProviderChanged = - currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider - - // Save global state settings atomically - const globalStateConfig = { - ...currentConfig, - codebaseIndexEnabled: settings.codebaseIndexEnabled, - codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl, - codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider, - codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl, - codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId, - codebaseIndexEmbedderModelDimension: settings.codebaseIndexEmbedderModelDimension, // Generic dimension - codebaseIndexOpenAiCompatibleBaseUrl: settings.codebaseIndexOpenAiCompatibleBaseUrl, - codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults, - codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore, - } - - // Save global state first - await updateGlobalState("codebaseIndexConfig", globalStateConfig) - - // Save secrets directly using context proxy - if (settings.codeIndexOpenAiKey !== undefined) { - await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey) - } - if (settings.codeIndexQdrantApiKey !== undefined) { - await provider.contextProxy.storeSecret("codeIndexQdrantApiKey", settings.codeIndexQdrantApiKey) - } - if (settings.codebaseIndexOpenAiCompatibleApiKey !== undefined) { - await provider.contextProxy.storeSecret( - "codebaseIndexOpenAiCompatibleApiKey", - settings.codebaseIndexOpenAiCompatibleApiKey, - ) - } - if (settings.codebaseIndexGeminiApiKey !== undefined) { - await provider.contextProxy.storeSecret( - "codebaseIndexGeminiApiKey", - settings.codebaseIndexGeminiApiKey, - ) - } - if (settings.codebaseIndexMistralApiKey !== undefined) { - await provider.contextProxy.storeSecret( - "codebaseIndexMistralApiKey", - settings.codebaseIndexMistralApiKey, - ) - } - - // Send success response first - settings are saved regardless of validation - await provider.postMessageToWebview({ - type: "codeIndexSettingsSaved", - success: true, - settings: globalStateConfig, - }) - - // Update webview state - await provider.postStateToWebview() - - // Then handle validation and initialization if (provider.codeIndexManager) { - // If embedder provider changed, perform proactive validation - if (embedderProviderChanged) { - try { - // Force handleSettingsChange which will trigger validation - await provider.codeIndexManager.handleSettingsChange() - } catch (error) { - // Validation failed - the error state is already set by handleSettingsChange - provider.log( - `Embedder validation failed after provider change: ${error instanceof Error ? error.message : String(error)}`, - ) - // Send validation error to webview - await provider.postMessageToWebview({ - type: "indexingStatusUpdate", - values: provider.codeIndexManager.getCurrentStatus(), - }) - // Exit early - don't try to start indexing with invalid configuration - break - } - } else { - // No provider change, just handle settings normally - try { - await provider.codeIndexManager.handleSettingsChange() - } catch (error) { - // Log but don't fail - settings are saved - provider.log( - `Settings change handling error: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } + await provider.codeIndexManager.handleExternalSettingsChange() - // Wait a bit more to ensure everything is ready - await new Promise((resolve) => setTimeout(resolve, 200)) - - // Auto-start indexing if now enabled and configured + // If now configured and enabled, start indexing automatically if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) { if (!provider.codeIndexManager.isInitialized) { - try { - await provider.codeIndexManager.initialize(provider.contextProxy) - provider.log(`Code index manager initialized after settings save`) - } catch (error) { - provider.log( - `Code index initialization failed: ${error instanceof Error ? error.message : String(error)}`, - ) - // Send error status to webview - await provider.postMessageToWebview({ - type: "indexingStatusUpdate", - values: provider.codeIndexManager.getCurrentStatus(), - }) - } + await provider.codeIndexManager.initialize(provider.contextProxy) } + // Start indexing in background (no await) + provider.codeIndexManager.startIndexing() } } } catch (error) { - provider.log(`Error saving code index settings: ${error.message || error}`) - await provider.postMessageToWebview({ - type: "codeIndexSettingsSaved", - success: false, - error: error.message || "Failed to save settings", - }) + provider.log( + `[CodeIndexManager] Error during background CodeIndexManager configuration/indexing: ${error.message || error}`, + ) } + + await provider.postStateToWebview() break } - case "requestIndexingStatus": { const status = provider.codeIndexManager!.getCurrentStatus() provider.postMessageToWebview({ @@ -2112,28 +1529,6 @@ export const webviewMessageHandler = async ( }) break } - case "requestCodeIndexSecretStatus": { - // Check if secrets are set using the VSCode context directly for async access - const hasOpenAiKey = !!(await provider.context.secrets.get("codeIndexOpenAiKey")) - const hasQdrantApiKey = !!(await provider.context.secrets.get("codeIndexQdrantApiKey")) - const hasOpenAiCompatibleApiKey = !!(await provider.context.secrets.get( - "codebaseIndexOpenAiCompatibleApiKey", - )) - const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey")) - const hasMistralApiKey = !!(await provider.context.secrets.get("codebaseIndexMistralApiKey")) - - provider.postMessageToWebview({ - type: "codeIndexSecretStatus", - values: { - hasOpenAiKey, - hasQdrantApiKey, - hasOpenAiCompatibleApiKey, - hasGeminiApiKey, - hasMistralApiKey, - }, - }) - break - } case "startIndexing": { try { const manager = provider.codeIndexManager! @@ -2203,7 +1598,6 @@ export const webviewMessageHandler = async ( ) await provider.postStateToWebview() console.log(`Marketplace item installed and config file opened: ${configFilePath}`) - // Send success message to webview provider.postMessageToWebview({ type: "marketplaceInstallResult", @@ -2229,45 +1623,8 @@ export const webviewMessageHandler = async ( try { await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions) await provider.postStateToWebview() - - // Send success message to webview - provider.postMessageToWebview({ - type: "marketplaceRemoveResult", - success: true, - slug: message.mpItem.id, - }) } catch (error) { console.error(`Error removing marketplace item: ${error}`) - - // Show error message to user - vscode.window.showErrorMessage( - `Failed to remove marketplace item: ${error instanceof Error ? error.message : String(error)}`, - ) - - // Send error message to webview - provider.postMessageToWebview({ - type: "marketplaceRemoveResult", - success: false, - error: error instanceof Error ? error.message : String(error), - slug: message.mpItem.id, - }) - } - } else { - // MarketplaceManager not available or missing required parameters - const errorMessage = !marketplaceManager - ? "Marketplace manager is not available" - : "Missing required parameters for marketplace item removal" - console.error(errorMessage) - - vscode.window.showErrorMessage(errorMessage) - - if (message.mpItem?.id) { - provider.postMessageToWebview({ - type: "marketplaceRemoveResult", - success: false, - error: errorMessage, - slug: message.mpItem.id, - }) } } break @@ -2293,11 +1650,7 @@ export const webviewMessageHandler = async ( case "switchTab": { if (message.tab) { - // Capture tab shown event for all switchTab messages (which are user-initiated) - if (TelemetryService.hasInstance()) { - TelemetryService.instance.captureTabShown(message.tab) - } - + // Send a message to the webview to switch to the specified tab await provider.postMessageToWebview({ type: "action", action: "switchTab", tab: message.tab }) } break diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 39bc1df8f8..8fdc1276cf 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -21,7 +21,10 @@ "confirmation": { "reset_state": "Estàs segur que vols restablir tots els estats i emmagatzematge secret a l'extensió? Això no es pot desfer.", "delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?", - "delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}" + "delete_custom_mode": "Estàs segur que vols eliminar aquest mode personalitzat?", + "delete_message": "Què vols eliminar?", + "just_this_message": "Només aquest missatge", + "this_and_subsequent": "Aquest i tots els missatges posteriors" }, "errors": { "invalid_data_uri": "Format d'URI de dades no vàlid", @@ -32,7 +35,6 @@ "could_not_open_file_generic": "No s'ha pogut obrir el fitxer!", "checkpoint_timeout": "S'ha esgotat el temps en intentar restaurar el punt de control.", "checkpoint_failed": "Ha fallat la restauració del punt de control.", - "git_not_installed": "Git és necessari per a la funció de punts de control. Si us plau, instal·la Git per activar els punts de control.", "no_workspace": "Si us plau, obre primer una carpeta de projecte", "update_support_prompt": "Ha fallat l'actualització del missatge de suport", "reset_support_prompt": "Ha fallat el restabliment del missatge de suport", @@ -54,39 +56,21 @@ "cannot_access_path": "No es pot accedir a la ruta {{path}}: {{error}}", "settings_import_failed": "Ha fallat la importació de la configuració: {{error}}.", "mistake_limit_guidance": "Això pot indicar un error en el procés de pensament del model o la incapacitat d'utilitzar una eina correctament, que es pot mitigar amb orientació de l'usuari (p. ex. \"Prova de dividir la tasca en passos més petits\").", - "violated_organization_allowlist": "Ha fallat l'execució de la tasca: el perfil actual no és compatible amb la configuració de la teva organització", + "violated_organization_allowlist": "Ha fallat l'execució de la tasca: el perfil actual infringeix la configuració de la teva organització", "condense_failed": "Ha fallat la condensació del context", "condense_not_enough_messages": "No hi ha prou missatges per condensar el context", "condensed_recently": "El context s'ha condensat recentment; s'omet aquest intent", "condense_handler_invalid": "El gestor de l'API per condensar el context no és vàlid", "condense_context_grew": "La mida del context ha augmentat durant la condensació; s'omet aquest intent", - "url_timeout": "El lloc web ha trigat massa a carregar (timeout). Això pot ser degut a una connexió lenta, un lloc web pesat o temporalment no disponible. Pots tornar-ho a provar més tard o comprovar si la URL és correcta.", - "url_not_found": "No s'ha pogut trobar l'adreça del lloc web. Comprova si la URL és correcta i torna-ho a provar.", - "no_internet": "No hi ha connexió a internet. Comprova la teva connexió de xarxa i torna-ho a provar.", - "url_forbidden": "L'accés a aquest lloc web està prohibit. El lloc pot bloquejar l'accés automatitzat o requerir autenticació.", - "url_page_not_found": "No s'ha trobat la pàgina. Comprova si la URL és correcta.", - "url_fetch_failed": "Error en obtenir el contingut de la URL: {{error}}", - "url_fetch_error_with_url": "Error en obtenir contingut per {{url}}: {{error}}", - "command_timeout": "L'execució de la comanda ha superat el temps d'espera de {{seconds}} segons", "share_task_failed": "Ha fallat compartir la tasca. Si us plau, torna-ho a provar.", "share_no_active_task": "No hi ha cap tasca activa per compartir", "share_auth_required": "Es requereix autenticació. Si us plau, inicia sessió per compartir tasques.", "share_not_enabled": "La compartició de tasques no està habilitada per a aquesta organització.", - "share_task_not_found": "Tasca no trobada o accés denegat.", - "delete_rules_folder_failed": "Error en eliminar la carpeta de regles: {{rulesFolderPath}}. Error: {{error}}", - "claudeCode": { - "processExited": "El procés Claude Code ha sortit amb codi {{exitCode}}.", - "errorOutput": "Sortida d'error: {{output}}", - "processExitedWithError": "El procés Claude Code ha sortit amb codi {{exitCode}}. Sortida d'error: {{output}}", - "stoppedWithReason": "Claude Code s'ha aturat per la raó: {{reason}}", - "apiKeyModelPlanMismatch": "Les claus API i els plans de subscripció permeten models diferents. Assegura't que el model seleccionat estigui inclòs al teu pla." - }, - "mode_import_failed": "Ha fallat la importació del mode: {{error}}" + "share_task_not_found": "Tasca no trobada o accés denegat." }, "warnings": { "no_terminal_content": "No s'ha seleccionat contingut de terminal", - "missing_task_files": "Els fitxers d'aquesta tasca falten. Vols eliminar-la de la llista de tasques?", - "auto_import_failed": "Ha fallat la importació automàtica de la configuració de RooCode: {{error}}" + "missing_task_files": "Els fitxers d'aquesta tasca falten. Vols eliminar-la de la llista de tasques?" }, "info": { "no_changes": "No s'han trobat canvis.", @@ -95,31 +79,22 @@ "custom_storage_path_set": "Ruta d'emmagatzematge personalitzada establerta: {{path}}", "default_storage_path": "S'ha reprès l'ús de la ruta d'emmagatzematge predeterminada", "settings_imported": "Configuració importada correctament.", - "auto_import_success": "Configuració de RooCode importada automàticament des de {{filename}}", "share_link_copied": "Enllaç de compartició copiat al portapapers", "image_copied_to_clipboard": "URI de dades de la imatge copiada al portapapers", "image_saved": "Imatge desada a {{path}}", "organization_share_link_copied": "Enllaç de compartició d'organització copiat al porta-retalls!", - "public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!", - "mode_exported": "Mode '{{mode}}' exportat correctament", - "mode_imported": "Mode importat correctament" + "public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!" }, "answers": { "yes": "Sí", "no": "No", + "cancel": "Cancel·lar", "remove": "Eliminar", "keep": "Mantenir" }, - "buttons": { - "save": "Desar", - "edit": "Editar", - "learn_more": "Més informació" - }, "tasks": { "canceled": "Error de tasca: Ha estat aturada i cancel·lada per l'usuari.", - "deleted": "Fallada de tasca: Ha estat aturada i eliminada per l'usuari.", - "incomplete": "Tasca #{{taskNumber}} (Incompleta)", - "no_messages": "Tasca #{{taskNumber}} (Sense missatges)" + "deleted": "Fallada de tasca: Ha estat aturada i eliminada per l'usuari." }, "storage": { "prompt_custom_path": "Introdueix una ruta d'emmagatzematge personalitzada per a l'historial de converses o deixa-ho buit per utilitzar la ubicació predeterminada", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Clau API de Groq", - "getGroqApiKey": "Obté la clau API de Groq", - "claudeCode": { - "pathLabel": "Ruta de Claude Code", - "description": "Ruta opcional a la teva CLI de Claude Code. Per defecte 'claude' si no s'estableix.", - "placeholder": "Per defecte: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "YAML no vàlid al fitxer .roomodes a la línia {{line}}. Comprova:\n• Indentació correcta (utilitza espais, no tabuladors)\n• Cometes i claudàtors coincidents\n• Sintaxi YAML vàlida", - "schemaValidationError": "Format de modes personalitzats no vàlid a .roomodes:\n{{issues}}", - "invalidFormat": "Format de modes personalitzats no vàlid. Assegura't que la teva configuració segueix el format YAML correcte.", - "updateFailed": "Error en actualitzar el mode personalitzat: {{error}}", - "deleteFailed": "Error en eliminar el mode personalitzat: {{error}}", - "resetFailed": "Error en restablir els modes personalitzats: {{error}}", - "modeNotFound": "Error d'escriptura: Mode no trobat", - "noWorkspaceForProject": "No s'ha trobat cap carpeta d'espai de treball per al mode específic del projecte", - "rulesCleanupFailed": "El mode s'ha suprimit correctament, però no s'ha pogut suprimir la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis de suprimir manualment." - }, - "scope": { - "project": "projecte", - "global": "global" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "El mode s'ha eliminat correctament, però no s'ha pogut eliminar la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis d'eliminar manualment." + "getGroqApiKey": "Obté la clau API de Groq" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Has d'estar autenticat amb el compte de Roo Code Cloud de la teva organització.", "verification_failed": "No s'ha pogut verificar l'autenticació de l'organització." } - }, - "prompts": { - "deleteMode": { - "title": "Suprimeix el mode personalitzat", - "description": "Esteu segur que voleu suprimir aquest mode {{scope}}? Això també suprimirà la carpeta de regles associada a: {{rulesFolderPath}}", - "descriptionNoRules": "Esteu segur que voleu suprimir aquest mode personalitzat?", - "confirm": "Suprimeix" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Evitar la finalització de tasques quan hi ha todos incomplets a la llista de todos" - } } } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index fbd800f602..e52de53868 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Möchtest du wirklich alle Zustände und geheimen Speicher in der Erweiterung zurücksetzen? Dies kann nicht rückgängig gemacht werden.", "delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?", - "delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}" + "delete_custom_mode": "Möchtest du diesen benutzerdefinierten Modus wirklich löschen?", + "delete_message": "Was möchtest du löschen?", + "just_this_message": "Nur diese Nachricht", + "this_and_subsequent": "Diese und alle nachfolgenden Nachrichten" }, "errors": { "invalid_data_uri": "Ungültiges Daten-URI-Format", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Datei konnte nicht geöffnet werden!", "checkpoint_timeout": "Zeitüberschreitung beim Versuch, den Checkpoint wiederherzustellen.", "checkpoint_failed": "Fehler beim Wiederherstellen des Checkpoints.", - "git_not_installed": "Git ist für die Checkpoint-Funktion erforderlich. Bitte installiere Git, um Checkpoints zu aktivieren.", "no_workspace": "Bitte öffne zuerst einen Projektordner", "update_support_prompt": "Fehler beim Aktualisieren der Support-Nachricht", "reset_support_prompt": "Fehler beim Zurücksetzen der Support-Nachricht", @@ -50,39 +52,21 @@ "cannot_access_path": "Zugriff auf Pfad {{path}} nicht möglich: {{error}}", "settings_import_failed": "Fehler beim Importieren der Einstellungen: {{error}}.", "mistake_limit_guidance": "Dies kann auf einen Fehler im Denkprozess des Modells oder die Unfähigkeit hinweisen, ein Tool richtig zu verwenden, was durch Benutzerführung behoben werden kann (z.B. \"Versuche, die Aufgabe in kleinere Schritte zu unterteilen\").", - "violated_organization_allowlist": "Aufgabe konnte nicht ausgeführt werden: Das aktuelle Profil ist nicht kompatibel mit den Einstellungen deiner Organisation", + "violated_organization_allowlist": "Aufgabe konnte nicht ausgeführt werden: Das aktuelle Profil verstößt gegen die Einstellungen deiner Organisation", "condense_failed": "Fehler beim Verdichten des Kontexts", "condense_not_enough_messages": "Nicht genügend Nachrichten zum Verdichten des Kontexts", "condensed_recently": "Kontext wurde kürzlich verdichtet; dieser Versuch wird übersprungen", "condense_handler_invalid": "API-Handler zum Verdichten des Kontexts ist ungültig", "condense_context_grew": "Kontextgröße ist während der Verdichtung gewachsen; dieser Versuch wird übersprungen", - "url_timeout": "Die Website hat zu lange zum Laden gebraucht (Timeout). Das könnte an einer langsamen Verbindung, einer schweren Website oder vorübergehender Nichtverfügbarkeit liegen. Du kannst es später nochmal versuchen oder prüfen, ob die URL korrekt ist.", - "url_not_found": "Die Website-Adresse konnte nicht gefunden werden. Bitte prüfe, ob die URL korrekt ist und versuche es erneut.", - "no_internet": "Keine Internetverbindung. Bitte prüfe deine Netzwerkverbindung und versuche es erneut.", - "url_forbidden": "Zugriff auf diese Website ist verboten. Die Seite könnte automatisierten Zugriff blockieren oder eine Authentifizierung erfordern.", - "url_page_not_found": "Die Seite wurde nicht gefunden. Bitte prüfe, ob die URL korrekt ist.", - "url_fetch_failed": "Fehler beim Abrufen des URL-Inhalts: {{error}}", - "url_fetch_error_with_url": "Fehler beim Abrufen des Inhalts für {{url}}: {{error}}", - "command_timeout": "Zeitüberschreitung bei der Befehlsausführung nach {{seconds}} Sekunden", "share_task_failed": "Teilen der Aufgabe fehlgeschlagen. Bitte versuche es erneut.", "share_no_active_task": "Keine aktive Aufgabe zum Teilen", "share_auth_required": "Authentifizierung erforderlich. Bitte melde dich an, um Aufgaben zu teilen.", "share_not_enabled": "Aufgabenfreigabe ist für diese Organisation nicht aktiviert.", - "share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert.", - "mode_import_failed": "Fehler beim Importieren des Modus: {{error}}", - "delete_rules_folder_failed": "Fehler beim Löschen des Regelordners: {{rulesFolderPath}}. Fehler: {{error}}", - "claudeCode": { - "processExited": "Claude Code Prozess wurde mit Code {{exitCode}} beendet.", - "errorOutput": "Fehlerausgabe: {{output}}", - "processExitedWithError": "Claude Code Prozess wurde mit Code {{exitCode}} beendet. Fehlerausgabe: {{output}}", - "stoppedWithReason": "Claude Code wurde mit Grund gestoppt: {{reason}}", - "apiKeyModelPlanMismatch": "API-Schlüssel und Abonnement-Pläne erlauben verschiedene Modelle. Stelle sicher, dass das ausgewählte Modell in deinem Plan enthalten ist." - } + "share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert." }, "warnings": { "no_terminal_content": "Kein Terminal-Inhalt ausgewählt", - "missing_task_files": "Die Dateien dieser Aufgabe fehlen. Möchtest du sie aus der Aufgabenliste entfernen?", - "auto_import_failed": "Fehler beim automatischen Import der RooCode-Einstellungen: {{error}}" + "missing_task_files": "Die Dateien dieser Aufgabe fehlen. Möchtest du sie aus der Aufgabenliste entfernen?" }, "info": { "no_changes": "Keine Änderungen gefunden.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Benutzerdefinierter Speicherpfad festgelegt: {{path}}", "default_storage_path": "Auf Standardspeicherpfad zurückgesetzt", "settings_imported": "Einstellungen erfolgreich importiert.", - "auto_import_success": "RooCode-Einstellungen automatisch importiert aus {{filename}}", "share_link_copied": "Share-Link in die Zwischenablage kopiert", "image_copied_to_clipboard": "Bild-Daten-URI in die Zwischenablage kopiert", "image_saved": "Bild gespeichert unter {{path}}", "organization_share_link_copied": "Organisations-Freigabelink in die Zwischenablage kopiert!", - "public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!", - "mode_exported": "Modus '{{mode}}' erfolgreich exportiert", - "mode_imported": "Modus erfolgreich importiert" + "public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!" }, "answers": { "yes": "Ja", "no": "Nein", + "cancel": "Abbrechen", "remove": "Entfernen", "keep": "Behalten" }, - "buttons": { - "save": "Speichern", - "edit": "Bearbeiten", - "learn_more": "Mehr erfahren" - }, "tasks": { "canceled": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und abgebrochen.", - "deleted": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und gelöscht.", - "incomplete": "Aufgabe #{{taskNumber}} (Unvollständig)", - "no_messages": "Aufgabe #{{taskNumber}} (Keine Nachrichten)" + "deleted": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und gelöscht." }, "storage": { "prompt_custom_path": "Gib den benutzerdefinierten Speicherpfad für den Gesprächsverlauf ein, leer lassen für Standardspeicherort", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Groq API-Schlüssel", - "getGroqApiKey": "Groq API-Schlüssel erhalten", - "claudeCode": { - "pathLabel": "Claude Code Pfad", - "description": "Optionaler Pfad zu deiner Claude Code CLI. Standardmäßig 'claude', falls nicht festgelegt.", - "placeholder": "Standard: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "Ungültiges YAML in .roomodes-Datei in Zeile {{line}}. Bitte überprüfe:\n• Korrekte Einrückung (verwende Leerzeichen, keine Tabs)\n• Passende Anführungszeichen und Klammern\n• Gültige YAML-Syntax", - "schemaValidationError": "Ungültiges Format für benutzerdefinierte Modi in .roomodes:\n{{issues}}", - "invalidFormat": "Ungültiges Format für benutzerdefinierte Modi. Bitte stelle sicher, dass deine Einstellungen dem korrekten YAML-Format folgen.", - "updateFailed": "Fehler beim Aktualisieren des benutzerdefinierten Modus: {{error}}", - "deleteFailed": "Fehler beim Löschen des benutzerdefinierten Modus: {{error}}", - "resetFailed": "Fehler beim Zurücksetzen der benutzerdefinierten Modi: {{error}}", - "modeNotFound": "Schreibfehler: Modus nicht gefunden", - "noWorkspaceForProject": "Kein Arbeitsbereich-Ordner für projektspezifischen Modus gefunden", - "rulesCleanupFailed": "Der Modus wurde erfolgreich gelöscht, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen." - }, - "scope": { - "project": "projekt", - "global": "global" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Der Modus wurde erfolgreich entfernt, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen." + "getGroqApiKey": "Groq API-Schlüssel erhalten" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Du musst mit dem Roo Code Cloud-Konto deiner Organisation authentifiziert sein.", "verification_failed": "Die Organisationsauthentifizierung konnte nicht verifiziert werden." } - }, - "prompts": { - "deleteMode": { - "title": "Benutzerdefinierten Modus löschen", - "description": "Bist du sicher, dass du diesen {{scope}}-Modus löschen möchtest? Dadurch wird auch der zugehörige Regelordner unter {{rulesFolderPath}} gelöscht", - "descriptionNoRules": "Bist du sicher, dass du diesen benutzerdefinierten Modus löschen möchtest?", - "confirm": "Löschen" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Aufgabenabschluss verhindern, wenn unvollständige Todos in der Todo-Liste vorhanden sind" - } } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index db6341c312..7d534d8bc3 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.", "delete_config_profile": "Are you sure you want to delete this configuration profile?", - "delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}" + "delete_custom_mode": "Are you sure you want to delete this custom mode?", + "delete_message": "What would you like to delete?", + "just_this_message": "Just this message", + "this_and_subsequent": "This and all subsequent messages" }, "errors": { "invalid_data_uri": "Invalid data URI format", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Could not open file!", "checkpoint_timeout": "Timed out when attempting to restore checkpoint.", "checkpoint_failed": "Failed to restore checkpoint.", - "git_not_installed": "Git is required for the checkpoints feature. Please install Git to enable checkpoints.", "no_workspace": "Please open a project folder first", "update_support_prompt": "Failed to update support prompt", "reset_support_prompt": "Failed to reset support prompt", @@ -50,39 +52,21 @@ "cannot_access_path": "Cannot access path {{path}}: {{error}}", "settings_import_failed": "Settings import failed: {{error}}.", "mistake_limit_guidance": "This may indicate a failure in the model's thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. \"Try breaking down the task into smaller steps\").", - "violated_organization_allowlist": "Failed to run task: the current profile isn't compatible with your organization settings", + "violated_organization_allowlist": "Failed to run task: the current profile violates your organization settings", "condense_failed": "Failed to condense context", "condense_not_enough_messages": "Not enough messages to condense context", "condensed_recently": "Context was condensed recently; skipping this attempt", "condense_handler_invalid": "API handler for condensing context is invalid", "condense_context_grew": "Context size increased during condensing; skipping this attempt", - "url_timeout": "The website took too long to load (timeout). This could be due to a slow connection, heavy website, or the site being temporarily unavailable. You can try again later or check if the URL is correct.", - "url_not_found": "The website address could not be found. Please check if the URL is correct and try again.", - "no_internet": "No internet connection. Please check your network connection and try again.", - "url_forbidden": "Access to this website is forbidden. The site may block automated access or require authentication.", - "url_page_not_found": "The page was not found. Please check if the URL is correct.", - "url_fetch_failed": "Failed to fetch URL content: {{error}}", - "url_fetch_error_with_url": "Error fetching content for {{url}}: {{error}}", - "command_timeout": "Command execution timed out after {{seconds}} seconds", "share_task_failed": "Failed to share task. Please try again.", "share_no_active_task": "No active task to share", "share_auth_required": "Authentication required. Please sign in to share tasks.", "share_not_enabled": "Task sharing is not enabled for this organization.", - "share_task_not_found": "Task not found or access denied.", - "mode_import_failed": "Failed to import mode: {{error}}", - "delete_rules_folder_failed": "Failed to delete rules folder: {{rulesFolderPath}}. Error: {{error}}", - "claudeCode": { - "processExited": "Claude Code process exited with code {{exitCode}}.", - "errorOutput": "Error output: {{output}}", - "processExitedWithError": "Claude Code process exited with code {{exitCode}}. Error output: {{output}}", - "stoppedWithReason": "Claude Code stopped with reason: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - } + "share_task_not_found": "Task not found or access denied." }, "warnings": { "no_terminal_content": "No terminal content selected", - "missing_task_files": "This task's files are missing. Would you like to remove it from the task list?", - "auto_import_failed": "Failed to auto-import RooCode settings: {{error}}" + "missing_task_files": "This task's files are missing. Would you like to remove it from the task list?" }, "info": { "no_changes": "No changes found.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Custom storage path set: {{path}}", "default_storage_path": "Reverted to using default storage path", "settings_imported": "Settings imported successfully.", - "auto_import_success": "RooCode settings automatically imported from {{filename}}", "share_link_copied": "Share link copied to clipboard", "organization_share_link_copied": "Organization share link copied to clipboard!", "public_share_link_copied": "Public share link copied to clipboard!", "image_copied_to_clipboard": "Image data URI copied to clipboard", - "image_saved": "Image saved to {{path}}", - "mode_exported": "Mode '{{mode}}' exported successfully", - "mode_imported": "Mode imported successfully" + "image_saved": "Image saved to {{path}}" }, "answers": { "yes": "Yes", "no": "No", + "cancel": "Cancel", "remove": "Remove", "keep": "Keep" }, - "buttons": { - "save": "Save", - "edit": "Edit", - "learn_more": "Learn More" - }, "tasks": { "canceled": "Task error: It was stopped and canceled by the user.", - "deleted": "Task failure: It was stopped and deleted by the user.", - "incomplete": "Task #{{taskNumber}} (Incomplete)", - "no_messages": "Task #{{taskNumber}} (No messages)" + "deleted": "Task failure: It was stopped and deleted by the user." }, "storage": { "prompt_custom_path": "Enter custom conversation history storage path, leave empty to use default location", @@ -127,46 +102,11 @@ "task_prompt": "What should Roo do?", "task_placeholder": "Type your task here" }, - "customModes": { - "errors": { - "yamlParseError": "Invalid YAML in .roomodes file at line {{line}}. Please check for:\n• Proper indentation (use spaces, not tabs)\n• Matching quotes and brackets\n• Valid YAML syntax", - "schemaValidationError": "Invalid custom modes format in .roomodes:\n{{issues}}", - "invalidFormat": "Invalid custom modes format. Please ensure your settings follow the correct YAML format.", - "updateFailed": "Failed to update custom mode: {{error}}", - "deleteFailed": "Failed to delete custom mode: {{error}}", - "resetFailed": "Failed to reset custom modes: {{error}}", - "modeNotFound": "Write error: Mode not found", - "noWorkspaceForProject": "No workspace folder found for project-specific mode", - "rulesCleanupFailed": "Mode deleted successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually." - }, - "scope": { - "project": "project", - "global": "global" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Mode removed successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually." - } - }, "mdm": { "errors": { "cloud_auth_required": "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", "organization_mismatch": "You must be authenticated with your organization's Roo Code Cloud account.", "verification_failed": "Unable to verify organization authentication." } - }, - "prompts": { - "deleteMode": { - "title": "Delete Custom Mode", - "description": "Are you sure you want to delete this {{scope}} mode? This will also delete the associated rules folder at: {{rulesFolderPath}}", - "descriptionNoRules": "Are you sure you want to delete this custom mode?", - "confirm": "Delete" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Prevent task completion when there are incomplete todos in the todo list" - } } } diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index cc04abfdae..62634f065b 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "¿Estás seguro de que deseas restablecer todo el estado y el almacenamiento secreto en la extensión? Esta acción no se puede deshacer.", "delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?", - "delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}" + "delete_custom_mode": "¿Estás seguro de que deseas eliminar este modo personalizado?", + "delete_message": "¿Qué deseas eliminar?", + "just_this_message": "Solo este mensaje", + "this_and_subsequent": "Este y todos los mensajes posteriores" }, "errors": { "invalid_data_uri": "Formato de URI de datos no válido", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "¡No se pudo abrir el archivo!", "checkpoint_timeout": "Se agotó el tiempo al intentar restaurar el punto de control.", "checkpoint_failed": "Error al restaurar el punto de control.", - "git_not_installed": "Git es necesario para la función de puntos de control. Por favor, instala Git para activar los puntos de control.", "no_workspace": "Por favor, abre primero una carpeta de proyecto", "update_support_prompt": "Error al actualizar el mensaje de soporte", "reset_support_prompt": "Error al restablecer el mensaje de soporte", @@ -50,39 +52,21 @@ "cannot_access_path": "No se puede acceder a la ruta {{path}}: {{error}}", "settings_import_failed": "Error al importar la configuración: {{error}}.", "mistake_limit_guidance": "Esto puede indicar un fallo en el proceso de pensamiento del modelo o la incapacidad de usar una herramienta correctamente, lo cual puede mitigarse con orientación del usuario (ej. \"Intenta dividir la tarea en pasos más pequeños\").", - "violated_organization_allowlist": "Error al ejecutar la tarea: el perfil actual no es compatible con la configuración de tu organización", + "violated_organization_allowlist": "Error al ejecutar la tarea: el perfil actual infringe la configuración de tu organización", "condense_failed": "Error al condensar el contexto", "condense_not_enough_messages": "No hay suficientes mensajes para condensar el contexto", "condensed_recently": "El contexto se condensó recientemente; se omite este intento", "condense_handler_invalid": "El manejador de API para condensar el contexto no es válido", "condense_context_grew": "El tamaño del contexto aumentó durante la condensación; se omite este intento", - "url_timeout": "El sitio web tardó demasiado en cargar (timeout). Esto podría deberse a una conexión lenta, un sitio web pesado o que esté temporalmente no disponible. Puedes intentarlo más tarde o verificar si la URL es correcta.", - "url_not_found": "No se pudo encontrar la dirección del sitio web. Por favor verifica si la URL es correcta e inténtalo de nuevo.", - "no_internet": "Sin conexión a internet. Por favor verifica tu conexión de red e inténtalo de nuevo.", - "url_forbidden": "El acceso a este sitio web está prohibido. El sitio puede bloquear el acceso automatizado o requerir autenticación.", - "url_page_not_found": "La página no fue encontrada. Por favor verifica si la URL es correcta.", - "url_fetch_failed": "Error al obtener el contenido de la URL: {{error}}", - "url_fetch_error_with_url": "Error al obtener contenido para {{url}}: {{error}}", - "command_timeout": "La ejecución del comando superó el tiempo de espera de {{seconds}} segundos", "share_task_failed": "Error al compartir la tarea. Por favor, inténtalo de nuevo.", "share_no_active_task": "No hay tarea activa para compartir", "share_auth_required": "Se requiere autenticación. Por favor, inicia sesión para compartir tareas.", "share_not_enabled": "La compartición de tareas no está habilitada para esta organización.", - "share_task_not_found": "Tarea no encontrada o acceso denegado.", - "mode_import_failed": "Error al importar el modo: {{error}}", - "delete_rules_folder_failed": "Error al eliminar la carpeta de reglas: {{rulesFolderPath}}. Error: {{error}}", - "claudeCode": { - "processExited": "El proceso de Claude Code terminó con código {{exitCode}}.", - "errorOutput": "Salida de error: {{output}}", - "processExitedWithError": "El proceso de Claude Code terminó con código {{exitCode}}. Salida de error: {{output}}", - "stoppedWithReason": "Claude Code se detuvo por la razón: {{reason}}", - "apiKeyModelPlanMismatch": "Las claves API y los planes de suscripción permiten diferentes modelos. Asegúrate de que el modelo seleccionado esté incluido en tu plan." - } + "share_task_not_found": "Tarea no encontrada o acceso denegado." }, "warnings": { "no_terminal_content": "No hay contenido de terminal seleccionado", - "missing_task_files": "Los archivos de esta tarea faltan. ¿Deseas eliminarla de la lista de tareas?", - "auto_import_failed": "Error al importar automáticamente la configuración de RooCode: {{error}}" + "missing_task_files": "Los archivos de esta tarea faltan. ¿Deseas eliminarla de la lista de tareas?" }, "info": { "no_changes": "No se encontraron cambios.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Ruta de almacenamiento personalizada establecida: {{path}}", "default_storage_path": "Se ha vuelto a usar la ruta de almacenamiento predeterminada", "settings_imported": "Configuración importada correctamente.", - "auto_import_success": "Configuración de RooCode importada automáticamente desde {{filename}}", "share_link_copied": "Enlace de compartir copiado al portapapeles", "image_copied_to_clipboard": "URI de datos de imagen copiada al portapapeles", "image_saved": "Imagen guardada en {{path}}", "organization_share_link_copied": "¡Enlace de compartición de organización copiado al portapapeles!", - "public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!", - "mode_exported": "Modo '{{mode}}' exportado correctamente", - "mode_imported": "Modo importado correctamente" + "public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!" }, "answers": { "yes": "Sí", "no": "No", + "cancel": "Cancelar", "remove": "Eliminar", "keep": "Mantener" }, - "buttons": { - "save": "Guardar", - "edit": "Editar", - "learn_more": "Más información" - }, "tasks": { "canceled": "Error de tarea: Fue detenida y cancelada por el usuario.", - "deleted": "Fallo de tarea: Fue detenida y eliminada por el usuario.", - "incomplete": "Tarea #{{taskNumber}} (Incompleta)", - "no_messages": "Tarea #{{taskNumber}} (Sin mensajes)" + "deleted": "Fallo de tarea: Fue detenida y eliminada por el usuario." }, "storage": { "prompt_custom_path": "Ingresa la ruta de almacenamiento personalizada para el historial de conversaciones, déjala vacía para usar la ubicación predeterminada", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Clave API de Groq", - "getGroqApiKey": "Obtener clave API de Groq", - "claudeCode": { - "pathLabel": "Ruta de Claude Code", - "description": "Ruta opcional a tu CLI de Claude Code. Por defecto 'claude' si no se establece.", - "placeholder": "Por defecto: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "YAML inválido en archivo .roomodes en línea {{line}}. Verifica:\n• Indentación correcta (usa espacios, no tabs)\n• Comillas y corchetes coincidentes\n• Sintaxis YAML válida", - "schemaValidationError": "Formato inválido de modos personalizados en .roomodes:\n{{issues}}", - "invalidFormat": "Formato inválido de modos personalizados. Asegúrate de que tu configuración siga el formato YAML correcto.", - "updateFailed": "Error al actualizar modo personalizado: {{error}}", - "deleteFailed": "Error al eliminar modo personalizado: {{error}}", - "resetFailed": "Error al restablecer modos personalizados: {{error}}", - "modeNotFound": "Error de escritura: Modo no encontrado", - "noWorkspaceForProject": "No se encontró carpeta de espacio de trabajo para modo específico del proyecto", - "rulesCleanupFailed": "El modo se eliminó correctamente, pero no se pudo eliminar la carpeta de reglas en {{rulesFolderPath}}. Es posible que debas eliminarla manualmente." - }, - "scope": { - "project": "proyecto", - "global": "global" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "El modo se eliminó correctamente, pero no se pudo eliminar la carpeta de reglas en {{rulesFolderPath}}. Es posible que debas eliminarla manually." + "getGroqApiKey": "Obtener clave API de Groq" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Debes estar autenticado con la cuenta de Roo Code Cloud de tu organización.", "verification_failed": "No se pudo verificar la autenticación de la organización." } - }, - "prompts": { - "deleteMode": { - "title": "Eliminar modo personalizado", - "description": "¿Estás seguro de que quieres eliminar este modo {{scope}}? Esto también eliminará la carpeta de reglas asociada en: {{rulesFolderPath}}", - "descriptionNoRules": "¿Estás seguro de que quieres eliminar este modo personalizado?", - "confirm": "Eliminar" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Prevenir la finalización de tareas cuando hay todos incompletos en la lista de todos" - } } } diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 73f3e3d396..811be35894 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Êtes-vous sûr de vouloir réinitialiser le global state et le stockage de secrets de l'extension ? Cette action est irréversible.", "delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?", - "delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}" + "delete_custom_mode": "Êtes-vous sûr de vouloir supprimer ce mode personnalisé ?", + "delete_message": "Que souhaitez-vous supprimer ?", + "just_this_message": "Uniquement ce message", + "this_and_subsequent": "Ce message et tous les messages suivants" }, "errors": { "invalid_data_uri": "Format d'URI de données invalide", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Impossible d'ouvrir le fichier !", "checkpoint_timeout": "Expiration du délai lors de la tentative de rétablissement du checkpoint.", "checkpoint_failed": "Échec du rétablissement du checkpoint.", - "git_not_installed": "Git est requis pour la fonctionnalité des points de contrôle. Veuillez installer Git pour activer les points de contrôle.", "no_workspace": "Veuillez d'abord ouvrir un espace de travail", "update_support_prompt": "Erreur lors de la mise à jour du prompt de support", "reset_support_prompt": "Erreur lors de la réinitialisation du prompt de support", @@ -50,39 +52,21 @@ "cannot_access_path": "Impossible d'accéder au chemin {{path}} : {{error}}", "settings_import_failed": "Échec de l'importation des paramètres : {{error}}", "mistake_limit_guidance": "Cela peut indiquer un échec dans le processus de réflexion du modèle ou une incapacité à utiliser un outil correctement, ce qui peut être atténué avec des conseils de l'utilisateur (par ex. \"Essaie de diviser la tâche en étapes plus petites\").", - "violated_organization_allowlist": "Échec de l'exécution de la tâche : le profil actuel n'est pas compatible avec les paramètres de votre organisation", + "violated_organization_allowlist": "Échec de l'exécution de la tâche : le profil actuel enfreint les paramètres de votre organisation", "condense_failed": "Échec de la condensation du contexte", "condense_not_enough_messages": "Pas assez de messages pour condenser le contexte", "condensed_recently": "Le contexte a été condensé récemment ; cette tentative est ignorée", "condense_handler_invalid": "Le gestionnaire d'API pour condenser le contexte est invalide", "condense_context_grew": "La taille du contexte a augmenté pendant la condensation ; cette tentative est ignorée", - "url_timeout": "Le site web a pris trop de temps à charger (timeout). Cela pourrait être dû à une connexion lente, un site web lourd ou temporairement indisponible. Tu peux réessayer plus tard ou vérifier si l'URL est correcte.", - "url_not_found": "L'adresse du site web n'a pas pu être trouvée. Vérifie si l'URL est correcte et réessaie.", - "no_internet": "Pas de connexion internet. Vérifie ta connexion réseau et réessaie.", - "url_forbidden": "L'accès à ce site web est interdit. Le site peut bloquer l'accès automatisé ou nécessiter une authentification.", - "url_page_not_found": "La page n'a pas été trouvée. Vérifie si l'URL est correcte.", - "url_fetch_failed": "Échec de récupération du contenu de l'URL : {{error}}", - "url_fetch_error_with_url": "Erreur lors de la récupération du contenu pour {{url}} : {{error}}", - "command_timeout": "L'exécution de la commande a expiré après {{seconds}} secondes", "share_task_failed": "Échec du partage de la tâche. Veuillez réessayer.", "share_no_active_task": "Aucune tâche active à partager", "share_auth_required": "Authentification requise. Veuillez vous connecter pour partager des tâches.", "share_not_enabled": "Le partage de tâches n'est pas activé pour cette organisation.", - "share_task_not_found": "Tâche non trouvée ou accès refusé.", - "mode_import_failed": "Échec de l'importation du mode : {{error}}", - "delete_rules_folder_failed": "Échec de la suppression du dossier de règles : {{rulesFolderPath}}. Erreur : {{error}}", - "claudeCode": { - "processExited": "Le processus Claude Code s'est terminé avec le code {{exitCode}}.", - "errorOutput": "Sortie d'erreur : {{output}}", - "processExitedWithError": "Le processus Claude Code s'est terminé avec le code {{exitCode}}. Sortie d'erreur : {{output}}", - "stoppedWithReason": "Claude Code s'est arrêté pour la raison : {{reason}}", - "apiKeyModelPlanMismatch": "Les clés API et les plans d'abonnement permettent différents modèles. Assurez-vous que le modèle sélectionné est inclus dans votre plan." - } + "share_task_not_found": "Tâche non trouvée ou accès refusé." }, "warnings": { "no_terminal_content": "Aucun contenu de terminal sélectionné", - "missing_task_files": "Les fichiers de cette tâche sont introuvables. Souhaitez-vous la supprimer de la liste des tâches ?", - "auto_import_failed": "Échec de l'importation automatique des paramètres RooCode : {{error}}" + "missing_task_files": "Les fichiers de cette tâche sont introuvables. Souhaitez-vous la supprimer de la liste des tâches ?" }, "info": { "no_changes": "Aucun changement trouvé.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Chemin de stockage personnalisé défini : {{path}}", "default_storage_path": "Retour au chemin de stockage par défaut", "settings_imported": "Paramètres importés avec succès.", - "auto_import_success": "Paramètres RooCode importés automatiquement depuis {{filename}}", "share_link_copied": "Lien de partage copié dans le presse-papiers", "image_copied_to_clipboard": "URI de données d'image copiée dans le presse-papiers", "image_saved": "Image enregistrée dans {{path}}", "organization_share_link_copied": "Lien de partage d'organisation copié dans le presse-papiers !", - "public_share_link_copied": "Lien de partage public copié dans le presse-papiers !", - "mode_exported": "Mode '{{mode}}' exporté avec succès", - "mode_imported": "Mode importé avec succès" + "public_share_link_copied": "Lien de partage public copié dans le presse-papiers !" }, "answers": { "yes": "Oui", "no": "Non", + "cancel": "Annuler", "remove": "Supprimer", "keep": "Conserver" }, - "buttons": { - "save": "Enregistrer", - "edit": "Modifier", - "learn_more": "En savoir plus" - }, "tasks": { "canceled": "Erreur de tâche : Elle a été arrêtée et annulée par l'utilisateur.", - "deleted": "Échec de la tâche : Elle a été arrêtée et supprimée par l'utilisateur.", - "incomplete": "Tâche #{{taskNumber}} (Incomplète)", - "no_messages": "Tâche #{{taskNumber}} (Aucun message)" + "deleted": "Échec de la tâche : Elle a été arrêtée et supprimée par l'utilisateur." }, "storage": { "prompt_custom_path": "Entrez le chemin de stockage personnalisé pour l'historique des conversations, laissez vide pour utiliser l'emplacement par défaut", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Clé API Groq", - "getGroqApiKey": "Obtenir la clé API Groq", - "claudeCode": { - "pathLabel": "Chemin de Claude Code", - "description": "Chemin optionnel vers votre CLI Claude Code. Par défaut 'claude' si non défini.", - "placeholder": "Par défaut : claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "YAML invalide dans le fichier .roomodes à la ligne {{line}}. Vérifie :\n• L'indentation correcte (utilise des espaces, pas de tabulations)\n• Les guillemets et crochets correspondants\n• La syntaxe YAML valide", - "schemaValidationError": "Format invalide des modes personnalisés dans .roomodes :\n{{issues}}", - "invalidFormat": "Format invalide des modes personnalisés. Assure-toi que tes paramètres suivent le format YAML correct.", - "updateFailed": "Échec de la mise à jour du mode personnalisé : {{error}}", - "deleteFailed": "Échec de la suppression du mode personnalisé : {{error}}", - "resetFailed": "Échec de la réinitialisation des modes personnalisés : {{error}}", - "modeNotFound": "Erreur d'écriture : Mode non trouvé", - "noWorkspaceForProject": "Aucun dossier d'espace de travail trouvé pour le mode spécifique au projet", - "rulesCleanupFailed": "Le mode a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement." - }, - "scope": { - "project": "projet", - "global": "global" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Le mode a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement." + "getGroqApiKey": "Obtenir la clé API Groq" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Vous devez être authentifié avec le compte Roo Code Cloud de votre organisation.", "verification_failed": "Impossible de vérifier l'authentification de l'organisation." } - }, - "prompts": { - "deleteMode": { - "title": "Supprimer le mode personnalisé", - "description": "Êtes-vous sûr de vouloir supprimer ce mode {{scope}} ? Cela supprimera également le dossier de règles associé à l'adresse : {{rulesFolderPath}}", - "descriptionNoRules": "Êtes-vous sûr de vouloir supprimer ce mode personnalisé ?", - "confirm": "Supprimer" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Empêcher la finalisation des tâches lorsqu'il y a des todos incomplets dans la liste de todos" - } } } diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 03f74e1af5..bbc5558afe 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "क्या आप वाकई एक्सटेंशन में सभी स्टेट और गुप्त स्टोरेज रीसेट करना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।", "delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?", - "delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}" + "delete_custom_mode": "क्या आप वाकई इस कस्टम मोड को हटाना चाहते हैं?", + "delete_message": "आप क्या हटाना चाहते हैं?", + "just_this_message": "सिर्फ यह संदेश", + "this_and_subsequent": "यह और सभी बाद के संदेश" }, "errors": { "invalid_data_uri": "अमान्य डेटा URI फॉर्मेट", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "फ़ाइल नहीं खोली जा सकी!", "checkpoint_timeout": "चेकपॉइंट को पुनर्स्थापित करने का प्रयास करते समय टाइमआउट हो गया।", "checkpoint_failed": "चेकपॉइंट पुनर्स्थापित करने में विफल।", - "git_not_installed": "चेकपॉइंट सुविधा के लिए Git आवश्यक है। कृपया चेकपॉइंट সক্ষম करने के लिए Git इंस्टॉल करें।", "no_workspace": "कृपया पहले प्रोजेक्ट फ़ोल्डर खोलें", "update_support_prompt": "सपोर्ट प्रॉम्प्ट अपडेट करने में विफल", "reset_support_prompt": "सपोर्ट प्रॉम्प्ट रीसेट करने में विफल", @@ -50,39 +52,21 @@ "cannot_access_path": "पाथ {{path}} तक पहुंच नहीं पा रहे हैं: {{error}}", "settings_import_failed": "सेटिंग्स इम्पोर्ट करने में विफल: {{error}}।", "mistake_limit_guidance": "यह मॉडल की सोच प्रक्रिया में विफलता या किसी टूल का सही उपयोग न कर पाने का संकेत हो सकता है, जिसे उपयोगकर्ता के मार्गदर्शन से ठीक किया जा सकता है (जैसे \"कार्य को छोटे चरणों में बांटने की कोशिश करें\")।", - "violated_organization_allowlist": "कार्य चलाने में विफल: वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स के साथ संगत नहीं है", + "violated_organization_allowlist": "कार्य चलाने में विफल: वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स का उल्लंघन करती है", "condense_failed": "संदर्भ को संक्षिप्त करने में विफल", "condense_not_enough_messages": "संदर्भ को संक्षिप्त करने के लिए पर्याप्त संदेश नहीं हैं", "condensed_recently": "संदर्भ हाल ही में संक्षिप्त किया गया था; इस प्रयास को छोड़ा जा रहा है", "condense_handler_invalid": "संदर्भ को संक्षिप्त करने के लिए API हैंडलर अमान्य है", "condense_context_grew": "संक्षिप्तीकरण के दौरान संदर्भ का आकार बढ़ गया; इस प्रयास को छोड़ा जा रहा है", - "url_timeout": "वेबसाइट लोड होने में बहुत समय लगा (टाइमआउट)। यह धीमे कनेक्शन, भारी वेबसाइट या अस्थायी रूप से अनुपलब्ध होने के कारण हो सकता है। आप बाद में फिर से कोशिश कर सकते हैं या जांच सकते हैं कि URL सही है या नहीं।", - "url_not_found": "वेबसाइट का पता नहीं मिल सका। कृपया जांचें कि URL सही है और फिर से कोशिश करें।", - "no_internet": "इंटरनेट कनेक्शन नहीं है। कृपया अपना नेटवर्क कनेक्शन जांचें और फिर से कोशिश करें।", - "url_forbidden": "इस वेबसाइट तक पहुंच प्रतिबंधित है। साइट स्वचालित पहुंच को ब्लॉक कर सकती है या प्रमाणीकरण की आवश्यकता हो सकती है।", - "url_page_not_found": "पेज नहीं मिला। कृपया जांचें कि URL सही है।", - "url_fetch_failed": "URL सामग्री प्राप्त करने में त्रुटि: {{error}}", - "url_fetch_error_with_url": "{{url}} के लिए सामग्री प्राप्त करने में त्रुटि: {{error}}", - "command_timeout": "कमांड निष्पादन {{seconds}} सेकंड के बाद समय समाप्त हो गया", "share_task_failed": "कार्य साझा करने में विफल। कृपया पुनः प्रयास करें।", "share_no_active_task": "साझा करने के लिए कोई सक्रिय कार्य नहीं", "share_auth_required": "प्रमाणीकरण आवश्यक है। कार्य साझा करने के लिए कृपया साइन इन करें।", "share_not_enabled": "इस संगठन के लिए कार्य साझाकरण सक्षम नहीं है।", - "share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।", - "mode_import_failed": "मोड आयात करने में विफल: {{error}}", - "delete_rules_folder_failed": "नियम फ़ोल्डर हटाने में विफल: {{rulesFolderPath}}। त्रुटि: {{error}}", - "claudeCode": { - "processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।", - "errorOutput": "त्रुटि आउटपुट: {{output}}", - "processExitedWithError": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई। त्रुटि आउटपुट: {{output}}", - "stoppedWithReason": "Claude Code इस कारण से रुका: {{reason}}", - "apiKeyModelPlanMismatch": "API कुंजी और सब्सक्रिप्शन प्लान अलग-अलग मॉडल की अनुमति देते हैं। सुनिश्चित करें कि चयनित मॉडल आपकी योजना में शामिल है।" - } + "share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।" }, "warnings": { "no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं", - "missing_task_files": "इस टास्क की फाइलें गायब हैं। क्या आप इसे टास्क सूची से हटाना चाहते हैं?", - "auto_import_failed": "RooCode सेटिंग्स का स्वचालित आयात विफल: {{error}}" + "missing_task_files": "इस टास्क की फाइलें गायब हैं। क्या आप इसे टास्क सूची से हटाना चाहते हैं?" }, "info": { "no_changes": "कोई परिवर्तन नहीं मिला।", @@ -91,31 +75,22 @@ "custom_storage_path_set": "कस्टम स्टोरेज पाथ सेट किया गया: {{path}}", "default_storage_path": "डिफ़ॉल्ट स्टोरेज पाथ का उपयोग पुनः शुरू किया गया", "settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।", - "auto_import_success": "RooCode सेटिंग्स {{filename}} से स्वचालित रूप से आयात की गईं", "share_link_copied": "साझा लिंक क्लिपबोर्ड पर कॉपी किया गया", "image_copied_to_clipboard": "छवि डेटा URI क्लिपबोर्ड में कॉपी की गई", "image_saved": "छवि {{path}} में सहेजी गई", "organization_share_link_copied": "संगठन साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!", - "public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!", - "mode_exported": "मोड '{{mode}}' सफलतापूर्वक निर्यात किया गया", - "mode_imported": "मोड सफलतापूर्वक आयात किया गया" + "public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!" }, "answers": { "yes": "हां", "no": "नहीं", + "cancel": "रद्द करें", "remove": "हटाएं", "keep": "रखें" }, - "buttons": { - "save": "सहेजें", - "edit": "संपादित करें", - "learn_more": "और अधिक जानें" - }, "tasks": { "canceled": "टास्क त्रुटि: इसे उपयोगकर्ता द्वारा रोका और रद्द किया गया था।", - "deleted": "टास्क विफलता: इसे उपयोगकर्ता द्वारा रोका और हटाया गया था।", - "incomplete": "टास्क #{{taskNumber}} (अधूरा)", - "no_messages": "टास्क #{{taskNumber}} (कोई संदेश नहीं)" + "deleted": "टास्क विफलता: इसे उपयोगकर्ता द्वारा रोका और हटाया गया था।" }, "storage": { "prompt_custom_path": "वार्तालाप इतिहास के लिए कस्टम स्टोरेज पाथ दर्ज करें, डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ दें", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "ग्रोक एपीआई कुंजी", - "getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें", - "claudeCode": { - "pathLabel": "क्लाउड कोड पाथ", - "description": "आपके क्लाउड कोड CLI का वैकल्पिक पाथ। सेट न होने पर डिफ़ॉल्ट रूप से 'claude'।", - "placeholder": "डिफ़ॉल्ट: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": ".roomodes फ़ाइल में लाइन {{line}} पर अमान्य YAML। कृपया जांचें:\n• सही इंडेंटेशन (टैब नहीं, स्पेस का उपयोग करें)\n• मैचिंग कोट्स और ब्रैकेट्स\n• वैध YAML सिंटैक्स", - "schemaValidationError": ".roomodes में अमान्य कस्टम मोड फॉर्मेट:\n{{issues}}", - "invalidFormat": "अमान्य कस्टम मोड फॉर्मेट। कृपया सुनिश्चित करें कि आपकी सेटिंग्स सही YAML फॉर्मेट का पालन करती हैं।", - "updateFailed": "कस्टम मोड अपडेट विफल: {{error}}", - "deleteFailed": "कस्टम मोड डिलीट विफल: {{error}}", - "resetFailed": "कस्टम मोड रीसेट विफल: {{error}}", - "modeNotFound": "लेखन त्रुटि: मोड नहीं मिला", - "noWorkspaceForProject": "प्रोजेक्ट-विशिष्ट मोड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला", - "rulesCleanupFailed": "मोड सफलतापूर्वक हटा दिया गया, लेकिन {{rulesFolderPath}} पर नियम फ़ोल्डर को हटाने में विफल रहा। आपको इसे मैन्युअल रूप से हटाना पड़ सकता है।" - }, - "scope": { - "project": "परियोजना", - "global": "वैश्विक" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "मोड सफलतापूर्वक हटा दिया गया, लेकिन {{rulesFolderPath}} पर नियम फ़ोल्डर को हटाने में विफल रहा। आपको इसे मैन्युअल रूप से हटाना पड़ सकता है।" + "getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "आपको अपने संगठन के Roo Code Cloud खाते से प्रमाणित होना होगा।", "verification_failed": "संगठन प्रमाणीकरण सत्यापित करने में असमर्थ।" } - }, - "prompts": { - "deleteMode": { - "title": "कस्टम मोड हटाएं", - "description": "क्या आप वाकई इस {{scope}} मोड को हटाना चाहते हैं? यह संबंधित नियम फ़ोल्डर को भी {{rulesFolderPath}} पर हटा देगा", - "descriptionNoRules": "क्या आप वाकई इस कस्टम मोड को हटाना चाहते हैं?", - "confirm": "हटाएं" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "जब टूडू सूची में अधूरे टूडू हों तो कार्य पूर्ण होने से रोकें" - } } } diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 822341f529..4c459324b1 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Apakah kamu yakin ingin mereset semua state dan secret storage di ekstensi? Ini tidak dapat dibatalkan.", "delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?", - "delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}" + "delete_custom_mode": "Apakah kamu yakin ingin menghapus mode kustom ini?", + "delete_message": "Apa yang ingin kamu hapus?", + "just_this_message": "Hanya pesan ini", + "this_and_subsequent": "Ini dan semua pesan selanjutnya" }, "errors": { "invalid_data_uri": "Format data URI tidak valid", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Tidak dapat membuka file!", "checkpoint_timeout": "Timeout saat mencoba memulihkan checkpoint.", "checkpoint_failed": "Gagal memulihkan checkpoint.", - "git_not_installed": "Git diperlukan untuk fitur checkpoint. Silakan instal Git untuk mengaktifkan checkpoint.", "no_workspace": "Silakan buka folder proyek terlebih dahulu", "update_support_prompt": "Gagal memperbarui support prompt", "reset_support_prompt": "Gagal mereset support prompt", @@ -50,39 +52,21 @@ "cannot_access_path": "Tidak dapat mengakses path {{path}}: {{error}}", "settings_import_failed": "Impor pengaturan gagal: {{error}}.", "mistake_limit_guidance": "Ini mungkin menunjukkan kegagalan dalam proses pemikiran model atau ketidakmampuan untuk menggunakan tool dengan benar, yang dapat diatasi dengan beberapa panduan pengguna (misalnya \"Coba bagi tugas menjadi langkah-langkah yang lebih kecil\").", - "violated_organization_allowlist": "Gagal menjalankan tugas: profil saat ini tidak kompatibel dengan pengaturan organisasi kamu", + "violated_organization_allowlist": "Gagal menjalankan tugas: profil saat ini melanggar pengaturan organisasi kamu", "condense_failed": "Gagal mengompres konteks", "condense_not_enough_messages": "Tidak cukup pesan untuk mengompres konteks", "condensed_recently": "Konteks baru saja dikompres; melewati percobaan ini", "condense_handler_invalid": "Handler API untuk mengompres konteks tidak valid", "condense_context_grew": "Ukuran konteks bertambah saat mengompres; melewati percobaan ini", - "url_timeout": "Situs web membutuhkan waktu terlalu lama untuk dimuat (timeout). Ini bisa disebabkan oleh koneksi lambat, situs web berat, atau sementara tidak tersedia. Kamu bisa mencoba lagi nanti atau memeriksa apakah URL sudah benar.", - "url_not_found": "Alamat situs web tidak dapat ditemukan. Silakan periksa apakah URL sudah benar dan coba lagi.", - "no_internet": "Tidak ada koneksi internet. Silakan periksa koneksi jaringan kamu dan coba lagi.", - "url_forbidden": "Akses ke situs web ini dilarang. Situs mungkin memblokir akses otomatis atau memerlukan autentikasi.", - "url_page_not_found": "Halaman tidak ditemukan. Silakan periksa apakah URL sudah benar.", - "url_fetch_failed": "Gagal mengambil konten URL: {{error}}", - "url_fetch_error_with_url": "Error mengambil konten untuk {{url}}: {{error}}", - "command_timeout": "Eksekusi perintah waktu habis setelah {{seconds}} detik", "share_task_failed": "Gagal membagikan tugas. Silakan coba lagi.", "share_no_active_task": "Tidak ada tugas aktif untuk dibagikan", "share_auth_required": "Autentikasi diperlukan. Silakan masuk untuk berbagi tugas.", "share_not_enabled": "Berbagi tugas tidak diaktifkan untuk organisasi ini.", - "share_task_not_found": "Tugas tidak ditemukan atau akses ditolak.", - "mode_import_failed": "Gagal mengimpor mode: {{error}}", - "delete_rules_folder_failed": "Gagal menghapus folder aturan: {{rulesFolderPath}}. Error: {{error}}", - "claudeCode": { - "processExited": "Proses Claude Code keluar dengan kode {{exitCode}}.", - "errorOutput": "Output error: {{output}}", - "processExitedWithError": "Proses Claude Code keluar dengan kode {{exitCode}}. Output error: {{output}}", - "stoppedWithReason": "Claude Code berhenti karena alasan: {{reason}}", - "apiKeyModelPlanMismatch": "Kunci API dan paket berlangganan memungkinkan model yang berbeda. Pastikan model yang dipilih termasuk dalam paket Anda." - } + "share_task_not_found": "Tugas tidak ditemukan atau akses ditolak." }, "warnings": { "no_terminal_content": "Tidak ada konten terminal yang dipilih", - "missing_task_files": "File tugas ini hilang. Apakah kamu ingin menghapusnya dari daftar tugas?", - "auto_import_failed": "Gagal mengimpor pengaturan RooCode secara otomatis: {{error}}" + "missing_task_files": "File tugas ini hilang. Apakah kamu ingin menghapusnya dari daftar tugas?" }, "info": { "no_changes": "Tidak ada perubahan ditemukan.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Path penyimpanan kustom diatur: {{path}}", "default_storage_path": "Kembali menggunakan path penyimpanan default", "settings_imported": "Pengaturan berhasil diimpor.", - "auto_import_success": "Pengaturan RooCode berhasil diimpor secara otomatis dari {{filename}}", "share_link_copied": "Link bagikan disalin ke clipboard", "image_copied_to_clipboard": "Data URI gambar disalin ke clipboard", "image_saved": "Gambar disimpan ke {{path}}", "organization_share_link_copied": "Tautan berbagi organisasi disalin ke clipboard!", - "public_share_link_copied": "Tautan berbagi publik disalin ke clipboard!", - "mode_exported": "Mode '{{mode}}' berhasil diekspor", - "mode_imported": "Mode berhasil diimpor" + "public_share_link_copied": "Tautan berbagi publik disalin ke clipboard!" }, "answers": { "yes": "Ya", "no": "Tidak", + "cancel": "Batal", "remove": "Hapus", "keep": "Simpan" }, - "buttons": { - "save": "Simpan", - "edit": "Edit", - "learn_more": "Pelajari Lebih Lanjut" - }, "tasks": { "canceled": "Error tugas: Dihentikan dan dibatalkan oleh pengguna.", - "deleted": "Kegagalan tugas: Dihentikan dan dihapus oleh pengguna.", - "incomplete": "Tugas #{{taskNumber}} (Tidak lengkap)", - "no_messages": "Tugas #{{taskNumber}} (Tidak ada pesan)" + "deleted": "Kegagalan tugas: Dihentikan dan dihapus oleh pengguna." }, "storage": { "prompt_custom_path": "Masukkan path penyimpanan riwayat percakapan kustom, biarkan kosong untuk menggunakan lokasi default", @@ -127,57 +102,11 @@ "task_prompt": "Apa yang harus Roo lakukan?", "task_placeholder": "Ketik tugas kamu di sini" }, - "settings": { - "providers": { - "groqApiKey": "Kunci API Groq", - "getGroqApiKey": "Dapatkan Kunci API Groq", - "claudeCode": { - "pathLabel": "Jalur Claude Code", - "description": "Jalur opsional ke CLI Claude Code Anda. Defaultnya 'claude' jika tidak diatur.", - "placeholder": "Default: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "YAML tidak valid dalam file .roomodes pada baris {{line}}. Silakan periksa:\n• Indentasi yang benar (gunakan spasi, bukan tab)\n• Tanda kutip dan kurung yang cocok\n• Sintaks YAML yang valid", - "schemaValidationError": "Format mode kustom tidak valid dalam .roomodes:\n{{issues}}", - "invalidFormat": "Format mode kustom tidak valid. Pastikan pengaturan kamu mengikuti format YAML yang benar.", - "updateFailed": "Gagal memperbarui mode kustom: {{error}}", - "deleteFailed": "Gagal menghapus mode kustom: {{error}}", - "resetFailed": "Gagal mereset mode kustom: {{error}}", - "modeNotFound": "Kesalahan tulis: Mode tidak ditemukan", - "noWorkspaceForProject": "Tidak ditemukan folder workspace untuk mode khusus proyek", - "rulesCleanupFailed": "Mode berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual." - }, - "scope": { - "project": "proyek", - "global": "global" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Mode berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual." - } - }, "mdm": { "errors": { "cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.", "organization_mismatch": "Kamu harus diautentikasi dengan akun Roo Code Cloud organisasi kamu.", "verification_failed": "Tidak dapat memverifikasi autentikasi organisasi." } - }, - "prompts": { - "deleteMode": { - "title": "Hapus Mode Kustom", - "description": "Anda yakin ingin menghapus mode {{scope}} ini? Ini juga akan menghapus folder aturan terkait di: {{rulesFolderPath}}", - "descriptionNoRules": "Anda yakin ingin menghapus mode kustom ini?", - "confirm": "Hapus" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Mencegah penyelesaian tugas ketika ada todo yang belum selesai dalam daftar todo" - } } } diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 7ae45cc4c5..55eb562dfe 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Sei sicuro di voler reimpostare tutti gli stati e l'archiviazione segreta nell'estensione? Questa azione non può essere annullata.", "delete_config_profile": "Sei sicuro di voler eliminare questo profilo di configurazione?", - "delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}" + "delete_custom_mode": "Sei sicuro di voler eliminare questa modalità personalizzata?", + "delete_message": "Cosa desideri eliminare?", + "just_this_message": "Solo questo messaggio", + "this_and_subsequent": "Questo e tutti i messaggi successivi" }, "errors": { "invalid_data_uri": "Formato URI dati non valido", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Impossibile aprire il file!", "checkpoint_timeout": "Timeout durante il tentativo di ripristinare il checkpoint.", "checkpoint_failed": "Impossibile ripristinare il checkpoint.", - "git_not_installed": "Git è richiesto per la funzione di checkpoint. Per favore, installa Git per abilitare i checkpoint.", "no_workspace": "Per favore, apri prima una cartella di progetto", "update_support_prompt": "Errore durante l'aggiornamento del messaggio di supporto", "reset_support_prompt": "Errore durante il ripristino del messaggio di supporto", @@ -50,39 +52,21 @@ "cannot_access_path": "Impossibile accedere al percorso {{path}}: {{error}}", "settings_import_failed": "Importazione delle impostazioni fallita: {{error}}.", "mistake_limit_guidance": "Questo può indicare un fallimento nel processo di pensiero del modello o l'incapacità di utilizzare correttamente uno strumento, che può essere mitigato con la guida dell'utente (ad es. \"Prova a suddividere l'attività in passaggi più piccoli\").", - "violated_organization_allowlist": "Impossibile eseguire l'attività: il profilo corrente non è compatibile con le impostazioni della tua organizzazione", + "violated_organization_allowlist": "Impossibile eseguire l'attività: il profilo corrente viola le impostazioni della tua organizzazione", "condense_failed": "Impossibile condensare il contesto", "condense_not_enough_messages": "Non ci sono abbastanza messaggi per condensare il contesto", "condensed_recently": "Il contesto è stato condensato di recente; questo tentativo viene saltato", "condense_handler_invalid": "Il gestore API per condensare il contesto non è valido", "condense_context_grew": "La dimensione del contesto è aumentata durante la condensazione; questo tentativo viene saltato", - "url_timeout": "Il sito web ha impiegato troppo tempo a caricarsi (timeout). Questo potrebbe essere dovuto a una connessione lenta, un sito web pesante o temporaneamente non disponibile. Puoi riprovare più tardi o verificare se l'URL è corretto.", - "url_not_found": "L'indirizzo del sito web non è stato trovato. Verifica se l'URL è corretto e riprova.", - "no_internet": "Nessuna connessione internet. Verifica la tua connessione di rete e riprova.", - "url_forbidden": "L'accesso a questo sito web è vietato. Il sito potrebbe bloccare l'accesso automatizzato o richiedere autenticazione.", - "url_page_not_found": "La pagina non è stata trovata. Verifica se l'URL è corretto.", - "url_fetch_failed": "Errore nel recupero del contenuto URL: {{error}}", - "url_fetch_error_with_url": "Errore nel recupero del contenuto per {{url}}: {{error}}", - "command_timeout": "Esecuzione del comando scaduta dopo {{seconds}} secondi", "share_task_failed": "Condivisione dell'attività fallita. Riprova.", "share_no_active_task": "Nessuna attività attiva da condividere", "share_auth_required": "Autenticazione richiesta. Accedi per condividere le attività.", "share_not_enabled": "La condivisione delle attività non è abilitata per questa organizzazione.", - "share_task_not_found": "Attività non trovata o accesso negato.", - "mode_import_failed": "Importazione della modalità non riuscita: {{error}}", - "delete_rules_folder_failed": "Impossibile eliminare la cartella delle regole: {{rulesFolderPath}}. Errore: {{error}}", - "claudeCode": { - "processExited": "Il processo Claude Code è terminato con codice {{exitCode}}.", - "errorOutput": "Output di errore: {{output}}", - "processExitedWithError": "Il processo Claude Code è terminato con codice {{exitCode}}. Output di errore: {{output}}", - "stoppedWithReason": "Claude Code si è fermato per il motivo: {{reason}}", - "apiKeyModelPlanMismatch": "Le chiavi API e i piani di abbonamento consentono modelli diversi. Assicurati che il modello selezionato sia incluso nel tuo piano." - } + "share_task_not_found": "Attività non trovata o accesso negato." }, "warnings": { "no_terminal_content": "Nessun contenuto del terminale selezionato", - "missing_task_files": "I file di questa attività sono mancanti. Vuoi rimuoverla dall'elenco delle attività?", - "auto_import_failed": "Importazione automatica delle impostazioni RooCode fallita: {{error}}" + "missing_task_files": "I file di questa attività sono mancanti. Vuoi rimuoverla dall'elenco delle attività?" }, "info": { "no_changes": "Nessuna modifica trovata.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Percorso di archiviazione personalizzato impostato: {{path}}", "default_storage_path": "Tornato al percorso di archiviazione predefinito", "settings_imported": "Impostazioni importate con successo.", - "auto_import_success": "Impostazioni RooCode importate automaticamente da {{filename}}", "share_link_copied": "Link di condivisione copiato negli appunti", "image_copied_to_clipboard": "URI dati dell'immagine copiato negli appunti", "image_saved": "Immagine salvata in {{path}}", "organization_share_link_copied": "Link di condivisione organizzazione copiato negli appunti!", - "public_share_link_copied": "Link di condivisione pubblica copiato negli appunti!", - "mode_exported": "Modalità '{{mode}}' esportata con successo", - "mode_imported": "Modalità importata con successo" + "public_share_link_copied": "Link di condivisione pubblica copiato negli appunti!" }, "answers": { "yes": "Sì", "no": "No", + "cancel": "Annulla", "remove": "Rimuovi", "keep": "Mantieni" }, - "buttons": { - "save": "Salva", - "edit": "Modifica", - "learn_more": "Scopri di più" - }, "tasks": { "canceled": "Errore attività: È stata interrotta e annullata dall'utente.", - "deleted": "Fallimento attività: È stata interrotta ed eliminata dall'utente.", - "incomplete": "Attività #{{taskNumber}} (Incompleta)", - "no_messages": "Attività #{{taskNumber}} (Nessun messaggio)" + "deleted": "Fallimento attività: È stata interrotta ed eliminata dall'utente." }, "storage": { "prompt_custom_path": "Inserisci il percorso di archiviazione personalizzato per la cronologia delle conversazioni, lascia vuoto per utilizzare la posizione predefinita", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Chiave API Groq", - "getGroqApiKey": "Ottieni chiave API Groq", - "claudeCode": { - "pathLabel": "Percorso Claude Code", - "description": "Percorso opzionale alla tua CLI Claude Code. Predefinito 'claude' se non impostato.", - "placeholder": "Predefinito: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "YAML non valido nel file .roomodes alla riga {{line}}. Controlla:\n• Indentazione corretta (usa spazi, non tab)\n• Virgolette e parentesi corrispondenti\n• Sintassi YAML valida", - "schemaValidationError": "Formato modalità personalizzate non valido in .roomodes:\n{{issues}}", - "invalidFormat": "Formato modalità personalizzate non valido. Assicurati che le tue impostazioni seguano il formato YAML corretto.", - "updateFailed": "Aggiornamento modalità personalizzata fallito: {{error}}", - "deleteFailed": "Eliminazione modalità personalizzata fallita: {{error}}", - "resetFailed": "Reset modalità personalizzate fallito: {{error}}", - "modeNotFound": "Errore di scrittura: Modalità non trovata", - "noWorkspaceForProject": "Nessuna cartella workspace trovata per la modalità specifica del progetto", - "rulesCleanupFailed": "La modalità è stata eliminata con successo, ma non è stato possibile eliminare la cartella delle regole in {{rulesFolderPath}}. Potrebbe essere necessario eliminarla manualmente." - }, - "scope": { - "project": "progetto", - "global": "globale" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "La modalità è stata rimossa con successo, ma non è stato possibile eliminare la cartella delle regole in {{rulesFolderPath}}. Potrebbe essere necessario eliminarla manualmente." + "getGroqApiKey": "Ottieni chiave API Groq" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Devi essere autenticato con l'account Roo Code Cloud della tua organizzazione.", "verification_failed": "Impossibile verificare l'autenticazione dell'organizzazione." } - }, - "prompts": { - "deleteMode": { - "title": "Elimina Modalità Personalizzata", - "description": "Sei sicuro di voler eliminare questa modalità {{scope}}? Questo eliminerà anche la cartella delle regole associata a: {{rulesFolderPath}}", - "descriptionNoRules": "Sei sicuro di voler eliminare questa modalità personalizzata?", - "confirm": "Elimina" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Impedire il completamento delle attività quando ci sono todo incompleti nella lista dei todo" - } } } diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index da8124b48c..965ba73bca 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "拡張機能のすべての状態とシークレットストレージをリセットしてもよろしいですか?この操作は元に戻せません。", "delete_config_profile": "この設定プロファイルを削除してもよろしいですか?", - "delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}" + "delete_custom_mode": "このカスタムモードを削除してもよろしいですか?", + "delete_message": "何を削除しますか?", + "just_this_message": "このメッセージのみ", + "this_and_subsequent": "これ以降のすべてのメッセージ" }, "errors": { "invalid_data_uri": "データURIフォーマットが無効です", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "ファイルを開けませんでした!", "checkpoint_timeout": "チェックポイントの復元を試みる際にタイムアウトしました。", "checkpoint_failed": "チェックポイントの復元に失敗しました。", - "git_not_installed": "チェックポイント機能にはGitが必要です。チェックポイントを有効にするにはGitをインストールしてください。", "no_workspace": "まずプロジェクトフォルダを開いてください", "update_support_prompt": "サポートメッセージの更新に失敗しました", "reset_support_prompt": "サポートメッセージのリセットに失敗しました", @@ -50,39 +52,21 @@ "cannot_access_path": "パス {{path}} にアクセスできません:{{error}}", "settings_import_failed": "設定のインポートに失敗しました:{{error}}", "mistake_limit_guidance": "これは、モデルの思考プロセスの失敗やツールを適切に使用できないことを示している可能性があり、ユーザーのガイダンスによって軽減できます(例:「タスクをより小さなステップに分割してみてください」)。", - "violated_organization_allowlist": "タスクの実行に失敗しました: 現在のプロファイルは組織の設定と互換性がありません", + "violated_organization_allowlist": "タスクの実行に失敗しました: 現在のプロファイルは組織の設定に違反しています", "condense_failed": "コンテキストの圧縮に失敗しました", "condense_not_enough_messages": "コンテキストを圧縮するのに十分なメッセージがありません", "condensed_recently": "コンテキストは最近圧縮されました;この試行をスキップします", "condense_handler_invalid": "コンテキストを圧縮するためのAPIハンドラーが無効です", "condense_context_grew": "圧縮中にコンテキストサイズが増加しました;この試行をスキップします", - "url_timeout": "ウェブサイトの読み込みがタイムアウトしました。接続が遅い、ウェブサイトが重い、または一時的に利用できない可能性があります。後でもう一度試すか、URLが正しいか確認してください。", - "url_not_found": "ウェブサイトのアドレスが見つかりませんでした。URLが正しいか確認してもう一度試してください。", - "no_internet": "インターネット接続がありません。ネットワーク接続を確認してもう一度試してください。", - "url_forbidden": "このウェブサイトへのアクセスが禁止されています。サイトが自動アクセスをブロックしているか、認証が必要な可能性があります。", - "url_page_not_found": "ページが見つかりませんでした。URLが正しいか確認してください。", - "url_fetch_failed": "URLコンテンツの取得に失敗しました:{{error}}", - "url_fetch_error_with_url": "{{url}} のコンテンツ取得エラー:{{error}}", - "command_timeout": "コマンドの実行が{{seconds}}秒後にタイムアウトしました", "share_task_failed": "タスクの共有に失敗しました", "share_no_active_task": "共有するアクティブなタスクがありません", "share_auth_required": "認証が必要です。タスクを共有するにはサインインしてください。", "share_not_enabled": "この組織ではタスク共有が有効になっていません。", - "share_task_not_found": "タスクが見つからないか、アクセスが拒否されました。", - "mode_import_failed": "モードのインポートに失敗しました:{{error}}", - "delete_rules_folder_failed": "ルールフォルダの削除に失敗しました:{{rulesFolderPath}}。エラー:{{error}}", - "claudeCode": { - "processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。", - "errorOutput": "エラー出力:{{output}}", - "processExitedWithError": "Claude Code プロセスがコード {{exitCode}} で終了しました。エラー出力:{{output}}", - "stoppedWithReason": "Claude Code が理由により停止しました:{{reason}}", - "apiKeyModelPlanMismatch": "API キーとサブスクリプションプランでは異なるモデルが利用可能です。選択したモデルがプランに含まれていることを確認してください。" - } + "share_task_not_found": "タスクが見つからないか、アクセスが拒否されました。" }, "warnings": { "no_terminal_content": "選択されたターミナルコンテンツがありません", - "missing_task_files": "このタスクのファイルが見つかりません。タスクリストから削除しますか?", - "auto_import_failed": "RooCode設定の自動インポートに失敗しました:{{error}}" + "missing_task_files": "このタスクのファイルが見つかりません。タスクリストから削除しますか?" }, "info": { "no_changes": "変更は見つかりませんでした。", @@ -91,31 +75,22 @@ "custom_storage_path_set": "カスタムストレージパスが設定されました:{{path}}", "default_storage_path": "デフォルトのストレージパスに戻りました", "settings_imported": "設定が正常にインポートされました。", - "auto_import_success": "RooCode設定が{{filename}}から自動インポートされました", "share_link_copied": "共有リンクがクリップボードにコピーされました", "image_copied_to_clipboard": "画像データURIがクリップボードにコピーされました", "image_saved": "画像を{{path}}に保存しました", "organization_share_link_copied": "組織共有リンクがクリップボードにコピーされました!", - "public_share_link_copied": "公開共有リンクがクリップボードにコピーされました!", - "mode_exported": "モード「{{mode}}」が正常にエクスポートされました", - "mode_imported": "モードが正常にインポートされました" + "public_share_link_copied": "公開共有リンクがクリップボードにコピーされました!" }, "answers": { "yes": "はい", "no": "いいえ", + "cancel": "キャンセル", "remove": "削除", "keep": "保持" }, - "buttons": { - "save": "保存", - "edit": "編集", - "learn_more": "詳細" - }, "tasks": { "canceled": "タスクエラー:ユーザーによって停止およびキャンセルされました。", - "deleted": "タスク失敗:ユーザーによって停止および削除されました。", - "incomplete": "タスク #{{taskNumber}} (未完了)", - "no_messages": "タスク #{{taskNumber}} (メッセージなし)" + "deleted": "タスク失敗:ユーザーによって停止および削除されました。" }, "storage": { "prompt_custom_path": "会話履歴のカスタムストレージパスを入力してください。デフォルトの場所を使用する場合は空のままにしてください", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Groq APIキー", - "getGroqApiKey": "Groq APIキーを取得", - "claudeCode": { - "pathLabel": "Claude Code パス", - "description": "Claude Code CLI へのオプションのパス。設定されていない場合は、デフォルトで「claude」になります。", - "placeholder": "デフォルト: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": ".roomodes ファイルの {{line}} 行目で無効な YAML です。以下を確認してください:\n• 正しいインデント(タブではなくスペースを使用)\n• 引用符と括弧の対応\n• 有効な YAML 構文", - "schemaValidationError": ".roomodes のカスタムモード形式が無効です:\n{{issues}}", - "invalidFormat": "カスタムモード形式が無効です。設定が正しい YAML 形式に従っていることを確認してください。", - "updateFailed": "カスタムモードの更新に失敗しました:{{error}}", - "deleteFailed": "カスタムモードの削除に失敗しました:{{error}}", - "resetFailed": "カスタムモードのリセットに失敗しました:{{error}}", - "modeNotFound": "書き込みエラー:モードが見つかりません", - "noWorkspaceForProject": "プロジェクト固有モード用のワークスペースフォルダーが見つかりません", - "rulesCleanupFailed": "モードは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。" - }, - "scope": { - "project": "プロジェクト", - "global": "グローバル" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "モードは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。" + "getGroqApiKey": "Groq APIキーを取得" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "組織の Roo Code Cloud アカウントで認証する必要があります。", "verification_failed": "組織認証の確認ができませんでした。" } - }, - "prompts": { - "deleteMode": { - "title": "カスタムモードの削除", - "description": "この{{scope}}モードを削除してもよろしいですか?これにより、関連するルールフォルダーも{{rulesFolderPath}}で削除されます", - "descriptionNoRules": "このカスタムモードを削除してもよろしいですか?", - "confirm": "削除" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Todoリストに未完了のTodoがある場合、タスクの完了を防ぐ" - } } } diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index a95908ffec..670326b851 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "확장 프로그램의 모든 상태와 보안 저장소를 재설정하시겠습니까? 이 작업은 취소할 수 없습니다.", "delete_config_profile": "이 구성 프로필을 삭제하시겠습니까?", - "delete_custom_mode_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}" + "delete_custom_mode": "이 사용자 지정 모드를 삭제하시겠습니까?", + "delete_message": "무엇을 삭제하시겠습니까?", + "just_this_message": "이 메시지만", + "this_and_subsequent": "이 메시지와 모든 후속 메시지" }, "errors": { "invalid_data_uri": "잘못된 데이터 URI 형식", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "파일을 열 수 없습니다!", "checkpoint_timeout": "체크포인트 복원을 시도하는 중 시간 초과되었습니다.", "checkpoint_failed": "체크포인트 복원에 실패했습니다.", - "git_not_installed": "체크포인트 기능을 사용하려면 Git이 필요합니다. 체크포인트를 활성화하려면 Git을 설치하세요.", "no_workspace": "먼저 프로젝트 폴더를 열어주세요", "update_support_prompt": "지원 프롬프트 업데이트에 실패했습니다", "reset_support_prompt": "지원 프롬프트 재설정에 실패했습니다", @@ -50,39 +52,21 @@ "cannot_access_path": "경로 {{path}}에 접근할 수 없습니다: {{error}}", "settings_import_failed": "설정 가져오기 실패: {{error}}.", "mistake_limit_guidance": "이는 모델의 사고 과정 실패나 도구를 제대로 사용하지 못하는 것을 나타낼 수 있으며, 사용자 가이드를 통해 완화할 수 있습니다 (예: \"작업을 더 작은 단계로 나누어 시도해보세요\").", - "violated_organization_allowlist": "작업 실행 실패: 현재 프로필이 조직 설정과 호환되지 않습니다", + "violated_organization_allowlist": "작업 실행 실패: 현재 프로필이 조직 설정을 위반합니다", "condense_failed": "컨텍스트 압축에 실패했습니다", "condense_not_enough_messages": "컨텍스트를 압축할 메시지가 충분하지 않습니다", "condensed_recently": "컨텍스트가 최근 압축되었습니다; 이 시도를 건너뜁니다", "condense_handler_invalid": "컨텍스트 압축을 위한 API 핸들러가 유효하지 않습니다", "condense_context_grew": "압축 중 컨텍스트 크기가 증가했습니다; 이 시도를 건너뜁니다", - "url_timeout": "웹사이트 로딩이 너무 오래 걸렸습니다(타임아웃). 느린 연결, 무거운 웹사이트 또는 일시적으로 사용할 수 없는 상태일 수 있습니다. 나중에 다시 시도하거나 URL이 올바른지 확인해 주세요.", - "url_not_found": "웹사이트 주소를 찾을 수 없습니다. URL이 올바른지 확인하고 다시 시도해 주세요.", - "no_internet": "인터넷 연결이 없습니다. 네트워크 연결을 확인하고 다시 시도해 주세요.", - "url_forbidden": "이 웹사이트에 대한 접근이 금지되었습니다. 사이트가 자동 접근을 차단하거나 인증이 필요할 수 있습니다.", - "url_page_not_found": "페이지를 찾을 수 없습니다. URL이 올바른지 확인해 주세요.", - "url_fetch_failed": "URL 콘텐츠 가져오기 실패: {{error}}", - "url_fetch_error_with_url": "{{url}} 콘텐츠 가져오기 오류: {{error}}", - "command_timeout": "명령 실행 시간이 {{seconds}}초 후 초과되었습니다", "share_task_failed": "작업 공유에 실패했습니다", "share_no_active_task": "공유할 활성 작업이 없습니다", "share_auth_required": "인증이 필요합니다. 작업을 공유하려면 로그인하세요.", "share_not_enabled": "이 조직에서는 작업 공유가 활성화되지 않았습니다.", - "share_task_not_found": "작업을 찾을 수 없거나 액세스가 거부되었습니다.", - "mode_import_failed": "모드 가져오기 실패: {{error}}", - "delete_rules_folder_failed": "규칙 폴더 삭제 실패: {{rulesFolderPath}}. 오류: {{error}}", - "claudeCode": { - "processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.", - "errorOutput": "오류 출력: {{output}}", - "processExitedWithError": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다. 오류 출력: {{output}}", - "stoppedWithReason": "Claude Code가 다음 이유로 중지되었습니다: {{reason}}", - "apiKeyModelPlanMismatch": "API 키와 구독 플랜에서 다른 모델을 허용합니다. 선택한 모델이 플랜에 포함되어 있는지 확인하세요." - } + "share_task_not_found": "작업을 찾을 수 없거나 액세스가 거부되었습니다." }, "warnings": { "no_terminal_content": "선택된 터미널 내용이 없습니다", - "missing_task_files": "이 작업의 파일이 누락되었습니다. 작업 목록에서 제거하시겠습니까?", - "auto_import_failed": "RooCode 설정 자동 가져오기 실패: {{error}}" + "missing_task_files": "이 작업의 파일이 누락되었습니다. 작업 목록에서 제거하시겠습니까?" }, "info": { "no_changes": "변경 사항이 없습니다.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "사용자 지정 저장 경로 설정됨: {{path}}", "default_storage_path": "기본 저장 경로로 되돌아갔습니다", "settings_imported": "설정이 성공적으로 가져와졌습니다.", - "auto_import_success": "{{filename}}에서 RooCode 설정을 자동으로 가져왔습니다", "share_link_copied": "공유 링크가 클립보드에 복사되었습니다", "image_copied_to_clipboard": "이미지 데이터 URI가 클립보드에 복사되었습니다", "image_saved": "이미지가 {{path}}에 저장되었습니다", "organization_share_link_copied": "조직 공유 링크가 클립보드에 복사되었습니다!", - "public_share_link_copied": "공개 공유 링크가 클립보드에 복사되었습니다!", - "mode_exported": "'{{mode}}' 모드가 성공적으로 내보내졌습니다", - "mode_imported": "모드를 성공적으로 가져왔습니다" + "public_share_link_copied": "공개 공유 링크가 클립보드에 복사되었습니다!" }, "answers": { "yes": "예", "no": "아니오", + "cancel": "취소", "remove": "제거", "keep": "유지" }, - "buttons": { - "save": "저장", - "edit": "편집", - "learn_more": "더 알아보기" - }, "tasks": { "canceled": "작업 오류: 사용자에 의해 중지 및 취소되었습니다.", - "deleted": "작업 실패: 사용자에 의해 중지 및 삭제되었습니다.", - "incomplete": "작업 #{{taskNumber}} (미완료)", - "no_messages": "작업 #{{taskNumber}} (메시지 없음)" + "deleted": "작업 실패: 사용자에 의해 중지 및 삭제되었습니다." }, "storage": { "prompt_custom_path": "대화 내역을 위한 사용자 지정 저장 경로를 입력하세요. 기본 위치를 사용하려면 비워두세요", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Groq API 키", - "getGroqApiKey": "Groq API 키 받기", - "claudeCode": { - "pathLabel": "Claude Code 경로", - "description": "Claude Code CLI의 선택적 경로입니다. 설정되지 않은 경우 기본값은 'claude'입니다.", - "placeholder": "기본값: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": ".roomodes 파일의 {{line}}번째 줄에서 유효하지 않은 YAML입니다. 다음을 확인하세요:\n• 올바른 들여쓰기 (탭이 아닌 공백 사용)\n• 일치하는 따옴표와 괄호\n• 유효한 YAML 구문", - "schemaValidationError": ".roomodes의 사용자 정의 모드 형식이 유효하지 않습니다:\n{{issues}}", - "invalidFormat": "사용자 정의 모드 형식이 유효하지 않습니다. 설정이 올바른 YAML 형식을 따르는지 확인하세요.", - "updateFailed": "사용자 정의 모드 업데이트 실패: {{error}}", - "deleteFailed": "사용자 정의 모드 삭제 실패: {{error}}", - "resetFailed": "사용자 정의 모드 재설정 실패: {{error}}", - "modeNotFound": "쓰기 오류: 모드를 찾을 수 없습니다", - "noWorkspaceForProject": "프로젝트별 모드용 작업 공간 폴더를 찾을 수 없습니다", - "rulesCleanupFailed": "모드가 성공적으로 삭제되었지만 {{rulesFolderPath}}의 규칙 폴더를 삭제하지 못했습니다. 수동으로 삭제해야 할 수도 있습니다." - }, - "scope": { - "project": "프로젝트", - "global": "글로벌" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "모드가 성공적으로 제거되었지만 {{rulesFolderPath}}의 규칙 폴더를 삭제하지 못했습니다. 수동으로 삭제해야 할 수도 있습니다." + "getGroqApiKey": "Groq API 키 받기" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "조직의 Roo Code Cloud 계정으로 인증해야 합니다.", "verification_failed": "조직 인증을 확인할 수 없습니다." } - }, - "prompts": { - "deleteMode": { - "title": "사용자 정의 모드 삭제", - "description": "이 {{scope}} 모드를 삭제하시겠습니까? 이렇게 하면 {{rulesFolderPath}}의 관련 규칙 폴더도 삭제됩니다.", - "descriptionNoRules": "이 사용자 정의 모드를 삭제하시겠습니까?", - "confirm": "삭제" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "할 일 목록에 미완료된 할 일이 있을 때 작업 완료를 방지" - } } } diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index ac7df81e42..7a97a6b1bc 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Weet je zeker dat je alle status en geheime opslag in de extensie wilt resetten? Dit kan niet ongedaan worden gemaakt.", "delete_config_profile": "Weet je zeker dat je dit configuratieprofiel wilt verwijderen?", - "delete_custom_mode_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}" + "delete_custom_mode": "Weet je zeker dat je deze aangepaste modus wilt verwijderen?", + "delete_message": "Wat wil je verwijderen?", + "just_this_message": "Alleen dit bericht", + "this_and_subsequent": "Dit en alle volgende berichten" }, "errors": { "invalid_data_uri": "Ongeldig data-URI-formaat", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Kon bestand niet openen!", "checkpoint_timeout": "Time-out bij het herstellen van checkpoint.", "checkpoint_failed": "Herstellen van checkpoint mislukt.", - "git_not_installed": "Git is vereist voor de checkpoint-functie. Installeer Git om checkpoints in te schakelen.", "no_workspace": "Open eerst een projectmap", "update_support_prompt": "Bijwerken van ondersteuningsprompt mislukt", "reset_support_prompt": "Resetten van ondersteuningsprompt mislukt", @@ -50,39 +52,21 @@ "cannot_access_path": "Kan pad {{path}} niet openen: {{error}}", "settings_import_failed": "Importeren van instellingen mislukt: {{error}}.", "mistake_limit_guidance": "Dit kan duiden op een fout in het denkproces van het model of het onvermogen om een tool correct te gebruiken, wat kan worden verminderd met gebruikersbegeleiding (bijv. \"Probeer de taak op te delen in kleinere stappen\").", - "violated_organization_allowlist": "Taak uitvoeren mislukt: het huidige profiel is niet compatibel met de instellingen van uw organisatie", + "violated_organization_allowlist": "Taak uitvoeren mislukt: het huidige profiel schendt de instellingen van uw organisatie", "condense_failed": "Comprimeren van context mislukt", "condense_not_enough_messages": "Niet genoeg berichten om context te comprimeren", "condensed_recently": "Context is recent gecomprimeerd; deze poging wordt overgeslagen", "condense_handler_invalid": "API-handler voor het comprimeren van context is ongeldig", "condense_context_grew": "Contextgrootte nam toe tijdens comprimeren; deze poging wordt overgeslagen", - "url_timeout": "De website deed er te lang over om te laden (timeout). Dit kan komen door een trage verbinding, een zware website of tijdelijke onbeschikbaarheid. Je kunt het later opnieuw proberen of controleren of de URL correct is.", - "url_not_found": "Het websiteadres kon niet worden gevonden. Controleer of de URL correct is en probeer opnieuw.", - "no_internet": "Geen internetverbinding. Controleer je netwerkverbinding en probeer opnieuw.", - "url_forbidden": "Toegang tot deze website is verboden. De site kan geautomatiseerde toegang blokkeren of authenticatie vereisen.", - "url_page_not_found": "De pagina werd niet gevonden. Controleer of de URL correct is.", - "url_fetch_failed": "Fout bij ophalen van URL-inhoud: {{error}}", - "url_fetch_error_with_url": "Fout bij ophalen van inhoud voor {{url}}: {{error}}", - "command_timeout": "Time-out bij uitvoeren van commando na {{seconds}} seconden", "share_task_failed": "Delen van taak mislukt", "share_no_active_task": "Geen actieve taak om te delen", "share_auth_required": "Authenticatie vereist. Log in om taken te delen.", "share_not_enabled": "Taken delen is niet ingeschakeld voor deze organisatie.", - "share_task_not_found": "Taak niet gevonden of toegang geweigerd.", - "mode_import_failed": "Importeren van modus mislukt: {{error}}", - "delete_rules_folder_failed": "Kan regelmap niet verwijderen: {{rulesFolderPath}}. Fout: {{error}}", - "claudeCode": { - "processExited": "Claude Code proces beëindigd met code {{exitCode}}.", - "errorOutput": "Foutuitvoer: {{output}}", - "processExitedWithError": "Claude Code proces beëindigd met code {{exitCode}}. Foutuitvoer: {{output}}", - "stoppedWithReason": "Claude Code gestopt om reden: {{reason}}", - "apiKeyModelPlanMismatch": "API-sleutels en abonnementsplannen staan verschillende modellen toe. Zorg ervoor dat het geselecteerde model is opgenomen in je plan." - } + "share_task_not_found": "Taak niet gevonden of toegang geweigerd." }, "warnings": { "no_terminal_content": "Geen terminalinhoud geselecteerd", - "missing_task_files": "De bestanden van deze taak ontbreken. Wil je deze uit de takenlijst verwijderen?", - "auto_import_failed": "Automatisch importeren van RooCode-instellingen mislukt: {{error}}" + "missing_task_files": "De bestanden van deze taak ontbreken. Wil je deze uit de takenlijst verwijderen?" }, "info": { "no_changes": "Geen wijzigingen gevonden.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Aangepast opslagpad ingesteld: {{path}}", "default_storage_path": "Terug naar standaard opslagpad", "settings_imported": "Instellingen succesvol geïmporteerd.", - "auto_import_success": "RooCode-instellingen automatisch geïmporteerd van {{filename}}", "share_link_copied": "Deellink gekopieerd naar klembord", "image_copied_to_clipboard": "Afbeelding data-URI gekopieerd naar klembord", "image_saved": "Afbeelding opgeslagen naar {{path}}", "organization_share_link_copied": "Organisatie deel-link gekopieerd naar klembord!", - "public_share_link_copied": "Openbare deel-link gekopieerd naar klembord!", - "mode_exported": "Modus '{{mode}}' succesvol geëxporteerd", - "mode_imported": "Modus succesvol geïmporteerd" + "public_share_link_copied": "Openbare deel-link gekopieerd naar klembord!" }, "answers": { "yes": "Ja", "no": "Nee", + "cancel": "Annuleren", "remove": "Verwijderen", "keep": "Behouden" }, - "buttons": { - "save": "Opslaan", - "edit": "Bewerken", - "learn_more": "Meer informatie" - }, "tasks": { "canceled": "Taakfout: gestopt en geannuleerd door gebruiker.", - "deleted": "Taakfout: gestopt en verwijderd door gebruiker.", - "incomplete": "Taak #{{taskNumber}} (Onvolledig)", - "no_messages": "Taak #{{taskNumber}} (Geen berichten)" + "deleted": "Taakfout: gestopt en verwijderd door gebruiker." }, "storage": { "prompt_custom_path": "Voer een aangepast opslagpad voor gespreksgeschiedenis in, laat leeg voor standaardlocatie", @@ -127,57 +102,11 @@ "task_prompt": "Wat moet Roo doen?", "task_placeholder": "Typ hier je taak" }, - "settings": { - "providers": { - "groqApiKey": "Groq API-sleutel", - "getGroqApiKey": "Groq API-sleutel ophalen", - "claudeCode": { - "pathLabel": "Claude Code Pad", - "description": "Optioneel pad naar je Claude Code CLI. Standaard 'claude' indien niet ingesteld.", - "placeholder": "Standaard: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "Ongeldige YAML in .roomodes bestand op regel {{line}}. Controleer:\n• Juiste inspringing (gebruik spaties, geen tabs)\n• Overeenkomende aanhalingstekens en haakjes\n• Geldige YAML syntaxis", - "schemaValidationError": "Ongeldig aangepaste modi formaat in .roomodes:\n{{issues}}", - "invalidFormat": "Ongeldig aangepaste modi formaat. Zorg ervoor dat je instellingen het juiste YAML formaat volgen.", - "updateFailed": "Aangepaste modus bijwerken mislukt: {{error}}", - "deleteFailed": "Aangepaste modus verwijderen mislukt: {{error}}", - "resetFailed": "Aangepaste modi resetten mislukt: {{error}}", - "modeNotFound": "Schrijffout: Modus niet gevonden", - "noWorkspaceForProject": "Geen workspace map gevonden voor projectspecifieke modus", - "rulesCleanupFailed": "Modus succesvol verwijderd, maar het verwijderen van de regelsmap op {{rulesFolderPath}} is mislukt. Je moet deze mogelijk handmatig verwijderen." - }, - "scope": { - "project": "project", - "global": "globaal" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Modus succesvol verwijderd, maar het verwijderen van de regelsmap op {{rulesFolderPath}} is mislukt. Je moet deze mogelijk handmatig verwijderen." - } - }, "mdm": { "errors": { "cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.", "organization_mismatch": "Je moet geauthenticeerd zijn met het Roo Code Cloud-account van je organisatie.", "verification_failed": "Kan organisatie-authenticatie niet verifiëren." } - }, - "prompts": { - "deleteMode": { - "title": "Aangepaste modus verwijderen", - "description": "Weet je zeker dat je deze {{scope}}-modus wilt verwijderen? Dit zal ook de bijbehorende regelsmap op {{rulesFolderPath}} verwijderen", - "descriptionNoRules": "Weet je zeker dat je deze aangepaste modus wilt verwijderen?", - "confirm": "Verwijderen" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Voorkom taakafronding wanneer er onvolledige todos in de todolijst staan" - } } } diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index e24960af89..87293db303 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Czy na pewno chcesz zresetować wszystkie stany i tajne magazyny w rozszerzeniu? Tej operacji nie można cofnąć.", "delete_config_profile": "Czy na pewno chcesz usunąć ten profil konfiguracyjny?", - "delete_custom_mode_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}" + "delete_custom_mode": "Czy na pewno chcesz usunąć ten niestandardowy tryb?", + "delete_message": "Co chcesz usunąć?", + "just_this_message": "Tylko tę wiadomość", + "this_and_subsequent": "Tę i wszystkie kolejne wiadomości" }, "errors": { "invalid_data_uri": "Nieprawidłowy format URI danych", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Nie można otworzyć pliku!", "checkpoint_timeout": "Upłynął limit czasu podczas próby przywrócenia punktu kontrolnego.", "checkpoint_failed": "Nie udało się przywrócić punktu kontrolnego.", - "git_not_installed": "Funkcja punktów kontrolnych wymaga oprogramowania Git. Zainstaluj Git, aby włączyć punkty kontrolne.", "no_workspace": "Najpierw otwórz folder projektu", "update_support_prompt": "Nie udało się zaktualizować komunikatu wsparcia", "reset_support_prompt": "Nie udało się zresetować komunikatu wsparcia", @@ -50,39 +52,21 @@ "cannot_access_path": "Nie można uzyskać dostępu do ścieżki {{path}}: {{error}}", "settings_import_failed": "Nie udało się zaimportować ustawień: {{error}}.", "mistake_limit_guidance": "To może wskazywać na błąd w procesie myślowym modelu lub niezdolność do prawidłowego użycia narzędzia, co można złagodzić poprzez wskazówki użytkownika (np. \"Spróbuj podzielić zadanie na mniejsze kroki\").", - "violated_organization_allowlist": "Nie udało się uruchomić zadania: bieżący profil nie jest kompatybilny z ustawieniami Twojej organizacji", + "violated_organization_allowlist": "Nie udało się uruchomić zadania: bieżący profil narusza ustawienia Twojej organizacji", "condense_failed": "Nie udało się skondensować kontekstu", "condense_not_enough_messages": "Za mało wiadomości do skondensowania kontekstu", "condensed_recently": "Kontekst został niedawno skondensowany; pomijanie tej próby", "condense_handler_invalid": "Nieprawidłowy handler API do kondensowania kontekstu", "condense_context_grew": "Rozmiar kontekstu wzrósł podczas kondensacji; pomijanie tej próby", - "url_timeout": "Strona internetowa ładowała się zbyt długo (timeout). Może to być spowodowane wolnym połączeniem, ciężką stroną lub tymczasową niedostępnością. Możesz spróbować ponownie później lub sprawdzić, czy URL jest poprawny.", - "url_not_found": "Nie można znaleźć adresu strony internetowej. Sprawdź, czy URL jest poprawny i spróbuj ponownie.", - "no_internet": "Brak połączenia z internetem. Sprawdź połączenie sieciowe i spróbuj ponownie.", - "url_forbidden": "Dostęp do tej strony internetowej jest zabroniony. Strona może blokować automatyczny dostęp lub wymagać uwierzytelnienia.", - "url_page_not_found": "Strona nie została znaleziona. Sprawdź, czy URL jest poprawny.", - "url_fetch_failed": "Błąd pobierania zawartości URL: {{error}}", - "url_fetch_error_with_url": "Błąd pobierania zawartości dla {{url}}: {{error}}", - "command_timeout": "Przekroczono limit czasu wykonania polecenia po {{seconds}} sekundach", "share_task_failed": "Nie udało się udostępnić zadania", "share_no_active_task": "Brak aktywnego zadania do udostępnienia", "share_auth_required": "Wymagana autoryzacja. Zaloguj się, aby udostępniać zadania.", "share_not_enabled": "Udostępnianie zadań nie jest włączone dla tej organizacji.", - "share_task_not_found": "Zadanie nie znalezione lub dostęp odmówiony.", - "mode_import_failed": "Import trybu nie powiódł się: {{error}}", - "delete_rules_folder_failed": "Nie udało się usunąć folderu reguł: {{rulesFolderPath}}. Błąd: {{error}}", - "claudeCode": { - "processExited": "Proces Claude Code zakończył się kodem {{exitCode}}.", - "errorOutput": "Wyjście błędu: {{output}}", - "processExitedWithError": "Proces Claude Code zakończył się kodem {{exitCode}}. Wyjście błędu: {{output}}", - "stoppedWithReason": "Claude Code zatrzymał się z powodu: {{reason}}", - "apiKeyModelPlanMismatch": "Klucze API i plany subskrypcji pozwalają na różne modele. Upewnij się, że wybrany model jest zawarty w twoim planie." - } + "share_task_not_found": "Zadanie nie znalezione lub dostęp odmówiony." }, "warnings": { "no_terminal_content": "Nie wybrano zawartości terminala", - "missing_task_files": "Pliki tego zadania są brakujące. Czy chcesz usunąć je z listy zadań?", - "auto_import_failed": "Nie udało się automatycznie zaimportować ustawień RooCode: {{error}}" + "missing_task_files": "Pliki tego zadania są brakujące. Czy chcesz usunąć je z listy zadań?" }, "info": { "no_changes": "Nie znaleziono zmian.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Ustawiono niestandardową ścieżkę przechowywania: {{path}}", "default_storage_path": "Wznowiono używanie domyślnej ścieżki przechowywania", "settings_imported": "Ustawienia zaimportowane pomyślnie.", - "auto_import_success": "Ustawienia RooCode zostały automatycznie zaimportowane z {{filename}}", "share_link_copied": "Link udostępniania skopiowany do schowka", "image_copied_to_clipboard": "URI danych obrazu skopiowane do schowka", "image_saved": "Obraz zapisany w {{path}}", "organization_share_link_copied": "Link udostępniania organizacji skopiowany do schowka!", - "public_share_link_copied": "Publiczny link udostępniania skopiowany do schowka!", - "mode_exported": "Tryb '{{mode}}' pomyślnie wyeksportowany", - "mode_imported": "Tryb pomyślnie zaimportowany" + "public_share_link_copied": "Publiczny link udostępniania skopiowany do schowka!" }, "answers": { "yes": "Tak", "no": "Nie", + "cancel": "Anuluj", "remove": "Usuń", "keep": "Zachowaj" }, - "buttons": { - "save": "Zapisz", - "edit": "Edytuj", - "learn_more": "Dowiedz się więcej" - }, "tasks": { "canceled": "Błąd zadania: Zostało zatrzymane i anulowane przez użytkownika.", - "deleted": "Niepowodzenie zadania: Zostało zatrzymane i usunięte przez użytkownika.", - "incomplete": "Zadanie #{{taskNumber}} (Niekompletne)", - "no_messages": "Zadanie #{{taskNumber}} (Brak wiadomości)" + "deleted": "Niepowodzenie zadania: Zostało zatrzymane i usunięte przez użytkownika." }, "storage": { "prompt_custom_path": "Wprowadź niestandardową ścieżkę przechowywania dla historii konwersacji lub pozostaw puste, aby użyć lokalizacji domyślnej", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Klucz API Groq", - "getGroqApiKey": "Uzyskaj klucz API Groq", - "claudeCode": { - "pathLabel": "Ścieżka Claude Code", - "description": "Opcjonalna ścieżka do Twojego CLI Claude Code. Domyślnie 'claude', jeśli nie ustawiono.", - "placeholder": "Domyślnie: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "Nieprawidłowy YAML w pliku .roomodes w linii {{line}}. Sprawdź:\n• Prawidłowe wcięcia (używaj spacji, nie tabulatorów)\n• Pasujące cudzysłowy i nawiasy\n• Prawidłową składnię YAML", - "schemaValidationError": "Nieprawidłowy format trybów niestandardowych w .roomodes:\n{{issues}}", - "invalidFormat": "Nieprawidłowy format trybów niestandardowych. Upewnij się, że twoje ustawienia są zgodne z prawidłowym formatem YAML.", - "updateFailed": "Aktualizacja trybu niestandardowego nie powiodła się: {{error}}", - "deleteFailed": "Usunięcie trybu niestandardowego nie powiodło się: {{error}}", - "resetFailed": "Resetowanie trybów niestandardowych nie powiodło się: {{error}}", - "modeNotFound": "Błąd zapisu: Tryb nie został znaleziony", - "noWorkspaceForProject": "Nie znaleziono folderu obszaru roboczego dla trybu specyficznego dla projektu", - "rulesCleanupFailed": "Tryb został pomyślnie usunięty, ale nie udało się usunąć folderu reguł w {{rulesFolderPath}}. Może być konieczne ręczne usunięcie." - }, - "scope": { - "project": "projekt", - "global": "globalny" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Tryb został pomyślnie usunięty, ale nie udało się usunąć folderu reguł w {{rulesFolderPath}}. Może być konieczne ręczne usunięcie." + "getGroqApiKey": "Uzyskaj klucz API Groq" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Musisz być uwierzytelniony kontem Roo Code Cloud swojej organizacji.", "verification_failed": "Nie można zweryfikować uwierzytelnienia organizacji." } - }, - "prompts": { - "deleteMode": { - "title": "Usuń tryb niestandardowy", - "description": "Czy na pewno chcesz usunąć ten tryb {{scope}}? Spowoduje to również usunięcie powiązanego folderu z regułami w {{rulesFolderPath}}", - "descriptionNoRules": "Czy na pewno chcesz usunąć ten tryb niestandardowy?", - "confirm": "Usuń" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Zapobiegaj ukończeniu zadania gdy na liście zadań są nieukończone zadania" - } } } diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 6007beb41a..e2847d590d 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -21,7 +21,10 @@ "confirmation": { "reset_state": "Tem certeza de que deseja redefinir todo o estado e armazenamento secreto na extensão? Isso não pode ser desfeito.", "delete_config_profile": "Tem certeza de que deseja excluir este perfil de configuração?", - "delete_custom_mode_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}" + "delete_custom_mode": "Tem certeza de que deseja excluir este modo personalizado?", + "delete_message": "O que você gostaria de excluir?", + "just_this_message": "Apenas esta mensagem", + "this_and_subsequent": "Esta e todas as mensagens subsequentes" }, "errors": { "invalid_data_uri": "Formato de URI de dados inválido", @@ -32,7 +35,6 @@ "could_not_open_file_generic": "Não foi possível abrir o arquivo!", "checkpoint_timeout": "Tempo esgotado ao tentar restaurar o ponto de verificação.", "checkpoint_failed": "Falha ao restaurar o ponto de verificação.", - "git_not_installed": "O Git é necessário para o recurso de checkpoints. Por favor, instale o Git para habilitar os checkpoints.", "no_workspace": "Por favor, abra primeiro uma pasta de projeto", "update_support_prompt": "Falha ao atualizar o prompt de suporte", "reset_support_prompt": "Falha ao redefinir o prompt de suporte", @@ -54,39 +56,21 @@ "cannot_access_path": "Não é possível acessar o caminho {{path}}: {{error}}", "settings_import_failed": "Falha ao importar configurações: {{error}}", "mistake_limit_guidance": "Isso pode indicar uma falha no processo de pensamento do modelo ou incapacidade de usar uma ferramenta adequadamente, o que pode ser mitigado com orientação do usuário (ex. \"Tente dividir a tarefa em etapas menores\").", - "violated_organization_allowlist": "Falha ao executar a tarefa: o perfil atual não é compatível com as configurações da sua organização", + "violated_organization_allowlist": "Falha ao executar a tarefa: o perfil atual viola as configurações da sua organização", "condense_failed": "Falha ao condensar o contexto", "condense_not_enough_messages": "Não há mensagens suficientes para condensar o contexto", "condensed_recently": "O contexto foi condensado recentemente; pulando esta tentativa", "condense_handler_invalid": "O manipulador de API para condensar o contexto é inválido", "condense_context_grew": "O tamanho do contexto aumentou durante a condensação; pulando esta tentativa", - "url_timeout": "O site demorou muito para carregar (timeout). Isso pode ser devido a uma conexão lenta, site pesado ou temporariamente indisponível. Você pode tentar novamente mais tarde ou verificar se a URL está correta.", - "url_not_found": "O endereço do site não pôde ser encontrado. Verifique se a URL está correta e tente novamente.", - "no_internet": "Sem conexão com a internet. Verifique sua conexão de rede e tente novamente.", - "url_forbidden": "O acesso a este site está proibido. O site pode bloquear acesso automatizado ou exigir autenticação.", - "url_page_not_found": "A página não foi encontrada. Verifique se a URL está correta.", - "url_fetch_failed": "Falha ao buscar conteúdo da URL: {{error}}", - "url_fetch_error_with_url": "Erro ao buscar conteúdo para {{url}}: {{error}}", - "command_timeout": "A execução do comando excedeu o tempo limite após {{seconds}} segundos", "share_task_failed": "Falha ao compartilhar tarefa", "share_no_active_task": "Nenhuma tarefa ativa para compartilhar", "share_auth_required": "Autenticação necessária. Faça login para compartilhar tarefas.", "share_not_enabled": "O compartilhamento de tarefas não está habilitado para esta organização.", - "share_task_not_found": "Tarefa não encontrada ou acesso negado.", - "mode_import_failed": "Falha ao importar o modo: {{error}}", - "delete_rules_folder_failed": "Falha ao excluir pasta de regras: {{rulesFolderPath}}. Erro: {{error}}", - "claudeCode": { - "processExited": "O processo Claude Code saiu com código {{exitCode}}.", - "errorOutput": "Saída de erro: {{output}}", - "processExitedWithError": "O processo Claude Code saiu com código {{exitCode}}. Saída de erro: {{output}}", - "stoppedWithReason": "Claude Code parou pela razão: {{reason}}", - "apiKeyModelPlanMismatch": "Chaves de API e planos de assinatura permitem modelos diferentes. Certifique-se de que o modelo selecionado esteja incluído no seu plano." - } + "share_task_not_found": "Tarefa não encontrada ou acesso negado." }, "warnings": { "no_terminal_content": "Nenhum conteúdo do terminal selecionado", - "missing_task_files": "Os arquivos desta tarefa estão faltando. Deseja removê-la da lista de tarefas?", - "auto_import_failed": "Falha ao importar automaticamente as configurações do RooCode: {{error}}" + "missing_task_files": "Os arquivos desta tarefa estão faltando. Deseja removê-la da lista de tarefas?" }, "info": { "no_changes": "Nenhuma alteração encontrada.", @@ -95,31 +79,22 @@ "custom_storage_path_set": "Caminho de armazenamento personalizado definido: {{path}}", "default_storage_path": "Retornado ao caminho de armazenamento padrão", "settings_imported": "Configurações importadas com sucesso.", - "auto_import_success": "Configurações do RooCode importadas automaticamente de {{filename}}", "share_link_copied": "Link de compartilhamento copiado para a área de transferência", "image_copied_to_clipboard": "URI de dados da imagem copiada para a área de transferência", "image_saved": "Imagem salva em {{path}}", "organization_share_link_copied": "Link de compartilhamento da organização copiado para a área de transferência!", - "public_share_link_copied": "Link de compartilhamento público copiado para a área de transferência!", - "mode_exported": "Modo '{{mode}}' exportado com sucesso", - "mode_imported": "Modo importado com sucesso" + "public_share_link_copied": "Link de compartilhamento público copiado para a área de transferência!" }, "answers": { "yes": "Sim", "no": "Não", + "cancel": "Cancelar", "remove": "Remover", "keep": "Manter" }, - "buttons": { - "save": "Salvar", - "edit": "Editar", - "learn_more": "Saiba Mais" - }, "tasks": { "canceled": "Erro na tarefa: Foi interrompida e cancelada pelo usuário.", - "deleted": "Falha na tarefa: Foi interrompida e excluída pelo usuário.", - "incomplete": "Tarefa #{{taskNumber}} (Incompleta)", - "no_messages": "Tarefa #{{taskNumber}} (Sem mensagens)" + "deleted": "Falha na tarefa: Foi interrompida e excluída pelo usuário." }, "storage": { "prompt_custom_path": "Digite o caminho de armazenamento personalizado para o histórico de conversas, deixe em branco para usar o local padrão", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Chave de API Groq", - "getGroqApiKey": "Obter chave de API Groq", - "claudeCode": { - "pathLabel": "Caminho do Claude Code", - "description": "Caminho opcional para sua CLI do Claude Code. Padrão 'claude' se não for definido.", - "placeholder": "Padrão: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "YAML inválido no arquivo .roomodes na linha {{line}}. Verifique:\n• Indentação correta (use espaços, não tabs)\n• Aspas e colchetes correspondentes\n• Sintaxe YAML válida", - "schemaValidationError": "Formato de modos personalizados inválido em .roomodes:\n{{issues}}", - "invalidFormat": "Formato de modos personalizados inválido. Certifique-se de que suas configurações seguem o formato YAML correto.", - "updateFailed": "Falha ao atualizar modo personalizado: {{error}}", - "deleteFailed": "Falha ao excluir modo personalizado: {{error}}", - "resetFailed": "Falha ao redefinir modos personalizados: {{error}}", - "modeNotFound": "Erro de escrita: Modo não encontrado", - "noWorkspaceForProject": "Nenhuma pasta de workspace encontrada para modo específico do projeto", - "rulesCleanupFailed": "O modo foi excluído com sucesso, mas falhou ao excluir a pasta de regras em {{rulesFolderPath}}. Você pode precisar excluí-la manualmente." - }, - "scope": { - "project": "projeto", - "global": "global" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "O modo foi removido com sucesso, mas falhou ao excluir a pasta de regras em {{rulesFolderPath}}. Você pode precisar excluí-la manualmente." + "getGroqApiKey": "Obter chave de API Groq" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Você deve estar autenticado com a conta Roo Code Cloud da sua organização.", "verification_failed": "Não foi possível verificar a autenticação da organização." } - }, - "prompts": { - "deleteMode": { - "title": "Excluir Modo Personalizado", - "description": "Tem certeza de que deseja excluir este modo {{scope}}? Isso também excluirá a pasta de regras associada em: {{rulesFolderPath}}", - "descriptionNoRules": "Tem certeza de que deseja excluir este modo personalizado?", - "confirm": "Excluir" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Impedir a conclusão de tarefas quando há todos incompletos na lista de todos" - } } } diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 4d3daaf743..9d5f29a44a 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Вы уверены, что хотите сбросить все состояние и секретное хранилище в расширении? Это действие нельзя отменить.", "delete_config_profile": "Вы уверены, что хотите удалить этот профиль конфигурации?", - "delete_custom_mode_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}" + "delete_custom_mode": "Вы уверены, что хотите удалить этот пользовательский режим?", + "delete_message": "Что вы хотите удалить?", + "just_this_message": "Только это сообщение", + "this_and_subsequent": "Это и все последующие сообщения" }, "errors": { "invalid_data_uri": "Неверный формат URI данных", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Не удалось открыть файл!", "checkpoint_timeout": "Превышено время ожидания при попытке восстановления контрольной точки.", "checkpoint_failed": "Не удалось восстановить контрольную точку.", - "git_not_installed": "Для функции контрольных точек требуется Git. Пожалуйста, установите Git, чтобы включить контрольные точки.", "no_workspace": "Пожалуйста, сначала откройте папку проекта", "update_support_prompt": "Не удалось обновить промпт поддержки", "reset_support_prompt": "Не удалось сбросить промпт поддержки", @@ -50,39 +52,21 @@ "cannot_access_path": "Невозможно получить доступ к пути {{path}}: {{error}}", "settings_import_failed": "Не удалось импортировать настройки: {{error}}.", "mistake_limit_guidance": "Это может указывать на сбой в процессе мышления модели или неспособность правильно использовать инструмент, что можно смягчить с помощью руководства пользователя (например, \"Попробуйте разбить задачу на более мелкие шаги\").", - "violated_organization_allowlist": "Не удалось выполнить задачу: текущий профиль несовместим с настройками вашей организации", + "violated_organization_allowlist": "Не удалось выполнить задачу: текущий профиль нарушает настройки вашей организации", "condense_failed": "Не удалось сжать контекст", "condense_not_enough_messages": "Недостаточно сообщений для сжатия контекста", "condensed_recently": "Контекст был недавно сжат; пропускаем эту попытку", "condense_handler_invalid": "Обработчик API для сжатия контекста недействителен", "condense_context_grew": "Размер контекста увеличился во время сжатия; пропускаем эту попытку", - "url_timeout": "Веб-сайт слишком долго загружался (таймаут). Это может быть из-за медленного соединения, тяжелого веб-сайта или временной недоступности. Ты можешь попробовать позже или проверить правильность URL.", - "url_not_found": "Адрес веб-сайта не найден. Проверь правильность URL и попробуй снова.", - "no_internet": "Нет подключения к интернету. Проверь сетевое подключение и попробуй снова.", - "url_forbidden": "Доступ к этому веб-сайту запрещен. Сайт может блокировать автоматический доступ или требовать аутентификацию.", - "url_page_not_found": "Страница не найдена. Проверь правильность URL.", - "url_fetch_failed": "Ошибка получения содержимого URL: {{error}}", - "url_fetch_error_with_url": "Ошибка получения содержимого для {{url}}: {{error}}", - "command_timeout": "Время выполнения команды истекло через {{seconds}} секунд", "share_task_failed": "Не удалось поделиться задачей", "share_no_active_task": "Нет активной задачи для совместного использования", "share_auth_required": "Требуется аутентификация. Войдите в систему для совместного доступа к задачам.", "share_not_enabled": "Совместный доступ к задачам не включен для этой организации.", - "share_task_not_found": "Задача не найдена или доступ запрещен.", - "mode_import_failed": "Не удалось импортировать режим: {{error}}", - "delete_rules_folder_failed": "Не удалось удалить папку правил: {{rulesFolderPath}}. Ошибка: {{error}}", - "claudeCode": { - "processExited": "Процесс Claude Code завершился с кодом {{exitCode}}.", - "errorOutput": "Вывод ошибки: {{output}}", - "processExitedWithError": "Процесс Claude Code завершился с кодом {{exitCode}}. Вывод ошибки: {{output}}", - "stoppedWithReason": "Claude Code остановился по причине: {{reason}}", - "apiKeyModelPlanMismatch": "API-ключи и планы подписки позволяют использовать разные модели. Убедитесь, что выбранная модель включена в ваш план." - } + "share_task_not_found": "Задача не найдена или доступ запрещен." }, "warnings": { "no_terminal_content": "Не выбрано содержимое терминала", - "missing_task_files": "Файлы этой задачи отсутствуют. Хотите удалить её из списка задач?", - "auto_import_failed": "Не удалось автоматически импортировать настройки RooCode: {{error}}" + "missing_task_files": "Файлы этой задачи отсутствуют. Хотите удалить её из списка задач?" }, "info": { "no_changes": "Изменения не найдены.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Установлен пользовательский путь хранения: {{path}}", "default_storage_path": "Возвращено использование пути хранения по умолчанию", "settings_imported": "Настройки успешно импортированы.", - "auto_import_success": "Настройки RooCode автоматически импортированы из {{filename}}", "share_link_copied": "Ссылка для совместного использования скопирована в буфер обмена", "image_copied_to_clipboard": "URI данных изображения скопирован в буфер обмена", "image_saved": "Изображение сохранено в {{path}}", "organization_share_link_copied": "Ссылка для совместного доступа организации скопирована в буфер обмена!", - "public_share_link_copied": "Публичная ссылка для совместного доступа скопирована в буфер обмена!", - "mode_exported": "Режим '{{mode}}' успешно экспортирован", - "mode_imported": "Режим успешно импортирован" + "public_share_link_copied": "Публичная ссылка для совместного доступа скопирована в буфер обмена!" }, "answers": { "yes": "Да", "no": "Нет", + "cancel": "Отмена", "remove": "Удалить", "keep": "Оставить" }, - "buttons": { - "save": "Сохранить", - "edit": "Редактировать", - "learn_more": "Узнать больше" - }, "tasks": { "canceled": "Ошибка задачи: Она была остановлена и отменена пользователем.", - "deleted": "Сбой задачи: Она была остановлена и удалена пользователем.", - "incomplete": "Задача #{{taskNumber}} (Незавершенная)", - "no_messages": "Задача #{{taskNumber}} (Нет сообщений)" + "deleted": "Сбой задачи: Она была остановлена и удалена пользователем." }, "storage": { "prompt_custom_path": "Введите пользовательский путь хранения истории разговоров, оставьте пустым для использования расположения по умолчанию", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Ключ API Groq", - "getGroqApiKey": "Получить ключ API Groq", - "claudeCode": { - "pathLabel": "Путь к Claude Code", - "description": "Необязательный путь к вашему CLI Claude Code. По умолчанию 'claude', если не установлено.", - "placeholder": "По умолчанию: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "Недопустимый YAML в файле .roomodes на строке {{line}}. Проверь:\n• Правильные отступы (используй пробелы, не табы)\n• Соответствующие кавычки и скобки\n• Допустимый синтаксис YAML", - "schemaValidationError": "Недопустимый формат пользовательских режимов в .roomodes:\n{{issues}}", - "invalidFormat": "Недопустимый формат пользовательских режимов. Убедись, что твои настройки соответствуют правильному формату YAML.", - "updateFailed": "Не удалось обновить пользовательский режим: {{error}}", - "deleteFailed": "Не удалось удалить пользовательский режим: {{error}}", - "resetFailed": "Не удалось сбросить пользовательские режимы: {{error}}", - "modeNotFound": "Ошибка записи: Режим не найден", - "noWorkspaceForProject": "Не найдена папка рабочего пространства для режима, специфичного для проекта", - "rulesCleanupFailed": "Режим успешно удален, но не удалось удалить папку правил в {{rulesFolderPath}}. Возможно, вам придется удалить ее вручную." - }, - "scope": { - "project": "проект", - "global": "глобальный" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Режим успешно удален, но не удалось удалить папку правил в {{rulesFolderPath}}. Возможно, вам придется удалить ее вручную." + "getGroqApiKey": "Получить ключ API Groq" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Вы должны быть аутентифицированы с учетной записью Roo Code Cloud вашей организации.", "verification_failed": "Не удается проверить аутентификацию организации." } - }, - "prompts": { - "deleteMode": { - "title": "Удалить пользовательский режим", - "description": "Вы уверены, что хотите удалить этот режим {{scope}}? Это также удалит связанную папку правил по адресу: {{rulesFolderPath}}", - "descriptionNoRules": "Вы уверены, что хотите удалить этот пользовательский режим?", - "confirm": "Удалить" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Предотвратить завершение задач при наличии незавершенных дел в списке дел" - } } } diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index e2dfca734b..893117222a 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Uzantıdaki tüm durumları ve gizli depolamayı sıfırlamak istediğinizden emin misiniz? Bu işlem geri alınamaz.", "delete_config_profile": "Bu yapılandırma profilini silmek istediğinizden emin misiniz?", - "delete_custom_mode_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}" + "delete_custom_mode": "Bu özel modu silmek istediğinizden emin misiniz?", + "delete_message": "Neyi silmek istersiniz?", + "just_this_message": "Sadece bu mesajı", + "this_and_subsequent": "Bu ve sonraki tüm mesajları" }, "errors": { "invalid_data_uri": "Geçersiz veri URI formatı", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Dosya açılamadı!", "checkpoint_timeout": "Kontrol noktasını geri yüklemeye çalışırken zaman aşımına uğradı.", "checkpoint_failed": "Kontrol noktası geri yüklenemedi.", - "git_not_installed": "Kontrol noktaları özelliği için Git gereklidir. Kontrol noktalarını etkinleştirmek için lütfen Git'i yükleyin.", "no_workspace": "Lütfen önce bir proje klasörü açın", "update_support_prompt": "Destek istemi güncellenemedi", "reset_support_prompt": "Destek istemi sıfırlanamadı", @@ -50,39 +52,21 @@ "cannot_access_path": "{{path}} yoluna erişilemiyor: {{error}}", "settings_import_failed": "Ayarlar içe aktarılamadı: {{error}}.", "mistake_limit_guidance": "Bu, modelin düşünce sürecindeki bir başarısızlığı veya bir aracı düzgün kullanamama durumunu gösterebilir, bu da kullanıcı rehberliği ile hafifletilebilir (örn. \"Görevi daha küçük adımlara bölmeyi deneyin\").", - "violated_organization_allowlist": "Görev yürütülemedi: Geçerli profil kuruluşunuzun ayarlarıyla uyumlu değil", + "violated_organization_allowlist": "Görev yürütülemedi: Geçerli profil kuruluşunuzun ayarlarını ihlal ediyor", "condense_failed": "Bağlam sıkıştırılamadı", "condense_not_enough_messages": "Bağlamı sıkıştırmak için yeterli mesaj yok", "condensed_recently": "Bağlam yakın zamanda sıkıştırıldı; bu deneme atlanıyor", "condense_handler_invalid": "Bağlamı sıkıştırmak için API işleyicisi geçersiz", "condense_context_grew": "Sıkıştırma sırasında bağlam boyutu arttı; bu deneme atlanıyor", - "url_timeout": "Web sitesi yüklenmesi çok uzun sürdü (zaman aşımı). Bu yavaş bağlantı, ağır web sitesi veya geçici olarak kullanılamama nedeniyle olabilir. Daha sonra tekrar deneyebilir veya URL'nin doğru olup olmadığını kontrol edebilirsin.", - "url_not_found": "Web sitesi adresi bulunamadı. URL'nin doğru olup olmadığını kontrol et ve tekrar dene.", - "no_internet": "İnternet bağlantısı yok. Ağ bağlantını kontrol et ve tekrar dene.", - "url_forbidden": "Bu web sitesine erişim yasak. Site otomatik erişimi engelliyor veya kimlik doğrulama gerektiriyor olabilir.", - "url_page_not_found": "Sayfa bulunamadı. URL'nin doğru olup olmadığını kontrol et.", - "url_fetch_failed": "URL içeriği getirme hatası: {{error}}", - "url_fetch_error_with_url": "{{url}} için içerik getirme hatası: {{error}}", - "command_timeout": "Komut çalıştırma {{seconds}} saniye sonra zaman aşımına uğradı", "share_task_failed": "Görev paylaşılamadı", "share_no_active_task": "Paylaşılacak aktif görev yok", "share_auth_required": "Kimlik doğrulama gerekli. Görevleri paylaşmak için lütfen giriş yapın.", "share_not_enabled": "Bu kuruluş için görev paylaşımı etkinleştirilmemiş.", - "share_task_not_found": "Görev bulunamadı veya erişim reddedildi.", - "mode_import_failed": "Mod içe aktarılamadı: {{error}}", - "delete_rules_folder_failed": "Kurallar klasörü silinemedi: {{rulesFolderPath}}. Hata: {{error}}", - "claudeCode": { - "processExited": "Claude Code işlemi {{exitCode}} koduyla çıktı.", - "errorOutput": "Hata çıktısı: {{output}}", - "processExitedWithError": "Claude Code işlemi {{exitCode}} koduyla çıktı. Hata çıktısı: {{output}}", - "stoppedWithReason": "Claude Code şu nedenle durdu: {{reason}}", - "apiKeyModelPlanMismatch": "API anahtarları ve abonelik planları farklı modellere izin verir. Seçilen modelin planınıza dahil olduğundan emin olun." - } + "share_task_not_found": "Görev bulunamadı veya erişim reddedildi." }, "warnings": { "no_terminal_content": "Seçili terminal içeriği yok", - "missing_task_files": "Bu görevin dosyaları eksik. Görev listesinden kaldırmak istiyor musunuz?", - "auto_import_failed": "RooCode ayarları otomatik olarak içe aktarılamadı: {{error}}" + "missing_task_files": "Bu görevin dosyaları eksik. Görev listesinden kaldırmak istiyor musunuz?" }, "info": { "no_changes": "Değişiklik bulunamadı.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Özel depolama yolu ayarlandı: {{path}}", "default_storage_path": "Varsayılan depolama yoluna geri dönüldü", "settings_imported": "Ayarlar başarıyla içe aktarıldı.", - "auto_import_success": "RooCode ayarları {{filename}} dosyasından otomatik olarak içe aktarıldı", "share_link_copied": "Paylaşım bağlantısı panoya kopyalandı", "image_copied_to_clipboard": "Resim veri URI'si panoya kopyalandı", "image_saved": "Resim {{path}} konumuna kaydedildi", "organization_share_link_copied": "Kuruluş paylaşım bağlantısı panoya kopyalandı!", - "public_share_link_copied": "Herkese açık paylaşım bağlantısı panoya kopyalandı!", - "mode_exported": "'{{mode}}' modu başarıyla dışa aktarıldı", - "mode_imported": "Mod başarıyla içe aktarıldı" + "public_share_link_copied": "Herkese açık paylaşım bağlantısı panoya kopyalandı!" }, "answers": { "yes": "Evet", "no": "Hayır", + "cancel": "İptal", "remove": "Kaldır", "keep": "Koru" }, - "buttons": { - "save": "Kaydet", - "edit": "Düzenle", - "learn_more": "Daha Fazla Bilgi" - }, "tasks": { "canceled": "Görev hatası: Kullanıcı tarafından durduruldu ve iptal edildi.", - "deleted": "Görev başarısız: Kullanıcı tarafından durduruldu ve silindi.", - "incomplete": "Görev #{{taskNumber}} (Tamamlanmamış)", - "no_messages": "Görev #{{taskNumber}} (Mesaj yok)" + "deleted": "Görev başarısız: Kullanıcı tarafından durduruldu ve silindi." }, "storage": { "prompt_custom_path": "Konuşma geçmişi için özel depolama yolunu girin, varsayılan konumu kullanmak için boş bırakın", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Groq API Anahtarı", - "getGroqApiKey": "Groq API Anahtarı Al", - "claudeCode": { - "pathLabel": "Claude Code Yolu", - "description": "Claude Code CLI'nizin isteğe bağlı yolu. Ayarlanmazsa varsayılan olarak 'claude' olur.", - "placeholder": "Varsayılan: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": ".roomodes dosyasının {{line}}. satırında geçersiz YAML. Kontrol et:\n• Doğru girinti (tab değil boşluk kullan)\n• Eşleşen tırnak işaretleri ve parantezler\n• Geçerli YAML sözdizimi", - "schemaValidationError": ".roomodes'ta geçersiz özel mod formatı:\n{{issues}}", - "invalidFormat": "Geçersiz özel mod formatı. Ayarlarının doğru YAML formatını takip ettiğinden emin ol.", - "updateFailed": "Özel mod güncellemesi başarısız: {{error}}", - "deleteFailed": "Özel mod silme başarısız: {{error}}", - "resetFailed": "Özel modları sıfırlama başarısız: {{error}}", - "modeNotFound": "Yazma hatası: Mod bulunamadı", - "noWorkspaceForProject": "Proje özel modu için çalışma alanı klasörü bulunamadı", - "rulesCleanupFailed": "Mod başarıyla silindi, ancak {{rulesFolderPath}} konumundaki kurallar klasörü silinemedi. Manuel olarak silmeniz gerekebilir." - }, - "scope": { - "project": "proje", - "global": "küresel" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Mod başarıyla kaldırıldı, ancak {{rulesFolderPath}} konumundaki kurallar klasörü silinemedi. Manuel olarak silmeniz gerekebilir." + "getGroqApiKey": "Groq API Anahtarı Al" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "Kuruluşunuzun Roo Code Cloud hesabıyla kimlik doğrulaması yapmalısınız.", "verification_failed": "Kuruluş kimlik doğrulaması doğrulanamıyor." } - }, - "prompts": { - "deleteMode": { - "title": "Özel Modu Sil", - "description": "Bu {{scope}} modunu silmek istediğinizden emin misiniz? Bu, {{rulesFolderPath}} adresindeki ilişkili kurallar klasörünü de silecektir", - "descriptionNoRules": "Bu özel modu silmek istediğinizden emin misiniz?", - "confirm": "Sil" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Todo listesinde tamamlanmamış todolar olduğunda görev tamamlanmasını engelle" - } } } diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 15e4ef8b77..cc8a22f8a2 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "Bạn có chắc chắn muốn đặt lại tất cả trạng thái và lưu trữ bí mật trong tiện ích mở rộng không? Hành động này không thể hoàn tác.", "delete_config_profile": "Bạn có chắc chắn muốn xóa hồ sơ cấu hình này không?", - "delete_custom_mode_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}" + "delete_custom_mode": "Bạn có chắc chắn muốn xóa chế độ tùy chỉnh này không?", + "delete_message": "Bạn muốn xóa gì?", + "just_this_message": "Chỉ tin nhắn này", + "this_and_subsequent": "Tin nhắn này và tất cả tin nhắn tiếp theo" }, "errors": { "invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "Không thể mở tệp!", "checkpoint_timeout": "Đã hết thời gian khi cố gắng khôi phục điểm kiểm tra.", "checkpoint_failed": "Không thể khôi phục điểm kiểm tra.", - "git_not_installed": "Yêu cầu Git cho tính năng điểm kiểm tra. Vui lòng cài đặt Git để bật điểm kiểm tra.", "no_workspace": "Vui lòng mở thư mục dự án trước", "update_support_prompt": "Không thể cập nhật lời nhắc hỗ trợ", "reset_support_prompt": "Không thể đặt lại lời nhắc hỗ trợ", @@ -50,39 +52,21 @@ "cannot_access_path": "Không thể truy cập đường dẫn {{path}}: {{error}}", "settings_import_failed": "Nhập cài đặt thất bại: {{error}}.", "mistake_limit_guidance": "Điều này có thể cho thấy sự thất bại trong quá trình suy nghĩ của mô hình hoặc không thể sử dụng công cụ đúng cách, có thể được giảm thiểu bằng hướng dẫn của người dùng (ví dụ: \"Hãy thử chia nhỏ nhiệm vụ thành các bước nhỏ hơn\").", - "violated_organization_allowlist": "Không thể chạy tác vụ: hồ sơ hiện tại không tương thích với cài đặt của tổ chức của bạn", + "violated_organization_allowlist": "Không thể chạy tác vụ: hồ sơ hiện tại vi phạm cài đặt của tổ chức của bạn", "condense_failed": "Không thể nén ngữ cảnh", "condense_not_enough_messages": "Không đủ tin nhắn để nén ngữ cảnh", "condensed_recently": "Ngữ cảnh đã được nén gần đây; bỏ qua lần thử này", "condense_handler_invalid": "Trình xử lý API để nén ngữ cảnh không hợp lệ", "condense_context_grew": "Kích thước ngữ cảnh tăng lên trong quá trình nén; bỏ qua lần thử này", - "url_timeout": "Trang web mất quá nhiều thời gian để tải (timeout). Điều này có thể do kết nối chậm, trang web nặng hoặc tạm thời không khả dụng. Bạn có thể thử lại sau hoặc kiểm tra xem URL có đúng không.", - "url_not_found": "Không thể tìm thấy địa chỉ trang web. Vui lòng kiểm tra URL có đúng không và thử lại.", - "no_internet": "Không có kết nối internet. Vui lòng kiểm tra kết nối mạng và thử lại.", - "url_forbidden": "Truy cập vào trang web này bị cấm. Trang có thể chặn truy cập tự động hoặc yêu cầu xác thực.", - "url_page_not_found": "Không tìm thấy trang. Vui lòng kiểm tra URL có đúng không.", - "url_fetch_failed": "Lỗi lấy nội dung URL: {{error}}", - "url_fetch_error_with_url": "Lỗi lấy nội dung cho {{url}}: {{error}}", - "command_timeout": "Thực thi lệnh đã hết thời gian chờ sau {{seconds}} giây", "share_task_failed": "Không thể chia sẻ nhiệm vụ", "share_no_active_task": "Không có nhiệm vụ hoạt động để chia sẻ", "share_auth_required": "Cần xác thực. Vui lòng đăng nhập để chia sẻ nhiệm vụ.", "share_not_enabled": "Chia sẻ nhiệm vụ không được bật cho tổ chức này.", - "share_task_not_found": "Không tìm thấy nhiệm vụ hoặc truy cập bị từ chối.", - "mode_import_failed": "Nhập chế độ thất bại: {{error}}", - "delete_rules_folder_failed": "Không thể xóa thư mục quy tắc: {{rulesFolderPath}}. Lỗi: {{error}}", - "claudeCode": { - "processExited": "Tiến trình Claude Code thoát với mã {{exitCode}}.", - "errorOutput": "Đầu ra lỗi: {{output}}", - "processExitedWithError": "Tiến trình Claude Code thoát với mã {{exitCode}}. Đầu ra lỗi: {{output}}", - "stoppedWithReason": "Claude Code dừng lại vì lý do: {{reason}}", - "apiKeyModelPlanMismatch": "Khóa API và gói đăng ký cho phép các mô hình khác nhau. Đảm bảo rằng mô hình đã chọn được bao gồm trong gói của bạn." - } + "share_task_not_found": "Không tìm thấy nhiệm vụ hoặc truy cập bị từ chối." }, "warnings": { "no_terminal_content": "Không có nội dung terminal được chọn", - "missing_task_files": "Các tệp của nhiệm vụ này bị thiếu. Bạn có muốn xóa nó khỏi danh sách nhiệm vụ không?", - "auto_import_failed": "Không thể tự động nhập cài đặt RooCode: {{error}}" + "missing_task_files": "Các tệp của nhiệm vụ này bị thiếu. Bạn có muốn xóa nó khỏi danh sách nhiệm vụ không?" }, "info": { "no_changes": "Không tìm thấy thay đổi nào.", @@ -91,31 +75,22 @@ "custom_storage_path_set": "Đã thiết lập đường dẫn lưu trữ tùy chỉnh: {{path}}", "default_storage_path": "Đã quay lại sử dụng đường dẫn lưu trữ mặc định", "settings_imported": "Cài đặt đã được nhập thành công.", - "auto_import_success": "Cài đặt RooCode đã được tự động nhập từ {{filename}}", "share_link_copied": "Liên kết chia sẻ đã được sao chép vào clipboard", "image_copied_to_clipboard": "URI dữ liệu hình ảnh đã được sao chép vào clipboard", "image_saved": "Hình ảnh đã được lưu vào {{path}}", "organization_share_link_copied": "Liên kết chia sẻ tổ chức đã được sao chép vào clipboard!", - "public_share_link_copied": "Liên kết chia sẻ công khai đã được sao chép vào clipboard!", - "mode_exported": "Chế độ '{{mode}}' đã được xuất thành công", - "mode_imported": "Chế độ đã được nhập thành công" + "public_share_link_copied": "Liên kết chia sẻ công khai đã được sao chép vào clipboard!" }, "answers": { "yes": "Có", "no": "Không", + "cancel": "Hủy", "remove": "Xóa", "keep": "Giữ" }, - "buttons": { - "save": "Lưu", - "edit": "Chỉnh sửa", - "learn_more": "Tìm hiểu thêm" - }, "tasks": { "canceled": "Lỗi nhiệm vụ: Nó đã bị dừng và hủy bởi người dùng.", - "deleted": "Nhiệm vụ thất bại: Nó đã bị dừng và xóa bởi người dùng.", - "incomplete": "Nhiệm vụ #{{taskNumber}} (Chưa hoàn thành)", - "no_messages": "Nhiệm vụ #{{taskNumber}} (Không có tin nhắn)" + "deleted": "Nhiệm vụ thất bại: Nó đã bị dừng và xóa bởi người dùng." }, "storage": { "prompt_custom_path": "Nhập đường dẫn lưu trữ tùy chỉnh cho lịch sử hội thoại, để trống để sử dụng vị trí mặc định", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Khóa API Groq", - "getGroqApiKey": "Lấy khóa API Groq", - "claudeCode": { - "pathLabel": "Đường dẫn Claude Code", - "description": "Đường dẫn tùy chọn đến CLI Claude Code của bạn. Mặc định là 'claude' nếu không được đặt.", - "placeholder": "Mặc định: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": "YAML không hợp lệ trong tệp .roomodes tại dòng {{line}}. Vui lòng kiểm tra:\n• Thụt lề đúng (dùng dấu cách, không dùng tab)\n• Dấu ngoặc kép và ngoặc đơn khớp nhau\n• Cú pháp YAML hợp lệ", - "schemaValidationError": "Định dạng chế độ tùy chỉnh không hợp lệ trong .roomodes:\n{{issues}}", - "invalidFormat": "Định dạng chế độ tùy chỉnh không hợp lệ. Vui lòng đảm bảo cài đặt của bạn tuân theo định dạng YAML đúng.", - "updateFailed": "Cập nhật chế độ tùy chỉnh thất bại: {{error}}", - "deleteFailed": "Xóa chế độ tùy chỉnh thất bại: {{error}}", - "resetFailed": "Đặt lại chế độ tùy chỉnh thất bại: {{error}}", - "modeNotFound": "Lỗi ghi: Không tìm thấy chế độ", - "noWorkspaceForProject": "Không tìm thấy thư mục workspace cho chế độ dành riêng cho dự án", - "rulesCleanupFailed": "Đã xóa chế độ thành công, nhưng không thể xóa thư mục quy tắc tại {{rulesFolderPath}}. Bạn có thể cần xóa thủ công." - }, - "scope": { - "project": "dự án", - "global": "toàn cầu" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "Đã xóa chế độ thành công, nhưng không thể xóa thư mục quy tắc tại {{rulesFolderPath}}. Bạn có thể cần xóa thủ công." + "getGroqApiKey": "Lấy khóa API Groq" } }, "mdm": { @@ -166,25 +114,5 @@ "organization_mismatch": "Bạn phải được xác thực bằng tài khoản Roo Code Cloud của tổ chức.", "verification_failed": "Không thể xác minh xác thực tổ chức." } - }, - "prompts": { - "deleteMode": { - "title": "Xóa chế độ tùy chỉnh", - "description": "Bạn có chắc chắn muốn xóa chế độ {{scope}} này không? Thao tác này cũng θα xóa thư mục quy tắc liên quan tại {{rulesFolderPath}}", - "translations": { - "title": "Xóa chế độ", - "description": "Bạn có chắc chắn muốn xóa chế độ này không?", - "deleteMessage": "Chỉ chế độ này", - "rulesFolderMessage": "Thư mục quy tắc cũng sẽ bị xóa nếu tồn tại.", - "deleteConfirmation": "Chắc chắn xóa chế độ này?" - }, - "descriptionNoRules": "Bạn có chắc chắn muốn xóa chế độ tùy chỉnh này không?", - "confirm": "Xóa" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "Ngăn chặn hoàn thành nhiệm vụ khi có các todo chưa hoàn thành trong danh sách todo" - } } } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index edbbb6ae8c..b8e2307b85 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "您确定要重置扩展中的所有状态和密钥存储吗?此操作无法撤消。", "delete_config_profile": "您确定要删除此配置文件吗?", - "delete_custom_mode_with_rules": "您确定要删除此 {scope} 模式吗?\n\n这也将删除位于以下位置的关联规则文件夹:\n{rulesFolderPath}" + "delete_custom_mode": "您确定要删除此自定义模式吗?", + "delete_message": "您想删除什么?", + "just_this_message": "仅此消息", + "this_and_subsequent": "此消息及所有后续消息" }, "errors": { "invalid_mcp_config": "项目MCP配置格式无效", @@ -33,7 +36,6 @@ "could_not_open_file_generic": "无法打开文件!", "checkpoint_timeout": "尝试恢复检查点时超时。", "checkpoint_failed": "恢复检查点失败。", - "git_not_installed": "存档点功能需要 Git。请安装 Git 以启用存档点。", "no_workspace": "请先打开项目文件夹", "update_support_prompt": "更新支持消息失败", "reset_support_prompt": "重置支持消息失败", @@ -55,39 +57,21 @@ "cannot_access_path": "无法访问路径 {{path}}:{{error}}", "settings_import_failed": "设置导入失败:{{error}}。", "mistake_limit_guidance": "这可能表明模型思维过程失败或无法正确使用工具,可通过用户指导来缓解(例如\"尝试将任务分解为更小的步骤\")。", - "violated_organization_allowlist": "执行任务失败:当前配置文件与您的组织设置不兼容", + "violated_organization_allowlist": "执行任务失败:当前配置文件违反了您的组织设置", "condense_failed": "压缩上下文失败", "condense_not_enough_messages": "没有足够的对话来压缩上下文", "condensed_recently": "上下文最近已压缩;跳过此次尝试", "condense_handler_invalid": "压缩上下文的API处理程序无效", "condense_context_grew": "压缩过程中上下文大小增加;跳过此次尝试", - "url_timeout": "网站加载超时。这可能是由于网络连接缓慢、网站负载过重或暂时不可用。你可以稍后重试或检查 URL 是否正确。", - "url_not_found": "找不到网站地址。请检查 URL 是否正确并重试。", - "no_internet": "无网络连接。请检查网络连接并重试。", - "url_forbidden": "访问此网站被禁止。该网站可能阻止自动访问或需要身份验证。", - "url_page_not_found": "页面未找到。请检查 URL 是否正确。", - "url_fetch_failed": "获取 URL 内容失败:{{error}}", - "url_fetch_error_with_url": "获取 {{url}} 内容时出错:{{error}}", - "command_timeout": "命令执行超时,{{seconds}} 秒后", "share_task_failed": "分享任务失败。请重试。", "share_no_active_task": "没有活跃任务可分享", "share_auth_required": "需要身份验证。请登录以分享任务。", "share_not_enabled": "此组织未启用任务分享功能。", - "share_task_not_found": "未找到任务或访问被拒绝。", - "mode_import_failed": "导入模式失败:{{error}}", - "delete_rules_folder_failed": "删除规则文件夹失败:{{rulesFolderPath}}。错误:{{error}}", - "claudeCode": { - "processExited": "Claude Code 进程退出,退出码:{{exitCode}}。", - "errorOutput": "错误输出:{{output}}", - "processExitedWithError": "Claude Code 进程退出,退出码:{{exitCode}}。错误输出:{{output}}", - "stoppedWithReason": "Claude Code 停止,原因:{{reason}}", - "apiKeyModelPlanMismatch": "API 密钥和订阅计划支持不同的模型。请确保所选模型包含在您的计划中。" - } + "share_task_not_found": "未找到任务或访问被拒绝。" }, "warnings": { "no_terminal_content": "没有选择终端内容", - "missing_task_files": "此任务的文件丢失。您想从任务列表中删除它吗?", - "auto_import_failed": "自动导入 RooCode 设置失败:{{error}}" + "missing_task_files": "此任务的文件丢失。您想从任务列表中删除它吗?" }, "info": { "no_changes": "未找到更改。", @@ -96,31 +80,22 @@ "custom_storage_path_set": "自定义存储路径已设置:{{path}}", "default_storage_path": "已恢复使用默认存储路径", "settings_imported": "设置已成功导入。", - "auto_import_success": "已自动导入 RooCode 设置:{{filename}}", "share_link_copied": "分享链接已复制到剪贴板", "image_copied_to_clipboard": "图片数据 URI 已复制到剪贴板", "image_saved": "图片已保存到 {{path}}", "organization_share_link_copied": "组织分享链接已复制到剪贴板!", - "public_share_link_copied": "公开分享链接已复制到剪贴板!", - "mode_exported": "模式 '{{mode}}' 已成功导出", - "mode_imported": "模式已成功导入" + "public_share_link_copied": "公开分享链接已复制到剪贴板!" }, "answers": { "yes": "是", "no": "否", + "cancel": "取消", "remove": "删除", "keep": "保留" }, - "buttons": { - "save": "保存", - "edit": "编辑", - "learn_more": "了解更多" - }, "tasks": { "canceled": "任务错误:它已被用户停止并取消。", - "deleted": "任务失败:它已被用户停止并删除。", - "incomplete": "任务 #{{taskNumber}} (未完成)", - "no_messages": "任务 #{{taskNumber}} (无消息)" + "deleted": "任务失败:它已被用户停止并删除。" }, "storage": { "prompt_custom_path": "输入自定义会话历史存储路径,留空以使用默认位置", @@ -135,34 +110,7 @@ "settings": { "providers": { "groqApiKey": "Groq API 密钥", - "getGroqApiKey": "获取 Groq API 密钥", - "claudeCode": { - "pathLabel": "Claude Code 路径", - "description": "Claude Code CLI 的可选路径。如果未设置,默认为 'claude'。", - "placeholder": "默认: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": ".roomodes 文件第 {{line}} 行 YAML 格式无效。请检查:\n• 正确的缩进(使用空格,不要使用制表符)\n• 匹配的引号和括号\n• 有效的 YAML 语法", - "schemaValidationError": ".roomodes 中自定义模式格式无效:\n{{issues}}", - "invalidFormat": "自定义模式格式无效。请确保你的设置遵循正确的 YAML 格式。", - "updateFailed": "更新自定义模式失败:{{error}}", - "deleteFailed": "删除自定义模式失败:{{error}}", - "resetFailed": "重置自定义模式失败:{{error}}", - "modeNotFound": "写入错误:未找到模式", - "noWorkspaceForProject": "未找到项目特定模式的工作区文件夹", - "rulesCleanupFailed": "模式删除成功,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。" - }, - "scope": { - "project": "项目", - "global": "全局" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "模式已成功移除,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。" + "getGroqApiKey": "获取 Groq API 密钥" } }, "mdm": { @@ -171,18 +119,5 @@ "organization_mismatch": "您必须使用组织的 Roo Code Cloud 账户进行身份验证。", "verification_failed": "无法验证组织身份验证。" } - }, - "prompts": { - "deleteMode": { - "title": "删除自定义模式", - "description": "您确定要删除此 {{scope}} 模式吗?这也将删除位于 {{rulesFolderPath}} 的关联规则文件夹", - "descriptionNoRules": "您确定要删除此自定义模式吗?", - "confirm": "删除" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "当待办事项列表中有未完成的待办事项时阻止任务完成" - } } } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index e7887025f1..57e065a7f7 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -17,7 +17,10 @@ "confirmation": { "reset_state": "您確定要重設擴充套件中的所有狀態和金鑰儲存嗎?此操作無法復原。", "delete_config_profile": "您確定要刪除此設定檔案嗎?", - "delete_custom_mode_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾:\n{rulesFolderPath}" + "delete_custom_mode": "您確定要刪除此自訂模式嗎?", + "delete_message": "您想刪除哪些內容?", + "just_this_message": "僅這則訊息", + "this_and_subsequent": "這則訊息及所有後續訊息" }, "errors": { "invalid_data_uri": "資料 URI 格式無效", @@ -28,7 +31,6 @@ "could_not_open_file_generic": "無法開啟檔案!", "checkpoint_timeout": "嘗試恢復檢查點時超時。", "checkpoint_failed": "恢復檢查點失敗。", - "git_not_installed": "存檔點功能需要 Git。請安裝 Git 以啟用存檔點。", "no_workspace": "請先開啟專案資料夾", "update_support_prompt": "更新支援訊息失敗", "reset_support_prompt": "重設支援訊息失敗", @@ -50,39 +52,21 @@ "cannot_access_path": "無法存取路徑 {{path}}:{{error}}", "settings_import_failed": "設定匯入失敗:{{error}}。", "mistake_limit_guidance": "這可能表明模型思維過程失敗或無法正確使用工具,可透過使用者指導來緩解(例如「嘗試將工作分解為更小的步驟」)。", - "violated_organization_allowlist": "執行工作失敗:目前設定檔與您的組織設定不相容", + "violated_organization_allowlist": "執行工作失敗:目前設定檔違反了您的組織設定", "condense_failed": "壓縮上下文失敗", "condense_not_enough_messages": "沒有足夠的訊息來壓縮上下文", "condensed_recently": "上下文最近已壓縮;跳過此次嘗試", "condense_handler_invalid": "壓縮上下文的 API 處理程式無效", "condense_context_grew": "壓縮過程中上下文大小增加;跳過此次嘗試", - "url_timeout": "網站載入超時。這可能是由於網路連線緩慢、網站負載過重或暫時無法使用。你可以稍後重試或檢查 URL 是否正確。", - "url_not_found": "找不到網站位址。請檢查 URL 是否正確並重試。", - "no_internet": "無網路連線。請檢查網路連線並重試。", - "url_forbidden": "存取此網站被禁止。該網站可能封鎖自動存取或需要身分驗證。", - "url_page_not_found": "找不到頁面。請檢查 URL 是否正確。", - "url_fetch_failed": "取得 URL 內容失敗:{{error}}", - "url_fetch_error_with_url": "取得 {{url}} 內容時發生錯誤:{{error}}", - "command_timeout": "命令執行超時,{{seconds}} 秒後", "share_task_failed": "分享工作失敗。請重試。", "share_no_active_task": "沒有活躍的工作可分享", "share_auth_required": "需要身份驗證。請登入以分享工作。", "share_not_enabled": "此組織未啟用工作分享功能。", - "share_task_not_found": "未找到工作或存取被拒絕。", - "delete_rules_folder_failed": "刪除規則資料夾失敗: {{rulesFolderPath}}。錯誤: {{error}}", - "claudeCode": { - "processExited": "Claude Code 程序退出,退出碼:{{exitCode}}。", - "errorOutput": "錯誤輸出:{{output}}", - "processExitedWithError": "Claude Code 程序退出,退出碼:{{exitCode}}。錯誤輸出:{{output}}", - "stoppedWithReason": "Claude Code 停止,原因:{{reason}}", - "apiKeyModelPlanMismatch": "API 金鑰和訂閱方案允許不同的模型。請確保所選模型包含在您的方案中。" - }, - "mode_import_failed": "匯入模式失敗:{{error}}" + "share_task_not_found": "未找到工作或存取被拒絕。" }, "warnings": { "no_terminal_content": "沒有選擇終端機內容", - "missing_task_files": "此工作的檔案遺失。您想從工作列表中刪除它嗎?", - "auto_import_failed": "自動匯入 RooCode 設定失敗:{{error}}" + "missing_task_files": "此工作的檔案遺失。您想從工作列表中刪除它嗎?" }, "info": { "no_changes": "沒有找到更改。", @@ -91,31 +75,22 @@ "custom_storage_path_set": "自訂儲存路徑已設定:{{path}}", "default_storage_path": "已恢復使用預設儲存路徑", "settings_imported": "設定已成功匯入。", - "auto_import_success": "已自動匯入 RooCode 設定:{{filename}}", "share_link_copied": "分享連結已複製到剪貼簿", "image_copied_to_clipboard": "圖片資料 URI 已複製到剪貼簿", "image_saved": "圖片已儲存至 {{path}}", "organization_share_link_copied": "組織分享連結已複製到剪貼簿!", - "public_share_link_copied": "公開分享連結已複製到剪貼簿!", - "mode_exported": "模式 '{{mode}}' 已成功匯出", - "mode_imported": "模式已成功匯入" + "public_share_link_copied": "公開分享連結已複製到剪貼簿!" }, "answers": { "yes": "是", "no": "否", + "cancel": "取消", "remove": "刪除", "keep": "保留" }, - "buttons": { - "save": "儲存", - "edit": "編輯", - "learn_more": "了解更多" - }, "tasks": { "canceled": "工作錯誤:它已被使用者停止並取消。", - "deleted": "工作失敗:它已被使用者停止並刪除。", - "incomplete": "工作 #{{taskNumber}} (未完成)", - "no_messages": "工作 #{{taskNumber}} (無訊息)" + "deleted": "工作失敗:它已被使用者停止並刪除。" }, "storage": { "prompt_custom_path": "輸入自訂會話歷史儲存路徑,留空以使用預設位置", @@ -130,34 +105,7 @@ "settings": { "providers": { "groqApiKey": "Groq API 金鑰", - "getGroqApiKey": "取得 Groq API 金鑰", - "claudeCode": { - "pathLabel": "Claude Code 路徑", - "description": "Claude Code CLI 的選用路徑。如果未設定,預設為 'claude'。", - "placeholder": "預設: claude" - } - } - }, - "customModes": { - "errors": { - "yamlParseError": ".roomodes 檔案第 {{line}} 行 YAML 格式無效。請檢查:\n• 正確的縮排(使用空格,不要使用定位字元)\n• 匹配的引號和括號\n• 有效的 YAML 語法", - "schemaValidationError": ".roomodes 中自訂模式格式無效:\n{{issues}}", - "invalidFormat": "自訂模式格式無效。請確保你的設定遵循正確的 YAML 格式。", - "updateFailed": "更新自訂模式失敗:{{error}}", - "deleteFailed": "刪除自訂模式失敗:{{error}}", - "resetFailed": "重設自訂模式失敗:{{error}}", - "modeNotFound": "寫入錯誤:未找到模式", - "noWorkspaceForProject": "未找到專案特定模式的工作區資料夾", - "rulesCleanupFailed": "模式已成功刪除,但無法刪除位於 {{rulesFolderPath}} 的規則資料夾。您可能需要手動刪除。" - }, - "scope": { - "project": "專案", - "global": "全域" - } - }, - "marketplace": { - "mode": { - "rulesCleanupFailed": "模式已成功移除,但無法刪除位於 {{rulesFolderPath}} 的規則資料夾。您可能需要手動刪除。" + "getGroqApiKey": "取得 Groq API 金鑰" } }, "mdm": { @@ -166,18 +114,5 @@ "organization_mismatch": "您必須使用組織的 Roo Code Cloud 帳戶進行身份驗證。", "verification_failed": "無法驗證組織身份驗證。" } - }, - "prompts": { - "deleteMode": { - "title": "刪除自訂模式", - "description": "您確定要刪除此 {{scope}} 模式嗎?這也將刪除位於 {{rulesFolderPath}} 的關聯規則資料夾", - "descriptionNoRules": "您確定要刪除此自訂模式嗎?", - "confirm": "刪除" - } - }, - "commands": { - "preventCompletionWithOpenTodos": { - "description": "當待辦事項清單中有未完成的待辦事項時阻止工作完成" - } } } diff --git a/src/integrations/claude-code/__tests__/message-filter.spec.ts b/src/integrations/claude-code/__tests__/message-filter.spec.ts deleted file mode 100644 index 25f4948cb3..0000000000 --- a/src/integrations/claude-code/__tests__/message-filter.spec.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { describe, test, expect } from "vitest" -import { filterMessagesForClaudeCode } from "../message-filter" -import type { Anthropic } from "@anthropic-ai/sdk" - -describe("filterMessagesForClaudeCode", () => { - test("should pass through string messages unchanged", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: "Hello, this is a simple text message", - }, - ] - - const result = filterMessagesForClaudeCode(messages) - - expect(result).toEqual(messages) - }) - - test("should pass through text-only content blocks unchanged", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "This is a text block", - }, - ], - }, - ] - - const result = filterMessagesForClaudeCode(messages) - - expect(result).toEqual(messages) - }) - - test("should replace image blocks with text placeholders", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "Here's an image:", - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/png", - data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==", - }, - }, - ], - }, - ] - - const result = filterMessagesForClaudeCode(messages) - - expect(result).toEqual([ - { - role: "user", - content: [ - { - type: "text", - text: "Here's an image:", - }, - { - type: "text", - text: "[Image (base64): image/png not supported by Claude Code]", - }, - ], - }, - ]) - }) - - test("should handle image blocks with unknown source types", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "image", - source: undefined as any, - }, - ], - }, - ] - - const result = filterMessagesForClaudeCode(messages) - - expect(result).toEqual([ - { - role: "user", - content: [ - { - type: "text", - text: "[Image (unknown): unknown not supported by Claude Code]", - }, - ], - }, - ]) - }) - - test("should handle mixed content with multiple images", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "Compare these images:", - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/jpeg", - data: "base64data1", - }, - }, - { - type: "text", - text: "and", - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/gif", - data: "base64data2", - }, - }, - { - type: "text", - text: "What do you think?", - }, - ], - }, - ] - - const result = filterMessagesForClaudeCode(messages) - - expect(result).toEqual([ - { - role: "user", - content: [ - { - type: "text", - text: "Compare these images:", - }, - { - type: "text", - text: "[Image (base64): image/jpeg not supported by Claude Code]", - }, - { - type: "text", - text: "and", - }, - { - type: "text", - text: "[Image (base64): image/gif not supported by Claude Code]", - }, - { - type: "text", - text: "What do you think?", - }, - ], - }, - ]) - }) - - test("should handle multiple messages with images", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: "First message with text only", - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I can help with that.", - }, - ], - }, - { - role: "user", - content: [ - { - type: "text", - text: "Here's an image:", - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/png", - data: "imagedata", - }, - }, - ], - }, - ] - - const result = filterMessagesForClaudeCode(messages) - - expect(result).toEqual([ - { - role: "user", - content: "First message with text only", - }, - { - role: "assistant", - content: [ - { - type: "text", - text: "I can help with that.", - }, - ], - }, - { - role: "user", - content: [ - { - type: "text", - text: "Here's an image:", - }, - { - type: "text", - text: "[Image (base64): image/png not supported by Claude Code]", - }, - ], - }, - ]) - }) - - test("should preserve other content block types unchanged", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "Regular text", - }, - // This would be some other content type that's not an image - { - type: "tool_use" as any, - id: "tool_123", - name: "test_tool", - input: { test: "data" }, - }, - ], - }, - ] - - const result = filterMessagesForClaudeCode(messages) - - expect(result).toEqual(messages) - }) -}) diff --git a/src/integrations/claude-code/message-filter.ts b/src/integrations/claude-code/message-filter.ts deleted file mode 100644 index 25ffacce6b..0000000000 --- a/src/integrations/claude-code/message-filter.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Anthropic } from "@anthropic-ai/sdk" - -/** - * Filters out image blocks from messages since Claude Code doesn't support images. - * Replaces image blocks with text placeholders similar to how VSCode LM provider handles it. - */ -export function filterMessagesForClaudeCode( - messages: Anthropic.Messages.MessageParam[], -): Anthropic.Messages.MessageParam[] { - return messages.map((message) => { - // Handle simple string messages - if (typeof message.content === "string") { - return message - } - - // Handle complex message structures - const filteredContent = message.content.map((block) => { - if (block.type === "image") { - // Replace image blocks with text placeholders - const sourceType = block.source?.type || "unknown" - const mediaType = block.source?.media_type || "unknown" - return { - type: "text" as const, - text: `[Image (${sourceType}): ${mediaType} not supported by Claude Code]`, - } - } - return block - }) - - return { - ...message, - content: filteredContent, - } - }) -} diff --git a/src/integrations/claude-code/types.ts b/src/integrations/claude-code/types.ts deleted file mode 100644 index 36edaee2ed..0000000000 --- a/src/integrations/claude-code/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Anthropic } from "@anthropic-ai/sdk" - -type InitMessage = { - type: "system" - subtype: "init" - session_id: string - tools: string[] - mcp_servers: string[] - apiKeySource: "none" | "/login managed key" | string -} - -type AssistantMessage = { - type: "assistant" - message: Anthropic.Messages.Message - session_id: string -} - -type ErrorMessage = { - type: "error" -} - -type ResultMessage = { - type: "result" - subtype: "success" - total_cost_usd: number - is_error: boolean - duration_ms: number - duration_api_ms: number - num_turns: number - result: string - session_id: string -} - -export type ClaudeCodeMessage = InitMessage | AssistantMessage | ErrorMessage | ResultMessage diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 64820beffb..b97886d32d 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -4,7 +4,6 @@ import * as fs from "fs/promises" import * as diff from "diff" import stripBom from "strip-bom" import { XMLBuilder } from "fast-xml-parser" -import delay from "delay" import { createDirectoriesForFile } from "../../utils/fs" import { arePathsEqual, getReadablePath } from "../../utils/path" @@ -12,12 +11,10 @@ import { formatResponse } from "../../core/prompts/responses" import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics" import { ClineSayTool } from "../../shared/ExtensionMessage" import { Task } from "../../core/task/Task" -import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" import { DecorationController } from "./DecorationController" export const DIFF_VIEW_URI_SCHEME = "cline-diff" -export const DIFF_VIEW_LABEL_CHANGES = "Original ↔ Roo's Changes" // TODO: https://github.com/cline/cline/pull/3354 export class DiffViewProvider { @@ -36,14 +33,8 @@ export class DiffViewProvider { private activeLineController?: DecorationController private streamedLines: string[] = [] private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [] - private taskRef: WeakRef - constructor( - private cwd: string, - task: Task, - ) { - this.taskRef = new WeakRef(task) - } + constructor(private cwd: string) {} async open(relPath: string): Promise { this.relPath = relPath @@ -130,7 +121,7 @@ export class DiffViewProvider { } // Place cursor at the beginning of the diff editor to keep it out of - // the way of the stream animation, but do this without stealing focus + // the way of the stream animation. const beginningOfDocument = new vscode.Position(0, 0) diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument) @@ -138,14 +129,13 @@ export class DiffViewProvider { // Replace all content up to the current line with accumulated lines. const edit = new vscode.WorkspaceEdit() const rangeToReplace = new vscode.Range(0, 0, endLine, 0) - const contentToReplace = - accumulatedLines.slice(0, endLine).join("\n") + (accumulatedLines.length > 0 ? "\n" : "") + const contentToReplace = accumulatedLines.slice(0, endLine + 1).join("\n") + "\n" edit.replace(document.uri, rangeToReplace, this.stripAllBOMs(contentToReplace)) await vscode.workspace.applyEdit(edit) // Update decorations. this.activeLineController.setActiveLine(endLine) this.fadedOverlayController.updateOverlayAfterLine(endLine, document.lineCount) - // Scroll to the current line without stealing focus. + // Scroll to the current line. const ranges = this.activeDiffEditor?.visibleRanges if (ranges && ranges.length > 0 && ranges[0].start.line < endLine && ranges[0].end.line > endLine) { this.scrollEditorToLine(endLine) @@ -187,7 +177,7 @@ export class DiffViewProvider { } } - async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{ + async saveChanges(): Promise<{ newProblemsMessage: string | undefined userEdits: string | undefined finalContent: string | undefined @@ -222,53 +212,29 @@ export class DiffViewProvider { // and can address them accordingly. If problems don't change immediately after // applying a fix, won't be notified, which is generally fine since the // initial fix is usually correct and it may just take time for linters to catch up. - - let newProblemsMessage = "" - - if (diagnosticsEnabled) { - // Add configurable delay to allow linters time to process and clean up issues - // like unused imports (especially important for Go and other languages) - // Ensure delay is non-negative - const safeDelayMs = Math.max(0, writeDelayMs) - - try { - await delay(safeDelayMs) - } catch (error) { - // Log error but continue - delay failure shouldn't break the save operation - console.warn(`Failed to apply write delay: ${error}`) - } - - const postDiagnostics = vscode.languages.getDiagnostics() + const postDiagnostics = vscode.languages.getDiagnostics() - // Get diagnostic settings from state - const task = this.taskRef.deref() - const state = await task?.providerRef.deref()?.getState() - const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true - const maxDiagnosticMessages = state?.maxDiagnosticMessages ?? 50 + const newProblems = await diagnosticsToProblemsString( + getNewDiagnostics(this.preDiagnostics, postDiagnostics), + [ + vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention) + ], + this.cwd, + ) // Will be empty string if no errors. - const newProblems = await diagnosticsToProblemsString( - getNewDiagnostics(this.preDiagnostics, postDiagnostics), - [ - vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention) - ], - this.cwd, - includeDiagnosticMessages, - maxDiagnosticMessages, - ) // Will be empty string if no errors. - - newProblemsMessage = - newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : "" - } + const newProblemsMessage = + newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : "" // If the edited content has different EOL characters, we don't want to // show a diff with all the EOL differences. const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n" - // Normalize EOL characters without trimming content - const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL) + // `trimEnd` to fix issue where editor adds in extra new line + // automatically. + const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // Just in case the new content has a mix of varying EOL characters. - const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL) + const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL if (normalizedEditedContent !== normalizedNewContent) { // User made changes before approving edit. @@ -418,25 +384,12 @@ export class DiffViewProvider { private async closeAllDiffViews(): Promise { const closeOps = vscode.window.tabGroups.all .flatMap((group) => group.tabs) - .filter((tab) => { - // Check for standard diff views with our URI scheme - if ( + .filter( + (tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input.original.scheme === DIFF_VIEW_URI_SCHEME && - !tab.isDirty - ) { - return true - } - - // Also check by tab label for our specific diff views - // This catches cases where the diff view might be created differently - // when files are pre-opened as text documents - if (tab.label.includes(DIFF_VIEW_LABEL_CHANGES) && !tab.isDirty) { - return true - } - - return false - }) + !tab.isDirty, + ) .map((tab) => vscode.window.tabGroups.close(tab).then( () => undefined, @@ -534,22 +487,17 @@ export class DiffViewProvider { }), ) - // Pre-open the file as a text document to ensure it doesn't open in preview mode - // This fixes issues with files that have custom editor associations (like markdown preview) - vscode.window - .showTextDocument(uri, { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }) - .then(() => { - // Execute the diff command after ensuring the file is open as text - return vscode.commands.executeCommand( - "vscode.diff", - vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ - query: Buffer.from(this.originalContent ?? "").toString("base64"), - }), - uri, - `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, - { preserveFocus: true }, - ) - }) + // Execute the diff command + vscode.commands + .executeCommand( + "vscode.diff", + vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ + query: Buffer.from(this.originalContent ?? "").toString("base64"), + }), + uri, + `${fileName}: ${fileExists ? "Original ↔ Roo's Changes" : "New File"} (Editable)`, + { preserveFocus: true }, + ) .then( () => { // Command executed successfully, now wait for the editor to appear @@ -585,7 +533,7 @@ export class DiffViewProvider { for (const part of diffs) { if (part.added || part.removed) { - // Found the first diff, scroll to it without stealing focus. + // Found the first diff, scroll to it. this.activeDiffEditor.revealRange( new vscode.Range(lineCount, 0, lineCount, 0), vscode.TextEditorRevealType.InCenter, diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index 7159aca57a..aa6e492bcd 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -1,90 +1,24 @@ -import { DiffViewProvider, DIFF_VIEW_URI_SCHEME, DIFF_VIEW_LABEL_CHANGES } from "../DiffViewProvider" +import { DiffViewProvider } from "../DiffViewProvider" import * as vscode from "vscode" -import * as path from "path" -import delay from "delay" - -// Mock delay -vi.mock("delay", () => ({ - default: vi.fn().mockResolvedValue(undefined), -})) - -// Mock fs/promises -vi.mock("fs/promises", () => ({ - readFile: vi.fn().mockResolvedValue("file content"), - writeFile: vi.fn().mockResolvedValue(undefined), -})) - -// Mock utils -vi.mock("../../../utils/fs", () => ({ - createDirectoriesForFile: vi.fn().mockResolvedValue([]), -})) - -// Mock path -vi.mock("path", () => ({ - resolve: vi.fn((cwd, relPath) => `${cwd}/${relPath}`), - basename: vi.fn((path) => path.split("/").pop()), -})) // Mock vscode vi.mock("vscode", () => ({ workspace: { applyEdit: vi.fn(), - onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })), - textDocuments: [], - fs: { - stat: vi.fn(), - }, }, window: { createTextEditorDecorationType: vi.fn(), - showTextDocument: vi.fn(), - onDidChangeVisibleTextEditors: vi.fn(() => ({ dispose: vi.fn() })), - tabGroups: { - all: [], - close: vi.fn(), - }, - visibleTextEditors: [], - }, - commands: { - executeCommand: vi.fn(), - }, - languages: { - getDiagnostics: vi.fn(() => []), - }, - DiagnosticSeverity: { - Error: 0, - Warning: 1, - Information: 2, - Hint: 3, }, WorkspaceEdit: vi.fn().mockImplementation(() => ({ replace: vi.fn(), delete: vi.fn(), })), - ViewColumn: { - Active: 1, - Beside: 2, - One: 1, - Two: 2, - Three: 3, - Four: 4, - Five: 5, - Six: 6, - Seven: 7, - Eight: 8, - Nine: 9, - }, Range: vi.fn(), Position: vi.fn(), Selection: vi.fn(), TextEditorRevealType: { InCenter: 2, }, - TabInputTextDiff: class TabInputTextDiff {}, - Uri: { - file: vi.fn((path) => ({ fsPath: path })), - parse: vi.fn((uri) => ({ with: vi.fn(() => ({})) })), - }, })) // Mock DecorationController @@ -92,7 +26,6 @@ vi.mock("../DecorationController", () => ({ DecorationController: vi.fn().mockImplementation(() => ({ setActiveLine: vi.fn(), updateOverlayAfterLine: vi.fn(), - addLines: vi.fn(), clear: vi.fn(), })), })) @@ -101,7 +34,6 @@ describe("DiffViewProvider", () => { let diffViewProvider: DiffViewProvider const mockCwd = "/mock/cwd" let mockWorkspaceEdit: { replace: any; delete: any } - let mockTask: any beforeEach(() => { vi.clearAllMocks() @@ -111,19 +43,7 @@ describe("DiffViewProvider", () => { } vi.mocked(vscode.WorkspaceEdit).mockImplementation(() => mockWorkspaceEdit as any) - // Create a mock Task instance - mockTask = { - providerRef: { - deref: vi.fn().mockReturnValue({ - getState: vi.fn().mockResolvedValue({ - includeDiagnosticMessages: true, - maxDiagnosticMessages: 50, - }), - }), - }, - } - - diffViewProvider = new DiffViewProvider(mockCwd, mockTask) + diffViewProvider = new DiffViewProvider(mockCwd) // Mock the necessary properties and methods ;(diffViewProvider as any).relPath = "test.txt" ;(diffViewProvider as any).activeDiffEditor = { @@ -140,11 +60,7 @@ describe("DiffViewProvider", () => { revealRange: vi.fn(), } ;(diffViewProvider as any).activeLineController = { setActiveLine: vi.fn(), clear: vi.fn() } - ;(diffViewProvider as any).fadedOverlayController = { - updateOverlayAfterLine: vi.fn(), - addLines: vi.fn(), - clear: vi.fn(), - } + ;(diffViewProvider as any).fadedOverlayController = { updateOverlayAfterLine: vi.fn(), clear: vi.fn() } }) describe("update method", () => { @@ -177,258 +93,4 @@ describe("DiffViewProvider", () => { expect(mockWorkspaceEdit.replace).toHaveBeenCalledWith(expect.anything(), expect.anything(), "New content") }) }) - - describe("open method", () => { - it("should pre-open file as text document before executing diff command", async () => { - // Setup - const mockEditor = { - document: { - uri: { fsPath: `${mockCwd}/test.md` }, - getText: vi.fn().mockReturnValue(""), - lineCount: 0, - }, - selection: { - active: { line: 0, character: 0 }, - anchor: { line: 0, character: 0 }, - }, - edit: vi.fn().mockResolvedValue(true), - revealRange: vi.fn(), - } - - // Track the order of calls - const callOrder: string[] = [] - - // Mock showTextDocument to track when it's called - vi.mocked(vscode.window.showTextDocument).mockImplementation(async (uri, options) => { - callOrder.push("showTextDocument") - expect(options).toEqual({ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }) - return mockEditor as any - }) - - // Mock executeCommand to track when it's called - vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command) => { - callOrder.push("executeCommand") - expect(command).toBe("vscode.diff") - return undefined - }) - - // Mock workspace.onDidOpenTextDocument to trigger immediately - vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { - // Trigger the callback immediately with the document - setTimeout(() => { - callback({ uri: { fsPath: `${mockCwd}/test.md` } } as any) - }, 0) - return { dispose: vi.fn() } - }) - - // Mock window.visibleTextEditors to return our editor - vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] - - // Set up for file - ;(diffViewProvider as any).editType = "modify" - - // Execute open - await diffViewProvider.open("test.md") - - // Verify that showTextDocument was called before executeCommand - expect(callOrder).toEqual(["showTextDocument", "executeCommand"]) - - // Verify that showTextDocument was called with preview: false and preserveFocus: true - expect(vscode.window.showTextDocument).toHaveBeenCalledWith( - expect.objectContaining({ fsPath: `${mockCwd}/test.md` }), - { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }, - ) - - // Verify that the diff command was executed - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - "vscode.diff", - expect.any(Object), - expect.any(Object), - `test.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - { preserveFocus: true }, - ) - }) - - it("should handle showTextDocument failure", async () => { - // Mock showTextDocument to fail - vi.mocked(vscode.window.showTextDocument).mockRejectedValue(new Error("Cannot open file")) - - // Mock workspace.onDidOpenTextDocument - vi.mocked(vscode.workspace.onDidOpenTextDocument).mockReturnValue({ dispose: vi.fn() }) - - // Mock window.onDidChangeVisibleTextEditors - vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - - // Set up for file - ;(diffViewProvider as any).editType = "modify" - - // Try to open and expect rejection - await expect(diffViewProvider.open("test.md")).rejects.toThrow( - "Failed to execute diff command for /mock/cwd/test.md: Cannot open file", - ) - }) - }) - - describe("closeAllDiffViews method", () => { - it("should close diff views including those identified by label", async () => { - // Mock tab groups with various types of tabs - const mockTabs = [ - // Normal diff view - { - input: { - constructor: { name: "TabInputTextDiff" }, - original: { scheme: DIFF_VIEW_URI_SCHEME }, - modified: { fsPath: "/test/file1.ts" }, - }, - label: `file1.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - isDirty: false, - }, - // Diff view identified by label (for pre-opened files) - { - input: { - constructor: { name: "TabInputTextDiff" }, - original: { scheme: "file" }, // Different scheme due to pre-opening - modified: { fsPath: "/test/file2.md" }, - }, - label: `file2.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - isDirty: false, - }, - // Regular file tab (should not be closed) - { - input: { - constructor: { name: "TabInputText" }, - uri: { fsPath: "/test/file3.js" }, - }, - label: "file3.js", - isDirty: false, - }, - // Dirty diff view (should not be closed) - { - input: { - constructor: { name: "TabInputTextDiff" }, - original: { scheme: DIFF_VIEW_URI_SCHEME }, - modified: { fsPath: "/test/file4.ts" }, - }, - label: `file4.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - isDirty: true, - }, - ] - - // Make tabs appear as TabInputTextDiff instances - mockTabs.forEach((tab) => { - if (tab.input.constructor.name === "TabInputTextDiff") { - Object.setPrototypeOf(tab.input, vscode.TabInputTextDiff.prototype) - } - }) - - // Mock the tabGroups getter - Object.defineProperty(vscode.window.tabGroups, "all", { - get: () => [ - { - tabs: mockTabs as any, - }, - ], - configurable: true, - }) - - const closedTabs: any[] = [] - vi.mocked(vscode.window.tabGroups.close).mockImplementation((tab) => { - closedTabs.push(tab) - return Promise.resolve(true) - }) - - // Execute closeAllDiffViews - await (diffViewProvider as any).closeAllDiffViews() - - // Verify that only the appropriate tabs were closed - expect(closedTabs).toHaveLength(2) - expect(closedTabs[0].label).toBe(`file1.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`) - expect(closedTabs[1].label).toBe(`file2.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`) - - // Verify that the regular file and dirty diff were not closed - expect(closedTabs.find((t) => t.label === "file3.js")).toBeUndefined() - expect( - closedTabs.find((t) => t.label === `file4.ts: ${DIFF_VIEW_LABEL_CHANGES} (Editable)` && t.isDirty), - ).toBeUndefined() - }) - }) - - describe("saveChanges method with diagnostic settings", () => { - beforeEach(() => { - // Setup common mocks for saveChanges tests - ;(diffViewProvider as any).relPath = "test.ts" - ;(diffViewProvider as any).newContent = "new content" - ;(diffViewProvider as any).activeDiffEditor = { - document: { - getText: vi.fn().mockReturnValue("new content"), - isDirty: false, - save: vi.fn().mockResolvedValue(undefined), - }, - } - ;(diffViewProvider as any).preDiagnostics = [] - - // Mock vscode functions - vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) - vi.mocked(vscode.languages.getDiagnostics).mockReturnValue([]) - }) - - it("should apply diagnostic delay when diagnosticsEnabled is true", async () => { - const mockDelay = vi.mocked(delay) - mockDelay.mockClear() - - // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - - const result = await diffViewProvider.saveChanges(true, 3000) - - // Verify delay was called with correct duration - expect(mockDelay).toHaveBeenCalledWith(3000) - expect(vscode.languages.getDiagnostics).toHaveBeenCalled() - expect(result.newProblemsMessage).toBe("") - }) - - it("should skip diagnostics when diagnosticsEnabled is false", async () => { - const mockDelay = vi.mocked(delay) - mockDelay.mockClear() - - // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - - const result = await diffViewProvider.saveChanges(false, 2000) - - // Verify delay was NOT called and diagnostics were NOT checked - expect(mockDelay).not.toHaveBeenCalled() - expect(vscode.languages.getDiagnostics).not.toHaveBeenCalled() - expect(result.newProblemsMessage).toBe("") - }) - - it("should use default values when no parameters provided", async () => { - const mockDelay = vi.mocked(delay) - mockDelay.mockClear() - - // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - - const result = await diffViewProvider.saveChanges() - - // Verify default behavior (enabled=true, delay=2000ms) - expect(mockDelay).toHaveBeenCalledWith(1000) - expect(vscode.languages.getDiagnostics).toHaveBeenCalled() - expect(result.newProblemsMessage).toBe("") - }) - - it("should handle custom delay values", async () => { - const mockDelay = vi.mocked(delay) - mockDelay.mockClear() - - // Mock closeAllDiffViews - ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) - - const result = await diffViewProvider.saveChanges(true, 5000) - - // Verify custom delay was used - expect(mockDelay).toHaveBeenCalledWith(5000) - expect(vscode.languages.getDiagnostics).toHaveBeenCalled() - }) - }) }) diff --git a/src/package.json b/src/package.json index 1fac5fb365..20147b1033 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.23.19", + "version": "3.21.2", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -75,16 +75,16 @@ "title": "%command.newTask.title%", "icon": "$(add)" }, - { - "command": "roo-cline.promptsButtonClicked", - "title": "%command.prompts.title%", - "icon": "$(organization)" - }, { "command": "roo-cline.mcpButtonClicked", "title": "%command.mcpServers.title%", "icon": "$(server)" }, + { + "command": "roo-cline.promptsButtonClicked", + "title": "%command.prompts.title%", + "icon": "$(organization)" + }, { "command": "roo-cline.historyButtonClicked", "title": "%command.history.title%", @@ -103,7 +103,8 @@ { "command": "roo-cline.accountButtonClicked", "title": "Account", - "icon": "$(account)" + "icon": "$(account)", + "when": "config.roo-cline.rooCodeCloudEnabled" }, { "command": "roo-cline.settingsButtonClicked", @@ -160,11 +161,6 @@ "title": "%command.setCustomStoragePath.title%", "category": "%configuration.title%" }, - { - "command": "roo-cline.importSettings", - "title": "%command.importSettings.title%", - "category": "%configuration.title%" - }, { "command": "roo-cline.focusInput", "title": "%command.focusInput.title%", @@ -224,38 +220,38 @@ "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.marketplaceButtonClicked", + "command": "roo-cline.promptsButtonClicked", "group": "navigation@2", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.settingsButtonClicked", + "command": "roo-cline.mcpButtonClicked", "group": "navigation@3", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.accountButtonClicked", + "command": "roo-cline.marketplaceButtonClicked", "group": "navigation@4", "when": "view == roo-cline.SidebarProvider" }, { "command": "roo-cline.historyButtonClicked", - "group": "overflow@1", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.promptsButtonClicked", - "group": "overflow@2", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.mcpButtonClicked", - "group": "overflow@3", + "group": "navigation@5", "when": "view == roo-cline.SidebarProvider" }, { "command": "roo-cline.popoutButtonClicked", - "group": "overflow@4", + "group": "navigation@6", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.accountButtonClicked", + "group": "navigation@7", + "when": "view == roo-cline.SidebarProvider && config.roo-cline.rooCodeCloudEnabled" + }, + { + "command": "roo-cline.settingsButtonClicked", + "group": "navigation@8", "when": "view == roo-cline.SidebarProvider" } ], @@ -266,38 +262,33 @@ "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.marketplaceButtonClicked", + "command": "roo-cline.promptsButtonClicked", "group": "navigation@2", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.settingsButtonClicked", + "command": "roo-cline.mcpButtonClicked", "group": "navigation@3", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.accountButtonClicked", + "command": "roo-cline.marketplaceButtonClicked", "group": "navigation@4", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { "command": "roo-cline.historyButtonClicked", - "group": "overflow@1", + "group": "navigation@5", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.promptsButtonClicked", - "group": "overflow@2", - "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" + "command": "roo-cline.accountButtonClicked", + "group": "navigation@6", + "when": "activeWebviewPanelId == roo-cline.TabPanelProvider && config.roo-cline.rooCodeCloudEnabled" }, { - "command": "roo-cline.mcpButtonClicked", - "group": "overflow@3", - "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" - }, - { - "command": "roo-cline.popoutButtonClicked", - "group": "overflow@4", + "command": "roo-cline.settingsButtonClicked", + "group": "navigation@7", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } ] @@ -330,34 +321,6 @@ ], "description": "%commands.allowedCommands.description%" }, - "roo-cline.deniedCommands": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "description": "%commands.deniedCommands.description%" - }, - "roo-cline.commandExecutionTimeout": { - "type": "number", - "default": 0, - "minimum": 0, - "maximum": 600, - "description": "%commands.commandExecutionTimeout.description%" - }, - "roo-cline.commandTimeoutAllowlist": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "description": "%commands.commandTimeoutAllowlist.description%" - }, - "roo-cline.preventCompletionWithOpenTodos": { - "type": "boolean", - "default": false, - "description": "%commands.preventCompletionWithOpenTodos.description%" - }, "roo-cline.vsCodeLmModelSelector": { "type": "object", "properties": { @@ -377,20 +340,10 @@ "default": "", "description": "%settings.customStoragePath.description%" }, - "roo-cline.enableCodeActions": { + "roo-cline.rooCodeCloudEnabled": { "type": "boolean", - "default": true, - "description": "%settings.enableCodeActions.description%" - }, - "roo-cline.autoImportSettingsPath": { - "type": "string", - "default": "", - "description": "%settings.autoImportSettingsPath.description%" - }, - "roo-cline.useAgentRules": { - "type": "boolean", - "default": true, - "description": "%settings.useAgentRules.description%" + "default": false, + "description": "%settings.rooCodeCloudEnabled.description%" } } } @@ -406,7 +359,7 @@ "vsix": "mkdirp ../bin && vsce package --no-dependencies --out ../bin", "publish:marketplace": "vsce publish --no-dependencies && ovsx publish --no-dependencies", "watch:bundle": "pnpm bundle --watch", - "watch:tsc": "cd .. && tsc --noEmit --watch --project src/tsconfig.json", + "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "clean": "rimraf README.md CHANGELOG.md LICENSE dist mock .turbo" }, "dependencies": { @@ -457,17 +410,16 @@ "pdf-parse": "^1.1.1", "pkce-challenge": "^5.0.0", "pretty-bytes": "^7.0.0", - "proper-lockfile": "^4.1.2", "ps-tree": "^1.2.0", "puppeteer-chromium-resolver": "^24.0.0", "puppeteer-core": "^23.4.0", "reconnecting-eventsource": "^1.6.4", "sanitize-filename": "^1.6.3", + "sax": "^1.4.1", "say": "^0.16.0", "serialize-error": "^12.0.0", "simple-git": "^3.27.0", "sound-play": "^1.1.0", - "stream-json": "^1.8.0", "string-similarity": "^4.0.4", "strip-ansi": "^7.1.0", "strip-bom": "^5.0.0", @@ -495,9 +447,8 @@ "@types/node": "20.x", "@types/node-cache": "^4.1.3", "@types/node-ipc": "^9.2.3", - "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", - "@types/stream-json": "^1.7.8", + "@types/sax": "^1.2.7", "@types/string-similarity": "^4.0.2", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.5", diff --git a/src/package.nls.ca.json b/src/package.nls.ca.json index 54a0ed4923..a9c3a93dad 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Obrir en una Nova Pestanya", "command.focusInput.title": "Enfocar Camp d'Entrada", "command.setCustomStoragePath.title": "Establir Ruta d'Emmagatzematge Personalitzada", - "command.importSettings.title": "Importar Configuració", "command.terminal.addToContext.title": "Afegir Contingut del Terminal al Context", "command.terminal.fixCommand.title": "Corregir Aquesta Ordre", "command.terminal.explainCommand.title": "Explicar Aquesta Ordre", @@ -27,14 +26,9 @@ "command.documentation.title": "Documentació", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Ordres que es poden executar automàticament quan 'Aprova sempre les operacions d'execució' està activat", - "commands.deniedCommands.description": "Prefixos d'ordres que seran automàticament denegats sense demanar aprovació. En cas de conflictes amb ordres permeses, la coincidència de prefix més llarga té prioritat. Afegeix * per denegar totes les ordres.", - "commands.commandExecutionTimeout.description": "Temps màxim en segons per esperar que l'execució de l'ordre es completi abans d'esgotar el temps (0 = sense temps límit, 1-600s, per defecte: 0s)", - "commands.commandTimeoutAllowlist.description": "Prefixos d'ordres que estan exclosos del temps límit d'execució d'ordres. Les ordres que coincideixin amb aquests prefixos s'executaran sense restriccions de temps límit.", "settings.vsCodeLmModelSelector.description": "Configuració per a l'API del model de llenguatge VSCode", "settings.vsCodeLmModelSelector.vendor.description": "El proveïdor del model de llenguatge (p. ex. copilot)", "settings.vsCodeLmModelSelector.family.description": "La família del model de llenguatge (p. ex. gpt-4)", "settings.customStoragePath.description": "Ruta d'emmagatzematge personalitzada. Deixeu-la buida per utilitzar la ubicació predeterminada. Admet rutes absolutes (p. ex. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Habilitar correccions ràpides de Roo Code.", - "settings.autoImportSettingsPath.description": "Ruta a un fitxer de configuració de RooCode per importar automàticament en iniciar l'extensió. Admet rutes absolutes i rutes relatives al directori d'inici (per exemple, '~/Documents/roo-code-settings.json'). Deixeu-ho en blanc per desactivar la importació automàtica.", - "settings.useAgentRules.description": "Activa la càrrega de fitxers AGENTS.md per a regles específiques de l'agent (vegeu https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud." } diff --git a/src/package.nls.de.json b/src/package.nls.de.json index 4630154093..e9496d7ede 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "In Neuem Tab Öffnen", "command.focusInput.title": "Eingabefeld Fokussieren", "command.setCustomStoragePath.title": "Benutzerdefinierten Speicherpfad Festlegen", - "command.importSettings.title": "Einstellungen Importieren", "command.terminal.addToContext.title": "Terminal-Inhalt zum Kontext Hinzufügen", "command.terminal.fixCommand.title": "Diesen Befehl Reparieren", "command.terminal.explainCommand.title": "Diesen Befehl Erklären", @@ -27,14 +26,9 @@ "command.documentation.title": "Dokumentation", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Befehle, die automatisch ausgeführt werden können, wenn 'Ausführungsoperationen immer genehmigen' aktiviert ist", - "commands.deniedCommands.description": "Befehlspräfixe, die automatisch abgelehnt werden, ohne nach Genehmigung zu fragen. Bei Konflikten mit erlaubten Befehlen hat die längste Präfix-Übereinstimmung Vorrang. Füge * hinzu, um alle Befehle abzulehnen.", - "commands.commandExecutionTimeout.description": "Maximale Zeit in Sekunden, die auf den Abschluss der Befehlsausführung gewartet wird, bevor ein Timeout auftritt (0 = kein Timeout, 1-600s, Standard: 0s)", - "commands.commandTimeoutAllowlist.description": "Befehlspräfixe, die vom Timeout der Befehlsausführung ausgeschlossen sind. Befehle, die diesen Präfixen entsprechen, werden ohne Timeout-Beschränkungen ausgeführt.", "settings.vsCodeLmModelSelector.description": "Einstellungen für die VSCode-Sprachmodell-API", "settings.vsCodeLmModelSelector.vendor.description": "Der Anbieter des Sprachmodells (z.B. copilot)", "settings.vsCodeLmModelSelector.family.description": "Die Familie des Sprachmodells (z.B. gpt-4)", "settings.customStoragePath.description": "Benutzerdefinierter Speicherpfad. Leer lassen, um den Standardspeicherort zu verwenden. Unterstützt absolute Pfade (z.B. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Code Schnelle Problembehebung aktivieren.", - "settings.autoImportSettingsPath.description": "Pfad zu einer RooCode-Konfigurationsdatei, die beim Start der Erweiterung automatisch importiert wird. Unterstützt absolute Pfade und Pfade relativ zum Home-Verzeichnis (z.B. '~/Documents/roo-code-settings.json'). Leer lassen, um den automatischen Import zu deaktivieren.", - "settings.useAgentRules.description": "Aktiviert das Laden von AGENTS.md-Dateien für agentenspezifische Regeln (siehe https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Aktiviere Roo Code Cloud." } diff --git a/src/package.nls.es.json b/src/package.nls.es.json index d162a96e56..1b3e09c17b 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Abrir en Nueva Pestaña", "command.focusInput.title": "Enfocar Campo de Entrada", "command.setCustomStoragePath.title": "Establecer Ruta de Almacenamiento Personalizada", - "command.importSettings.title": "Importar Configuración", "command.terminal.addToContext.title": "Añadir Contenido de Terminal al Contexto", "command.terminal.fixCommand.title": "Corregir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", @@ -27,14 +26,9 @@ "command.documentation.title": "Documentación", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Comandos que pueden ejecutarse automáticamente cuando 'Aprobar siempre operaciones de ejecución' está activado", - "commands.deniedCommands.description": "Prefijos de comandos que serán automáticamente denegados sin solicitar aprobación. En caso de conflictos con comandos permitidos, la coincidencia de prefijo más larga tiene prioridad. Añade * para denegar todos los comandos.", - "commands.commandExecutionTimeout.description": "Tiempo máximo en segundos para esperar que se complete la ejecución del comando antes de que expire (0 = sin tiempo límite, 1-600s, predeterminado: 0s)", - "commands.commandTimeoutAllowlist.description": "Prefijos de comandos que están excluidos del tiempo límite de ejecución de comandos. Los comandos que coincidan con estos prefijos se ejecutarán sin restricciones de tiempo límite.", "settings.vsCodeLmModelSelector.description": "Configuración para la API del modelo de lenguaje VSCode", "settings.vsCodeLmModelSelector.vendor.description": "El proveedor del modelo de lenguaje (ej. copilot)", "settings.vsCodeLmModelSelector.family.description": "La familia del modelo de lenguaje (ej. gpt-4)", "settings.customStoragePath.description": "Ruta de almacenamiento personalizada. Dejar vacío para usar la ubicación predeterminada. Admite rutas absolutas (ej. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Habilitar correcciones rápidas de Roo Code.", - "settings.autoImportSettingsPath.description": "Ruta a un archivo de configuración de RooCode para importar automáticamente al iniciar la extensión. Admite rutas absolutas y rutas relativas al directorio de inicio (por ejemplo, '~/Documents/roo-code-settings.json'). Dejar vacío para desactivar la importación automática.", - "settings.useAgentRules.description": "Habilita la carga de archivos AGENTS.md para reglas específicas del agente (ver https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud." } diff --git a/src/package.nls.fr.json b/src/package.nls.fr.json index ad4a4b7771..0782ecab05 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Ouvrir dans un Nouvel Onglet", "command.focusInput.title": "Focus sur le Champ de Saisie", "command.setCustomStoragePath.title": "Définir le Chemin de Stockage Personnalisé", - "command.importSettings.title": "Importer les Paramètres", "command.terminal.addToContext.title": "Ajouter le Contenu du Terminal au Contexte", "command.terminal.fixCommand.title": "Corriger cette Commande", "command.terminal.explainCommand.title": "Expliquer cette Commande", @@ -27,14 +26,9 @@ "command.documentation.title": "Documentation", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Commandes pouvant être exécutées automatiquement lorsque 'Toujours approuver les opérations d'exécution' est activé", - "commands.deniedCommands.description": "Préfixes de commandes qui seront automatiquement refusés sans demander d'approbation. En cas de conflit avec les commandes autorisées, la correspondance de préfixe la plus longue a la priorité. Ajouter * pour refuser toutes les commandes.", - "commands.commandExecutionTimeout.description": "Temps maximum en secondes pour attendre que l'exécution de la commande se termine avant expiration (0 = pas de délai, 1-600s, défaut : 0s)", - "commands.commandTimeoutAllowlist.description": "Préfixes de commandes qui sont exclus du délai d'exécution des commandes. Les commandes correspondant à ces préfixes s'exécuteront sans restrictions de délai.", "settings.vsCodeLmModelSelector.description": "Paramètres pour l'API du modèle de langage VSCode", "settings.vsCodeLmModelSelector.vendor.description": "Le fournisseur du modèle de langage (ex: copilot)", "settings.vsCodeLmModelSelector.family.description": "La famille du modèle de langage (ex: gpt-4)", "settings.customStoragePath.description": "Chemin de stockage personnalisé. Laisser vide pour utiliser l'emplacement par défaut. Prend en charge les chemins absolus (ex: 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Activer les correctifs rapides de Roo Code.", - "settings.autoImportSettingsPath.description": "Chemin d'accès à un fichier de configuration RooCode à importer automatiquement au démarrage de l'extension. Prend en charge les chemins absolus et les chemins relatifs au répertoire de base (par exemple, '~/Documents/roo-code-settings.json'). Laisser vide pour désactiver l'importation automatique.", - "settings.useAgentRules.description": "Activer le chargement des fichiers AGENTS.md pour les règles spécifiques à l'agent (voir https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Activer Roo Code Cloud." } diff --git a/src/package.nls.hi.json b/src/package.nls.hi.json index 5f5cbba42c..a1855f4cb6 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "नए टैब में खोलें", "command.focusInput.title": "इनपुट फ़ील्ड पर फोकस करें", "command.setCustomStoragePath.title": "कस्टम स्टोरेज पाथ सेट करें", - "command.importSettings.title": "सेटिंग्स इम्पोर्ट करें", "command.terminal.addToContext.title": "टर्मिनल सामग्री को संदर्भ में जोड़ें", "command.terminal.fixCommand.title": "यह कमांड ठीक करें", "command.terminal.explainCommand.title": "यह कमांड समझाएं", @@ -27,14 +26,9 @@ "command.documentation.title": "दस्तावेज़ीकरण", "configuration.title": "Roo Code", "commands.allowedCommands.description": "वे कमांड जो स्वचालित रूप से निष्पादित की जा सकती हैं जब 'हमेशा निष्पादन संचालन को स्वीकृत करें' सक्रिय हो", - "commands.deniedCommands.description": "कमांड प्रीफिक्स जो स्वचालित रूप से अस्वीकार कर दिए जाएंगे बिना अनुमोदन मांगे। अनुमतित कमांड के साथ संघर्ष की स्थिति में, सबसे लंबा प्रीफिक्स मैच प्राथमिकता लेता है। सभी कमांड को अस्वीकार करने के लिए * जोड़ें।", - "commands.commandExecutionTimeout.description": "कमांड निष्पादन पूरा होने का इंतजार करने के लिए अधिकतम समय सेकंड में, समय समाप्त होने से पहले (0 = कोई समय सीमा नहीं, 1-600s, डिफ़ॉल्ट: 0s)", - "commands.commandTimeoutAllowlist.description": "कमांड प्रीफिक्स जो कमांड निष्पादन टाइमआउट से बाहर रखे गए हैं। इन प्रीफिक्स से मेल खाने वाले कमांड बिना टाइमआउट प्रतिबंधों के चलेंगे।", "settings.vsCodeLmModelSelector.description": "VSCode भाषा मॉडल API के लिए सेटिंग्स", "settings.vsCodeLmModelSelector.vendor.description": "भाषा मॉडल का विक्रेता (उदा. copilot)", "settings.vsCodeLmModelSelector.family.description": "भाषा मॉडल का परिवार (उदा. gpt-4)", "settings.customStoragePath.description": "कस्टम स्टोरेज पाथ। डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ें। पूर्ण पथ का समर्थन करता है (उदा. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Code त्वरित सुधार सक्षम करें", - "settings.autoImportSettingsPath.description": "RooCode कॉन्फ़िगरेशन फ़ाइल का पथ जिसे एक्सटेंशन स्टार्टअप पर स्वचालित रूप से आयात किया जाएगा। होम डायरेक्टरी के सापेक्ष पूर्ण पथ और पथों का समर्थन करता है (उदाहरण के लिए '~/Documents/roo-code-settings.json')। ऑटो-इंपोर्ट को अक्षम करने के लिए खाली छोड़ दें।", - "settings.useAgentRules.description": "एजेंट-विशिष्ट नियमों के लिए AGENTS.md फ़ाइलों को लोड करना सक्षम करें (देखें https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Roo Code Cloud सक्षम करें।" } diff --git a/src/package.nls.id.json b/src/package.nls.id.json index 5a6ba9706c..56685d86a5 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -20,21 +20,15 @@ "command.addToContext.title": "Tambahkan ke Konteks", "command.focusInput.title": "Fokus ke Field Input", "command.setCustomStoragePath.title": "Atur Path Penyimpanan Kustom", - "command.importSettings.title": "Impor Pengaturan", "command.terminal.addToContext.title": "Tambahkan Konten Terminal ke Konteks", "command.terminal.fixCommand.title": "Perbaiki Perintah Ini", "command.terminal.explainCommand.title": "Jelaskan Perintah Ini", "command.acceptInput.title": "Terima Input/Saran", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Perintah yang dapat dijalankan secara otomatis ketika 'Selalu setujui operasi eksekusi' diaktifkan", - "commands.deniedCommands.description": "Awalan perintah yang akan otomatis ditolak tanpa meminta persetujuan. Jika terjadi konflik dengan perintah yang diizinkan, pencocokan awalan terpanjang akan diprioritaskan. Tambahkan * untuk menolak semua perintah.", - "commands.commandExecutionTimeout.description": "Waktu maksimum dalam detik untuk menunggu eksekusi perintah selesai sebelum timeout (0 = tanpa timeout, 1-600s, default: 0s)", - "commands.commandTimeoutAllowlist.description": "Awalan perintah yang dikecualikan dari timeout eksekusi perintah. Perintah yang cocok dengan awalan ini akan berjalan tanpa batasan timeout.", "settings.vsCodeLmModelSelector.description": "Pengaturan untuk API Model Bahasa VSCode", "settings.vsCodeLmModelSelector.vendor.description": "Vendor dari model bahasa (misalnya copilot)", "settings.vsCodeLmModelSelector.family.description": "Keluarga dari model bahasa (misalnya gpt-4)", "settings.customStoragePath.description": "Path penyimpanan kustom. Biarkan kosong untuk menggunakan lokasi default. Mendukung path absolut (misalnya 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Aktifkan perbaikan cepat Roo Code.", - "settings.autoImportSettingsPath.description": "Path ke file konfigurasi RooCode untuk diimpor secara otomatis saat ekstensi dimulai. Mendukung path absolut dan path relatif terhadap direktori home (misalnya '~/Documents/roo-code-settings.json'). Biarkan kosong untuk menonaktifkan impor otomatis.", - "settings.useAgentRules.description": "Aktifkan pemuatan file AGENTS.md untuk aturan khusus agen (lihat https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Aktifkan Roo Code Cloud." } diff --git a/src/package.nls.it.json b/src/package.nls.it.json index c77f1f9282..0d491db802 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Apri in Nuova Scheda", "command.focusInput.title": "Focalizza Campo di Input", "command.setCustomStoragePath.title": "Imposta Percorso di Archiviazione Personalizzato", - "command.importSettings.title": "Importa Impostazioni", "command.terminal.addToContext.title": "Aggiungi Contenuto del Terminale al Contesto", "command.terminal.fixCommand.title": "Correggi Questo Comando", "command.terminal.explainCommand.title": "Spiega Questo Comando", @@ -27,14 +26,9 @@ "command.documentation.title": "Documentazione", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Comandi che possono essere eseguiti automaticamente quando 'Approva sempre le operazioni di esecuzione' è attivato", - "commands.deniedCommands.description": "Prefissi di comandi che verranno automaticamente rifiutati senza richiedere approvazione. In caso di conflitti con comandi consentiti, la corrispondenza del prefisso più lungo ha la precedenza. Aggiungi * per rifiutare tutti i comandi.", - "commands.commandExecutionTimeout.description": "Tempo massimo in secondi per attendere il completamento dell'esecuzione del comando prima del timeout (0 = nessun timeout, 1-600s, predefinito: 0s)", - "commands.commandTimeoutAllowlist.description": "Prefissi di comandi che sono esclusi dal timeout di esecuzione dei comandi. I comandi che corrispondono a questi prefissi verranno eseguiti senza restrizioni di timeout.", "settings.vsCodeLmModelSelector.description": "Impostazioni per l'API del modello linguistico VSCode", "settings.vsCodeLmModelSelector.vendor.description": "Il fornitore del modello linguistico (es. copilot)", "settings.vsCodeLmModelSelector.family.description": "La famiglia del modello linguistico (es. gpt-4)", "settings.customStoragePath.description": "Percorso di archiviazione personalizzato. Lasciare vuoto per utilizzare la posizione predefinita. Supporta percorsi assoluti (es. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Abilita correzioni rapide di Roo Code.", - "settings.autoImportSettingsPath.description": "Percorso di un file di configurazione di RooCode da importare automaticamente all'avvio dell'estensione. Supporta percorsi assoluti e percorsi relativi alla directory home (ad es. '~/Documents/roo-code-settings.json'). Lasciare vuoto per disabilitare l'importazione automatica.", - "settings.useAgentRules.description": "Abilita il caricamento dei file AGENTS.md per regole specifiche dell'agente (vedi https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Abilita Roo Code Cloud." } diff --git a/src/package.nls.ja.json b/src/package.nls.ja.json index aea89f5ff7..0f8949b1f7 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -20,21 +20,15 @@ "command.addToContext.title": "コンテキストに追加", "command.focusInput.title": "入力フィールドにフォーカス", "command.setCustomStoragePath.title": "カスタムストレージパスの設定", - "command.importSettings.title": "設定をインポート", "command.terminal.addToContext.title": "ターミナルの内容をコンテキストに追加", "command.terminal.fixCommand.title": "このコマンドを修正", "command.terminal.explainCommand.title": "このコマンドを説明", "command.acceptInput.title": "入力/提案を承認", "configuration.title": "Roo Code", "commands.allowedCommands.description": "'常に実行操作を承認する'が有効な場合に自動実行できるコマンド", - "commands.deniedCommands.description": "承認を求めずに自動的に拒否されるコマンドプレフィックス。許可されたコマンドとの競合がある場合、最長プレフィックスマッチが優先されます。すべてのコマンドを拒否するには * を追加してください。", - "commands.commandExecutionTimeout.description": "コマンド実行の完了を待つ最大時間(秒)、タイムアウトまで(0 = タイムアウトなし、1-600秒、デフォルト: 0秒)", - "commands.commandTimeoutAllowlist.description": "コマンド実行タイムアウトから除外されるコマンドプレフィックス。これらのプレフィックスに一致するコマンドは、タイムアウト制限なしで実行されます。", "settings.vsCodeLmModelSelector.description": "VSCode 言語モデル API の設定", "settings.vsCodeLmModelSelector.vendor.description": "言語モデルのベンダー(例:copilot)", "settings.vsCodeLmModelSelector.family.description": "言語モデルのファミリー(例:gpt-4)", "settings.customStoragePath.description": "カスタムストレージパス。デフォルトの場所を使用する場合は空のままにします。絶対パスをサポートします(例:'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Codeのクイック修正を有効にする。", - "settings.autoImportSettingsPath.description": "拡張機能の起動時に自動的にインポートするRooCode設定ファイルへのパス。絶対パスとホームディレクトリからの相対パスをサポートします(例:'~/Documents/roo-code-settings.json')。自動インポートを無効にするには、空のままにします。", - "settings.useAgentRules.description": "エージェント固有のルールのためにAGENTS.mdファイルの読み込みを有効にします(参照:https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Roo Code Cloud を有効にする。" } diff --git a/src/package.nls.json b/src/package.nls.json index 1285a2367f..b05dac3b36 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -20,22 +20,15 @@ "command.addToContext.title": "Add To Context", "command.focusInput.title": "Focus Input Field", "command.setCustomStoragePath.title": "Set Custom Storage Path", - "command.importSettings.title": "Import Settings", "command.terminal.addToContext.title": "Add Terminal Content to Context", "command.terminal.fixCommand.title": "Fix This Command", "command.terminal.explainCommand.title": "Explain This Command", "command.acceptInput.title": "Accept Input/Suggestion", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled", - "commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.", - "commands.commandExecutionTimeout.description": "Maximum time in seconds to wait for command execution to complete before timing out (0 = no timeout, 1-600s, default: 0s)", - "commands.commandTimeoutAllowlist.description": "Command prefixes that are excluded from the command execution timeout. Commands matching these prefixes will run without timeout restrictions.", - "commands.preventCompletionWithOpenTodos.description": "Prevent task completion when there are incomplete todos in the todo list", "settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API", "settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)", "settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)", "settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Enable Roo Code quick fixes", - "settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.", - "settings.useAgentRules.description": "Enable loading of AGENTS.md files for agent-specific rules (see https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud." } diff --git a/src/package.nls.ko.json b/src/package.nls.ko.json index 83c63b2b68..beddd14f83 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "새 탭에서 열기", "command.focusInput.title": "입력 필드 포커스", "command.setCustomStoragePath.title": "사용자 지정 저장소 경로 설정", - "command.importSettings.title": "설정 가져오기", "command.terminal.addToContext.title": "터미널 내용을 컨텍스트에 추가", "command.terminal.fixCommand.title": "이 명령어 수정", "command.terminal.explainCommand.title": "이 명령어 설명", @@ -27,14 +26,9 @@ "command.documentation.title": "문서", "configuration.title": "Roo Code", "commands.allowedCommands.description": "'항상 실행 작업 승인' 이 활성화되어 있을 때 자동으로 실행할 수 있는 명령어", - "commands.deniedCommands.description": "승인을 요청하지 않고 자동으로 거부될 명령어 접두사. 허용된 명령어와 충돌하는 경우 가장 긴 접두사 일치가 우선됩니다. 모든 명령어를 거부하려면 *를 추가하세요.", - "commands.commandExecutionTimeout.description": "명령어 실행이 완료되기를 기다리는 최대 시간(초), 타임아웃 전까지 (0 = 타임아웃 없음, 1-600초, 기본값: 0초)", - "commands.commandTimeoutAllowlist.description": "명령어 실행 타임아웃에서 제외되는 명령어 접두사. 이러한 접두사와 일치하는 명령어는 타임아웃 제한 없이 실행됩니다.", "settings.vsCodeLmModelSelector.description": "VSCode 언어 모델 API 설정", "settings.vsCodeLmModelSelector.vendor.description": "언어 모델 공급자 (예: copilot)", "settings.vsCodeLmModelSelector.family.description": "언어 모델 계열 (예: gpt-4)", "settings.customStoragePath.description": "사용자 지정 저장소 경로. 기본 위치를 사용하려면 비워두세요. 절대 경로를 지원합니다 (예: 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Code 빠른 수정 사용 설정", - "settings.autoImportSettingsPath.description": "확장 프로그램 시작 시 자동으로 가져올 RooCode 구성 파일의 경로입니다. 절대 경로 및 홈 디렉토리에 대한 상대 경로를 지원합니다(예: '~/Documents/roo-code-settings.json'). 자동 가져오기를 비활성화하려면 비워 둡니다.", - "settings.useAgentRules.description": "에이전트별 규칙에 대한 AGENTS.md 파일 로드를 활성화합니다 (참조: https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Roo Code Cloud 사용 설정" } diff --git a/src/package.nls.nl.json b/src/package.nls.nl.json index d57da551d8..6ef27343c7 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -20,21 +20,15 @@ "command.addToContext.title": "Toevoegen aan Context", "command.focusInput.title": "Focus op Invoerveld", "command.setCustomStoragePath.title": "Aangepast Opslagpad Instellen", - "command.importSettings.title": "Instellingen Importeren", "command.terminal.addToContext.title": "Terminalinhoud aan Context Toevoegen", "command.terminal.fixCommand.title": "Repareer Dit Commando", "command.terminal.explainCommand.title": "Leg Dit Commando Uit", "command.acceptInput.title": "Invoer/Suggestie Accepteren", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Commando's die automatisch kunnen worden uitgevoerd wanneer 'Altijd goedkeuren uitvoerbewerkingen' is ingeschakeld", - "commands.deniedCommands.description": "Commando-prefixen die automatisch worden geweigerd zonder om goedkeuring te vragen. Bij conflicten met toegestane commando's heeft de langste prefix-match voorrang. Voeg * toe om alle commando's te weigeren.", - "commands.commandExecutionTimeout.description": "Maximale tijd in seconden om te wachten tot commando-uitvoering voltooid is voordat er een timeout optreedt (0 = geen timeout, 1-600s, standaard: 0s)", - "commands.commandTimeoutAllowlist.description": "Commando-prefixen die zijn uitgesloten van de commando-uitvoering timeout. Commando's die overeenkomen met deze prefixen worden uitgevoerd zonder timeout-beperkingen.", "settings.vsCodeLmModelSelector.description": "Instellingen voor VSCode Language Model API", "settings.vsCodeLmModelSelector.vendor.description": "De leverancier van het taalmodel (bijv. copilot)", "settings.vsCodeLmModelSelector.family.description": "De familie van het taalmodel (bijv. gpt-4)", "settings.customStoragePath.description": "Aangepast opslagpad. Laat leeg om de standaardlocatie te gebruiken. Ondersteunt absolute paden (bijv. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Snelle correcties van Roo Code inschakelen.", - "settings.autoImportSettingsPath.description": "Pad naar een RooCode-configuratiebestand om automatisch te importeren bij het opstarten van de extensie. Ondersteunt absolute paden en paden ten opzichte van de thuismap (bijv. '~/Documents/roo-code-settings.json'). Laat leeg om automatisch importeren uit te schakelen.", - "settings.useAgentRules.description": "Laden van AGENTS.md-bestanden voor agentspecifieke regels inschakelen (zie https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Roo Code Cloud inschakelen." } diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index 9157c74ede..1565299f43 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Otwórz w Nowej Karcie", "command.focusInput.title": "Fokus na Pole Wprowadzania", "command.setCustomStoragePath.title": "Ustaw Niestandardową Ścieżkę Przechowywania", - "command.importSettings.title": "Importuj Ustawienia", "command.terminal.addToContext.title": "Dodaj Zawartość Terminala do Kontekstu", "command.terminal.fixCommand.title": "Napraw tę Komendę", "command.terminal.explainCommand.title": "Wyjaśnij tę Komendę", @@ -27,14 +26,9 @@ "command.documentation.title": "Dokumentacja", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Polecenia, które mogą być wykonywane automatycznie, gdy włączona jest opcja 'Zawsze zatwierdzaj operacje wykonania'", - "commands.deniedCommands.description": "Prefiksy poleceń, które będą automatycznie odrzucane bez pytania o zatwierdzenie. W przypadku konfliktów z dozwolonymi poleceniami, najdłuższe dopasowanie prefiksu ma pierwszeństwo. Dodaj * aby odrzucić wszystkie polecenia.", - "commands.commandExecutionTimeout.description": "Maksymalny czas w sekundach oczekiwania na zakończenie wykonania polecenia przed przekroczeniem limitu czasu (0 = brak limitu czasu, 1-600s, domyślnie: 0s)", - "commands.commandTimeoutAllowlist.description": "Prefiksy poleceń, które są wykluczone z limitu czasu wykonania poleceń. Polecenia pasujące do tych prefiksów będą wykonywane bez ograniczeń czasowych.", "settings.vsCodeLmModelSelector.description": "Ustawienia dla API modelu językowego VSCode", "settings.vsCodeLmModelSelector.vendor.description": "Dostawca modelu językowego (np. copilot)", "settings.vsCodeLmModelSelector.family.description": "Rodzina modelu językowego (np. gpt-4)", "settings.customStoragePath.description": "Niestandardowa ścieżka przechowywania. Pozostaw puste, aby użyć domyślnej lokalizacji. Obsługuje ścieżki bezwzględne (np. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Włącz szybkie poprawki Roo Code.", - "settings.autoImportSettingsPath.description": "Ścieżka do pliku konfiguracyjnego RooCode, który ma być automatycznie importowany podczas uruchamiania rozszerzenia. Obsługuje ścieżki bezwzględne i ścieżki względne do katalogu domowego (np. '~/Documents/roo-code-settings.json'). Pozostaw puste, aby wyłączyć automatyczne importowanie.", - "settings.useAgentRules.description": "Włącz wczytywanie plików AGENTS.md dla reguł specyficznych dla agenta (zobacz https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Włącz Roo Code Cloud." } diff --git a/src/package.nls.pt-BR.json b/src/package.nls.pt-BR.json index e66199b039..ce21b7d7f6 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Abrir em Nova Aba", "command.focusInput.title": "Focar Campo de Entrada", "command.setCustomStoragePath.title": "Definir Caminho de Armazenamento Personalizado", - "command.importSettings.title": "Importar Configurações", "command.terminal.addToContext.title": "Adicionar Conteúdo do Terminal ao Contexto", "command.terminal.fixCommand.title": "Corrigir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", @@ -27,14 +26,9 @@ "command.documentation.title": "Documentação", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Comandos que podem ser executados automaticamente quando 'Sempre aprovar operações de execução' está ativado", - "commands.deniedCommands.description": "Prefixos de comandos que serão automaticamente negados sem solicitar aprovação. Em caso de conflitos com comandos permitidos, a correspondência de prefixo mais longa tem precedência. Adicione * para negar todos os comandos.", - "commands.commandExecutionTimeout.description": "Tempo máximo em segundos para aguardar a conclusão da execução do comando antes do timeout (0 = sem timeout, 1-600s, padrão: 0s)", - "commands.commandTimeoutAllowlist.description": "Prefixos de comandos que são excluídos do timeout de execução de comandos. Comandos que correspondem a esses prefixos serão executados sem restrições de timeout.", "settings.vsCodeLmModelSelector.description": "Configurações para a API do modelo de linguagem do VSCode", "settings.vsCodeLmModelSelector.vendor.description": "O fornecedor do modelo de linguagem (ex: copilot)", "settings.vsCodeLmModelSelector.family.description": "A família do modelo de linguagem (ex: gpt-4)", "settings.customStoragePath.description": "Caminho de armazenamento personalizado. Deixe vazio para usar o local padrão. Suporta caminhos absolutos (ex: 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Habilitar correções rápidas do Roo Code.", - "settings.autoImportSettingsPath.description": "Caminho para um arquivo de configuração do RooCode para importar automaticamente na inicialização da extensão. Suporta caminhos absolutos e caminhos relativos ao diretório inicial (por exemplo, '~/Documents/roo-code-settings.json'). Deixe em branco para desativar a importação automática.", - "settings.useAgentRules.description": "Habilita o carregamento de arquivos AGENTS.md para regras específicas do agente (consulte https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud." } diff --git a/src/package.nls.ru.json b/src/package.nls.ru.json index 443d6df3e0..5c2b6a030b 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -20,21 +20,15 @@ "command.addToContext.title": "Добавить в контекст", "command.focusInput.title": "Фокус на поле ввода", "command.setCustomStoragePath.title": "Указать путь хранения", - "command.importSettings.title": "Импортировать настройки", "command.terminal.addToContext.title": "Добавить содержимое терминала в контекст", "command.terminal.fixCommand.title": "Исправить эту команду", "command.terminal.explainCommand.title": "Объяснить эту команду", "command.acceptInput.title": "Принять ввод/предложение", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Команды, которые могут быть автоматически выполнены, когда включена опция 'Всегда подтверждать операции выполнения'", - "commands.deniedCommands.description": "Префиксы команд, которые будут автоматически отклонены без запроса подтверждения. В случае конфликтов с разрешенными командами приоритет имеет самое длинное совпадение префикса. Добавьте * чтобы отклонить все команды.", - "commands.commandExecutionTimeout.description": "Максимальное время в секундах для ожидания завершения выполнения команды до истечения времени ожидания (0 = без тайм-аута, 1-600с, по умолчанию: 0с)", - "commands.commandTimeoutAllowlist.description": "Префиксы команд, которые исключены из тайм-аута выполнения команд. Команды, соответствующие этим префиксам, будут выполняться без ограничений по времени.", "settings.vsCodeLmModelSelector.description": "Настройки для VSCode Language Model API", "settings.vsCodeLmModelSelector.vendor.description": "Поставщик языковой модели (например, copilot)", "settings.vsCodeLmModelSelector.family.description": "Семейство языковой модели (например, gpt-4)", "settings.customStoragePath.description": "Пользовательский путь хранения. Оставьте пустым для использования пути по умолчанию. Поддерживает абсолютные пути (например, 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Включить быстрые исправления Roo Code.", - "settings.autoImportSettingsPath.description": "Путь к файлу конфигурации RooCode для автоматического импорта при запуске расширения. Поддерживает абсолютные пути и пути относительно домашнего каталога (например, '~/Documents/roo-code-settings.json'). Оставьте пустым, чтобы отключить автоматический импорт.", - "settings.useAgentRules.description": "Включить загрузку файлов AGENTS.md для специфичных для агента правил (см. https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Включить Roo Code Cloud." } diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index 707335b17b..59d50324d6 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Yeni Sekmede Aç", "command.focusInput.title": "Giriş Alanına Odaklan", "command.setCustomStoragePath.title": "Özel Depolama Yolunu Ayarla", - "command.importSettings.title": "Ayarları İçe Aktar", "command.terminal.addToContext.title": "Terminal İçeriğini Bağlama Ekle", "command.terminal.fixCommand.title": "Bu Komutu Düzelt", "command.terminal.explainCommand.title": "Bu Komutu Açıkla", @@ -27,14 +26,9 @@ "command.documentation.title": "Dokümantasyon", "configuration.title": "Roo Code", "commands.allowedCommands.description": "'Her zaman yürütme işlemlerini onayla' etkinleştirildiğinde otomatik olarak yürütülebilen komutlar", - "commands.deniedCommands.description": "Onay istenmeden otomatik olarak reddedilecek komut önekleri. İzin verilen komutlarla çakışma durumunda en uzun önek eşleşmesi öncelik alır. Tüm komutları reddetmek için * ekleyin.", - "commands.commandExecutionTimeout.description": "Komut yürütmesinin tamamlanmasını beklemek için maksimum süre (saniye), zaman aşımından önce (0 = zaman aşımı yok, 1-600s, varsayılan: 0s)", - "commands.commandTimeoutAllowlist.description": "Komut yürütme zaman aşımından hariç tutulan komut önekleri. Bu öneklerle eşleşen komutlar zaman aşımı kısıtlamaları olmadan çalışacaktır.", "settings.vsCodeLmModelSelector.description": "VSCode dil modeli API'si için ayarlar", "settings.vsCodeLmModelSelector.vendor.description": "Dil modelinin sağlayıcısı (örn: copilot)", "settings.vsCodeLmModelSelector.family.description": "Dil modelinin ailesi (örn: gpt-4)", "settings.customStoragePath.description": "Özel depolama yolu. Varsayılan konumu kullanmak için boş bırakın. Mutlak yolları destekler (örn: 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Code hızlı düzeltmeleri etkinleştir.", - "settings.autoImportSettingsPath.description": "Uzantı başlangıcında otomatik olarak içe aktarılacak bir RooCode yapılandırma dosyasının yolu. Mutlak yolları ve ana dizine göreli yolları destekler (ör. '~/Documents/roo-code-settings.json'). Otomatik içe aktarmayı devre dışı bırakmak için boş bırakın.", - "settings.useAgentRules.description": "Aracıya özgü kurallar için AGENTS.md dosyalarının yüklenmesini etkinleştirin (bkz. https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Roo Code Cloud'u Etkinleştir." } diff --git a/src/package.nls.vi.json b/src/package.nls.vi.json index 2de4bfa140..33f54ebe5c 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "Mở trong Tab Mới", "command.focusInput.title": "Tập Trung vào Trường Nhập", "command.setCustomStoragePath.title": "Đặt Đường Dẫn Lưu Trữ Tùy Chỉnh", - "command.importSettings.title": "Nhập Cài Đặt", "command.terminal.addToContext.title": "Thêm Nội Dung Terminal vào Ngữ Cảnh", "command.terminal.fixCommand.title": "Sửa Lệnh Này", "command.terminal.explainCommand.title": "Giải Thích Lệnh Này", @@ -27,14 +26,9 @@ "command.documentation.title": "Tài Liệu", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Các lệnh có thể được thực thi tự động khi 'Luôn phê duyệt các thao tác thực thi' được bật", - "commands.deniedCommands.description": "Các tiền tố lệnh sẽ được tự động từ chối mà không yêu cầu phê duyệt. Trong trường hợp xung đột với các lệnh được phép, việc khớp tiền tố dài nhất sẽ được ưu tiên. Thêm * để từ chối tất cả các lệnh.", - "commands.commandExecutionTimeout.description": "Thời gian tối đa tính bằng giây để chờ việc thực thi lệnh hoàn thành trước khi hết thời gian chờ (0 = không có thời gian chờ, 1-600s, mặc định: 0s)", - "commands.commandTimeoutAllowlist.description": "Các tiền tố lệnh được loại trừ khỏi thời gian chờ thực thi lệnh. Các lệnh khớp với những tiền tố này sẽ chạy mà không có giới hạn thời gian chờ.", "settings.vsCodeLmModelSelector.description": "Cài đặt cho API mô hình ngôn ngữ VSCode", "settings.vsCodeLmModelSelector.vendor.description": "Nhà cung cấp mô hình ngôn ngữ (ví dụ: copilot)", "settings.vsCodeLmModelSelector.family.description": "Họ mô hình ngôn ngữ (ví dụ: gpt-4)", "settings.customStoragePath.description": "Đường dẫn lưu trữ tùy chỉnh. Để trống để sử dụng vị trí mặc định. Hỗ trợ đường dẫn tuyệt đối (ví dụ: 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Bật sửa lỗi nhanh Roo Code.", - "settings.autoImportSettingsPath.description": "Đường dẫn đến tệp cấu hình RooCode để tự động nhập khi khởi động tiện ích mở rộng. Hỗ trợ đường dẫn tuyệt đối và đường dẫn tương đối đến thư mục chính (ví dụ: '~/Documents/roo-code-settings.json'). Để trống để tắt tính năng tự động nhập.", - "settings.useAgentRules.description": "Bật tải tệp AGENTS.md cho các quy tắc dành riêng cho tác nhân (xem https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "Bật Roo Code Cloud." } diff --git a/src/package.nls.zh-CN.json b/src/package.nls.zh-CN.json index 8fccecb351..ad10328e20 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "在新标签页中打开", "command.focusInput.title": "聚焦输入框", "command.setCustomStoragePath.title": "设置自定义存储路径", - "command.importSettings.title": "导入设置", "command.terminal.addToContext.title": "将终端内容添加到上下文", "command.terminal.fixCommand.title": "修复此命令", "command.terminal.explainCommand.title": "解释此命令", @@ -27,14 +26,9 @@ "command.documentation.title": "文档", "configuration.title": "Roo Code", "commands.allowedCommands.description": "当启用'始终批准执行操作'时可以自动执行的命令", - "commands.deniedCommands.description": "将自动拒绝而无需请求批准的命令前缀。与允许命令冲突时,最长前缀匹配优先。添加 * 拒绝所有命令。", - "commands.commandExecutionTimeout.description": "等待命令执行完成的最大时间(秒),超时前(0 = 无超时,1-600秒,默认:0秒)", - "commands.commandTimeoutAllowlist.description": "从命令执行超时中排除的命令前缀。匹配这些前缀的命令将在没有超时限制的情况下运行。", "settings.vsCodeLmModelSelector.description": "VSCode 语言模型 API 的设置", "settings.vsCodeLmModelSelector.vendor.description": "语言模型的供应商(例如:copilot)", "settings.vsCodeLmModelSelector.family.description": "语言模型的系列(例如:gpt-4)", "settings.customStoragePath.description": "自定义存储路径。留空以使用默认位置。支持绝对路径(例如:'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "启用 Roo Code 快速修复", - "settings.autoImportSettingsPath.description": "RooCode 配置文件的路径,用于在扩展启动时自动导入。支持绝对路径和相对于主目录的路径(例如 '~/Documents/roo-code-settings.json')。留空以禁用自动导入。", - "settings.useAgentRules.description": "为特定于代理的规则启用 AGENTS.md 文件的加载(请参阅 https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "启用 Roo Code Cloud。" } diff --git a/src/package.nls.zh-TW.json b/src/package.nls.zh-TW.json index e3a6253d75..b903fc6859 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -9,7 +9,6 @@ "command.openInNewTab.title": "在新分頁中開啟", "command.focusInput.title": "聚焦輸入框", "command.setCustomStoragePath.title": "設定自訂儲存路徑", - "command.importSettings.title": "匯入設定", "command.terminal.addToContext.title": "將終端內容新增到上下文", "command.terminal.fixCommand.title": "修復此命令", "command.terminal.explainCommand.title": "解釋此命令", @@ -27,14 +26,9 @@ "command.documentation.title": "文件", "configuration.title": "Roo Code", "commands.allowedCommands.description": "當啟用'始終批准執行操作'時可以自動執行的命令", - "commands.deniedCommands.description": "將自動拒絕而無需請求批准的命令前綴。與允許命令衝突時,最長前綴匹配優先。新增 * 拒絕所有命令。", - "commands.commandExecutionTimeout.description": "等待命令執行完成的最大時間(秒),逾時前(0 = 無逾時,1-600秒,預設:0秒)", - "commands.commandTimeoutAllowlist.description": "從命令執行逾時中排除的命令前綴。符合這些前綴的命令將在沒有逾時限制的情況下執行。", "settings.vsCodeLmModelSelector.description": "VSCode 語言模型 API 的設定", "settings.vsCodeLmModelSelector.vendor.description": "語言模型供應商(例如:copilot)", "settings.vsCodeLmModelSelector.family.description": "語言模型系列(例如:gpt-4)", "settings.customStoragePath.description": "自訂儲存路徑。留空以使用預設位置。支援絕對路徑(例如:'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "啟用 Roo Code 快速修復。", - "settings.autoImportSettingsPath.description": "RooCode 設定檔案的路徑,用於在擴充功能啟動時自動匯入。支援絕對路徑和相對於主目錄的路徑(例如 '~/Documents/roo-code-settings.json')。留空以停用自動匯入。", - "settings.useAgentRules.description": "為特定於代理的規則啟用 AGENTS.md 檔案的載入(請參閱 https://agent-rules.org/)" + "settings.rooCodeCloudEnabled.description": "啟用 Roo Code Cloud。" } diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 75b432f01d..6e159dbd5f 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -24,7 +24,6 @@ export class BrowserSession { private page?: Page private currentMousePosition?: string private lastConnectionAttempt?: number - private isUsingRemoteBrowser: boolean = false constructor(context: vscode.ExtensionContext) { this.context = context @@ -74,7 +73,6 @@ export class BrowserSession { defaultViewport: this.getViewport(), // headless: false, }) - this.isUsingRemoteBrowser = false } /** @@ -91,7 +89,6 @@ export class BrowserSession { console.log(`Connected to remote browser at ${chromeHostUrl}`) this.context.globalState.update("cachedChromeHostUrl", chromeHostUrl) this.lastConnectionAttempt = Date.now() - this.isUsingRemoteBrowser = true return true } catch (error) { @@ -198,12 +195,15 @@ export class BrowserSession { if (this.browser || this.page) { console.log("closing browser...") - if (this.isUsingRemoteBrowser && this.browser) { + const remoteBrowserEnabled = this.context.globalState.get("remoteBrowserEnabled") as boolean | undefined + if (remoteBrowserEnabled && this.browser) { await this.browser.disconnect().catch(() => {}) } else { await this.browser?.close().catch(() => {}) + this.resetBrowserState() } - this.resetBrowserState() + + // this.resetBrowserState() } return {} } @@ -215,7 +215,6 @@ export class BrowserSession { this.browser = undefined this.page = undefined this.currentMousePosition = undefined - this.isUsingRemoteBrowser = false } async doAction(action: (page: Page) => Promise): Promise { diff --git a/src/services/browser/__tests__/BrowserSession.spec.ts b/src/services/browser/__tests__/BrowserSession.spec.ts deleted file mode 100644 index 0ba43a382c..0000000000 --- a/src/services/browser/__tests__/BrowserSession.spec.ts +++ /dev/null @@ -1,234 +0,0 @@ -// npx vitest services/browser/__tests__/BrowserSession.spec.ts - -import { describe, it, expect, vi, beforeEach } from "vitest" -import { BrowserSession } from "../BrowserSession" -import { discoverChromeHostUrl, tryChromeHostUrl } from "../browserDiscovery" -import { fileExistsAtPath } from "../../../utils/fs" - -// Mock dependencies -vi.mock("vscode", () => ({ - ExtensionContext: vi.fn(), - Uri: { - file: vi.fn((path) => ({ fsPath: path })), - }, -})) - -// Mock puppeteer-core -vi.mock("puppeteer-core", () => { - const mockBrowser = { - newPage: vi.fn().mockResolvedValue({ - goto: vi.fn().mockResolvedValue(undefined), - on: vi.fn(), - off: vi.fn(), - screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"), - url: vi.fn().mockReturnValue("https://example.com"), - }), - pages: vi.fn().mockResolvedValue([]), - close: vi.fn().mockResolvedValue(undefined), - disconnect: vi.fn().mockResolvedValue(undefined), - } - - return { - Browser: vi.fn(), - Page: vi.fn(), - TimeoutError: class TimeoutError extends Error {}, - launch: vi.fn().mockResolvedValue(mockBrowser), - connect: vi.fn().mockResolvedValue(mockBrowser), - } -}) - -// Mock PCR -vi.mock("puppeteer-chromium-resolver", () => { - return { - default: vi.fn().mockResolvedValue({ - puppeteer: { - launch: vi.fn().mockImplementation(async () => { - const { launch } = await import("puppeteer-core") - return launch() - }), - }, - executablePath: "/mock/path/to/chromium", - }), - } -}) - -// Mock fs -vi.mock("fs/promises", () => ({ - mkdir: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn(), - writeFile: vi.fn(), - access: vi.fn(), -})) - -// Mock fileExistsAtPath -vi.mock("../../../utils/fs", () => ({ - fileExistsAtPath: vi.fn().mockResolvedValue(false), -})) - -// Mock browser discovery functions -vi.mock("../browserDiscovery", () => ({ - discoverChromeHostUrl: vi.fn().mockResolvedValue(null), - tryChromeHostUrl: vi.fn().mockResolvedValue(false), -})) - -// Mock delay -vi.mock("delay", () => ({ - default: vi.fn().mockResolvedValue(undefined), -})) - -// Mock p-wait-for -vi.mock("p-wait-for", () => ({ - default: vi.fn().mockResolvedValue(undefined), -})) - -describe("BrowserSession", () => { - let browserSession: BrowserSession - let mockContext: any - - beforeEach(() => { - vi.clearAllMocks() - - // Set up mock context - mockContext = { - globalState: { - get: vi.fn(), - update: vi.fn(), - }, - globalStorageUri: { - fsPath: "/mock/global/storage/path", - }, - extensionUri: { - fsPath: "/mock/extension/path", - }, - } - - // Create browser session - browserSession = new BrowserSession(mockContext) - }) - - describe("Remote browser disabled", () => { - it("should launch a local browser when remote browser is disabled", async () => { - // Mock context to indicate remote browser is disabled - mockContext.globalState.get.mockImplementation((key: string) => { - if (key === "remoteBrowserEnabled") return false - return undefined - }) - - await browserSession.launchBrowser() - - const puppeteerCore = await import("puppeteer-core") - - // Verify that a local browser was launched - expect(puppeteerCore.launch).toHaveBeenCalled() - - // Verify that remote browser connection was not attempted - expect(discoverChromeHostUrl).not.toHaveBeenCalled() - expect(tryChromeHostUrl).not.toHaveBeenCalled() - - expect((browserSession as any).isUsingRemoteBrowser).toBe(false) - }) - }) - - describe("Remote browser successfully connects", () => { - it("should connect to a remote browser when enabled and connection succeeds", async () => { - // Mock context to indicate remote browser is enabled - mockContext.globalState.get.mockImplementation((key: string) => { - if (key === "remoteBrowserEnabled") return true - if (key === "remoteBrowserHost") return "http://remote-browser:9222" - return undefined - }) - - // Mock successful remote browser connection - vi.mocked(tryChromeHostUrl).mockResolvedValue(true) - - await browserSession.launchBrowser() - - const puppeteerCore = await import("puppeteer-core") - - // Verify that connect was called - expect(puppeteerCore.connect).toHaveBeenCalled() - - // Verify that local browser was not launched - expect(puppeteerCore.launch).not.toHaveBeenCalled() - - expect((browserSession as any).isUsingRemoteBrowser).toBe(true) - }) - }) - - describe("Remote browser enabled but falls back to local", () => { - it("should fall back to local browser when remote connection fails", async () => { - // Mock context to indicate remote browser is enabled - mockContext.globalState.get.mockImplementation((key: string) => { - if (key === "remoteBrowserEnabled") return true - if (key === "remoteBrowserHost") return "http://remote-browser:9222" - return undefined - }) - - // Mock failed remote browser connection - vi.mocked(tryChromeHostUrl).mockResolvedValue(false) - vi.mocked(discoverChromeHostUrl).mockResolvedValue(null) - - await browserSession.launchBrowser() - - // Import puppeteer-core to check if launch was called - const puppeteerCore = await import("puppeteer-core") - - // Verify that local browser was launched as fallback - expect(puppeteerCore.launch).toHaveBeenCalled() - - // Verify that isUsingRemoteBrowser is false - expect((browserSession as any).isUsingRemoteBrowser).toBe(false) - }) - }) - - describe("closeBrowser", () => { - it("should close a local browser properly", async () => { - const puppeteerCore = await import("puppeteer-core") - - // Create a mock browser directly - const mockBrowser = { - newPage: vi.fn().mockResolvedValue({}), - pages: vi.fn().mockResolvedValue([]), - close: vi.fn().mockResolvedValue(undefined), - disconnect: vi.fn().mockResolvedValue(undefined), - } - - // Set browser and page on the session - ;(browserSession as any).browser = mockBrowser - ;(browserSession as any).page = {} - ;(browserSession as any).isUsingRemoteBrowser = false - - await browserSession.closeBrowser() - - // Verify that browser.close was called - expect(mockBrowser.close).toHaveBeenCalled() - expect(mockBrowser.disconnect).not.toHaveBeenCalled() - - // Verify that browser state was reset - expect((browserSession as any).browser).toBeUndefined() - expect((browserSession as any).page).toBeUndefined() - expect((browserSession as any).isUsingRemoteBrowser).toBe(false) - }) - - it("should disconnect from a remote browser properly", async () => { - // Create a mock browser directly - const mockBrowser = { - newPage: vi.fn().mockResolvedValue({}), - pages: vi.fn().mockResolvedValue([]), - close: vi.fn().mockResolvedValue(undefined), - disconnect: vi.fn().mockResolvedValue(undefined), - } - - // Set browser and page on the session - ;(browserSession as any).browser = mockBrowser - ;(browserSession as any).page = {} - ;(browserSession as any).isUsingRemoteBrowser = true - - await browserSession.closeBrowser() - - // Verify that browser.disconnect was called - expect(mockBrowser.disconnect).toHaveBeenCalled() - expect(mockBrowser.close).not.toHaveBeenCalled() - }) - }) -}) diff --git a/src/services/code-index/__tests__/cache-manager.spec.ts b/src/services/code-index/__tests__/cache-manager.spec.ts index 54775c9069..75f4eb3063 100644 --- a/src/services/code-index/__tests__/cache-manager.spec.ts +++ b/src/services/code-index/__tests__/cache-manager.spec.ts @@ -4,14 +4,6 @@ import { createHash } from "crypto" import debounce from "lodash.debounce" import { CacheManager } from "../cache-manager" -// Mock safeWriteJson utility -vitest.mock("../../../utils/safeWriteJson", () => ({ - safeWriteJson: vitest.fn().mockResolvedValue(undefined), -})) - -// Import the mocked version -import { safeWriteJson } from "../../../utils/safeWriteJson" - // Mock vscode vitest.mock("vscode", () => ({ Uri: { @@ -106,7 +98,7 @@ describe("CacheManager", () => { cacheManager.updateHash(filePath, hash) expect(cacheManager.getHash(filePath)).toBe(hash) - expect(safeWriteJson).toHaveBeenCalled() + expect(vscode.workspace.fs.writeFile).toHaveBeenCalled() }) it("should delete hash and trigger save", () => { @@ -117,7 +109,7 @@ describe("CacheManager", () => { cacheManager.deleteHash(filePath) expect(cacheManager.getHash(filePath)).toBeUndefined() - expect(safeWriteJson).toHaveBeenCalled() + expect(vscode.workspace.fs.writeFile).toHaveBeenCalled() }) it("should return shallow copy of hashes", () => { @@ -142,16 +134,18 @@ describe("CacheManager", () => { cacheManager.updateHash(filePath, hash) - expect(safeWriteJson).toHaveBeenCalledWith(mockCachePath.fsPath, expect.any(Object)) + expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(mockCachePath, expect.any(Uint8Array)) // Verify the saved data - const savedData = (safeWriteJson as Mock).mock.calls[0][1] + const savedData = JSON.parse( + Buffer.from((vscode.workspace.fs.writeFile as Mock).mock.calls[0][1]).toString(), + ) expect(savedData).toEqual({ [filePath]: hash }) }) it("should handle save errors gracefully", async () => { const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) - ;(safeWriteJson as Mock).mockRejectedValue(new Error("Save failed")) + ;(vscode.workspace.fs.writeFile as Mock).mockRejectedValue(new Error("Save failed")) cacheManager.updateHash("test.ts", "hash") @@ -168,19 +162,19 @@ describe("CacheManager", () => { it("should clear cache file and reset state", async () => { cacheManager.updateHash("test.ts", "hash") - // Reset the mock to ensure safeWriteJson succeeds for clearCacheFile - ;(safeWriteJson as Mock).mockClear() - ;(safeWriteJson as Mock).mockResolvedValue(undefined) + // Reset the mock to ensure writeFile succeeds for clearCacheFile + ;(vscode.workspace.fs.writeFile as Mock).mockClear() + ;(vscode.workspace.fs.writeFile as Mock).mockResolvedValue(undefined) await cacheManager.clearCacheFile() - expect(safeWriteJson).toHaveBeenCalledWith(mockCachePath.fsPath, {}) + expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(mockCachePath, Buffer.from("{}")) expect(cacheManager.getAllHashes()).toEqual({}) }) it("should handle clear errors gracefully", async () => { const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) - ;(safeWriteJson as Mock).mockRejectedValue(new Error("Save failed")) + ;(vscode.workspace.fs.writeFile as Mock).mockRejectedValue(new Error("Save failed")) await cacheManager.clearCacheFile() diff --git a/src/services/code-index/cache-manager.ts b/src/services/code-index/cache-manager.ts index a9a4f0ac47..f66f933a0b 100644 --- a/src/services/code-index/cache-manager.ts +++ b/src/services/code-index/cache-manager.ts @@ -2,9 +2,6 @@ import * as vscode from "vscode" import { createHash } from "crypto" import { ICacheManager } from "./interfaces/cache" import debounce from "lodash.debounce" -import { safeWriteJson } from "../../utils/safeWriteJson" -import { TelemetryService } from "@roo-code/telemetry" -import { TelemetryEventName } from "@roo-code/types" /** * Manages the cache for code indexing @@ -41,11 +38,6 @@ export class CacheManager implements ICacheManager { this.fileHashes = JSON.parse(cacheData.toString()) } catch (error) { this.fileHashes = {} - TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - location: "initialize", - }) } } @@ -54,14 +46,9 @@ export class CacheManager implements ICacheManager { */ private async _performSave(): Promise { try { - await safeWriteJson(this.cachePath.fsPath, this.fileHashes) + await vscode.workspace.fs.writeFile(this.cachePath, Buffer.from(JSON.stringify(this.fileHashes, null, 2))) } catch (error) { console.error("Failed to save cache:", error) - TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - location: "_performSave", - }) } } @@ -70,15 +57,10 @@ export class CacheManager implements ICacheManager { */ async clearCacheFile(): Promise { try { - await safeWriteJson(this.cachePath.fsPath, {}) + await vscode.workspace.fs.writeFile(this.cachePath, Buffer.from("{}")) this.fileHashes = {} } catch (error) { console.error("Failed to clear cache file:", error, this.cachePath) - TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - location: "clearCacheFile", - }) } } diff --git a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts index e539c2edde..51a1d3b4e6 100644 --- a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts +++ b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts @@ -70,9 +70,7 @@ describe("QdrantVectorStore", () => { it("should correctly initialize QdrantClient and collectionName in constructor", () => { expect(QdrantClient).toHaveBeenCalledTimes(1) expect(QdrantClient).toHaveBeenCalledWith({ - host: "mock-qdrant", - https: false, - port: 6333, + url: mockQdrantUrl, apiKey: mockApiKey, headers: { "User-Agent": "Roo-Code", @@ -89,9 +87,7 @@ describe("QdrantVectorStore", () => { const vectorStoreWithDefaults = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize) expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, + url: "http://localhost:6333", // Should use default QDRANT_URL apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -103,9 +99,7 @@ describe("QdrantVectorStore", () => { const vectorStoreWithoutKey = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, mockVectorSize) expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "mock-qdrant", - https: false, - port: 6333, + url: mockQdrantUrl, apiKey: undefined, headers: { "User-Agent": "Roo-Code", @@ -113,401 +107,6 @@ describe("QdrantVectorStore", () => { }) }) - describe("URL Parsing and Explicit Port Handling", () => { - describe("HTTPS URL handling", () => { - it("should use explicit port 443 for HTTPS URLs without port (fixes the main bug)", () => { - const vectorStore = new QdrantVectorStore( - mockWorkspacePath, - "https://qdrant.ashbyfam.com", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "qdrant.ashbyfam.com", - https: true, - port: 443, - prefix: undefined, // No prefix for root path - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com") - }) - - it("should use explicit port for HTTPS URLs with explicit port", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "https://example.com:9000", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "example.com", - https: true, - port: 9000, - prefix: undefined, // No prefix for root path - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("https://example.com:9000") - }) - - it("should use port 443 for HTTPS URLs with paths and query parameters", () => { - const vectorStore = new QdrantVectorStore( - mockWorkspacePath, - "https://example.com/api/v1?key=value", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "example.com", - https: true, - port: 443, - prefix: "/api/v1", // Should have prefix - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("https://example.com/api/v1?key=value") - }) - }) - - describe("HTTP URL handling", () => { - it("should use explicit port 80 for HTTP URLs without port", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://example.com", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "example.com", - https: false, - port: 80, - prefix: undefined, // No prefix for root path - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://example.com") - }) - - it("should use explicit port for HTTP URLs with explicit port", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:8080", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 8080, - prefix: undefined, // No prefix for root path - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://localhost:8080") - }) - - it("should use port 80 for HTTP URLs while preserving paths and query parameters", () => { - const vectorStore = new QdrantVectorStore( - mockWorkspacePath, - "http://example.com/api/v1?key=value", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "example.com", - https: false, - port: 80, - prefix: "/api/v1", // Should have prefix - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://example.com/api/v1?key=value") - }) - }) - - describe("Hostname handling", () => { - it("should convert hostname to http with port 80", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "qdrant.example.com", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "qdrant.example.com", - https: false, - port: 80, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://qdrant.example.com") - }) - - it("should handle hostname:port format with explicit port", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "localhost:6333", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") - }) - - it("should handle explicit HTTP URLs correctly", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "http://localhost:9000", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 9000, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://localhost:9000") - }) - }) - - describe("IP address handling", () => { - it("should convert IP address to http with port 80", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "192.168.1.100", - https: false, - port: 80, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100") - }) - - it("should handle IP:port format with explicit port", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "192.168.1.100:6333", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "192.168.1.100", - https: false, - port: 6333, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://192.168.1.100:6333") - }) - }) - - describe("Edge cases", () => { - it("should handle undefined URL with host-based config", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, undefined as any, mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") - }) - - it("should handle empty string URL with host-based config", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") - }) - - it("should handle whitespace-only URL with host-based config", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, " ", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://localhost:6333") - }) - }) - - describe("Invalid URL fallback", () => { - it("should treat invalid URLs as hostnames with port 80", () => { - const vectorStore = new QdrantVectorStore(mockWorkspacePath, "invalid-url-format", mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "invalid-url-format", - https: false, - port: 80, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStore as any).qdrantUrl).toBe("http://invalid-url-format") - }) - }) - }) - - describe("URL Prefix Handling", () => { - it("should pass the URL pathname as prefix to QdrantClient if not root", () => { - const vectorStoreWithPrefix = new QdrantVectorStore( - mockWorkspacePath, - "http://localhost:6333/some/path", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - prefix: "/some/path", - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreWithPrefix as any).qdrantUrl).toBe("http://localhost:6333/some/path") - }) - - it("should not pass prefix if the URL pathname is root ('/')", () => { - const vectorStoreWithoutPrefix = new QdrantVectorStore( - mockWorkspacePath, - "http://localhost:6333/", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - prefix: undefined, - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreWithoutPrefix as any).qdrantUrl).toBe("http://localhost:6333/") - }) - - it("should handle HTTPS URL with path as prefix", () => { - const vectorStoreWithHttpsPrefix = new QdrantVectorStore( - mockWorkspacePath, - "https://qdrant.ashbyfam.com/api", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "qdrant.ashbyfam.com", - https: true, - port: 443, - prefix: "/api", - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreWithHttpsPrefix as any).qdrantUrl).toBe("https://qdrant.ashbyfam.com/api") - }) - - it("should normalize URL pathname by removing trailing slash for prefix", () => { - const vectorStoreWithTrailingSlash = new QdrantVectorStore( - mockWorkspacePath, - "http://localhost:6333/api/", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - prefix: "/api", // Trailing slash should be removed - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreWithTrailingSlash as any).qdrantUrl).toBe("http://localhost:6333/api/") - }) - - it("should normalize URL pathname by removing multiple trailing slashes for prefix", () => { - const vectorStoreWithMultipleTrailingSlashes = new QdrantVectorStore( - mockWorkspacePath, - "http://localhost:6333/api///", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - prefix: "/api", // All trailing slashes should be removed - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreWithMultipleTrailingSlashes as any).qdrantUrl).toBe("http://localhost:6333/api///") - }) - - it("should handle multiple path segments correctly for prefix", () => { - const vectorStoreWithMultiSegment = new QdrantVectorStore( - mockWorkspacePath, - "http://localhost:6333/api/v1/qdrant", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - prefix: "/api/v1/qdrant", - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreWithMultiSegment as any).qdrantUrl).toBe("http://localhost:6333/api/v1/qdrant") - }) - - it("should handle complex URL with multiple segments, multiple trailing slashes, query params, and fragment", () => { - const complexUrl = "https://example.com/ollama/api/v1///?key=value#pos" - const vectorStoreComplex = new QdrantVectorStore(mockWorkspacePath, complexUrl, mockVectorSize) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "example.com", - https: true, - port: 443, - prefix: "/ollama/api/v1", // Trailing slash removed, query/fragment ignored - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreComplex as any).qdrantUrl).toBe(complexUrl) - }) - - it("should ignore query parameters and fragments when determining prefix", () => { - const vectorStoreWithQueryParams = new QdrantVectorStore( - mockWorkspacePath, - "http://localhost:6333/api/path?key=value#fragment", - mockVectorSize, - ) - expect(QdrantClient).toHaveBeenLastCalledWith({ - host: "localhost", - https: false, - port: 6333, - prefix: "/api/path", // Query params and fragment should be ignored - apiKey: undefined, - headers: { - "User-Agent": "Roo-Code", - }, - }) - expect((vectorStoreWithQueryParams as any).qdrantUrl).toBe( - "http://localhost:6333/api/path?key=value#fragment", - ) - }) - }) - describe("initialize", () => { it("should create a new collection if none exists and return true", async () => { // Mock getCollection to throw a 404-like error diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 5121d65b97..32c4466e19 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -24,56 +24,14 @@ export class QdrantVectorStore implements IVectorStore { * @param url Optional URL to the Qdrant server */ constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) { - // Parse the URL to determine the appropriate QdrantClient configuration - const parsedUrl = this.parseQdrantUrl(url) - - // Store the resolved URL for our property - this.qdrantUrl = parsedUrl - - try { - const urlObj = new URL(parsedUrl) - - // Always use host-based configuration with explicit ports to avoid QdrantClient defaults - let port: number - let useHttps: boolean - - if (urlObj.port) { - // Explicit port specified - use it and determine protocol - port = Number(urlObj.port) - useHttps = urlObj.protocol === "https:" - } else { - // No explicit port - use protocol defaults - if (urlObj.protocol === "https:") { - port = 443 - useHttps = true - } else { - // http: or other protocols default to port 80 - port = 80 - useHttps = false - } - } - - this.client = new QdrantClient({ - host: urlObj.hostname, - https: useHttps, - port: port, - prefix: urlObj.pathname === "/" ? undefined : urlObj.pathname.replace(/\/+$/, ""), - apiKey, - headers: { - "User-Agent": "Roo-Code", - }, - }) - } catch (urlError) { - // If URL parsing fails, fall back to URL-based config - // Note: This fallback won't correctly handle prefixes, but it's a last resort for malformed URLs. - this.client = new QdrantClient({ - url: parsedUrl, - apiKey, - headers: { - "User-Agent": "Roo-Code", - }, - }) - } + this.qdrantUrl = url || "http://localhost:6333" + this.client = new QdrantClient({ + url: this.qdrantUrl, + apiKey, + headers: { + "User-Agent": "Roo-Code", + }, + }) // Generate collection name from workspace path const hash = createHash("sha256").update(workspacePath).digest("hex") @@ -81,50 +39,6 @@ export class QdrantVectorStore implements IVectorStore { this.collectionName = `ws-${hash.substring(0, 16)}` } - /** - * Parses and normalizes Qdrant server URLs to handle various input formats - * @param url Raw URL input from user - * @returns Properly formatted URL for QdrantClient - */ - private parseQdrantUrl(url: string | undefined): string { - // Handle undefined/null/empty cases - if (!url || url.trim() === "") { - return "http://localhost:6333" - } - - const trimmedUrl = url.trim() - - // Check if it starts with a protocol - if (!trimmedUrl.startsWith("http://") && !trimmedUrl.startsWith("https://") && !trimmedUrl.includes("://")) { - // No protocol - treat as hostname - return this.parseHostname(trimmedUrl) - } - - try { - // Attempt to parse as complete URL - return as-is, let constructor handle ports - const parsedUrl = new URL(trimmedUrl) - return trimmedUrl - } catch { - // Failed to parse as URL - treat as hostname - return this.parseHostname(trimmedUrl) - } - } - - /** - * Handles hostname-only inputs - * @param hostname Raw hostname input - * @returns Properly formatted URL with http:// prefix - */ - private parseHostname(hostname: string): string { - if (hostname.includes(":")) { - // Has port - add http:// prefix if missing - return hostname.startsWith("http") ? hostname : `http://${hostname}` - } else { - // No port - add http:// prefix without port (let constructor handle port assignment) - return `http://${hostname}` - } - } - private async getCollectionInfo(): Promise { try { const collectionInfo = await this.client.getCollection(this.collectionName) diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index be002e2f1d..75f14b0d4c 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -5,7 +5,6 @@ import * as yaml from "yaml" import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" -import type { CustomModesManager } from "../../core/config/CustomModesManager" export interface InstallOptions extends InstallMarketplaceItemOptions { target: "project" | "global" @@ -13,10 +12,7 @@ export interface InstallOptions extends InstallMarketplaceItemOptions { } export class SimpleInstaller { - constructor( - private readonly context: vscode.ExtensionContext, - private readonly customModesManager?: CustomModesManager, - ) {} + constructor(private readonly context: vscode.ExtensionContext) {} async installItem(item: MarketplaceItem, options: InstallOptions): Promise<{ filePath: string; line?: number }> { const { target } = options @@ -44,48 +40,6 @@ export class SimpleInstaller { throw new Error("Mode content should not be an array") } - // If CustomModesManager is available, use importModeWithRules - if (this.customModesManager) { - // Transform marketplace content to import format (wrap in customModes array) - const importData = { - customModes: [yaml.parse(item.content)], - } - const importYaml = yaml.stringify(importData) - - // Call customModesManager.importModeWithRules - const result = await this.customModesManager.importModeWithRules(importYaml, target) - - if (!result.success) { - throw new Error(result.error || "Failed to import mode") - } - - // Return the file path and line number for VS Code to open - const filePath = await this.getModeFilePath(target) - - // Try to find the line number where the mode was added - let line: number | undefined - try { - const fileContent = await fs.readFile(filePath, "utf-8") - const lines = fileContent.split("\n") - const modeData = yaml.parse(item.content) - - // Find the line containing the slug of the added mode - if (modeData?.slug) { - const slugLineIndex = lines.findIndex( - (l) => l.includes(`slug: ${modeData.slug}`) || l.includes(`slug: "${modeData.slug}"`), - ) - if (slugLineIndex >= 0) { - line = slugLineIndex + 1 // Convert to 1-based line number - } - } - } catch (error) { - // If we can't find the line number, that's okay - } - - return { filePath, line } - } - - // Fallback to original implementation if CustomModesManager is not available const filePath = await this.getModeFilePath(target) const modeData = yaml.parse(item.content) @@ -93,9 +47,7 @@ export class SimpleInstaller { let existingData: any = { customModes: [] } try { const existing = await fs.readFile(filePath, "utf-8") - const parsed = yaml.parse(existing) - // Ensure we have a valid object with customModes array - existingData = parsed && typeof parsed === "object" ? parsed : { customModes: [] } + existingData = yaml.parse(existing) || { customModes: [] } } catch (error: any) { if (error.code === "ENOENT") { // File doesn't exist, use default structure - this is fine @@ -132,7 +84,7 @@ export class SimpleInstaller { // Write back to file await fs.mkdir(path.dirname(filePath), { recursive: true }) - const yamlContent = yaml.stringify(existingData, { lineWidth: 0 }) + const yamlContent = yaml.stringify(existingData) await fs.writeFile(filePath, yamlContent, "utf-8") // Calculate approximate line number where the new mode was added @@ -294,38 +246,51 @@ export class SimpleInstaller { } private async removeMode(item: MarketplaceItem, target: "project" | "global"): Promise { - if (!this.customModesManager) { - throw new Error("CustomModesManager is not available") - } + const filePath = await this.getModeFilePath(target) - // Parse the item content to get the slug - let content: string - if (Array.isArray(item.content)) { - // Array of McpInstallationMethod objects - use first method - content = item.content[0].content - } else { - content = item.content || "" - } - - let modeSlug: string try { - const modeData = yaml.parse(content) - modeSlug = modeData.slug - } catch (error) { - throw new Error("Invalid mode content: unable to parse YAML") + const existing = await fs.readFile(filePath, "utf-8") + let existingData: any + + try { + existingData = yaml.parse(existing) + } catch (parseError) { + // If we can't parse the file, we can't safely remove a mode + const fileName = target === "project" ? ".roomodes" : "custom-modes.yaml" + throw new Error( + `Cannot remove mode: The ${fileName} file contains invalid YAML. ` + + `Please fix the syntax errors before removing modes.`, + ) + } + + if (existingData?.customModes) { + // Parse the item content to get the slug + let content: string + if (Array.isArray(item.content)) { + // Array of McpInstallationMethod objects - use first method + content = item.content[0].content + } else { + content = item.content + } + const modeData = yaml.parse(content || "") + + if (!modeData.slug) { + return // Nothing to remove if no slug + } + + // Remove mode with matching slug + existingData.customModes = existingData.customModes.filter((mode: any) => mode.slug !== modeData.slug) + + // Always write back the file, even if empty + await fs.writeFile(filePath, yaml.stringify(existingData), "utf-8") + } + } catch (error: any) { + if (error.code === "ENOENT") { + // File doesn't exist, nothing to remove + return + } + throw error } - - if (!modeSlug) { - throw new Error("Mode missing slug identifier") - } - - // Get the current modes to determine the source - const modes = await this.customModesManager.getCustomModes() - const mode = modes.find((m) => m.slug === modeSlug) - - // Use CustomModesManager to delete the mode configuration - // This also handles rules folder deletion - await this.customModesManager.deleteCustomMode(modeSlug, true) } private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise { diff --git a/src/services/mcp/__tests__/McpHub.spec.ts b/src/services/mcp/__tests__/McpHub.spec.ts index 7dc7f00c04..1ed2993f2a 100644 --- a/src/services/mcp/__tests__/McpHub.spec.ts +++ b/src/services/mcp/__tests__/McpHub.spec.ts @@ -5,43 +5,6 @@ import { ServerConfigSchema, McpHub } from "../McpHub" import fs from "fs/promises" import { vi, Mock } from "vitest" -// Mock fs/promises before importing anything that uses it -vi.mock("fs/promises", () => ({ - default: { - access: vi.fn().mockResolvedValue(undefined), - writeFile: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn().mockResolvedValue("{}"), - unlink: vi.fn().mockResolvedValue(undefined), - rename: vi.fn().mockResolvedValue(undefined), - lstat: vi.fn().mockImplementation(() => - Promise.resolve({ - isDirectory: () => true, - }), - ), - mkdir: vi.fn().mockResolvedValue(undefined), - }, - access: vi.fn().mockResolvedValue(undefined), - writeFile: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn().mockResolvedValue("{}"), - unlink: vi.fn().mockResolvedValue(undefined), - rename: vi.fn().mockResolvedValue(undefined), - lstat: vi.fn().mockImplementation(() => - Promise.resolve({ - isDirectory: () => true, - }), - ), - mkdir: vi.fn().mockResolvedValue(undefined), -})) - -// Mock safeWriteJson -vi.mock("../../../utils/safeWriteJson", () => ({ - safeWriteJson: vi.fn(async (filePath, data) => { - // Instead of trying to write to the file system, just call fs.writeFile mock - // This avoids the complex file locking and temp file operations - return fs.writeFile(filePath, JSON.stringify(data), "utf8") - }), -})) - vi.mock("vscode", () => ({ workspace: { createFileSystemWatcher: vi.fn().mockReturnValue({ diff --git a/src/services/mdm/MdmService.ts b/src/services/mdm/MdmService.ts index 67d684b176..e88601dfb3 100644 --- a/src/services/mdm/MdmService.ts +++ b/src/services/mdm/MdmService.ts @@ -6,7 +6,6 @@ import { z } from "zod" import { CloudService, getClerkBaseUrl, PRODUCTION_CLERK_BASE_URL } from "@roo-code/cloud" import { Package } from "../../shared/package" -import { t } from "../../i18n" // MDM Configuration Schema const mdmConfigSchema = z.object({ @@ -35,6 +34,8 @@ export class MdmService { this.mdmConfig = await this.loadMdmConfig() if (this.mdmConfig) { this.log("[MDM] Loaded MDM configuration:", this.mdmConfig) + // Automatically enable Roo Code Cloud when MDM config is present + await this.ensureCloudEnabled() } else { this.log("[MDM] No MDM configuration found") } @@ -58,6 +59,23 @@ export class MdmService { return this.mdmConfig?.organizationId } + /** + * Ensure Roo Code Cloud is enabled when MDM config is present + */ + private async ensureCloudEnabled(): Promise { + try { + const config = vscode.workspace.getConfiguration(Package.name) + const currentValue = config.get("rooCodeCloudEnabled", false) + + if (!currentValue) { + this.log("[MDM] Enabling Roo Code Cloud due to MDM policy") + await config.update("rooCodeCloudEnabled", true, vscode.ConfigurationTarget.Global) + } + } catch (error) { + this.log("[MDM] Error enabling Roo Code Cloud:", error) + } + } + /** * Check if the current state is compliant with MDM policy */ @@ -71,7 +89,7 @@ export class MdmService { if (!CloudService.hasInstance() || !CloudService.instance.hasOrIsAcquiringActiveSession()) { return { compliant: false, - reason: t("mdm.errors.cloud_auth_required"), + reason: "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", } } @@ -79,35 +97,18 @@ export class MdmService { const requiredOrgId = this.getRequiredOrganizationId() if (requiredOrgId) { try { - // First try to get from active session - let currentOrgId = CloudService.instance.getOrganizationId() - - // If no active session, check stored credentials - if (!currentOrgId) { - const storedOrgId = CloudService.instance.getStoredOrganizationId() - - // null means personal account, which is not compliant for org requirements - if (storedOrgId === null || storedOrgId !== requiredOrgId) { - return { - compliant: false, - reason: t("mdm.errors.organization_mismatch"), - } - } - - currentOrgId = storedOrgId - } - + const currentOrgId = CloudService.instance.getOrganizationId() if (currentOrgId !== requiredOrgId) { return { compliant: false, - reason: t("mdm.errors.organization_mismatch"), + reason: "You must be authenticated with your organization's Roo Code Cloud account.", } } } catch (error) { this.log("[MDM] Error checking organization ID:", error) return { compliant: false, - reason: t("mdm.errors.verification_failed"), + reason: "Unable to verify organization authentication.", } } } diff --git a/src/services/mdm/__tests__/MdmService.spec.ts b/src/services/mdm/__tests__/MdmService.spec.ts index 81ff61652b..a7a48a5eb3 100644 --- a/src/services/mdm/__tests__/MdmService.spec.ts +++ b/src/services/mdm/__tests__/MdmService.spec.ts @@ -43,19 +43,6 @@ vi.mock("../../../shared/package", () => ({ }, })) -vi.mock("../../../i18n", () => ({ - t: vi.fn((key: string) => { - const translations: Record = { - "mdm.errors.cloud_auth_required": - "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", - "mdm.errors.organization_mismatch": - "You must be authenticated with your organization's Roo Code Cloud account.", - "mdm.errors.verification_failed": "Unable to verify organization authentication.", - } - return translations[key] || key - }), -})) - import * as fs from "fs" import * as os from "os" import * as vscode from "vscode" @@ -278,7 +265,7 @@ describe("MdmService", () => { expect(compliance.compliant).toBe(false) if (!compliance.compliant) { - expect(compliance.reason).toContain("Your organization requires Roo Code Cloud authentication") + expect(compliance.reason).toContain("requires Roo Code Cloud authentication") } }) @@ -300,9 +287,7 @@ describe("MdmService", () => { expect(compliance.compliant).toBe(false) if (!compliance.compliant) { - expect(compliance.reason).toContain( - "You must be authenticated with your organization's Roo Code Cloud account", - ) + expect(compliance.reason).toContain("organization's Roo Code Cloud account") } }) @@ -340,6 +325,102 @@ describe("MdmService", () => { }) }) + describe("cloud enablement", () => { + it("should enable Roo Code Cloud when MDM config is present and setting is disabled", async () => { + const mockConfig = { + requireCloudAuth: true, + organizationId: "test-org-123", + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), // rooCodeCloudEnabled is false + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline") + expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false) + expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1) // ConfigurationTarget.Global + }) + + it("should not update setting when Roo Code Cloud is already enabled", async () => { + const mockConfig = { + requireCloudAuth: true, + organizationId: "test-org-123", + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(true), // rooCodeCloudEnabled is already true + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false) + expect(mockVsCodeConfig.update).not.toHaveBeenCalled() + }) + + it("should enable cloud even when requireCloudAuth is false", async () => { + const mockConfig = { + requireCloudAuth: false, // Cloud auth not required, but config file exists + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1) + }) + + it("should not enable cloud when no MDM config exists", async () => { + mockFs.existsSync.mockReturnValue(false) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVsCodeConfig.update).not.toHaveBeenCalled() + }) + + it("should handle VSCode configuration errors gracefully", async () => { + const mockConfig = { + requireCloudAuth: true, + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), + update: vi.fn().mockRejectedValue(new Error("Configuration update failed")), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + // Should not throw + await expect(MdmService.createInstance()).resolves.toBeInstanceOf(MdmService) + }) + }) + describe("singleton pattern", () => { it("should throw error when accessing instance before creation", () => { expect(() => MdmService.getInstance()).toThrow("MdmService not initialized") diff --git a/src/services/roo-config/__tests__/index.spec.ts b/src/services/roo-config/__tests__/index.spec.ts deleted file mode 100644 index 8e9bf929cc..0000000000 --- a/src/services/roo-config/__tests__/index.spec.ts +++ /dev/null @@ -1,301 +0,0 @@ -import * as path from "path" -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" - -// Use vi.hoisted to ensure mocks are available during hoisting -const { mockStat, mockReadFile, mockHomedir } = vi.hoisted(() => ({ - mockStat: vi.fn(), - mockReadFile: vi.fn(), - mockHomedir: vi.fn(), -})) - -// Mock fs/promises module -vi.mock("fs/promises", () => ({ - default: { - stat: mockStat, - readFile: mockReadFile, - }, -})) - -// Mock os module -vi.mock("os", () => ({ - homedir: mockHomedir, -})) - -import { - getGlobalRooDirectory, - getProjectRooDirectoryForCwd, - directoryExists, - fileExists, - readFileIfExists, - getRooDirectoriesForCwd, - loadConfiguration, -} from "../index" - -describe("RooConfigService", () => { - beforeEach(() => { - vi.clearAllMocks() - mockHomedir.mockReturnValue("/mock/home") - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe("getGlobalRooDirectory", () => { - it("should return correct path for global .roo directory", () => { - const result = getGlobalRooDirectory() - expect(result).toBe(path.join("/mock/home", ".roo")) - }) - - it("should handle different home directories", () => { - mockHomedir.mockReturnValue("/different/home") - const result = getGlobalRooDirectory() - expect(result).toBe(path.join("/different/home", ".roo")) - }) - }) - - describe("getProjectRooDirectoryForCwd", () => { - it("should return correct path for given cwd", () => { - const cwd = "/custom/project/path" - const result = getProjectRooDirectoryForCwd(cwd) - expect(result).toBe(path.join(cwd, ".roo")) - }) - }) - - describe("directoryExists", () => { - it("should return true for existing directory", async () => { - mockStat.mockResolvedValue({ isDirectory: () => true } as any) - - const result = await directoryExists("/some/path") - - expect(result).toBe(true) - expect(mockStat).toHaveBeenCalledWith("/some/path") - }) - - it("should return false for non-existing path", async () => { - const error = new Error("ENOENT") as any - error.code = "ENOENT" - mockStat.mockRejectedValue(error) - - const result = await directoryExists("/non/existing/path") - - expect(result).toBe(false) - }) - - it("should return false for ENOTDIR error", async () => { - const error = new Error("ENOTDIR") as any - error.code = "ENOTDIR" - mockStat.mockRejectedValue(error) - - const result = await directoryExists("/not/a/directory") - - expect(result).toBe(false) - }) - - it("should throw unexpected errors", async () => { - const error = new Error("Permission denied") as any - error.code = "EACCES" - mockStat.mockRejectedValue(error) - - await expect(directoryExists("/permission/denied")).rejects.toThrow("Permission denied") - }) - - it("should return false for files", async () => { - mockStat.mockResolvedValue({ isDirectory: () => false } as any) - - const result = await directoryExists("/some/file.txt") - - expect(result).toBe(false) - }) - }) - - describe("fileExists", () => { - it("should return true for existing file", async () => { - mockStat.mockResolvedValue({ isFile: () => true } as any) - - const result = await fileExists("/some/file.txt") - - expect(result).toBe(true) - expect(mockStat).toHaveBeenCalledWith("/some/file.txt") - }) - - it("should return false for non-existing file", async () => { - const error = new Error("ENOENT") as any - error.code = "ENOENT" - mockStat.mockRejectedValue(error) - - const result = await fileExists("/non/existing/file.txt") - - expect(result).toBe(false) - }) - - it("should return false for ENOTDIR error", async () => { - const error = new Error("ENOTDIR") as any - error.code = "ENOTDIR" - mockStat.mockRejectedValue(error) - - const result = await fileExists("/not/a/directory/file.txt") - - expect(result).toBe(false) - }) - - it("should throw unexpected errors", async () => { - const error = new Error("Permission denied") as any - error.code = "EACCES" - mockStat.mockRejectedValue(error) - - await expect(fileExists("/permission/denied/file.txt")).rejects.toThrow("Permission denied") - }) - - it("should return false for directories", async () => { - mockStat.mockResolvedValue({ isFile: () => false } as any) - - const result = await fileExists("/some/directory") - - expect(result).toBe(false) - }) - }) - - describe("readFileIfExists", () => { - it("should return file content for existing file", async () => { - mockReadFile.mockResolvedValue("file content") - - const result = await readFileIfExists("/some/file.txt") - - expect(result).toBe("file content") - expect(mockReadFile).toHaveBeenCalledWith("/some/file.txt", "utf-8") - }) - - it("should return null for non-existing file", async () => { - const error = new Error("ENOENT") as any - error.code = "ENOENT" - mockReadFile.mockRejectedValue(error) - - const result = await readFileIfExists("/non/existing/file.txt") - - expect(result).toBe(null) - }) - - it("should return null for ENOTDIR error", async () => { - const error = new Error("ENOTDIR") as any - error.code = "ENOTDIR" - mockReadFile.mockRejectedValue(error) - - const result = await readFileIfExists("/not/a/directory/file.txt") - - expect(result).toBe(null) - }) - - it("should return null for EISDIR error", async () => { - const error = new Error("EISDIR") as any - error.code = "EISDIR" - mockReadFile.mockRejectedValue(error) - - const result = await readFileIfExists("/is/a/directory") - - expect(result).toBe(null) - }) - - it("should throw unexpected errors", async () => { - const error = new Error("Permission denied") as any - error.code = "EACCES" - mockReadFile.mockRejectedValue(error) - - await expect(readFileIfExists("/permission/denied/file.txt")).rejects.toThrow("Permission denied") - }) - }) - - describe("getRooDirectoriesForCwd", () => { - it("should return directories for given cwd", () => { - const cwd = "/custom/project/path" - - const result = getRooDirectoriesForCwd(cwd) - - expect(result).toEqual([path.join("/mock/home", ".roo"), path.join(cwd, ".roo")]) - }) - }) - - describe("loadConfiguration", () => { - it("should load global configuration only when project does not exist", async () => { - const error = new Error("ENOENT") as any - error.code = "ENOENT" - mockReadFile.mockResolvedValueOnce("global content").mockRejectedValueOnce(error) - - const result = await loadConfiguration("rules/rules.md", "/project/path") - - expect(result).toEqual({ - global: "global content", - project: null, - merged: "global content", - }) - }) - - it("should load project configuration only when global does not exist", async () => { - const error = new Error("ENOENT") as any - error.code = "ENOENT" - mockReadFile.mockRejectedValueOnce(error).mockResolvedValueOnce("project content") - - const result = await loadConfiguration("rules/rules.md", "/project/path") - - expect(result).toEqual({ - global: null, - project: "project content", - merged: "project content", - }) - }) - - it("should merge global and project configurations with project overriding global", async () => { - mockReadFile.mockResolvedValueOnce("global content").mockResolvedValueOnce("project content") - - const result = await loadConfiguration("rules/rules.md", "/project/path") - - expect(result).toEqual({ - global: "global content", - project: "project content", - merged: "global content\n\n# Project-specific rules (override global):\n\nproject content", - }) - }) - - it("should return empty merged content when neither exists", async () => { - const error = new Error("ENOENT") as any - error.code = "ENOENT" - mockReadFile.mockRejectedValueOnce(error).mockRejectedValueOnce(error) - - const result = await loadConfiguration("rules/rules.md", "/project/path") - - expect(result).toEqual({ - global: null, - project: null, - merged: "", - }) - }) - - it("should propagate unexpected errors from global file read", async () => { - const error = new Error("Permission denied") as any - error.code = "EACCES" - mockReadFile.mockRejectedValueOnce(error) - - await expect(loadConfiguration("rules/rules.md", "/project/path")).rejects.toThrow("Permission denied") - }) - - it("should propagate unexpected errors from project file read", async () => { - const globalError = new Error("ENOENT") as any - globalError.code = "ENOENT" - const projectError = new Error("Permission denied") as any - projectError.code = "EACCES" - - mockReadFile.mockRejectedValueOnce(globalError).mockRejectedValueOnce(projectError) - - await expect(loadConfiguration("rules/rules.md", "/project/path")).rejects.toThrow("Permission denied") - }) - - it("should use correct file paths", async () => { - mockReadFile.mockResolvedValue("content") - - await loadConfiguration("rules/rules.md", "/project/path") - - expect(mockReadFile).toHaveBeenCalledWith(path.join("/mock/home", ".roo", "rules/rules.md"), "utf-8") - expect(mockReadFile).toHaveBeenCalledWith(path.join("/project/path", ".roo", "rules/rules.md"), "utf-8") - }) - }) -}) diff --git a/src/services/roo-config/index.ts b/src/services/roo-config/index.ts deleted file mode 100644 index b46c39e354..0000000000 --- a/src/services/roo-config/index.ts +++ /dev/null @@ -1,252 +0,0 @@ -import * as path from "path" -import * as os from "os" -import fs from "fs/promises" - -/** - * Gets the global .roo directory path based on the current platform - * - * @returns The absolute path to the global .roo directory - * - * @example Platform-specific paths: - * ``` - * // macOS/Linux: ~/.roo/ - * // Example: /Users/john/.roo - * - * // Windows: %USERPROFILE%\.roo\ - * // Example: C:\Users\john\.roo - * ``` - * - * @example Usage: - * ```typescript - * const globalDir = getGlobalRooDirectory() - * // Returns: "/Users/john/.roo" (on macOS/Linux) - * // Returns: "C:\\Users\\john\\.roo" (on Windows) - * ``` - */ -export function getGlobalRooDirectory(): string { - const homeDir = os.homedir() - return path.join(homeDir, ".roo") -} - -/** - * Gets the project-local .roo directory path for a given cwd - * - * @param cwd - Current working directory (project path) - * @returns The absolute path to the project-local .roo directory - * - * @example - * ```typescript - * const projectDir = getProjectRooDirectoryForCwd('/Users/john/my-project') - * // Returns: "/Users/john/my-project/.roo" - * - * const windowsProjectDir = getProjectRooDirectoryForCwd('C:\\Users\\john\\my-project') - * // Returns: "C:\\Users\\john\\my-project\\.roo" - * ``` - * - * @example Directory structure: - * ``` - * /Users/john/my-project/ - * ├── .roo/ # Project-local configuration directory - * │ ├── rules/ - * │ │ └── rules.md - * │ ├── custom-instructions.md - * │ └── config/ - * │ └── settings.json - * ├── src/ - * │ └── index.ts - * └── package.json - * ``` - */ -export function getProjectRooDirectoryForCwd(cwd: string): string { - return path.join(cwd, ".roo") -} - -/** - * Checks if a directory exists - */ -export async function directoryExists(dirPath: string): Promise { - try { - const stat = await fs.stat(dirPath) - return stat.isDirectory() - } catch (error: any) { - // Only catch expected "not found" errors - if (error.code === "ENOENT" || error.code === "ENOTDIR") { - return false - } - // Re-throw unexpected errors (permission, I/O, etc.) - throw error - } -} - -/** - * Checks if a file exists - */ -export async function fileExists(filePath: string): Promise { - try { - const stat = await fs.stat(filePath) - return stat.isFile() - } catch (error: any) { - // Only catch expected "not found" errors - if (error.code === "ENOENT" || error.code === "ENOTDIR") { - return false - } - // Re-throw unexpected errors (permission, I/O, etc.) - throw error - } -} - -/** - * Reads a file safely, returning null if it doesn't exist - */ -export async function readFileIfExists(filePath: string): Promise { - try { - return await fs.readFile(filePath, "utf-8") - } catch (error: any) { - // Only catch expected "not found" errors - if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EISDIR") { - return null - } - // Re-throw unexpected errors (permission, I/O, etc.) - throw error - } -} - -/** - * Gets the ordered list of .roo directories to check (global first, then project-local) - * - * @param cwd - Current working directory (project path) - * @returns Array of directory paths to check in order [global, project-local] - * - * @example - * ```typescript - * // For a project at /Users/john/my-project - * const directories = getRooDirectoriesForCwd('/Users/john/my-project') - * // Returns: - * // [ - * // '/Users/john/.roo', // Global directory - * // '/Users/john/my-project/.roo' // Project-local directory - * // ] - * ``` - * - * @example Directory structure: - * ``` - * /Users/john/ - * ├── .roo/ # Global configuration - * │ ├── rules/ - * │ │ └── rules.md - * │ └── custom-instructions.md - * └── my-project/ - * ├── .roo/ # Project-specific configuration - * │ ├── rules/ - * │ │ └── rules.md # Overrides global rules - * │ └── project-notes.md - * └── src/ - * └── index.ts - * ``` - */ -export function getRooDirectoriesForCwd(cwd: string): string[] { - const directories: string[] = [] - - // Add global directory first - directories.push(getGlobalRooDirectory()) - - // Add project-local directory second - directories.push(getProjectRooDirectoryForCwd(cwd)) - - return directories -} - -/** - * Loads configuration from multiple .roo directories with project overriding global - * - * @param relativePath - The relative path within each .roo directory (e.g., 'rules/rules.md') - * @param cwd - Current working directory (project path) - * @returns Object with global and project content, plus merged content - * - * @example - * ```typescript - * // Load rules configuration for a project - * const config = await loadConfiguration('rules/rules.md', '/Users/john/my-project') - * - * // Returns: - * // { - * // global: "Global rules content...", // From ~/.roo/rules/rules.md - * // project: "Project rules content...", // From /Users/john/my-project/.roo/rules/rules.md - * // merged: "Global rules content...\n\n# Project-specific rules (override global):\n\nProject rules content..." - * // } - * ``` - * - * @example File paths resolved: - * ``` - * relativePath: 'rules/rules.md' - * cwd: '/Users/john/my-project' - * - * Reads from: - * - Global: /Users/john/.roo/rules/rules.md - * - Project: /Users/john/my-project/.roo/rules/rules.md - * - * Other common relativePath examples: - * - 'custom-instructions.md' - * - 'config/settings.json' - * - 'templates/component.tsx' - * ``` - * - * @example Merging behavior: - * ``` - * // If only global exists: - * { global: "content", project: null, merged: "content" } - * - * // If only project exists: - * { global: null, project: "content", merged: "content" } - * - * // If both exist: - * { - * global: "global content", - * project: "project content", - * merged: "global content\n\n# Project-specific rules (override global):\n\nproject content" - * } - * ``` - */ -export async function loadConfiguration( - relativePath: string, - cwd: string, -): Promise<{ - global: string | null - project: string | null - merged: string -}> { - const globalDir = getGlobalRooDirectory() - const projectDir = getProjectRooDirectoryForCwd(cwd) - - const globalFilePath = path.join(globalDir, relativePath) - const projectFilePath = path.join(projectDir, relativePath) - - // Read global configuration - const globalContent = await readFileIfExists(globalFilePath) - - // Read project-local configuration - const projectContent = await readFileIfExists(projectFilePath) - - // Merge configurations - project overrides global - let merged = "" - - if (globalContent) { - merged += globalContent - } - - if (projectContent) { - if (merged) { - merged += "\n\n# Project-specific rules (override global):\n\n" - } - merged += projectContent - } - - return { - global: globalContent, - project: projectContent, - merged: merged || "", - } -} - -// Export with backward compatibility alias -export const loadRooConfiguration: typeof loadConfiguration = loadConfiguration diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 000762e317..9a2c9230bd 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -9,7 +9,6 @@ import type { ClineMessage, OrganizationAllowList, CloudUserInfo, - ShareVisibility, } from "@roo-code/types" import { GitCommit } from "../utils/git" @@ -67,17 +66,12 @@ export interface ExtensionMessage { | "ollamaModels" | "lmStudioModels" | "vsCodeLmModels" - | "huggingFaceModels" | "vsCodeLmApiAvailable" | "updatePrompt" | "systemPrompt" | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" - | "exportModeResult" - | "importModeResult" - | "checkRulesDirectoryResult" - | "deleteCustomModeCheck" | "currentCheckpointUpdated" | "showHumanRelayDialog" | "humanRelayResponse" @@ -102,13 +96,7 @@ export interface ExtensionMessage { | "indexCleared" | "codebaseIndexConfig" | "marketplaceInstallResult" - | "marketplaceRemoveResult" | "marketplaceData" - | "shareTaskSuccess" - | "codeIndexSettingsSaved" - | "codeIndexSecretStatus" - | "showDeleteMessageDialog" - | "showEditMessageDialog" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -137,28 +125,6 @@ export interface ExtensionMessage { ollamaModels?: string[] lmStudioModels?: string[] vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[] - huggingFaceModels?: Array<{ - _id: string - id: string - inferenceProviderMapping: Array<{ - provider: string - providerId: string - status: "live" | "staging" | "error" - task: "conversational" - }> - trendingScore: number - config: { - architectures: string[] - model_type: string - tokenizer_config?: { - chat_template?: string | Array<{ name: string; template: string }> - model_max_length?: number - } - } - tags: string[] - pipeline_tag: "text-generation" | "image-text-to-text" - library_name?: string - }> mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ProviderSettingsEntry[] @@ -173,18 +139,12 @@ export interface ExtensionMessage { error?: string setting?: string value?: any - hasContent?: boolean // For checkRulesDirectoryResult items?: MarketplaceItem[] userInfo?: CloudUserInfo organizationAllowList?: OrganizationAllowList tab?: string marketplaceItems?: MarketplaceItem[] marketplaceInstalledMetadata?: MarketplaceInstalledMetadata - visibility?: ShareVisibility - rulesFolderPath?: string - settings?: any - messageTs?: number - context?: string } export type ExtensionState = Pick< @@ -209,9 +169,7 @@ export type ExtensionState = Pick< | "alwaysAllowModeSwitch" | "alwaysAllowSubtasks" | "alwaysAllowExecute" - | "alwaysAllowUpdateTodoList" | "allowedCommands" - | "deniedCommands" | "allowedMaxRequests" | "browserToolEnabled" | "browserViewportSize" @@ -229,7 +187,6 @@ export type ExtensionState = Pick< // | "maxReadFileLine" // Optional in GlobalSettings, required here. | "maxConcurrentFileReads" // Optional in GlobalSettings, required here. | "terminalOutputLineLimit" - | "terminalOutputCharacterLimit" | "terminalShellIntegrationTimeout" | "terminalShellIntegrationDisabled" | "terminalCommandDelay" @@ -239,7 +196,6 @@ export type ExtensionState = Pick< | "terminalZshP10k" | "terminalZdotdir" | "terminalCompressProgressBar" - | "diagnosticsEnabled" | "diffEnabled" | "fuzzyMatchThreshold" // | "experiments" // Optional in GlobalSettings, required here. @@ -258,8 +214,6 @@ export type ExtensionState = Pick< | "codebaseIndexConfig" | "codebaseIndexModels" | "profileThresholds" - | "includeDiagnosticMessages" - | "maxDiagnosticMessages" > & { version: string clineMessages: ClineMessage[] @@ -299,7 +253,6 @@ export type ExtensionState = Pick< cloudUserInfo: CloudUserInfo | null cloudIsAuthenticated: boolean - cloudApiUrl?: string sharingEnabled: boolean organizationAllowList: OrganizationAllowList @@ -308,7 +261,6 @@ export type ExtensionState = Pick< marketplaceItems?: MarketplaceItem[] marketplaceInstalledMetadata?: { project: Record; global: Record } profileThresholds: Record - hasOpenedModeSelector: boolean } export interface ClineSayTool { @@ -412,7 +364,6 @@ export interface ClineApiReqInfo { cost?: number cancelReason?: ClineApiReqCancelReason streamingFailedMessage?: string - apiProtocol?: "anthropic" | "openai" } export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 795e276522..dcded2a69d 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -6,7 +6,6 @@ import type { ModeConfig, InstallMarketplaceItemOptions, MarketplaceItem, - ShareVisibility, } from "@roo-code/types" import { marketplaceItemSchema } from "@roo-code/types" @@ -18,13 +17,8 @@ export type PromptMode = Mode | "enhance" export type AudioType = "notification" | "celebration" | "progress_loop" -export interface UpdateTodoListPayload { - todos: any[] -} - export interface WebviewMessage { type: - | "updateTodoList" | "deleteMultipleTasksWithIds" | "currentApiConfigName" | "saveApiConfiguration" @@ -36,16 +30,12 @@ export interface WebviewMessage { | "getListApiConfiguration" | "customInstructions" | "allowedCommands" - | "deniedCommands" | "alwaysAllowReadOnly" | "alwaysAllowReadOnlyOutsideWorkspace" | "alwaysAllowWrite" | "alwaysAllowWriteOutsideWorkspace" | "alwaysAllowWriteProtected" | "alwaysAllowExecute" - | "alwaysAllowFollowupQuestions" - | "alwaysAllowUpdateTodoList" - | "followupAutoApproveTimeoutMs" | "webviewDidLaunch" | "newTask" | "askResponse" @@ -67,7 +57,6 @@ export interface WebviewMessage { | "requestOllamaModels" | "requestLmStudioModels" | "requestVsCodeLmModels" - | "requestHuggingFaceModels" | "openImage" | "saveImage" | "openFile" @@ -81,7 +70,6 @@ export interface WebviewMessage { | "alwaysAllowModeSwitch" | "allowedMaxRequests" | "alwaysAllowSubtasks" - | "alwaysAllowUpdateTodoList" | "autoCondenseContext" | "autoCondenseContextPercent" | "condensingApiConfigId" @@ -108,16 +96,11 @@ export interface WebviewMessage { | "updateMcpTimeout" | "fuzzyMatchThreshold" | "writeDelayMs" - | "diagnosticsEnabled" | "enhancePrompt" | "enhancedPrompt" | "draggedImages" | "deleteMessage" - | "deleteMessageConfirm" - | "submitEditedMessage" - | "editMessageConfirm" | "terminalOutputLineLimit" - | "terminalOutputCharacterLimit" | "terminalShellIntegrationTimeout" | "terminalShellIntegrationDisabled" | "terminalCommandDelay" @@ -154,7 +137,6 @@ export interface WebviewMessage { | "humanRelayResponse" | "humanRelayCancel" | "browserToolEnabled" - | "codebaseIndexEnabled" | "telemetrySetting" | "showRooIgnoredFiles" | "testBrowserConnection" @@ -163,12 +145,9 @@ export interface WebviewMessage { | "language" | "maxReadFileLine" | "maxConcurrentFileReads" - | "includeDiagnosticMessages" - | "maxDiagnosticMessages" | "searchFiles" | "toggleApiConfigPin" | "setHistoryPreviewCollapsed" - | "hasOpenedModeSelector" | "accountButtonClicked" | "rooCloudSignIn" | "rooCloudSignOut" @@ -179,6 +158,7 @@ export interface WebviewMessage { | "indexingStatusUpdate" | "indexCleared" | "focusPanelRequest" + | "codebaseIndexConfig" | "profileThresholds" | "setHistoryPreviewCollapsed" | "openExternal" @@ -192,20 +172,9 @@ export interface WebviewMessage { | "fetchMarketplaceData" | "switchTab" | "profileThresholds" - | "shareTaskSuccess" - | "exportMode" - | "exportModeResult" - | "importMode" - | "importModeResult" - | "checkRulesDirectory" - | "checkRulesDirectoryResult" - | "saveCodeIndexSettingsAtomic" - | "requestCodeIndexSecretStatus" text?: string - editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" disabled?: boolean - context?: string dataUri?: string askResponse?: ClineAskResponse apiConfiguration?: ProviderSettings @@ -234,35 +203,13 @@ export interface WebviewMessage { ids?: string[] hasSystemPromptOverride?: boolean terminalOperation?: "continue" | "abort" - messageTs?: number historyPreviewCollapsed?: boolean filters?: { type?: string; search?: string; tags?: string[] } url?: string // For openExternal mpItem?: MarketplaceItem mpInstallOptions?: InstallMarketplaceItemOptions config?: Record // Add config to the payload - visibility?: ShareVisibility // For share visibility - hasContent?: boolean // For checkRulesDirectoryResult - checkOnly?: boolean // For deleteCustomMode check - codeIndexSettings?: { - // Global state settings - codebaseIndexEnabled: boolean - codebaseIndexQdrantUrl: string - codebaseIndexEmbedderProvider: "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" - codebaseIndexEmbedderBaseUrl?: string - codebaseIndexEmbedderModelId: string - codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers - codebaseIndexOpenAiCompatibleBaseUrl?: string - codebaseIndexSearchMaxResults?: number - codebaseIndexSearchMinScore?: number - - // Secret settings - codeIndexOpenAiKey?: string - codeIndexQdrantApiKey?: string - codebaseIndexOpenAiCompatibleApiKey?: string - codebaseIndexGeminiApiKey?: string - codebaseIndexMistralApiKey?: string - } + visibility?: "organization" | "public" // For share visibility } export const checkoutDiffPayloadSchema = z.object({ @@ -307,4 +254,3 @@ export type WebViewMessagePayload = | IndexingStatusPayload | IndexClearedPayload | InstallMarketplaceItemWithParametersPayload - | UpdateTodoListPayload diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index eca2dd4fe0..4093e8541d 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -5,8 +5,8 @@ export function checkExistKey(config: ProviderSettings | undefined) { return false } - // Special case for human-relay, fake-ai, and claude-code providers which don't need any configuration. - if (config.apiProvider && ["human-relay", "fake-ai", "claude-code"].includes(config.apiProvider)) { + // Special case for human-relay and fake-ai providers which don't need any configuration. + if (config.apiProvider === "human-relay" || config.apiProvider === "fake-ai") { return true } diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 168f4ff867..56d41f3c73 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -60,20 +60,7 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] { } // Main modes configuration as an ordered array -// Note: The first mode in this array is the default mode for new installations export const modes: readonly ModeConfig[] = [ - { - slug: "architect", - name: "🏗️ Architect", - roleDefinition: - "You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.", - whenToUse: - "Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.", - description: "Plan and design before implementation", - groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], - customInstructions: - "1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n - Specific and actionable\n - Listed in logical execution order\n - Focused on a single, well-defined outcome\n - Clear enough that another mode could execute it independently\n\n **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\"\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**", - }, { slug: "code", name: "💻 Code", @@ -81,9 +68,19 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.", whenToUse: "Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.", - description: "Write, modify, and refactor code", groups: ["read", "edit", "browser", "command", "mcp"], }, + { + slug: "architect", + name: "🏗️ Architect", + roleDefinition: + "You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.", + whenToUse: + "Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.", + groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], + customInstructions: + "1. Do some information gathering (for example using read_file or search_files) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\n\n4. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.\n\n5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.\n\n6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.", + }, { slug: "ask", name: "❓ Ask", @@ -91,7 +88,6 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.", whenToUse: "Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.", - description: "Get answers and explanations", groups: ["read", "browser", "mcp"], customInstructions: "You can analyze code, explain concepts, and access external resources. Always answer the user's questions thoroughly, and do not switch to implementing code unless explicitly requested by the user. Include Mermaid diagrams when they clarify your response.", @@ -103,7 +99,6 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", whenToUse: "Use this mode when you're troubleshooting issues, investigating errors, or diagnosing problems. Specialized in systematic debugging, adding logging, analyzing stack traces, and identifying root causes before applying fixes.", - description: "Diagnose and fix software issues", groups: ["read", "edit", "browser", "command", "mcp"], customInstructions: "Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.", @@ -115,7 +110,6 @@ export const modes: readonly ModeConfig[] = [ "You are Roo, a strategic workflow orchestrator who coordinates complex tasks by delegating them to appropriate specialized modes. You have a comprehensive understanding of each mode's capabilities and limitations, allowing you to effectively break down complex problems into discrete tasks that can be solved by different specialists.", whenToUse: "Use this mode for complex, multi-step projects that require coordination across different specialties. Ideal when you need to break down large tasks into subtasks, manage workflows, or coordinate work that spans multiple domains or expertise areas.", - description: "Coordinate tasks across multiple modes", groups: [], customInstructions: "Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\n\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\n\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask's specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\n * All necessary context from the parent task or previous subtasks required to complete the work.\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\n * A statement that these specific instructions supersede any conflicting general instructions the subtask's mode might have.\n\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\n\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you're delegating specific tasks to specific modes.\n\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\n\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\n\n7. Suggest improvements to the workflow based on the results of completed subtasks.\n\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating a subtask rather than overloading the current one.", @@ -183,41 +177,29 @@ export function findModeBySlug(slug: string, modes: readonly ModeConfig[] | unde /** * Get the mode selection based on the provided mode slug, prompt component, and custom modes. * If a custom mode is found, it takes precedence over the built-in modes. - * If no custom mode is found, the built-in mode is used with partial merging from promptComponent. + * If no custom mode is found, the built-in mode is used. * If neither is found, the default mode is used. */ export function getModeSelection(mode: string, promptComponent?: PromptComponent, customModes?: ModeConfig[]) { const customMode = findModeBySlug(mode, customModes) const builtInMode = findModeBySlug(mode, modes) - // If we have a custom mode, use it entirely - if (customMode) { - return { - roleDefinition: customMode.roleDefinition || "", - baseInstructions: customMode.customInstructions || "", - description: customMode.description || "", - } - } + const modeToUse = customMode || promptComponent || builtInMode - // Otherwise, use built-in mode as base and merge with promptComponent - const baseMode = builtInMode || modes[0] // fallback to default mode + const roleDefinition = modeToUse?.roleDefinition || "" + const baseInstructions = modeToUse?.customInstructions || "" return { - roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition || "", - baseInstructions: promptComponent?.customInstructions || baseMode.customInstructions || "", - description: baseMode.description || "", + roleDefinition, + baseInstructions, } } -// Edit operation parameters that indicate an actual edit operation -const EDIT_OPERATION_PARAMS = ["diff", "content", "operations", "search", "replace", "args", "line"] as const - // Custom error class for file restrictions export class FileRestrictionError extends Error { - constructor(mode: string, pattern: string, description: string | undefined, filePath: string, tool?: string) { - const toolInfo = tool ? `Tool '${tool}' in mode '${mode}'` : `This mode (${mode})` + constructor(mode: string, pattern: string, description: string | undefined, filePath: string) { super( - `${toolInfo} can only edit files matching pattern: ${pattern}${description ? ` (${description})` : ""}. Got: ${filePath}`, + `This mode (${mode}) can only edit files matching pattern: ${pattern}${description ? ` (${description})` : ""}. Got: ${filePath}`, ) this.name = "FileRestrictionError" } @@ -276,48 +258,12 @@ export function isToolAllowedForMode( // For the edit group, check file regex if specified if (groupName === "edit" && options.fileRegex) { const filePath = toolParams?.path - // Check if this is an actual edit operation (not just path-only for streaming) - const isEditOperation = EDIT_OPERATION_PARAMS.some((param) => toolParams?.[param]) - - // Handle single file path validation - if (filePath && isEditOperation && !doesFileMatchRegex(filePath, options.fileRegex)) { - throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath, tool) - } - - // Handle XML args parameter (used by MULTI_FILE_APPLY_DIFF experiment) - if (toolParams?.args && typeof toolParams.args === "string") { - // Extract file paths from XML args with improved validation - try { - const filePathMatches = toolParams.args.match(/([^<]+)<\/path>/g) - if (filePathMatches) { - for (const match of filePathMatches) { - // More robust path extraction with validation - const pathMatch = match.match(/([^<]+)<\/path>/) - if (pathMatch && pathMatch[1]) { - const extractedPath = pathMatch[1].trim() - // Validate that the path is not empty and doesn't contain invalid characters - if (extractedPath && !extractedPath.includes("<") && !extractedPath.includes(">")) { - if (!doesFileMatchRegex(extractedPath, options.fileRegex)) { - throw new FileRestrictionError( - mode.name, - options.fileRegex, - options.description, - extractedPath, - tool, - ) - } - } - } - } - } - } catch (error) { - // Re-throw FileRestrictionError as it's an expected validation error - if (error instanceof FileRestrictionError) { - throw error - } - // If XML parsing fails, log the error but don't block the operation - console.warn(`Failed to parse XML args for file restriction validation: ${error}`) - } + if ( + filePath && + (toolParams.diff || toolParams.content || toolParams.operations) && + !doesFileMatchRegex(filePath, options.fileRegex) + ) { + throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath) } } @@ -336,7 +282,6 @@ export const defaultPrompts: Readonly = Object.freeze( roleDefinition: mode.roleDefinition, whenToUse: mode.whenToUse, customInstructions: mode.customInstructions, - description: mode.description, }, ]), ), @@ -353,7 +298,6 @@ export async function getAllModesWithPrompts(context: vscode.ExtensionContext): roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition, whenToUse: customModePrompts[mode.slug]?.whenToUse ?? mode.whenToUse, customInstructions: customModePrompts[mode.slug]?.customInstructions ?? mode.customInstructions, - // description is not overridable via customModePrompts, so we keep the original })) } @@ -377,7 +321,6 @@ export async function getFullModeDetails( // Get the base custom instructions const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || "" const baseWhenToUse = promptComponent?.whenToUse || baseMode.whenToUse || "" - const baseDescription = promptComponent?.description || baseMode.description || "" // If we have cwd, load and combine all custom instructions let fullCustomInstructions = baseCustomInstructions @@ -396,7 +339,6 @@ export async function getFullModeDetails( ...baseMode, roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition, whenToUse: baseWhenToUse, - description: baseDescription, customInstructions: fullCustomInstructions, } } @@ -411,16 +353,6 @@ export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): return mode.roleDefinition } -// Helper function to safely get description -export function getDescription(modeSlug: string, customModes?: ModeConfig[]): string { - const mode = getModeBySlug(modeSlug, customModes) - if (!mode) { - console.warn(`No mode found for slug: ${modeSlug}`) - return "" - } - return mode.description ?? "" -} - // Helper function to safely get whenToUse export function getWhenToUse(modeSlug: string, customModes?: ModeConfig[]): string { const mode = getModeBySlug(modeSlug, customModes) diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 67972243fe..45077b86d7 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -1,8 +1,5 @@ -import { Anthropic } from "@anthropic-ai/sdk" - import type { ClineAsk, ToolProgressStatus, ToolGroup, ToolName } from "@roo-code/types" - -export type ToolResponse = string | Array +import { ToolDirective, ToolParamName, ToolResponse } from "../core/message-parsing/directives/" export type AskApproval = ( type: ClineAsk, @@ -21,149 +18,6 @@ export type AskFinishSubTaskApproval = () => Promise export type ToolDescription = () => string -export interface TextContent { - type: "text" - content: string - partial: boolean -} - -export const toolParamNames = [ - "command", - "path", - "content", - "line_count", - "regex", - "file_pattern", - "recursive", - "action", - "url", - "coordinate", - "text", - "server_name", - "tool_name", - "arguments", - "uri", - "question", - "result", - "diff", - "mode_slug", - "reason", - "line", - "mode", - "message", - "cwd", - "follow_up", - "task", - "size", - "search", - "replace", - "use_regex", - "ignore_case", - "args", - "start_line", - "end_line", - "query", - "args", - "todos", -] as const - -export type ToolParamName = (typeof toolParamNames)[number] - -export interface ToolUse { - type: "tool_use" - name: ToolName - // params is a partial record, allowing only some or none of the possible parameters to be used - params: Partial> - partial: boolean -} - -export interface ExecuteCommandToolUse extends ToolUse { - name: "execute_command" - // Pick, "command"> makes "command" required, but Partial<> makes it optional - params: Partial, "command" | "cwd">> -} - -export interface ReadFileToolUse extends ToolUse { - name: "read_file" - params: Partial, "args" | "path" | "start_line" | "end_line">> -} - -export interface FetchInstructionsToolUse extends ToolUse { - name: "fetch_instructions" - params: Partial, "task">> -} - -export interface WriteToFileToolUse extends ToolUse { - name: "write_to_file" - params: Partial, "path" | "content" | "line_count">> -} - -export interface InsertCodeBlockToolUse extends ToolUse { - name: "insert_content" - params: Partial, "path" | "line" | "content">> -} - -export interface CodebaseSearchToolUse extends ToolUse { - name: "codebase_search" - params: Partial, "query" | "path">> -} - -export interface SearchFilesToolUse extends ToolUse { - name: "search_files" - params: Partial, "path" | "regex" | "file_pattern">> -} - -export interface ListFilesToolUse extends ToolUse { - name: "list_files" - params: Partial, "path" | "recursive">> -} - -export interface ListCodeDefinitionNamesToolUse extends ToolUse { - name: "list_code_definition_names" - params: Partial, "path">> -} - -export interface BrowserActionToolUse extends ToolUse { - name: "browser_action" - params: Partial, "action" | "url" | "coordinate" | "text" | "size">> -} - -export interface UseMcpToolToolUse extends ToolUse { - name: "use_mcp_tool" - params: Partial, "server_name" | "tool_name" | "arguments">> -} - -export interface AccessMcpResourceToolUse extends ToolUse { - name: "access_mcp_resource" - params: Partial, "server_name" | "uri">> -} - -export interface AskFollowupQuestionToolUse extends ToolUse { - name: "ask_followup_question" - params: Partial, "question" | "follow_up">> -} - -export interface AttemptCompletionToolUse extends ToolUse { - name: "attempt_completion" - params: Partial, "result">> -} - -export interface SwitchModeToolUse extends ToolUse { - name: "switch_mode" - params: Partial, "mode_slug" | "reason">> -} - -export interface NewTaskToolUse extends ToolUse { - name: "new_task" - params: Partial, "mode" | "message">> -} - -export interface SearchAndReplaceToolUse extends ToolUse { - name: "search_and_replace" - params: Required, "path" | "search" | "replace">> & - Partial, "use_regex" | "ignore_case" | "start_line" | "end_line">> -} - // Define tool group configuration export type ToolGroupConfig = { tools: readonly string[] @@ -189,7 +43,6 @@ export const TOOL_DISPLAY_NAMES: Record = { insert_content: "insert content", search_and_replace: "search and replace", codebase_search: "codebase search", - update_todo_list: "update todo list", } as const // Define available tool groups. @@ -228,7 +81,6 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ "attempt_completion", "switch_mode", "new_task", - "update_todo_list", ] as const export type DiffResult = @@ -280,5 +132,5 @@ export interface DiffStrategy { endLine?: number, ): Promise - getProgressStatus?(toolUse: ToolUse, result?: any): ToolProgressStatus + getProgressStatus?(ToolDirective: ToolDirective, result?: any): ToolProgressStatus } diff --git a/src/utils/__tests__/git.spec.ts b/src/utils/__tests__/git.spec.ts index f87ae5667b..754d041e29 100644 --- a/src/utils/__tests__/git.spec.ts +++ b/src/utils/__tests__/git.spec.ts @@ -1,21 +1,6 @@ import { ExecException } from "child_process" -import * as vscode from "vscode" -import * as fs from "fs" -import * as path from "path" -import { - checkGitInstalled, - searchCommits, - getCommitInfo, - getWorkingState, - getGitRepositoryInfo, - sanitizeGitUrl, - extractRepositoryName, - getWorkspaceGitInfo, - GitRepositoryInfo, - convertGitUrlToHttps, -} from "../git" -import { truncateOutput } from "../../integrations/misc/extract-text" +import { searchCommits, getCommitInfo, getWorkingState } from "../git" type ExecFunction = ( command: string, @@ -30,24 +15,6 @@ vitest.mock("child_process", () => ({ exec: vitest.fn(), })) -// Mock fs.promises -vitest.mock("fs", () => ({ - promises: { - access: vitest.fn(), - readFile: vitest.fn(), - }, -})) - -// Create a mock for vscode -const mockWorkspaceFolders = vitest.fn() -vitest.mock("vscode", () => ({ - workspace: { - get workspaceFolders() { - return mockWorkspaceFolders() - }, - }, -})) - // Mock util.promisify to return our own mock function vitest.mock("util", () => ({ promisify: vitest.fn((fn: ExecFunction): PromisifiedExec => { @@ -84,54 +51,6 @@ describe("git utils", () => { vitest.clearAllMocks() }) - describe("checkGitInstalled", () => { - it("should return true when git --version succeeds", async () => { - vitest.mocked(exec).mockImplementation((command: string, options: any, callback: any) => { - if (command === "git --version") { - callback(null, { stdout: "git version 2.39.2", stderr: "" }) - return {} as any - } - callback(new Error("Unexpected command")) - return {} as any - }) - - const result = await checkGitInstalled() - expect(result).toBe(true) - expect(vitest.mocked(exec)).toHaveBeenCalledWith("git --version", {}, expect.any(Function)) - }) - - it("should return false when git --version fails", async () => { - vitest.mocked(exec).mockImplementation((command: string, options: any, callback: any) => { - if (command === "git --version") { - callback(new Error("git not found")) - return {} as any - } - callback(new Error("Unexpected command")) - return {} as any - }) - - const result = await checkGitInstalled() - expect(result).toBe(false) - expect(vitest.mocked(exec)).toHaveBeenCalledWith("git --version", {}, expect.any(Function)) - }) - - it("should handle unexpected errors gracefully", async () => { - vitest.mocked(exec).mockImplementation((command: string, options: any, callback: any) => { - if (command === "git --version") { - // Simulate an unexpected error - callback(new Error("Unexpected system error")) - return {} as any - } - callback(new Error("Unexpected command")) - return {} as any - }) - - const result = await checkGitInstalled() - expect(result).toBe(false) - expect(vitest.mocked(exec)).toHaveBeenCalledWith("git --version", {}, expect.any(Function)) - }) - }) - describe("searchCommits", () => { const mockCommitData = [ "abc123def456", @@ -250,6 +169,7 @@ describe("git utils", () => { if (command === cmd) { callback(null, response) return {} as any + return {} as any } } callback(new Error("Unexpected command")) @@ -297,6 +217,7 @@ describe("git utils", () => { if (command.startsWith(cmd)) { callback(null, response) return {} as any + return {} as any } } callback(new Error("Unexpected command")) @@ -308,7 +229,6 @@ describe("git utils", () => { expect(result).toContain("Author: John Doe") expect(result).toContain("Files Changed:") expect(result).toContain("Full Changes:") - expect(vitest.mocked(truncateOutput)).toHaveBeenCalled() }) it("should return error message when git is not installed", async () => { @@ -377,7 +297,6 @@ describe("git utils", () => { expect(result).toContain("Working directory changes:") expect(result).toContain("src/file1.ts") expect(result).toContain("src/file2.ts") - expect(vitest.mocked(truncateOutput)).toHaveBeenCalled() }) it("should return message when working directory is clean", async () => { @@ -392,6 +311,7 @@ describe("git utils", () => { if (command === cmd) { callback(null, response) return {} as any + return {} as any } } callback(new Error("Unexpected command")) @@ -441,398 +361,3 @@ describe("git utils", () => { }) }) }) - -describe("getGitRepositoryInfo", () => { - const workspaceRoot = "/test/workspace" - const gitDir = path.join(workspaceRoot, ".git") - const configPath = path.join(gitDir, "config") - const headPath = path.join(gitDir, "HEAD") - - beforeEach(() => { - vitest.clearAllMocks() - }) - - it("should return empty object when not a git repository", async () => { - // Mock fs.access to throw error (directory doesn't exist) - vitest.mocked(fs.promises.access).mockRejectedValueOnce(new Error("ENOENT")) - - const result = await getGitRepositoryInfo(workspaceRoot) - - expect(result).toEqual({}) - expect(fs.promises.access).toHaveBeenCalledWith(gitDir) - }) - - it("should extract repository info from git config", async () => { - // Clear previous mocks - vitest.clearAllMocks() - - // Create a spy to track the implementation - const gitSpy = vitest.spyOn(fs.promises, "readFile") - - // Mock successful access to .git directory - vitest.mocked(fs.promises.access).mockResolvedValue(undefined) - - // Mock git config file content - const mockConfig = ` -[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - ignorecase = true - precomposeunicode = true -[remote "origin"] - url = https://github.com/RooCodeInc/Roo-Code.git - fetch = +refs/heads/*:refs/remotes/origin/* -[branch "main"] - remote = origin - merge = refs/heads/main -` - // Mock HEAD file content - const mockHead = "ref: refs/heads/main" - - // Setup the readFile mock to return different values based on the path - gitSpy.mockImplementation((path: any, encoding: any) => { - if (path === configPath) { - return Promise.resolve(mockConfig) - } else if (path === headPath) { - return Promise.resolve(mockHead) - } - return Promise.reject(new Error(`Unexpected path: ${path}`)) - }) - - const result = await getGitRepositoryInfo(workspaceRoot) - - expect(result).toEqual({ - repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", - repositoryName: "RooCodeInc/Roo-Code", - defaultBranch: "main", - }) - - // Verify config file was read - expect(gitSpy).toHaveBeenCalledWith(configPath, "utf8") - - // The implementation might not always read the HEAD file if it already found the branch in config - // So we don't assert that it was called - }) - - it("should handle missing repository URL in config", async () => { - // Clear previous mocks - vitest.clearAllMocks() - - // Create a spy to track the implementation - const gitSpy = vitest.spyOn(fs.promises, "readFile") - - // Mock successful access to .git directory - vitest.mocked(fs.promises.access).mockResolvedValue(undefined) - - // Mock git config file without URL - const mockConfig = ` -[core] - repositoryformatversion = 0 - filemode = true - bare = false -` - // Mock HEAD file content - const mockHead = "ref: refs/heads/main" - - // Setup the readFile mock to return different values based on the path - gitSpy.mockImplementation((path: any, encoding: any) => { - if (path === configPath) { - return Promise.resolve(mockConfig) - } else if (path === headPath) { - return Promise.resolve(mockHead) - } - return Promise.reject(new Error(`Unexpected path: ${path}`)) - }) - - const result = await getGitRepositoryInfo(workspaceRoot) - - expect(result).toEqual({ - defaultBranch: "main", - }) - }) - - it("should handle errors when reading git config", async () => { - // Clear previous mocks - vitest.clearAllMocks() - - // Create a spy to track the implementation - const gitSpy = vitest.spyOn(fs.promises, "readFile") - - // Mock successful access to .git directory - vitest.mocked(fs.promises.access).mockResolvedValue(undefined) - - // Setup the readFile mock to return different values based on the path - gitSpy.mockImplementation((path: any, encoding: any) => { - if (path === configPath) { - return Promise.reject(new Error("Failed to read config")) - } else if (path === headPath) { - return Promise.resolve("ref: refs/heads/main") - } - return Promise.reject(new Error(`Unexpected path: ${path}`)) - }) - - const result = await getGitRepositoryInfo(workspaceRoot) - - expect(result).toEqual({ - defaultBranch: "main", - }) - }) - - it("should handle errors when reading HEAD file", async () => { - // Clear previous mocks - vitest.clearAllMocks() - - // Create a spy to track the implementation - const gitSpy = vitest.spyOn(fs.promises, "readFile") - - // Mock successful access to .git directory - vitest.mocked(fs.promises.access).mockResolvedValue(undefined) - - // Setup the readFile mock to return different values based on the path - gitSpy.mockImplementation((path: any, encoding: any) => { - if (path === configPath) { - return Promise.resolve(` -[remote "origin"] - url = https://github.com/RooCodeInc/Roo-Code.git -`) - } else if (path === headPath) { - return Promise.reject(new Error("Failed to read HEAD")) - } - return Promise.reject(new Error(`Unexpected path: ${path}`)) - }) - - const result = await getGitRepositoryInfo(workspaceRoot) - - expect(result).toEqual({ - repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", - repositoryName: "RooCodeInc/Roo-Code", - }) - }) - - it("should convert SSH URLs to HTTPS format", async () => { - // Clear previous mocks - vitest.clearAllMocks() - - // Create a spy to track the implementation - const gitSpy = vitest.spyOn(fs.promises, "readFile") - - // Mock successful access to .git directory - vitest.mocked(fs.promises.access).mockResolvedValue(undefined) - - // Mock git config file with SSH URL - const mockConfig = ` -[core] - repositoryformatversion = 0 - filemode = true - bare = false -[remote "origin"] - url = git@github.com:RooCodeInc/Roo-Code.git - fetch = +refs/heads/*:refs/remotes/origin/* -[branch "main"] - remote = origin - merge = refs/heads/main -` - // Mock HEAD file content - const mockHead = "ref: refs/heads/main" - - // Setup the readFile mock to return different values based on the path - gitSpy.mockImplementation((path: any, encoding: any) => { - if (path === configPath) { - return Promise.resolve(mockConfig) - } else if (path === headPath) { - return Promise.resolve(mockHead) - } - return Promise.reject(new Error(`Unexpected path: ${path}`)) - }) - - const result = await getGitRepositoryInfo(workspaceRoot) - - // Verify that the SSH URL was converted to HTTPS - expect(result).toEqual({ - repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", - repositoryName: "RooCodeInc/Roo-Code", - defaultBranch: "main", - }) - }) -}) - -describe("convertGitUrlToHttps", () => { - it("should leave HTTPS URLs unchanged", () => { - const url = "https://github.com/RooCodeInc/Roo-Code.git" - const converted = convertGitUrlToHttps(url) - - expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") - }) - - it("should convert SSH URLs to HTTPS format", () => { - const url = "git@github.com:RooCodeInc/Roo-Code.git" - const converted = convertGitUrlToHttps(url) - - expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") - }) - - it("should convert SSH URLs with ssh:// prefix to HTTPS format", () => { - const url = "ssh://git@github.com/RooCodeInc/Roo-Code.git" - const converted = convertGitUrlToHttps(url) - - expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") - }) - - it("should handle URLs without git@ prefix", () => { - const url = "ssh://github.com/RooCodeInc/Roo-Code.git" - const converted = convertGitUrlToHttps(url) - - expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") - }) - - it("should handle invalid URLs gracefully", () => { - const url = "not-a-valid-url" - const converted = convertGitUrlToHttps(url) - - expect(converted).toBe("not-a-valid-url") - }) -}) - -describe("sanitizeGitUrl", () => { - it("should sanitize HTTPS URLs with credentials", () => { - const url = "https://username:password@github.com/RooCodeInc/Roo-Code.git" - const sanitized = sanitizeGitUrl(url) - - expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git") - }) - - it("should leave SSH URLs unchanged", () => { - const url = "git@github.com:RooCodeInc/Roo-Code.git" - const sanitized = sanitizeGitUrl(url) - - expect(sanitized).toBe("git@github.com:RooCodeInc/Roo-Code.git") - }) - - it("should leave SSH URLs with ssh:// prefix unchanged", () => { - const url = "ssh://git@github.com/RooCodeInc/Roo-Code.git" - const sanitized = sanitizeGitUrl(url) - - expect(sanitized).toBe("ssh://git@github.com/RooCodeInc/Roo-Code.git") - }) - - it("should remove tokens from other URL formats", () => { - const url = "https://oauth2:ghp_abcdef1234567890abcdef1234567890abcdef@github.com/RooCodeInc/Roo-Code.git" - const sanitized = sanitizeGitUrl(url) - - expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git") - }) - - it("should handle invalid URLs gracefully", () => { - const url = "not-a-valid-url" - const sanitized = sanitizeGitUrl(url) - - expect(sanitized).toBe("not-a-valid-url") - }) -}) - -describe("extractRepositoryName", () => { - it("should extract repository name from HTTPS URL", () => { - const url = "https://github.com/RooCodeInc/Roo-Code.git" - const repoName = extractRepositoryName(url) - - expect(repoName).toBe("RooCodeInc/Roo-Code") - }) - - it("should extract repository name from HTTPS URL without .git suffix", () => { - const url = "https://github.com/RooCodeInc/Roo-Code" - const repoName = extractRepositoryName(url) - - expect(repoName).toBe("RooCodeInc/Roo-Code") - }) - - it("should extract repository name from SSH URL", () => { - const url = "git@github.com:RooCodeInc/Roo-Code.git" - const repoName = extractRepositoryName(url) - - expect(repoName).toBe("RooCodeInc/Roo-Code") - }) - - it("should extract repository name from SSH URL with ssh:// prefix", () => { - const url = "ssh://git@github.com/RooCodeInc/Roo-Code.git" - const repoName = extractRepositoryName(url) - - expect(repoName).toBe("RooCodeInc/Roo-Code") - }) - - it("should return empty string for unrecognized URL formats", () => { - const url = "not-a-valid-git-url" - const repoName = extractRepositoryName(url) - - expect(repoName).toBe("") - }) - - it("should handle URLs with credentials", () => { - const url = "https://username:password@github.com/RooCodeInc/Roo-Code.git" - const repoName = extractRepositoryName(url) - - expect(repoName).toBe("RooCodeInc/Roo-Code") - }) -}) - -describe("getWorkspaceGitInfo", () => { - const workspaceRoot = "/test/workspace" - - beforeEach(() => { - vitest.clearAllMocks() - }) - - it("should return empty object when no workspace folders", async () => { - // Mock workspace with no folders - mockWorkspaceFolders.mockReturnValue(undefined) - - const result = await getWorkspaceGitInfo() - - expect(result).toEqual({}) - }) - - it("should return git info for the first workspace folder", async () => { - // Clear previous mocks - vitest.clearAllMocks() - - // Mock workspace with one folder - mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: workspaceRoot }, name: "workspace", index: 0 }]) - - // Create a spy to track the implementation - const gitSpy = vitest.spyOn(fs.promises, "access") - const readFileSpy = vitest.spyOn(fs.promises, "readFile") - - // Mock successful access to .git directory - gitSpy.mockResolvedValue(undefined) - - // Mock git config file content - const mockConfig = ` -[remote "origin"] - url = https://github.com/RooCodeInc/Roo-Code.git -[branch "main"] - remote = origin - merge = refs/heads/main -` - - // Setup the readFile mock to return config content - readFileSpy.mockImplementation((path: any, encoding: any) => { - if (path.includes("config")) { - return Promise.resolve(mockConfig) - } - return Promise.reject(new Error(`Unexpected path: ${path}`)) - }) - - const result = await getWorkspaceGitInfo() - - expect(result).toEqual({ - repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", - repositoryName: "RooCodeInc/Roo-Code", - defaultBranch: "main", - }) - - // Verify the fs operations were called with the correct workspace path - expect(gitSpy).toHaveBeenCalled() - expect(readFileSpy).toHaveBeenCalled() - }) -}) diff --git a/src/utils/__tests__/safeWriteJson.test.ts b/src/utils/__tests__/safeWriteJson.test.ts deleted file mode 100644 index f3b687595a..0000000000 --- a/src/utils/__tests__/safeWriteJson.test.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { vi, describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest" -import * as actualFsPromises from "fs/promises" -import * as fsSyncActual from "fs" -import { Writable } from "stream" -import { safeWriteJson } from "../safeWriteJson" -import * as path from "path" -import * as os from "os" - -const originalFsPromisesRename = actualFsPromises.rename -const originalFsPromisesUnlink = actualFsPromises.unlink -const originalFsPromisesWriteFile = actualFsPromises.writeFile -const _originalFsPromisesAccess = actualFsPromises.access -const originalFsPromisesMkdir = actualFsPromises.mkdir - -vi.mock("fs/promises", async () => { - const actual = await vi.importActual("fs/promises") - // Start with all actual implementations. - const mockedFs = { ...actual } - // Selectively wrap functions with vi.fn() if they are spied on - // or have their implementations changed in tests. - // This ensures that other fs.promises functions used by the SUT - // (like proper-lockfile's internals) will use their actual implementations. - mockedFs.writeFile = vi.fn(actual.writeFile) as any - mockedFs.readFile = vi.fn(actual.readFile) as any - mockedFs.rename = vi.fn(actual.rename) as any - mockedFs.unlink = vi.fn(actual.unlink) as any - mockedFs.access = vi.fn(actual.access) as any - mockedFs.mkdtemp = vi.fn(actual.mkdtemp) as any - mockedFs.rm = vi.fn(actual.rm) as any - mockedFs.readdir = vi.fn(actual.readdir) as any - mockedFs.mkdir = vi.fn(actual.mkdir) as any - // fs.stat and fs.lstat will be available via { ...actual } - - return mockedFs -}) - -// Mock the 'fs' module for fsSync.createWriteStream -vi.mock("fs", async () => { - const actualFs = await vi.importActual("fs") - return { - ...actualFs, // Spread actual implementations - createWriteStream: vi.fn(actualFs.createWriteStream) as any, // Default to actual, but mockable - } -}) - -import * as fs from "fs/promises" // This will now be the mocked version - -describe("safeWriteJson", () => { - let originalConsoleError: typeof console.error - - beforeAll(() => { - // Store original console.error - originalConsoleError = console.error - }) - - afterAll(() => { - // Restore original console.error - console.error = originalConsoleError - }) - - let tempDir: string - let currentTestFilePath: string - - beforeEach(async () => { - // Create a temporary directory for each test - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "safeWriteJson-test-")) - - // Create a unique file path for each test - currentTestFilePath = path.join(tempDir, "test-file.json") - - // Pre-create the file with initial content to ensure it exists - // This allows proper-lockfile to acquire a lock on an existing file. - await fs.writeFile(currentTestFilePath, JSON.stringify({ initial: "content" })) - }) - - afterEach(async () => { - // Clean up the temporary directory after each test - await fs.rm(tempDir, { recursive: true, force: true }) - - // Reset all mocks to their actual implementations - vi.restoreAllMocks() - }) - - // Helper function to read file content - async function readFileContent(filePath: string): Promise { - const readContent = await fs.readFile(filePath, "utf-8") - return JSON.parse(readContent) - } - - // Helper function to check if a file exists - async function fileExists(filePath: string): Promise { - try { - await fs.access(filePath) - return true - } catch { - return false - } - } - - // Success Scenarios - // Note: Since we pre-create the file in beforeEach, this test will overwrite it. - // If "creation from non-existence" is critical and locking prevents it, safeWriteJson or locking strategy needs review. - test("should successfully write a new file (overwriting initial content from beforeEach)", async () => { - const data = { message: "Hello, new world!" } - - await safeWriteJson(currentTestFilePath, data) - - const content = await readFileContent(currentTestFilePath) - expect(content).toEqual(data) - }) - - test("should successfully overwrite an existing file", async () => { - const initialData = { message: "Initial content" } - const newData = { message: "Updated content" } - - // Write initial data (overwriting the pre-created file from beforeEach) - await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) - - await safeWriteJson(currentTestFilePath, newData) - - const content = await readFileContent(currentTestFilePath) - expect(content).toEqual(newData) - }) - - // Failure Scenarios - test("should handle failure when writing to tempNewFilePath", async () => { - // currentTestFilePath exists due to beforeEach, allowing lock acquisition. - const data = { message: "test write failure" } - - const mockErrorStream = new Writable() as any - mockErrorStream._write = (_chunk: any, _encoding: any, callback: any) => { - callback(new Error("Write stream error")) - } - // Add missing WriteStream properties - mockErrorStream.close = vi.fn() - mockErrorStream.bytesWritten = 0 - mockErrorStream.path = "" - mockErrorStream.pending = false - - // Mock createWriteStream to return a stream that errors on write - ;(fsSyncActual.createWriteStream as any).mockImplementationOnce((_path: any, _options: any) => { - return mockErrorStream - }) - - await expect(safeWriteJson(currentTestFilePath, data)).rejects.toThrow("Write stream error") - - // Verify the original file still exists and is unchanged - const exists = await fileExists(currentTestFilePath) - expect(exists).toBe(true) - - // Verify content is unchanged (should still have the initial content from beforeEach) - const content = await readFileContent(currentTestFilePath) - expect(content).toEqual({ initial: "content" }) - }) - - test("should handle failure when renaming filePath to tempBackupFilePath (filePath exists)", async () => { - const initialData = { message: "Initial content, should remain" } - const newData = { message: "New content, should not be written" } - - // Overwrite the pre-created file with specific initial data - await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) - - const renameSpy = vi.spyOn(fs, "rename") - - // Mock rename to fail on the first call (filePath -> tempBackupFilePath) - renameSpy.mockImplementationOnce(async () => { - throw new Error("Rename to backup failed") - }) - - await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Rename to backup failed") - - // Verify the original file still exists with initial content - const content = await readFileContent(currentTestFilePath) - expect(content).toEqual(initialData) - }) - - test("should handle failure when renaming tempNewFilePath to filePath (filePath exists, backup succeeded)", async () => { - const initialData = { message: "Initial content, should be restored" } - const newData = { message: "New content" } - - // Overwrite the pre-created file with specific initial data - await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) - - const renameSpy = vi.spyOn(fs, "rename") - - // Track rename calls - let renameCallCount = 0 - - // Mock rename to succeed on first call (filePath -> tempBackupFilePath) - // and fail on second call (tempNewFilePath -> filePath) - renameSpy.mockImplementation(async (oldPath, newPath) => { - renameCallCount++ - if (renameCallCount === 1) { - // First call: filePath -> tempBackupFilePath (should succeed) - return originalFsPromisesRename(oldPath, newPath) - } else if (renameCallCount === 2) { - // Second call: tempNewFilePath -> filePath (should fail) - throw new Error("Rename from temp to final failed") - } else if (renameCallCount === 3) { - // Third call: tempBackupFilePath -> filePath (rollback, should succeed) - return originalFsPromisesRename(oldPath, newPath) - } - // Default: use original implementation - return originalFsPromisesRename(oldPath, newPath) - }) - - await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Rename from temp to final failed") - - // Verify the file was restored to initial content - const content = await readFileContent(currentTestFilePath) - expect(content).toEqual(initialData) - }) - - // Tests for directory creation functionality - test("should create parent directory if it doesn't exist", async () => { - // Create a path in a non-existent subdirectory of the temp dir - const subDir = path.join(tempDir, "new-subdir") - const filePath = path.join(subDir, "file.json") - const data = { test: "directory creation" } - - // Verify directory doesn't exist - await expect(fs.access(subDir)).rejects.toThrow() - - // Write file - await safeWriteJson(filePath, data) - - // Verify directory was created - await expect(fs.access(subDir)).resolves.toBeUndefined() - - // Verify file was written - const content = await readFileContent(filePath) - expect(content).toEqual(data) - }) - - test("should handle multi-level directory creation", async () => { - // Create a new non-existent subdirectory path with multiple levels - const deepDir = path.join(tempDir, "level1", "level2", "level3") - const filePath = path.join(deepDir, "deep-file.json") - const data = { nested: "deeply" } - - // Verify none of the directories exist - await expect(fs.access(path.join(tempDir, "level1"))).rejects.toThrow() - - // Write file - await safeWriteJson(filePath, data) - - // Verify all directories were created - await expect(fs.access(path.join(tempDir, "level1"))).resolves.toBeUndefined() - await expect(fs.access(path.join(tempDir, "level1", "level2"))).resolves.toBeUndefined() - await expect(fs.access(deepDir)).resolves.toBeUndefined() - - // Verify file was written - const content = await readFileContent(filePath) - expect(content).toEqual(data) - }) - - test("should handle directory creation permission errors", async () => { - // Mock mkdir to simulate a permission error - const mkdirSpy = vi.spyOn(fs, "mkdir") - mkdirSpy.mockImplementationOnce(async () => { - const error = new Error("EACCES: permission denied") as any - error.code = "EACCES" - throw error - }) - - const subDir = path.join(tempDir, "forbidden-dir") - const filePath = path.join(subDir, "file.json") - const data = { test: "permission error" } - - // Should throw the permission error - await expect(safeWriteJson(filePath, data)).rejects.toThrow("EACCES: permission denied") - - // Verify directory was not created - await expect(fs.access(subDir)).rejects.toThrow() - }) - - test("should successfully write to a non-existent file in an existing directory", async () => { - // Create directory but not the file - const subDir = path.join(tempDir, "existing-dir") - await fs.mkdir(subDir) - - const filePath = path.join(subDir, "new-file.json") - const data = { fresh: "file" } - - // Verify file doesn't exist yet - await expect(fs.access(filePath)).rejects.toThrow() - - // Write file - await safeWriteJson(filePath, data) - - // Verify file was created with correct content - const content = await readFileContent(filePath) - expect(content).toEqual(data) - }) - - test("should handle failure when deleting tempBackupFilePath (filePath exists, all renames succeed)", async () => { - const initialData = { message: "Initial content" } - const newData = { message: "Successfully written new content" } - - // Overwrite the pre-created file with specific initial data - await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) - - const unlinkSpy = vi.spyOn(fs, "unlink") - - // Mock unlink to fail when trying to delete the backup file - unlinkSpy.mockImplementationOnce(async () => { - throw new Error("Failed to delete backup file") - }) - - // The write should succeed even if backup deletion fails - await safeWriteJson(currentTestFilePath, newData) - - // Verify the new content was written successfully - const content = await readFileContent(currentTestFilePath) - expect(content).toEqual(newData) - }) - - // Test for console error suppression during backup deletion - test("should suppress console.error when backup deletion fails", async () => { - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) // Suppress console.error - const initialData = { message: "Initial" } - const newData = { message: "New" } - - await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) - - // Mock unlink to fail when deleting backup files - const unlinkSpy = vi.spyOn(fs, "unlink") - unlinkSpy.mockImplementation(async (filePath: any) => { - if (filePath.toString().includes(".bak_")) { - throw new Error("Backup deletion failed") - } - return originalFsPromisesUnlink(filePath) - }) - - await safeWriteJson(currentTestFilePath, newData) - - // Verify console.error was called with the expected message - expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Successfully wrote"), expect.any(Error)) - - consoleErrorSpy.mockRestore() - unlinkSpy.mockRestore() - }) - - // The expected error message might need to change if the mock behaves differently. - test("should handle failure when renaming tempNewFilePath to filePath (filePath initially exists)", async () => { - // currentTestFilePath exists due to beforeEach. - const initialData = { message: "Initial content" } - const newData = { message: "New content" } - - await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) - - const renameSpy = vi.spyOn(fs, "rename") - // Mock rename to fail on the second call (tempNewFilePath -> filePath) - // This test assumes that the first rename (filePath -> tempBackupFilePath) succeeds, - // which is the expected behavior when the file exists. - // The existing complex mock in `test("should handle failure when renaming tempNewFilePath to filePath (filePath exists, backup succeeded)"` - // might be more relevant or adaptable here. - - let renameCallCount = 0 - renameSpy.mockImplementation(async (oldPath, newPath) => { - renameCallCount++ - if (renameCallCount === 2) { - // Second call: tempNewFilePath -> filePath (should fail) - throw new Error("Rename failed") - } - // For all other calls, use the original implementation - return originalFsPromisesRename(oldPath, newPath) - }) - - await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Rename failed") - - // The file should be restored to its initial content - const content = await readFileContent(currentTestFilePath) - expect(content).toEqual(initialData) - }) - - test("should throw an error if an inter-process lock is already held for the filePath", async () => { - vi.resetModules() // Clear module cache to ensure fresh imports for this test - - const data = { message: "test lock failure" } - - // Create a new file path for this specific test to avoid conflicts - const lockTestFilePath = path.join(tempDir, "lock-test-file.json") - await fs.writeFile(lockTestFilePath, JSON.stringify({ initial: "lock test content" })) - - vi.doMock("proper-lockfile", () => ({ - ...vi.importActual("proper-lockfile"), - lock: vi.fn().mockRejectedValueOnce(new Error("Failed to get lock.")), - })) - - // Re-import safeWriteJson to use the mocked proper-lockfile - const { safeWriteJson: mockedSafeWriteJson } = await import("../safeWriteJson") - - await expect(mockedSafeWriteJson(lockTestFilePath, data)).rejects.toThrow("Failed to get lock.") - - // Clean up - await fs.unlink(lockTestFilePath).catch(() => {}) // Ignore errors if file doesn't exist - vi.unmock("proper-lockfile") // Ensure the mock is removed after this test - }) - test("should release lock even if an error occurs mid-operation", async () => { - const data = { message: "test lock release on error" } - - // Mock createWriteStream to throw an error - const createWriteStreamSpy = vi.spyOn(fsSyncActual, "createWriteStream") - createWriteStreamSpy.mockImplementationOnce((_path: any, _options: any) => { - const errorStream = new Writable() as any - errorStream._write = (_chunk: any, _encoding: any, callback: any) => { - callback(new Error("Stream write error")) - } - // Add missing WriteStream properties - errorStream.close = vi.fn() - errorStream.bytesWritten = 0 - errorStream.path = _path - errorStream.pending = false - return errorStream - }) - - // This should throw but still release the lock - await expect(safeWriteJson(currentTestFilePath, data)).rejects.toThrow("Stream write error") - - // Reset the mock to allow the second call to work normally - createWriteStreamSpy.mockRestore() - - // If the lock wasn't released, this second attempt would fail with a lock error - // Instead, it should succeed (proving the lock was released) - await expect(safeWriteJson(currentTestFilePath, data)).resolves.toBeUndefined() - }) - - test("should handle fs.access error that is not ENOENT", async () => { - const data = { message: "access error test" } - const accessSpy = vi.spyOn(fs, "access").mockImplementationOnce(async () => { - const error = new Error("EACCES: permission denied") as any - error.code = "EACCES" - throw error - }) - - // Create a path that will trigger the access check - const testPath = path.join(tempDir, "access-error-test.json") - - await expect(safeWriteJson(testPath, data)).rejects.toThrow("EACCES: permission denied") - - // Verify access was called - expect(accessSpy).toHaveBeenCalled() - }) - - // Test for rollback failure scenario - test("should log error and re-throw original if rollback fails", async () => { - const initialData = { message: "Initial, should be lost if rollback fails" } - const newData = { message: "New content" } - - await originalFsPromisesWriteFile(currentTestFilePath, JSON.stringify(initialData)) - - const renameSpy = vi.spyOn(fs, "rename") - const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) // Suppress console.error - - let renameCallCount = 0 - renameSpy.mockImplementation(async (oldPath, newPath) => { - renameCallCount++ - if (renameCallCount === 2) { - // Second call: tempNewFilePath -> filePath (fail) - throw new Error("Primary rename failed") - } else if (renameCallCount === 3) { - // Third call: tempBackupFilePath -> filePath (rollback, also fail) - throw new Error("Rollback rename failed") - } - return originalFsPromisesRename(oldPath, newPath) - }) - - // Should throw the original error, not the rollback error - await expect(safeWriteJson(currentTestFilePath, newData)).rejects.toThrow("Primary rename failed") - - // Verify console.error was called for the rollback failure - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining("Failed to restore backup"), - expect.objectContaining({ message: "Rollback rename failed" }), - ) - - consoleErrorSpy.mockRestore() - }) -}) diff --git a/src/utils/git.ts b/src/utils/git.ts index 42d069416e..640af7fd29 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,6 +1,3 @@ -import * as vscode from "vscode" -import * as path from "path" -import { promises as fs } from "fs" import { exec } from "child_process" import { promisify } from "util" import { truncateOutput } from "../integrations/misc/extract-text" @@ -8,12 +5,6 @@ import { truncateOutput } from "../integrations/misc/extract-text" const execAsync = promisify(exec) const GIT_OUTPUT_LINE_LIMIT = 500 -export interface GitRepositoryInfo { - repositoryUrl?: string - repositoryName?: string - defaultBranch?: string -} - export interface GitCommit { hash: string shortHash: string @@ -22,185 +13,6 @@ export interface GitCommit { date: string } -/** - * Extracts git repository information from the workspace's .git directory - * @param workspaceRoot The root path of the workspace - * @returns Git repository information or empty object if not a git repository - */ -export async function getGitRepositoryInfo(workspaceRoot: string): Promise { - try { - const gitDir = path.join(workspaceRoot, ".git") - - // Check if .git directory exists - try { - await fs.access(gitDir) - } catch { - // Not a git repository - return {} - } - - const gitInfo: GitRepositoryInfo = {} - - // Try to read git config file - try { - const configPath = path.join(gitDir, "config") - const configContent = await fs.readFile(configPath, "utf8") - - // Very simple approach - just find any URL line - const urlMatch = configContent.match(/url\s*=\s*(.+?)(?:\r?\n|$)/m) - - if (urlMatch && urlMatch[1]) { - const url = urlMatch[1].trim() - // Sanitize the URL and convert to HTTPS format for telemetry - gitInfo.repositoryUrl = convertGitUrlToHttps(sanitizeGitUrl(url)) - const repositoryName = extractRepositoryName(url) - if (repositoryName) { - gitInfo.repositoryName = repositoryName - } - } - - // Extract default branch (if available) - const branchMatch = configContent.match(/\[branch "([^"]+)"\]/i) - if (branchMatch && branchMatch[1]) { - gitInfo.defaultBranch = branchMatch[1] - } - } catch (error) { - // Ignore config reading errors - } - - // Try to read HEAD file to get current branch - if (!gitInfo.defaultBranch) { - try { - const headPath = path.join(gitDir, "HEAD") - const headContent = await fs.readFile(headPath, "utf8") - const branchMatch = headContent.match(/ref: refs\/heads\/(.+)/) - if (branchMatch && branchMatch[1]) { - gitInfo.defaultBranch = branchMatch[1].trim() - } - } catch (error) { - // Ignore HEAD reading errors - } - } - - return gitInfo - } catch (error) { - // Return empty object on any error - return {} - } -} - -/** - * Converts a git URL to HTTPS format - * @param url The git URL to convert - * @returns The URL in HTTPS format, or the original URL if conversion is not possible - */ -export function convertGitUrlToHttps(url: string): string { - try { - // Already HTTPS, just return it - if (url.startsWith("https://")) { - return url - } - - // Handle SSH format: git@github.com:user/repo.git -> https://github.com/user/repo.git - if (url.startsWith("git@")) { - const match = url.match(/git@([^:]+):(.+)/) - if (match && match.length === 3) { - const [, host, path] = match - return `https://${host}/${path}` - } - } - - // Handle SSH with protocol: ssh://git@github.com/user/repo.git -> https://github.com/user/repo.git - if (url.startsWith("ssh://")) { - const match = url.match(/ssh:\/\/(?:git@)?([^\/]+)\/(.+)/) - if (match && match.length === 3) { - const [, host, path] = match - return `https://${host}/${path}` - } - } - - // Return original URL if we can't convert it - return url - } catch { - // If parsing fails, return original - return url - } -} - -/** - * Sanitizes a git URL to remove sensitive information like tokens - * @param url The original git URL - * @returns Sanitized URL - */ -export function sanitizeGitUrl(url: string): string { - try { - // Remove credentials from HTTPS URLs - if (url.startsWith("https://")) { - const urlObj = new URL(url) - // Remove username and password - urlObj.username = "" - urlObj.password = "" - return urlObj.toString() - } - - // For SSH URLs, return as-is (they don't contain sensitive tokens) - if (url.startsWith("git@") || url.startsWith("ssh://")) { - return url - } - - // For other formats, return as-is but remove any potential tokens - return url.replace(/:[a-f0-9]{40,}@/gi, "@") - } catch { - // If URL parsing fails, return original (might be SSH format) - return url - } -} - -/** - * Extracts repository name from a git URL - * @param url The git URL - * @returns Repository name or undefined - */ -export function extractRepositoryName(url: string): string { - try { - // Handle different URL formats - const patterns = [ - // HTTPS: https://github.com/user/repo.git -> user/repo - /https:\/\/[^\/]+\/([^\/]+\/[^\/]+?)(?:\.git)?$/, - // SSH: git@github.com:user/repo.git -> user/repo - /git@[^:]+:([^\/]+\/[^\/]+?)(?:\.git)?$/, - // SSH with user: ssh://git@github.com/user/repo.git -> user/repo - /ssh:\/\/[^\/]+\/([^\/]+\/[^\/]+?)(?:\.git)?$/, - ] - - for (const pattern of patterns) { - const match = url.match(pattern) - if (match && match[1]) { - return match[1].replace(/\.git$/, "") - } - } - - return "" - } catch { - return "" - } -} - -/** - * Gets git repository information for the current VSCode workspace - * @returns Git repository information or empty object if not available - */ -export async function getWorkspaceGitInfo(): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders - if (!workspaceFolders || workspaceFolders.length === 0) { - return {} - } - - // Use the first workspace folder - const workspaceRoot = workspaceFolders[0].uri.fsPath - return getGitRepositoryInfo(workspaceRoot) -} - async function checkGitRepo(cwd: string): Promise { try { await execAsync("git rev-parse --git-dir", { cwd }) @@ -210,16 +22,7 @@ async function checkGitRepo(cwd: string): Promise { } } -/** - * Checks if Git is installed on the system by attempting to run git --version - * @returns {Promise} True if Git is installed and accessible, false otherwise - * @example - * const isGitInstalled = await checkGitInstalled(); - * if (!isGitInstalled) { - * console.log("Git is not installed"); - * } - */ -export async function checkGitInstalled(): Promise { +async function checkGitInstalled(): Promise { try { await execAsync("git --version") return true diff --git a/src/utils/migrateSettings.ts b/src/utils/migrateSettings.ts index 43b1d7291f..406e5bd051 100644 --- a/src/utils/migrateSettings.ts +++ b/src/utils/migrateSettings.ts @@ -92,8 +92,8 @@ async function migrateCustomModesToYaml(settingsDir: string, outputChannel: vsco // Parse JSON to object (using the yaml library just to be safe/consistent) const customModesData = yaml.parse(jsonContent) - // Convert to YAML with no line width limit to prevent line breaks - const yamlContent = yaml.stringify(customModesData, { lineWidth: 0 }) + // Convert to YAML + const yamlContent = yaml.stringify(customModesData) // Write YAML file await fs.writeFile(newYamlPath, yamlContent, "utf-8") diff --git a/src/utils/safeWriteJson.ts b/src/utils/safeWriteJson.ts deleted file mode 100644 index 719bbd7216..0000000000 --- a/src/utils/safeWriteJson.ts +++ /dev/null @@ -1,235 +0,0 @@ -import * as fs from "fs/promises" -import * as fsSync from "fs" -import * as path from "path" -import * as lockfile from "proper-lockfile" -import Disassembler from "stream-json/Disassembler" -import Stringer from "stream-json/Stringer" - -/** - * Safely writes JSON data to a file. - * - Creates parent directories if they don't exist - * - Uses 'proper-lockfile' for inter-process advisory locking to prevent concurrent writes to the same path. - * - Writes to a temporary file first. - * - If the target file exists, it's backed up before being replaced. - * - Attempts to roll back and clean up in case of errors. - * - * @param {string} filePath - The absolute path to the target file. - * @param {any} data - The data to serialize to JSON and write. - * @returns {Promise} - */ - -async function safeWriteJson(filePath: string, data: any): Promise { - const absoluteFilePath = path.resolve(filePath) - let releaseLock = async () => {} // Initialized to a no-op - - // For directory creation - const dirPath = path.dirname(absoluteFilePath) - - // Ensure directory structure exists with improved reliability - try { - // Create directory with recursive option - await fs.mkdir(dirPath, { recursive: true }) - - // Verify directory exists after creation attempt - await fs.access(dirPath) - } catch (dirError: any) { - console.error(`Failed to create or access directory for ${absoluteFilePath}:`, dirError) - throw dirError - } - - // Acquire the lock before any file operations - try { - releaseLock = await lockfile.lock(absoluteFilePath, { - stale: 31000, // Stale after 31 seconds - update: 10000, // Update mtime every 10 seconds to prevent staleness if operation is long - realpath: false, // the file may not exist yet, which is acceptable - retries: { - // Configuration for retrying lock acquisition - retries: 5, // Number of retries after the initial attempt - factor: 2, // Exponential backoff factor (e.g., 100ms, 200ms, 400ms, ...) - minTimeout: 100, // Minimum time to wait before the first retry (in ms) - maxTimeout: 1000, // Maximum time to wait for any single retry (in ms) - }, - onCompromised: (err) => { - console.error(`Lock at ${absoluteFilePath} was compromised:`, err) - throw err - }, - }) - } catch (lockError) { - // If lock acquisition fails, we throw immediately. - // The releaseLock remains a no-op, so the finally block in the main file operations - // try-catch-finally won't try to release an unacquired lock if this path is taken. - console.error(`Failed to acquire lock for ${absoluteFilePath}:`, lockError) - // Propagate the lock acquisition error - throw lockError - } - - // Variables to hold the actual paths of temp files if they are created. - let actualTempNewFilePath: string | null = null - let actualTempBackupFilePath: string | null = null - - try { - // Step 1: Write data to a new temporary file. - actualTempNewFilePath = path.join( - path.dirname(absoluteFilePath), - `.${path.basename(absoluteFilePath)}.new_${Date.now()}_${Math.random().toString(36).substring(2)}.tmp`, - ) - - await _streamDataToFile(actualTempNewFilePath, data) - - // Step 2: Check if the target file exists. If so, rename it to a backup path. - try { - // Check for target file existence - await fs.access(absoluteFilePath) - // Target exists, create a backup path and rename. - actualTempBackupFilePath = path.join( - path.dirname(absoluteFilePath), - `.${path.basename(absoluteFilePath)}.bak_${Date.now()}_${Math.random().toString(36).substring(2)}.tmp`, - ) - await fs.rename(absoluteFilePath, actualTempBackupFilePath) - } catch (accessError: any) { - // Explicitly type accessError - if (accessError.code !== "ENOENT") { - // An error other than "file not found" occurred during access check. - throw accessError - } - // Target file does not exist, so no backup is made. actualTempBackupFilePath remains null. - } - - // Step 3: Rename the new temporary file to the target file path. - // This is the main "commit" step. - await fs.rename(actualTempNewFilePath, absoluteFilePath) - - // If we reach here, the new file is successfully in place. - // The original actualTempNewFilePath is now the main file, so we shouldn't try to clean it up as "temp". - // Mark as "used" or "committed" - actualTempNewFilePath = null - - // Step 4: If a backup was created, attempt to delete it. - if (actualTempBackupFilePath) { - try { - await fs.unlink(actualTempBackupFilePath) - // Mark backup as handled - actualTempBackupFilePath = null - } catch (unlinkBackupError) { - // Log this error, but do not re-throw. The main operation was successful. - // actualTempBackupFilePath remains set, indicating an orphaned backup. - console.error( - `Successfully wrote ${absoluteFilePath}, but failed to clean up backup ${actualTempBackupFilePath}:`, - unlinkBackupError, - ) - } - } - } catch (originalError) { - console.error(`Operation failed for ${absoluteFilePath}: [Original Error Caught]`, originalError) - - const newFileToCleanupWithinCatch = actualTempNewFilePath - const backupFileToRollbackOrCleanupWithinCatch = actualTempBackupFilePath - - // Attempt rollback if a backup was made - if (backupFileToRollbackOrCleanupWithinCatch) { - try { - await fs.rename(backupFileToRollbackOrCleanupWithinCatch, absoluteFilePath) - // Mark as handled, prevent later unlink of this path - actualTempBackupFilePath = null - } catch (rollbackError) { - // actualTempBackupFilePath (outer scope) remains pointing to backupFileToRollbackOrCleanupWithinCatch - console.error( - `[Catch] Failed to restore backup ${backupFileToRollbackOrCleanupWithinCatch} to ${absoluteFilePath}:`, - rollbackError, - ) - } - } - - // Cleanup the .new file if it exists - if (newFileToCleanupWithinCatch) { - try { - await fs.unlink(newFileToCleanupWithinCatch) - } catch (cleanupError) { - console.error( - `[Catch] Failed to clean up temporary new file ${newFileToCleanupWithinCatch}:`, - cleanupError, - ) - } - } - - // Cleanup the .bak file if it still needs to be (i.e., wasn't successfully restored) - if (actualTempBackupFilePath) { - try { - await fs.unlink(actualTempBackupFilePath) - } catch (cleanupError) { - console.error( - `[Catch] Failed to clean up temporary backup file ${actualTempBackupFilePath}:`, - cleanupError, - ) - } - } - throw originalError // This MUST be the error that rejects the promise. - } finally { - // Release the lock in the main finally block. - try { - // releaseLock will be the actual unlock function if lock was acquired, - // or the initial no-op if acquisition failed. - await releaseLock() - } catch (unlockError) { - // Do not re-throw here, as the originalError from the try/catch (if any) is more important. - console.error(`Failed to release lock for ${absoluteFilePath}:`, unlockError) - } - } -} - -/** - * Helper function to stream JSON data to a file. - * @param targetPath The path to write the stream to. - * @param data The data to stream. - * @returns Promise - */ -async function _streamDataToFile(targetPath: string, data: any): Promise { - // Stream data to avoid high memory usage for large JSON objects. - const fileWriteStream = fsSync.createWriteStream(targetPath, { encoding: "utf8" }) - const disassembler = Disassembler.disassembler() - // Output will be compact JSON as standard Stringer is used. - const stringer = Stringer.stringer() - - return new Promise((resolve, reject) => { - let errorOccurred = false - const handleError = (_streamName: string) => (err: Error) => { - if (!errorOccurred) { - errorOccurred = true - if (!fileWriteStream.destroyed) { - fileWriteStream.destroy(err) - } - reject(err) - } - } - - disassembler.on("error", handleError("Disassembler")) - stringer.on("error", handleError("Stringer")) - fileWriteStream.on("error", (err: Error) => { - if (!errorOccurred) { - errorOccurred = true - reject(err) - } - }) - - fileWriteStream.on("finish", () => { - if (!errorOccurred) { - resolve() - } - }) - - disassembler.pipe(stringer).pipe(fileWriteStream) - - // stream-json's Disassembler might error if `data` is undefined. - // JSON.stringify(undefined) would produce the string "undefined" if it's the root value. - // Writing 'null' is a safer JSON representation for a root undefined value. - if (data === undefined) { - disassembler.write(null) - } else { - disassembler.write(data) - } - disassembler.end() - }) -} - -export { safeWriteJson } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 3782242707..e63d8d0f4f 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState, useMemo } from "react" +import { useCallback, useEffect, useRef, useState, useMemo } from "react" import { useEvent } from "react-use" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" @@ -9,7 +9,6 @@ import { MarketplaceViewStateManager } from "./components/marketplace/Marketplac import { vscode } from "./utils/vscode" import { telemetryClient } from "./utils/TelemetryClient" import { TelemetryEventName } from "@roo-code/types" -import { initializeSourceMaps, exposeSourceMapsForDebugging } from "./utils/sourceMapInitializer" import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext" import ChatView, { ChatViewRef } from "./components/chat/ChatView" import HistoryView from "./components/history/HistoryView" @@ -19,38 +18,11 @@ import McpView from "./components/mcp/McpView" import { MarketplaceView } from "./components/marketplace/MarketplaceView" import ModesView from "./components/modes/ModesView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" -import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" -import ErrorBoundary from "./components/ErrorBoundary" import { AccountView } from "./components/account/AccountView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" -import { TooltipProvider } from "./components/ui/tooltip" -import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" -interface HumanRelayDialogState { - isOpen: boolean - requestId: string - promptText: string -} - -interface DeleteMessageDialogState { - isOpen: boolean - messageTs: number -} - -interface EditMessageDialogState { - isOpen: boolean - messageTs: number - text: string - images?: string[] -} - -// Memoize dialog components to prevent unnecessary re-renders -const MemoizedDeleteMessageDialog = React.memo(DeleteMessageDialog) -const MemoizedEditMessageDialog = React.memo(EditMessageDialog) -const MemoizedHumanRelayDialog = React.memo(HumanRelayDialog) - const tabsByMessageAction: Partial, Tab>> = { chatButtonClicked: "chat", settingsButtonClicked: "settings", @@ -71,7 +43,6 @@ const App = () => { machineId, cloudUserInfo, cloudIsAuthenticated, - cloudApiUrl, renderContext, mdmCompliant, } = useExtensionState() @@ -82,24 +53,16 @@ const App = () => { const [showAnnouncement, setShowAnnouncement] = useState(false) const [tab, setTab] = useState("chat") - const [humanRelayDialogState, setHumanRelayDialogState] = useState({ + const [humanRelayDialogState, setHumanRelayDialogState] = useState<{ + isOpen: boolean + requestId: string + promptText: string + }>({ isOpen: false, requestId: "", promptText: "", }) - const [deleteMessageDialogState, setDeleteMessageDialogState] = useState({ - isOpen: false, - messageTs: 0, - }) - - const [editMessageDialogState, setEditMessageDialogState] = useState({ - isOpen: false, - messageTs: 0, - text: "", - images: [], - }) - const settingsRef = useRef(null) const chatViewRef = useRef(null) @@ -111,7 +74,6 @@ const App = () => { } setCurrentSection(undefined) - setCurrentMarketplaceTab(undefined) if (settingsRef.current?.checkUnsaveChanges) { settingsRef.current.checkUnsaveChanges(() => setTab(newTab)) @@ -123,7 +85,6 @@ const App = () => { ) const [currentSection, setCurrentSection] = useState(undefined) - const [currentMarketplaceTab, setCurrentMarketplaceTab] = useState(undefined) const onMessage = useCallback( (e: MessageEvent) => { @@ -135,17 +96,14 @@ const App = () => { const targetTab = message.tab as Tab switchTab(targetTab) setCurrentSection(undefined) - setCurrentMarketplaceTab(undefined) } else { // Handle other actions using the mapping const newTab = tabsByMessageAction[message.action] const section = message.values?.section as string | undefined - const marketplaceTab = message.values?.marketplaceTab as string | undefined if (newTab) { switchTab(newTab) setCurrentSection(section) - setCurrentMarketplaceTab(marketplaceTab) } } } @@ -155,19 +113,6 @@ const App = () => { setHumanRelayDialogState({ isOpen: true, requestId, promptText }) } - if (message.type === "showDeleteMessageDialog" && message.messageTs) { - setDeleteMessageDialogState({ isOpen: true, messageTs: message.messageTs }) - } - - if (message.type === "showEditMessageDialog" && message.messageTs && message.text) { - setEditMessageDialogState({ - isOpen: true, - messageTs: message.messageTs, - text: message.text, - images: message.images || [], - }) - } - if (message.type === "acceptInput") { chatViewRef.current?.acceptInput() } @@ -193,20 +138,6 @@ const App = () => { // Tell the extension that we are ready to receive messages. useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), []) - // Initialize source map support for better error reporting - useEffect(() => { - // Initialize source maps for better error reporting in production - initializeSourceMaps() - - // Expose source map debugging utilities in production - if (process.env.NODE_ENV === "production") { - exposeSourceMapsForDebugging() - } - - // Log initialization for debugging - console.debug("App initialized with source map support") - }, []) - // Focus the WebView when non-interactive content is clicked (only in editor/tab mode) useAddNonInteractiveClickListener( useCallback(() => { @@ -240,17 +171,12 @@ const App = () => { setTab("chat")} targetSection={currentSection} /> )} {tab === "marketplace" && ( - switchTab("chat")} - targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined} - /> + switchTab("chat")} /> )} {tab === "account" && ( switchTab("chat")} /> )} @@ -260,7 +186,7 @@ const App = () => { showAnnouncement={showAnnouncement} hideAnnouncement={() => setShowAnnouncement(false)} /> - { onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} /> - setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))} - onConfirm={() => { - vscode.postMessage({ - type: "deleteMessageConfirm", - messageTs: deleteMessageDialogState.messageTs, - }) - setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false })) - }} - /> - setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))} - onConfirm={() => { - vscode.postMessage({ - type: "editMessageConfirm", - messageTs: editMessageDialogState.messageTs, - text: editMessageDialogState.text, - images: editMessageDialogState.images, - }) - setEditMessageDialogState((prev) => ({ ...prev, isOpen: false })) - }} - /> ) } @@ -299,17 +201,13 @@ const App = () => { const queryClient = new QueryClient() const AppWithProviders = () => ( - - - - - - - - - - - + + + + + + + ) export default AppWithProviders diff --git a/webview-ui/src/__tests__/App.spec.tsx b/webview-ui/src/__tests__/App.spec.tsx index 78d026cf36..bfe7695b86 100644 --- a/webview-ui/src/__tests__/App.spec.tsx +++ b/webview-ui/src/__tests__/App.spec.tsx @@ -1,7 +1,7 @@ // npx vitest run src/__tests__/App.spec.tsx import React from "react" -import { render, screen, act, cleanup } from "@/utils/test-utils" +import { render, screen, act, cleanup } from "@testing-library/react" import AppWithProviders from "../App" diff --git a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx index 0e5d0d6193..5a5ff463ef 100644 --- a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx +++ b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx @@ -1,6 +1,6 @@ // npm run test ContextWindowProgress.spec.tsx -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import TaskHeader from "@src/components/chat/TaskHeader" @@ -51,6 +51,7 @@ describe("ContextWindowProgress", () => { task: { ts: Date.now(), type: "say" as const, say: "text" as const, text: "Test task" }, tokensIn: 100, tokensOut: 50, + doesModelSupportPromptCache: true, totalCost: 0.001, contextTokens: 1000, onClose: vi.fn(), @@ -102,22 +103,18 @@ describe("ContextWindowProgress", () => { it("calculates percentages correctly", () => { renderComponent({ contextTokens: 1000, contextWindow: 4000 }) - // Verify that the token count and window size are displayed correctly - const tokenCount = screen.getByTestId("context-tokens-count") - const windowSize = screen.getByTestId("context-window-size") + // Instead of checking the title attribute, verify the data-test-id + // which identifies the element containing info about the percentage of tokens used + const tokenUsageDiv = screen.getByTestId("context-tokens-used") + expect(tokenUsageDiv).toBeInTheDocument() - expect(tokenCount).toBeInTheDocument() - expect(tokenCount).toHaveTextContent("1000") + // Just verify that the element has a title attribute (the actual text is translated and may vary) + expect(tokenUsageDiv).toHaveAttribute("title") - expect(windowSize).toBeInTheDocument() - expect(windowSize).toHaveTextContent("4000") - - // The progress bar is now wrapped in tooltips, but we can verify the structure exists - // by checking for the progress bar container - const progressBarContainer = screen.getByTestId("context-tokens-count").parentElement - expect(progressBarContainer).toBeInTheDocument() - - // Verify the flex container has the expected structure - expect(progressBarContainer?.querySelector(".flex-1.relative")).toBeInTheDocument() + // We can't reliably test computed styles in JSDOM, so we'll just check + // that the component appears to be working correctly by checking for expected elements + // The context-window-label is not part of the ContextWindowProgress component + expect(screen.getByTestId("context-tokens-count")).toBeInTheDocument() + expect(screen.getByTestId("context-tokens-count")).toHaveTextContent("1000") }) }) diff --git a/webview-ui/src/components/account/AccountView.tsx b/webview-ui/src/components/account/AccountView.tsx index e3d1a293a7..a59ac716d7 100644 --- a/webview-ui/src/components/account/AccountView.tsx +++ b/webview-ui/src/components/account/AccountView.tsx @@ -1,56 +1,21 @@ -import { useEffect, useRef } from "react" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import type { CloudUserInfo } from "@roo-code/types" -import { TelemetryEventName } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { vscode } from "@src/utils/vscode" -import { telemetryClient } from "@src/utils/TelemetryClient" type AccountViewProps = { userInfo: CloudUserInfo | null isAuthenticated: boolean - cloudApiUrl?: string onDone: () => void } -export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: AccountViewProps) => { +export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewProps) => { const { t } = useAppTranslation() - const wasAuthenticatedRef = useRef(false) const rooLogoUri = (window as any).IMAGES_BASE_URI + "/roo-logo.svg" - // Track authentication state changes to detect successful logout - useEffect(() => { - if (isAuthenticated) { - wasAuthenticatedRef.current = true - } else if (wasAuthenticatedRef.current && !isAuthenticated) { - // User just logged out successfully - telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS) - wasAuthenticatedRef.current = false - } - }, [isAuthenticated]) - - const handleConnectClick = () => { - // Send telemetry for account connect action - telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED) - vscode.postMessage({ type: "rooCloudSignIn" }) - } - - const handleLogoutClick = () => { - // Send telemetry for account logout action - telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_CLICKED) - vscode.postMessage({ type: "rooCloudSignOut" }) - } - - const handleVisitCloudWebsite = () => { - // Send telemetry for cloud website visit - telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED) - const cloudUrl = cloudApiUrl || "https://app.roocode.com" - vscode.postMessage({ type: "openExternal", url: cloudUrl }) - } - return (
@@ -76,9 +41,9 @@ export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }:
)}
- {userInfo.name && ( -

{userInfo.name}

- )} +

+ {userInfo?.name || t("account:unknownUser")} +

{userInfo?.email && (

{userInfo?.email}

)} @@ -97,18 +62,18 @@ export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: )}
- - {t("account:visitCloudWebsite")} - - + vscode.postMessage({ type: "rooCloudSignOut" })} + className="w-full"> {t("account:logOut")}
) : ( <> -
-
+
+
- -
-

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

-

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

-
    -
  • - - {t("account:cloudBenefitHistory")} -
  • -
  • - - {t("account:cloudBenefitSharing")} -
  • -
  • - - {t("account:cloudBenefitMetrics")} -
  • -
-
-
- - {t("account:connect")} + vscode.postMessage({ type: "rooCloudSignIn" })} + className="w-full"> + {t("account:signIn")}
diff --git a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx deleted file mode 100644 index d6fd3013e6..0000000000 --- a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { render, screen } from "@/utils/test-utils" -import { describe, it, expect, vi } from "vitest" -import { AccountView } from "../AccountView" - -// Mock the translation context -vi.mock("@src/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ - t: (key: string) => { - const translations: Record = { - "account:title": "Account", - "settings:common.done": "Done", - "account:signIn": "Connect to Roo Code Cloud", - "account:cloudBenefitsTitle": "Connect to Roo Code Cloud", - "account:cloudBenefitsSubtitle": "Sync your prompts and telemetry to enable:", - "account:cloudBenefitHistory": "Online task history", - "account:cloudBenefitSharing": "Sharing and collaboration features", - "account:cloudBenefitMetrics": "Task, token, and cost-based usage metrics", - "account:logOut": "Log out", - } - return translations[key] || key - }, - }), -})) - -// Mock vscode utilities -vi.mock("@src/utils/vscode", () => ({ - vscode: { - postMessage: vi.fn(), - }, -})) - -// Mock telemetry client -vi.mock("@src/utils/TelemetryClient", () => ({ - telemetryClient: { - capture: vi.fn(), - }, -})) - -// Mock window global for images -Object.defineProperty(window, "IMAGES_BASE_URI", { - value: "/images", - writable: true, -}) - -describe("AccountView", () => { - it("should display benefits when user is not authenticated", () => { - render( - {}} - />, - ) - - // Check that the benefits section is displayed - expect(screen.getByRole("heading", { name: "Connect to Roo Code Cloud" })).toBeInTheDocument() - expect(screen.getByText("Sync your prompts and telemetry to enable:")).toBeInTheDocument() - expect(screen.getByText("Online task history")).toBeInTheDocument() - expect(screen.getByText("Sharing and collaboration features")).toBeInTheDocument() - expect(screen.getByText("Task, token, and cost-based usage metrics")).toBeInTheDocument() - - // Check that the connect button is also present - expect(screen.getByText("account:connect")).toBeInTheDocument() - }) - - it("should not display benefits when user is authenticated", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - } - - render( - {}} - />, - ) - - // Check that the benefits section is NOT displayed - expect(screen.queryByText("Sync your prompts and telemetry to enable:")).not.toBeInTheDocument() - expect(screen.queryByText("Online task history")).not.toBeInTheDocument() - expect(screen.queryByText("Sharing and collaboration features")).not.toBeInTheDocument() - expect(screen.queryByText("Task, token, and cost-based usage metrics")).not.toBeInTheDocument() - - // Check that user info is displayed instead - expect(screen.getByText("Test User")).toBeInTheDocument() - expect(screen.getByText("test@example.com")).toBeInTheDocument() - }) -}) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6c541353eb..19f6c8a995 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -19,17 +19,16 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" -import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui" +import { SelectDropdown, DropdownOptionType, Button } from "@/components/ui" import Thumbnails from "../common/Thumbnails" -import ModeSelector from "./ModeSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide-react" -import { IndexingStatusBadge } from "./IndexingStatusBadge" +import { VolumeX, Pin, Check } from "lucide-react" +import { IconButton } from "./IconButton" +import { IndexingStatusDot } from "./IndexingStatusBadge" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" -import { EditModeControls } from "./EditModeControls" interface ChatTextAreaProps { inputValue: string @@ -46,9 +45,6 @@ interface ChatTextAreaProps { mode: Mode setMode: (value: Mode) => void modeShortcutText: string - // Edit mode props - isEditMode?: boolean - onCancel?: () => void } const ChatTextArea = forwardRef( @@ -68,8 +64,6 @@ const ChatTextArea = forwardRef( mode, setMode, modeShortcutText, - isEditMode = false, - onCancel, }, ref, ) => { @@ -80,12 +74,12 @@ const ChatTextArea = forwardRef( currentApiConfigName, listApiConfigMeta, customModes, - customModePrompts, cwd, pinnedApiConfigs, togglePinnedApiConfig, taskHistory, clineMessages, + codebaseIndexConfig, } = useExtensionState() // Find the ID and display text for the currently selected API configuration @@ -121,25 +115,8 @@ const ChatTextArea = forwardRef( const message = event.data if (message.type === "enhancedPrompt") { - if (message.text && textAreaRef.current) { - try { - // Use execCommand to replace text while preserving undo history - if (document.execCommand) { - // Use native browser methods to preserve undo stack - const textarea = textAreaRef.current - - // Focus the textarea to ensure it's the active element - textarea.focus() - - // Select all text first - textarea.select() - document.execCommand("insertText", false, message.text) - } else { - setInputValue(message.text) - } - } catch { - setInputValue(message.text) - } + if (message.text) { + setInputValue(message.text) } setIsEnhancingPrompt(false) @@ -216,8 +193,6 @@ const ChatTextArea = forwardRef( } }, [inputValue, sendingDisabled, setInputValue, t]) - const allModes = useMemo(() => getAllModes(customModes), [customModes]) - const queryItems = useMemo(() => { return [ { type: ContextMenuOptionType.Problems, value: "problems" }, @@ -347,7 +322,7 @@ const ChatTextArea = forwardRef( selectedType, queryItems, fileSearchResults, - allModes, + getAllModes(customModes), ) const optionsLength = options.length @@ -384,7 +359,7 @@ const ChatTextArea = forwardRef( selectedType, queryItems, fileSearchResults, - allModes, + getAllModes(customModes), )[selectedMenuIndex] if ( selectedOption && @@ -471,7 +446,7 @@ const ChatTextArea = forwardRef( setInputValue, justDeletedSpaceAfterMention, queryItems, - allModes, + customModes, fileSearchResults, handleHistoryNavigation, resetHistoryNavigation, @@ -802,392 +777,20 @@ const ChatTextArea = forwardRef( const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})` - // Common mode selector handler - const handleModeChange = useCallback( - (value: Mode) => { - setMode(value) - vscode.postMessage({ type: "mode", text: value }) - }, - [setMode], - ) - - // Helper function to render mode selector - const renderModeSelector = () => ( - - ) - - // Helper function to get API config dropdown options - const getApiConfigOptions = useMemo(() => { - const pinnedConfigs = (listApiConfigMeta || []) - .filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: true, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - const unpinnedConfigs = (listApiConfigMeta || []) - .filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: false, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - const hasPinnedAndUnpinned = pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 - - return [ - ...pinnedConfigs, - ...(hasPinnedAndUnpinned - ? [ - { - value: "sep-pinned", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - ] - : []), - ...unpinnedConfigs, - { - value: "sep-2", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - { - value: "settingsButtonClicked", - label: t("chat:edit"), - type: DropdownOptionType.ACTION, - }, - ] - }, [listApiConfigMeta, pinnedApiConfigs, t]) - - // Helper function to handle API config change - const handleApiConfigChange = useCallback((value: string) => { - if (value === "settingsButtonClicked") { - vscode.postMessage({ - type: "loadApiConfiguration", - text: value, - values: { section: "providers" }, - }) - } else { - vscode.postMessage({ type: "loadApiConfigurationById", text: value }) - } - }, []) - - // Helper function to render API config item - const renderApiConfigItem = useCallback( - ({ type, value, label, pinned }: any) => { - if (type !== DropdownOptionType.ITEM) { - return label - } - - const config = listApiConfigMeta?.find((c) => c.id === value) - const isCurrentConfig = config?.name === currentApiConfigName - - return ( -
-
- {label} -
-
-
- -
- - - -
-
- ) - }, - [listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig], - ) - - // Helper function to render non-edit mode controls - const renderNonEditModeControls = () => ( -
-
-
{renderModeSelector()}
- -
- -
-
- -
- {isTtsPlaying && ( - - - - )} - - - - -
-
- ) - - // Helper function to render the text area section - const renderTextAreaSection = () => ( -
-
- { - if (typeof ref === "function") { - ref(el) - } else if (ref) { - ref.current = el - } - textAreaRef.current = el - }} - value={inputValue} - onChange={(e) => { - handleInputChange(e) - updateHighlights() - }} - onFocus={() => setIsFocused(true)} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} - onBlur={handleBlur} - onPaste={handlePaste} - onSelect={updateCursorPosition} - onMouseUp={updateCursorPosition} - onHeightChange={(height) => { - if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) { - setTextAreaBaseHeight(height) - } - - onHeightChange?.(height) - }} - placeholder={placeholderText} - minRows={3} - maxRows={15} - autoFocus={true} - className={cn( - "w-full", - "text-vscode-input-foreground", - "font-vscode-font-family", - "text-vscode-editor-font-size", - "leading-vscode-editor-line-height", - "cursor-text", - isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2", - isFocused - ? "border border-vscode-focusBorder outline outline-vscode-focusBorder" - : isDraggingOver - ? "border-2 border-dashed border-vscode-focusBorder" - : "border border-transparent", - isDraggingOver - ? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]" - : "bg-vscode-input-background", - "transition-background-color duration-150 ease-in-out", - "will-change-background-color", - "min-h-[90px]", - "box-border", - "rounded", - "resize-none", - "overflow-x-hidden", - "overflow-y-auto", - "pr-9", - "flex-none flex-grow", - "z-[2]", - "scrollbar-none", - "scrollbar-hide", - )} - onScroll={() => updateHighlights()} - /> - -
- - - -
- - {!isEditMode && ( -
- - - -
- )} - - {!inputValue && !isEditMode && ( -
- {placeholderBottomText} -
- )} -
- ) - return (
( setSelectedIndex={setSelectedMenuIndex} selectedType={selectedType} queryItems={queryItems} - modes={allModes} + modes={getAllModes(customModes)} loading={searchLoading} dynamicSearchResults={fileSearchResults} />
)} +
+
+ { + if (typeof ref === "function") { + ref(el) + } else if (ref) { + ref.current = el + } + textAreaRef.current = el + }} + value={inputValue} + onChange={(e) => { + handleInputChange(e) + updateHighlights() + }} + onFocus={() => setIsFocused(true)} + onKeyDown={handleKeyDown} + onKeyUp={handleKeyUp} + onBlur={handleBlur} + onPaste={handlePaste} + onSelect={updateCursorPosition} + onMouseUp={updateCursorPosition} + onHeightChange={(height) => { + if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) { + setTextAreaBaseHeight(height) + } - {renderTextAreaSection()} + onHeightChange?.(height) + }} + placeholder={placeholderText} + minRows={3} + maxRows={15} + autoFocus={true} + className={cn( + "w-full", + "text-vscode-input-foreground", + "font-vscode-font-family", + "text-vscode-editor-font-size", + "leading-vscode-editor-line-height", + "cursor-text", + "py-1.5 px-2", + isFocused + ? "border border-vscode-focusBorder outline outline-vscode-focusBorder" + : isDraggingOver + ? "border-2 border-dashed border-vscode-focusBorder" + : "border border-transparent", + isDraggingOver + ? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]" + : "bg-vscode-input-background", + "transition-background-color duration-150 ease-in-out", + "will-change-background-color", + "min-h-[90px]", + "box-border", + "rounded", + "resize-none", + "overflow-x-hidden", + "overflow-y-auto", + "pr-2", + "flex-none flex-grow", + "z-[2]", + "scrollbar-none", + )} + onScroll={() => updateHighlights()} + /> + + {isTtsPlaying && ( + + )} + + {!inputValue && ( +
+ {placeholderBottomText} +
+ )} +
- - {isEditMode && ( - - )}
{selectedImages.length > 0 && ( @@ -1280,7 +994,180 @@ const ChatTextArea = forwardRef( /> )} - {!isEditMode && renderNonEditModeControls()} +
+
+
+ ({ + value: mode.slug, + label: mode.name, + type: DropdownOptionType.ITEM, + })), + { + value: "sep-1", + label: t("chat:separator"), + type: DropdownOptionType.SEPARATOR, + }, + { + value: "promptsButtonClicked", + label: t("chat:edit"), + type: DropdownOptionType.ACTION, + }, + ]} + onChange={(value) => { + setMode(value as Mode) + vscode.postMessage({ type: "mode", text: value }) + }} + shortcutText={modeShortcutText} + triggerClassName="w-full" + /> +
+ +
+ pinnedApiConfigs && pinnedApiConfigs[config.id]) + .map((config) => ({ + value: config.id, + label: config.name, + name: config.name, // Keep name for comparison with currentApiConfigName. + type: DropdownOptionType.ITEM, + pinned: true, + })) + .sort((a, b) => a.label.localeCompare(b.label)), + // If we have pinned items and unpinned items, add a separator. + ...(pinnedApiConfigs && + Object.keys(pinnedApiConfigs).length > 0 && + (listApiConfigMeta || []).some((config) => !pinnedApiConfigs[config.id]) + ? [ + { + value: "sep-pinned", + label: t("chat:separator"), + type: DropdownOptionType.SEPARATOR, + }, + ] + : []), + // Unpinned items sorted alphabetically. + ...(listApiConfigMeta || []) + .filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id]) + .map((config) => ({ + value: config.id, + label: config.name, + name: config.name, // Keep name for comparison with currentApiConfigName. + type: DropdownOptionType.ITEM, + pinned: false, + })) + .sort((a, b) => a.label.localeCompare(b.label)), + { + value: "sep-2", + label: t("chat:separator"), + type: DropdownOptionType.SEPARATOR, + }, + { + value: "settingsButtonClicked", + label: t("chat:edit"), + type: DropdownOptionType.ACTION, + }, + ]} + onChange={(value) => { + if (value === "settingsButtonClicked") { + vscode.postMessage({ + type: "loadApiConfiguration", + text: value, + values: { section: "providers" }, + }) + } else { + vscode.postMessage({ type: "loadApiConfigurationById", text: value }) + } + }} + triggerClassName="w-full text-ellipsis overflow-hidden" + itemClassName="group" + renderItem={({ type, value, label, pinned }) => { + if (type !== DropdownOptionType.ITEM) { + return label + } + + const config = listApiConfigMeta?.find((c) => c.id === value) + const isCurrentConfig = config?.name === currentApiConfigName + + return ( +
+
+ {label} +
+
+
+ +
+ +
+
+ ) + }} + /> +
+
+ +
+ {codebaseIndexConfig?.codebaseIndexEnabled && } + + + +
+
) }, diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index efd2db856c..90901c84e9 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -3,19 +3,16 @@ import { useDeepCompareEffect, useEvent, useMount } from "react-use" import debounce from "debounce" import { Virtuoso, type VirtuosoHandle } from "react-virtuoso" import removeMd from "remove-markdown" +import { Trans } from "react-i18next" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import useSound from "use-sound" import { LRUCache } from "lru-cache" -import { useDebounceEffect } from "@src/utils/useDebounceEffect" -import { appendImages } from "@src/utils/imageUtils" - import type { ClineAsk, ClineMessage } from "@roo-code/types" import { ClineSayBrowserAction, ClineSayTool, ExtensionMessage } from "@roo/ExtensionMessage" import { McpServer, McpTool } from "@roo/mcp" import { findLast } from "@roo/array" -import { FollowUpData, SuggestionItem } from "@roo-code/types" import { combineApiRequests } from "@roo/combineApiRequests" import { combineCommandSequences } from "@roo/combineCommandSequences" import { getApiMetrics } from "@roo/getApiMetrics" @@ -24,25 +21,15 @@ import { getAllModes } from "@roo/modes" import { ProfileValidator } from "@roo/ProfileValidator" import { vscode } from "@src/utils/vscode" -import { - getCommandDecision, - CommandDecision, - findLongestPrefixMatch, - parseCommand, -} from "@src/utils/command-validation" -import { useTranslation } from "react-i18next" +import { validateCommand } from "@src/utils/command-validation" +import { buildDocLink } from "@src/utils/docLinks" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" import RooHero from "@src/components/welcome/RooHero" import RooTips from "@src/components/welcome/RooTips" -import RooCloudCTA from "@src/components/welcome/RooCloudCTA" -import { StandardTooltip } from "@src/components/ui" -import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState" -import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles" import TelemetryBanner from "../common/TelemetryBanner" -import VersionIndicator from "../common/VersionIndicator" import { useTaskSearch } from "../history/useTaskSearch" import HistoryPreview from "../history/HistoryPreview" import Announcement from "./Announcement" @@ -54,7 +41,6 @@ import AutoApproveMenu from "./AutoApproveMenu" import SystemPromptWarning from "./SystemPromptWarning" import ProfileViolationWarning from "./ProfileViolationWarning" import { CheckpointWarning } from "./CheckpointWarning" -import { getLatestTodo } from "@roo/todo" export interface ChatViewProps { isHidden: boolean @@ -80,8 +66,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction messages.at(0), [messages]) - const latestTodos = useMemo(() => { - return getLatestTodo(messages) - }, [messages]) - const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages]) // Has to be after api_req_finished are all reduced into api_req_started messages. @@ -172,16 +148,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction(false) const [showCheckpointWarning, setShowCheckpointWarning] = useState(false) const [isCondensing, setIsCondensing] = useState(false) - const [showAnnouncementModal, setShowAnnouncementModal] = useState(false) const everVisibleMessagesTsRef = useRef>( new LRUCache({ max: 250, ttl: 1000 * 60 * 15, // 15 minutes TTL for long-running tasks }), ) - const autoApproveTimeoutRef = useRef(null) - const userRespondedRef = useRef(false) - const [currentFollowUpTs, setCurrentFollowUpTs] = useState(null) const clineAskRef = useRef(clineAsk) useEffect(() => { @@ -251,8 +223,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { setExpandedRows({}) everVisibleMessagesTsRef.current.clear() // Clear for new task - setCurrentFollowUpTs(null) // Clear follow-up answered state for new task - - // Clear any pending auto-approval timeout from previous task - if (autoApproveTimeoutRef.current) { - clearTimeout(autoApproveTimeoutRef.current) - autoApproveTimeoutRef.current = null - } - // Reset user response flag for new task - userRespondedRef.current = false }, [task?.ts]) - useEffect(() => { - if (isHidden) { - everVisibleMessagesTsRef.current.clear() - } - }, [isHidden]) - useEffect(() => () => everVisibleMessagesTsRef.current.clear(), []) useEffect(() => { @@ -510,22 +471,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const lastFollowUpMessage = messagesRef.current.findLast((msg) => msg.ask === "followup") - if (lastFollowUpMessage) { - setCurrentFollowUpTs(lastFollowUpMessage.ts) - } - }, []) - const handleChatReset = useCallback(() => { - // Clear any pending auto-approval timeout - if (autoApproveTimeoutRef.current) { - clearTimeout(autoApproveTimeoutRef.current) - autoApproveTimeoutRef.current = null - } - // Reset user response flag for new message - userRespondedRef.current = false - // Only reset message-specific state, preserving mode. setInputValue("") setSendingDisabled(true) @@ -543,16 +489,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction 0) { - // Mark that user has responded - this prevents any pending auto-approvals - userRespondedRef.current = true - if (messagesRef.current.length === 0) { vscode.postMessage({ type: "newTask", text, images }) } else if (clineAskRef.current) { - if (clineAskRef.current === "followup") { - markFollowUpAsAnswered() - } - // Use clineAskRef.current switch ( clineAskRef.current // Use clineAskRef.current @@ -576,7 +515,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - // Mark that user has responded - userRespondedRef.current = true - const trimmedInput = text?.trim() switch (clineAsk) { @@ -648,9 +584,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - // Mark that user has responded - userRespondedRef.current = true - const trimmedInput = text?.trim() if (isStreaming) { @@ -723,11 +656,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction 0) { setSelectedImages((prevImages) => - appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE), + [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE), ) } break @@ -782,15 +714,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction textAreaRef.current?.focus()) - useDebounceEffect( - () => { + useEffect(() => { + const timer = setTimeout(() => { if (!isHidden && !sendingDisabled && !enableButtons) { textAreaRef.current?.focus() } - }, - 50, - [isHidden, sendingDisabled, enableButtons], - ) + }, 50) + + return () => { + clearTimeout(timer) + } + }, [isHidden, sendingDisabled, enableButtons]) const visibleMessages = useMemo(() => { const newVisibleMessages = modifiedMessages.filter((message) => { @@ -920,70 +854,21 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - if (message?.type !== "ask") return "ask_user" - return getCommandDecision(message.text || "", allowedCommands || [], deniedCommands || []) - }, - [allowedCommands, deniedCommands], - ) - - // Check if a command message should be auto-approved. + // Check if a command message is allowed. const isAllowedCommand = useCallback( (message: ClineMessage | undefined): boolean => { - return getCommandDecisionForMessage(message) === "auto_approve" + if (message?.type !== "ask") return false + return validateCommand(message.text || "", allowedCommands || []) }, - [getCommandDecisionForMessage], + [allowedCommands], ) - // Check if a command message should be auto-denied. - const isDeniedCommand = useCallback( - (message: ClineMessage | undefined): boolean => { - return getCommandDecisionForMessage(message) === "auto_deny" - }, - [getCommandDecisionForMessage], - ) - - // Helper function to get the denied prefix for a command - const getDeniedPrefix = useCallback( - (command: string): string | null => { - if (!command || !deniedCommands?.length) return null - - // Parse the command into sub-commands and check each one - const subCommands = parseCommand(command) - for (const cmd of subCommands) { - const deniedMatch = findLongestPrefixMatch(cmd, deniedCommands) - if (deniedMatch) { - return deniedMatch - } - } - return null - }, - [deniedCommands], - ) - - // Create toggles object for useAutoApprovalState hook - const autoApprovalToggles = useAutoApprovalToggles() - - const { hasEnabledOptions } = useAutoApprovalState(autoApprovalToggles, autoApprovalEnabled) - const isAutoApproved = useCallback( (message: ClineMessage | undefined) => { - // First check if auto-approval is enabled AND we have at least one permission if (!autoApprovalEnabled || !message || message.type !== "ask") { return false } - // Use the hook's result instead of duplicating the logic - if (!hasEnabledOptions) { - return false - } - - if (message.ask === "followup") { - return alwaysAllowFollowupQuestions - } - if (message.ask === "browser_action_launch") { return alwaysAllowBrowser } @@ -1011,10 +896,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - return () => { - if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") { - ;(scrollToBottomSmooth as any).cancel() - } - } - }, [scrollToBottomSmooth]) - const scrollToBottomAuto = useCallback(() => { virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, @@ -1254,13 +1124,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - let timer: NodeJS.Timeout | undefined + let timerId: NodeJS.Timeout | undefined if (!disableAutoScrollRef.current) { - timer = setTimeout(() => scrollToBottomSmooth(), 50) + timerId = setTimeout(() => scrollToBottomSmooth(), 50) } return () => { - if (timer) { - clearTimeout(timer) + if (timerId) { + clearTimeout(timerId) } } }, [groupedMessages.length, scrollToBottomSmooth]) @@ -1281,73 +1151,36 @@ const ChatViewComponent: React.ForwardRefRenderFunction { // Only show the warning when there's a task but no visible messages yet - if (task && modifiedMessages.length === 0 && !isStreaming && !isHidden) { + if (task && modifiedMessages.length === 0 && !isStreaming) { const timer = setTimeout(() => { setShowCheckpointWarning(true) }, 5000) // 5 seconds return () => clearTimeout(timer) - } else { - setShowCheckpointWarning(false) } - }, [task, modifiedMessages.length, isStreaming, isHidden]) + }, [task, modifiedMessages.length, isStreaming]) // Effect to hide the checkpoint warning when messages appear useEffect(() => { - if (modifiedMessages.length > 0 || isStreaming || isHidden) { + if (modifiedMessages.length > 0 || isStreaming) { setShowCheckpointWarning(false) } - }, [modifiedMessages.length, isStreaming, isHidden]) + }, [modifiedMessages.length, isStreaming]) const placeholderText = task ? t("chat:typeMessage") : t("chat:typeTask") - // Function to switch to a specific mode - const switchToMode = useCallback( - (modeSlug: string): void => { - // Update local state and notify extension to sync mode change - setMode(modeSlug) - - // Send the mode switch message - vscode.postMessage({ - type: "mode", - text: modeSlug, - }) - }, - [setMode], - ) - const handleSuggestionClickInRow = useCallback( - (suggestion: SuggestionItem, event?: React.MouseEvent) => { - // Mark that user has responded if this is a manual click (not auto-approval) - if (event) { - userRespondedRef.current = true - } - - // Mark the current follow-up question as answered when a suggestion is clicked - if (clineAsk === "followup" && !event?.shiftKey) { - markFollowUpAsAnswered() - } - - // Check if we need to switch modes - if (suggestion.mode) { - // Only switch modes if it's a manual click (event exists) or auto-approval is allowed - const isManualClick = !!event - if (isManualClick || alwaysAllowModeSwitch) { - // Switch mode without waiting - switchToMode(suggestion.mode) - } - } - + (answer: string, event?: React.MouseEvent) => { if (event?.shiftKey) { // Always append to existing text, don't overwrite setInputValue((currentValue) => { - return currentValue !== "" ? `${currentValue} \n${suggestion.answer}` : suggestion.answer + return currentValue !== "" ? `${currentValue} \n${answer}` : answer }) } else { - handleSendMessage(suggestion.answer, []) + handleSendMessage(answer, []) } }, - [handleSendMessage, setInputValue, switchToMode, alwaysAllowModeSwitch, clineAsk, markFollowUpAsAnswered], + [handleSendMessage, setInputValue], // setInputValue is stable, handleSendMessage depends on clineAsk ) const handleBatchFileResponse = useCallback((response: { [key: string]: boolean }) => { @@ -1355,12 +1188,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - // Mark that user has responded - userRespondedRef.current = true - }, []) - const itemContent = useCallback( (index: number, messageOrGroup: ClineMessage | ClineMessage[]) => { // browser session group @@ -1396,26 +1223,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - let tool: any = {} - try { - tool = JSON.parse(messageOrGroup.text || "{}") - } catch (_) { - if (messageOrGroup.text?.includes("updateTodoList")) { - tool = { tool: "updateTodoList" } - } - } - if (tool.tool === "updateTodoList" && alwaysAllowUpdateTodoList) { - return false - } - return tool.tool === "updateTodoList" && enableButtons && !!primaryButtonText - })() - } /> ) }, @@ -1428,112 +1235,42 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - if (autoApproveTimeoutRef.current) { - clearTimeout(autoApproveTimeoutRef.current) - autoApproveTimeoutRef.current = null - } - + // Only proceed if we have an ask and buttons are enabled. if (!clineAsk || !enableButtons) { return } - // Exit early if user has already responded - if (userRespondedRef.current) { - return - } - - const autoApproveOrReject = async () => { - // Check for auto-reject first (commands that should be denied) - if (lastMessage?.ask === "command" && isDeniedCommand(lastMessage)) { - // Get the denied prefix for the localized message - const deniedPrefix = getDeniedPrefix(lastMessage.text || "") - if (deniedPrefix) { - // Create the localized auto-deny message and send it with the rejection - const autoDenyMessage = tSettings("autoApprove.execute.autoDenied", { prefix: deniedPrefix }) - - vscode.postMessage({ - type: "askResponse", - askResponse: "noButtonClicked", - text: autoDenyMessage, - }) - } else { - // Auto-reject denied commands immediately if no prefix found - vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" }) - } - - setSendingDisabled(true) - setClineAsk(undefined) - setEnableButtons(false) - return - } - - // Then check for auto-approve + const autoApprove = async () => { if (lastMessage?.ask && isAutoApproved(lastMessage)) { - // Special handling for follow-up questions - if (lastMessage.ask === "followup") { - // Handle invalid JSON - let followUpData: FollowUpData = {} - try { - followUpData = JSON.parse(lastMessage.text || "{}") as FollowUpData - } catch (error) { - console.error("Failed to parse follow-up data:", error) + // Note that `isAutoApproved` can only return true if + // lastMessage is an ask of type "browser_action_launch", + // "use_mcp_server", "command", or "tool". + + // Add delay for write operations. + if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) { + await new Promise((resolve) => setTimeout(resolve, writeDelayMs)) + if (!isMountedRef.current) { return } - - if (followUpData && followUpData.suggest && followUpData.suggest.length > 0) { - // Wait for the configured timeout before auto-selecting the first suggestion - await new Promise((resolve) => { - autoApproveTimeoutRef.current = setTimeout(() => { - autoApproveTimeoutRef.current = null - resolve() - }, followupAutoApproveTimeoutMs) - }) - - // Check if user responded manually - if (userRespondedRef.current) { - return - } - - // Get the first suggestion - const firstSuggestion = followUpData.suggest[0] - - // Handle the suggestion click - handleSuggestionClickInRow(firstSuggestion) - return - } - } else if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) { - await new Promise((resolve) => { - autoApproveTimeoutRef.current = setTimeout(() => { - autoApproveTimeoutRef.current = null - resolve() - }, writeDelayMs) - }) } vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }) - setSendingDisabled(true) - setClineAsk(undefined) - setEnableButtons(false) - } - } - autoApproveOrReject() - - return () => { - if (autoApproveTimeoutRef.current) { - clearTimeout(autoApproveTimeoutRef.current) - autoApproveTimeoutRef.current = null + // This is copied from `handlePrimaryButtonClick`, which we used + // to call from `autoApprove`. I'm not sure how many of these + // things are actually needed. + if (isMountedRef.current) { + setSendingDisabled(true) + setClineAsk(undefined) + setEnableButtons(false) + } } } + autoApprove() }, [ clineAsk, enableButtons, @@ -1544,22 +1281,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction m.slug === mode) const nextModeIndex = (currentModeIndex + 1) % allModes.length // Update local state and notify extension to sync mode change - switchToMode(allModes[nextModeIndex].slug) - }, [mode, customModes, switchToMode]) - - // Function to handle switching to previous mode - const switchToPreviousMode = useCallback(() => { - const allModes = getAllModes(customModes) - const currentModeIndex = allModes.findIndex((m) => m.slug === mode) - const previousModeIndex = (currentModeIndex - 1 + allModes.length) % allModes.length - // Update local state and notify extension to sync mode change - switchToMode(allModes[previousModeIndex].slug) - }, [mode, customModes, switchToMode]) + setMode(allModes[nextModeIndex].slug) + vscode.postMessage({ + type: "mode", + text: allModes[nextModeIndex].slug, + }) + }, [mode, setMode, customModes]) // Add keyboard event handler const handleKeyDown = useCallback( (event: KeyboardEvent) => { - // Check for Command/Ctrl + Period (with or without Shift) - // Using event.code for better cross-platform compatibility - if ((event.metaKey || event.ctrlKey) && event.code === "Period") { + // Check for Command + . (period) + if ((event.metaKey || event.ctrlKey) && event.key === ".") { event.preventDefault() // Prevent default browser behavior - - if (event.shiftKey) { - // Shift + Period = Previous mode - switchToPreviousMode() - } else { - // Just Period = Next mode - switchToNextMode() - } + switchToNextMode() } }, - [switchToNextMode, switchToPreviousMode], + [switchToNextMode], ) // Add event listener @@ -1627,28 +1343,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction - {(showAnnouncement || showAnnouncementModal) && ( - { - if (showAnnouncementModal) { - setShowAnnouncementModal(false) - } - if (showAnnouncement) { - hideAnnouncement() - } - }} - /> - )} + {showAnnouncement && } {task ? ( <> {hasSystemPromptOverride && ( @@ -1672,7 +1375,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) : ( -
+
{/* Moved Task Bar Header Here */} {tasks.length !== 0 && (
@@ -1688,20 +1391,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction 0 ? "mt-0" : ""} px-3.5 min-[370px]:px-10 pt-5 transition-all duration-300`}> - {/* Version indicator in top-right corner - only on welcome screen */} - setShowAnnouncementModal(true)} - className="absolute top-2 right-3 z-10" - /> - {telemetrySetting === "unset" && } - -
- {cloudIsAuthenticated || taskHistory.length < 4 ? : } -
{/* Show the task history preview if expanded and tasks exist */} {taskHistory.length > 0 && isExpanded && } +

+ + the docs + + ), + }} + /> +

+
)} @@ -1722,7 +1428,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction +
)} @@ -1733,7 +1439,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction
-
- -
- {areButtonsVisible && ( + + {showScrollToBottom ? ( +
+
{ + scrollToBottomSmooth() + disableAutoScrollRef.current = false + }} + title={t("chat:scrollToBottom")}> + +
+
+ ) : (
- {showScrollToBottom ? ( - - { - scrollToBottomSmooth() - disableAutoScrollRef.current = false - }}> - - - - ) : ( - <> - {primaryButtonText && !isStreaming && ( - - handlePrimaryButtonClick(inputValue, selectedImages)}> - {primaryButtonText} - - - )} - {(secondaryButtonText || isStreaming) && ( - - handleSecondaryButtonClick(inputValue, selectedImages)}> - {isStreaming ? t("chat:cancel.title") : secondaryButtonText} - - - )} - + {primaryButtonText && !isStreaming && ( + handlePrimaryButtonClick(inputValue, selectedImages)}> + {primaryButtonText} + + )} + {(secondaryButtonText || isStreaming) && ( + handleSecondaryButtonClick(inputValue, selectedImages)}> + {isStreaming ? t("chat:cancel.title") : secondaryButtonText} + )}
)} diff --git a/webview-ui/src/components/chat/CodebaseSearchResult.tsx b/webview-ui/src/components/chat/CodebaseSearchResult.tsx index 8280ea3d47..d5a8e6407f 100644 --- a/webview-ui/src/components/chat/CodebaseSearchResult.tsx +++ b/webview-ui/src/components/chat/CodebaseSearchResult.tsx @@ -1,7 +1,5 @@ import React from "react" -import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" -import { StandardTooltip } from "@/components/ui" interface CodebaseSearchResultProps { filePath: string @@ -13,8 +11,6 @@ interface CodebaseSearchResultProps { } const CodebaseSearchResult: React.FC = ({ filePath, score, startLine, endLine }) => { - const { t } = useTranslation("chat") - const handleClick = () => { console.log(filePath) vscode.postMessage({ @@ -27,23 +23,19 @@ const CodebaseSearchResult: React.FC = ({ filePath, s } return ( - -
-
- - {filePath.split("/").at(-1)}:{startLine === endLine ? startLine : `${startLine}-${endLine}`} - - - {filePath.split("/").slice(0, -1).join("/")} - - - {score.toFixed(3)} - -
+
+
+ + {filePath.split("/").at(-1)}:{startLine}-{endLine} + + + {filePath.split("/").slice(0, -1).join("/")} +
- +
) } diff --git a/webview-ui/src/components/chat/ContextWindowProgress.tsx b/webview-ui/src/components/chat/ContextWindowProgress.tsx index 1ae80bb3db..a5490d9d4f 100644 --- a/webview-ui/src/components/chat/ContextWindowProgress.tsx +++ b/webview-ui/src/components/chat/ContextWindowProgress.tsx @@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next" import { formatLargeNumber } from "@/utils/format" import { calculateTokenDistribution } from "@/utils/model-utils" -import { StandardTooltip } from "@/components/ui" interface ContextWindowProgressProps { contextWindow: number @@ -27,70 +26,64 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens const safeContextWindow = Math.max(0, contextWindow) const safeContextTokens = Math.max(0, contextTokens) - // Combine all tooltip content into a single tooltip - const tooltipContent = ( -
-
- {t("chat:tokenProgress.tokensUsed", { - used: formatLargeNumber(safeContextTokens), - total: formatLargeNumber(safeContextWindow), - })} -
- {reservedForOutput > 0 && ( -
- {t("chat:tokenProgress.reservedForResponse", { - amount: formatLargeNumber(reservedForOutput), - })} -
- )} - {availableSize > 0 && ( -
- {t("chat:tokenProgress.availableSpace", { - amount: formatLargeNumber(availableSize), - })} -
- )} -
- ) - return ( <>
{formatLargeNumber(safeContextTokens)}
- -
- {/* Main progress bar container */} -
- {/* Current tokens container */} -
- {/* Current tokens used - darkest */} -
-
+
+ {/* Invisible overlay for hover area */} +
- {/* Container for reserved tokens */} + {/* Main progress bar container */} +
+ {/* Current tokens container */} +
+ {/* Invisible overlay for current tokens section */}
- {/* Reserved for output section - medium gray */} -
-
- - {/* Empty section (if any) */} - {availablePercent > 0 && ( -
- {/* Available space - transparent */} -
- )} + className="absolute h-4 -top-[7px] w-full z-6" + title={t("chat:tokenProgress.tokensUsed", { + used: formatLargeNumber(safeContextTokens), + total: formatLargeNumber(safeContextWindow), + })} + data-testid="context-tokens-used" + /> + {/* Current tokens used - darkest */} +
+ + {/* Container for reserved tokens */} +
+ {/* Invisible overlay for reserved section */} +
+ {/* Reserved for output section - medium gray */} +
+
+ + {/* Empty section (if any) */} + {availablePercent > 0 && ( +
+ {/* Invisible overlay for available space */} +
+
+ )}
- +
{formatLargeNumber(safeContextWindow)}
diff --git a/webview-ui/src/components/chat/FollowUpSuggest.tsx b/webview-ui/src/components/chat/FollowUpSuggest.tsx index 1ffe31bbcb..44a30ca803 100644 --- a/webview-ui/src/components/chat/FollowUpSuggest.tsx +++ b/webview-ui/src/components/chat/FollowUpSuggest.tsx @@ -1,100 +1,23 @@ -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import { Edit } from "lucide-react" -import { Button, StandardTooltip } from "@/components/ui" +import { Button } from "@/components/ui" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { useExtensionState } from "@src/context/ExtensionStateContext" -import { SuggestionItem } from "@roo-code/types" - -const DEFAULT_FOLLOWUP_TIMEOUT_MS = 60000 -const COUNTDOWN_INTERVAL_MS = 1000 interface FollowUpSuggestProps { - suggestions?: SuggestionItem[] - onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void + suggestions?: string[] + onSuggestionClick?: (answer: string, event?: React.MouseEvent) => void ts: number - onCancelAutoApproval?: () => void - isAnswered?: boolean } -export const FollowUpSuggest = ({ - suggestions = [], - onSuggestionClick, - ts = 1, - onCancelAutoApproval, - isAnswered = false, -}: FollowUpSuggestProps) => { - const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState() - const [countdown, setCountdown] = useState(null) - const [suggestionSelected, setSuggestionSelected] = useState(false) +export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }: FollowUpSuggestProps) => { const { t } = useAppTranslation() - - // Start countdown timer when auto-approval is enabled for follow-up questions - useEffect(() => { - // Only start countdown if auto-approval is enabled for follow-up questions and no suggestion has been selected - // Also stop countdown if the question has been answered - if ( - autoApprovalEnabled && - alwaysAllowFollowupQuestions && - suggestions.length > 0 && - !suggestionSelected && - !isAnswered - ) { - // Start with the configured timeout in seconds - const timeoutMs = - typeof followupAutoApproveTimeoutMs === "number" && !isNaN(followupAutoApproveTimeoutMs) - ? followupAutoApproveTimeoutMs - : DEFAULT_FOLLOWUP_TIMEOUT_MS - - // Convert milliseconds to seconds for the countdown - setCountdown(Math.floor(timeoutMs / 1000)) - - // Update countdown every second - const intervalId = setInterval(() => { - setCountdown((prevCountdown) => { - if (prevCountdown === null || prevCountdown <= 1) { - clearInterval(intervalId) - return null - } - return prevCountdown - 1 - }) - }, COUNTDOWN_INTERVAL_MS) - - // Clean up interval on unmount and notify parent component - return () => { - clearInterval(intervalId) - // Notify parent component that this component is unmounting - // so it can clear any related timeouts - onCancelAutoApproval?.() - } - } else { - setCountdown(null) - } - }, [ - autoApprovalEnabled, - alwaysAllowFollowupQuestions, - suggestions, - followupAutoApproveTimeoutMs, - suggestionSelected, - onCancelAutoApproval, - isAnswered, - ]) const handleSuggestionClick = useCallback( - (suggestion: SuggestionItem, event: React.MouseEvent) => { - // Mark a suggestion as selected if it's not a shift-click (which just copies to input) - if (!event.shiftKey) { - setSuggestionSelected(true) - // Also notify parent component to cancel auto-approval timeout - // This prevents race conditions between visual countdown and actual timeout - onCancelAutoApproval?.() - } - - // Pass the suggestion object to the parent component - // The parent component will handle mode switching if needed + (suggestion: string, event: React.MouseEvent) => { onSuggestionClick?.(suggestion, event) }, - [onSuggestionClick, onCancelAutoApproval], + [onSuggestionClick], ) // Don't render if there are no suggestions or no click handler. @@ -104,47 +27,29 @@ export const FollowUpSuggest = ({ return (
- {suggestions.map((suggestion, index) => { - const isFirstSuggestion = index === 0 - - return ( -
- +
{ + e.stopPropagation() + // Simulate shift-click by directly calling the handler with shiftKey=true. + onSuggestionClick?.(suggestion, { ...e, shiftKey: true }) + }} + title={t("chat:followUpSuggest.copyToInput")}> + - {suggestion.mode && ( -
- - {suggestion.mode} -
- )} - -
{ - e.stopPropagation() - // Simulate shift-click by directly calling the handler with shiftKey=true. - onSuggestionClick?.(suggestion, { ...e, shiftKey: true }) - }}> - -
-
- ) - })} +
+ ))}
) } diff --git a/webview-ui/src/components/chat/IconButton.tsx b/webview-ui/src/components/chat/IconButton.tsx index 75d8bc4b0b..208b836158 100644 --- a/webview-ui/src/components/chat/IconButton.tsx +++ b/webview-ui/src/components/chat/IconButton.tsx @@ -1,5 +1,4 @@ import { cn } from "@/lib/utils" -import { StandardTooltip } from "@/components/ui" interface IconButtonProps extends React.ButtonHTMLAttributes { iconClass: string @@ -36,17 +35,15 @@ export const IconButton: React.FC = ({ const iconClasses = cn("codicon", iconClass, isLoading && "codicon-modifier-spin") - const button = ( + return ( ) - - return {button} } diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index ba838284d7..a209ce8723 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -2,7 +2,6 @@ import { memo, useState } from "react" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import { useCopyToClipboard } from "@src/utils/clipboard" -import { StandardTooltip } from "@src/components/ui" import MarkdownBlock from "../common/MarkdownBlock" @@ -35,31 +34,30 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia borderRadius: "4px", }}> - - { - const success = await copyWithFeedback(markdown) - if (success) { - const button = document.activeElement as HTMLElement - if (button) { - button.style.background = "var(--vscode-button-background)" - setTimeout(() => { - button.style.background = "" - }, 200) - } + { + const success = await copyWithFeedback(markdown) + if (success) { + const button = document.activeElement as HTMLElement + if (button) { + button.style.background = "var(--vscode-button-background)" + setTimeout(() => { + button.style.background = "" + }, 200) } - }}> - - - + } + }} + title="Copy as markdown"> + +
)}
diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx index a96f368a17..8e0882340b 100644 --- a/webview-ui/src/components/chat/McpExecution.tsx +++ b/webview-ui/src/components/chat/McpExecution.tsx @@ -242,7 +242,6 @@ export const McpExecution = ({ serverName={useMcpServer.serverName} serverSource={server?.source} alwaysAllowMcp={alwaysAllowMcp} - isInChatContext={true} />
)} @@ -257,7 +256,6 @@ export const McpExecution = ({ serverName={serverName} serverSource={undefined} alwaysAllowMcp={alwaysAllowMcp} - isInChatContext={true} />
)} diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx deleted file mode 100644 index 336e9f8357..0000000000 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from "react" -import { ChevronUp, Check } from "lucide-react" -import { cn } from "@/lib/utils" -import { useRooPortal } from "@/components/ui/hooks/useRooPortal" -import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" -import { IconButton } from "./IconButton" -import { vscode } from "@/utils/vscode" -import { useExtensionState } from "@/context/ExtensionStateContext" -import { useAppTranslation } from "@/i18n/TranslationContext" -import { Mode, getAllModes } from "@roo/modes" -import { ModeConfig, CustomModePrompts } from "@roo-code/types" -import { telemetryClient } from "@/utils/TelemetryClient" -import { TelemetryEventName } from "@roo-code/types" - -interface ModeSelectorProps { - value: Mode - onChange: (value: Mode) => void - disabled?: boolean - title?: string - triggerClassName?: string - modeShortcutText: string - customModes?: ModeConfig[] - customModePrompts?: CustomModePrompts -} - -export const ModeSelector = ({ - value, - onChange, - disabled = false, - title = "", - triggerClassName = "", - modeShortcutText, - customModes, - customModePrompts, -}: ModeSelectorProps) => { - const [open, setOpen] = React.useState(false) - const portalContainer = useRooPortal("roo-portal") - const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() - const { t } = useAppTranslation() - - const trackModeSelectorOpened = () => { - // Track telemetry every time the mode selector is opened - telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED) - - // Track first-time usage for UI purposes - if (!hasOpenedModeSelector) { - setHasOpenedModeSelector(true) - vscode.postMessage({ type: "hasOpenedModeSelector", bool: true }) - } - } - - // Get all modes including custom modes and merge custom prompt descriptions - const modes = React.useMemo(() => { - const allModes = getAllModes(customModes) - return allModes.map((mode) => ({ - ...mode, - description: customModePrompts?.[mode.slug]?.description ?? mode.description, - })) - }, [customModes, customModePrompts]) - - // Find the selected mode - const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) - - const trigger = ( - - - {selectedMode?.name || ""} - - ) - - return ( - { - if (isOpen) trackModeSelectorOpened() - setOpen(isOpen) - }} - data-testid="mode-selector-root"> - {title ? {trigger} : trigger} - - -
-
-
-

{t("chat:modeSelector.title")}

-
- { - window.postMessage( - { - type: "action", - action: "marketplaceButtonClicked", - values: { marketplaceTab: "mode" }, - }, - "*", - ) - - setOpen(false) - }} - /> - { - vscode.postMessage({ - type: "switchTab", - tab: "modes", - }) - setOpen(false) - }} - /> -
-
-

- {t("chat:modeSelector.description")} -
- {modeShortcutText} -

-
- - {/* Mode List */} -
- {modes.map((mode) => ( -
{ - onChange(mode.slug as Mode) - setOpen(false) - }} - data-testid="mode-selector-item"> -
-

{mode.name}

- {mode.description && ( -

- {mode.description} -

- )} -
- {mode.slug === value ? ( - - ) : ( -
- )} -
- ))} -
-
- - - ) -} - -export default ModeSelector diff --git a/webview-ui/src/components/chat/TaskActions.tsx b/webview-ui/src/components/chat/TaskActions.tsx index 603b6be3e0..cef27408eb 100644 --- a/webview-ui/src/components/chat/TaskActions.tsx +++ b/webview-ui/src/components/chat/TaskActions.tsx @@ -5,11 +5,20 @@ import { useTranslation } from "react-i18next" import type { HistoryItem } from "@roo-code/types" import { vscode } from "@/utils/vscode" -import { useCopyToClipboard } from "@/utils/clipboard" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { + Button, + Popover, + PopoverContent, + PopoverTrigger, + Command, + CommandList, + CommandItem, + CommandGroup, +} from "@/components/ui" import { DeleteTaskDialog } from "../history/DeleteTaskDialog" import { IconButton } from "./IconButton" -import { ShareButton } from "./ShareButton" interface TaskActionsProps { item?: HistoryItem @@ -18,24 +27,72 @@ interface TaskActionsProps { export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => { const [deleteTaskId, setDeleteTaskId] = useState(null) + const [shareDropdownOpen, setShareDropdownOpen] = useState(false) const { t } = useTranslation() - const { copyWithFeedback, showCopyFeedback } = useCopyToClipboard() + const { sharingEnabled } = useExtensionState() + + const handleShare = (visibility: "organization" | "public") => { + vscode.postMessage({ + type: "shareCurrentTask", + visibility, + }) + setShareDropdownOpen(false) + } return (
- + {item?.id && sharingEnabled && ( + + + + + + + + + handleShare("organization")} + className="cursor-pointer"> +
+ +
+ {t("chat:task.shareWithOrganization")} + + {t("chat:task.shareWithOrganizationDescription")} + +
+
+
+ handleShare("public")} className="cursor-pointer"> +
+ +
+ {t("chat:task.sharePublicly")} + + {t("chat:task.sharePubliclyDescription")} + +
+
+
+
+
+
+
+
+ )} vscode.postMessage({ type: "exportCurrentTask" })} /> - {item?.task && ( - copyWithFeedback(item.task, e)} - /> - )} {!!item?.size && item.size > 0 && ( <>
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 1896df486b..7e7ad860b3 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -10,14 +10,13 @@ import { getModelMaxOutputTokens } from "@roo/api" import { formatLargeNumber } from "@src/utils/format" import { cn } from "@src/lib/utils" -import { Button, StandardTooltip } from "@src/components/ui" +import { Button } from "@src/components/ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import Thumbnails from "../common/Thumbnails" import { TaskActions } from "./TaskActions" -import { ShareButton } from "./ShareButton" import { ContextWindowProgress } from "./ContextWindowProgress" import { Mention } from "./Mention" import { TodoListDisplay } from "./TodoListDisplay" @@ -26,6 +25,7 @@ export interface TaskHeaderProps { task: ClineMessage tokensIn: number tokensOut: number + doesModelSupportPromptCache: boolean cacheWrites?: number cacheReads?: number totalCost: number @@ -40,6 +40,7 @@ const TaskHeader = ({ task, tokensIn, tokensOut, + doesModelSupportPromptCache, cacheWrites, cacheReads, totalCost, @@ -61,14 +62,13 @@ const TaskHeader = ({ const { width: windowWidth } = useWindowSize() const condenseButton = ( - - - + ) const hasTodos = todos && Array.isArray(todos) && todos.length > 0 @@ -102,11 +102,14 @@ const TaskHeader = ({ )}
- - - +
{/* Collapsed state: Track context and cost if we have any */} {!isTaskExpanded && contextWindow > 0 && ( @@ -121,7 +124,6 @@ const TaskHeader = ({ } /> {condenseButton} - {!!totalCost && ${totalCost.toFixed(2)}}
)} @@ -188,24 +190,25 @@ const TaskHeader = ({ {!totalCost && }
- {((typeof cacheReads === "number" && cacheReads > 0) || - (typeof cacheWrites === "number" && cacheWrites > 0)) && ( -
- {t("chat:task.cache")} - {typeof cacheWrites === "number" && cacheWrites > 0 && ( - - - {formatLargeNumber(cacheWrites)} - - )} - {typeof cacheReads === "number" && cacheReads > 0 && ( - - - {formatLargeNumber(cacheReads)} - - )} -
- )} + {doesModelSupportPromptCache && + ((typeof cacheReads === "number" && cacheReads > 0) || + (typeof cacheWrites === "number" && cacheWrites > 0)) && ( +
+ {t("chat:task.cache")} + {typeof cacheWrites === "number" && cacheWrites > 0 && ( + + + {formatLargeNumber(cacheWrites)} + + )} + {typeof cacheReads === "number" && cacheReads > 0 && ( + + + {formatLargeNumber(cacheReads)} + + )} +
+ )} {!!totalCost && (
diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx index 42c2d6ff50..7048022440 100644 --- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import { Package } from "@roo/package" diff --git a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx index 6b2a290c63..7aef88d0de 100644 --- a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index f53bab76a4..8d76468896 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -1,4 +1,4 @@ -import { render, fireEvent, screen } from "@/utils/test-utils" +import { render, fireEvent, screen } from "@testing-library/react" import { defaultModeSlug } from "@roo/modes" @@ -907,7 +907,7 @@ describe("ChatTextArea", () => { describe("selectApiConfig", () => { // Helper function to get the API config dropdown const getApiConfigDropdown = () => { - return screen.getByTestId("dropdown-trigger") + return screen.getByTitle("chat:selectApiConfig") } it("should be enabled independently of sendingDisabled", () => { render() diff --git a/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx index 15405396f7..3e819904fe 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx @@ -1,6 +1,6 @@ // npx vitest run src/components/chat/__tests__/ChatView.auto-approve.spec.tsx -import { render, waitFor } from "@/utils/test-utils" +import { render, waitFor } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext" diff --git a/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx index 7545dae140..1825882d63 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx @@ -1,7 +1,7 @@ // npx vitest run src/components/chat/__tests__/ChatView.spec.tsx import React from "react" -import { render, waitFor, act } from "@/utils/test-utils" +import { render, waitFor, act } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext" diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx index 37eb291530..57f2a52a09 100644 --- a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils" +import { render, screen, fireEvent, waitFor, act } from "@testing-library/react" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx deleted file mode 100644 index d6fc81368d..0000000000 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react" -import { render, screen } from "@/utils/test-utils" -import { describe, test, expect, vi } from "vitest" -import ModeSelector from "../ModeSelector" -import { Mode } from "@roo/modes" - -// Mock the dependencies -vi.mock("@/utils/vscode", () => ({ - vscode: { - postMessage: vi.fn(), - }, -})) - -vi.mock("@/context/ExtensionStateContext", () => ({ - useExtensionState: () => ({ - hasOpenedModeSelector: false, - setHasOpenedModeSelector: vi.fn(), - }), -})) - -vi.mock("@/i18n/TranslationContext", () => ({ - useAppTranslation: () => ({ - t: (key: string) => key, - }), -})) - -vi.mock("@/components/ui/hooks/useRooPortal", () => ({ - useRooPortal: () => document.body, -})) - -describe("ModeSelector", () => { - test("shows custom description from customModePrompts", () => { - const customModePrompts = { - code: { - description: "Custom code mode description", - }, - } - - render( - , - ) - - // The component should be rendered - expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() - }) - - test("falls back to default description when no custom prompt", () => { - render() - - // The component should be rendered - expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() - }) -}) diff --git a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx deleted file mode 100644 index cbe5620264..0000000000 --- a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { describe, test, expect, vi, beforeEach } from "vitest" -import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" -import { ShareButton } from "../ShareButton" -import { useTranslation } from "react-i18next" -import { vscode } from "@/utils/vscode" - -// Mock the vscode utility -vi.mock("@/utils/vscode", () => ({ - vscode: { - postMessage: vi.fn(), - }, -})) - -// Mock react-i18next -vi.mock("react-i18next") - -// Mock the extension state context -vi.mock("@/context/ExtensionStateContext", () => ({ - ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => children, - useExtensionState: () => ({ - sharingEnabled: true, - cloudIsAuthenticated: true, - cloudUserInfo: { - id: "test-user", - email: "test@example.com", - organizationName: "Test Organization", - }, - }), -})) - -// Mock telemetry client -vi.mock("@/utils/TelemetryClient", () => ({ - telemetryClient: { - capture: vi.fn(), - }, -})) - -const mockUseTranslation = vi.mocked(useTranslation) -const mockVscode = vi.mocked(vscode) - -describe("ShareButton", () => { - const mockT = vi.fn((key: string) => key) - const mockItem = { - id: "test-task-id", - number: 1, - ts: Date.now(), - task: "Test Task", - tokensIn: 100, - tokensOut: 50, - totalCost: 0.01, - } - - beforeEach(() => { - vi.clearAllMocks() - - mockUseTranslation.mockReturnValue({ - t: mockT, - i18n: {} as any, - ready: true, - } as any) - }) - - test("renders share button", () => { - render() - - const button = screen.getByRole("button") - expect(button).toBeInTheDocument() - }) - - test("opens popover when clicked", async () => { - render() - - const button = screen.getByRole("button") - fireEvent.click(button) - - await waitFor(() => { - expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() - }) - }) - - test("sends organization share message when organization button clicked", async () => { - render() - - // Open popover - const button = screen.getByRole("button") - fireEvent.click(button) - - await waitFor(() => { - expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() - }) - - // Click organization share button - const orgButton = screen.getByText("chat:task.shareWithOrganization") - fireEvent.click(orgButton) - - expect(mockVscode.postMessage).toHaveBeenCalledWith({ - type: "shareCurrentTask", - visibility: "organization", - }) - }) - - test("sends public share message when public button clicked", async () => { - render() - - // Open popover - const button = screen.getByRole("button") - fireEvent.click(button) - - await waitFor(() => { - expect(screen.getByText("chat:task.sharePublicly")).toBeInTheDocument() - }) - - // Click public share button - const publicButton = screen.getByText("chat:task.sharePublicly") - fireEvent.click(publicButton) - - expect(mockVscode.postMessage).toHaveBeenCalledWith({ - type: "shareCurrentTask", - visibility: "public", - }) - }) - - test("displays success message when shareTaskSuccess message received", async () => { - const mockAddEventListener = vi.fn() - const mockRemoveEventListener = vi.fn() - - // Mock window.addEventListener - Object.defineProperty(window, "addEventListener", { - value: mockAddEventListener, - writable: true, - }) - Object.defineProperty(window, "removeEventListener", { - value: mockRemoveEventListener, - writable: true, - }) - - render() - - // Get the message event listener that was registered - const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] - - expect(messageListener).toBeDefined() - - // Open popover first - const button = screen.getByRole("button") - fireEvent.click(button) - - await waitFor(() => { - expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() - }) - - // Simulate receiving a shareTaskSuccess message - const mockEvent = { - data: { - type: "shareTaskSuccess", - visibility: "organization", - text: "https://example.com/share/123", - }, - } - - messageListener(mockEvent) - - await waitFor(() => { - expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument() - }) - }) - - test("displays different success messages based on visibility", async () => { - const mockAddEventListener = vi.fn() - - Object.defineProperty(window, "addEventListener", { - value: mockAddEventListener, - writable: true, - }) - - render() - - const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] - - // Open popover - const button = screen.getByRole("button") - fireEvent.click(button) - - await waitFor(() => { - expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() - }) - - // Test public visibility success message - const publicEvent = { - data: { - type: "shareTaskSuccess", - visibility: "public", - text: "https://example.com/share/456", - }, - } - - messageListener(publicEvent) - - await waitFor(() => { - expect(screen.getByText("chat:task.shareSuccessPublic")).toBeInTheDocument() - }) - }) - - test("auto-hides success message after 5 seconds", async () => { - vi.useFakeTimers({ shouldAdvanceTime: true }) - - const mockAddEventListener = vi.fn() - - Object.defineProperty(window, "addEventListener", { - value: mockAddEventListener, - writable: true, - }) - - render() - - const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] - - // Open popover - const button = screen.getByRole("button") - fireEvent.click(button) - - await vi.waitFor(() => { - expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() - }) - - // Simulate success message - const mockEvent = { - data: { - type: "shareTaskSuccess", - visibility: "organization", - text: "https://example.com/share/123", - }, - } - - messageListener(mockEvent) - - await vi.waitFor(() => { - expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument() - }) - - // Fast-forward 5 seconds - await vi.advanceTimersByTimeAsync(5000) - - // The success message and share options should both be gone (popover closed) - expect(screen.queryByText("chat:task.shareSuccessOrganization")).not.toBeInTheDocument() - expect(screen.queryByText("chat:task.shareWithOrganization")).not.toBeInTheDocument() - - vi.useRealTimers() - }) - - test("clears previous success state when sharing again", async () => { - vi.useFakeTimers({ shouldAdvanceTime: true }) - - const mockAddEventListener = vi.fn() - - Object.defineProperty(window, "addEventListener", { - value: mockAddEventListener, - writable: true, - }) - - render() - - const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1] - - // Open popover - const button = screen.getByRole("button") - fireEvent.click(button) - - await vi.waitFor(() => { - expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() - }) - - // Click organization share button first time - const orgButton = screen.getByText("chat:task.shareWithOrganization") - fireEvent.click(orgButton) - - // Verify first share message was sent - expect(mockVscode.postMessage).toHaveBeenCalledWith({ - type: "shareCurrentTask", - visibility: "organization", - }) - - // Clear mock to track new calls - mockVscode.postMessage.mockClear() - - // Show success message - const mockEvent = { - data: { - type: "shareTaskSuccess", - visibility: "organization", - text: "https://example.com/share/123", - }, - } - - messageListener(mockEvent) - - await vi.waitFor(() => { - expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument() - }) - - // Wait for success message to auto-hide after 5 seconds - await vi.advanceTimersByTimeAsync(5000) - - // Success message should be gone and popover should be closed - expect(screen.queryByText("chat:task.shareSuccessOrganization")).not.toBeInTheDocument() - - // Open popover again - fireEvent.click(button) - await vi.waitFor(() => { - expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument() - }) - - // Click share again - const orgButton2 = screen.getByText("chat:task.shareWithOrganization") - fireEvent.click(orgButton2) - - // Verify the share message was sent again (no success message should be showing) - expect(mockVscode.postMessage).toHaveBeenCalledWith({ - type: "shareCurrentTask", - visibility: "organization", - }) - - vi.useRealTimers() - }) -}) diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index c04f7e45e5..784a263531 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -1,7 +1,7 @@ // npx vitest src/components/chat/__tests__/TaskHeader.spec.tsx import React from "react" -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import type { ProviderSettings } from "@roo-code/types" @@ -49,6 +49,7 @@ describe("TaskHeader", () => { task: { type: "say", ts: Date.now(), text: "Test task", images: [] }, tokensIn: 100, tokensOut: 50, + doesModelSupportPromptCache: true, totalCost: 0.05, contextTokens: 200, buttonsDisabled: false, @@ -93,33 +94,22 @@ describe("TaskHeader", () => { it("should render the condense context button", () => { renderTaskHeader() - // Find the button that contains the FoldVertical icon - const buttons = screen.getAllByRole("button") - const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) - expect(condenseButton).toBeDefined() - expect(condenseButton?.querySelector("svg")).toBeInTheDocument() + expect(screen.getByTitle("chat:task.condenseContext")).toBeInTheDocument() }) it("should call handleCondenseContext when condense context button is clicked", () => { const handleCondenseContext = vi.fn() renderTaskHeader({ handleCondenseContext }) - // Find the button that contains the FoldVertical icon - const buttons = screen.getAllByRole("button") - const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) - expect(condenseButton).toBeDefined() - fireEvent.click(condenseButton!) + const condenseButton = screen.getByTitle("chat:task.condenseContext") + fireEvent.click(condenseButton) expect(handleCondenseContext).toHaveBeenCalledWith("test-task-id") }) it("should disable the condense context button when buttonsDisabled is true", () => { const handleCondenseContext = vi.fn() renderTaskHeader({ buttonsDisabled: true, handleCondenseContext }) - // Find the button that contains the FoldVertical icon - const buttons = screen.getAllByRole("button") - const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) - expect(condenseButton).toBeDefined() - expect(condenseButton).toBeDisabled() - fireEvent.click(condenseButton!) + const condenseButton = screen.getByTitle("chat:task.condenseContext") + fireEvent.click(condenseButton) expect(handleCondenseContext).not.toHaveBeenCalled() }) }) diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index 21b4f486c7..348e230619 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -2,7 +2,7 @@ import { useState, useCallback } from "react" import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons" import { useTranslation } from "react-i18next" -import { Button, Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" +import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui" import { useRooPortal } from "@/components/ui/hooks" import { vscode } from "@src/utils/vscode" @@ -48,11 +48,13 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec return (
{isDiffAvailable && ( - - - + )} {isRestoreAvailable && ( - - - - - + + +
{!isCurrent && ( diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index 28492acd8b..da3eb6429c 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -4,11 +4,8 @@ import { useCopyToClipboard } from "@src/utils/clipboard" import { getHighlighter, isLanguageLoaded, normalizeLanguage, ExtendedLanguage } from "@src/utils/highlighter" import { bundledLanguages } from "shiki" import type { ShikiTransformer } from "shiki" -import { toJsxRuntime } from "hast-util-to-jsx-runtime" -import { Fragment, jsx, jsxs } from "react/jsx-runtime" import { ChevronDown, ChevronUp, WrapText, AlignJustify, Copy, Check } from "lucide-react" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { StandardTooltip } from "@/components/ui" export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))" export const WRAPPER_ALPHA = "cc" // 80% opacity @@ -228,17 +225,13 @@ const CodeBlock = memo( const [windowShade, setWindowShade] = useState(initialWindowShade) const [currentLanguage, setCurrentLanguage] = useState(() => normalizeLanguage(language)) const userChangedLanguageRef = useRef(false) - const [highlightedCode, setHighlightedCode] = useState(null) + const [highlightedCode, setHighlightedCode] = useState("") const [showCollapseButton, setShowCollapseButton] = useState(true) const codeBlockRef = useRef(null) const preRef = useRef(null) const copyButtonWrapperRef = useRef(null) const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard() const { t } = useAppTranslation() - const isMountedRef = useRef(true) - const buttonPositionTimeoutRef = useRef(null) - const collapseTimeout1Ref = useRef(null) - const collapseTimeout2Ref = useRef(null) // Update current language when prop changes, but only if user hasn't // made a selection. @@ -250,30 +243,19 @@ const CodeBlock = memo( } }, [language, currentLanguage]) - // Syntax highlighting with cached Shiki instance and mounted state management + // Syntax highlighting with cached Shiki instance. useEffect(() => { - // Set mounted state at the beginning of this effect - isMountedRef.current = true - - // Create a safe fallback using React elements instead of HTML string - const fallback = ( -
-					{source || ""}
-				
- ) + const fallback = `
${source || ""}
` const highlight = async () => { // Show plain text if language needs to be loaded. if (currentLanguage && !isLanguageLoaded(currentLanguage)) { - if (isMountedRef.current) { - setHighlightedCode(fallback) - } + setHighlightedCode(fallback) } const highlighter = await getHighlighter(currentLanguage) - if (!isMountedRef.current) return - const hast = await highlighter.codeToHast(source || "", { + const html = await highlighter.codeToHtml(source || "", { lang: currentLanguage || "txt", theme: document.body.className.toLowerCase().includes("light") ? "github-light" : "github-dark", transformers: [ @@ -295,53 +277,14 @@ const CodeBlock = memo( }, ] as ShikiTransformer[], }) - if (!isMountedRef.current) return - // Convert HAST to React elements using hast-util-to-jsx-runtime - // This approach eliminates XSS vulnerabilities by avoiding dangerouslySetInnerHTML - // while maintaining the exact same visual output and syntax highlighting - try { - const reactElement = toJsxRuntime(hast, { - Fragment, - jsx, - jsxs, - // Don't override components - let them render as-is to maintain exact output - }) - - if (isMountedRef.current) { - setHighlightedCode(reactElement) - } - } catch (error) { - console.error("[CodeBlock] Error converting HAST to JSX:", error) - if (isMountedRef.current) { - setHighlightedCode(fallback) - } - } + setHighlightedCode(html) } highlight().catch((e) => { console.error("[CodeBlock] Syntax highlighting error:", e, "\nStack trace:", e.stack) - if (isMountedRef.current) { - setHighlightedCode(fallback) - } + setHighlightedCode(fallback) }) - - // Cleanup function - manage mounted state and clear all timeouts - return () => { - isMountedRef.current = false - if (buttonPositionTimeoutRef.current) { - clearTimeout(buttonPositionTimeoutRef.current) - buttonPositionTimeoutRef.current = null - } - if (collapseTimeout1Ref.current) { - clearTimeout(collapseTimeout1Ref.current) - collapseTimeout1Ref.current = null - } - if (collapseTimeout2Ref.current) { - clearTimeout(collapseTimeout2Ref.current) - collapseTimeout2Ref.current = null - } - } }, [source, currentLanguage, collapsedHeight]) // Check if content height exceeds collapsed height whenever content changes @@ -512,15 +455,8 @@ const CodeBlock = memo( // Update button position and scroll when highlightedCode changes useEffect(() => { if (highlightedCode) { - // Clear any existing timeout before setting a new one - if (buttonPositionTimeoutRef.current) { - clearTimeout(buttonPositionTimeoutRef.current) - } // Update button position - buttonPositionTimeoutRef.current = setTimeout(() => { - updateCodeBlockButtonPosition() - buttonPositionTimeoutRef.current = null // Optional: Clear ref after execution - }, 0) + setTimeout(updateCodeBlockButtonPosition, 0) // Scroll to bottom if needed (immediately after Shiki updates) if (shouldScrollAfterHighlightRef.current) { @@ -543,12 +479,6 @@ const CodeBlock = memo( shouldScrollAfterHighlightRef.current = false } } - // Cleanup function for this effect - return () => { - if (buttonPositionTimeoutRef.current) { - clearTimeout(buttonPositionTimeoutRef.current) - } - } }, [highlightedCode, updateCodeBlockButtonPosition]) // Advanced inertial scroll chaining @@ -750,55 +680,41 @@ const CodeBlock = memo( } {showCollapseButton && ( - - { - // Get the current code block element - const codeBlock = codeBlockRef.current // Capture ref early - // Toggle window shade state - setWindowShade(!windowShade) + { + // Get the current code block element and scrollable container + const codeBlock = codeBlockRef.current + const scrollContainer = document.querySelector('[data-virtuoso-scroller="true"]') + if (!codeBlock || !scrollContainer) return - // Clear any previous timeouts - if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current) - if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current) + // Toggle window shade state + setWindowShade(!windowShade) - // After UI updates, ensure code block is visible and update button position - collapseTimeout1Ref.current = setTimeout( - () => { - if (codeBlock) { - // Check if codeBlock element still exists - codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" }) + // After UI updates, ensure code block is visible and update button position + setTimeout( + () => { + codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" }) - // Wait for scroll to complete before updating button position - collapseTimeout2Ref.current = setTimeout(() => { - // updateCodeBlockButtonPosition itself should also check for refs if needed - updateCodeBlockButtonPosition() - collapseTimeout2Ref.current = null - }, 50) - } - collapseTimeout1Ref.current = null - }, - WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50, - ) - }}> - {windowShade ? : } - - + // Wait for scroll to complete before updating button position + setTimeout(() => { + updateCodeBlockButtonPosition() + }, 50) + }, + WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50, + ) + }} + title={t(`chat:codeblock.tooltips.${windowShade ? "expand" : "collapse"}`)}> + {windowShade ? : } + )} - - setWordWrap(!wordWrap)}> - {wordWrap ? : } - - - - - {showCopyFeedback ? : } - - + setWordWrap(!wordWrap)} + title={t(`chat:codeblock.tooltips.${wordWrap ? "disable_wrap" : "enable_wrap"}`)}> + {wordWrap ? : } + + + {showCopyFeedback ? : } + )} @@ -807,7 +723,7 @@ const CodeBlock = memo( ) // Memoized content component to prevent unnecessary re-renders of highlighted code -const MemoizedCodeContent = memo(({ children }: { children: React.ReactNode }) => <>{children}) +const MemoizedCodeContent = memo(({ html }: { html: string }) =>
) // Memoized StyledPre component const MemoizedStyledPre = memo( @@ -825,7 +741,7 @@ const MemoizedStyledPre = memo( wordWrap: boolean windowShade: boolean collapsedHeight?: number - highlightedCode: React.ReactNode + highlightedCode: string updateCodeBlockButtonPosition: (forceHide?: boolean) => void }) => ( updateCodeBlockButtonPosition(true)} onMouseUp={() => updateCodeBlockButtonPosition(false)}> - {highlightedCode} + ), ) diff --git a/webview-ui/src/components/common/IconButton.tsx b/webview-ui/src/components/common/IconButton.tsx index ac8b7ca1e2..70a66ba9f1 100644 --- a/webview-ui/src/components/common/IconButton.tsx +++ b/webview-ui/src/components/common/IconButton.tsx @@ -1,5 +1,3 @@ -import { StandardTooltip } from "@/components/ui" - interface IconButtonProps { icon: string onClick?: (e: React.MouseEvent) => void @@ -33,21 +31,15 @@ export function IconButton({ const handleClick = onClick || ((_event: React.MouseEvent) => {}) - const button = ( + return ( ) - - if (title) { - return {button} - } - - return button } diff --git a/webview-ui/src/components/common/MermaidActionButtons.tsx b/webview-ui/src/components/common/MermaidActionButtons.tsx index 79558b9b03..46ded57644 100644 --- a/webview-ui/src/components/common/MermaidActionButtons.tsx +++ b/webview-ui/src/components/common/MermaidActionButtons.tsx @@ -2,7 +2,6 @@ import React from "react" import { useAppTranslation } from "@src/i18n/TranslationContext" import { IconButton } from "./IconButton" import { ZoomControls } from "./ZoomControls" -import { StandardTooltip } from "@/components/ui" interface MermaidActionButtonsProps { onZoom?: (e: React.MouseEvent) => void @@ -41,51 +40,41 @@ export const MermaidActionButtons: React.FC = ({ zoomInTitle={t("common:mermaid.buttons.zoomIn")} zoomOutTitle={t("common:mermaid.buttons.zoomOut")} /> - - { - e.stopPropagation() - onViewCode() - }} - /> - - - - - - ) - } - - return ( - <> - {onZoom && ( - - - - )} - { e.stopPropagation() onViewCode() }} + title={t("common:mermaid.buttons.viewCode")} /> - - - - - {onSave && ( - - - - )} - {onClose && ( - - - - )} + + + ) + } + + return ( + <> + {onZoom && } + { + e.stopPropagation() + onViewCode() + }} + title={t("common:mermaid.buttons.viewCode")} + /> + + {onSave && } + {onClose && } ) } diff --git a/webview-ui/src/components/common/MermaidButton.tsx b/webview-ui/src/components/common/MermaidButton.tsx index 8d77502c6d..6f2c7870f2 100644 --- a/webview-ui/src/components/common/MermaidButton.tsx +++ b/webview-ui/src/components/common/MermaidButton.tsx @@ -7,7 +7,6 @@ import { Modal } from "./Modal" import { TabButton } from "./TabButton" import { IconButton } from "./IconButton" import { ZoomControls } from "./ZoomControls" -import { StandardTooltip } from "@/components/ui" const MIN_ZOOM = 0.5 const MAX_ZOOM = 20 @@ -161,9 +160,11 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
- - setShowModal(false)} /> - + setShowModal(false)} + title={t("common:mermaid.buttons.close")} + />
- - - - - - - - ) : ( - { - e.stopPropagation() - copyWithFeedback(code, e) - }} + onClick={handleCopy} + title={t("common:mermaid.buttons.copy")} /> - + + + ) : ( + { + e.stopPropagation() + copyWithFeedback(code, e) + }} + title={t("common:mermaid.buttons.copy")} + /> )}
diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx index 63eb262b05..6ce2f79994 100644 --- a/webview-ui/src/components/common/TelemetryBanner.tsx +++ b/webview-ui/src/components/common/TelemetryBanner.tsx @@ -45,7 +45,7 @@ const TelemetryBanner = () => { window.postMessage({ type: "action", action: "settingsButtonClicked", - values: { section: "about" }, // Link directly to about settings with telemetry controls + values: { section: "advanced" }, // Link directly to advanced settings with telemetry controls }) } @@ -54,12 +54,7 @@ const TelemetryBanner = () => {
{t("welcome:telemetry.title")}
- , - }} - /> + {t("welcome:telemetry.anonymousTelemetry")}
- - adjustZoom?.(zoomOutStep)) : undefined} - onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomOutStep) : undefined} - onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - /> - + adjustZoom?.(zoomOutStep)) : undefined} + onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomOutStep) : undefined} + onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + />
{Math.round(zoomLevel * 100)}%
- - adjustZoom?.(zoomInStep)) : undefined} - onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomInStep) : undefined} - onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - /> - + adjustZoom?.(zoomInStep)) : undefined} + onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomInStep) : undefined} + onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + />
) } diff --git a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx index f413745b61..af481a5aee 100644 --- a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx @@ -1,6 +1,6 @@ // npx vitest run src/components/common/__tests__/CodeBlock.spec.tsx -import { render, screen, fireEvent, act } from "@/utils/test-utils" +import { render, screen, fireEvent, act } from "@testing-library/react" import CodeBlock from "../CodeBlock" @@ -207,15 +207,9 @@ describe("CodeBlock", () => { codeBlock.setAttribute("data-partially-visible", "true") } - // Find the copy button by looking for the button containing the Copy icon - const buttons = screen.getAllByRole("button") - const copyButton = buttons.find((btn) => btn.querySelector("svg.lucide-copy")) - - expect(copyButton).toBeTruthy() - if (copyButton) { - await act(async () => { - fireEvent.click(copyButton) - }) - } + const copyButton = screen.getByTitle("Copy code") + await act(async () => { + fireEvent.click(copyButton) + }) }) }) diff --git a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx index ec97e4e667..5190f7fe82 100644 --- a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import MarkdownBlock from "../MarkdownBlock" import { vi } from "vitest" diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx index 4243ff8d5a..743b150aae 100644 --- a/webview-ui/src/components/history/CopyButton.tsx +++ b/webview-ui/src/components/history/CopyButton.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react" import { useClipboard } from "@/components/ui/hooks" -import { Button, StandardTooltip } from "@/components/ui" +import { Button } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@/lib/utils" @@ -25,15 +25,14 @@ export const CopyButton = ({ itemTask }: CopyButtonProps) => { ) return ( - - - + ) } diff --git a/webview-ui/src/components/history/DeleteButton.tsx b/webview-ui/src/components/history/DeleteButton.tsx index 3e99027546..b91f13bd50 100644 --- a/webview-ui/src/components/history/DeleteButton.tsx +++ b/webview-ui/src/components/history/DeleteButton.tsx @@ -1,6 +1,6 @@ import { useCallback } from "react" -import { Button, StandardTooltip } from "@/components/ui" +import { Button } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { vscode } from "@/utils/vscode" @@ -25,15 +25,14 @@ export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => { ) return ( - - - + ) } diff --git a/webview-ui/src/components/history/ExportButton.tsx b/webview-ui/src/components/history/ExportButton.tsx index fabc8d3d15..eeba0ccaf4 100644 --- a/webview-ui/src/components/history/ExportButton.tsx +++ b/webview-ui/src/components/history/ExportButton.tsx @@ -1,5 +1,5 @@ import { vscode } from "@/utils/vscode" -import { Button, StandardTooltip } from "@/components/ui" +import { Button } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { useCallback } from "react" @@ -15,15 +15,14 @@ export const ExportButton = ({ itemId }: { itemId: string }) => { ) return ( - - - + ) } diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index 2f156d0418..2d6ee5fa3d 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -5,16 +5,7 @@ import { Virtuoso } from "react-virtuoso" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { - Button, - Checkbox, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - StandardTooltip, -} from "@/components/ui" +import { Button, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { Tab, TabContent, TabHeader } from "../common/Tab" @@ -84,22 +75,20 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {

{t("history:history")}

- - - + + {isSelectionMode ? t("history:exitSelection") : t("history:selectionMode")} +
diff --git a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx index bdcff23cdd..9fe49663d1 100644 --- a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx +++ b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { vscode } from "@/utils/vscode" diff --git a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx index ac1b39859d..0ba1b27a1f 100644 --- a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { useClipboard } from "@/components/ui/hooks" diff --git a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx index 19b333ab44..42c17f5335 100644 --- a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { DeleteButton } from "../DeleteButton" diff --git a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx index f8e244e9bf..e78101f37d 100644 --- a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx +++ b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { vscode } from "@/utils/vscode" diff --git a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx index 1dda83305c..68f4407400 100644 --- a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx b/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx index 7951574963..db7398e384 100644 --- a/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import type { HistoryItem } from "@roo-code/types" diff --git a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx index 3079844aad..030c36f503 100644 --- a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { useExtensionState } from "@src/context/ExtensionStateContext" diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx index 9d4a939a1e..9fcc11e572 100644 --- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import TaskItem from "../TaskItem" diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index 661cecf122..f1390b7d55 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import TaskItemFooter from "../TaskItemFooter" diff --git a/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx index 090bf2521f..02f554d697 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import TaskItemHeader from "../TaskItemHeader" diff --git a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx index bea79814fa..e047a81cf3 100644 --- a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx +++ b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx @@ -1,4 +1,4 @@ -import { renderHook, act } from "@/utils/test-utils" +import { renderHook, act } from "@testing-library/react" import type { HistoryItem } from "@roo-code/types" diff --git a/webview-ui/src/components/marketplace/MarketplaceView.tsx b/webview-ui/src/components/marketplace/MarketplaceView.tsx index b47e1aa875..e74e5bba1b 100644 --- a/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -12,9 +12,8 @@ import { TooltipProvider } from "@/components/ui/tooltip" interface MarketplaceViewProps { onDone?: () => void stateManager: MarketplaceViewStateManager - targetTab?: "mcp" | "mode" } -export function MarketplaceView({ stateManager, onDone, targetTab }: MarketplaceViewProps) { +export function MarketplaceView({ stateManager, onDone }: MarketplaceViewProps) { const { t } = useAppTranslation() const [state, manager] = useStateManager(stateManager) const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false) @@ -27,12 +26,6 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace } }, [state.allItems, hasReceivedInitialState]) - useEffect(() => { - if (targetTab && (targetTab === "mcp" || targetTab === "mode")) { - manager.transition({ type: "SET_ACTIVE_TAB", payload: { tab: targetTab } }) - } - }, [targetTab, manager]) - // Ensure marketplace state manager processes messages when component mounts useEffect(() => { // When the marketplace view first mounts, we need to trigger a state update @@ -81,7 +74,7 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace const filteredTags = useMemo(() => allTags, [allTags]) return ( - +
diff --git a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts index 7498ebfc59..372c89a12b 100644 --- a/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts +++ b/webview-ui/src/components/marketplace/MarketplaceViewStateManager.ts @@ -370,18 +370,6 @@ export class MarketplaceViewStateManager { // Error case void this.transition({ type: "FETCH_ERROR" }) } else { - // Check if a specific tab is requested - if ( - message.values?.marketplaceTab && - (message.values.marketplaceTab === "mcp" || message.values.marketplaceTab === "mode") - ) { - // Set the active tab - void this.transition({ - type: "SET_ACTIVE_TAB", - payload: { tab: message.values.marketplaceTab }, - }) - } - // Refresh request void this.transition({ type: "FETCH_ITEMS" }) } diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx index 06078d638c..8e62af73c9 100644 --- a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx +++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx @@ -1,6 +1,6 @@ // npx vitest run src/components/marketplace/__tests__/MarketplaceListView.spec.tsx -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { TooltipProvider } from "@/components/ui/tooltip" @@ -49,7 +49,7 @@ describe("MarketplaceListView", () => { const renderWithProviders = (props = {}) => render( - + , diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx index 95b2dea54b..10290b70b3 100644 --- a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx +++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { MarketplaceView } from "../MarketplaceView" diff --git a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx index b7e9951b0f..876f5f3f86 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx @@ -195,25 +195,8 @@ export const MarketplaceInstallModal: React.FC = ( } const handlePostInstallAction = (tab: "mcp" | "modes") => { - if (tab === "mcp") { - // Navigate to MCP tab - window.postMessage( - { - type: "action", - action: "mcpButtonClicked", - }, - "*", - ) - } else { - // Navigate to Modes tab - window.postMessage( - { - type: "action", - action: "promptsButtonClicked", - }, - "*", - ) - } + // Send message to switch to the appropriate tab + vscode.postMessage({ type: "switchTab", tab }) // Close the modal onClose() } diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx index 13c515ea63..b59291a35b 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect } from "react" +import React, { useMemo, useState } from "react" import { MarketplaceItem, TelemetryEventName } from "@roo-code/types" import { vscode } from "@/utils/vscode" import { telemetryClient } from "@/utils/TelemetryClient" @@ -7,19 +7,9 @@ import { useAppTranslation } from "@/i18n/TranslationContext" import { isValidUrl } from "../../../utils/url" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" -import { StandardTooltip } from "@/components/ui" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { MarketplaceInstallModal } from "./MarketplaceInstallModal" import { useExtensionState } from "@/context/ExtensionStateContext" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui" interface ItemInstalledMetadata { type: string @@ -39,30 +29,6 @@ export const MarketplaceItemCard: React.FC = ({ item, const { t } = useAppTranslation() const { cwd } = useExtensionState() const [showInstallModal, setShowInstallModal] = useState(false) - const [showRemoveConfirm, setShowRemoveConfirm] = useState(false) - const [removeTarget, setRemoveTarget] = useState<"project" | "global">("project") - const [removeError, setRemoveError] = useState(null) - - // Listen for removal result messages - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - const message = event.data - if (message.type === "marketplaceRemoveResult" && message.slug === item.id) { - if (message.success) { - // Removal succeeded - refresh marketplace data - vscode.postMessage({ - type: "fetchMarketplaceData", - }) - } else { - // Removal failed - show error message to user - setRemoveError(message.error || t("marketplace:items.unknownError")) - } - } - } - - window.addEventListener("message", handleMessage) - return () => window.removeEventListener("message", handleMessage) - }, [item.id, t]) const typeLabel = useMemo(() => { const labels: Partial> = { @@ -113,25 +79,37 @@ export const MarketplaceItemCard: React.FC = ({ item,
{isInstalled ? ( /* Single Remove button when installed */ - + + + + + + + {isInstalledInProject ? t("marketplace:items.card.removeProjectTooltip") - : t("marketplace:items.card.removeGlobalTooltip") - }> - - + : t("marketplace:items.card.removeGlobalTooltip")} + + ) : ( /* Single Install button when not installed */ )} - - {/* Error message display */} - {removeError && ( -
- {t("marketplace:items.removeFailed", { error: removeError })} -
- )}
@@ -168,28 +139,26 @@ export const MarketplaceItemCard: React.FC = ({ item, {item.tags && item.tags.length > 0 && item.tags.map((tag) => ( - { + const newTags = filters.tags.includes(tag) + ? filters.tags.filter((t: string) => t !== tag) + : [...filters.tags, tag] + setFilters({ tags: newTags }) + }} + title={ filters.tags.includes(tag) ? t("marketplace:filters.tags.clear", { count: tag }) : t("marketplace:filters.tags.clickToFilter") }> - - + {tag} + ))}
)} @@ -202,49 +171,6 @@ export const MarketplaceItemCard: React.FC = ({ item, onClose={() => setShowInstallModal(false)} hasWorkspace={!!cwd} /> - - {/* Remove Confirmation Dialog */} - - - - - {item.type === "mode" - ? t("marketplace:removeConfirm.mode.title") - : t("marketplace:removeConfirm.mcp.title")} - - - {item.type === "mode" ? ( - <> - {t("marketplace:removeConfirm.mode.message", { modeName: item.name })} -
- {t("marketplace:removeConfirm.mode.rulesWarning")} -
- - ) : ( - t("marketplace:removeConfirm.mcp.message", { mcpName: item.name }) - )} -
-
- - {t("marketplace:removeConfirm.cancel")} - { - // Clear any previous error - setRemoveError(null) - - vscode.postMessage({ - type: "removeInstalledMarketplaceItem", - mpItem: item, - mpInstallOptions: { target: removeTarget }, - }) - - setShowRemoveConfirm(false) - }}> - {t("marketplace:removeConfirm.confirm")} - - -
-
) } diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx index 29af469f80..8ffb15abd4 100644 --- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx +++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { render, screen, fireEvent, waitFor } from "@testing-library/react" import { MarketplaceItem } from "@roo-code/types" diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx index e586fbb9e1..6bc9453cd9 100644 --- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx +++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { render, screen, fireEvent, waitFor } from "@testing-library/react" import { MarketplaceItem } from "@roo-code/types" diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx index 1f1ed9030b..bd88de7229 100644 --- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx +++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@/utils/test-utils" +import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { MarketplaceItem } from "@roo-code/types" @@ -57,7 +57,7 @@ vi.mock("@/i18n/TranslationContext", () => ({ })) const renderWithProviders = (ui: React.ReactElement) => { - return render({ui}) + return render({ui}) } describe("MarketplaceItemCard", () => { diff --git a/webview-ui/src/components/mcp/McpToolRow.tsx b/webview-ui/src/components/mcp/McpToolRow.tsx index aa57b18fd9..76664fea46 100644 --- a/webview-ui/src/components/mcp/McpToolRow.tsx +++ b/webview-ui/src/components/mcp/McpToolRow.tsx @@ -4,20 +4,16 @@ import { McpTool } from "@roo/mcp" import { useAppTranslation } from "@src/i18n/TranslationContext" import { vscode } from "@src/utils/vscode" -import { StandardTooltip, ToggleSwitch } from "@/components/ui" type McpToolRowProps = { tool: McpTool serverName?: string serverSource?: "global" | "project" alwaysAllowMcp?: boolean - isInChatContext?: boolean } -const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatContext = false }: McpToolRowProps) => { +const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp }: McpToolRowProps) => { const { t } = useAppTranslation() - const isToolEnabled = tool.enabledForPrompt ?? true - const handleAlwaysAllowChange = () => { if (!serverName) return vscode.postMessage({ @@ -48,29 +44,17 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo onClick={(e) => e.stopPropagation()}> {/* Tool name section */}
- - - - {tool.name} - - + + + {tool.name} +
{/* Controls section */} {serverName && (
- {/* Always Allow checkbox - only show when tool is enabled */} - {alwaysAllowMcp && isToolEnabled && ( + {/* Always Allow checkbox */} + {alwaysAllowMcp && ( )} - {/* Enabled toggle switch - only show in settings context */} - {!isInChatContext && ( - - - - )} + {/* Enabled eye button */} +
)}
{tool.description && ( -
- {tool.description} -
+
{tool.description}
)} - {isToolEnabled && - tool.inputSchema && + {tool.inputSchema && "properties" in tool.inputSchema && Object.keys(tool.inputSchema.properties as Record).length > 0 && (
diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx index f686a23f00..43889c3cc5 100644 --- a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx +++ b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, fireEvent, screen } from "@/utils/test-utils" +import { render, fireEvent, screen } from "@testing-library/react" import { vscode } from "@src/utils/vscode" @@ -12,7 +12,6 @@ vi.mock("@src/i18n/TranslationContext", () => ({ "mcp:tool.alwaysAllow": "Always allow", "mcp:tool.parameters": "Parameters", "mcp:tool.noDescription": "No description", - "mcp:tool.togglePromptInclusion": "Toggle prompt inclusion", } return translations[key] || key }, @@ -49,7 +48,6 @@ describe("McpToolRow", () => { name: "test-tool", description: "A test tool", alwaysAllow: false, - enabledForPrompt: true, } beforeEach(() => { @@ -143,146 +141,4 @@ describe("McpToolRow", () => { expect(screen.getByText("First parameter")).toBeInTheDocument() expect(screen.getByText("Second parameter")).toBeInTheDocument() }) - - it("shows toggle switch when serverName is provided and not in chat context", () => { - render() - - const toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" }) - expect(toggleSwitch).toBeInTheDocument() - }) - - it("hides toggle switch when isInChatContext is true", () => { - render() - - const toggleSwitch = screen.queryByRole("switch", { name: "Toggle prompt inclusion" }) - expect(toggleSwitch).not.toBeInTheDocument() - }) - - it("shows correct toggle switch state based on enabledForPrompt", () => { - // Test when enabled (should be checked) - const { rerender } = render() - - let toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" }) - expect(toggleSwitch).toHaveAttribute("aria-checked", "true") - - // Test when disabled (should not be checked) - const disabledTool = { ...mockTool, enabledForPrompt: false } - rerender() - - toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" }) - expect(toggleSwitch).toHaveAttribute("aria-checked", "false") - }) - - it("sends message to toggle enabledForPrompt when toggle switch is clicked", () => { - render() - - const toggleSwitch = screen.getByRole("switch", { name: "Toggle prompt inclusion" }) - fireEvent.click(toggleSwitch) - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: "toggleToolEnabledForPrompt", - serverName: "test-server", - source: "global", - toolName: "test-tool", - isEnabled: false, - }) - }) - - it("hides always allow checkbox when tool is disabled", () => { - const disabledTool = { ...mockTool, enabledForPrompt: false } - render() - - expect(screen.queryByText("Always allow")).not.toBeInTheDocument() - }) - - it("shows always allow checkbox when tool is enabled", () => { - const enabledTool = { ...mockTool, enabledForPrompt: true } - render() - - expect(screen.getByText("Always allow")).toBeInTheDocument() - }) - - it("hides parameters section when tool is disabled", () => { - const disabledToolWithSchema = { - ...mockTool, - enabledForPrompt: false, - inputSchema: { - type: "object", - properties: { - param1: { - type: "string", - description: "First parameter", - }, - }, - required: ["param1"], - }, - } - - render() - - expect(screen.queryByText("Parameters")).not.toBeInTheDocument() - expect(screen.queryByText("param1")).not.toBeInTheDocument() - expect(screen.queryByText("First parameter")).not.toBeInTheDocument() - }) - - it("shows parameters section when tool is enabled", () => { - const enabledToolWithSchema = { - ...mockTool, - enabledForPrompt: true, - inputSchema: { - type: "object", - properties: { - param1: { - type: "string", - description: "First parameter", - }, - }, - required: ["param1"], - }, - } - - render() - - expect(screen.getByText("Parameters")).toBeInTheDocument() - expect(screen.getByText("param1")).toBeInTheDocument() - expect(screen.getByText("First parameter")).toBeInTheDocument() - }) - - it("grays out tool name and description when tool is disabled", () => { - const disabledTool = { - ...mockTool, - enabledForPrompt: false, - description: "A disabled tool", - } - render() - - const toolName = screen.getByText("test-tool") - const toolDescription = screen.getByText("A disabled tool") - - // Check that the tool name has the grayed out classes - expect(toolName).toHaveClass("text-vscode-descriptionForeground", "opacity-60") - - // Check that the description has reduced opacity - expect(toolDescription).toHaveClass("opacity-40") - }) - - it("shows normal styling for tool name and description when tool is enabled", () => { - const enabledTool = { - ...mockTool, - enabledForPrompt: true, - description: "An enabled tool", - } - render() - - const toolName = screen.getByText("test-tool") - const toolDescription = screen.getByText("An enabled tool") - - // Check that the tool name has normal styling - expect(toolName).toHaveClass("text-vscode-foreground") - expect(toolName).not.toHaveClass("text-vscode-descriptionForeground", "opacity-60") - - // Check that the description has normal opacity - expect(toolDescription).toHaveClass("opacity-80") - expect(toolDescription).not.toHaveClass("opacity-40") - }) }) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 170d03b0e4..18f4bdf3d2 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -5,10 +5,9 @@ import { VSCodeRadio, VSCodeTextArea, VSCodeLink, - VSCodeTextField, } from "@vscode/webview-ui-toolkit/react" import { Trans } from "react-i18next" -import { ChevronDown, X, Upload, Download } from "lucide-react" +import { ChevronsUpDown, X } from "lucide-react" import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types" @@ -16,7 +15,6 @@ import { Mode, getRoleDefinition, getWhenToUse, - getDescription, getCustomInstructions, getAllModes, findModeBySlug as findCustomModeBySlug, @@ -45,9 +43,7 @@ import { CommandItem, CommandGroup, Input, - StandardTooltip, } from "@src/components/ui" -import { DeleteModeDialog } from "@src/components/modes/DeleteModeDialog" // Get all available groups that should show in prompts view const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable) @@ -93,27 +89,12 @@ const ModesView = ({ onDone }: ModesViewProps) => { const [showConfigMenu, setShowConfigMenu] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) const [isSystemPromptDisclosureOpen, setIsSystemPromptDisclosureOpen] = useState(false) - const [isExporting, setIsExporting] = useState(false) - const [isImporting, setIsImporting] = useState(false) - const [showImportDialog, setShowImportDialog] = useState(false) - const [hasRulesToExport, setHasRulesToExport] = useState>({}) - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) - const [modeToDelete, setModeToDelete] = useState<{ - slug: string - name: string - source?: string - rulesFolderPath?: string - } | null>(null) // State for mode selection popover and search const [open, setOpen] = useState(false) const [searchValue, setSearchValue] = useState("") const searchInputRef = useRef(null) - // Local state for mode name input to allow visual emptying - const [localModeName, setLocalModeName] = useState("") - const [currentEditingModeSlug, setCurrentEditingModeSlug] = useState(null) - // Direct update functions const updateAgentPrompt = useCallback( (mode: Mode, promptData: PromptComponent) => { @@ -124,9 +105,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { if (updatedPrompt.roleDefinition === getRoleDefinition(mode)) { delete updatedPrompt.roleDefinition } - if (updatedPrompt.description === getDescription(mode)) { - delete updatedPrompt.description - } if (updatedPrompt.whenToUse === getWhenToUse(mode)) { delete updatedPrompt.whenToUse } @@ -142,7 +120,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => { const source = modeConfig.source || "global" - vscode.postMessage({ type: "updateCustomMode", slug, @@ -206,30 +183,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { return customModes?.find(findMode) || modes.find(findMode) }, [visualMode, customModes, modes]) - // Check if the current mode has rules to export - const checkRulesDirectory = useCallback((slug: string) => { - vscode.postMessage({ - type: "checkRulesDirectory", - slug: slug, - }) - }, []) - - // Check rules directory when mode changes - useEffect(() => { - const currentMode = getCurrentMode() - if (currentMode?.slug && hasRulesToExport[currentMode.slug] === undefined) { - checkRulesDirectory(currentMode.slug) - } - }, [getCurrentMode, checkRulesDirectory, hasRulesToExport]) - - // Reset local name state when mode changes - useEffect(() => { - if (currentEditingModeSlug && currentEditingModeSlug !== visualMode) { - setCurrentEditingModeSlug(null) - setLocalModeName("") - } - }, [visualMode, currentEditingModeSlug]) - // Helper function to safely access mode properties const getModeProperty = ( mode: ModeConfig | undefined, @@ -241,7 +194,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { // State for create mode dialog const [newModeName, setNewModeName] = useState("") const [newModeSlug, setNewModeSlug] = useState("") - const [newModeDescription, setNewModeDescription] = useState("") const [newModeRoleDefinition, setNewModeRoleDefinition] = useState("") const [newModeWhenToUse, setNewModeWhenToUse] = useState("") const [newModeCustomInstructions, setNewModeCustomInstructions] = useState("") @@ -251,7 +203,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Field-specific error states const [nameError, setNameError] = useState("") const [slugError, setSlugError] = useState("") - const [descriptionError, setDescriptionError] = useState("") const [roleDefinitionError, setRoleDefinitionError] = useState("") const [groupsError, setGroupsError] = useState("") @@ -260,7 +211,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Reset form fields setNewModeName("") setNewModeSlug("") - setNewModeDescription("") setNewModeGroups(availableGroups) setNewModeRoleDefinition("") setNewModeWhenToUse("") @@ -269,7 +219,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Reset error states setNameError("") setSlugError("") - setDescriptionError("") setRoleDefinitionError("") setGroupsError("") }, []) @@ -303,7 +252,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { // Clear previous errors setNameError("") setSlugError("") - setDescriptionError("") setRoleDefinitionError("") setGroupsError("") @@ -311,7 +259,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { const newMode: ModeConfig = { slug: newModeSlug, name: newModeName, - description: newModeDescription.trim() || undefined, roleDefinition: newModeRoleDefinition.trim(), whenToUse: newModeWhenToUse.trim() || undefined, customInstructions: newModeCustomInstructions.trim() || undefined, @@ -335,9 +282,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { case "slug": setSlugError(message) break - case "description": - setDescriptionError(message) - break case "roleDefinition": setRoleDefinitionError(message) break @@ -357,7 +301,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { }, [ newModeName, newModeSlug, - newModeDescription, newModeRoleDefinition, newModeWhenToUse, // Add whenToUse dependency newModeCustomInstructions, @@ -405,7 +348,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { } if (customMode) { const source = customMode.source || "global" - updateCustomMode(customMode.slug, { ...customMode, groups: newGroups, @@ -428,14 +370,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { return () => document.removeEventListener("click", handleClickOutside) }, [showConfigMenu]) - // Use a ref to store the current modeToDelete value - const modeToDeleteRef = useRef(modeToDelete) - - // Update the ref whenever modeToDelete changes - useEffect(() => { - modeToDeleteRef.current = modeToDelete - }, [modeToDelete]) - useEffect(() => { const handler = (event: MessageEvent) => { const message = event.data @@ -445,50 +379,14 @@ const ModesView = ({ onDone }: ModesViewProps) => { setSelectedPromptTitle(`System Prompt (${message.mode} mode)`) setIsDialogOpen(true) } - } else if (message.type === "exportModeResult") { - setIsExporting(false) - - if (!message.success) { - // Show error message - console.error("Failed to export mode:", message.error) - } - } else if (message.type === "importModeResult") { - setIsImporting(false) - setShowImportDialog(false) - - if (!message.success) { - // Only log error if it's not a cancellation - if (message.error !== "cancelled") { - console.error("Failed to import mode:", message.error) - } - } - } else if (message.type === "checkRulesDirectoryResult") { - setHasRulesToExport((prev) => ({ - ...prev, - [message.slug]: message.hasContent, - })) - } else if (message.type === "deleteCustomModeCheck") { - // Handle the check response - // Use the ref to get the current modeToDelete value - const currentModeToDelete = modeToDeleteRef.current - if (message.slug && currentModeToDelete && currentModeToDelete.slug === message.slug) { - setModeToDelete({ - ...currentModeToDelete, - rulesFolderPath: message.rulesFolderPath, - }) - setShowDeleteConfirm(true) - } } } window.addEventListener("message", handler) return () => window.removeEventListener("message", handler) - }, []) // Empty dependency array - only register once + }, []) - const handleAgentReset = ( - modeSlug: string, - type: "roleDefinition" | "description" | "whenToUse" | "customInstructions", - ) => { + const handleAgentReset = (modeSlug: string, type: "roleDefinition" | "whenToUse" | "customInstructions") => { // Only reset for built-in modes const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent const updatedPrompt = { ...existingPrompt } @@ -513,29 +411,30 @@ const ModesView = ({ onDone }: ModesViewProps) => {
e.stopPropagation()} className="flex justify-between items-center mb-3">

{t("prompts:modes.title")}

- - - +
- - - + {showConfigMenu && (
e.stopPropagation()} @@ -573,23 +472,6 @@ const ModesView = ({ onDone }: ModesViewProps) => {
)}
- - -
@@ -611,10 +493,10 @@ const ModesView = ({ onDone }: ModesViewProps) => { variant="combobox" role="combobox" aria-expanded={open} - className="justify-between w-60" + className="grow justify-between" data-testid="mode-select-trigger">
{getCurrentMode()?.name || t("prompts:modes.selectMode")}
- + @@ -700,9 +582,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { {/* API Configuration - Moved Here */}
{t("prompts:apiConfiguration.title")}
-
- {t("prompts:apiConfiguration.select")} -
+
+ {t("prompts:apiConfiguration.select")} +
- {/* Name section */}
{/* Only show name and delete for custom modes */} {visualMode && findModeBySlug(visualMode, customModes) && ( @@ -737,84 +618,52 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{ + value={getModeProperty(findModeBySlug(visualMode, customModes), "name") ?? ""} + onChange={(e) => { const customMode = findModeBySlug(visualMode, customModes) if (customMode) { - setCurrentEditingModeSlug(visualMode) - setLocalModeName(customMode.name) - } - }} - onChange={(e) => { - setLocalModeName(e.target.value) - }} - onBlur={() => { - const customMode = findModeBySlug(visualMode, customModes) - if (customMode && localModeName.trim()) { - // Only update if the name is not empty updateCustomMode(visualMode, { ...customMode, - name: localModeName, + name: e.target.value, source: customMode.source || "global", }) } - // Clear the editing state - setCurrentEditingModeSlug(null) }} className="w-full" /> - - - +
)} - - {/* Role Definition section */}
{t("prompts:roleDefinition.title")}
{!findModeBySlug(visualMode, customModes) && ( - - - + )}
@@ -851,84 +700,29 @@ const ModesView = ({ onDone }: ModesViewProps) => { } }} className="w-full" - rows={5} + rows={4} data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`} />
- {/* Description section */} -
-
-
{t("prompts:description.title")}
- {!findModeBySlug(visualMode, customModes) && ( - - - - )} -
-
- {t("prompts:description.description")} -
- { - const customMode = findModeBySlug(visualMode, customModes) - const prompt = customModePrompts?.[visualMode] as PromptComponent - return customMode?.description ?? prompt?.description ?? getDescription(visualMode) - })()} - onChange={(e) => { - const value = - (e as unknown as CustomEvent)?.detail?.target?.value || - ((e as any).target as HTMLTextAreaElement).value - const customMode = findModeBySlug(visualMode, customModes) - if (customMode) { - // For custom modes, update the JSON file - updateCustomMode(visualMode, { - ...customMode, - description: value.trim() || undefined, - source: customMode.source || "global", - }) - } else { - // For built-in modes, update the prompts - updateAgentPrompt(visualMode, { - description: value.trim() || undefined, - }) - } - }} - className="w-full" - data-testid={`${getCurrentMode()?.slug || "code"}-description-textfield`} - /> -
- {/* When to Use section */}
{t("prompts:whenToUse.title")}
{!findModeBySlug(visualMode, customModes) && ( - - - + )}
@@ -961,7 +755,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { } }} className="w-full" - rows={4} + rows={3} data-testid={`${getCurrentMode()?.slug || "code"}-when-to-use-textarea`} />
@@ -973,20 +767,18 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{t("prompts:tools.title")}
{findModeBySlug(visualMode, customModes) && ( - setIsToolsEditMode(!isToolsEditMode)} + title={ isToolsEditMode ? t("prompts:tools.doneEditing") : t("prompts:tools.editTools") }> - - + + )}
{!findModeBySlug(visualMode, customModes) && ( @@ -1068,20 +860,19 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{t("prompts:customInstructions.title")}
{!findModeBySlug(visualMode, customModes) && ( - - - + )}
@@ -1121,7 +912,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { }) } }} - rows={10} + rows={4} className="w-full" data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`} /> @@ -1159,7 +950,7 @@ const ModesView = ({ onDone }: ModesViewProps) => {
-
+
- - - -
- - {/* Export/Import Mode Buttons */} -
- {/* Export button - visible when any mode is selected */} - {getCurrentMode() && ( - - )} - {/* Import button - always visible */}
- {/* Advanced Features Disclosure */} + {/* Custom System Prompt Disclosure */}
{isSystemPromptDisclosureOpen && ( -
- {/* Override System Prompt Section */} -
-

- Override System Prompt -

-
- { - const currentMode = getCurrentMode() - if (!currentMode) return +
+ { + const currentMode = getCurrentMode() + if (!currentMode) return - vscode.postMessage({ - type: "openFile", - text: `./.roo/system-prompt-${currentMode.slug}`, - values: { - create: true, - content: "", - }, - }) - }} - /> - ), - "1": ( - - ), - "2": , - }} - /> -
-
+ vscode.postMessage({ + type: "openFile", + text: `./.roo/system-prompt-${currentMode.slug}`, + values: { + create: true, + content: "", + }, + }) + }} + /> + ), + "1": ( + + ), + "2": , + }} + />
)}
@@ -1442,23 +1189,6 @@ const ModesView = ({ onDone }: ModesViewProps) => { )}
-
-
{t("prompts:createModeDialog.description.label")}
-
- {t("prompts:createModeDialog.description.description")} -
- { - setNewModeDescription((e.target as HTMLInputElement).value) - }} - className="w-full" - /> - {descriptionError && ( -
{descriptionError}
- )} -
-
{t("prompts:createModeDialog.whenToUse.label")}
@@ -1563,85 +1293,6 @@ const ModesView = ({ onDone }: ModesViewProps) => {
)} - - {/* Import Mode Dialog */} - {showImportDialog && ( -
-
-

{t("prompts:modes.importMode")}

-

- {t("prompts:importMode.selectLevel")} -

-
- - -
-
- - -
-
-
- )} - - {/* Delete Mode Confirmation Dialog */} - { - if (modeToDelete) { - vscode.postMessage({ - type: "deleteCustomMode", - slug: modeToDelete.slug, - }) - setShowDeleteConfirm(false) - setModeToDelete(null) - } - }} - /> ) } diff --git a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx index e202114bbb..47ff05613c 100644 --- a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx +++ b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/modes/__tests__/ModesView.spec.tsx -import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { render, screen, fireEvent, waitFor } from "@testing-library/react" import ModesView from "../ModesView" import { ExtensionStateContext } from "@src/context/ExtensionStateContext" import { vscode } from "@src/utils/vscode" @@ -138,13 +138,10 @@ describe("PromptsView", () => { await fireEvent.click(resetButton) // Verify it only resets role definition - // When resetting a built-in mode's role definition, the field should be removed entirely - // from the customPrompt object, not set to undefined. - // This allows the default role definition from the built-in mode to be used instead. expect(vscode.postMessage).toHaveBeenCalledWith({ type: "updatePrompt", promptMode: "code", - customPrompt: {}, // Empty object because the role definition field is removed entirely + customPrompt: { roleDefinition: undefined }, }) // Cleanup before testing custom mode @@ -162,46 +159,6 @@ describe("PromptsView", () => { expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument() }) - it("description section behavior for different mode types", async () => { - const customMode = { - slug: "custom-mode", - name: "Custom Mode", - roleDefinition: "Custom role", - description: "Custom description", - groups: [], - } - - // Test with built-in mode (code) - description section should be shown with reset button - const { unmount } = render( - - - , - ) - - // Verify description reset button IS present for built-in modes - // because built-in modes can have their descriptions customized and reset - expect(screen.queryByTestId("description-reset")).toBeInTheDocument() - - // Cleanup before testing custom mode - unmount() - - // Test with custom mode - description section should be shown - render( - - - , - ) - - // Verify description section is present for custom modes - // but reset button is NOT present (since custom modes manage their own descriptions) - expect(screen.queryByTestId("description-reset")).not.toBeInTheDocument() - - // Verify the description text field is present for custom modes - expect(screen.getByTestId("custom-mode-description-textfield")).toBeInTheDocument() - }) - it("handles clearing custom instructions correctly", async () => { const setCustomInstructions = vitest.fn() renderPromptsView({ diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index 01979060c3..f2e6ea6c7f 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -72,12 +72,7 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro {t("settings:footer.telemetry.label")}

- , - }} - /> + {t("settings:footer.telemetry.description")}

diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index e737678c01..d0a0a6aa44 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -1,12 +1,27 @@ import { memo, useEffect, useRef, useState } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { AlertTriangle } from "lucide-react" +import { ChevronsUpDown, Check, X, AlertTriangle } from "lucide-react" import type { ProviderSettingsEntry, OrganizationAllowList } from "@roo-code/types" import { useAppTranslation } from "@/i18n/TranslationContext" -import { Button, Input, Dialog, DialogContent, DialogTitle, StandardTooltip, SearchableSelect } from "@/components/ui" -import type { SearchableSelectOption } from "@/components/ui" +import { cn } from "@/lib/utils" +import { + Button, + Input, + Dialog, + DialogContent, + DialogTitle, + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui" interface ApiConfigManagerProps { currentApiConfigName?: string @@ -34,8 +49,12 @@ const ApiConfigManager = ({ const [inputValue, setInputValue] = useState("") const [newProfileName, setNewProfileName] = useState("") const [error, setError] = useState(null) + const [open, setOpen] = useState(false) + const [searchValue, setSearchValue] = useState("") const inputRef = useRef(null) const newProfileInputRef = useRef(null) + const searchInputRef = useRef(null) + const searchResetTimeoutRef = useRef(null) // Check if a profile is valid based on the organization allow list const isProfileValid = (profile: ProviderSettingsEntry): boolean => { @@ -108,10 +127,42 @@ const ApiConfigManager = ({ useEffect(() => { resetCreateState() resetRenameState() + // Reset search value when current profile changes + const timeoutId = setTimeout(() => setSearchValue(""), 100) + return () => clearTimeout(timeoutId) }, [currentApiConfigName]) + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (searchResetTimeoutRef.current) { + clearTimeout(searchResetTimeoutRef.current) + } + } + }, []) + + const onOpenChange = (open: boolean) => { + setOpen(open) + + // Reset search when closing the popover + if (!open) { + // Clear any existing timeout + if (searchResetTimeoutRef.current) { + clearTimeout(searchResetTimeoutRef.current) + } + searchResetTimeoutRef.current = setTimeout(() => setSearchValue(""), 100) + } + } + + const onClearSearch = () => { + setSearchValue("") + searchInputRef.current?.focus() + } + const handleSelectConfig = (configName: string) => { if (!configName) return + + setOpen(false) onSelectConfig(configName) } @@ -197,25 +248,23 @@ const ApiConfigManager = ({ }} className="grow" /> - - - - - - + +
{error && (
@@ -226,61 +275,122 @@ const ApiConfigManager = ({ ) : ( <>
- { - const valid = isProfileValid(config) - return { - value: config.name, - label: config.name, - disabled: !valid, - icon: !valid ? ( - - - - - - ) : undefined, - } as SearchableSelectOption - })} - placeholder={t("settings:common.select")} - searchPlaceholder={t("settings:providers.searchPlaceholder")} - emptyMessage={t("settings:providers.noMatchFound")} - className="grow" - data-testid="select-component" - /> - - - + + + + + + +
+ + {searchValue.length > 0 && ( +
+ +
+ )} +
+ + + {searchValue && ( +
+ {t("settings:providers.noMatchFound")} +
+ )} +
+ + {listApiConfigMeta + .filter((config) => + searchValue + ? config.name.toLowerCase().includes(searchValue.toLowerCase()) + : true, + ) + .map((config) => { + const valid = isProfileValid(config) + return ( + +
+ {!valid && ( + + + + )} + {config.name} +
+ +
+ ) + })} +
+
+
+
+
+ {currentApiConfigName && ( <> - - - - + + + - + } + data-testid="delete-profile-button" + disabled={isOnlyProfile}> + + )}
@@ -325,7 +435,7 @@ const ApiConfigManager = ({ }} /> {error && ( -

+

{error}

)} diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 38d2ceebd3..76aefae9e6 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -15,7 +15,6 @@ import { litellmDefaultModelId, openAiNativeDefaultModelId, anthropicDefaultModelId, - claudeCodeDefaultModelId, geminiDefaultModelId, deepSeekDefaultModelId, moonshotDefaultModelId, @@ -54,7 +53,6 @@ import { Anthropic, Bedrock, Chutes, - ClaudeCode, DeepSeek, Gemini, Glama, @@ -286,7 +284,6 @@ const ApiOptions = ({ requesty: { field: "requestyModelId", default: requestyDefaultModelId }, litellm: { field: "litellmModelId", default: litellmDefaultModelId }, anthropic: { field: "apiModelId", default: anthropicDefaultModelId }, - "claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId }, "openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId }, gemini: { field: "apiModelId", default: geminiDefaultModelId }, deepseek: { field: "apiModelId", default: deepSeekDefaultModelId }, @@ -423,10 +420,6 @@ const ApiOptions = ({ )} - {selectedProvider === "claude-code" && ( - - )} - {selectedProvider === "openai-native" && ( )} diff --git a/webview-ui/src/components/settings/AutoApproveToggle.tsx b/webview-ui/src/components/settings/AutoApproveToggle.tsx index e8b51b01ef..9a53cc8ac6 100644 --- a/webview-ui/src/components/settings/AutoApproveToggle.tsx +++ b/webview-ui/src/components/settings/AutoApproveToggle.tsx @@ -2,7 +2,7 @@ import type { GlobalSettings } from "@roo-code/types" import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@/lib/utils" -import { Button, StandardTooltip } from "@/components/ui" +import { Button } from "@/components/ui" type AutoApproveToggles = Pick< GlobalSettings, @@ -117,20 +117,20 @@ export const AutoApproveToggle = ({ onToggle, ...props }: AutoApproveToggleProps "[@media(min-width:1200px)]:max-w-[1800px]", )}> {Object.values(autoApproveSettingsConfig).map(({ key, descriptionKey, labelKey, icon, testId }) => ( - - - + ))}
) diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index a71132d62b..c891444a62 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -6,15 +6,7 @@ import { supportPrompt, SupportPromptType } from "@roo/support-prompt" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { - Button, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - StandardTooltip, -} from "@src/components/ui" +import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { MessageSquare } from "lucide-react" @@ -132,14 +124,15 @@ const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts }: Prom
- handleSupportReset(activeSupportOption)} + title={t("prompts:supportPrompts.resetPrompt", { promptType: activeSupportOption, })}> - - + +
(({ onDone, t

{t("settings:header.title")}

- - - - - - + } + onClick={handleSubmit} + disabled={!isChangeDetected || !isSettingValid} + data-testid="save-button"> + {t("settings:common.save")} + +
@@ -531,7 +529,7 @@ const SettingsView = forwardRef(({ onDone, t if (isCompactMode) { // Wrap in Tooltip and manually add onClick to the trigger return ( - + {/* Clone to avoid ref issues if triggerComponent itself had a key */} diff --git a/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx index 152d853444..3d791d9868 100644 --- a/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ApiConfigManager.spec.tsx -import { render, screen, fireEvent, within } from "@/utils/test-utils" +import { render, screen, fireEvent, within } from "@testing-library/react" import ApiConfigManager from "../ApiConfigManager" @@ -41,7 +41,6 @@ vitest.mock("@/components/ui", () => ({ data-testid={dataTestId} /> ), - StandardTooltip: ({ children, content }: any) =>
{children}
, // New components for searchable dropdown Popover: ({ children, open }: any) => (
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index 7b7f9b33e4..ba5c637422 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ApiOptions.spec.tsx -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { type ModelInfo, type ProviderSettings, openAiModelInfoSaneDefaults } from "@roo-code/types" @@ -71,7 +71,6 @@ vi.mock("@/components/ui", () => ({ {children} ), - StandardTooltip: ({ children, content }: any) =>
{children}
, // Add missing components used by ModelPicker Command: ({ children }: any) =>
{children}
, CommandEmpty: ({ children }: any) =>
{children}
, diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx index 270ed305ea..503f08d5c3 100644 --- a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent } from "@testing-library/react" import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 61444267f2..9a7d451d1d 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -1,320 +1,123 @@ -// npx vitest src/components/settings/__tests__/ContextManagementSettings.spec.tsx +import { render, screen, fireEvent } from "@testing-library/react" -import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" -import { ContextManagementSettings } from "../ContextManagementSettings" +import { ContextManagementSettings } from "@src/components/settings/ContextManagementSettings" -// Mock the translation hook -vi.mock("@/hooks/useAppTranslation", () => ({ +// Mock translation hook to return the key as the translation +vitest.mock("@/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ - t: (key: string) => { - // Return specific translations for our test cases - if (key === "settings:contextManagement.diagnostics.maxMessages.unlimitedLabel") { - return "Unlimited" - } - return key - }, + t: (key: string) => key, }), })) -// Mock the UI components -vi.mock("@/components/ui", () => ({ - ...vi.importActual("@/components/ui"), - Slider: ({ value, onValueChange, "data-testid": dataTestId, disabled }: any) => ( - onValueChange([parseFloat(e.target.value)])} - onKeyDown={(e) => { - const currentValue = value?.[0] ?? 0 - if (e.key === "ArrowRight") { - onValueChange([currentValue + 1]) - } else if (e.key === "ArrowLeft") { - onValueChange([currentValue - 1]) - } - }} - data-testid={dataTestId} - disabled={disabled} - role="slider" - /> - ), - Input: ({ value, onChange, "data-testid": dataTestId, ...props }: any) => ( - - ), - Button: ({ children, onClick, ...props }: any) => ( - - ), - Select: ({ children, ...props }: any) => ( -
- {children} -
- ), - SelectTrigger: ({ children, ...props }: any) =>
{children}
, - SelectValue: ({ children, ...props }: any) =>
{children}
, - SelectContent: ({ children, ...props }: any) =>
{children}
, - SelectItem: ({ children, ...props }: any) =>
{children}
, -})) - // Mock vscode utilities - this is necessary since we're not in a VSCode environment +import { vscode } from "@/utils/vscode" -vi.mock("@/utils/vscode", () => ({ +vitest.mock("@/utils/vscode", () => ({ vscode: { - postMessage: vi.fn(), + postMessage: vitest.fn(), }, })) // Mock VSCode components to behave like standard HTML elements -vi.mock("@vscode/webview-ui-toolkit/react", () => ({ +vitest.mock("@vscode/webview-ui-toolkit/react", () => ({ VSCodeCheckbox: ({ checked, onChange, children, "data-testid": dataTestId, ...props }: any) => ( -