diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5bfc08f80f..44626273b5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -53,6 +53,16 @@ body: validations: required: true + - type: textarea + id: roo-code-tasks + attributes: + label: Roo Code Task Links (Optional) + description: | + If you have any publicly shared task links that demonstrate the issue, please paste them here. + This helps maintainers understand the context. + Example: https://app.roocode.com/share/task-id + placeholder: Paste your Roo Code share links here, one per line + - type: textarea id: steps attributes: @@ -85,4 +95,4 @@ body: attributes: label: 📄 Relevant Logs or Errors (Optional) description: Paste API logs, terminal output, or errors here. Use triple backticks (```) for code formatting. - render: shell \ No newline at end of file + render: shell diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 85263d8813..4863f9ffa6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -57,6 +57,16 @@ body: label: Additional context (optional) description: Mockups, screenshots, links, user quotes, or other relevant information that supports your proposal. + - type: textarea + id: roo-code-tasks + attributes: + label: Roo Code Task Links (Optional) + description: | + If you used Roo Code to explore this feature request or develop solutions, share the public task links here. + This helps maintainers understand the context and any exploration you've done. + Example: https://app.roocode.com/share/task-id + placeholder: Paste your Roo Code share links here, one per line + - type: checkboxes id: checklist attributes: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 765c70614c..e83e44cd66 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,6 +12,14 @@ Before submitting your PR, please ensure: Closes: # +### Roo Code Task Context (Optional) + + + ### Description + + gh api repos/[owner]/[repo] --jq '.default_branch' + + + + + gh api repos/[owner]/[repo]/contents/README.md --jq '.content' | base64 -d + + + + + gh api repos/[owner]/[repo]/actions/runs --jq '.workflow_runs[0:5] | .[] | .id, .status, .conclusion' + + + + + + Check GitHub Actions workflow status + Use to monitor CI/CD pipeline + gh run list --repo [owner]/[repo] --limit 5 + + + gh run list --repo octocat/hello-world --limit 5 + + + + + + + + gh: Not authenticated. Run 'gh auth login' to authenticate. + + Ask user to authenticate: + + GitHub CLI is not authenticated. Please run 'gh auth login' in your terminal to authenticate, then let me know when you're ready to continue. + + I've authenticated, please continue + I need help with authentication + Let's use a different approach + + + + + + + HTTP 403: Resource not accessible by integration + + Check if working from a fork is needed: + + gh repo fork [owner]/[repo] --clone + + + + + \ No newline at end of file diff --git a/.roo/rules-issue-fixer-orchestrator/5_pull_request_workflow.xml b/.roo/rules-issue-fixer-orchestrator/5_pull_request_workflow.xml new file mode 100644 index 0000000000..041fa0347c --- /dev/null +++ b/.roo/rules-issue-fixer-orchestrator/5_pull_request_workflow.xml @@ -0,0 +1,106 @@ + + + 1. Ensure all changes are committed with proper message format + 2. Push to appropriate branch (fork or direct) + 3. Prepare comprehensive PR description + 4. Get user approval before creating PR + 5. Extract owner and repo from the provided GitHub URL + + + + - Bug fixes: "fix: [description] (#[issue-number])" + - Features: "feat: [description] (#[issue-number])" + - Follow conventional commit format + + + A comprehensive PR description is critical. The subtask responsible for preparing the PR content should generate a body that includes the following markdown structure: + + ```markdown + ## Description + + Fixes #[issue number] + + [Detailed description of what was changed and why] + + ## Changes Made + + - [Specific change 1 with file references] + - [Specific change 2 with technical details] + - [Any refactoring or cleanup done] + + ## Testing + + - [x] All existing tests pass + - [x] Added tests for [specific functionality] + - [x] Manual testing completed: + - [Specific manual test 1] + - [Specific manual test 2] + + ## Translations + + [If translations were added/updated] + - [x] All user-facing strings have been translated + - [x] Updated language files: [list of languages] + - [x] Translations reviewed for consistency + + [If no translations needed] + - No user-facing string changes in this PR + + ## Verification of Acceptance Criteria + + [For each criterion from the issue, show it's met] + - [x] Criterion 1: [How it's verified] + - [x] Criterion 2: [How it's verified] + + ## Checklist + + - [x] Code follows project style guidelines + - [x] Self-review completed + - [x] Comments added for complex logic + - [x] Documentation updated (if needed) + - [x] No breaking changes (or documented if any) + - [x] Accessibility checked (for UI changes) + - [x] Translations added/updated (for UI changes) + + ## Screenshots/Demo (if applicable) + + [Add before/after screenshots for UI changes] + [Add terminal output for CLI changes] + ``` + + + + Use a consistent format for branch names. + + - Bug fixes: `fix/issue-[number]-[brief-description]` + - Features: `feat/issue-[number]-[brief-description]` + + + + + Use GitHub CLI to create the pull request: + + gh pr create --repo [owner]/[repo] --base main --title "[title]" --body "[description]" --maintainer-can-modify + + + If working from a fork, ensure you've forked first: + + gh repo fork [owner]/[repo] --clone + + + The gh CLI automatically handles fork workflows. + + + + 1. Comment on original issue with PR link: + + gh issue comment [issue-number] --repo [owner]/[repo] --body "PR #[pr-number] has been created to address this issue: [PR URL]" + + 2. Inform user of successful creation + 3. Provide next steps and tracking info + 4. Monitor PR checks: + + gh pr checks [pr-number] --repo [owner]/[repo] --watch + + + \ No newline at end of file diff --git a/.roo/rules-issue-fixer-orchestrator/6_testing_guidelines.xml b/.roo/rules-issue-fixer-orchestrator/6_testing_guidelines.xml new file mode 100644 index 0000000000..721a89f2b9 --- /dev/null +++ b/.roo/rules-issue-fixer-orchestrator/6_testing_guidelines.xml @@ -0,0 +1,10 @@ + + - Always run existing tests before making changes (baseline) + - Add tests for any new functionality + - Add regression tests for bug fixes + - Test edge cases and error conditions + - Run the full test suite before completing + - For UI changes, test in multiple themes + - Verify accessibility (keyboard navigation, screen readers) + - Test performance impact for large operations + \ No newline at end of file diff --git a/.roo/rules-issue-fixer-orchestrator/7_communication_style.xml b/.roo/rules-issue-fixer-orchestrator/7_communication_style.xml new file mode 100644 index 0000000000..b956a9375b --- /dev/null +++ b/.roo/rules-issue-fixer-orchestrator/7_communication_style.xml @@ -0,0 +1,11 @@ + + - Be clear about what you're doing at each step + - Explain technical decisions and trade-offs + - Ask for clarification if requirements are ambiguous + - Provide regular progress updates for complex issues + - Summarize changes clearly for non-technical stakeholders + - Use issue numbers and links for reference + - Inform the user when delegating to translate mode + - Include translation status in progress updates + - Mention in PR description if translations were added + \ No newline at end of file diff --git a/.roo/rules-issue-fixer-orchestrator/8_github_communication_guidelines.xml b/.roo/rules-issue-fixer-orchestrator/8_github_communication_guidelines.xml new file mode 100644 index 0000000000..627908f1f7 --- /dev/null +++ b/.roo/rules-issue-fixer-orchestrator/8_github_communication_guidelines.xml @@ -0,0 +1,16 @@ + + + - Provide brief status updates when working on complex issues + - Ask specific questions if requirements are unclear + - Share findings when investigation reveals important context + - Keep progress updates factual and concise + - Example: "Found the root cause in the theme detection logic. Working on a fix that preserves backward compatibility." + + + + - Follow conventional commit format: "type: description (#issue-number)" + - Keep first line under 72 characters + - Be specific about what changed + - Example: "fix: resolve button visibility in dark theme (#123)" + + \ No newline at end of file diff --git a/.roo/rules-issue-fixer-orchestrator/9_translation_handling.xml b/.roo/rules-issue-fixer-orchestrator/9_translation_handling.xml new file mode 100644 index 0000000000..15d196263c --- /dev/null +++ b/.roo/rules-issue-fixer-orchestrator/9_translation_handling.xml @@ -0,0 +1,125 @@ + + + The issue-fixer-orchestrator mode must ensure all user-facing content is properly translated before creating a pull request. This is achieved by delegating translation tasks to the specialized translate mode. + + + + + Any changes to React/Vue/Angular components + + - webview-ui/src/**/*.tsx + - webview-ui/src/**/*.jsx + - src/**/*.tsx (if contains UI elements) + + + - New text strings in JSX + - Updated button labels, tooltips, or placeholders + - Error messages displayed to users + - Any hardcoded strings that should use i18n + + + + + User-facing documentation changes + + - README.md + - docs/**/*.md + - webview-ui/src/components/chat/Announcement.tsx + - Any markdown files visible to end users + + + + + Direct changes to translation files + + - src/i18n/locales/**/*.json + - webview-ui/src/i18n/locales/**/*.json + + When English (en) locale is updated, all other locales must be synchronized + + + + New or modified error messages + + - API error responses + - Validation messages + - System notifications + - Status messages + + + + + + + Detect Translation Needs + + - Read the modified_files.json from the implementation step + - Check each file against the patterns above + - Determine if any user-facing content was changed + + + + + Prepare Translation Context + + - Gather all context files (issue details, implementation plan, modified files) + - Identify specific strings or content that need translation + - Note any special terminology or context from the issue + + + + + Delegate to Translate Mode + + - Use new_task to create a translation subtask + - Provide clear instructions about what needs translation + - Include paths to all context files + - Specify expected output (translation_summary.md) + + + + + Verify Translation Completion + + - Wait for the translate mode subtask to complete + - Read the translation_summary.md file + - Confirm all necessary translations were handled + - Only proceed to PR creation after confirmation + + + + + + Template for creating translation subtasks + + - Clear identification of the issue being fixed + - List of modified files requiring translation review + - Path to context files for understanding the changes + - Specific instructions for what to translate + - Expected output format and location + + + + + Always check for translations AFTER verification passes + Don't skip translation even for "minor" UI changes + Ensure the translate mode has access to full context + Wait for translation completion before creating PR + Include translation changes in the PR description + + + + + Assuming no translations needed without checking + Always analyze modified files for user-facing content + + + Proceeding to PR creation before translations complete + Wait for translation_summary.md confirmation + + + Not providing enough context to translate mode + Include issue details and implementation plan + + + \ No newline at end of file diff --git a/.roo/rules-issue-fixer/1_Workflow.xml b/.roo/rules-issue-fixer/1_Workflow.xml index 40971a1064..db1e968c67 100644 --- a/.roo/rules-issue-fixer/1_Workflow.xml +++ b/.roo/rules-issue-fixer/1_Workflow.xml @@ -1,62 +1,41 @@ - Determine Workflow Type and Retrieve Context + Retrieve Issue Context - First, determine what type of work is needed. The user will provide either: - - An issue number/URL (e.g., "#123" or GitHub issue URL) - for new implementation - - A PR number/URL (e.g., "#456" or GitHub PR URL) - for addressing review feedback - - A description of changes needed for an existing PR - - For Issue-based workflow: - Extract the issue number and retrieve it: - - - github - get_issue - - { - "owner": "RooCodeInc", - "repo": "Roo-Code", - "issue_number": [extracted number] - } - - - - For PR Review workflow: - Extract the PR number and retrieve it: - - - github - get_pull_request - - { - "owner": "RooCodeInc", - "repo": "Roo-Code", - "pull_number": [extracted number] - } - - - - Then get PR review comments: - - - github - get_pull_request_reviews - - { - "owner": "RooCodeInc", - "repo": "Roo-Code", - "pull_number": [extracted number] - } - - - - Analyze the context to determine: - 1. Type of work (new issue implementation vs PR feedback) - 2. All requirements and acceptance criteria - 3. Specific changes requested (for PR reviews) - 4. Technical details mentioned - 5. Any linked issues or discussions + The user should provide a full GitHub issue URL (e.g., "https://github.com/owner/repo/issues/123") for implementation. + + Parse the URL to extract: + - Owner (organization or username) + - Repository name + - Issue number + + For example, from https://github.com/RooCodeInc/Roo-Code/issues/123: + - Owner: RooCodeInc + - Repo: Roo-Code + - Issue: 123 + + Then retrieve the issue: + + + gh issue view [issue-number] --repo [owner]/[repo] --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author + + + If the command fails with an authentication error (e.g., "gh: Not authenticated" or "HTTP 401"), ask the user to authenticate: + + GitHub CLI is not authenticated. Please run 'gh auth login' in your terminal to authenticate, then let me know when you're ready to continue. + + I've authenticated, please continue + I need help with authentication + Let's use a different approach + + + + Analyze the issue to determine: + 1. All requirements and acceptance criteria + 2. Technical details mentioned + 3. Any linked issues or discussions + + Note: For PR review feedback, users should use the dedicated pr-fixer mode instead. @@ -69,23 +48,20 @@ - Community suggestions - Any decisions or changes to requirements - - github - get_issue_comments - - { - "owner": "RooCodeInc", - "repo": "Roo-Code", - "issue_number": [issue number] - } - - + + gh issue view [issue number] --repo [owner]/[repo] --comments + Also check for: 1. Related issues mentioned in the body or comments 2. Linked pull requests 3. Referenced discussions + If related PRs are mentioned, view them: + + gh pr view [pr-number] --repo [owner]/[repo] + + Document all requirements and constraints found. @@ -109,16 +85,9 @@ - Identify patterns to follow - Find related components and utilities - For PR Reviews: - - Search for files mentioned in review comments - - Find related files that use similar patterns - - Locate test files for modified functionality - - Identify files that import/depend on changed code - Example searches based on issue type: - Bug: Search for error messages, function names, component names - Feature: Search for similar functionality, API endpoints, UI components - - PR Review: Search for patterns mentioned in feedback CRITICAL: Always read multiple related files together to understand: - Current code patterns and conventions @@ -133,10 +102,15 @@ - read_file to examine specific implementations (read multiple files at once) - search_files for specific patterns or error messages - Also use GitHub tools: - - list_commits to see recent changes to affected files - - get_commit to understand specific changes - - list_pull_requests to find related PRs + Also use GitHub CLI to check recent changes: + + gh api repos/[owner]/[repo]/commits?path=[file-path]&per_page=10 --jq '.[].sha + " " + .[].commit.message' + + + Search for related PRs: + + gh pr list --repo [owner]/[repo] --search "[relevant search terms]" --limit 10 + Document: - All files that need modification @@ -252,11 +226,63 @@ - [ ] No linting errors If any criteria fail, return to implementation step. - - - - - Run Tests and Checks + + + + + Check for Translation Requirements + + After implementing changes, analyze if any translations are required: + + Translation is needed if the implementation includes: + 1. New user-facing text strings in UI components + 2. New error messages or user notifications + 3. Updated documentation files that need localization + 4. New command descriptions or tooltips + 5. Changes to announcement files or release notes + 6. New configuration options with user-visible descriptions + + Check for these patterns: + - Hard-coded strings in React components (.tsx/.jsx files) + - New entries needed in i18n JSON files + - Updated markdown documentation files + - New VSCode command contributions + - Changes to user-facing configuration schemas + + If translations are required: + + + translate + Translation needed for issue #[issue-number] implementation. + + The following changes require translation into all supported languages: + + **Files with new/updated user-facing content:** + - [List specific files and what content needs translation] + - [Include context about where the strings appear] + - [Note any special formatting or constraints] + + **Translation scope:** + - [Specify if it's new strings, updated strings, or both] + - [List specific JSON keys that need attention] + - [Note any markdown files that need localization] + + **Context for translators:** + - [Explain the feature/fix being implemented] + - [Provide context about how the text is used] + - [Note any technical terms or constraints] + + Please ensure all translations maintain consistency with existing terminology and follow the project's localization guidelines. + + + Wait for the translation task to complete before proceeding to testing. + + If no translations are required, continue to the next step. + + + + + Run Tests and Checks Run comprehensive tests to ensure quality: @@ -289,7 +315,7 @@ - + Prepare Summary Create a comprehensive summary of the implementation: @@ -341,7 +367,7 @@ - + Prepare for Pull Request If user wants to create a pull request, prepare everything needed: @@ -412,13 +438,13 @@ **Branch:** [branch-name] **Title:** [PR title] - **Target:** RooCodeInc/Roo-Code (main branch) + **Target:** [owner]/[repo] (main branch) Here's the PR description: [Show prepared PR description] - Would you like me to create this pull request to RooCodeInc/Roo-Code? + Would you like me to create this pull request to [owner]/[repo]? Yes, create the pull request Let me review the PR description first @@ -429,49 +455,31 @@ - + Create Pull Request - Once user approves, create the pull request using GitHub MCP: - - - github - create_pull_request - - { - "owner": "RooCodeInc", - "repo": "Roo-Code", - "title": "[Type]: [Brief description] (#[issue-number])", - "head": "[user-fork-owner]:[branch-name]", - "base": "main", - "body": "[Complete PR description from step 9]", - "draft": false, - "maintainer_can_modify": true - } - - - - Note: The "head" parameter format depends on where the branch exists: - - If user has push access: "branch-name" - - If working from a fork: "username:branch-name" + Once user approves, create the pull request using GitHub CLI: + + If the user doesn't have push access to [owner]/[repo], fork the repository: + + gh repo fork [owner]/[repo] --clone + + + Create the pull request: + + gh pr create --repo [owner]/[repo] --base main --title "[Type]: [Brief description] (#[issue-number])" --body "[Complete PR description from step 10]" --maintainer-can-modify + + + The gh CLI will automatically handle the fork workflow if needed. After PR creation: - 1. Capture the PR number and URL from the response + 1. Capture the PR number and URL from the command output 2. Link the PR to the issue by commenting on the issue 3. Inform the user of the successful creation - - github - add_issue_comment - - { - "owner": "RooCodeInc", - "repo": "Roo-Code", - "issue_number": [original issue number], - "body": "PR #[new PR number] has been created to address this issue: [PR URL]" - } - - + + gh issue comment [original issue number] --repo [owner]/[repo] --body "PR #[new PR number] has been created to address this issue: [PR URL]" + Final message to user: ``` @@ -492,13 +500,13 @@ - + Monitor PR Checks After the PR is created, monitor the CI/CD checks to ensure they pass: - gh pr checks --watch + gh pr checks [PR number] --repo [owner]/[repo] --watch This command will: diff --git a/.roo/rules-issue-fixer/2_best_practices.xml b/.roo/rules-issue-fixer/2_best_practices.xml index 7d3a87aa9a..dede40a92f 100644 --- a/.roo/rules-issue-fixer/2_best_practices.xml +++ b/.roo/rules-issue-fixer/2_best_practices.xml @@ -12,4 +12,7 @@ - Update documentation when needed - Add tests for any new functionality - Check for accessibility issues (for UI changes) + - Delegate translation tasks to translate mode when implementing user-facing changes + - Always check for hard-coded strings and internationalization needs + - Wait for translation completion before proceeding to final testing \ No newline at end of file diff --git a/.roo/rules-issue-fixer/4_github_cli_usage.xml b/.roo/rules-issue-fixer/4_github_cli_usage.xml new file mode 100644 index 0000000000..e12fb06a5b --- /dev/null +++ b/.roo/rules-issue-fixer/4_github_cli_usage.xml @@ -0,0 +1,221 @@ + + + This mode uses the GitHub CLI (gh) for all GitHub operations. + The mode assumes the user has gh installed and authenticated. If authentication errors occur, + the mode will prompt the user to authenticate. + + Users must provide full GitHub issue URLs (e.g., https://github.com/owner/repo/issues/123) + so the mode can extract the repository information dynamically. + + + + https://github.com/[owner]/[repo]/issues/[number] + + - Owner: The organization or username + - Repo: The repository name + - Number: The issue number + + + + + Assume authenticated, handle errors gracefully + Only check authentication if a gh command fails with auth error + + - "gh: Not authenticated" + - "HTTP 401" + - "HTTP 403: Resource not accessible" + + + + + + Retrieve the issue details at the start + Always use first to get the full issue content + gh issue view [issue-number] --repo [owner]/[repo] --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author + + + gh issue view 123 --repo octocat/hello-world --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author + + + + + + Get additional context and requirements from issue comments + Always use after viewing issue to see full discussion + gh issue view [issue-number] --repo [owner]/[repo] --comments + + + gh issue view 123 --repo octocat/hello-world --comments + + + + + + + Find recent changes to affected files + Use during codebase exploration + gh api repos/[owner]/[repo]/commits?path=[file-path]&per_page=10 + + + gh api repos/octocat/hello-world/commits?path=src/api/index.ts&per_page=10 --jq '.[].sha + " " + .[].commit.message' + + + + + + Search for code patterns on GitHub + Use to supplement local codebase_search + gh search code "[search-query]" --repo [owner]/[repo] + + + gh search code "function handleError" --repo octocat/hello-world --limit 10 + + + + + + + + Add progress updates or ask questions on issues + Use if clarification needed or to show progress + gh issue comment [issue-number] --repo [owner]/[repo] --body "[comment]" + + + gh issue comment 123 --repo octocat/hello-world --body "Working on this issue. Found the root cause in the theme detection logic." + + + + + + Find related or similar PRs + Use to understand similar changes + gh pr list --repo [owner]/[repo] --search "[search-terms]" + + + gh pr list --repo octocat/hello-world --search "dark theme" --limit 10 + + + + + + View the diff of a pull request + Use to understand changes in a PR + gh pr diff [pr-number] --repo [owner]/[repo] + + + gh pr diff 456 --repo octocat/hello-world + + + + + + + + Create a pull request + Use in step 11 after user approval + + - Target the repository from the provided URL + - Use "main" as the base branch unless specified otherwise + - Include issue number in PR title + - Use --maintainer-can-modify flag + + gh pr create --repo [owner]/[repo] --base main --title "[title]" --body "[body]" --maintainer-can-modify + + + gh pr create --repo octocat/hello-world --base main --title "fix: Resolve dark theme button visibility (#123)" --body "## Description + +Fixes #123 + +[Full PR description]" --maintainer-can-modify + + + + If working from a fork, ensure the fork is set as the remote and push the branch there first. + The gh CLI will automatically handle the fork workflow. + + + + + Fork the repository if user doesn't have push access + Use if user needs to work from a fork + gh repo fork [owner]/[repo] --clone + + + gh repo fork octocat/hello-world --clone + + + + + + Monitor CI/CD checks on a pull request + Use after creating PR to ensure checks pass + gh pr checks [pr-number] --repo [owner]/[repo] --watch + + + gh pr checks 789 --repo octocat/hello-world --watch + + + + + + + + Access GitHub API directly for advanced operations + Use when specific gh commands don't provide needed functionality + + + + gh api repos/[owner]/[repo] --jq '.default_branch' + + + + + gh api repos/[owner]/[repo]/contents/README.md --jq '.content' | base64 -d + + + + + gh api repos/[owner]/[repo]/actions/runs --jq '.workflow_runs[0:5] | .[] | .id, .status, .conclusion' + + + + + + Check GitHub Actions workflow status + Use to monitor CI/CD pipeline + gh run list --repo [owner]/[repo] --limit 5 + + + gh run list --repo octocat/hello-world --limit 5 + + + + + + + + gh: Not authenticated. Run 'gh auth login' to authenticate. + + Ask user to authenticate: + + GitHub CLI is not authenticated. Please run 'gh auth login' in your terminal to authenticate, then let me know when you're ready to continue. + + I've authenticated, please continue + I need help with authentication + Let's use a different approach + + + + + + + HTTP 403: Resource not accessible by integration + + Check if working from a fork is needed: + + gh repo fork [owner]/[repo] --clone + + + + + \ No newline at end of file diff --git a/.roo/rules-issue-fixer/4_github_mcp_tool_usage.xml b/.roo/rules-issue-fixer/4_github_mcp_tool_usage.xml deleted file mode 100644 index 49b12f96ca..0000000000 --- a/.roo/rules-issue-fixer/4_github_mcp_tool_usage.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - Retrieve the issue details at the start - Always use first to get the full issue content - - - - Get additional context and requirements - Always use after get_issue to see full discussion - - - - Find recent changes to affected files - Use during codebase exploration - - - - Find code patterns on GitHub - Use to supplement local codebase_search - - - - - - Add progress updates or ask questions - Use if clarification needed or to show progress - - - - Find related or similar PRs - Use to understand similar changes - - - - Get details of related PRs - Use when issue references specific PRs - - - - - - Create a pull request to RooCodeInc/Roo-Code - Use in step 10 after user approval - - - Always target RooCodeInc/Roo-Code repository - - Use "main" as the base branch unless specified otherwise - - Include issue number in PR title - - Set maintainer_can_modify to true - - - - github - create_pull_request - - { - "owner": "RooCodeInc", - "repo": "Roo-Code", - "title": "fix: Resolve dark theme button visibility (#123)", - "head": "username:fix/issue-123-dark-theme-button", - "base": "main", - "body": "## Description\n\nFixes #123\n\n[Full PR description]", - "draft": false, - "maintainer_can_modify": true - } - - - - - - - Fork the repository if user doesn't have push access - Use if user needs to work from a fork - - - github - fork_repository - - { - "owner": "RooCodeInc", - "repo": "Roo-Code" - } - - - - - - \ No newline at end of file diff --git a/.roo/rules-issue-fixer/5_pull_request_workflow.xml b/.roo/rules-issue-fixer/5_pull_request_workflow.xml index 79102fcf37..60a8385e3e 100644 --- a/.roo/rules-issue-fixer/5_pull_request_workflow.xml +++ b/.roo/rules-issue-fixer/5_pull_request_workflow.xml @@ -4,6 +4,7 @@ 2. Push to appropriate branch (fork or direct) 3. Prepare comprehensive PR description 4. Get user approval before creating PR + 5. Extract owner and repo from the provided GitHub URL @@ -22,9 +23,30 @@ - Screenshots/demos if applicable + + Use GitHub CLI to create the pull request: + + gh pr create --repo [owner]/[repo] --base main --title "[title]" --body "[description]" --maintainer-can-modify + + + If working from a fork, ensure you've forked first: + + gh repo fork [owner]/[repo] --clone + + + The gh CLI automatically handles fork workflows. + + - 1. Comment on original issue with PR link + 1. Comment on original issue with PR link: + + gh issue comment [issue-number] --repo [owner]/[repo] --body "PR #[pr-number] has been created to address this issue: [PR URL]" + 2. Inform user of successful creation 3. Provide next steps and tracking info + 4. Monitor PR checks: + + gh pr checks [pr-number] --repo [owner]/[repo] --watch + \ No newline at end of file diff --git a/.roo/rules-issue-fixer/8_github_communication_guidelines.xml b/.roo/rules-issue-fixer/8_github_communication_guidelines.xml index 91ef0de89e..627908f1f7 100644 --- a/.roo/rules-issue-fixer/8_github_communication_guidelines.xml +++ b/.roo/rules-issue-fixer/8_github_communication_guidelines.xml @@ -1,14 +1,4 @@ - - - Keep comments concise and focused on technical substance - - Avoid overly verbose explanations unless specifically requested - - Sound human and conversational, not robotic - - Address specific feedback points directly - - Use bullet points for multiple changes - - Reference line numbers or specific code when relevant - - Example: "Updated the error handling in `validateInput()` to catch edge cases as requested. Also added the missing null check on line 45." - - - Provide brief status updates when working on complex issues - Ask specific questions if requirements are unclear diff --git a/.roo/rules-issue-fixer/9_pr_review_workflow.xml b/.roo/rules-issue-fixer/9_pr_review_workflow.xml deleted file mode 100644 index 849ee7f1df..0000000000 --- a/.roo/rules-issue-fixer/9_pr_review_workflow.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - When working on PR review feedback: - 1. Read all review comments carefully - 2. Identify specific changes requested - 3. Group related feedback into logical changes - 4. Address each point systematically - 5. Test changes thoroughly - 6. Respond to each review comment when pushing updates - 7. Use "Resolved" or brief explanations for each addressed point - - - - For partial workflows (user-requested changes to existing PRs): - 1. Focus only on the specific changes requested - 2. Don't refactor unrelated code unless explicitly asked - 3. Maintain consistency with existing PR approach - 4. Test only the modified functionality unless broader testing is needed - 5. Update PR description if significant changes are made - - - - When responding to review comments: - - "✅ Fixed - [brief description of change]" - - "✅ Added - [what was added]" - - "✅ Updated - [what was changed]" - - "❓ Question - [if clarification needed]" - - Keep responses short and action-oriented - - \ No newline at end of file diff --git a/.roo/rules-pr-fixer/1_workflow.xml b/.roo/rules-pr-fixer/1_workflow.xml index 845a05d4ea..4b43e99217 100644 --- a/.roo/rules-pr-fixer/1_workflow.xml +++ b/.roo/rules-pr-fixer/1_workflow.xml @@ -41,17 +41,24 @@ Execute the user's chosen course of action. - Check out the PR branch locally using 'gh pr checkout'. - Apply code changes based on review feedback. - Fix failing tests. - Resolve conflicts by rebasing the PR branch and force-pushing. + Check out the PR branch locally using 'gh pr checkout --force'. + Determine if the PR is from a fork by checking 'gh pr view --json isCrossRepository'. + Apply code changes based on review feedback using file editing tools. + Fix failing tests by modifying test files or source code as needed. + For conflict resolution: Use GIT_EDITOR=true for non-interactive rebases, then resolve conflicts via file editing. + If changes affect user-facing content (i18n files, UI components, announcements), delegate translation updates using the new_task tool with translate mode. + Commit changes using git commands. + Push changes to the correct remote (origin for same-repo PRs, fork remote for cross-repo PRs) using 'git push --force-with-lease'. Verify that the pushed changes resolve the issues. - Use 'gh pr checks --watch' to monitor the CI/CD pipeline and ensure all workflows execute successfully. + Use 'gh pr checks --watch' to monitor check status in real-time until all checks complete. + If needed, check specific workflow runs with 'gh run list --pr' for detailed CI/CD pipeline status. + Verify that all translation updates (if any) have been completed and committed. + Confirm PR is ready for review by checking mergeable state with 'gh pr view --json'. @@ -60,5 +67,6 @@ All actionable review comments have been addressed. All tests are passing. The PR is free of merge conflicts. + All required translations have been completed and committed (if changes affect user-facing content). \ No newline at end of file diff --git a/.roo/rules-pr-fixer/2_best_practices.xml b/.roo/rules-pr-fixer/2_best_practices.xml index 1576daa3c1..e9ef4a8b27 100644 --- a/.roo/rules-pr-fixer/2_best_practices.xml +++ b/.roo/rules-pr-fixer/2_best_practices.xml @@ -10,6 +10,16 @@ Address issues one at a time (e.g., fix tests first, then address comments). This makes the process more manageable and easier to validate. Tackling all issues at once can be complex and error-prone. + + Handle Fork Remotes Correctly + Always check if a PR comes from a fork (cross-repository) before pushing changes. Use 'gh pr view --json isCrossRepository' to determine the correct remote. + Pushing to the wrong remote (e.g., origin instead of fork) will fail for cross-repository PRs. + + PR from a fork + Check isCrossRepository, add fork remote if needed, push to fork + Always push to origin without checking PR source + + diff --git a/.roo/rules-pr-fixer/3_common_patterns.xml b/.roo/rules-pr-fixer/3_common_patterns.xml index 659aa7d07f..5d2c7f033f 100644 --- a/.roo/rules-pr-fixer/3_common_patterns.xml +++ b/.roo/rules-pr-fixer/3_common_patterns.xml @@ -24,31 +24,87 @@ - - A sequence of commands to resolve merge conflicts locally using rebase. + + Commands to detect merge conflicts. + + + + + Rebase operations using GIT_EDITOR to prevent interactive prompts. + + + + Check current conflict status without interactive input. + - Command to check out a pull request branch locally. + Check out a pull request branch locally. + + + + + Determine the correct remote to push to (handles forks). + + + + + Monitor PR checks in real-time as they run. + + + + + Push operations that handle both origin and fork remotes correctly. - - After pushing changes, use this command to monitor the CI/CD pipeline in real-time. + + + Commit operations that work in automated environments. diff --git a/.roo/rules-pr-fixer/4_tool_usage.xml b/.roo/rules-pr-fixer/4_tool_usage.xml index 10361dc3ec..15833f3dd7 100644 --- a/.roo/rules-pr-fixer/4_tool_usage.xml +++ b/.roo/rules-pr-fixer/4_tool_usage.xml @@ -11,6 +11,11 @@ Quickly identifies if there are failing automated checks that need investigation. + new_task (mode: translate) + When changes affect user-facing content, i18n files, or UI components that require translation. + Ensures translation consistency across all supported languages when PR fixes involve user-facing changes. + + gh pr checks --watch After pushing a fix, to confirm that the changes have resolved the CI/CD failures. Provides real-time feedback on whether the fix was successful. @@ -35,6 +40,41 @@ Use this command to get the exact error messages from failing tests. Search the log for keywords like 'error', 'failed', or 'exception' to quickly find the root cause. + Always specify run ID explicitly to avoid interactive selection prompts. + + + + + + Use --force flag: 'gh pr checkout --force' + If gh checkout fails, use: git fetch origin pull//head: + + + + + + Use --force-with-lease for safer force pushing. + Use GIT_EDITOR=true to prevent interactive prompts during rebases. + Always determine the correct remote before pushing (origin vs fork). + + + Check if PR is from a fork: 'gh pr view --json isCrossRepository' + If isCrossRepository is true, add fork remote if needed + Push to appropriate remote: 'git push --force-with-lease ' + + + Use 'GIT_EDITOR=true git rebase main' to start rebase + If conflicts occur, edit files to resolve them + Use 'git add .' and 'git rebase --continue' to proceed + + + + + + Use --watch flag to monitor checks in real-time: 'gh pr checks --watch' + For one-time status checks, use --json flag: 'gh pr checks --json state,conclusion,name' + The --watch flag automatically updates the display as check statuses change. + Use 'gh run list --pr ' to get detailed workflow status if needed. @@ -45,5 +85,34 @@ Example suggestions: "Address review comments first.", "Tackle the failing tests.", "Resolve merge conflicts." + + + + Use when PR fixes involve changes to user-facing strings, i18n files, or UI components. + Provide specific details about what content needs translation in the message. + Include file paths and descriptions of the changes made. + List all affected languages that need updates. + Wait for translation completion before proceeding to validation phase. + + + Changes to webview-ui/src/i18n/locales/en/*.json files + Changes to src/i18n/locales/en/*.json files + Modifications to UI components with user-facing text + Updates to announcement files or documentation requiring localization + Addition of new error messages or user notifications + + +translate +Translation updates needed for PR #1234 fixes. Please translate the following changes: + +Files modified: +- webview-ui/src/i18n/locales/en/common.json: Added new error message "connection_failed" +- webview-ui/src/components/settings/ApiSettings.tsx: Updated button text from "Save" to "Save Configuration" + +Please ensure all supported languages (ca, de, es, fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW) are updated with appropriate translations for these changes. + + ]]> + \ No newline at end of file diff --git a/.roo/rules-pr-fixer/5_examples.xml b/.roo/rules-pr-fixer/5_examples.xml index e34a79421f..0ff98de250 100644 --- a/.roo/rules-pr-fixer/5_examples.xml +++ b/.roo/rules-pr-fixer/5_examples.xml @@ -62,7 +62,7 @@ Check out the pull request branch. - gh pr checkout 4365 + gh pr checkout 4365 --force The PR branch is now ready for local edits. @@ -82,13 +82,13 @@ - After pushing the changes, watch the PR checks to confirm the fix. + After pushing the changes, monitor PR checks in real-time. - gh pr checks --watch + gh pr checks 4365 --watch - Confirm that all checks are passing after the fix. + Monitor checks continuously until all complete. The --watch flag provides real-time updates as check statuses change. @@ -97,4 +97,112 @@ Use a combination of the GitHub MCP server and the `gh` CLI to get a complete picture of the PR's status. + + + + A pull request has review comments requesting UI text changes that require translation updates across all supported languages. + + + + Fix PR #1234 in RooCodeInc/Roo-Code - the reviewer asked to change button text and there are some failing tests. + + + + + Get PR details and analyze changes. + + + github + get_pull_request + + { + "owner": "RooCodeInc", + "repo": "Roo-Code", + "pullNumber": 1234 + } + + + + Identify the files changed and review feedback requiring UI text modifications. + + + + Check out PR and apply the requested changes. + + +gh pr checkout 1234 --force + + + Make the requested button text changes in the UI components. + + + + Identify translation requirements and delegate to translate mode. + + +translate +Translation updates needed for PR #1234 fixes. The following changes were made based on review feedback: + +Files modified: +- webview-ui/src/components/settings/ApiSettings.tsx: Changed button text from "Save" to "Save Configuration" +- webview-ui/src/i18n/locales/en/common.json: Updated key "save_button" to "save_config_button" + +Please update all supported languages (ca, de, es, fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW) with appropriate translations for: +- New key "save_config_button" with translation equivalent to "Save Configuration" +- Any other text changes that affect user-facing content + +Ensure consistency across all language files and maintain the same context and tone as existing translations. + + + Translation subtask created and all language files updated. + + + + Commit all changes including translations with automated git configuration. + + +git add . && git commit -m "fix: update button text and translations as requested in review" + + + All code changes and translation updates are now committed. + + + + Check if PR is from a fork and push to correct remote. + + +gh pr view 1234 --json isCrossRepository,headRepositoryOwner,headRefName + + + Determine if this is a cross-repository PR to know which remote to push to. + + + + Push changes to the appropriate remote. + + +git push --force-with-lease origin + + + Push changes safely to update the pull request. Use 'fork' remote instead if PR is from a fork. + + + + Monitor CI status in real-time. + + +gh pr checks 1234 --watch + + + Watch CI checks continuously until all tests pass. The --watch flag provides automatic updates as check statuses change. + + + + + Always check if PR fixes involve user-facing content that requires translation. + Use new_task with translate mode to ensure consistent translation updates. + Include detailed context about what changed and why in translation requests. + Verify translation completeness before considering the PR fix complete. + + diff --git a/.roo/rules-pr-reviewer/1_workflow.xml b/.roo/rules-pr-reviewer/1_workflow.xml index a6a36241e2..31b70d981d 100644 --- a/.roo/rules-pr-reviewer/1_workflow.xml +++ b/.roo/rules-pr-reviewer/1_workflow.xml @@ -100,7 +100,7 @@ - Examine existing PR comments to understand the current state of discussion. Always verify whether a comment is current or already addressed before suggesting action. + Examine existing PR comments to understand the current state of discussion. When reading the comments and reviews, you must verify which are resolved by reading the files they refer to, since they might already be resolved. This prevents you from making redundant suggestions. diff --git a/.roo/rules-pr-reviewer/2_best_practices.xml b/.roo/rules-pr-reviewer/2_best_practices.xml index 29adcba946..69ba8088f0 100644 --- a/.roo/rules-pr-reviewer/2_best_practices.xml +++ b/.roo/rules-pr-reviewer/2_best_practices.xml @@ -2,7 +2,7 @@ - Always fetch and review the entire PR diff before commenting - Check for and review any associated issue for context - Check out the PR locally for better context understanding - - Review existing comments to avoid duplicate feedback + - Review existing comments and verify against the current code to avoid redundant feedback on already resolved issues - Focus on the changes made, not unrelated code - Ensure all changes are directly related to the linked issue - Use a friendly, curious tone in all comments diff --git a/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml b/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml index 6a5b707c12..0868956e87 100644 --- a/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml +++ b/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml @@ -7,7 +7,7 @@ - Using markdown headings (###, ##, #) in review comments - Using excessive markdown formatting when plain text would suffice - Submitting comments without user preview/approval - - Ignoring existing PR comments and discussions + - Ignoring existing PR comments or failing to verify if they have already been resolved by checking the code - Forgetting to check for an associated issue for additional context - Missing critical security or performance issues - Not checking for proper i18n in UI changes diff --git a/.roo/rules-translate/001-general-rules.md b/.roo/rules-translate/001-general-rules.md index 2b747f77b9..e27b9793e2 100644 --- a/.roo/rules-translate/001-general-rules.md +++ b/.roo/rules-translate/001-general-rules.md @@ -64,8 +64,10 @@ 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. Create appropriate translations for all other supported languages - 5. Validate your changes with the missing translations script + 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 - 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/.roo/rules/rules.md b/.roo/rules/rules.md index d3795393f3..b423021844 100644 --- a/.roo/rules/rules.md +++ b/.roo/rules/rules.md @@ -6,6 +6,12 @@ - Ensure all tests pass before submitting changes - The vitest framework is used for testing; the `describe`, `test`, `it`, etc functions are defined by default in `tsconfig.json` and therefore don't need to be imported - Tests must be run from the same directory as the `package.json` file that specifies `vitest` in `devDependencies` + - Run tests with: `npx vitest ` + - Do NOT run tests from project root - this causes "vitest: command not found" error + - Tests must be run from inside the correct workspace: + - Backend tests: `cd src && npx vitest path/to/test-file` (don't include `src/` in path) + - UI tests: `cd webview-ui && npx vitest src/path/to/test-file` + - Example: For `src/tests/user.test.ts`, run `cd src && npx vitest tests/user.test.ts` NOT `npx vitest src/tests/user.test.ts` 2. Lint Rules: diff --git a/.roomodes b/.roomodes index 236026c738..b4229b11f2 100644 --- a/.roomodes +++ b/.roomodes @@ -1,24 +1,10 @@ customModes: - slug: mode-writer name: ✍️ Mode Writer - roleDefinition: >- - You are Roo, a mode creation specialist focused on designing and implementing custom modes for the Roo-Code project. Your expertise includes: - - Understanding the mode system architecture and configuration - - Creating well-structured mode definitions with clear roles and responsibilities - - Writing comprehensive XML-based special instructions using best practices - - Ensuring modes have appropriate tool group permissions - - Crafting clear whenToUse descriptions for the Orchestrator - - Following XML structuring best practices for clarity and parseability - - You help users create new modes by: - - Gathering requirements about the mode's purpose and workflow - - Defining appropriate roleDefinition and whenToUse descriptions - - Selecting the right tool groups and file restrictions - - Creating detailed XML instruction files in the .roo folder - - Ensuring instructions are well-organized with proper XML tags - - Following established patterns from existing modes - whenToUse: >- - Use this mode when you need to create a new custom mode. + roleDefinition: |- + You are Roo, a mode creation specialist focused on designing and implementing custom modes for the Roo-Code project. Your expertise includes: - Understanding the mode system architecture and configuration - Creating well-structured mode definitions with clear roles and responsibilities - Writing comprehensive XML-based special instructions using best practices - Ensuring modes have appropriate tool group permissions - Crafting clear whenToUse descriptions for the Orchestrator - Following XML structuring best practices for clarity and parseability + You help users create new modes by: - Gathering requirements about the mode's purpose and workflow - Defining appropriate roleDefinition and whenToUse descriptions - Selecting the right tool groups and file restrictions - Creating detailed XML instruction files in the .roo folder - Ensuring instructions are well-organized with proper XML tags - Following established patterns from existing modes + whenToUse: Use this mode when you need to create a new custom mode. groups: - read - - edit @@ -29,30 +15,11 @@ customModes: source: project - slug: test name: 🧪 Test - roleDefinition: >- - You are Roo, a Vitest testing specialist with deep expertise in: - - Writing and maintaining Vitest test suites - - Test-driven development (TDD) practices - - Mocking and stubbing with Vitest - - Integration testing strategies - - TypeScript testing patterns - - Code coverage analysis - - Test performance optimization - - Your focus is on maintaining high test quality and coverage across the codebase, working primarily with: - - Test files in __tests__ directories - - Mock implementations in __mocks__ - - Test utilities and helpers - - Vitest configuration and setup - - You ensure tests are: - - Well-structured and maintainable - - Following Vitest best practices - - Properly typed with TypeScript - - Providing meaningful coverage - - Using appropriate mocking strategies - whenToUse: >- - Use this mode when you need to write, modify, or maintain tests for the codebase. + roleDefinition: |- + You are Roo, a Vitest testing specialist with deep expertise in: - Writing and maintaining Vitest test suites - Test-driven development (TDD) practices - Mocking and stubbing with Vitest - Integration testing strategies - TypeScript testing patterns - Code coverage analysis - Test performance optimization + Your focus is on maintaining high test quality and coverage across the codebase, working primarily with: - Test files in __tests__ directories - Mock implementations in __mocks__ - Test utilities and helpers - Vitest configuration and setup + You ensure tests are: - Well-structured and maintainable - Following Vitest best practices - Properly typed with TypeScript - Providing meaningful coverage - Using appropriate mocking strategies + whenToUse: Use this mode when you need to write, modify, or maintain tests for the codebase. groups: - read - browser @@ -74,12 +41,7 @@ customModes: - Tests must be run from the same directory as the `package.json` file that specifies `vitest` in `devDependencies` - slug: design-engineer name: 🎨 Design Engineer - roleDefinition: >- - You are Roo, an expert Design Engineer focused on VSCode Extension development. Your expertise includes: - - Implementing UI designs with high fidelity using React, Shadcn, Tailwind and TypeScript. - - Ensuring interfaces are responsive and adapt to different screen sizes. - - Collaborating with team members to translate broad directives into robust and detailed designs capturing edge cases. - - Maintaining uniformity and consistency across the user interface. + roleDefinition: "You are Roo, an expert Design Engineer focused on VSCode Extension development. Your expertise includes: - Implementing UI designs with high fidelity using React, Shadcn, Tailwind and TypeScript. - Ensuring interfaces are responsive and adapt to different screen sizes. - Collaborating with team members to translate broad directives into robust and detailed designs capturing edge cases. - Maintaining uniformity and consistency across the user interface." groups: - read - - edit @@ -93,32 +55,12 @@ customModes: - slug: release-engineer name: 🚀 Release Engineer roleDefinition: You are Roo, a release engineer specialized in automating the release process for software projects. You have expertise in version control, changelogs, release notes, creating changesets, and coordinating with translation teams to ensure a smooth release process. - customInstructions: >- - When preparing a release: - 1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt ` - 2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt}] | sort_by(.number)'` - 3. Summarize the changes and ask the user whether this should be a major, minor, or patch release - 4. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: - - ``` - --- - "roo-cline": patch|minor|major - --- - - [list of changes] - ``` - - - Always include contributor attribution using format: (thanks @username!) - - Provide brief descriptions of each item to explain the change - - Order the list from most important to least important - - Example: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" - - CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. - - 5. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) - 6. Ask the user to confirm the English version - 7. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages - 8. Commit and push the changeset file to the repository - 9. The GitHub Actions workflow will automatically: + customInstructions: |- + When preparing a release: 1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt ` 2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt}] | sort_by(.number)'` 3. Summarize the changes and ask the user whether this should be a major, minor, or patch release 4. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: + ``` --- "roo-cline": patch|minor|major --- + [list of changes] ``` + - Always include contributor attribution using format: (thanks @username!) - Provide brief descriptions of each item to explain the change - Order the list from most important to least important - Example: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" - CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. + 5. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) 6. Ask the user to confirm the English version 7. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages 8. Commit and push the changeset file to the repository 9. The GitHub Actions workflow will automatically: - Create a version bump PR when changesets are merged to main - Update the CHANGELOG.md with proper formatting - Publish the release when the version bump PR is merged @@ -140,7 +82,7 @@ customModes: source: project - slug: issue-fixer name: 🔧 Issue Fixer - roleDefinition: >- + roleDefinition: |- You are a GitHub issue resolution specialist focused on fixing bugs and implementing feature requests from GitHub issues. Your expertise includes: - Analyzing GitHub issues to understand requirements and acceptance criteria - Exploring codebases to identify all affected files and dependencies @@ -148,30 +90,20 @@ customModes: - Building new features based on detailed proposals - Ensuring all acceptance criteria are met before completion - Creating pull requests with proper documentation - - Handling PR review feedback and implementing requested changes - - Making concise, human-sounding GitHub comments that focus on technical substance + - Using GitHub CLI for all GitHub operations - You work with issues from the RooCodeInc/Roo-Code repository, transforming them into working code that addresses all requirements while maintaining code quality and consistency. You also handle partial workflows for existing PRs when changes are requested by maintainers or users through the review process. - whenToUse: Use this mode when you have a GitHub issue (bug report or feature request) that needs to be fixed or implemented, OR when you need to address feedback on an existing pull request. Provide the issue number, PR number, or URL, and this mode will guide you through understanding the requirements, implementing the solution, and preparing for submission or updates. + You work with issues from any GitHub repository, transforming them into working code that addresses all requirements while maintaining code quality and consistency. You use the GitHub CLI (gh) for all GitHub operations instead of MCP tools. + whenToUse: Use this mode when you have a GitHub issue (bug report or feature request) that needs to be fixed or implemented. Provide the issue URL, and this mode will guide you through understanding the requirements, implementing the solution, and preparing for submission. groups: - read - edit - command - - mcp source: project - slug: issue-writer name: 📝 Issue Writer - roleDefinition: >- - You are Roo, a GitHub issue creation specialist focused on crafting well-structured, detailed issues based on the project's issue templates. Your expertise includes: - - Understanding and analyzing user requirements for bug reports and feature requests - - Exploring codebases thoroughly to gather relevant technical context - - Creating comprehensive GitHub issues following XML-based templates - - Ensuring issues contain all necessary information for developers - - Using GitHub MCP tools to create issues programmatically - - You work with two primary issue types: - - Bug Reports: Documenting reproducible bugs with clear steps and expected outcomes - - Feature Proposals: Creating detailed, actionable feature requests with clear problem statements, solutions, and acceptance criteria + roleDefinition: |- + You are Roo, a GitHub issue creation specialist focused on crafting well-structured, detailed issues based on the project's issue templates. Your expertise includes: - Understanding and analyzing user requirements for bug reports and feature requests - Exploring codebases thoroughly to gather relevant technical context - Creating comprehensive GitHub issues following XML-based templates - Ensuring issues contain all necessary information for developers - Using GitHub MCP tools to create issues programmatically + You work with two primary issue types: - Bug Reports: Documenting reproducible bugs with clear steps and expected outcomes - Feature Proposals: Creating detailed, actionable feature requests with clear problem statements, solutions, and acceptance criteria whenToUse: Use this mode when you need to create a GitHub issue for bug reports or feature requests. This mode will guide you through gathering all necessary information, exploring the codebase for context, and creating a well-structured issue in the RooCodeInc/Roo-Code repository. groups: - read @@ -180,27 +112,10 @@ customModes: source: project - slug: integration-tester name: 🧪 Integration Tester - roleDefinition: >- - You are Roo, an integration testing specialist focused on VSCode E2E tests with expertise in: - - Writing and maintaining integration tests using Mocha and VSCode Test framework - - Testing Roo Code API interactions and event-driven workflows - - Creating complex multi-step task scenarios and mode switching sequences - - Validating message formats, API responses, and event emission patterns - - Test data generation and fixture management - - Coverage analysis and test scenario identification - - Your focus is on ensuring comprehensive integration test coverage for the Roo Code extension, working primarily with: - - E2E test files in apps/vscode-e2e/src/suite/ - - Test utilities and helpers - - API type definitions in packages/types/ - - Extension API testing patterns - - You ensure integration tests are: - - Comprehensive and cover critical user workflows - - Following established Mocha TDD patterns - - Using async/await with proper timeout handling - - Validating both success and failure scenarios - - Properly typed with TypeScript + roleDefinition: |- + You are Roo, an integration testing specialist focused on VSCode E2E tests with expertise in: - Writing and maintaining integration tests using Mocha and VSCode Test framework - Testing Roo Code API interactions and event-driven workflows - Creating complex multi-step task scenarios and mode switching sequences - Validating message formats, API responses, and event emission patterns - Test data generation and fixture management - Coverage analysis and test scenario identification + Your focus is on ensuring comprehensive integration test coverage for the Roo Code extension, working primarily with: - E2E test files in apps/vscode-e2e/src/suite/ - Test utilities and helpers - API type definitions in packages/types/ - Extension API testing patterns + You ensure integration tests are: - Comprehensive and cover critical user workflows - Following established Mocha TDD patterns - Using async/await with proper timeout handling - Validating both success and failure scenarios - Properly typed with TypeScript groups: - read - command @@ -210,16 +125,8 @@ customModes: source: project - slug: pr-reviewer name: 🔍 PR Reviewer - roleDefinition: >- - You are Roo, a pull request reviewer specializing in code quality, structure, and translation consistency. Your expertise includes: - - Analyzing pull request diffs and understanding code changes in context - - Evaluating code quality, identifying code smells and technical debt - - Ensuring structural consistency across the codebase - - Verifying proper internationalization (i18n) for UI changes - - Providing constructive feedback with a friendly, curious tone - - Reviewing test coverage and quality without executing tests - - Identifying opportunities for code improvements and refactoring - + roleDefinition: |- + You are Roo, a pull request reviewer specializing in code quality, structure, and translation consistency. Your expertise includes: - Analyzing pull request diffs and understanding code changes in context - Evaluating code quality, identifying code smells and technical debt - Ensuring structural consistency across the codebase - Verifying proper internationalization (i18n) for UI changes - Providing constructive feedback with a friendly, curious tone - Reviewing test coverage and quality without executing tests - Identifying opportunities for code improvements and refactoring You work primarily with the RooCodeInc/Roo-Code repository, using GitHub MCP tools to fetch and review pull requests. You check out PRs locally for better context understanding and focus on providing actionable, constructive feedback that helps improve code quality. whenToUse: Use this mode to review pull requests on the Roo-Code GitHub repository or any other repository if specified by the user. groups: @@ -232,10 +139,8 @@ customModes: source: project - slug: docs-extractor name: 📚 Docs Extractor - roleDefinition: >- - You are Roo, a comprehensive documentation extraction specialist focused on analyzing and documenting all technical and non-technical information about features and components within codebases. - whenToUse: >- - Use this mode when you need to extract comprehensive documentation about any feature, component, or aspect of a codebase. + roleDefinition: You are Roo, a comprehensive documentation extraction specialist focused on analyzing and documenting all technical and non-technical information about features and components within codebases. + whenToUse: Use this mode when you need to extract comprehensive documentation about any feature, component, or aspect of a codebase. groups: - read - - edit @@ -245,18 +150,23 @@ customModes: - 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. + 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. groups: - read - edit - command - mcp + - slug: issue-fixer-orchestrator + name: 🔧 Issue Fixer Orchestrator + roleDefinition: |- + You are an orchestrator for fixing GitHub issues. Your primary role is to coordinate a series of specialized subtasks to resolve an issue from start to finish. + **Your Orchestration Responsibilities:** - Delegate analysis, implementation, and testing to specialized subtasks using the `new_task` tool. - Manage the workflow and pass context between steps using temporary files. - Present plans, results, and pull requests to the user for approval at key milestones. + **Your Core Expertise Includes:** - Analyzing GitHub issues to understand requirements and acceptance criteria. - Exploring codebases to identify all affected files and dependencies. - Guiding the implementation of high-quality fixes and features. - Ensuring comprehensive test coverage. - Overseeing the creation of well-documented pull requests. - Using the GitHub CLI (gh) for all final GitHub operations like creating a pull request. + whenToUse: Use this mode to orchestrate the process of fixing a GitHub issue. Provide a GitHub issue URL, and this mode will coordinate a series of subtasks to analyze the issue, explore the code, create a plan, implement the solution, and prepare a pull request. + groups: + - read + - edit + - command + source: project + description: Issue Fixer mode ported into an orchestrator diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0265bef8..9a629487e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,82 @@ # Roo Code Changelog +## [3.22.6] - 2025-07-02 + +- Add timer-based auto approve for follow up questions (thanks @liwilliam2021!) +- Add import/export modes functionality +- Add persistent version indicator on chat screen +- Add automatic configuration import on extension startup (thanks @takakoutso!) +- Add user-configurable search score threshold slider for semantic search (thanks @hannesrudolph!) +- Add default headers and testing for litellm fetcher (thanks @andrewshu2000!) +- Fix consistent cancellation error messages for thinking vs streaming phases +- Fix AWS Bedrock cross-region inference profile mapping (thanks @KevinZhao!) +- Fix URL loading timeout issues in @ mentions (thanks @MuriloFP!) +- Fix API retry exponential backoff capped at 10 minutes (thanks @MuriloFP!) +- Fix Qdrant URL field auto-filling with default value (thanks @SannidhyaSah!) +- Fix profile context condensation threshold (thanks @PaperBoardOfficial!) +- Fix apply_diff tool documentation for multi-file capabilities +- Fix cache files excluded from rules compilation (thanks @MuriloFP!) +- Add streamlined extension installation and documentation (thanks @devxpain!) +- Prevent Architect mode from providing time estimates +- Remove context size from environment details +- Change default mode to architect for new installations +- Suppress Mermaid error rendering +- Improve Mermaid buttons with light background in light mode (thanks @chrarnoldus!) +- Add .vscode/ to write-protected files/directories +- Update AWS Bedrock cross-region inference profile mapping (thanks @KevinZhao!) + +## [3.22.5] - 2025-06-28 + +- Remove Gemini CLI provider while we work with Google on a better integration + +## [3.22.4] - 2025-06-27 + +- Fix: resolve E2BIG error by passing large prompts via stdin to Claude CLI (thanks @Fovty!) +- Add optional mode suggestions to follow-up questions +- Fix: move StandardTooltip inside PopoverTrigger in ShareButton (thanks @daniel-lxs!) + +## [3.22.3] - 2025-06-27 + +- Restore JSON backwards compatibility for .roomodes files (thanks @daniel-lxs!) + +## [3.22.2] - 2025-06-27 + +- Fix: eliminate XSS vulnerability in CodeBlock component (thanks @KJ7LNW!) +- Fix terminal keyboard shortcut error when adding content to context (thanks @MuriloFP!) +- Fix checkpoint popover not opening due to StandardTooltip wrapper conflict (thanks @daniel-lxs!) +- Fix(i18n): correct gemini cli error translation paths (thanks @daniel-lxs!) +- Code Index (Qdrant) recreate services when change configurations (thanks @catrielmuller!) + +## [3.22.1] - 2025-06-26 + +- Add Gemini CLI provider (thanks Cline!) +- Fix undefined mcp command (thanks @qdaxb!) +- Use upstream_inference_cost for OpenRouter BYOK cost calculation and show cached token count (thanks @chrarnoldus!) +- Update maxTokens value for qwen/qwen3-32b model on Groq (thanks @KanTakahiro!) +- Standardize tooltip delays to 300ms + +## [3.22.0] - 2025-06-25 + +- Add 1-click task sharing +- Add support for loading rules from a global .roo directory (thanks @samhvw8!) +- Modes selector improvements (thanks @brunobergher!) +- Use safeWriteJson for all JSON file writes to avoid task history corruption (thanks @KJ7LNW!) +- Improve YAML error handling when editing modes +- Register importSettings as VSCode command (thanks @shivamd1810!) +- Add default task names for empty tasks (thanks @daniel-lxs!) +- Improve translation workflow to avoid unnecessary file reads (thanks @KJ7LNW!) +- Allow write_to_file to handle newline-only and empty content (thanks @Githubguy132010!) +- Address multiple memory leaks in CodeBlock component (thanks @kiwina!) +- Memory cleanup (thanks @xyOz-dev!) +- Fix port handling bug in code indexing for HTTPS URLs (thanks @benashby!) +- Improve Bedrock error handling for throttling and streaming contexts +- Handle long Claude code messages (thanks @daniel-lxs!) +- Fixes to Claude Code caching and image upload +- Disable reasoning budget UI controls for Claude Code provider +- Remove temperature parameter for Azure OpenAI reasoning models (thanks @ExactDoug!) +- Allowed commands import/export (thanks @catrielmuller!) +- Add VS Code setting to disable quick fix context actions (thanks @OlegOAndreev!) + ## [3.21.5] - 2025-06-23 - Fix Qdrant URL prefix handling for QdrantClient initialization (thanks @CW-B-W!) diff --git a/README.md b/README.md index af209a1429..4d6e5cd9db 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,13 @@ Check out the [CHANGELOG](CHANGELOG.md) for detailed updates and fixes. --- -## 🎉 Roo Code 3.21 Released +## 🎉 Roo Code 3.22 Released -Roo Code 3.21 brings major new features and improvements based on your feedback! +Roo Code 3.22 brings powerful new features and significant improvements to enhance your development workflow! -- **Roo Marketplace Launch** - The marketplace is now live! The marketplace is now live! Discover and install modes and MCPs easier than ever before. -- **Gemini 2.5 Models** - Added support for new Gemini 2.5 Pro, Flash, and Flash Lite models. -- **Excel File Support & More** - Added Excel (.xlsx) file support and numerous bug fixes and improvements! +- **1-Click Task Sharing** - Share your tasks instantly with colleagues and the community with a single click. +- **Global .roo Directory Support** - Load rules and configurations from a global .roo directory for consistent settings across projects. +- **Improved Architect to Code Transitions** - Seamless handoffs from planning in Architect mode to implementation in Code mode. --- @@ -138,22 +138,54 @@ pnpm install 3. **Run the extension**: -Press `F5` (or **Run** → **Start Debugging**) in VSCode to open a new window with Roo Code running. +There are several ways to run the Roo Code extension: -Changes to the webview will appear immediately. Changes to the core extension will require a restart of the extension host. +### Development Mode (F5) -Alternatively you can build a .vsix and install it directly in VSCode: +For active development, use VSCode's built-in debugging: -```sh -pnpm vsix -``` +Press `F5` (or go to **Run** → **Start Debugging**) in VSCode. This will open a new VSCode window with the Roo Code extension running. + +- Changes to the webview will appear immediately. +- Changes to the core extension will also hot reload automatically. + +### Automated VSIX Installation -A `.vsix` file will appear in the `bin/` directory which can be installed with: +To build and install the extension as a VSIX package directly into VSCode: ```sh -code --install-extension bin/roo-cline-.vsix +pnpm install:vsix [-y] [--editor=] ``` +This command will: + +- Ask which editor command to use (code/cursor/code-insiders) - defaults to 'code' +- Uninstall any existing version of the extension. +- Build the latest VSIX package. +- Install the newly built VSIX. +- Prompt you to restart VS Code for changes to take effect. + +Options: + +- `-y`: Skip all confirmation prompts and use defaults +- `--editor=`: Specify the editor command (e.g., `--editor=cursor` or `--editor=code-insiders`) + +### Manual VSIX Installation + +If you prefer to install the VSIX package manually: + +1. First, build the VSIX package: + ```sh + pnpm vsix + ``` +2. A `.vsix` file will be generated in the `bin/` directory (e.g., `bin/roo-cline-.vsix`). +3. Install it manually using the VSCode CLI: + ```sh + code --install-extension bin/roo-cline-.vsix + ``` + +--- + We use [changesets](https://github.com/changesets/changesets) for versioning and publishing. Check our `CHANGELOG.md` for release notes. --- @@ -176,40 +208,42 @@ Thanks to all our contributors who have helped make Roo Code better! -| 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
| jr
jr
| NyxJae
NyxJae
| MuriloFP
MuriloFP
| -| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| xyOz-dev
xyOz-dev
| sachasayan
sachasayan
| -| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| monotykamary
monotykamary
| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| qdaxb
qdaxb
| -| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| dtrugman
dtrugman
| lloydchang
lloydchang
| vigneshsubbiah16
vigneshsubbiah16
| Szpadel
Szpadel
| -| lupuletic
lupuletic
| kiwina
kiwina
| Premshay
Premshay
| psv2522
psv2522
| olweraltuve
olweraltuve
| diarmidmackenzie
diarmidmackenzie
| -| chrarnoldus
chrarnoldus
| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| afshawnlotfi
afshawnlotfi
| RaySinner
RaySinner
| nbihan-mediware
nbihan-mediware
| -| ChuKhaLi
ChuKhaLi
| hassoncs
hassoncs
| emshvac
emshvac
| kyle-apex
kyle-apex
| noritaka1166
noritaka1166
| pdecat
pdecat
| -| SannidhyaSah
SannidhyaSah
| StevenTCramer
StevenTCramer
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| -| dleffel
dleffel
| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| sammcj
sammcj
| -| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| benzntech
benzntech
| 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
| axkirillov
axkirillov
| anton-otee
anton-otee
| bramburn
bramburn
| olearycrew
olearycrew
| -| brunobergher
brunobergher
| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| julionav
julionav
| -| SplittyDev
SplittyDev
| mdp
mdp
| napter
napter
| ross
ross
| philfung
philfung
| dairui1
dairui1
| -| dqroid
dqroid
| forestyoo
forestyoo
| GOODBOY008
GOODBOY008
| hatsu38
hatsu38
| hongzio
hongzio
| im47cn
im47cn
| -| shoopapa
shoopapa
| jwcraig
jwcraig
| nevermorec
nevermorec
| bannzai
bannzai
| axmo
axmo
| asychin
asychin
| -| amittell
amittell
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| vladstudio
vladstudio
| tmsjngx0
tmsjngx0
| -| Githubguy132010
Githubguy132010
| tgfjt
tgfjt
| maekawataiki
maekawataiki
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| -| user202729
user202729
| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shaybc
shaybc
| seedlord
seedlord
| 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
| kohii
kohii
| kinandan
kinandan
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| -| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| Deon588
Deon588
| dleen
dleen
| devxpain
devxpain
| -| CW-B-W
CW-B-W
| chadgauth
chadgauth
| thecolorblue
thecolorblue
| bogdan0083
bogdan0083
| benashby
benashby
| Atlogit
Atlogit
| -| atlasgong
atlasgong
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| alarno
alarno
| -| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| AlexandruSmirnov
AlexandruSmirnov
| -| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| SECKainersdorfer
SECKainersdorfer
| R-omk
R-omk
| Sarke
Sarke
| -| OlegOAndreev
OlegOAndreev
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| markijbema
markijbema
| -| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| Rexarrior
Rexarrior
| KanTakahiro
KanTakahiro
| -| ksze
ksze
| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| celestial-vault
celestial-vault
| +| 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
| jr
jr
| MuriloFP
MuriloFP
| nissa-seru
nissa-seru
| jquanton
jquanton
| NyxJae
NyxJae
| +| elianiva
elianiva
| d-oit
d-oit
| punkpeye
punkpeye
| wkordalski
wkordalski
| xyOz-dev
xyOz-dev
| qdaxb
qdaxb
| +| feifei325
feifei325
| zhangtony239
zhangtony239
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| monotykamary
monotykamary
| sachasayan
sachasayan
| cannuri
cannuri
| +| vigneshsubbiah16
vigneshsubbiah16
| shariqriazz
shariqriazz
| pugazhendhi-m
pugazhendhi-m
| lloydchang
lloydchang
| dtrugman
dtrugman
| chrarnoldus
chrarnoldus
| +| Szpadel
Szpadel
| diarmidmackenzie
diarmidmackenzie
| olweraltuve
olweraltuve
| psv2522
psv2522
| Premshay
Premshay
| kiwina
kiwina
| +| lupuletic
lupuletic
| aheizi
aheizi
| SannidhyaSah
SannidhyaSah
| PeterDaveHello
PeterDaveHello
| hassoncs
hassoncs
| ChuKhaLi
ChuKhaLi
| +| nbihan-mediware
nbihan-mediware
| RaySinner
RaySinner
| afshawnlotfi
afshawnlotfi
| dleffel
dleffel
| StevenTCramer
StevenTCramer
| pdecat
pdecat
| +| noritaka1166
noritaka1166
| kyle-apex
kyle-apex
| emshvac
emshvac
| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| +| slytechnical
slytechnical
| arthurauffray
arthurauffray
| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| sammcj
sammcj
| +| Ruakij
Ruakij
| p12tic
p12tic
| gtaylor
gtaylor
| aitoroses
aitoroses
| axkirillov
axkirillov
| ross
ross
| +| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| taisukeoe
taisukeoe
| liwilliam2021
liwilliam2021
| 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
| benzntech
benzntech
| anton-otee
anton-otee
| +| bramburn
bramburn
| 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
| dairui1
dairui1
| dqroid
dqroid
| forestyoo
forestyoo
| +| GOODBOY008
GOODBOY008
| hatsu38
hatsu38
| hongzio
hongzio
| im47cn
im47cn
| shoopapa
shoopapa
| jwcraig
jwcraig
| +| kinandan
kinandan
| nevermorec
nevermorec
| bannzai
bannzai
| axmo
axmo
| asychin
asychin
| amittell
amittell
| +| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| vladstudio
vladstudio
| tmsjngx0
tmsjngx0
| tgfjt
tgfjt
| +| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| +| takakoutso
takakoutso
| student20880
student20880
| shohei-ihaya
shohei-ihaya
| shivamd1810
shivamd1810
| shaybc
shaybc
| seedlord
seedlord
| +| 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
| kohii
kohii
| 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
| +| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| 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
| +| KevinZhao
KevinZhao
| ksze
ksze
| Fovty
Fovty
| Jdo300
Jdo300
| hesara
hesara
| DeXtroTip
DeXtroTip
| +| pfitz
pfitz
| ExactDoug
ExactDoug
| | | | | diff --git a/apps/vscode-e2e/src/suite/task.test.ts b/apps/vscode-e2e/src/suite/task.test.ts index 96b1198fd4..31e03271b5 100644 --- a/apps/vscode-e2e/src/suite/task.test.ts +++ b/apps/vscode-e2e/src/suite/task.test.ts @@ -20,7 +20,7 @@ suite("Roo Code Task", function () { }) const taskId = await api.startNewTask({ - configuration: { mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, text: "Hello world, what is your name? Respond with 'My name is ...'", }) diff --git a/apps/web-roo-code/src/app/enterprise/page.tsx b/apps/web-roo-code/src/app/enterprise/page.tsx index c89d2abd11..d2c38fba05 100644 --- a/apps/web-roo-code/src/app/enterprise/page.tsx +++ b/apps/web-roo-code/src/app/enterprise/page.tsx @@ -1,9 +1,10 @@ -import { Code, CheckCircle, Shield, Users, Zap, Workflow } from "lucide-react" +import { Code, CheckCircle, Shield, Users, Zap, Workflow, Lock } from "lucide-react" import { Button } from "@/components/ui" import { AnimatedText } from "@/components/animated-text" import { AnimatedBackground } from "@/components/homepage" import { ContactForm } from "@/components/enterprise/contact-form" +import { EXTERNAL_LINKS } from "@/lib/constants" export default async function Enterprise() { return ( @@ -385,6 +386,63 @@ export default async function Enterprise() { + {/* Security Hook Section */} +
+
+
+
+
+
+ +
+

Enterprise-Grade Security

+

+ Built with security-first principles to meet stringent enterprise requirements while + maintaining developer productivity. +

+
    +
  • + + SOC 2 Type I Certified with Type II in observation +
  • +
  • + + End-to-end encryption for all data transmission +
  • +
  • + + Security-first architecture with explicit permissions +
  • +
  • + + Complete audit trails and compliance reporting +
  • +
  • + + Open-source transparency for security verification +
  • +
+
+
+
+ +

Security-First Design

+

+ Every feature built with enterprise security requirements in mind +

+
+ +
+
+
+
+
+ {/* CTA Section */}
diff --git a/apps/web-roo-code/src/components/chromes/footer.tsx b/apps/web-roo-code/src/components/chromes/footer.tsx index 0d322f31e1..57d4c8ae8b 100644 --- a/apps/web-roo-code/src/components/chromes/footer.tsx +++ b/apps/web-roo-code/src/components/chromes/footer.tsx @@ -118,6 +118,15 @@ export function Footer() { Enterprise +
  • + + Security + +
  • Enterprise + + Security + setIsMenuOpen(false)}> Enterprise + setIsMenuOpen(false)}> + Security + 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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Llicència diff --git a/locales/de/README.md b/locales/de/README.md index 1c1eb41f19..2dbaaa4363 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -50,13 +50,13 @@ Sehen Sie sich das [CHANGELOG](../../CHANGELOG.md) für detaillierte Updates und --- -## 🎉 Roo Code 3.21 veröffentlicht +## 🎉 Roo Code 3.22 veröffentlicht -Roo Code 3.21 bringt wichtige neue Funktionen und Verbesserungen basierend auf eurem Feedback! +Roo Code 3.22 bringt mächtige neue Funktionen und bedeutende Verbesserungen, um deinen Entwicklungsworkflow zu verbessern! -- **Roo Marketplace Launch** - Der Marketplace ist jetzt live! Der Marketplace ist jetzt live! Entdecke und installiere Modi und MCPs einfacher als je zuvor. -- **Gemini 2.5 Modelle** - Unterstützung für neue Gemini 2.5 Pro, Flash und Flash Lite Modelle hinzugefügt. -- **Excel-Datei-Unterstützung & Mehr** - Excel (.xlsx) Datei-Unterstützung hinzugefügt und zahlreiche Fehlerbehebungen und Verbesserungen! +- **1-Klick-Aufgaben-Teilen** - Teile deine Aufgaben sofort mit Kollegen und der Community mit einem einzigen Klick. +- **Globale .roo-Verzeichnis-Unterstützung** - Lade Regeln und Konfigurationen aus einem globalen .roo-Verzeichnis für konsistente Einstellungen über Projekte hinweg. +- **Verbesserte Übergänge von Architekt zu Code** - Nahtlose Übergaben von der Planung im Architekten-Modus zur Implementierung im Code-Modus. --- @@ -184,37 +184,39 @@ Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code zu verbessern! |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Lizenz diff --git a/locales/es/README.md b/locales/es/README.md index aa15874979..45942a0ee5 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -50,13 +50,13 @@ Consulta el [CHANGELOG](../../CHANGELOG.md) para ver actualizaciones detalladas --- -## 🎉 Roo Code 3.21 Lanzado +## 🎉 Roo Code 3.22 Lanzado -¡Roo Code 3.21 trae importantes nuevas funciones y mejoras basadas en vuestros comentarios! +¡Roo Code 3.22 trae nuevas funcionalidades poderosas y mejoras significativas para mejorar tu flujo de trabajo de desarrollo! -- **Lanzamiento del Marketplace de Roo** - ¡El marketplace ya está en funcionamiento! ¡El marketplace ya está en funcionamiento! Descubre e instala modos y MCPs más fácilmente que nunca. -- **Modelos Gemini 2.5** - Se ha añadido soporte para los nuevos modelos Gemini 2.5 Pro, Flash y Flash Lite. -- **Soporte de Archivos Excel y Más** - ¡Se ha añadido soporte para archivos Excel (.xlsx) y numerosas correcciones de errores y mejoras! +- **Compartir tareas con 1 clic** - Comparte tus tareas instantáneamente con colegas y la comunidad con un solo clic. +- **Soporte de directorio .roo global** - Carga reglas y configuraciones desde un directorio .roo global para configuraciones consistentes entre proyectos. +- **Transiciones mejoradas de Arquitecto a Código** - Transferencias fluidas desde la planificación en modo Arquitecto hasta la implementación en modo Código. --- @@ -184,37 +184,39 @@ Usamos [changesets](https://github.com/changesets/changesets) para versionar y p |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Licencia diff --git a/locales/fr/README.md b/locales/fr/README.md index a7100ba924..cb7c02166b 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -50,13 +50,13 @@ Consultez le [CHANGELOG](../../CHANGELOG.md) pour des mises à jour détaillées --- -## 🎉 Roo Code 3.21 est sorti +## 🎉 Roo Code 3.22 est sorti -Roo Code 3.21 apporte de nouvelles fonctionnalités majeures et des améliorations basées sur vos retours ! +Roo Code 3.22 apporte de puissantes nouvelles fonctionnalités et des améliorations significatives pour améliorer ton flux de travail de développement ! -- **Le marketplace est maintenant en ligne ! Le marketplace est maintenant en ligne !** Découvrez et installez des modes et des MCPs plus facilement que jamais. -- **Ajout du support pour les nouveaux modèles Gemini 2.5 Pro, Flash et Flash Lite.** -- **Support des Fichiers Excel et Plus !** - Support MCP amélioré, plus de contrôles Mermaid, support de réflexion dans Amazon Bedrock, et bien plus ! +- **Partage de tâches en 1 clic** - Partage tes tâches instantanément avec tes collègues et la communauté en un seul clic. +- **Support du répertoire .roo global** - Charge les règles et configurations depuis un répertoire .roo global pour des paramètres cohérents entre les projets. +- **Transitions améliorées d'Architecte vers Code** - Transferts fluides de la planification en mode Architecte vers l'implémentation en mode Code. --- @@ -184,37 +184,39 @@ Merci à tous nos contributeurs qui ont aidé à améliorer Roo Code ! |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Licence diff --git a/locales/hi/README.md b/locales/hi/README.md index 0184824533..38cfae33a4 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -184,37 +184,39 @@ Roo Code को बेहतर बनाने में मदद करने |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## लाइसेंस diff --git a/locales/id/README.md b/locales/id/README.md index 06f88d3b30..2dfd3000fd 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -178,37 +178,39 @@ Terima kasih kepada semua kontributor kami yang telah membantu membuat Roo Code |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## License diff --git a/locales/it/README.md b/locales/it/README.md index 7d96654b89..34f6d3d029 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -184,37 +184,39 @@ Grazie a tutti i nostri contributori che hanno aiutato a migliorare Roo Code! |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Licenza diff --git a/locales/ja/README.md b/locales/ja/README.md index d91743b525..07b12f1792 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -50,13 +50,13 @@ --- -## 🎉 Roo Code 3.21 リリース +## 🎉 Roo Code 3.22 リリース -Roo Code 3.21は、皆様のフィードバックに基づく新しい主要機能と改善をもたらします! +Roo Code 3.22は、開発ワークフローを向上させる強力な新機能と重要な改善をもたらします! -- **マーケットプレイスが稼働開始!マーケットプレイスが稼働開始!** これまで以上に簡単にモードとMCPを発見してインストールできます。 -- **新しいGemini 2.5 Pro、Flash、Flash Liteモデルのサポートを追加。** -- **Excel ファイルサポートなど** - MCP サポートの向上、Mermaid制御の追加、Amazon Bedrock thinking サポート、その他多数! +- **1クリックタスク共有** - 同僚やコミュニティとタスクを1クリックで瞬時に共有できます。 +- **グローバル.rooディレクトリサポート** - プロジェクト間で一貫した設定のためにグローバル.rooディレクトリからルールと設定を読み込みます。 +- **改善されたアーキテクトからコードへの移行** - アーキテクトモードでの計画からコードモードでの実装へのシームレスな引き継ぎ。 --- @@ -184,37 +184,39 @@ Roo Codeの改善に貢献してくれたすべての貢献者に感謝します |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## ライセンス diff --git a/locales/ko/README.md b/locales/ko/README.md index d994aa3e11..8d9e0381c1 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -50,13 +50,13 @@ --- -## 🎉 Roo Code 3.21 출시 +## 🎉 Roo Code 3.22 출시 -Roo Code 3.21이 여러분의 피드백을 바탕으로 한 새로운 주요 기능과 개선사항을 제공합니다! +Roo Code 3.22가 개발 워크플로우를 향상시키는 강력한 새 기능과 중요한 개선사항을 제공합니다! -- **마켓플레이스가 이제 라이브입니다! 마켓플레이스가 이제 라이브입니다!** 그 어느 때보다 쉽게 모드와 MCP를 발견하고 설치하세요. -- **새로운 Gemini 2.5 Pro, Flash, Flash Lite 모델 지원을 추가했습니다.** -- **Excel 파일 지원 등** - 향상된 MCP 지원, 더 많은 Mermaid 제어, Amazon Bedrock thinking 지원 등! +- **1클릭 작업 공유** - 동료와 커뮤니티에 한 번의 클릭으로 즉시 작업을 공유하세요. +- **글로벌 .roo 디렉토리 지원** - 프로젝트 간 일관된 설정을 위해 글로벌 .roo 디렉토리에서 규칙과 구성을 로드합니다. +- **개선된 아키텍트에서 코드로의 전환** - 아키텍트 모드에서의 계획부터 코드 모드에서의 구현까지 원활한 인수인계. --- @@ -184,37 +184,39 @@ Roo Code를 더 좋게 만드는 데 도움을 준 모든 기여자에게 감사 |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## 라이선스 diff --git a/locales/nl/README.md b/locales/nl/README.md index b3094153e1..b083ddf1b1 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -50,13 +50,13 @@ Bekijk de [CHANGELOG](../../CHANGELOG.md) voor gedetailleerde updates en fixes. --- -## 🎉 Roo Code 3.21 Uitgebracht +## 🎉 Roo Code 3.22 Uitgebracht -Roo Code 3.21 brengt krachtige nieuwe functies en verbeteringen op basis van jullie feedback! +Roo Code 3.22 brengt krachtige nieuwe functies en significante verbeteringen om je ontwikkelingsworkflow te verbeteren! -- **De marketplace is nu live! De marketplace is nu live!** Ontdek en installeer modi en MCP's eenvoudiger dan ooit tevoren. -- **Ondersteuning toegevoegd voor nieuwe Gemini 2.5 Pro, Flash en Flash Lite modellen.** -- **Excel Bestandsondersteuning & Meer** - Verbeterde Mermaid-controls voor betere diagramvisualisatie en nieuwe Amazon Bedrock thinking-ondersteuning voor meer geavanceerde AI-interacties! +- **1-Klik Taak Delen** - Deel je taken direct met collega's en de gemeenschap met een enkele klik. +- **Globale .roo Directory Ondersteuning** - Laad regels en configuraties vanuit een globale .roo directory voor consistente instellingen tussen projecten. +- **Verbeterde Architect naar Code Overgangen** - Naadloze overdrachten van planning in Architect-modus naar implementatie in Code-modus. --- @@ -184,37 +184,39 @@ Dank aan alle bijdragers die Roo Code beter hebben gemaakt! |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Licentie diff --git a/locales/pl/README.md b/locales/pl/README.md index 687a5721b4..86be4bc846 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -50,13 +50,13 @@ Sprawdź [CHANGELOG](../../CHANGELOG.md), aby uzyskać szczegółowe informacje --- -## 🎉 Roo Code 3.21 został wydany +## 🎉 Roo Code 3.22 został wydany -Roo Code 3.21 wprowadza potężne nowe funkcje i usprawnienia na podstawie opinii użytkowników! +Roo Code 3.22 wprowadza potężne nowe funkcje i znaczące usprawnienia, aby ulepszyć Twój przepływ pracy deweloperskiej! -- **Marketplace jest teraz na żywo! Marketplace jest teraz na żywo!** Odkrywaj i instaluj tryby oraz MCP łatwiej niż kiedykolwiek wcześniej. -- **Dodano wsparcie dla nowych modeli Gemini 2.5 Pro, Flash i Flash Lite.** -- **Wsparcie plików Excel i więcej** - Ulepszone kontrolki Mermaid dla lepszej wizualizacji diagramów oraz nowe wsparcie Amazon Bedrock thinking dla bardziej zaawansowanych interakcji z AI! +- **Udostępnianie zadań jednym kliknięciem** - Udostępniaj swoje zadania natychmiast współpracownikom i społeczności jednym kliknięciem. +- **Wsparcie globalnego katalogu .roo** - Ładuj zasady i konfiguracje z globalnego katalogu .roo dla spójnych ustawień między projektami. +- **Ulepszone przejścia z Architekta do Kodu** - Płynne przekazania od planowania w trybie Architekta do implementacji w trybie Kodu. --- @@ -184,37 +184,39 @@ Dziękujemy wszystkim naszym współtwórcom, którzy pomogli ulepszyć Roo Code |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Licencja diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 8a03cc7703..ffcdd994d3 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -50,13 +50,13 @@ Confira o [CHANGELOG](../../CHANGELOG.md) para atualizações e correções deta --- -## 🎉 Roo Code 3.21 foi lançado +## 🎉 Roo Code 3.22 foi lançado -O Roo Code 3.21 introduz novos recursos poderosos e melhorias baseadas no feedback dos usuários! +O Roo Code 3.22 traz novos recursos poderosos e melhorias significativas para aprimorar seu fluxo de trabalho de desenvolvimento! -- **O marketplace está agora disponível! O marketplace está agora disponível!** Descubra e instale modos e MCPs mais facilmente do que nunca. -- **Adicionado suporte para os novos modelos Gemini 2.5 Pro, Flash e Flash Lite.** -- **Suporte a Arquivos Excel e Mais** - Controles Mermaid aprimorados para melhor visualização de diagramas e novo suporte Amazon Bedrock thinking para interações de IA mais avançadas! +- **Compartilhamento de Tarefas com 1 Clique** - Compartilhe suas tarefas instantaneamente com colegas e a comunidade com um único clique. +- **Suporte a Diretório .roo Global** - Carregue regras e configurações de um diretório .roo global para configurações consistentes entre projetos. +- **Transições Aprimoradas de Arquiteto para Código** - Transferências perfeitas do planejamento no modo Arquiteto para implementação no modo Código. --- @@ -184,37 +184,39 @@ Obrigado a todos os nossos contribuidores que ajudaram a tornar o Roo Code melho |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Licença diff --git a/locales/ru/README.md b/locales/ru/README.md index 96db7bdb3f..35250c85ec 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -50,13 +50,13 @@ --- -## 🎉 Выпущен Roo Code 3.21 +## 🎉 Выпущен Roo Code 3.22 -Roo Code 3.21 представляет экспериментальный маркетплейс и улучшения файловых операций! +Roo Code 3.22 представляет мощные новые функции и значительные улучшения для повышения эффективности вашего рабочего процесса разработки! -- **Маркетплейс теперь доступен! Маркетплейс теперь доступен!** Открывайте и устанавливайте режимы и MCP проще, чем когда-либо. -- **Добавлена поддержка новых моделей Gemini 2.5 Pro, Flash и Flash Lite.** -- **Поддержка файлов Excel и многое другое!** - Новые элементы управления Mermaid и поддержка мышления Amazon Bedrock для расширенных возможностей MCP. +- **Обмен задачами в 1 клик** - Мгновенно делитесь своими задачами с коллегами и сообществом одним кликом. +- **Поддержка глобального каталога .roo** - Загружайте правила и конфигурации из глобального каталога .roo для согласованных настроек между проектами. +- **Улучшенные переходы от Архитектора к Коду** - Плавные передачи от планирования в режиме Архитектора к реализации в режиме Кода. --- @@ -184,37 +184,39 @@ code --install-extension bin/roo-cline-.vsix |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Лицензия diff --git a/locales/tr/README.md b/locales/tr/README.md index 6ff6f24c76..ae52f4aeec 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -50,13 +50,13 @@ Detaylı güncellemeler ve düzeltmeler için [CHANGELOG](../../CHANGELOG.md) do --- -## 🎉 Roo Code 3.21 Yayınlandı +## 🎉 Roo Code 3.22 Yayınlandı -Roo Code 3.21 deneysel pazar yeri ve gelişmiş dosya işlemleri sunuyor! +Roo Code 3.22 geliştirme iş akışınızı geliştirmek için güçlü yeni özellikler ve önemli iyileştirmeler getiriyor! -- **Pazar yeri artık canlı! Pazar yeri artık canlı!** Modları ve MCP'leri her zamankinden daha kolay keşfedin ve kurun. -- **Yeni Gemini 2.5 Pro, Flash ve Flash Lite modelleri için destek eklendi.** -- **Excel Dosya Desteği ve Daha Fazlası!** - Gelişmiş MCP yetenekleri için yeni Mermaid kontrolleri ve Amazon Bedrock düşünce desteği. +- **1-Tık Görev Paylaşımı** - Görevlerinizi tek tıkla meslektaşlarınız ve toplulukla anında paylaşın. +- **Global .roo Dizin Desteği** - Projeler arası tutarlı ayarlar için global .roo dizininden kuralları ve konfigürasyonları yükleyin. +- **Gelişmiş Mimar'dan Kod'a Geçişler** - Mimar modunda planlamadan Kod modunda uygulamaya sorunsuz aktarımlar. --- @@ -184,37 +184,39 @@ Roo Code'u daha iyi hale getirmeye yardımcı olan tüm katkıda bulunanlara te |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Lisans diff --git a/locales/vi/README.md b/locales/vi/README.md index 56d286cd98..9bc0a1a0a3 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -50,13 +50,13 @@ Kiểm tra [CHANGELOG](../../CHANGELOG.md) để biết thông tin chi tiết v --- -## 🎉 Đã Phát Hành Roo Code 3.21 +## 🎉 Đã Phát Hành Roo Code 3.22 -Roo Code 3.21 giới thiệu marketplace thử nghiệm và cải tiến các thao tác tập tin! +Roo Code 3.22 mang đến những tính năng mới mạnh mẽ và cải tiến đáng kể để nâng cao quy trình phát triển của bạn! -- **Marketplace hiện đã hoạt động! Marketplace hiện đã hoạt động!** Khám phá và cài đặt các chế độ và MCP dễ dàng hơn bao giờ hết. -- **Đã thêm hỗ trợ cho các mô hình Gemini 2.5 Pro, Flash và Flash Lite mới.** -- **Hỗ trợ tập tin Excel và nhiều hơn nữa!** - Các điều khiển Mermaid mới và hỗ trợ suy nghĩ Amazon Bedrock cho khả năng MCP nâng cao. +- **Chia Sẻ Tác Vụ 1-Cú Nhấp** - Chia sẻ tác vụ của bạn ngay lập tức với đồng nghiệp và cộng đồng chỉ bằng một cú nhấp. +- **Hỗ Trợ Thư Mục .roo Toàn Cục** - Tải quy tắc và cấu hình từ thư mục .roo toàn cục để có cài đặt nhất quán giữa các dự án. +- **Cải Thiện Chuyển Đổi từ Architect sang Code** - Chuyển giao liền mạch từ lập kế hoạch trong chế độ Architect sang triển khai trong chế độ Code. --- @@ -184,37 +184,39 @@ Cảm ơn tất cả những người đóng góp đã giúp cải thiện Roo C |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## Giấy Phép diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index ad89772d36..c8e06ba65b 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -50,13 +50,13 @@ --- -## 🎉 Roo Code 3.21 已发布 +## 🎉 Roo Code 3.22 已发布 -Roo Code 3.21 根据您的反馈带来重要的新功能和改进! +Roo Code 3.22 带来强大的新功能和重大改进,提升您的开发工作流程! -- **市场现已上线!市场现已上线!** 从新市场发现和安装模式和 MCP 比以往更容易(在实验性设置中启用)。 -- **新增 Gemini 2.5 Pro、Flash 和 Flash Lite 模型支持。** 多个并发文件写入现在在实验性设置中可用,多个并发读取已从实验性功能毕业,现在位于上下文设置中。 -- **Excel 文件支持及更多功能!** - 增强的 MCP 支持、更多 Mermaid 控件、Amazon Bedrock 中的思考支持,以及更多功能! +- **一键任务分享** - 一键即可与同事和社区分享您的任务。 +- **全局 .roo 目录支持** - 从全局 .roo 目录加载规则和配置,确保项目间设置一致。 +- **改进的架构师到代码模式转换** - 从架构师模式的规划到代码模式的实现,实现无缝交接。 --- @@ -184,37 +184,39 @@ code --install-extension bin/roo-cline-.vsix |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## 许可证 diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 245c66205a..770d0303a5 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -51,13 +51,13 @@ --- -## 🎉 Roo Code 3.21 已發布 +## 🎉 Roo Code 3.22 已發布 -Roo Code 3.21 推出實驗性市集和檔案操作改進! +Roo Code 3.22 帶來強大的新功能和重大改進,以提升您的開發工作流程! -- **市集現已上線!市集現已上線!** 從新市集探索並安裝模式和 MCP 比以往更容易(在實驗性設定中啟用)。 -- **新增 Gemini 2.5 Pro、Flash 和 Flash Lite 模型支援。** 多重同時檔案寫入現在可在實驗性設定中使用,多重同時讀取已移至上下文設定。 -- **Excel 檔案支援及更多功能!** - 新的 Mermaid 控制項和 Amazon Bedrock 思考支援,提供增強的 MCP 功能。 +- **一鍵任務分享** - 只需一鍵即可立即與同事和社群分享您的任務。 +- **全域 .roo 目錄支援** - 從全域 .roo 目錄載入規則和設定,確保專案間設定的一致性。 +- **改進的架構師到程式碼轉換** - 從架構師模式的規劃到程式碼模式的實作,實現無縫交接。 --- @@ -185,37 +185,39 @@ code --install-extension bin/roo-cline-.vsix |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
    |jr
    jr
    |NyxJae
    NyxJae
    |MuriloFP
    MuriloFP
    | -|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |sachasayan
    sachasayan
    | -|Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |cannuri
    cannuri
    |feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |qdaxb
    qdaxb
    | -|shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |dtrugman
    dtrugman
    |lloydchang
    lloydchang
    |vigneshsubbiah16
    vigneshsubbiah16
    |Szpadel
    Szpadel
    | -|lupuletic
    lupuletic
    |kiwina
    kiwina
    |Premshay
    Premshay
    |psv2522
    psv2522
    |olweraltuve
    olweraltuve
    |diarmidmackenzie
    diarmidmackenzie
    | -|chrarnoldus
    chrarnoldus
    |PeterDaveHello
    PeterDaveHello
    |aheizi
    aheizi
    |afshawnlotfi
    afshawnlotfi
    |RaySinner
    RaySinner
    |nbihan-mediware
    nbihan-mediware
    | -|ChuKhaLi
    ChuKhaLi
    |hassoncs
    hassoncs
    |emshvac
    emshvac
    |kyle-apex
    kyle-apex
    |noritaka1166
    noritaka1166
    |pdecat
    pdecat
    | -|SannidhyaSah
    SannidhyaSah
    |StevenTCramer
    StevenTCramer
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    |slytechnical
    slytechnical
    | -|dleffel
    dleffel
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | -|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |benzntech
    benzntech
    |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
    |axkirillov
    axkirillov
    |anton-otee
    anton-otee
    |bramburn
    bramburn
    |olearycrew
    olearycrew
    | -|brunobergher
    brunobergher
    |snoyiatk
    snoyiatk
    |GitlyHallows
    GitlyHallows
    |jcbdev
    jcbdev
    |Chenjiayuan195
    Chenjiayuan195
    |julionav
    julionav
    | -|SplittyDev
    SplittyDev
    |mdp
    mdp
    |napter
    napter
    |ross
    ross
    |philfung
    philfung
    |dairui1
    dairui1
    | -|dqroid
    dqroid
    |forestyoo
    forestyoo
    |GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    | -|shoopapa
    shoopapa
    |jwcraig
    jwcraig
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    | -|amittell
    amittell
    |Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    | -|Githubguy132010
    Githubguy132010
    |tgfjt
    tgfjt
    |maekawataiki
    maekawataiki
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    | -|user202729
    user202729
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shaybc
    shaybc
    |seedlord
    seedlord
    |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
    |kohii
    kohii
    |kinandan
    kinandan
    |linegel
    linegel
    |edwin-truthsearch-io
    edwin-truthsearch-io
    | -|EamonNerbonne
    EamonNerbonne
    |dbasclpy
    dbasclpy
    |dflatline
    dflatline
    |Deon588
    Deon588
    |dleen
    dleen
    |devxpain
    devxpain
    | +|System233
    System233
    |jr
    jr
    |MuriloFP
    MuriloFP
    |nissa-seru
    nissa-seru
    |jquanton
    jquanton
    |NyxJae
    NyxJae
    | +|elianiva
    elianiva
    |d-oit
    d-oit
    |punkpeye
    punkpeye
    |wkordalski
    wkordalski
    |xyOz-dev
    xyOz-dev
    |qdaxb
    qdaxb
    | +|feifei325
    feifei325
    |zhangtony239
    zhangtony239
    |Smartsheet-JB-Brown
    Smartsheet-JB-Brown
    |monotykamary
    monotykamary
    |sachasayan
    sachasayan
    |cannuri
    cannuri
    | +|vigneshsubbiah16
    vigneshsubbiah16
    |shariqriazz
    shariqriazz
    |pugazhendhi-m
    pugazhendhi-m
    |lloydchang
    lloydchang
    |dtrugman
    dtrugman
    |chrarnoldus
    chrarnoldus
    | +|Szpadel
    Szpadel
    |diarmidmackenzie
    diarmidmackenzie
    |olweraltuve
    olweraltuve
    |psv2522
    psv2522
    |Premshay
    Premshay
    |kiwina
    kiwina
    | +|lupuletic
    lupuletic
    |aheizi
    aheizi
    |SannidhyaSah
    SannidhyaSah
    |PeterDaveHello
    PeterDaveHello
    |hassoncs
    hassoncs
    |ChuKhaLi
    ChuKhaLi
    | +|nbihan-mediware
    nbihan-mediware
    |RaySinner
    RaySinner
    |afshawnlotfi
    afshawnlotfi
    |dleffel
    dleffel
    |StevenTCramer
    StevenTCramer
    |pdecat
    pdecat
    | +|noritaka1166
    noritaka1166
    |kyle-apex
    kyle-apex
    |emshvac
    emshvac
    |Lunchb0ne
    Lunchb0ne
    |SmartManoj
    SmartManoj
    |vagadiya
    vagadiya
    | +|slytechnical
    slytechnical
    |arthurauffray
    arthurauffray
    |upamune
    upamune
    |NamesMT
    NamesMT
    |taylorwilsdon
    taylorwilsdon
    |sammcj
    sammcj
    | +|Ruakij
    Ruakij
    |p12tic
    p12tic
    |gtaylor
    gtaylor
    |aitoroses
    aitoroses
    |axkirillov
    axkirillov
    |ross
    ross
    | +|mr-ryan-james
    mr-ryan-james
    |heyseth
    heyseth
    |taisukeoe
    taisukeoe
    |liwilliam2021
    liwilliam2021
    |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
    |benzntech
    benzntech
    |anton-otee
    anton-otee
    | +|bramburn
    bramburn
    |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
    |dairui1
    dairui1
    |dqroid
    dqroid
    |forestyoo
    forestyoo
    | +|GOODBOY008
    GOODBOY008
    |hatsu38
    hatsu38
    |hongzio
    hongzio
    |im47cn
    im47cn
    |shoopapa
    shoopapa
    |jwcraig
    jwcraig
    | +|kinandan
    kinandan
    |nevermorec
    nevermorec
    |bannzai
    bannzai
    |axmo
    axmo
    |asychin
    asychin
    |amittell
    amittell
    | +|Yoshino-Yukitaro
    Yoshino-Yukitaro
    |Yikai-Liao
    Yikai-Liao
    |zxdvd
    zxdvd
    |vladstudio
    vladstudio
    |tmsjngx0
    tmsjngx0
    |tgfjt
    tgfjt
    | +|maekawataiki
    maekawataiki
    |AlexandruSmirnov
    AlexandruSmirnov
    |PretzelVector
    PretzelVector
    |zetaloop
    zetaloop
    |cdlliuy
    cdlliuy
    |user202729
    user202729
    | +|takakoutso
    takakoutso
    |student20880
    student20880
    |shohei-ihaya
    shohei-ihaya
    |shivamd1810
    shivamd1810
    |shaybc
    shaybc
    |seedlord
    seedlord
    | +|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
    |kohii
    kohii
    |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
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    |alarno
    alarno
    | -|nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    |AlexandruSmirnov
    AlexandruSmirnov
    | +|atlasgong
    atlasgong
    |andrewshu2000
    andrewshu2000
    |andreastempsch
    andreastempsch
    |alasano
    alasano
    |QuinsZouls
    QuinsZouls
    |HadesArchitect
    HadesArchitect
    | +|alarno
    alarno
    |nexon33
    nexon33
    |adilhafeez
    adilhafeez
    |adamwlarson
    adamwlarson
    |adamhill
    adamhill
    |AMHesch
    AMHesch
    | |samsilveira
    samsilveira
    |01Rian
    01Rian
    |RSO
    RSO
    |SECKainersdorfer
    SECKainersdorfer
    |R-omk
    R-omk
    |Sarke
    Sarke
    | -|OlegOAndreev
    OlegOAndreev
    |kvokka
    kvokka
    |ecmasx
    ecmasx
    |mollux
    mollux
    |marvijo-code
    marvijo-code
    |markijbema
    markijbema
    | -|mamertofabian
    mamertofabian
    |monkeyDluffy6017
    monkeyDluffy6017
    |libertyteeth
    libertyteeth
    |shtse8
    shtse8
    |Rexarrior
    Rexarrior
    |KanTakahiro
    KanTakahiro
    | -|ksze
    ksze
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    |pfitz
    pfitz
    |celestial-vault
    celestial-vault
    | +|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
    | +|KevinZhao
    KevinZhao
    |ksze
    ksze
    |Fovty
    Fovty
    |Jdo300
    Jdo300
    |hesara
    hesara
    |DeXtroTip
    DeXtroTip
    | +|pfitz
    pfitz
    |ExactDoug
    ExactDoug
    | | | | | ## 授權 diff --git a/package.json b/package.json index 0741895029..61f1f6cdaf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "vsix": "turbo vsix --log-order grouped --output-logs new-only", "vsix:nightly": "turbo vsix:nightly --log-order grouped --output-logs new-only", "clean": "turbo clean --log-order grouped --output-logs new-only && rimraf dist out bin .vite-port .turbo", + "install:vsix": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix && node scripts/install-vsix.js", "changeset:version": "cp CHANGELOG.md src/CHANGELOG.md && changeset version && cp -vf src/CHANGELOG.md .", "knip": "knip --include files", "update-contributors": "node scripts/update-contributors.js", diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 12aafee9dc..f48062a1a3 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -10,7 +10,8 @@ import type { import { TelemetryService } from "@roo-code/telemetry" import { CloudServiceCallbacks } from "./types" -import { AuthService } from "./AuthService" +import type { AuthService } from "./auth" +import { WebAuthService, StaticTokenAuthService } from "./auth" import { SettingsService } from "./SettingsService" import { TelemetryClient } from "./TelemetryClient" import { ShareService, TaskNotFoundError } from "./ShareService" @@ -43,7 +44,13 @@ export class CloudService { } try { - this.authService = new AuthService(this.context, this.log) + const cloudToken = process.env.ROO_CODE_CLOUD_TOKEN + if (cloudToken && cloudToken.length > 0) { + this.authService = new StaticTokenAuthService(this.context, cloudToken, this.log) + } else { + this.authService = new WebAuthService(this.context, this.log) + } + await this.authService.initialize() this.authService.on("attempting-session", this.authListener) diff --git a/packages/cloud/src/SettingsService.ts b/packages/cloud/src/SettingsService.ts index 68c6f2fe48..f4c36e45b5 100644 --- a/packages/cloud/src/SettingsService.ts +++ b/packages/cloud/src/SettingsService.ts @@ -8,7 +8,7 @@ import { } from "@roo-code/types" import { getRooCodeApiUrl } from "./Config" -import { AuthService } from "./AuthService" +import type { AuthService } from "./auth" import { RefreshTimer } from "./RefreshTimer" const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings" diff --git a/packages/cloud/src/ShareService.ts b/packages/cloud/src/ShareService.ts index 07176d3e9d..5dcc7cae3f 100644 --- a/packages/cloud/src/ShareService.ts +++ b/packages/cloud/src/ShareService.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode" import { shareResponseSchema } from "@roo-code/types" import { getRooCodeApiUrl } from "./Config" -import type { AuthService } from "./AuthService" +import type { AuthService } from "./auth" import type { SettingsService } from "./SettingsService" import { getUserAgent } from "./utils" diff --git a/packages/cloud/src/TelemetryClient.ts b/packages/cloud/src/TelemetryClient.ts index ea48fcf269..de37ea503b 100644 --- a/packages/cloud/src/TelemetryClient.ts +++ b/packages/cloud/src/TelemetryClient.ts @@ -7,7 +7,7 @@ import { import { BaseTelemetryClient } from "@roo-code/telemetry" import { getRooCodeApiUrl } from "./Config" -import { AuthService } from "./AuthService" +import type { AuthService } from "./auth" import { SettingsService } from "./SettingsService" export class TelemetryClient extends BaseTelemetryClient { diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts index 6ed8c9741c..5320a51864 100644 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ b/packages/cloud/src/__tests__/CloudService.test.ts @@ -4,7 +4,7 @@ import * as vscode from "vscode" import type { ClineMessage } from "@roo-code/types" import { CloudService } from "../CloudService" -import { AuthService } from "../AuthService" +import { WebAuthService } from "../auth/WebAuthService" import { SettingsService } from "../SettingsService" import { ShareService, TaskNotFoundError } from "../ShareService" import { TelemetryClient } from "../TelemetryClient" @@ -27,7 +27,7 @@ vi.mock("vscode", () => ({ vi.mock("@roo-code/telemetry") -vi.mock("../AuthService") +vi.mock("../auth/WebAuthService") vi.mock("../SettingsService") @@ -149,7 +149,7 @@ describe("CloudService", () => { }, } - vi.mocked(AuthService).mockImplementation(() => mockAuthService as unknown as AuthService) + vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService) vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService) vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService) vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient) @@ -175,7 +175,7 @@ describe("CloudService", () => { const cloudService = await CloudService.createInstance(mockContext, callbacks) expect(cloudService).toBeInstanceOf(CloudService) - expect(AuthService).toHaveBeenCalledWith(mockContext, expect.any(Function)) + expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function)) expect(SettingsService).toHaveBeenCalledWith( mockContext, mockAuthService, diff --git a/packages/cloud/src/__tests__/ShareService.test.ts b/packages/cloud/src/__tests__/ShareService.test.ts index dd2e5f1ae5..dd5b669603 100644 --- a/packages/cloud/src/__tests__/ShareService.test.ts +++ b/packages/cloud/src/__tests__/ShareService.test.ts @@ -4,7 +4,7 @@ import type { MockedFunction } from "vitest" import * as vscode from "vscode" import { ShareService, TaskNotFoundError } from "../ShareService" -import type { AuthService } from "../AuthService" +import type { AuthService } from "../auth" import type { SettingsService } from "../SettingsService" // Mock fetch diff --git a/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts b/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts new file mode 100644 index 0000000000..cbf3a7b998 --- /dev/null +++ b/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts @@ -0,0 +1,174 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import * as vscode from "vscode" + +import { StaticTokenAuthService } from "../../auth/StaticTokenAuthService" + +// Mock vscode +vi.mock("vscode", () => ({ + window: { + showInformationMessage: vi.fn(), + }, + env: { + openExternal: vi.fn(), + uriScheme: "vscode", + }, + Uri: { + parse: vi.fn(), + }, +})) + +describe("StaticTokenAuthService", () => { + let authService: StaticTokenAuthService + let mockContext: vscode.ExtensionContext + let mockLog: (...args: unknown[]) => void + const testToken = "test-static-token" + + beforeEach(() => { + mockLog = vi.fn() + + // Create a minimal mock that satisfies the constructor requirements + const mockContextPartial = { + extension: { + packageJSON: { + publisher: "TestPublisher", + name: "test-extension", + }, + }, + globalState: { + get: vi.fn(), + update: vi.fn(), + }, + secrets: { + get: vi.fn(), + store: vi.fn(), + delete: vi.fn(), + onDidChange: vi.fn(), + }, + subscriptions: [], + } + + // Use type assertion for test mocking + mockContext = mockContextPartial as unknown as vscode.ExtensionContext + + authService = new StaticTokenAuthService(mockContext, testToken, mockLog) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe("constructor", () => { + it("should create instance and log static token mode", () => { + expect(authService).toBeInstanceOf(StaticTokenAuthService) + expect(mockLog).toHaveBeenCalledWith("[auth] Using static token authentication mode") + }) + + it("should use console.log as default logger", () => { + const serviceWithoutLog = new StaticTokenAuthService( + mockContext as unknown as vscode.ExtensionContext, + testToken, + ) + // Can't directly test console.log usage, but constructor should not throw + expect(serviceWithoutLog).toBeInstanceOf(StaticTokenAuthService) + }) + }) + + describe("initialize", () => { + it("should start in active-session state", async () => { + await authService.initialize() + expect(authService.getState()).toBe("active-session") + }) + + it("should emit active-session event on initialize", async () => { + const spy = vi.fn() + authService.on("active-session", spy) + + await authService.initialize() + + expect(spy).toHaveBeenCalledWith({ previousState: "initializing" }) + }) + + it("should log successful initialization", async () => { + await authService.initialize() + expect(mockLog).toHaveBeenCalledWith("[auth] Static token auth service initialized in active-session state") + }) + }) + + describe("getSessionToken", () => { + it("should return the provided token", () => { + expect(authService.getSessionToken()).toBe(testToken) + }) + + it("should return different token when constructed with different token", () => { + const differentToken = "different-token" + const differentService = new StaticTokenAuthService(mockContext, differentToken, mockLog) + expect(differentService.getSessionToken()).toBe(differentToken) + }) + }) + + describe("getUserInfo", () => { + it("should return empty object", () => { + expect(authService.getUserInfo()).toEqual({}) + }) + }) + + describe("getStoredOrganizationId", () => { + it("should return null", () => { + expect(authService.getStoredOrganizationId()).toBeNull() + }) + }) + + describe("authentication state methods", () => { + it("should always return true for isAuthenticated", () => { + expect(authService.isAuthenticated()).toBe(true) + }) + + it("should always return true for hasActiveSession", () => { + expect(authService.hasActiveSession()).toBe(true) + }) + + it("should always return true for hasOrIsAcquiringActiveSession", () => { + expect(authService.hasOrIsAcquiringActiveSession()).toBe(true) + }) + + it("should return active-session for getState", () => { + expect(authService.getState()).toBe("active-session") + }) + }) + + describe("disabled authentication methods", () => { + const expectedErrorMessage = "Authentication methods are disabled in StaticTokenAuthService" + + it("should throw error for login", async () => { + await expect(authService.login()).rejects.toThrow(expectedErrorMessage) + }) + + it("should throw error for logout", async () => { + await expect(authService.logout()).rejects.toThrow(expectedErrorMessage) + }) + + it("should throw error for handleCallback", async () => { + await expect(authService.handleCallback("code", "state")).rejects.toThrow(expectedErrorMessage) + }) + + it("should throw error for handleCallback with organization", async () => { + await expect(authService.handleCallback("code", "state", "org_123")).rejects.toThrow(expectedErrorMessage) + }) + }) + + describe("event emission", () => { + it("should be able to register and emit events", async () => { + const activeSessionSpy = vi.fn() + const userInfoSpy = vi.fn() + + authService.on("active-session", activeSessionSpy) + authService.on("user-info", userInfoSpy) + + await authService.initialize() + + expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" }) + // user-info event is not emitted in static token mode + expect(userInfoSpy).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/cloud/src/__tests__/AuthService.spec.ts b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts similarity index 95% rename from packages/cloud/src/__tests__/AuthService.spec.ts rename to packages/cloud/src/__tests__/auth/WebAuthService.spec.ts index 944bcd2b24..0e6681c20b 100644 --- a/packages/cloud/src/__tests__/AuthService.spec.ts +++ b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts @@ -4,15 +4,15 @@ import { vi, Mock, beforeEach, afterEach, describe, it, expect } from "vitest" import crypto from "crypto" import * as vscode from "vscode" -import { AuthService } from "../AuthService" -import { RefreshTimer } from "../RefreshTimer" -import * as Config from "../Config" -import * as utils from "../utils" +import { WebAuthService } from "../../auth/WebAuthService" +import { RefreshTimer } from "../../RefreshTimer" +import * as Config from "../../Config" +import * as utils from "../../utils" // Mock external dependencies -vi.mock("../RefreshTimer") -vi.mock("../Config") -vi.mock("../utils") +vi.mock("../../RefreshTimer") +vi.mock("../../Config") +vi.mock("../../utils") vi.mock("crypto") // Mock fetch globally @@ -34,8 +34,8 @@ vi.mock("vscode", () => ({ }, })) -describe("AuthService", () => { - let authService: AuthService +describe("WebAuthService", () => { + let authService: WebAuthService let mockTimer: { start: Mock stop: Mock @@ -97,7 +97,8 @@ describe("AuthService", () => { stop: vi.fn(), reset: vi.fn(), } - vi.mocked(RefreshTimer).mockImplementation(() => mockTimer as unknown as RefreshTimer) + const MockedRefreshTimer = vi.mocked(RefreshTimer) + MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer) // Setup config mocks - use production URL by default to maintain existing test behavior vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") @@ -112,7 +113,7 @@ describe("AuthService", () => { // Setup log mock mockLog = vi.fn() - authService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + authService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) }) afterEach(() => { @@ -138,9 +139,9 @@ describe("AuthService", () => { }) it("should use console.log as default logger", () => { - const serviceWithoutLog = new AuthService(mockContext as unknown as vscode.ExtensionContext) + const serviceWithoutLog = new WebAuthService(mockContext as unknown as vscode.ExtensionContext) // Can't directly test console.log usage, but constructor should not throw - expect(serviceWithoutLog).toBeInstanceOf(AuthService) + expect(serviceWithoutLog).toBeInstanceOf(WebAuthService) }) }) @@ -434,7 +435,7 @@ describe("AuthService", () => { const credentials = { clientToken: "test-token", sessionId: "test-session" } mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - const authenticatedService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const authenticatedService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) await authenticatedService.initialize() expect(authenticatedService.isAuthenticated()).toBe(true) @@ -460,7 +461,7 @@ describe("AuthService", () => { const credentials = { clientToken: "test-token", sessionId: "test-session" } mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - const attemptingService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const attemptingService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) await attemptingService.initialize() expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true) @@ -960,7 +961,7 @@ describe("AuthService", () => { // Mock getClerkBaseUrl to return production URL vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") - const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) const credentials = { clientToken: "test-token", sessionId: "test-session" } await service.initialize() @@ -977,7 +978,7 @@ describe("AuthService", () => { // Mock getClerkBaseUrl to return custom URL vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) const credentials = { clientToken: "test-token", sessionId: "test-session" } await service.initialize() @@ -993,7 +994,7 @@ describe("AuthService", () => { const customUrl = "https://custom.clerk.com" vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) const credentials = { clientToken: "test-token", sessionId: "test-session" } mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) @@ -1008,7 +1009,7 @@ describe("AuthService", () => { const customUrl = "https://custom.clerk.com" vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) - const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) await service.initialize() await service["clearCredentials"]() @@ -1027,7 +1028,7 @@ describe("AuthService", () => { return { dispose: vi.fn() } }) - const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) await service.initialize() // Simulate credentials change event with scoped key @@ -1054,7 +1055,7 @@ describe("AuthService", () => { return { dispose: vi.fn() } }) - const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) await service.initialize() const inactiveSessionSpy = vi.fn() @@ -1078,7 +1079,7 @@ describe("AuthService", () => { return { dispose: vi.fn() } }) - const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) + const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) await service.initialize() const inactiveSessionSpy = vi.fn() diff --git a/packages/cloud/src/auth/AuthService.ts b/packages/cloud/src/auth/AuthService.ts new file mode 100644 index 0000000000..11ed5161ed --- /dev/null +++ b/packages/cloud/src/auth/AuthService.ts @@ -0,0 +1,33 @@ +import EventEmitter from "events" +import type { CloudUserInfo } from "@roo-code/types" + +export interface AuthServiceEvents { + "attempting-session": [data: { previousState: AuthState }] + "inactive-session": [data: { previousState: AuthState }] + "active-session": [data: { previousState: AuthState }] + "logged-out": [data: { previousState: AuthState }] + "user-info": [data: { userInfo: CloudUserInfo }] +} + +export type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session" + +export interface AuthService extends EventEmitter { + // Lifecycle + initialize(): Promise + + // Authentication methods + login(): Promise + logout(): Promise + handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise + + // State methods + getState(): AuthState + isAuthenticated(): boolean + hasActiveSession(): boolean + hasOrIsAcquiringActiveSession(): boolean + + // Token and user info + getSessionToken(): string | undefined + getUserInfo(): CloudUserInfo | null + getStoredOrganizationId(): string | null +} diff --git a/packages/cloud/src/auth/StaticTokenAuthService.ts b/packages/cloud/src/auth/StaticTokenAuthService.ts new file mode 100644 index 0000000000..11fc18d3fb --- /dev/null +++ b/packages/cloud/src/auth/StaticTokenAuthService.ts @@ -0,0 +1,68 @@ +import EventEmitter from "events" +import * as vscode from "vscode" +import type { CloudUserInfo } from "@roo-code/types" +import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" + +export class StaticTokenAuthService extends EventEmitter implements AuthService { + private state: AuthState = "active-session" + private token: string + private log: (...args: unknown[]) => void + + constructor(context: vscode.ExtensionContext, token: string, log?: (...args: unknown[]) => void) { + super() + this.token = token + this.log = log || console.log + this.log("[auth] Using static token authentication mode") + } + + public async initialize(): Promise { + const previousState: AuthState = "initializing" + this.state = "active-session" + this.emit("active-session", { previousState }) + this.log("[auth] Static token auth service initialized in active-session state") + } + + public async login(): Promise { + throw new Error("Authentication methods are disabled in StaticTokenAuthService") + } + + public async logout(): Promise { + throw new Error("Authentication methods are disabled in StaticTokenAuthService") + } + + public async handleCallback( + _code: string | null, + _state: string | null, + _organizationId?: string | null, + ): Promise { + throw new Error("Authentication methods are disabled in StaticTokenAuthService") + } + + public getState(): AuthState { + return this.state + } + + public getSessionToken(): string | undefined { + return this.token + } + + public isAuthenticated(): boolean { + return true + } + + public hasActiveSession(): boolean { + return true + } + + public hasOrIsAcquiringActiveSession(): boolean { + return true + } + + public getUserInfo(): CloudUserInfo | null { + return {} + } + + public getStoredOrganizationId(): string | null { + return null + } +} diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/auth/WebAuthService.ts similarity index 96% rename from packages/cloud/src/AuthService.ts rename to packages/cloud/src/auth/WebAuthService.ts index cd8e1362c1..d14cbe67d8 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/auth/WebAuthService.ts @@ -6,17 +6,10 @@ import { z } from "zod" import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types" -import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "./Config" -import { RefreshTimer } from "./RefreshTimer" -import { getUserAgent } from "./utils" - -export interface AuthServiceEvents { - "attempting-session": [data: { previousState: AuthState }] - "inactive-session": [data: { previousState: AuthState }] - "active-session": [data: { previousState: AuthState }] - "logged-out": [data: { previousState: AuthState }] - "user-info": [data: { userInfo: CloudUserInfo }] -} +import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "../Config" +import { RefreshTimer } from "../RefreshTimer" +import { getUserAgent } from "../utils" +import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" const authCredentialsSchema = z.object({ clientToken: z.string().min(1, "Client token cannot be empty"), @@ -28,8 +21,6 @@ type AuthCredentials = z.infer const AUTH_STATE_KEY = "clerk-auth-state" -type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session" - const clerkSignInResponseSchema = z.object({ response: z.object({ created_session_id: z.string(), @@ -85,7 +76,7 @@ class InvalidClientTokenError extends Error { } } -export class AuthService extends EventEmitter { +export class WebAuthService extends EventEmitter implements AuthService { private context: vscode.ExtensionContext private timer: RefreshTimer private state: AuthState = "initializing" diff --git a/packages/cloud/src/auth/index.ts b/packages/cloud/src/auth/index.ts new file mode 100644 index 0000000000..b04a805295 --- /dev/null +++ b/packages/cloud/src/auth/index.ts @@ -0,0 +1,3 @@ +export type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" +export { WebAuthService } from "./WebAuthService" +export { StaticTokenAuthService } from "./StaticTokenAuthService" diff --git a/packages/telemetry/src/BaseTelemetryClient.ts b/packages/telemetry/src/BaseTelemetryClient.ts index ab8ab56f59..2eb308b414 100644 --- a/packages/telemetry/src/BaseTelemetryClient.ts +++ b/packages/telemetry/src/BaseTelemetryClient.ts @@ -25,13 +25,21 @@ 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 the telemetry properties directly from the provider. + // Get properties from the provider providerProperties = await provider.getTelemetryProperties() } catch (error) { // Log error but continue with capturing the event. @@ -43,7 +51,10 @@ export abstract class BaseTelemetryClient implements TelemetryClient { // Merge provider properties with event-specific properties. // Event properties take precedence in case of conflicts. - return { ...providerProperties, ...(event.properties || {}) } + 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))) } public abstract capture(event: TelemetryEvent): Promise diff --git a/packages/telemetry/src/PostHogTelemetryClient.ts b/packages/telemetry/src/PostHogTelemetryClient.ts index 243176ed45..f1c46577df 100644 --- a/packages/telemetry/src/PostHogTelemetryClient.ts +++ b/packages/telemetry/src/PostHogTelemetryClient.ts @@ -13,6 +13,8 @@ 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( @@ -26,6 +28,19 @@ 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/__tests__/PostHogTelemetryClient.test.ts b/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts index c94dbdb734..282d1d6c6a 100644 --- a/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts +++ b/packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts @@ -70,6 +70,29 @@ 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() @@ -112,6 +135,54 @@ 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() @@ -211,6 +282,48 @@ 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/src/codebase-index.ts b/packages/types/src/codebase-index.ts index e86c17627f..7263068415 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -10,6 +10,7 @@ export const codebaseIndexConfigSchema = z.object({ codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible"]).optional(), codebaseIndexEmbedderBaseUrl: z.string().optional(), codebaseIndexEmbedderModelId: z.string().optional(), + codebaseIndexSearchMinScore: z.number().min(0).max(1).optional(), }) export type CodebaseIndexConfig = z.infer diff --git a/packages/types/src/followup.ts b/packages/types/src/followup.ts new file mode 100644 index 0000000000..1a5424cd11 --- /dev/null +++ b/packages/types/src/followup.ts @@ -0,0 +1,41 @@ +import { z } from "zod" + +/** + * Interface for follow-up data structure used in follow-up questions + * This represents the data structure for follow-up questions that the LLM can ask + * to gather more information needed to complete a task. + */ +export interface FollowUpData { + /** The question being asked by the LLM */ + question?: string + /** Array of suggested answers that the user can select */ + suggest?: Array +} + +/** + * Interface for a suggestion item with optional mode switching + */ +export interface SuggestionItem { + /** The text of the suggestion */ + answer: string + /** Optional mode to switch to when selecting this suggestion */ + mode?: string +} + +/** + * Zod schema for SuggestionItem + */ +export const suggestionItemSchema = z.object({ + answer: z.string(), + mode: z.string().optional(), +}) + +/** + * Zod schema for FollowUpData + */ +export const followUpDataSchema = z.object({ + question: z.string().optional(), + suggest: z.array(suggestionItemSchema).optional(), +}) + +export type FollowUpDataType = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index e713cafa4c..e6d9a1eed5 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -45,6 +45,8 @@ export const globalSettingsSchema = z.object({ alwaysAllowModeSwitch: z.boolean().optional(), alwaysAllowSubtasks: z.boolean().optional(), alwaysAllowExecute: z.boolean().optional(), + alwaysAllowFollowupQuestions: z.boolean().optional(), + followupAutoApproveTimeoutMs: z.number().optional(), allowedCommands: z.array(z.string()).optional(), allowedMaxRequests: z.number().nullish(), autoCondenseContext: z.boolean().optional(), @@ -105,6 +107,8 @@ export const globalSettingsSchema = z.object({ 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 @@ -189,6 +193,8 @@ export const EVALS_SETTINGS: RooCodeSettings = { alwaysAllowModeSwitch: true, alwaysAllowSubtasks: true, alwaysAllowExecute: true, + alwaysAllowFollowupQuestions: true, + followupAutoApproveTimeoutMs: 0, allowedCommands: ["*"], browserToolEnabled: false, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 345bc3e311..6c6db9ccd5 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -4,6 +4,7 @@ export * from "./api.js" export * from "./codebase-index.js" export * from "./cloud.js" export * from "./experiment.js" +export * from "./followup.js" export * from "./global-settings.js" export * from "./history.js" export * from "./ipc.js" diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 609d0fefbf..e940ececd1 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -19,6 +19,7 @@ export const providerNames = [ "vscode-lm", "lmstudio", "gemini", + "gemini-cli", "openai-native", "mistral", "deepseek", @@ -158,6 +159,11 @@ 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(), @@ -223,6 +229,7 @@ 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") })), @@ -250,6 +257,7 @@ export const providerSettingsSchema = z.object({ ...vsCodeLmSchema.shape, ...lmStudioSchema.shape, ...geminiSchema.shape, + ...geminiCliSchema.shape, ...openAiNativeSchema.shape, ...mistralSchema.shape, ...deepSeekSchema.shape, diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index a15f041252..58e860dd94 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -360,78 +360,49 @@ export const BEDROCK_MAX_TOKENS = 4096 export const BEDROCK_DEFAULT_CONTEXT = 128_000 -export const BEDROCK_REGION_INFO: Record< - string, - { - regionId: string - description: string - pattern?: string - multiRegion?: boolean - } -> = { - /* - * This JSON generated by AWS's AI assistant - Amazon Q on March 29, 2025 - * - * - Africa (Cape Town) region does not appear to support Amazon Bedrock at this time. - * - Some Asia Pacific regions, such as Asia Pacific (Hong Kong) and Asia Pacific (Jakarta), are not listed among the supported regions for Bedrock services. - * - Middle East regions, including Middle East (Bahrain) and Middle East (UAE), are not mentioned in the list of supported regions for Bedrock. [3] - * - China regions (Beijing and Ningxia) are not listed as supported for Amazon Bedrock. - * - Some newer or specialized AWS regions may not have Bedrock support yet. - */ - "us.": { regionId: "us-east-1", description: "US East (N. Virginia)", pattern: "us-", multiRegion: true }, - "use.": { regionId: "us-east-1", description: "US East (N. Virginia)" }, - "use1.": { regionId: "us-east-1", description: "US East (N. Virginia)" }, - "use2.": { regionId: "us-east-2", description: "US East (Ohio)" }, - "usw.": { regionId: "us-west-2", description: "US West (Oregon)" }, - "usw2.": { regionId: "us-west-2", description: "US West (Oregon)" }, - "ug.": { - regionId: "us-gov-west-1", - description: "AWS GovCloud (US-West)", - pattern: "us-gov-", - multiRegion: true, - }, - "uge1.": { regionId: "us-gov-east-1", description: "AWS GovCloud (US-East)" }, - "ugw1.": { regionId: "us-gov-west-1", description: "AWS GovCloud (US-West)" }, - "eu.": { regionId: "eu-west-1", description: "Europe (Ireland)", pattern: "eu-", multiRegion: true }, - "euw1.": { regionId: "eu-west-1", description: "Europe (Ireland)" }, - "euw2.": { regionId: "eu-west-2", description: "Europe (London)" }, - "euw3.": { regionId: "eu-west-3", description: "Europe (Paris)" }, - "euc1.": { regionId: "eu-central-1", description: "Europe (Frankfurt)" }, - "euc2.": { regionId: "eu-central-2", description: "Europe (Zurich)" }, - "eun1.": { regionId: "eu-north-1", description: "Europe (Stockholm)" }, - "eus1.": { regionId: "eu-south-1", description: "Europe (Milan)" }, - "eus2.": { regionId: "eu-south-2", description: "Europe (Spain)" }, - "ap.": { - regionId: "ap-southeast-1", - description: "Asia Pacific (Singapore)", - pattern: "ap-", - multiRegion: true, - }, - "ape1.": { regionId: "ap-east-1", description: "Asia Pacific (Hong Kong)" }, - "apne1.": { regionId: "ap-northeast-1", description: "Asia Pacific (Tokyo)" }, - "apne2.": { regionId: "ap-northeast-2", description: "Asia Pacific (Seoul)" }, - "apne3.": { regionId: "ap-northeast-3", description: "Asia Pacific (Osaka)" }, - "aps1.": { regionId: "ap-south-1", description: "Asia Pacific (Mumbai)" }, - "aps2.": { regionId: "ap-south-2", description: "Asia Pacific (Hyderabad)" }, - "apse1.": { regionId: "ap-southeast-1", description: "Asia Pacific (Singapore)" }, - "apse2.": { regionId: "ap-southeast-2", description: "Asia Pacific (Sydney)" }, - "ca.": { regionId: "ca-central-1", description: "Canada (Central)", pattern: "ca-", multiRegion: true }, - "cac1.": { regionId: "ca-central-1", description: "Canada (Central)" }, - "sa.": { regionId: "sa-east-1", description: "South America (São Paulo)", pattern: "sa-", multiRegion: true }, - "sae1.": { regionId: "sa-east-1", description: "South America (São Paulo)" }, +// AWS Bedrock Inference Profile mapping based on official documentation +// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html +// This mapping is pre-ordered by pattern length (descending) to ensure more specific patterns match first +export const AWS_INFERENCE_PROFILE_MAPPING: Array<[string, string]> = [ + // US Government Cloud → ug. inference profile (most specific prefix first) + ["us-gov-", "ug."], + // Americas regions → us. inference profile + ["us-", "us."], + // Europe regions → eu. inference profile + ["eu-", "eu."], + // Asia Pacific regions → apac. inference profile + ["ap-", "apac."], + // Canada regions → ca. inference profile + ["ca-", "ca."], + // South America regions → sa. inference profile + ["sa-", "sa."], +] - // These are not official - they weren't generated by Amazon Q nor were - // found in the AWS documentation but another Roo contributor found apac. - // Was needed so I've added the pattern of the other geo zones. - "apac.": { regionId: "ap-southeast-1", description: "Default APAC region", pattern: "ap-", multiRegion: true }, - "emea.": { regionId: "eu-west-1", description: "Default EMEA region", pattern: "eu-", multiRegion: true }, - "amer.": { regionId: "us-east-1", description: "Default Americas region", pattern: "us-", multiRegion: true }, -} - -export const BEDROCK_REGIONS = Object.values(BEDROCK_REGION_INFO) - // Extract all region IDs - .map((info) => ({ value: info.regionId, label: info.regionId })) - // Filter to unique region IDs (remove duplicates) - .filter((region, index, self) => index === self.findIndex((r) => r.value === region.value)) - // Sort alphabetically by region ID - .sort((a, b) => a.value.localeCompare(b.value)) +// AWS Bedrock supported regions for the regions dropdown +// Based on official AWS documentation +export const BEDROCK_REGIONS = [ + { value: "us-east-1", label: "us-east-1" }, + { value: "us-east-2", label: "us-east-2" }, + { value: "us-west-1", label: "us-west-1" }, + { value: "us-west-2", label: "us-west-2" }, + { value: "ap-northeast-1", label: "ap-northeast-1" }, + { value: "ap-northeast-2", label: "ap-northeast-2" }, + { value: "ap-northeast-3", label: "ap-northeast-3" }, + { value: "ap-south-1", label: "ap-south-1" }, + { value: "ap-south-2", label: "ap-south-2" }, + { value: "ap-southeast-1", label: "ap-southeast-1" }, + { value: "ap-southeast-2", label: "ap-southeast-2" }, + { value: "ap-east-1", label: "ap-east-1" }, + { value: "eu-central-1", label: "eu-central-1" }, + { value: "eu-central-2", label: "eu-central-2" }, + { value: "eu-west-1", label: "eu-west-1" }, + { value: "eu-west-2", label: "eu-west-2" }, + { value: "eu-west-3", label: "eu-west-3" }, + { value: "eu-north-1", label: "eu-north-1" }, + { value: "eu-south-1", label: "eu-south-1" }, + { value: "eu-south-2", label: "eu-south-2" }, + { value: "ca-central-1", label: "ca-central-1" }, + { value: "sa-east-1", label: "sa-east-1" }, + { value: "us-gov-east-1", label: "us-gov-east-1" }, + { value: "us-gov-west-1", label: "us-gov-west-1" }, +].sort((a, b) => a.value.localeCompare(b.value)) diff --git a/packages/types/src/providers/claude-code.ts b/packages/types/src/providers/claude-code.ts index 6b58f074ce..d0fff0f2ee 100644 --- a/packages/types/src/providers/claude-code.ts +++ b/packages/types/src/providers/claude-code.ts @@ -8,26 +8,41 @@ export const claudeCodeModels = { "claude-sonnet-4-20250514": { ...anthropicModels["claude-sonnet-4-20250514"], supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, }, "claude-opus-4-20250514": { ...anthropicModels["claude-opus-4-20250514"], supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, }, "claude-3-7-sonnet-20250219": { ...anthropicModels["claude-3-7-sonnet-20250219"], supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, }, "claude-3-5-sonnet-20241022": { ...anthropicModels["claude-3-5-sonnet-20241022"], supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, }, "claude-3-5-haiku-20241022": { ...anthropicModels["claude-3-5-haiku-20241022"], supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, // Claude Code does report cache tokens + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, }, } as const satisfies Record diff --git a/packages/types/src/providers/groq.ts b/packages/types/src/providers/groq.ts index 1782a6a72a..49667e357e 100644 --- a/packages/types/src/providers/groq.ts +++ b/packages/types/src/providers/groq.ts @@ -70,7 +70,7 @@ export const groqModels = { description: "Alibaba Qwen QwQ 32B model, 128K context.", }, "qwen/qwen3-32b": { - maxTokens: 131072, + maxTokens: 40960, contextWindow: 131072, supportsImages: false, supportsPromptCache: false, diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 26f5ab894a..6ad2eb3a7a 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -25,6 +25,7 @@ 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", @@ -101,12 +102,7 @@ export const telemetryPropertiesSchema = z.object({ ...gitPropertiesSchema.shape, }) -export const cloudTelemetryPropertiesSchema = z.object({ - ...telemetryPropertiesSchema.shape, -}) - export type TelemetryProperties = z.infer -export type CloudTelemetryProperties = z.infer export type GitProperties = z.infer /** @@ -131,6 +127,7 @@ 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, @@ -161,12 +158,12 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.MODE_SETTINGS_CHANGED, TelemetryEventName.CUSTOM_MODE_CREATED, ]), - properties: cloudTelemetryPropertiesSchema, + properties: telemetryPropertiesSchema, }), z.object({ type: z.literal(TelemetryEventName.TASK_MESSAGE), properties: z.object({ - ...cloudTelemetryPropertiesSchema.shape, + ...telemetryPropertiesSchema.shape, taskId: z.string(), message: clineMessageSchema, }), @@ -174,7 +171,7 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(TelemetryEventName.LLM_COMPLETION), properties: z.object({ - ...cloudTelemetryPropertiesSchema.shape, + ...telemetryPropertiesSchema.shape, inputTokens: z.number(), outputTokens: z.number(), cacheReadTokens: z.number().optional(), @@ -200,7 +197,6 @@ export type TelemetryEventSubscription = export interface TelemetryPropertiesProvider { getTelemetryProperties(): Promise - getCloudTelemetryProperties?(): Promise } /** diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index e6640e9bb6..00f6bbbcba 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -48,6 +48,7 @@ export const commandIds = [ "newTask", "setCustomStoragePath", + "importSettings", "focusInput", "acceptInput", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bee914824..5030055fea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -699,6 +699,9 @@ 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 @@ -726,6 +729,9 @@ 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 @@ -802,9 +808,15 @@ 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/string-similarity': specifier: ^4.0.2 version: 4.0.2 @@ -946,6 +958,9 @@ 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) @@ -3850,6 +3865,9 @@ 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==} @@ -3861,12 +3879,21 @@ 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/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@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==} @@ -7943,6 +7970,9 @@ 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==} @@ -8278,6 +8308,10 @@ 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'} @@ -8609,9 +8643,15 @@ 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'} @@ -13022,7 +13062,6 @@ snapshots: '@types/node@20.19.1': dependencies: undici-types: 6.21.0 - optional: true '@types/node@22.15.29': dependencies: @@ -13030,6 +13069,10 @@ 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)': @@ -13041,10 +13084,21 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/retry@0.12.5': {} + '@types/shell-quote@1.7.5': {} '@types/stack-utils@2.0.3': {} + '@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': {} @@ -17757,6 +17811,12 @@ 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 @@ -18231,6 +18291,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -18640,10 +18702,16 @@ 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/install-vsix.js b/scripts/install-vsix.js new file mode 100644 index 0000000000..0ed9b6d376 --- /dev/null +++ b/scripts/install-vsix.js @@ -0,0 +1,91 @@ +const { execSync } = require("child_process") +const fs = require("fs") +const readline = require("readline") + +// detect "yes" flags +const autoYes = process.argv.includes("-y") + +// detect editor command from args or default to "code" +const editorArg = process.argv.find((arg) => arg.startsWith("--editor=")) +const defaultEditor = editorArg ? editorArg.split("=")[1] : "code" + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}) + +const askQuestion = (question) => { + return new Promise((resolve) => { + rl.question(question, (answer) => { + resolve(answer) + }) + }) +} + +async function main() { + try { + const packageJson = JSON.parse(fs.readFileSync("./src/package.json", "utf-8")) + const name = packageJson.name + const version = packageJson.version + const vsixFileName = `./bin/${name}-${version}.vsix` + const publisher = packageJson.publisher + const extensionId = `${publisher}.${name}` + + console.log("\n🚀 Roo Code VSIX Installer") + console.log("========================") + console.log("\nThis script will:") + console.log("1. Uninstall any existing version of the Roo Code extension") + console.log("2. Install the newly built VSIX package") + console.log(`\nExtension: ${extensionId}`) + console.log(`VSIX file: ${vsixFileName}`) + + // Ask for editor command if not provided + let editorCommand = defaultEditor + if (!editorArg && !autoYes) { + const editorAnswer = await askQuestion( + "\nWhich editor command to use? (code/cursor/code-insiders) [default: code]: ", + ) + if (editorAnswer.trim()) { + editorCommand = editorAnswer.trim() + } + } + + // skip prompt if auto-yes + const answer = autoYes ? "y" : await askQuestion("\nDo you wish to continue? (y/n): ") + + if (answer.toLowerCase() !== "y") { + console.log("Installation cancelled.") + rl.close() + process.exit(0) + } + + console.log(`\nProceeding with installation using '${editorCommand}' command...`) + + try { + execSync(`${editorCommand} --uninstall-extension ${extensionId}`, { stdio: "inherit" }) + } catch (e) { + console.log("Extension not installed, skipping uninstall step") + } + + if (!fs.existsSync(vsixFileName)) { + console.error(`\n❌ VSIX file not found: ${vsixFileName}`) + console.error("Make sure the build completed successfully") + rl.close() + process.exit(1) + } + + execSync(`${editorCommand} --install-extension ${vsixFileName}`, { stdio: "inherit" }) + + console.log(`\n✅ Successfully installed extension from ${vsixFileName}`) + console.log("\n⚠️ IMPORTANT: You need to restart VS Code for the changes to take effect.") + console.log(" Please close and reopen VS Code to use the updated extension.\n") + + rl.close() + } catch (error) { + console.error("\n❌ Failed to install extension:", error.message) + rl.close() + process.exit(1) + } +} + +main() diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index fc30878c7b..8e84981d8a 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -13,6 +13,8 @@ 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 { t } from "../i18n" /** * Helper to get the visible ClineProvider instance or log if not found. @@ -171,6 +173,22 @@ 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) diff --git a/src/activate/registerTerminalActions.ts b/src/activate/registerTerminalActions.ts index eb494d66da..9773d01d38 100644 --- a/src/activate/registerTerminalActions.ts +++ b/src/activate/registerTerminalActions.ts @@ -20,7 +20,7 @@ const registerTerminalAction = ( ) => { context.subscriptions.push( vscode.commands.registerCommand(getTerminalCommand(command), async (args: any) => { - let content = args.selection + let content = args?.selection if (!content || content === "") { content = await Terminal.getTerminalContents(promptType === "TERMINAL_ADD_TO_CONTEXT" ? -1 : 1) diff --git a/src/api/providers/__tests__/bedrock-inference-profiles.spec.ts b/src/api/providers/__tests__/bedrock-inference-profiles.spec.ts new file mode 100644 index 0000000000..7eef16d241 --- /dev/null +++ b/src/api/providers/__tests__/bedrock-inference-profiles.spec.ts @@ -0,0 +1,249 @@ +// npx vitest run src/api/providers/__tests__/bedrock-inference-profiles.spec.ts + +import { AWS_INFERENCE_PROFILE_MAPPING } from "@roo-code/types" +import { AwsBedrockHandler } from "../bedrock" +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock AWS SDK +vitest.mock("@aws-sdk/client-bedrock-runtime", () => { + return { + BedrockRuntimeClient: vitest.fn().mockImplementation(() => ({ + send: vitest.fn(), + config: { region: "us-east-1" }, + })), + ConverseCommand: vitest.fn(), + ConverseStreamCommand: vitest.fn(), + } +}) + +describe("AWS Bedrock Inference Profiles", () => { + // Helper function to create a handler with specific options + const createHandler = (options: Partial = {}) => { + const defaultOptions: ApiHandlerOptions = { + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + awsRegion: "us-east-1", + ...options, + } + return new AwsBedrockHandler(defaultOptions) + } + + describe("AWS_INFERENCE_PROFILE_MAPPING constant", () => { + it("should contain all expected region mappings", () => { + expect(AWS_INFERENCE_PROFILE_MAPPING).toEqual([ + ["us-gov-", "ug."], + ["us-", "us."], + ["eu-", "eu."], + ["ap-", "apac."], + ["ca-", "ca."], + ["sa-", "sa."], + ]) + }) + + it("should be ordered by pattern length (descending)", () => { + const lengths = AWS_INFERENCE_PROFILE_MAPPING.map(([pattern]) => pattern.length) + const sortedLengths = [...lengths].sort((a, b) => b - a) + expect(lengths).toEqual(sortedLengths) + }) + + it("should have valid inference profile prefixes", () => { + AWS_INFERENCE_PROFILE_MAPPING.forEach(([regionPattern, inferenceProfile]) => { + expect(regionPattern).toMatch(/^[a-z-]+$/) + expect(inferenceProfile).toMatch(/^[a-z]+\.$/) + }) + }) + }) + + describe("getPrefixForRegion function", () => { + it("should return correct prefix for US government regions", () => { + const handler = createHandler() + expect((handler as any).constructor.getPrefixForRegion("us-gov-east-1")).toBe("ug.") + expect((handler as any).constructor.getPrefixForRegion("us-gov-west-1")).toBe("ug.") + }) + + it("should return correct prefix for US commercial regions", () => { + const handler = createHandler() + expect((handler as any).constructor.getPrefixForRegion("us-east-1")).toBe("us.") + expect((handler as any).constructor.getPrefixForRegion("us-west-1")).toBe("us.") + expect((handler as any).constructor.getPrefixForRegion("us-west-2")).toBe("us.") + }) + + it("should return correct prefix for European regions", () => { + const handler = createHandler() + expect((handler as any).constructor.getPrefixForRegion("eu-west-1")).toBe("eu.") + expect((handler as any).constructor.getPrefixForRegion("eu-central-1")).toBe("eu.") + expect((handler as any).constructor.getPrefixForRegion("eu-north-1")).toBe("eu.") + expect((handler as any).constructor.getPrefixForRegion("eu-south-1")).toBe("eu.") + }) + + it("should return correct prefix for Asia Pacific regions", () => { + const handler = createHandler() + expect((handler as any).constructor.getPrefixForRegion("ap-southeast-1")).toBe("apac.") + expect((handler as any).constructor.getPrefixForRegion("ap-northeast-1")).toBe("apac.") + expect((handler as any).constructor.getPrefixForRegion("ap-south-1")).toBe("apac.") + expect((handler as any).constructor.getPrefixForRegion("ap-east-1")).toBe("apac.") + }) + + it("should return correct prefix for Canada regions", () => { + const handler = createHandler() + expect((handler as any).constructor.getPrefixForRegion("ca-central-1")).toBe("ca.") + expect((handler as any).constructor.getPrefixForRegion("ca-west-1")).toBe("ca.") + }) + + it("should return correct prefix for South America regions", () => { + const handler = createHandler() + expect((handler as any).constructor.getPrefixForRegion("sa-east-1")).toBe("sa.") + }) + + it("should return undefined for unsupported regions", () => { + const handler = createHandler() + expect((handler as any).constructor.getPrefixForRegion("af-south-1")).toBeUndefined() + expect((handler as any).constructor.getPrefixForRegion("me-south-1")).toBeUndefined() + expect((handler as any).constructor.getPrefixForRegion("cn-north-1")).toBeUndefined() + expect((handler as any).constructor.getPrefixForRegion("invalid-region")).toBeUndefined() + }) + + it("should prioritize longer patterns over shorter ones", () => { + const handler = createHandler() + // us-gov- should be matched before us- + expect((handler as any).constructor.getPrefixForRegion("us-gov-east-1")).toBe("ug.") + expect((handler as any).constructor.getPrefixForRegion("us-gov-west-1")).toBe("ug.") + + // Regular us- regions should still work + expect((handler as any).constructor.getPrefixForRegion("us-east-1")).toBe("us.") + expect((handler as any).constructor.getPrefixForRegion("us-west-2")).toBe("us.") + }) + }) + + describe("Cross-region inference integration", () => { + it("should apply ug. prefix for US government regions", () => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "us-gov-east-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + expect(model.id).toBe("ug.anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should apply us. prefix for US commercial regions", () => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "us-east-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + expect(model.id).toBe("us.anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should apply eu. prefix for European regions", () => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "eu-west-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + expect(model.id).toBe("eu.anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should apply apac. prefix for Asia Pacific regions", () => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "ap-southeast-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + expect(model.id).toBe("apac.anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should apply ca. prefix for Canada regions", () => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "ca-central-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + expect(model.id).toBe("ca.anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should apply sa. prefix for South America regions", () => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "sa-east-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + expect(model.id).toBe("sa.anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should not apply prefix when cross-region inference is disabled", () => { + const handler = createHandler({ + awsUseCrossRegionInference: false, + awsRegion: "us-gov-east-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + expect(model.id).toBe("anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should handle unsupported regions gracefully", () => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "af-south-1", // Unsupported region + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const model = handler.getModel() + // Should remain unchanged when no prefix is found + expect(model.id).toBe("anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should work with different model IDs", () => { + const testModels = [ + "anthropic.claude-3-haiku-20240307-v1:0", + "anthropic.claude-3-opus-20240229-v1:0", + "amazon.nova-pro-v1:0", + "meta.llama3-1-70b-instruct-v1:0", + ] + + testModels.forEach((modelId) => { + const handler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "eu-west-1", + apiModelId: modelId, + }) + + const model = handler.getModel() + expect(model.id).toBe(`eu.${modelId}`) + }) + }) + + it("should prioritize us-gov- over us- in cross-region inference", () => { + // Test that us-gov-east-1 gets ug. prefix, not us. + const govHandler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "us-gov-east-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const govModel = govHandler.getModel() + expect(govModel.id).toBe("ug.anthropic.claude-3-sonnet-20240229-v1:0") + + // Test that regular us-east-1 still gets us. prefix + const usHandler = createHandler({ + awsUseCrossRegionInference: true, + awsRegion: "us-east-1", + apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0", + }) + + const usModel = usHandler.getModel() + expect(usModel.id).toBe("us.anthropic.claude-3-sonnet-20240229-v1:0") + }) + }) +}) diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index 80f6338629..ad0ae2bdb5 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -74,42 +74,6 @@ describe("AwsBedrockHandler", () => { expect(modelInfo.info).toBeDefined() }) - it("should handle inference-profile ARN with apne3 region prefix", () => { - const originalParseArn = AwsBedrockHandler.prototype["parseArn"] - const parseArnMock = vi.fn().mockImplementation(function (this: any, arn: string, region?: string) { - return originalParseArn.call(this, arn, region) - }) - AwsBedrockHandler.prototype["parseArn"] = parseArnMock - - try { - const customArnHandler = new AwsBedrockHandler({ - apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", - awsAccessKey: "test-access-key", - awsSecretKey: "test-secret-key", - awsRegion: "ap-northeast-3", - awsCustomArn: - "arn:aws:bedrock:ap-northeast-3:123456789012:inference-profile/apne3.anthropic.claude-3-5-sonnet-20241022-v2:0", - }) - - const modelInfo = customArnHandler.getModel() - - expect(modelInfo.id).toBe( - "arn:aws:bedrock:ap-northeast-3:123456789012:inference-profile/apne3.anthropic.claude-3-5-sonnet-20241022-v2:0", - ) - expect(modelInfo.info).toBeDefined() - - expect(parseArnMock).toHaveBeenCalledWith( - "arn:aws:bedrock:ap-northeast-3:123456789012:inference-profile/apne3.anthropic.claude-3-5-sonnet-20241022-v2:0", - "ap-northeast-3", - ) - - expect((customArnHandler as any).arnInfo.modelId).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0") - expect((customArnHandler as any).arnInfo.crossRegionInference).toBe(false) - } finally { - AwsBedrockHandler.prototype["parseArn"] = originalParseArn - } - }) - it("should use default prompt router model when prompt router arn is entered but no model can be identified from the ARN", () => { const customArnHandler = new AwsBedrockHandler({ awsCustomArn: @@ -127,6 +91,270 @@ describe("AwsBedrockHandler", () => { }) }) + describe("region mapping and cross-region inference", () => { + describe("getPrefixForRegion", () => { + it("should return correct prefix for US regions", () => { + // Access private static method using type casting + const getPrefixForRegion = (AwsBedrockHandler as any).getPrefixForRegion + + expect(getPrefixForRegion("us-east-1")).toBe("us.") + expect(getPrefixForRegion("us-west-2")).toBe("us.") + expect(getPrefixForRegion("us-gov-west-1")).toBe("ug.") + }) + + it("should return correct prefix for EU regions", () => { + const getPrefixForRegion = (AwsBedrockHandler as any).getPrefixForRegion + + expect(getPrefixForRegion("eu-west-1")).toBe("eu.") + expect(getPrefixForRegion("eu-central-1")).toBe("eu.") + expect(getPrefixForRegion("eu-north-1")).toBe("eu.") + }) + + it("should return correct prefix for APAC regions", () => { + const getPrefixForRegion = (AwsBedrockHandler as any).getPrefixForRegion + + expect(getPrefixForRegion("ap-southeast-1")).toBe("apac.") + expect(getPrefixForRegion("ap-northeast-1")).toBe("apac.") + expect(getPrefixForRegion("ap-south-1")).toBe("apac.") + }) + + it("should return undefined for unsupported regions", () => { + const getPrefixForRegion = (AwsBedrockHandler as any).getPrefixForRegion + + expect(getPrefixForRegion("unknown-region")).toBeUndefined() + expect(getPrefixForRegion("")).toBeUndefined() + expect(getPrefixForRegion("invalid")).toBeUndefined() + }) + }) + + describe("isSystemInferenceProfile", () => { + it("should return true for AWS inference profile prefixes", () => { + const isSystemInferenceProfile = (AwsBedrockHandler as any).isSystemInferenceProfile + + expect(isSystemInferenceProfile("us.")).toBe(true) + expect(isSystemInferenceProfile("eu.")).toBe(true) + expect(isSystemInferenceProfile("apac.")).toBe(true) + }) + + it("should return false for other prefixes", () => { + const isSystemInferenceProfile = (AwsBedrockHandler as any).isSystemInferenceProfile + + expect(isSystemInferenceProfile("ap.")).toBe(false) + expect(isSystemInferenceProfile("apne1.")).toBe(false) + expect(isSystemInferenceProfile("use1.")).toBe(false) + expect(isSystemInferenceProfile("custom.")).toBe(false) + expect(isSystemInferenceProfile("")).toBe(false) + }) + }) + + describe("parseBaseModelId", () => { + it("should remove defined inference profile prefixes", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + // Access private method using type casting + const parseBaseModelId = (handler as any).parseBaseModelId.bind(handler) + + expect(parseBaseModelId("us.anthropic.claude-3-5-sonnet-20241022-v2:0")).toBe( + "anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + expect(parseBaseModelId("eu.anthropic.claude-3-haiku-20240307-v1:0")).toBe( + "anthropic.claude-3-haiku-20240307-v1:0", + ) + expect(parseBaseModelId("apac.anthropic.claude-3-opus-20240229-v1:0")).toBe( + "anthropic.claude-3-opus-20240229-v1:0", + ) + }) + + it("should not modify model IDs without defined prefixes", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const parseBaseModelId = (handler as any).parseBaseModelId.bind(handler) + + expect(parseBaseModelId("anthropic.claude-3-5-sonnet-20241022-v2:0")).toBe( + "anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + expect(parseBaseModelId("amazon.titan-text-express-v1")).toBe("amazon.titan-text-express-v1") + }) + + it("should not modify model IDs with other prefixes", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const parseBaseModelId = (handler as any).parseBaseModelId.bind(handler) + + // Other prefixes should be preserved as part of the model ID + expect(parseBaseModelId("ap.anthropic.claude-3-5-sonnet-20241022-v2:0")).toBe( + "ap.anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + expect(parseBaseModelId("apne1.anthropic.claude-3-5-sonnet-20241022-v2:0")).toBe( + "apne1.anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + expect(parseBaseModelId("use1.anthropic.claude-3-5-sonnet-20241022-v2:0")).toBe( + "use1.anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + }) + }) + + describe("cross-region inference integration", () => { + it("should apply correct prefix when cross-region inference is enabled", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseCrossRegionInference: true, + }) + + const model = handler.getModel() + expect(model.id).toBe("us.anthropic.claude-3-5-sonnet-20241022-v2:0") + }) + + it("should apply correct prefix for different regions", () => { + const euHandler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "eu-west-1", + awsUseCrossRegionInference: true, + }) + + const apacHandler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "ap-southeast-1", + awsUseCrossRegionInference: true, + }) + + expect(euHandler.getModel().id).toBe("eu.anthropic.claude-3-5-sonnet-20241022-v2:0") + expect(apacHandler.getModel().id).toBe("apac.anthropic.claude-3-5-sonnet-20241022-v2:0") + }) + + it("should not apply prefix when cross-region inference is disabled", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseCrossRegionInference: false, + }) + + const model = handler.getModel() + expect(model.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0") + }) + + it("should not apply prefix for unsupported regions", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "unknown-region", + awsUseCrossRegionInference: true, + }) + + const model = handler.getModel() + expect(model.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0") + }) + }) + + describe("ARN parsing with inference profiles", () => { + it("should detect cross-region inference from ARN model ID", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn( + "arn:aws:bedrock:us-east-1:123456789012:foundation-model/us.anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + + expect(result.isValid).toBe(true) + expect(result.crossRegionInference).toBe(true) + expect(result.modelId).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0") + }) + + it("should not detect cross-region inference for non-prefixed models", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn( + "arn:aws:bedrock:us-east-1:123456789012:foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + + expect(result.isValid).toBe(true) + expect(result.crossRegionInference).toBe(false) + expect(result.modelId).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0") + }) + + it("should detect cross-region inference for defined prefixes", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const euResult = parseArn( + "arn:aws:bedrock:eu-west-1:123456789012:foundation-model/eu.anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + const apacResult = parseArn( + "arn:aws:bedrock:ap-southeast-1:123456789012:foundation-model/apac.anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + + expect(euResult.crossRegionInference).toBe(true) + expect(euResult.modelId).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0") + + expect(apacResult.crossRegionInference).toBe(true) + expect(apacResult.modelId).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0") + }) + + it("should not detect cross-region inference for other prefixes", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + // Other prefixes should not trigger cross-region inference detection + const result = parseArn( + "arn:aws:bedrock:us-east-1:123456789012:foundation-model/ap.anthropic.claude-3-5-sonnet-20241022-v2:0", + ) + + expect(result.crossRegionInference).toBe(false) + expect(result.modelId).toBe("ap.anthropic.claude-3-5-sonnet-20241022-v2:0") // Should be preserved as-is + }) + }) + }) + describe("image handling", () => { const mockImageData = Buffer.from("test-image-data").toString("base64") @@ -242,4 +470,98 @@ describe("AwsBedrockHandler", () => { expect(secondImage.image).toHaveProperty("format", "png") }) }) + + describe("error handling and validation", () => { + it("should handle invalid regions gracefully", () => { + expect(() => { + new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "", // Empty region + }) + }).not.toThrow() + }) + + it("should validate ARN format and provide helpful error messages", () => { + expect(() => { + new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsCustomArn: "invalid-arn-format", + }) + }).toThrow(/INVALID_ARN_FORMAT/) + }) + + it("should handle malformed ARNs with missing components", () => { + expect(() => { + new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsCustomArn: "arn:aws:bedrock:us-east-1", + }) + }).toThrow(/INVALID_ARN_FORMAT/) + }) + }) + + describe("model information and configuration", () => { + it("should preserve model information after applying cross-region prefixes", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseCrossRegionInference: true, + }) + + const model = handler.getModel() + + // Model ID should have prefix + expect(model.id).toBe("us.anthropic.claude-3-5-sonnet-20241022-v2:0") + + // But model info should remain the same + expect(model.info.maxTokens).toBe(8192) + expect(model.info.contextWindow).toBe(200_000) + expect(model.info.supportsImages).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + }) + + it("should handle model configuration overrides correctly", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + modelMaxTokens: 4096, + awsModelContextWindow: 100_000, + }) + + const model = handler.getModel() + + // Should use override values + expect(model.info.maxTokens).toBe(4096) + expect(model.info.contextWindow).toBe(100_000) + }) + + it("should handle unknown models with sensible defaults", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "unknown.model.id", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const model = handler.getModel() + + // Should fall back to default model info + expect(model.info.maxTokens).toBeDefined() + expect(model.info.contextWindow).toBeDefined() + expect(typeof model.info.supportsImages).toBe("boolean") + expect(typeof model.info.supportsPromptCache).toBe("boolean") + }) + }) }) diff --git a/src/api/providers/__tests__/claude-code-caching.spec.ts b/src/api/providers/__tests__/claude-code-caching.spec.ts new file mode 100644 index 0000000000..b7f7ff852a --- /dev/null +++ b/src/api/providers/__tests__/claude-code-caching.spec.ts @@ -0,0 +1,305 @@ +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__/claude-code.spec.ts b/src/api/providers/__tests__/claude-code.spec.ts index 5bc4c6f1ea..d0dfa68eb8 100644 --- a/src/api/providers/__tests__/claude-code.spec.ts +++ b/src/api/providers/__tests__/claude-code.spec.ts @@ -34,7 +34,7 @@ describe("ClaudeCodeHandler", () => { const model = handler.getModel() expect(model.id).toBe("claude-3-5-sonnet-20241022") expect(model.info.supportsImages).toBe(false) - expect(model.info.supportsPromptCache).toBe(false) + expect(model.info.supportsPromptCache).toBe(true) // Claude Code now supports prompt caching }) test("should use default model when invalid model provided", () => { diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index fc809819e8..86d57ab3f5 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -599,7 +599,7 @@ describe("OpenAiHandler", () => { stream: true, stream_options: { include_usage: true }, reasoning_effort: "medium", - temperature: 0.5, + temperature: undefined, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 32000, }), @@ -640,7 +640,7 @@ describe("OpenAiHandler", () => { stream: true, stream_options: { include_usage: true }, reasoning_effort: "medium", - temperature: 0.7, + temperature: undefined, }), {}, ) @@ -682,7 +682,7 @@ describe("OpenAiHandler", () => { { role: "user", content: "Hello!" }, ], reasoning_effort: "medium", - temperature: 0.3, + temperature: undefined, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 65536, // Using default maxTokens from o3Options }), @@ -712,7 +712,7 @@ describe("OpenAiHandler", () => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ - temperature: 0, // Default temperature + temperature: undefined, // Temperature is not supported for O3 models }), {}, ) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index d8a370e08f..a25fea5200 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -20,7 +20,7 @@ import { BEDROCK_DEFAULT_TEMPERATURE, BEDROCK_MAX_TOKENS, BEDROCK_DEFAULT_CONTEXT, - BEDROCK_REGION_INFO, + AWS_INFERENCE_PROFILE_MAPPING, } from "@roo-code/types" import { ApiStream } from "../transform/stream" @@ -482,7 +482,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH if (streamEvent.contentBlockStart) { const cbStart = streamEvent.contentBlockStart - // Check if this is a reasoning block (official AWS SDK structure) + // Check if this is a reasoning block (AWS SDK structure) if (cbStart.contentBlock?.reasoningContent) { if (cbStart.contentBlockIndex && cbStart.contentBlockIndex > 0) { yield { type: "reasoning", text: "\n" } @@ -493,7 +493,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } } // Check for thinking block - handle both possible AWS SDK structures - // cbStart.contentBlock: newer/official structure + // cbStart.contentBlock: newer structure // cbStart.content_block: alternative structure seen in some AWS SDK versions else if (cbStart.contentBlock?.type === "thinking" || cbStart.content_block?.type === "thinking") { const contentBlock = cbStart.contentBlock || cbStart.content_block @@ -522,11 +522,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH // Process reasoning and text content deltas // Multiple structures are supported for AWS SDK compatibility: - // - delta.reasoningContent.text: official AWS docs structure for reasoning + // - delta.reasoningContent.text: AWS docs structure for reasoning // - delta.thinking: alternative structure for thinking content // - delta.text: standard text content if (delta) { - // Check for reasoningContent property (official AWS SDK structure) + // Check for reasoningContent property (AWS SDK structure) if (delta.reasoningContent?.text) { yield { type: "reasoning", @@ -827,7 +827,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH if (originalModelId && result.modelId !== originalModelId) { // If the model ID changed after parsing, it had a region prefix let prefix = originalModelId.replace(result.modelId, "") - result.crossRegionInference = AwsBedrockHandler.prefixIsMultiRegion(prefix) + result.crossRegionInference = AwsBedrockHandler.isSystemInferenceProfile(prefix) } // Check if region in ARN matches provided region (if specified) @@ -851,29 +851,21 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH } //This strips any region prefix that used on cross-region model inference ARNs - private parseBaseModelId(modelId: string) { + private parseBaseModelId(modelId: string): string { if (!modelId) { return modelId } - const knownRegionPrefixes = AwsBedrockHandler.getPrefixList() - - // Find if the model ID starts with any known region prefix - const matchedPrefix = knownRegionPrefixes.find((prefix) => modelId.startsWith(prefix)) - - if (matchedPrefix) { - // Remove the region prefix from the model ID - return modelId.substring(matchedPrefix.length) - } else { - // If no known prefix was found, check for a generic pattern - // Look for a pattern where the first segment before a dot doesn't contain dots or colons - // and the remaining parts still contain at least one dot - const genericPrefixMatch = modelId.match(/^([^.:]+)\.(.+\..+)$/) - - if (genericPrefixMatch) { - return genericPrefixMatch[2] + // Remove AWS cross-region inference profile prefixes + // as defined in AWS_INFERENCE_PROFILE_MAPPING + for (const [_, inferenceProfile] of AWS_INFERENCE_PROFILE_MAPPING) { + if (modelId.startsWith(inferenceProfile)) { + // Remove the inference profile prefix from the model ID + return modelId.substring(inferenceProfile.length) } } + + // Return the model ID as-is for all other cases return modelId } @@ -950,14 +942,12 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH //a model was selected from the drop down modelConfig = this.getModelById(this.options.apiModelId as string) - if (this.options.awsUseCrossRegionInference) { - // Get the current region - const region = this.options.awsRegion || "" - // Use the helper method to get the appropriate prefix for this region - const prefix = AwsBedrockHandler.getPrefixForRegion(region) - - // Apply the prefix if one was found, otherwise use the model ID as is - modelConfig.id = prefix ? `${prefix}${modelConfig.id}` : modelConfig.id + // Add cross-region inference prefix if enabled + if (this.options.awsUseCrossRegionInference && this.options.awsRegion) { + const prefix = AwsBedrockHandler.getPrefixForRegion(this.options.awsRegion) + if (prefix) { + modelConfig.id = `${prefix}${modelConfig.id}` + } } } @@ -1023,24 +1013,23 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH * *************************************************************************************/ - private static getPrefixList(): string[] { - return Object.keys(BEDROCK_REGION_INFO) - } - private static getPrefixForRegion(region: string): string | undefined { - for (const [prefix, info] of Object.entries(BEDROCK_REGION_INFO)) { - if (info.pattern && region.startsWith(info.pattern)) { - return prefix + // Use AWS recommended inference profile prefixes + // Array is pre-sorted by pattern length (descending) to ensure more specific patterns match first + for (const [regionPattern, inferenceProfile] of AWS_INFERENCE_PROFILE_MAPPING) { + if (region.startsWith(regionPattern)) { + return inferenceProfile } } + return undefined } - private static prefixIsMultiRegion(arnPrefix: string): boolean { - for (const [prefix, info] of Object.entries(BEDROCK_REGION_INFO)) { - if (arnPrefix === prefix) { - if (info?.multiRegion) return info.multiRegion - else return false + private static isSystemInferenceProfile(prefix: string): boolean { + // Check if the prefix is defined in AWS_INFERENCE_PROFILE_MAPPING + for (const [_, inferenceProfile] of AWS_INFERENCE_PROFILE_MAPPING) { + if (prefix === inferenceProfile) { + return true } } return false diff --git a/src/api/providers/fetchers/__tests__/litellm.spec.ts b/src/api/providers/fetchers/__tests__/litellm.spec.ts index f4db3bc12e..07bbe9871a 100644 --- a/src/api/providers/fetchers/__tests__/litellm.spec.ts +++ b/src/api/providers/fetchers/__tests__/litellm.spec.ts @@ -4,6 +4,7 @@ vi.mock("axios") import type { Mock } from "vitest" import axios from "axios" import { getLiteLLMModels } from "../litellm" +import { DEFAULT_HEADERS } from "../../constants" const mockedAxios = axios as typeof axios & { get: Mock @@ -32,6 +33,7 @@ describe("getLiteLLMModels", () => { headers: { Authorization: "Bearer test-api-key", "Content-Type": "application/json", + ...DEFAULT_HEADERS, }, timeout: 5000, }) @@ -83,6 +85,7 @@ describe("getLiteLLMModels", () => { headers: { Authorization: "Bearer test-api-key", "Content-Type": "application/json", + ...DEFAULT_HEADERS, }, timeout: 5000, }) @@ -125,6 +128,7 @@ describe("getLiteLLMModels", () => { expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", { headers: { "Content-Type": "application/json", + ...DEFAULT_HEADERS, }, timeout: 5000, }) diff --git a/src/api/providers/fetchers/litellm.ts b/src/api/providers/fetchers/litellm.ts index 47617cd390..0891527406 100644 --- a/src/api/providers/fetchers/litellm.ts +++ b/src/api/providers/fetchers/litellm.ts @@ -4,6 +4,7 @@ import { LITELLM_COMPUTER_USE_MODELS } from "@roo-code/types" import type { ModelRecord } from "../../../shared/api" +import { DEFAULT_HEADERS } from "../constants" /** * Fetches available models from a LiteLLM server * @@ -16,6 +17,7 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise try { const headers: Record = { "Content-Type": "application/json", + ...DEFAULT_HEADERS, } if (apiKey) { diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 5956187e41..fef700268d 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -2,6 +2,7 @@ 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" @@ -22,7 +23,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 fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data)) + await safeWriteJson(path.join(cacheDir, filename), data) } async function readModels(router: RouterName): Promise { diff --git a/src/api/providers/fetchers/modelEndpointCache.ts b/src/api/providers/fetchers/modelEndpointCache.ts index c69e7c82a3..256ae84048 100644 --- a/src/api/providers/fetchers/modelEndpointCache.ts +++ b/src/api/providers/fetchers/modelEndpointCache.ts @@ -2,6 +2,7 @@ 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" @@ -18,7 +19,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 fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data, null, 2)) + await safeWriteJson(path.join(cacheDir, filename), data) } async function readModelEndpoints(key: string): Promise { diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index b4f256f43a..f5e4e4c985 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.startsWith("o3-mini")) { + if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) { 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: this.options.modelTemperature ?? 0, + temperature: undefined, } // 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: this.options.modelTemperature ?? 0, + temperature: undefined, } // 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 51d97963e7..6565daa238 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -48,13 +48,11 @@ interface CompletionUsage { } total_tokens?: number cost?: number - is_byok?: boolean + cost_details?: { + upstream_inference_cost?: number + } } -// 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 @@ -168,11 +166,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH type: "usage", inputTokens: lastUsage.prompt_tokens || 0, outputTokens: lastUsage.completion_tokens || 0, - // 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, + cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens, reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens, - totalCost: (lastUsage.is_byok ? BYOK_COST_MULTIPLIER : 1) * (lastUsage.cost || 0), + totalCost: (lastUsage.cost_details?.upstream_inference_cost || 0) + (lastUsage.cost || 0), } } } diff --git a/src/api/providers/router-provider.ts b/src/api/providers/router-provider.ts index c64b29571a..25e9a11e1b 100644 --- a/src/api/providers/router-provider.ts +++ b/src/api/providers/router-provider.ts @@ -7,6 +7,8 @@ import { ApiHandlerOptions, RouterName, ModelRecord } from "../../shared/api" import { BaseProvider } from "./base-provider" import { getModels } from "./fetchers/modelCache" +import { DEFAULT_HEADERS } from "./constants" + type RouterProviderOptions = { name: RouterName baseURL: string @@ -43,7 +45,14 @@ export abstract class RouterProvider extends BaseProvider { this.defaultModelId = defaultModelId this.defaultModelInfo = defaultModelInfo - this.client = new OpenAI({ baseURL, apiKey }) + this.client = new OpenAI({ + baseURL, + apiKey, + defaultHeaders: { + ...DEFAULT_HEADERS, + ...(options.openAiHeaders || {}), + }, + }) } public async fetchModel() { diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 21c2709f90..9f29185eba 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -3,17 +3,45 @@ import * as path from "path" import * as fs from "fs/promises" import * as yaml from "yaml" +import stripBom from "strip-bom" -import { type ModeConfig, customModesSettingsSchema } from "@roo-code/types" +import { type ModeConfig, type PromptComponent, customModesSettingsSchema, modeConfigSchema } from "@roo-code/types" import { fileExistsAtPath } from "../../utils/fs" import { getWorkspacePath } from "../../utils/path" +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" +// Type definitions for import/export functionality +interface RuleFile { + relativePath: string + content: string +} + +interface ExportedModeConfig extends ModeConfig { + rulesFiles?: RuleFile[] +} + +interface ImportData { + customModes: ExportedModeConfig[] +} + +interface ExportResult { + success: boolean + yaml?: string + error?: string +} + +interface ImportResult { + success: boolean + error?: string +} + export class CustomModesManager { private static readonly cacheTTL = 10_000 @@ -73,12 +101,99 @@ 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 { + return yaml.parse(cleanedContent) + } 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 = yaml.parse(content) + const settings = this.parseYamlSafely(content, filePath) 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 [] } @@ -89,8 +204,11 @@ export class CustomModesManager { // Add source to each mode return result.data.customModes.map((mode) => ({ ...mode, source })) } catch (error) { - const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}` - console.error(`[CustomModesManager] ${errorMsg}`) + // 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}`) + } return [] } } @@ -124,7 +242,7 @@ export class CustomModesManager { const fileExists = await fileExistsAtPath(filePath) if (!fileExists) { - await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] }))) + await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] }, { lineWidth: 0 }))) } return filePath @@ -147,13 +265,12 @@ export class CustomModesManager { await this.getCustomModesFilePath() const content = await fs.readFile(settingsPath, "utf-8") - const errorMessage = - "Invalid custom modes format. Please ensure your settings follow the correct YAML format." + const errorMessage = t("common:customModes.errors.invalidFormat") let config: any try { - config = yaml.parse(content) + config = this.parseYamlSafely(content, settingsPath) } catch (error) { console.error(error) vscode.window.showErrorMessage(errorMessage) @@ -284,7 +401,7 @@ export class CustomModesManager { if (!workspaceFolders || workspaceFolders.length === 0) { logger.error("Failed to update project mode: No workspace folder found", { slug }) - throw new Error("No workspace folder found for project-specific mode") + throw new Error(t("common:customModes.errors.noWorkspaceForProject")) } const workspaceRoot = getWorkspacePath() @@ -318,7 +435,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(`Failed to update custom mode: ${errorMessage}`) + vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage })) } } @@ -329,20 +446,20 @@ export class CustomModesManager { content = await fs.readFile(filePath, "utf-8") } catch (error) { // File might not exist yet. - content = yaml.stringify({ customModes: [] }) + content = yaml.stringify({ customModes: [] }, { lineWidth: 0 }) } let settings try { - settings = yaml.parse(content) + settings = this.parseYamlSafely(content, filePath) } catch (error) { - console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, error) + // Error already logged in parseYamlSafely settings = { customModes: [] } } settings.customModes = operation(settings.customModes || []) - await fs.writeFile(filePath, yaml.stringify(settings), "utf-8") + await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0 }), "utf-8") } private async refreshMergedState(): Promise { @@ -373,7 +490,7 @@ export class CustomModesManager { const globalMode = settingsModes.find((m) => m.slug === slug) if (!projectMode && !globalMode) { - throw new Error("Write error: Mode not found") + throw new Error(t("common:customModes.errors.modeNotFound")) } await this.queueWrite(async () => { @@ -392,23 +509,398 @@ export class CustomModesManager { await this.refreshMergedState() }) } catch (error) { - vscode.window.showErrorMessage( - `Failed to delete custom mode: ${error instanceof Error ? error.message : String(error)}`, - ) + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage })) } } public async resetCustomModes(): Promise { try { const filePath = await this.getCustomModesFilePath() - await fs.writeFile(filePath, yaml.stringify({ customModes: [] })) + await fs.writeFile(filePath, yaml.stringify({ customModes: [] }, { lineWidth: 0 })) await this.context.globalState.update("customModes", []) this.clearCache() await this.onUpdate() } catch (error) { - vscode.window.showErrorMessage( - `Failed to reset custom modes: ${error instanceof Error ? error.message : String(error)}`, - ) + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customModes.errors.resetFailed", { error: errorMessage })) + } + } + + /** + * Checks if a mode has associated rules files in the .roo/rules-{slug}/ directory + * @param slug - The mode identifier to check + * @returns True if the mode has rules files with content, false otherwise + */ + public async checkRulesDirectoryHasContent(slug: string): Promise { + try { + // Get workspace path + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return false + } + + // Check if .roomodes file exists and contains this mode + // This ensures we can only consolidate rules for modes that have been customized + const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME) + try { + const roomodesExists = await fileExistsAtPath(roomodesPath) + if (roomodesExists) { + const roomodesContent = await fs.readFile(roomodesPath, "utf-8") + const roomodesData = yaml.parse(roomodesContent) + const roomodesModes = roomodesData?.customModes || [] + + // Check if this specific mode exists in .roomodes + const modeInRoomodes = roomodesModes.find((m: any) => m.slug === slug) + if (!modeInRoomodes) { + return false // Mode not customized in .roomodes, cannot consolidate + } + } else { + // If no .roomodes file exists, check if it's in global custom modes + const allModes = await this.getCustomModes() + const mode = allModes.find((m) => m.slug === slug) + + if (!mode) { + return false // Not a custom mode, cannot consolidate + } + } + } catch (error) { + // If we can't read .roomodes, fall back to checking custom modes + const allModes = await this.getCustomModes() + const mode = allModes.find((m) => m.slug === slug) + + if (!mode) { + return false // Not a custom mode, cannot consolidate + } + } + + // Check for .roo/rules-{slug}/ directory + const modeRulesDir = path.join(workspacePath, ".roo", `rules-${slug}`) + + try { + const stats = await fs.stat(modeRulesDir) + if (!stats.isDirectory()) { + return false + } + } catch (error) { + return false + } + + // Check if directory has any content files + try { + const entries = await fs.readdir(modeRulesDir, { withFileTypes: true }) + + for (const entry of entries) { + if (entry.isFile()) { + // Use path.join with modeRulesDir and entry.name for compatibility + const filePath = path.join(modeRulesDir, entry.name) + const content = await fs.readFile(filePath, "utf-8") + if (content.trim()) { + return true // Found at least one file with content + } + } + } + + return false // No files with content found + } catch (error) { + return false + } + } catch (error) { + logger.error("Failed to check rules directory for mode", { + slug, + error: error instanceof Error ? error.message : String(error), + }) + return false + } + } + + /** + * Exports a mode configuration with its associated rules files into a shareable YAML format + * @param slug - The mode identifier to export + * @param customPrompts - Optional custom prompts to merge into the export + * @returns Success status with YAML content or error message + */ + public async exportModeWithRules(slug: string, customPrompts?: PromptComponent): Promise { + try { + // Import modes from shared to check built-in modes + const { modes: builtInModes } = await import("../../shared/modes") + + // Get all current modes + const allModes = await this.getCustomModes() + let mode = allModes.find((m) => m.slug === slug) + + // If mode not found in custom modes, check if it's a built-in mode that has been customized + if (!mode) { + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return { success: false, error: "No workspace found" } + } + + const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME) + try { + const roomodesExists = await fileExistsAtPath(roomodesPath) + if (roomodesExists) { + const roomodesContent = await fs.readFile(roomodesPath, "utf-8") + const roomodesData = yaml.parse(roomodesContent) + const roomodesModes = roomodesData?.customModes || [] + + // Find the mode in .roomodes + mode = roomodesModes.find((m: any) => m.slug === slug) + } + } catch (error) { + // Continue to check built-in modes + } + + // If still not found, check if it's a built-in mode + if (!mode) { + const builtInMode = builtInModes.find((m) => m.slug === slug) + if (builtInMode) { + // Use the built-in mode as the base + mode = { ...builtInMode } + } else { + return { success: false, error: "Mode not found" } + } + } + } + + // Get workspace path + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return { success: false, error: "No workspace found" } + } + + // Check for .roo/rules-{slug}/ directory + const modeRulesDir = path.join(workspacePath, ".roo", `rules-${slug}`) + + let rulesFiles: RuleFile[] = [] + try { + const stats = await fs.stat(modeRulesDir) + if (stats.isDirectory()) { + // Extract content specific to this mode by looking for the mode-specific rules + const entries = await fs.readdir(modeRulesDir, { withFileTypes: true }) + + for (const entry of entries) { + if (entry.isFile()) { + // Use path.join with modeRulesDir and entry.name for compatibility + const filePath = path.join(modeRulesDir, entry.name) + const content = await fs.readFile(filePath, "utf-8") + if (content.trim()) { + // Calculate relative path from .roo directory + const relativePath = path.relative(path.join(workspacePath, ".roo"), filePath) + rulesFiles.push({ relativePath, content: content.trim() }) + } + } + } + } + } catch (error) { + // Directory doesn't exist, which is fine - mode might not have rules + } + + // Create an export mode with rules files preserved + const exportMode: ExportedModeConfig = { + ...mode, + // Remove source property for export + source: "project" as const, + } + + // Merge custom prompts if provided + if (customPrompts) { + if (customPrompts.roleDefinition) exportMode.roleDefinition = customPrompts.roleDefinition + if (customPrompts.description) exportMode.description = customPrompts.description + if (customPrompts.whenToUse) exportMode.whenToUse = customPrompts.whenToUse + if (customPrompts.customInstructions) exportMode.customInstructions = customPrompts.customInstructions + } + + // Add rules files if any exist + if (rulesFiles.length > 0) { + exportMode.rulesFiles = rulesFiles + } + + // Generate YAML + const exportData = { + customModes: [exportMode], + } + + const yamlContent = yaml.stringify(exportData) + + return { success: true, yaml: yamlContent } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + logger.error("Failed to export mode with rules", { slug, error: errorMessage }) + return { success: false, error: errorMessage } + } + } + + /** + * Imports modes from YAML content, including their associated rules files + * @param yamlContent - The YAML content containing mode configurations + * @param source - Target level for import: "global" (all projects) or "project" (current workspace only) + * @returns Success status with optional error message + */ + public async importModeWithRules( + yamlContent: string, + source: "global" | "project" = "project", + ): Promise { + try { + // Parse the YAML content with proper type validation + let importData: ImportData + try { + const parsed = yaml.parse(yamlContent) + + // Validate the structure + if (!parsed?.customModes || !Array.isArray(parsed.customModes) || parsed.customModes.length === 0) { + return { success: false, error: "Invalid import format: Expected 'customModes' array in YAML" } + } + + importData = parsed as ImportData + } catch (parseError) { + return { + success: false, + error: `Invalid YAML format: ${parseError instanceof Error ? parseError.message : "Failed to parse YAML"}`, + } + } + + // Check workspace availability early if importing at project level + if (source === "project") { + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return { success: false, error: "No workspace found" } + } + } + + // Process each mode in the import + for (const importMode of importData.customModes) { + const { rulesFiles, ...modeConfig } = importMode + + // Validate the mode configuration + const validationResult = modeConfigSchema.safeParse(modeConfig) + if (!validationResult.success) { + logger.error(`Invalid mode configuration for ${modeConfig.slug}`, { + errors: validationResult.error.errors, + }) + return { + success: false, + error: `Invalid mode configuration for ${modeConfig.slug}: ${validationResult.error.errors.map((e) => e.message).join(", ")}`, + } + } + + // Check for existing mode conflicts + const existingModes = await this.getCustomModes() + const existingMode = existingModes.find((m) => m.slug === importMode.slug) + if (existingMode) { + logger.info(`Overwriting existing mode: ${importMode.slug}`) + } + + // Import the mode configuration with the specified source + await this.updateCustomMode(importMode.slug, { + ...modeConfig, + source: source, // Use the provided source parameter + }) + + // Handle project-level imports + if (source === "project") { + const workspacePath = getWorkspacePath() + + // Always remove the existing rules folder for this mode if it exists + // This ensures that if the imported mode has no rules, the folder is cleaned up + const rulesFolderPath = path.join(workspacePath, ".roo", `rules-${importMode.slug}`) + try { + await fs.rm(rulesFolderPath, { recursive: true, force: true }) + logger.info(`Removed existing rules folder for mode ${importMode.slug}`) + } catch (error) { + // It's okay if the folder doesn't exist + logger.debug(`No existing rules folder to remove for mode ${importMode.slug}`) + } + + // Only create new rules files if they exist in the import + if (rulesFiles && Array.isArray(rulesFiles) && rulesFiles.length > 0) { + // Import the new rules files with path validation + for (const ruleFile of rulesFiles) { + if (ruleFile.relativePath && ruleFile.content) { + // Validate the relative path to prevent path traversal attacks + const normalizedRelativePath = path.normalize(ruleFile.relativePath) + + // Ensure the path doesn't contain traversal sequences + if (normalizedRelativePath.includes("..") || path.isAbsolute(normalizedRelativePath)) { + logger.error(`Invalid file path detected: ${ruleFile.relativePath}`) + continue // Skip this file but continue with others + } + + const targetPath = path.join(workspacePath, ".roo", normalizedRelativePath) + const normalizedTargetPath = path.normalize(targetPath) + const expectedBasePath = path.normalize(path.join(workspacePath, ".roo")) + + // Ensure the resolved path stays within the .roo directory + if (!normalizedTargetPath.startsWith(expectedBasePath)) { + logger.error(`Path traversal attempt detected: ${ruleFile.relativePath}`) + continue // Skip this file but continue with others + } + + // Ensure directory exists + const targetDir = path.dirname(targetPath) + await fs.mkdir(targetDir, { recursive: true }) + + // Write the file + await fs.writeFile(targetPath, ruleFile.content, "utf-8") + } + } + } + } else if (source === "global" && rulesFiles && Array.isArray(rulesFiles)) { + // For global imports, preserve the rules files structure in the global .roo directory + const globalRooDir = getGlobalRooDirectory() + + // Always remove the existing rules folder for this mode if it exists + // This ensures that if the imported mode has no rules, the folder is cleaned up + const rulesFolderPath = path.join(globalRooDir, `rules-${importMode.slug}`) + try { + await fs.rm(rulesFolderPath, { recursive: true, force: true }) + logger.info(`Removed existing global rules folder for mode ${importMode.slug}`) + } catch (error) { + // It's okay if the folder doesn't exist + logger.debug(`No existing global rules folder to remove for mode ${importMode.slug}`) + } + + // Import the new rules files with path validation + for (const ruleFile of rulesFiles) { + if (ruleFile.relativePath && ruleFile.content) { + // Validate the relative path to prevent path traversal attacks + const normalizedRelativePath = path.normalize(ruleFile.relativePath) + + // Ensure the path doesn't contain traversal sequences + if (normalizedRelativePath.includes("..") || path.isAbsolute(normalizedRelativePath)) { + logger.error(`Invalid file path detected: ${ruleFile.relativePath}`) + continue // Skip this file but continue with others + } + + const targetPath = path.join(globalRooDir, normalizedRelativePath) + const normalizedTargetPath = path.normalize(targetPath) + const expectedBasePath = path.normalize(globalRooDir) + + // Ensure the resolved path stays within the global .roo directory + if (!normalizedTargetPath.startsWith(expectedBasePath)) { + logger.error(`Path traversal attempt detected: ${ruleFile.relativePath}`) + continue // Skip this file but continue with others + } + + // Ensure directory exists + const targetDir = path.dirname(targetPath) + await fs.mkdir(targetDir, { recursive: true }) + + // Write the file + await fs.writeFile(targetPath, ruleFile.content, "utf-8") + } + } + } + } + + // Refresh the modes after import + await this.refreshMergedState() + + return { success: true } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + logger.error("Failed to import mode with rules", { error: errorMessage }) + return { success: false, error: errorMessage } } } diff --git a/src/core/config/__tests__/CustomModesManager.spec.ts b/src/core/config/__tests__/CustomModesManager.spec.ts index 7791b36ee8..c325a27f75 100644 --- a/src/core/config/__tests__/CustomModesManager.spec.ts +++ b/src/core/config/__tests__/CustomModesManager.spec.ts @@ -27,7 +27,14 @@ vi.mock("vscode", () => ({ }, })) -vi.mock("fs/promises") +vi.mock("fs/promises", () => ({ + mkdir: vi.fn(), + readFile: vi.fn(), + writeFile: vi.fn(), + stat: vi.fn(), + readdir: vi.fn(), + rm: vi.fn(), +})) vi.mock("../../../utils/fs") vi.mock("../../../utils/path") @@ -41,7 +48,8 @@ describe("CustomModesManager", () => { // Use path.sep to ensure correct path separators for the current platform 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` + const mockWorkspacePath = path.resolve("/mock/workspace") + const mockRoomodes = path.join(mockWorkspacePath, ".roomodes") beforeEach(() => { mockOnUpdate = vi.fn() @@ -57,14 +65,19 @@ describe("CustomModesManager", () => { }, } as unknown as vscode.ExtensionContext - mockWorkspaceFolders = [{ uri: { fsPath: "/mock/workspace" } }] + // mockWorkspacePath is now defined at the top level + mockWorkspaceFolders = [{ uri: { fsPath: mockWorkspacePath } }] ;(vscode.workspace as any).workspaceFolders = mockWorkspaceFolders ;(vscode.workspace.onDidSaveTextDocument as Mock).mockReturnValue({ dispose: vi.fn() }) - ;(getWorkspacePath as Mock).mockReturnValue("/mock/workspace") + ;(getWorkspacePath as Mock).mockReturnValue(mockWorkspacePath) ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { return path === mockSettingsPath || path === mockRoomodes }) ;(fs.mkdir as Mock).mockResolvedValue(undefined) + ;(fs.writeFile as Mock).mockResolvedValue(undefined) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([]) + ;(fs.rm as Mock).mockResolvedValue(undefined) ;(fs.readFile as Mock).mockImplementation(async (path: string) => { if (path === mockSettingsPath) { return yaml.stringify({ customModes: [] }) @@ -754,7 +767,7 @@ describe("CustomModesManager", () => { await manager.deleteCustomMode("non-existent-mode") - expect(mockShowError).toHaveBeenCalledWith(expect.stringContaining("Write error")) + expect(mockShowError).toHaveBeenCalledWith("customModes.errors.deleteFailed") }) }) @@ -786,5 +799,777 @@ describe("CustomModesManager", () => { ], }) }) + + describe("importModeWithRules", () => { + it("should return error when YAML content is invalid", async () => { + const invalidYaml = "invalid yaml content" + + const result = await manager.importModeWithRules(invalidYaml) + + expect(result.success).toBe(false) + expect(result.error).toContain("Invalid import format") + }) + + it("should return error when no custom modes found in YAML", async () => { + const emptyYaml = yaml.stringify({ customModes: [] }) + + const result = await manager.importModeWithRules(emptyYaml) + + expect(result.success).toBe(false) + expect(result.error).toBe("Invalid import format: Expected 'customModes' array in YAML") + }) + + it("should return error when no workspace is available", async () => { + ;(getWorkspacePath as Mock).mockReturnValue(null) + const validYaml = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test Role", + groups: ["read"], + }, + ], + }) + + const result = await manager.importModeWithRules(validYaml) + + expect(result.success).toBe(false) + expect(result.error).toBe("No workspace found") + }) + + it("should successfully import mode without rules files", async () => { + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "imported-mode", + name: "Imported Mode", + roleDefinition: "Imported Role", + groups: ["read", "edit"], + }, + ], + }) + + let roomodesContent: any = null + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + if (path === mockRoomodes && roomodesContent) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => { + if (path === mockRoomodes) { + roomodesContent = yaml.parse(content) + } + return Promise.resolve() + }) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining(".roomodes"), + expect.stringContaining("imported-mode"), + "utf-8", + ) + }) + + it("should successfully import mode with rules files", async () => { + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "imported-mode", + name: "Imported Mode", + roleDefinition: "Imported Role", + groups: ["read"], + rulesFiles: [ + { + relativePath: "rules-imported-mode/rule1.md", + content: "Rule 1 content", + }, + { + relativePath: "rules-imported-mode/subfolder/rule2.md", + content: "Rule 2 content", + }, + ], + }, + ], + }) + + let roomodesContent: any = null + let writtenFiles: Record = {} + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + if (path === mockRoomodes && roomodesContent) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => { + if (path === mockRoomodes) { + roomodesContent = yaml.parse(content) + } else { + writtenFiles[path] = content + } + return Promise.resolve() + }) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + + // Verify mode was imported + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining(".roomodes"), + expect.stringContaining("imported-mode"), + "utf-8", + ) + + // Verify rules files were created + expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining("rules-imported-mode"), { + recursive: true, + }) + expect(fs.mkdir).toHaveBeenCalledWith( + expect.stringContaining(path.join("rules-imported-mode", "subfolder")), + { recursive: true }, + ) + + // Verify file contents + const rule1Path = Object.keys(writtenFiles).find((p) => p.includes("rule1.md")) + const rule2Path = Object.keys(writtenFiles).find((p) => p.includes("rule2.md")) + expect(writtenFiles[rule1Path!]).toBe("Rule 1 content") + expect(writtenFiles[rule2Path!]).toBe("Rule 2 content") + }) + + it("should import multiple modes at once", async () => { + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "mode1", + name: "Mode 1", + roleDefinition: "Role 1", + groups: ["read"], + }, + { + slug: "mode2", + name: "Mode 2", + roleDefinition: "Role 2", + groups: ["edit"], + rulesFiles: [ + { + relativePath: "rules-mode2/rule.md", + content: "Mode 2 rules", + }, + ], + }, + ], + }) + + let roomodesContent: any = null + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + if (path === mockRoomodes && roomodesContent) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => { + if (path === mockRoomodes) { + roomodesContent = yaml.parse(content) + } + return Promise.resolve() + }) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + expect(roomodesContent.customModes).toHaveLength(2) + expect(roomodesContent.customModes[0].slug).toBe("mode1") + expect(roomodesContent.customModes[1].slug).toBe("mode2") + }) + + it("should handle import errors gracefully", async () => { + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test Role", + groups: ["read"], + rulesFiles: [ + { + relativePath: "rules-test-mode/rule.md", + content: "Rule content", + }, + ], + }, + ], + }) + + // Mock fs.readFile to work normally + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + if (path === mockRoomodes) { + throw new Error("File not found") + } + throw new Error("File not found") + }) + + // Mock fs.mkdir to fail when creating rules directory + ;(fs.mkdir as Mock).mockRejectedValue(new Error("Permission denied")) + + // Mock fs.writeFile to work normally for .roomodes but we won't get there + ;(fs.writeFile as Mock).mockResolvedValue(undefined) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(false) + expect(result.error).toContain("Permission denied") + }) + + it("should prevent path traversal attacks in import", async () => { + const maliciousYaml = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test Role", + groups: ["read"], + rulesFiles: [ + { + relativePath: "../../../etc/passwd", + content: "malicious content", + }, + { + relativePath: "rules-test-mode/../../../sensitive.txt", + content: "malicious content", + }, + { + relativePath: "/absolute/path/file.txt", + content: "malicious content", + }, + ], + }, + ], + }) + + let writtenFiles: string[] = [] + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + throw new Error("File not found") + }) + ;(fs.writeFile as Mock).mockImplementation(async (path: string) => { + writtenFiles.push(path) + return Promise.resolve() + }) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + const result = await manager.importModeWithRules(maliciousYaml) + + expect(result.success).toBe(true) + + // Verify that no files were written outside the .roo directory + const mockWorkspacePath = path.resolve("/mock/workspace") + const writtenRuleFiles = writtenFiles.filter((p) => !p.includes(".roomodes")) + writtenRuleFiles.forEach((filePath) => { + const normalizedPath = path.normalize(filePath) + const expectedBasePath = path.normalize(path.join(mockWorkspacePath, ".roo")) + expect(normalizedPath.startsWith(expectedBasePath)).toBe(true) + }) + + // Verify that malicious paths were not written + expect(writtenFiles.some((p) => p.includes("etc/passwd"))).toBe(false) + expect(writtenFiles.some((p) => p.includes("sensitive.txt"))).toBe(false) + expect(writtenFiles.some((p) => path.isAbsolute(p) && !p.startsWith(mockWorkspacePath))).toBe(false) + }) + + it("should handle malformed YAML gracefully", async () => { + const malformedYaml = ` + customModes: + - slug: test-mode + name: Test Mode + roleDefinition: Test Role + groups: [read + invalid yaml here + ` + + const result = await manager.importModeWithRules(malformedYaml) + + expect(result.success).toBe(false) + expect(result.error).toContain("Invalid YAML format") + }) + + it("should validate mode configuration during import", async () => { + const invalidModeYaml = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "", // Invalid: empty name + roleDefinition: "", // Invalid: empty role definition + groups: ["invalid-group"], // Invalid group + }, + ], + }) + + const result = await manager.importModeWithRules(invalidModeYaml) + + expect(result.success).toBe(false) + expect(result.error).toContain("Invalid mode configuration") + }) + + it("should remove existing rules folder when importing mode without rules", async () => { + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test Role", + groups: ["read"], + // No rulesFiles property - this mode has no rules + }, + ], + }) + + let roomodesContent: any = null + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + if (path === mockRoomodes && roomodesContent) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => { + if (path === mockRoomodes) { + roomodesContent = yaml.parse(content) + } + return Promise.resolve() + }) + ;(fs.rm as Mock).mockResolvedValue(undefined) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + + // Verify that fs.rm was called to remove the existing rules folder + expect(fs.rm).toHaveBeenCalledWith(expect.stringContaining(path.join(".roo", "rules-test-mode")), { + recursive: true, + force: true, + }) + + // Verify mode was imported + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringContaining(".roomodes"), + expect.stringContaining("test-mode"), + "utf-8", + ) + }) + + it("should remove existing rules folder and create new ones when importing mode with rules", async () => { + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test Role", + groups: ["read"], + rulesFiles: [ + { + relativePath: "rules-test-mode/new-rule.md", + content: "New rule content", + }, + ], + }, + ], + }) + + let roomodesContent: any = null + let writtenFiles: Record = {} + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + if (path === mockRoomodes && roomodesContent) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => { + if (path === mockRoomodes) { + roomodesContent = yaml.parse(content) + } else { + writtenFiles[path] = content + } + return Promise.resolve() + }) + ;(fs.rm as Mock).mockResolvedValue(undefined) + ;(fs.mkdir as Mock).mockResolvedValue(undefined) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + + // Verify that fs.rm was called to remove the existing rules folder + expect(fs.rm).toHaveBeenCalledWith(expect.stringContaining(path.join(".roo", "rules-test-mode")), { + recursive: true, + force: true, + }) + + // Verify new rules files were created + expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining("rules-test-mode"), { recursive: true }) + + // Verify file contents + const newRulePath = Object.keys(writtenFiles).find((p) => p.includes("new-rule.md")) + expect(writtenFiles[newRulePath!]).toBe("New rule content") + }) + }) + }) + + describe("checkRulesDirectoryHasContent", () => { + it("should return false when no workspace is available", async () => { + ;(getWorkspacePath as Mock).mockReturnValue(null) + + const result = await manager.checkRulesDirectoryHasContent("test-mode") + + expect(result).toBe(false) + }) + + it("should return false when mode is not in .roomodes file", async () => { + const roomodesContent = { customModes: [{ slug: "other-mode", name: "Other Mode" }] } + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + + const result = await manager.checkRulesDirectoryHasContent("test-mode") + + expect(result).toBe(false) + }) + + it("should return false when .roomodes doesn't exist and mode is not a custom mode", async () => { + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockSettingsPath + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + throw new Error("File not found") + }) + + const result = await manager.checkRulesDirectoryHasContent("test-mode") + + expect(result).toBe(false) + }) + + it("should return false when rules directory doesn't exist", async () => { + const roomodesContent = { customModes: [{ slug: "test-mode", name: "Test Mode" }] } + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockRejectedValue(new Error("Directory not found")) + + const result = await manager.checkRulesDirectoryHasContent("test-mode") + + expect(result).toBe(false) + }) + + it("should return false when rules directory is empty", async () => { + const roomodesContent = { customModes: [{ slug: "test-mode", name: "Test Mode" }] } + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([]) + + const result = await manager.checkRulesDirectoryHasContent("test-mode") + + expect(result).toBe(false) + }) + + it("should return true when rules directory has content files", async () => { + const roomodesContent = { customModes: [{ slug: "test-mode", name: "Test Mode" }] } + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + if (path.includes("rules-test-mode")) { + return "Some rule content" + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "rule1.md", isFile: () => true, parentPath: "/mock/workspace/.roo/rules-test-mode" }, + ]) + + const result = await manager.checkRulesDirectoryHasContent("test-mode") + + expect(result).toBe(true) + }) + + it("should work with global custom modes when .roomodes doesn't exist", async () => { + const settingsContent = { + customModes: [{ slug: "test-mode", name: "Test Mode", groups: ["read"], roleDefinition: "Test Role" }], + } + + // Create a fresh manager instance to avoid cache issues + const freshManager = new CustomModesManager(mockContext, mockOnUpdate) + + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockSettingsPath // .roomodes doesn't exist + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify(settingsContent) + } + if (path.includes("rules-test-mode")) { + return "Some rule content" + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "rule1.md", isFile: () => true, parentPath: "/mock/workspace/.roo/rules-test-mode" }, + ]) + + const result = await freshManager.checkRulesDirectoryHasContent("test-mode") + + expect(result).toBe(true) + }) + }) + + describe("exportModeWithRules", () => { + it("should return error when no workspace is available", async () => { + // Create a fresh manager instance to avoid cache issues + const freshManager = new CustomModesManager(mockContext, mockOnUpdate) + + // Mock no workspace folders + ;(vscode.workspace as any).workspaceFolders = [] + ;(getWorkspacePath as Mock).mockReturnValue(null) + ;(fileExistsAtPath as Mock).mockResolvedValue(false) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + throw new Error("File not found") + }) + + const result = await freshManager.exportModeWithRules("test-mode") + + expect(result.success).toBe(false) + expect(result.error).toBe("No workspace found") + }) + + it("should return error when mode is not found", async () => { + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + throw new Error("File not found") + }) + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockSettingsPath + }) + + const result = await manager.exportModeWithRules("test-mode") + + expect(result.success).toBe(false) + expect(result.error).toBe("Mode not found") + }) + + it("should successfully export mode without rules when rules directory doesn't exist", async () => { + const roomodesContent = { + customModes: [{ slug: "test-mode", name: "Test Mode", roleDefinition: "Test Role", groups: ["read"] }], + } + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockRejectedValue(new Error("Directory not found")) + + const result = await manager.exportModeWithRules("test-mode") + + expect(result.success).toBe(true) + expect(result.yaml).toContain("test-mode") + expect(result.yaml).toContain("Test Mode") + }) + + it("should successfully export mode without rules when no rule files are found", async () => { + const roomodesContent = { + customModes: [{ slug: "test-mode", name: "Test Mode", roleDefinition: "Test Role", groups: ["read"] }], + } + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([]) + + const result = await manager.exportModeWithRules("test-mode") + + expect(result.success).toBe(true) + expect(result.yaml).toContain("test-mode") + }) + + it("should successfully export mode with rules for a custom mode in .roomodes", async () => { + const roomodesContent = { + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test Role", + groups: ["read"], + customInstructions: "Existing instructions", + }, + ], + } + + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + if (path.includes("rules-test-mode")) { + return "New rule content from files" + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "rule1.md", isFile: () => true, parentPath: "/mock/workspace/.roo/rules-test-mode" }, + ]) + + const result = await manager.exportModeWithRules("test-mode") + + expect(result.success).toBe(true) + expect(result.yaml).toContain("test-mode") + expect(result.yaml).toContain("Existing instructions") + expect(result.yaml).toContain("New rule content from files") + // Should NOT delete the rules directory + expect(fs.rm).not.toHaveBeenCalled() + }) + + it("should successfully export mode with rules for a built-in mode customized in .roomodes", async () => { + const roomodesContent = { + customModes: [ + { + slug: "code", + name: "Custom Code Mode", + roleDefinition: "Custom Role", + groups: ["read"], + }, + ], + } + + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + if (path.includes("rules-code")) { + return "Custom rules for code mode" + } + if (path === mockSettingsPath) { + return yaml.stringify({ customModes: [] }) + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "rule1.md", isFile: () => true, parentPath: "/mock/workspace/.roo/rules-code" }, + ]) + + const result = await manager.exportModeWithRules("code") + + expect(result.success).toBe(true) + expect(result.yaml).toContain("Custom Code Mode") + expect(result.yaml).toContain("Custom rules for code mode") + // Should NOT delete the rules directory + expect(fs.rm).not.toHaveBeenCalled() + }) + + it("should handle file read errors gracefully", async () => { + const roomodesContent = { + customModes: [ + { + slug: "test-mode", + name: "Test Mode", + roleDefinition: "Test Role", + groups: ["read"], + }, + ], + } + + ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { + return path === mockRoomodes + }) + ;(fs.readFile as Mock).mockImplementation(async (path: string) => { + if (path === mockRoomodes) { + return yaml.stringify(roomodesContent) + } + if (path.includes("rules-test-mode")) { + throw new Error("Permission denied") + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "rule1.md", isFile: () => true, parentPath: "/mock/workspace/.roo/rules-test-mode" }, + ]) + + const result = await manager.exportModeWithRules("test-mode") + + // Should still succeed even if file read fails + expect(result.success).toBe(true) + expect(result.yaml).toContain("test-mode") + }) }) }) diff --git a/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts b/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts new file mode 100644 index 0000000000..251a33d211 --- /dev/null +++ b/src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts @@ -0,0 +1,474 @@ +// 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 4ba43f475e..1f6bd5f28e 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/src/core/config/__tests__/importExport.spec.ts @@ -8,10 +8,11 @@ import * as vscode from "vscode" import type { ProviderName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { importSettings, exportSettings } from "../importExport" +import { importSettings, importSettingsFromFile, importSettingsWithFeedback, 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" @@ -19,6 +20,8 @@ vi.mock("vscode", () => ({ window: { showOpenDialog: vi.fn(), showSaveDialog: vi.fn(), + showErrorMessage: vi.fn(), + showInformationMessage: vi.fn(), }, Uri: { file: vi.fn((filePath) => ({ fsPath: filePath })), @@ -30,10 +33,20 @@ 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", () => ({ @@ -43,6 +56,8 @@ vi.mock("os", () => ({ homedir: vi.fn(() => "/mock/home"), })) +vi.mock("../../../utils/safeWriteJson") + describe("importExport", () => { let mockProviderSettingsManager: ReturnType> let mockContextProxy: ReturnType> @@ -93,7 +108,7 @@ describe("importExport", () => { customModesManager: mockCustomModesManager, }) - expect(result).toEqual({ success: false }) + expect(result).toEqual({ success: false, error: "User cancelled file selection" }) expect(vscode.window.showOpenDialog).toHaveBeenCalledWith({ filters: { JSON: ["json"] }, @@ -143,9 +158,12 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ - ...previousProviderProfiles, currentApiConfigName: "test", - apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } }, + 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 }) @@ -216,11 +234,12 @@ 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. @@ -294,9 +313,11 @@ describe("importExport", () => { }) expect(result.success).toBe(true) - expect(result.providerProfiles?.apiConfigs["openai"]).toBeDefined() - expect(result.providerProfiles?.apiConfigs["default"]).toBeDefined() - expect(result.providerProfiles?.apiConfigs["default"].apiProvider).toBe("anthropic") + 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") + } }) it("should call updateCustomMode for each custom mode in config", async () => { @@ -334,6 +355,87 @@ 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", () => { @@ -384,11 +486,10 @@ describe("importExport", () => { expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true }) - expect(fs.writeFile).toHaveBeenCalledWith( - "/mock/path/roo-code-settings.json", - JSON.stringify({ providerProfiles: mockProviderProfiles, globalSettings: mockGlobalSettings }, null, 2), - "utf-8", - ) + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, + }) }) it("should include globalSettings when allowedMaxRequests is null", async () => { @@ -417,11 +518,10 @@ describe("importExport", () => { contextProxy: mockContextProxy, }) - expect(fs.writeFile).toHaveBeenCalledWith( - "/mock/path/roo-code-settings.json", - JSON.stringify({ providerProfiles: mockProviderProfiles, globalSettings: mockGlobalSettings }, null, 2), - "utf-8", - ) + expect(safeWriteJson).toHaveBeenCalledWith("/mock/path/roo-code-settings.json", { + providerProfiles: mockProviderProfiles, + globalSettings: mockGlobalSettings, + }) }) it("should handle errors during the export process", async () => { @@ -436,7 +536,8 @@ describe("importExport", () => { }) mockContextProxy.export.mockResolvedValue({ mode: "code" }) - ;(fs.writeFile as Mock).mockRejectedValue(new Error("Write error")) + // Simulate an error during the safeWriteJson operation + ;(safeWriteJson as Mock).mockRejectedValueOnce(new Error("Safe write error")) await exportSettings({ providerSettingsManager: mockProviderSettingsManager, @@ -447,8 +548,10 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true }) - expect(fs.writeFile).toHaveBeenCalled() + expect(safeWriteJson).toHaveBeenCalled() // safeWriteJson is called, but it will throw // 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 () => { @@ -474,7 +577,7 @@ describe("importExport", () => { expect(mockProviderSettingsManager.export).toHaveBeenCalled() expect(mockContextProxy.export).toHaveBeenCalled() expect(fs.mkdir).toHaveBeenCalled() - expect(fs.writeFile).not.toHaveBeenCalled() // Should not be called since mkdir failed. + expect(safeWriteJson).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 4830a5f987..6ffbbf7136 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import os from "os" import * as path from "path" import fs from "fs/promises" @@ -11,8 +12,9 @@ import { TelemetryService } from "@roo-code/telemetry" import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager" import { ContextProxy } from "./ContextProxy" import { CustomModesManager } from "./CustomModesManager" +import { t } from "../../i18n" -type ImportOptions = { +export type ImportOptions = { providerSettingsManager: ProviderSettingsManager contextProxy: ContextProxy customModesManager: CustomModesManager @@ -22,17 +24,22 @@ type ExportOptions = { providerSettingsManager: ProviderSettingsManager contextProxy: ContextProxy } - -export const importSettings = async ({ providerSettingsManager, contextProxy, customModesManager }: ImportOptions) => { - const uris = await vscode.window.showOpenDialog({ - filters: { JSON: ["json"] }, - canSelectMany: false, - }) - - if (!uris) { - return { success: false } +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, +) { const schema = z.object({ providerProfiles: providerProfilesSchema, globalSettings: globalSettingsSchema.optional(), @@ -41,8 +48,9 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu try { const previousProviderProfiles = await providerSettingsManager.export() - const data = JSON.parse(await fs.readFile(uris[0].fsPath, "utf-8")) - const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse(data) + const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse( + JSON.parse(await fs.readFile(filePath, "utf-8")), + ) const providerProfiles = { currentApiConfigName: newProviderProfiles.currentApiConfigName, @@ -60,7 +68,7 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu (globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)), ) - await providerSettingsManager.import(newProviderProfiles) + await providerSettingsManager.import(providerProfiles) await contextProxy.setValues(globalSettings) // Set the current provider. @@ -92,6 +100,45 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu } } +/** + * 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"] }, @@ -116,6 +163,47 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }: const dirname = path.dirname(uri.fsPath) await fs.mkdir(dirname, { recursive: true }) - await fs.writeFile(uri.fsPath, JSON.stringify({ providerProfiles, globalSettings }, null, 2), "utf-8") + await safeWriteJson(uri.fsPath, { providerProfiles, globalSettings }) } catch (e) {} } + +/** + * 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 })) + } +} diff --git a/src/core/context-tracking/FileContextTracker.ts b/src/core/context-tracking/FileContextTracker.ts index 323bb4122f..5741b62cfc 100644 --- a/src/core/context-tracking/FileContextTracker.ts +++ b/src/core/context-tracking/FileContextTracker.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as vscode from "vscode" import { getTaskDirectoryPath } from "../../utils/storage" @@ -130,7 +131,7 @@ export class FileContextTracker { const globalStoragePath = this.getContextProxy()!.globalStorageUri.fsPath const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.taskMetadata) - await fs.writeFile(filePath, JSON.stringify(metadata, null, 2)) + await safeWriteJson(filePath, metadata) } 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 1554201675..d875d723a1 100644 --- a/src/core/diff/strategies/multi-file-search-replace.ts +++ b/src/core/diff/strategies/multi-file-search-replace.ts @@ -95,6 +95,8 @@ export class MultiFileSearchReplaceDiffStrategy implements DiffStrategy { Description: Request to apply targeted modifications to one or more files by searching for specific sections of content and replacing them. This tool supports both single-file and multi-file operations, allowing you to make changes across multiple files in a single request. +**IMPORTANT: You MUST use multiple files in a single operation whenever possible to maximize efficiency and minimize back-and-forth.** + You can perform multiple distinct search and replace operations within a single \`apply_diff\` call by providing multiple SEARCH/REPLACE blocks in the \`diff\` parameter. This is the preferred way to make several targeted changes efficiently. The SEARCH section must exactly match existing content including whitespace and indentation. @@ -157,7 +159,7 @@ def calculate_total(items): -Search/Replace content with multi edits in one file: +Search/Replace content with multi edits across multiple files: diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 9e740a6571..b90ef4072d 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -93,7 +93,7 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy { getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string { return `## apply_diff Description: Request to apply targeted modifications to an existing file by searching for specific sections of content and replacing them. This tool is ideal for precise, surgical edits when you know the exact content to change. It helps maintain proper indentation and formatting. -You can perform multiple distinct search and replace operations within a single \`apply_diff\` call by providing multiple SEARCH/REPLACE blocks in the \`diff\` parameter. This is the preferred way to make several targeted changes to one file efficiently. +You can perform multiple distinct search and replace operations within a single \`apply_diff\` call by providing multiple SEARCH/REPLACE blocks in the \`diff\` parameter. This is the preferred way to make several targeted changes efficiently. The SEARCH section must exactly match existing content including whitespace and indentation. If you're not confident in the exact content to search for, use the read_file tool first to get the exact content. When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file. @@ -145,7 +145,7 @@ def calculate_total(items): \`\`\` -Search/Replace content with multi edits: +Search/Replace content with multiple edits: \`\`\` <<<<<<< SEARCH :start_line:1 diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts index 02423f8ebd..f7e63d86c6 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts @@ -146,7 +146,6 @@ describe("getEnvironmentDetails", () => { expect(result).toContain("# VSCode Visible Files") expect(result).toContain("# VSCode Open Tabs") expect(result).toContain("# Current Time") - expect(result).toContain("# Current Context Size (Tokens)") expect(result).toContain("# Current Cost") expect(result).toContain("# Current Mode") expect(result).toContain("test-model") diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 944eb94190..9120b5e8c7 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -197,13 +197,8 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo // Add context tokens information. const { contextTokens, totalCost } = getApiMetrics(cline.clineMessages) - const { id: modelId, info: modelInfo } = cline.api.getModel() - const contextWindow = modelInfo.contextWindow + const { id: modelId } = cline.api.getModel() - const contextPercentage = - contextTokens && contextWindow ? Math.round((contextTokens / contextWindow) * 100) : undefined - - details += `\n\n# Current Context Size (Tokens)\n${contextTokens ? `${contextTokens.toLocaleString()} (${contextPercentage}%)` : "(Not available)"}` details += `\n\n# Current Cost\n${totalCost !== null ? `$${totalCost.toFixed(2)}` : "(Not available)"}` // Add current mode and any mode-specific warnings. diff --git a/src/core/mentions/__tests__/index.spec.ts b/src/core/mentions/__tests__/index.spec.ts index 0f97c1ef89..e65dbb44e0 100644 --- a/src/core/mentions/__tests__/index.spec.ts +++ b/src/core/mentions/__tests__/index.spec.ts @@ -1,310 +1,160 @@ -import type { Mock } from "vitest" - -// Mock modules - must come before imports -vi.mock("vscode", () => { - const createMockUri = (scheme: string, path: string) => ({ - scheme, - authority: "", - path, - query: "", - fragment: "", - fsPath: path, - with: vi.fn(), - toString: () => path, - toJSON: () => ({ - scheme, - authority: "", - path, - query: "", - fragment: "", - }), - }) +// npx vitest core/mentions/__tests__/index.spec.ts - const mockExecuteCommand = vi.fn() - const mockOpenExternal = vi.fn() - const mockShowErrorMessage = vi.fn() - - return { - workspace: { - workspaceFolders: [ - { - uri: { fsPath: "/test/workspace" }, - }, - ] as { uri: { fsPath: string } }[] | undefined, - getWorkspaceFolder: vi.fn().mockReturnValue("/test/workspace"), - fs: { - stat: vi.fn(), - writeFile: vi.fn(), - }, - openTextDocument: vi.fn().mockResolvedValue({}), - }, - window: { - showErrorMessage: mockShowErrorMessage, - showInformationMessage: vi.fn(), - showWarningMessage: vi.fn(), - createTextEditorDecorationType: vi.fn(), - createOutputChannel: vi.fn(), - createWebviewPanel: vi.fn(), - showTextDocument: vi.fn().mockResolvedValue({}), - activeTextEditor: undefined as - | undefined - | { - document: { - uri: { fsPath: string } - } - }, - }, - commands: { - executeCommand: mockExecuteCommand, - }, - env: { - openExternal: mockOpenExternal, - }, - Uri: { - parse: vi.fn((url: string) => createMockUri("https", url)), - file: vi.fn((path: string) => createMockUri("file", path)), - }, - Position: vi.fn(), - Range: vi.fn(), - TextEdit: vi.fn(), - WorkspaceEdit: vi.fn(), - DiagnosticSeverity: { - Error: 0, - Warning: 1, - Information: 2, - Hint: 3, - }, - } -}) -vi.mock("../../../services/browser/UrlContentFetcher") -vi.mock("../../../utils/git") -vi.mock("../../../utils/path") -vi.mock("fs/promises", () => ({ - default: { - stat: vi.fn(), - readdir: vi.fn(), +import { describe, it, expect, vi, beforeEach } from "vitest" +import * as vscode from "vscode" +import { parseMentions } from "../index" +import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher" +import { t } from "../../../i18n" + +// Mock vscode +vi.mock("vscode", () => ({ + window: { + showErrorMessage: vi.fn(), }, - stat: vi.fn(), - readdir: vi.fn(), -})) -vi.mock("../../../integrations/misc/open-file", () => ({ - openFile: vi.fn(), -})) -vi.mock("../../../integrations/misc/extract-text", () => ({ - extractTextFromFile: vi.fn(), })) -// Now import the modules that use the mocks -import { parseMentions, openMention } from "../index" -import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher" -import * as git from "../../../utils/git" -import { getWorkspacePath } from "../../../utils/path" -import fs from "fs/promises" -import * as path from "path" -import { openFile } from "../../../integrations/misc/open-file" -import { extractTextFromFile } from "../../../integrations/misc/extract-text" -import * as vscode from "vscode" -;(getWorkspacePath as Mock).mockReturnValue("/test/workspace") +// Mock i18n +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string) => key), +})) -describe("mentions", () => { - const mockCwd = "/test/workspace" +describe("parseMentions - URL error handling", () => { let mockUrlContentFetcher: UrlContentFetcher + let consoleErrorSpy: any beforeEach(() => { vi.clearAllMocks() + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) - // Create a mock instance with just the methods we need mockUrlContentFetcher = { - launchBrowser: vi.fn().mockResolvedValue(undefined), - closeBrowser: vi.fn().mockResolvedValue(undefined), - urlToMarkdown: vi.fn().mockResolvedValue(""), - } as unknown as UrlContentFetcher - - // Reset all vscode mocks using vi.mocked - vi.mocked(vscode.workspace.fs.stat).mockReset() - vi.mocked(vscode.workspace.fs.writeFile).mockReset() - vi.mocked(vscode.workspace.openTextDocument) - .mockReset() - .mockResolvedValue({} as any) - vi.mocked(vscode.window.showTextDocument) - .mockReset() - .mockResolvedValue({} as any) - vi.mocked(vscode.window.showErrorMessage).mockReset() - vi.mocked(vscode.commands.executeCommand).mockReset() - vi.mocked(vscode.env.openExternal).mockReset() + launchBrowser: vi.fn(), + urlToMarkdown: vi.fn(), + closeBrowser: vi.fn(), + } as any + }) + + it("should handle timeout errors with appropriate message", async () => { + const timeoutError = new Error("Navigation timeout of 30000 ms exceeded") + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(timeoutError) + + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) + + expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching URL https://example.com:", timeoutError) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url") + expect(result).toContain("Error fetching content: Navigation timeout of 30000 ms exceeded") }) - describe("parseMentions", () => { - let mockUrlFetcher: UrlContentFetcher + it("should handle DNS resolution errors", async () => { + const dnsError = new Error("net::ERR_NAME_NOT_RESOLVED") + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(dnsError) - beforeEach(() => { - mockUrlFetcher = new (UrlContentFetcher as any)() - ;(fs.stat as Mock).mockResolvedValue({ isFile: () => true, isDirectory: () => false }) - ;(extractTextFromFile as Mock).mockResolvedValue("Mock file content") - }) + const result = await parseMentions("Check @https://nonexistent.example", "/test", mockUrlContentFetcher) - it("should parse git commit mentions", async () => { - const commitHash = "abc1234" - const commitInfo = `abc1234 Fix bug in parser + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url") + expect(result).toContain("Error fetching content: net::ERR_NAME_NOT_RESOLVED") + }) -Author: John Doe -Date: Mon Jan 5 23:50:06 2025 -0500 + it("should handle network disconnection errors", async () => { + const networkError = new Error("net::ERR_INTERNET_DISCONNECTED") + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(networkError) -Detailed commit message with multiple lines -- Fixed parsing issue -- Added tests` + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) - vi.mocked(git.getCommitInfo).mockResolvedValue(commitInfo) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url") + expect(result).toContain("Error fetching content: net::ERR_INTERNET_DISCONNECTED") + }) - const result = await parseMentions(`Check out this commit @${commitHash}`, mockCwd, mockUrlContentFetcher) + it("should handle 403 Forbidden errors", async () => { + const forbiddenError = new Error("403 Forbidden") + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(forbiddenError) - expect(result).toContain(`'${commitHash}' (see below for commit info)`) - expect(result).toContain(``) - expect(result).toContain(commitInfo) - }) + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) - it("should handle errors fetching git info", async () => { - const commitHash = "abc1234" - const errorMessage = "Failed to get commit info" + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url") + expect(result).toContain("Error fetching content: 403 Forbidden") + }) - vi.mocked(git.getCommitInfo).mockRejectedValue(new Error(errorMessage)) + it("should handle 404 Not Found errors", async () => { + const notFoundError = new Error("404 Not Found") + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(notFoundError) - const result = await parseMentions(`Check out this commit @${commitHash}`, mockCwd, mockUrlContentFetcher) + const result = await parseMentions("Check @https://example.com/missing", "/test", mockUrlContentFetcher) - expect(result).toContain(`'${commitHash}' (see below for commit info)`) - expect(result).toContain(``) - expect(result).toContain(`Error fetching commit info: ${errorMessage}`) - }) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url") + expect(result).toContain("Error fetching content: 404 Not Found") + }) - it("should correctly parse mentions with escaped spaces and fetch content", async () => { - const text = "Please check the file @/path/to/file\\ with\\ spaces.txt" - const expectedUnescaped = "path/to/file with spaces.txt" // Note: leading '/' removed by slice(1) in parseMentions - const expectedAbsPath = path.resolve(mockCwd, expectedUnescaped) + it("should handle generic errors with fallback message", async () => { + const genericError = new Error("Some unexpected error") + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(genericError) - const result = await parseMentions(text, mockCwd, mockUrlFetcher) + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) - // Check if fs.stat was called with the unescaped path - expect(fs.stat).toHaveBeenCalledWith(expectedAbsPath) - // Check if extractTextFromFile was called with the unescaped path - expect(extractTextFromFile).toHaveBeenCalledWith(expectedAbsPath) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url") + expect(result).toContain("Error fetching content: Some unexpected error") + }) - // Check the output format - expect(result).toContain(`'path/to/file\\ with\\ spaces.txt' (see below for file content)`) - expect(result).toContain( - `\nMock file content\n`, - ) - }) + it("should handle non-Error objects thrown", async () => { + const nonErrorObject = { code: "UNKNOWN", details: "Something went wrong" } + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockRejectedValue(nonErrorObject) - it("should handle folder mentions with escaped spaces", async () => { - const text = "Look in @/my\\ documents/folder\\ name/" - const expectedUnescaped = "my documents/folder name/" - const expectedAbsPath = path.resolve(mockCwd, expectedUnescaped) - ;(fs.stat as Mock).mockResolvedValue({ isFile: () => false, isDirectory: () => true }) - ;(fs.readdir as Mock).mockResolvedValue([]) // Empty directory + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) - const result = await parseMentions(text, mockCwd, mockUrlFetcher) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.url_fetch_error_with_url") + expect(result).toContain("Error fetching content:") + }) - expect(fs.stat).toHaveBeenCalledWith(expectedAbsPath) - expect(fs.readdir).toHaveBeenCalledWith(expectedAbsPath, { withFileTypes: true }) - expect(result).toContain(`'my\\ documents/folder\\ name/' (see below for folder content)`) - expect(result).toContain(``) // Content check might be more complex - }) + it("should handle browser launch errors correctly", async () => { + const launchError = new Error("Failed to launch browser") + vi.mocked(mockUrlContentFetcher.launchBrowser).mockRejectedValue(launchError) - it("should handle errors when accessing paths with escaped spaces", async () => { - const text = "Check @/nonexistent\\ file.txt" - const expectedUnescaped = "nonexistent file.txt" - const expectedAbsPath = path.resolve(mockCwd, expectedUnescaped) - const mockError = new Error("ENOENT: no such file or directory") - ;(fs.stat as Mock).mockRejectedValue(mockError) + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Error fetching content for https://example.com: Failed to launch browser", + ) + expect(result).toContain("Error fetching content: Failed to launch browser") + // Should not attempt to fetch URL if browser launch failed + expect(mockUrlContentFetcher.urlToMarkdown).not.toHaveBeenCalled() + }) + + it("should handle browser launch errors without message property", async () => { + const launchError = "String error" + vi.mocked(mockUrlContentFetcher.launchBrowser).mockRejectedValue(launchError) + + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "Error fetching content for https://example.com: String error", + ) + expect(result).toContain("Error fetching content: String error") + }) - const result = await parseMentions(text, mockCwd, mockUrlFetcher) + it("should successfully fetch URL content when no errors occur", async () => { + vi.mocked(mockUrlContentFetcher.urlToMarkdown).mockResolvedValue("# Example Content\n\nThis is the content.") - expect(fs.stat).toHaveBeenCalledWith(expectedAbsPath) - expect(result).toContain( - `\nError fetching content: Failed to access path "nonexistent\\ file.txt": ${mockError.message}\n`, - ) - }) + const result = await parseMentions("Check @https://example.com", "/test", mockUrlContentFetcher) - // Add more tests for parseMentions if needed (URLs, other mentions combined with escaped paths etc.) + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + expect(result).toContain('') + expect(result).toContain("# Example Content\n\nThis is the content.") + expect(result).toContain("") }) - describe("openMention", () => { - beforeEach(() => { - ;(getWorkspacePath as Mock).mockReturnValue(mockCwd) - }) - - it("should handle URLs", async () => { - const url = "https://example.com" - await openMention(url) - const mockUri = vscode.Uri.parse(url) - expect(vscode.env.openExternal).toHaveBeenCalled() - const calledArg = (vscode.env.openExternal as Mock).mock.calls[0][0] - expect(calledArg).toEqual( - expect.objectContaining({ - scheme: mockUri.scheme, - authority: mockUri.authority, - path: mockUri.path, - query: mockUri.query, - fragment: mockUri.fragment, - }), - ) - }) - - it("should unescape file path before opening", async () => { - const mention = "/file\\ with\\ spaces.txt" - const expectedUnescaped = "file with spaces.txt" - const expectedAbsPath = path.resolve(mockCwd, expectedUnescaped) - - await openMention(mention) - - expect(openFile).toHaveBeenCalledWith(expectedAbsPath) - expect(vscode.commands.executeCommand).not.toHaveBeenCalled() - }) - - it("should unescape folder path before revealing", async () => { - const mention = "/folder\\ with\\ spaces/" - const expectedUnescaped = "folder with spaces/" - const expectedAbsPath = path.resolve(mockCwd, expectedUnescaped) - const expectedUri = { fsPath: expectedAbsPath } // From mock - ;(vscode.Uri.file as Mock).mockReturnValue(expectedUri) - - await openMention(mention) - - expect(vscode.commands.executeCommand).toHaveBeenCalledWith("revealInExplorer", expectedUri) - expect(vscode.Uri.file).toHaveBeenCalledWith(expectedAbsPath) - expect(openFile).not.toHaveBeenCalled() - }) - - it("should handle mentions without paths correctly", async () => { - await openMention("problems") - expect(vscode.commands.executeCommand).toHaveBeenCalledWith("workbench.actions.view.problems") - - await openMention("terminal") - expect(vscode.commands.executeCommand).toHaveBeenCalledWith("workbench.action.terminal.focus") - - await openMention("http://example.com") - expect(vscode.env.openExternal).toHaveBeenCalled() // Check if called, specific URI mock might be needed for detailed check - - await openMention("git-changes") // Assuming no specific action for this yet - // Add expectations if an action is defined for git-changes - - await openMention("a1b2c3d") // Assuming no specific action for commit hashes yet - // Add expectations if an action is defined for commit hashes - }) - - it("should do nothing if mention is undefined or empty", async () => { - await openMention(undefined) - await openMention("") - expect(openFile).not.toHaveBeenCalled() - expect(vscode.commands.executeCommand).not.toHaveBeenCalled() - expect(vscode.env.openExternal).not.toHaveBeenCalled() - }) - - it("should do nothing if cwd is not available", async () => { - ;(getWorkspacePath as Mock).mockReturnValue(undefined) - await openMention("/some\\ path.txt") - expect(openFile).not.toHaveBeenCalled() - expect(vscode.commands.executeCommand).not.toHaveBeenCalled() - }) + it("should handle multiple URLs with mixed success and failure", async () => { + vi.mocked(mockUrlContentFetcher.urlToMarkdown) + .mockResolvedValueOnce("# First Site") + .mockRejectedValueOnce(new Error("timeout")) + + const result = await parseMentions( + "Check @https://example1.com and @https://example2.com", + "/test", + mockUrlContentFetcher, + ) + + expect(result).toContain('') + expect(result).toContain("# First Site") + expect(result).toContain('') + expect(result).toContain("Error fetching content: timeout") }) }) diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 8ae4f7f131..780b27d1f7 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -19,6 +19,32 @@ import { FileContextTracker } from "../context-tracking/FileContextTracker" import { RooIgnoreController } from "../ignore/RooIgnoreController" +import { t } from "../../i18n" + +function getUrlErrorMessage(error: unknown): string { + const errorMessage = error instanceof Error ? error.message : String(error) + + // Check for common error patterns and return appropriate message + if (errorMessage.includes("timeout")) { + return t("common:errors.url_timeout") + } + if (errorMessage.includes("net::ERR_NAME_NOT_RESOLVED")) { + return t("common:errors.url_not_found") + } + if (errorMessage.includes("net::ERR_INTERNET_DISCONNECTED")) { + return t("common:errors.no_internet") + } + if (errorMessage.includes("403") || errorMessage.includes("Forbidden")) { + return t("common:errors.url_forbidden") + } + if (errorMessage.includes("404") || errorMessage.includes("Not Found")) { + return t("common:errors.url_page_not_found") + } + + // Default error message + return t("common:errors.url_fetch_failed", { error: errorMessage }) +} + export async function openMention(mention?: string): Promise { if (!mention) { return @@ -84,7 +110,8 @@ export async function parseMentions( await urlContentFetcher.launchBrowser() } catch (error) { launchBrowserError = error - vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`) + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${errorMessage}`) } } @@ -92,14 +119,28 @@ export async function parseMentions( if (mention.startsWith("http")) { let result: string if (launchBrowserError) { - result = `Error fetching content: ${launchBrowserError.message}` + const errorMessage = + launchBrowserError instanceof Error ? launchBrowserError.message : String(launchBrowserError) + result = `Error fetching content: ${errorMessage}` } else { try { const markdown = await urlContentFetcher.urlToMarkdown(mention) result = markdown } catch (error) { - vscode.window.showErrorMessage(`Error fetching content for ${mention}: ${error.message}`) - result = `Error fetching content: ${error.message}` + console.error(`Error fetching URL ${mention}:`, error) + + // Get raw error message for AI + const rawErrorMessage = error instanceof Error ? error.message : String(error) + + // Get localized error message for UI notification + const localizedErrorMessage = getUrlErrorMessage(error) + + vscode.window.showErrorMessage( + t("common:errors.url_fetch_error_with_url", { url: mention, error: localizedErrorMessage }), + ) + + // Send raw error message to AI model + result = `Error fetching content: ${rawErrorMessage}` } } parsedText += `\n\n\n${result}\n` 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 dd621b34b8..47edfe467a 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 @@ -277,6 +277,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -284,6 +286,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -297,6 +302,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -468,9 +483,9 @@ Mode-specific Instructions: 4. 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. -5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file. +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. -6. Use the switch_mode tool to request that the user switch to another mode to implement the solution. +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** Rules: # Rules from .clinerules-architect: diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-rules.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-rules.snap index 8a4da6613d..1935611f44 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-rules.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-rules.snap @@ -6,7 +6,7 @@ USER'S CUSTOM INSTRUCTIONS The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. Rules: -# Rules from .clinerules-architect: +# Rules from .clinerules-code: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 3b8f357e8c..e665309432 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 @@ -174,6 +174,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -181,6 +183,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -194,6 +199,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/code-mode-rules.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/code-mode-rules.snap index 1935611f44..8a4da6613d 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/code-mode-rules.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/code-mode-rules.snap @@ -6,7 +6,7 @@ USER'S CUSTOM INSTRUCTIONS The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/combined-custom-instructions.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/combined-custom-instructions.snap index d9e638e9fa..2a2c4ba78a 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/combined-custom-instructions.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/combined-custom-instructions.snap @@ -12,7 +12,7 @@ Mode-specific Instructions: Custom test instructions Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/empty-mode-instructions.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/empty-mode-instructions.snap index 1935611f44..8a4da6613d 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/empty-mode-instructions.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/empty-mode-instructions.snap @@ -6,7 +6,7 @@ USER'S CUSTOM INSTRUCTIONS The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/generic-rules-fallback.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/generic-rules-fallback.snap index 1935611f44..8a4da6613d 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/generic-rules-fallback.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/generic-rules-fallback.snap @@ -6,7 +6,7 @@ USER'S CUSTOM INSTRUCTIONS The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/global-and-mode-instructions.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/global-and-mode-instructions.snap index 2fb6cfece2..f4cd5cbec9 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/global-and-mode-instructions.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/global-and-mode-instructions.snap @@ -12,7 +12,7 @@ Mode-specific Instructions: Mode-specific instructions Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 719e7f0ea7..c964d5f6ed 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -269,28 +269,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## use_mcp_tool Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters. Parameters: @@ -348,6 +326,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -355,6 +335,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -368,6 +351,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -543,8 +536,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 9f3ad102e8..5a25768204 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -269,28 +269,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## use_mcp_tool Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters. Parameters: @@ -348,6 +326,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -355,6 +335,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -368,6 +351,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -549,8 +542,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 2c7e9ec165..f74e394d2d 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -274,28 +274,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -304,6 +282,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -311,6 +291,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -324,6 +307,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -486,8 +479,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/prioritized-instructions-order.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/prioritized-instructions-order.snap index 5adfbb744e..b72616ec3c 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/prioritized-instructions-order.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/prioritized-instructions-order.snap @@ -12,7 +12,7 @@ Mode-specific Instructions: Second instruction Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/trimmed-mode-instructions.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/trimmed-mode-instructions.snap index 28497df14f..18dc9547ab 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/trimmed-mode-instructions.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/trimmed-mode-instructions.snap @@ -9,7 +9,7 @@ Mode-specific Instructions: Custom mode instructions Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/undefined-mode-instructions.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/undefined-mode-instructions.snap index 1935611f44..8a4da6613d 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/undefined-mode-instructions.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/undefined-mode-instructions.snap @@ -6,7 +6,7 @@ USER'S CUSTOM INSTRUCTIONS The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-custom-instructions.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-custom-instructions.snap index 9ee1dd3365..9e1f02ec2f 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-custom-instructions.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-custom-instructions.snap @@ -9,7 +9,7 @@ Mode-specific Instructions: Custom test instructions Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-preferred-language.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-preferred-language.snap index 2fba8f9cbb..c563fa36fb 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-preferred-language.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/with-preferred-language.snap @@ -9,7 +9,7 @@ Language Preference: You should always speak and think in the "es" language. Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 b4220ce2e8..47edfe467a 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -269,28 +269,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -299,6 +277,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -306,6 +286,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -319,6 +302,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -481,8 +474,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 6248770325..ecd22a6d6e 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -322,28 +322,6 @@ Example: Requesting to click on the element at coordinates 450,300 450,300 -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -352,6 +330,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -359,6 +339,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -372,6 +355,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -537,8 +530,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 b4220ce2e8..47edfe467a 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -269,28 +269,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -299,6 +277,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -306,6 +286,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -319,6 +302,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -481,8 +474,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 1fce8bf785..9514c93ea3 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -168,7 +168,7 @@ Examples: ## apply_diff Description: Request to apply targeted modifications to an existing file by searching for specific sections of content and replacing them. This tool is ideal for precise, surgical edits when you know the exact content to change. It helps maintain proper indentation and formatting. -You can perform multiple distinct search and replace operations within a single `apply_diff` call by providing multiple SEARCH/REPLACE blocks in the `diff` parameter. This is the preferred way to make several targeted changes to one file efficiently. +You can perform multiple distinct search and replace operations within a single `apply_diff` call by providing multiple SEARCH/REPLACE blocks in the `diff` parameter. This is the preferred way to make several targeted changes efficiently. The SEARCH section must exactly match existing content including whitespace and indentation. If you're not confident in the exact content to search for, use the read_file tool first to get the exact content. When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file. @@ -220,7 +220,7 @@ def calculate_total(items): ``` -Search/Replace content with multi edits: +Search/Replace content with multiple edits: ``` <<<<<<< SEARCH :start_line:1 @@ -357,28 +357,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -387,6 +365,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -394,6 +374,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -407,6 +390,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -569,8 +562,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 b4220ce2e8..47edfe467a 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -269,28 +269,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -299,6 +277,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -306,6 +286,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -319,6 +302,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -481,8 +474,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 d07dc1d874..32acd4c169 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -322,28 +322,6 @@ Example: Requesting to click on the element at coordinates 450,300 450,300 -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -352,6 +330,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -359,6 +339,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -372,6 +355,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -537,8 +530,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 9f3ad102e8..5a25768204 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -269,28 +269,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## use_mcp_tool Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters. Parameters: @@ -348,6 +326,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -355,6 +335,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -368,6 +351,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -549,8 +542,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of 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 b4220ce2e8..47edfe467a 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 @@ -1,4 +1,4 @@ -You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +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. ==== @@ -269,28 +269,6 @@ Examples: true -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: `touch ./testdata/example.file`, `dir ./examples/model1/data/yaml`, or `go test ./cmd/front --config ./cmd/front/config.yml`. If directed by the user, you may open a terminal in a different directory by using the `cwd` parameter. -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -- cwd: (optional) The working directory to execute the command in (default: /test/path) -Usage: - -Your command here -Working directory path (optional) - - -Example: Requesting to execute npm run dev - -npm run dev - - -Example: Requesting to execute ls in a specific directory if directed - -ls -la -/home/user/projects - - ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -299,6 +277,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -306,6 +286,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -319,6 +302,16 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + + + ## attempt_completion Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. @@ -481,8 +474,21 @@ The following additional instructions are provided by the user, and should be fo Language Preference: You should always speak and think in the "en" language. +Mode-specific Instructions: +1. Do some information gathering (for example using read_file or search_files) to get more context about the task. + +2. You should also ask the user clarifying questions to get a better understanding of the task. + +3. 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. + +4. 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. + +5. Use the switch_mode tool to request that the user switch to another mode to implement the solution. + +**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.** + Rules: -# Rules from .clinerules-code: +# Rules from .clinerules-architect: Mock mode-specific rules # Rules from .clinerules: Mock generic rules \ No newline at end of file diff --git a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts index 111cefaf27..9c8e003143 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.spec.ts +++ b/src/core/prompts/sections/__tests__/custom-instructions.spec.ts @@ -221,6 +221,106 @@ describe("loadRuleFiles", () => { expect(readFileMock).toHaveBeenCalledWith(expectedFile2Path, "utf-8") }) + it("should filter out cache files from .roo/rules/ directory", async () => { + // Simulate .roo/rules directory exists + statMock.mockResolvedValueOnce({ + isDirectory: vi.fn().mockReturnValue(true), + } as any) + + // Simulate listing files including cache files + readdirMock.mockResolvedValueOnce([ + { name: "rule1.txt", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: ".DS_Store", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: "Thumbs.db", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: "rule2.md", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { name: "cache.log", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { + name: "backup.bak", + isFile: () => true, + isSymbolicLink: () => false, + parentPath: "/fake/path/.roo/rules", + }, + { name: "temp.tmp", isFile: () => true, isSymbolicLink: () => false, parentPath: "/fake/path/.roo/rules" }, + { + name: "script.pyc", + isFile: () => true, + isSymbolicLink: () => false, + parentPath: "/fake/path/.roo/rules", + }, + ] as any) + + statMock.mockImplementation((path) => { + return Promise.resolve({ + isFile: vi.fn().mockReturnValue(true), + }) as any + }) + + readFileMock.mockImplementation((filePath: PathLike) => { + const pathStr = filePath.toString() + const normalizedPath = pathStr.replace(/\\/g, "/") + + // Only rule files should be read - cache files should be skipped + if (normalizedPath === "/fake/path/.roo/rules/rule1.txt") { + return Promise.resolve("rule 1 content") + } + if (normalizedPath === "/fake/path/.roo/rules/rule2.md") { + return Promise.resolve("rule 2 content") + } + + // Cache files should not be read due to filtering + // If they somehow are read, return recognizable content + if (normalizedPath === "/fake/path/.roo/rules/.DS_Store") { + return Promise.resolve("DS_STORE_BINARY_CONTENT") + } + if (normalizedPath === "/fake/path/.roo/rules/Thumbs.db") { + return Promise.resolve("THUMBS_DB_CONTENT") + } + if (normalizedPath === "/fake/path/.roo/rules/backup.bak") { + return Promise.resolve("BACKUP_CONTENT") + } + if (normalizedPath === "/fake/path/.roo/rules/cache.log") { + return Promise.resolve("LOG_CONTENT") + } + if (normalizedPath === "/fake/path/.roo/rules/temp.tmp") { + return Promise.resolve("TEMP_CONTENT") + } + if (normalizedPath === "/fake/path/.roo/rules/script.pyc") { + return Promise.resolve("PYTHON_BYTECODE") + } + + return Promise.reject({ code: "ENOENT" }) + }) + + const result = await loadRuleFiles("/fake/path") + + // Should contain rule files + expect(result).toContain("rule 1 content") + expect(result).toContain("rule 2 content") + + // Should NOT contain cache file content - they should be filtered out + expect(result).not.toContain("DS_STORE_BINARY_CONTENT") + expect(result).not.toContain("THUMBS_DB_CONTENT") + expect(result).not.toContain("BACKUP_CONTENT") + expect(result).not.toContain("LOG_CONTENT") + expect(result).not.toContain("TEMP_CONTENT") + expect(result).not.toContain("PYTHON_BYTECODE") + + // Verify cache files are not read at all + const expectedCacheFiles = [ + "/fake/path/.roo/rules/.DS_Store", + "/fake/path/.roo/rules/Thumbs.db", + "/fake/path/.roo/rules/backup.bak", + "/fake/path/.roo/rules/cache.log", + "/fake/path/.roo/rules/temp.tmp", + "/fake/path/.roo/rules/script.pyc", + ] + + for (const cacheFile of expectedCacheFiles) { + const expectedPath = process.platform === "win32" ? cacheFile.replace(/\//g, "\\") : cacheFile + expect(readFileMock).not.toHaveBeenCalledWith(expectedPath, "utf-8") + } + }) + it("should fall back to .roorules when .roo/rules/ is empty", async () => { // Simulate .roo/rules directory exists statMock.mockResolvedValueOnce({ diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 0e1ddfd24f..3c8558a57f 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -123,6 +123,10 @@ async function readTextFilesFromDirectory(dirPath: string): Promise item !== null) } catch (err) { return [] @@ -297,3 +301,44 @@ The following additional instructions are provided by the user, and should be fo ${joinedSections}` : "" } + +/** + * Check if a file should be included in rule compilation. + * Excludes cache files and system files that shouldn't be processed as rules. + */ +function shouldIncludeRuleFile(filename: string): boolean { + const basename = path.basename(filename) + + const cachePatterns = [ + "*.DS_Store", + "*.bak", + "*.cache", + "*.crdownload", + "*.db", + "*.dmp", + "*.dump", + "*.eslintcache", + "*.lock", + "*.log", + "*.old", + "*.part", + "*.partial", + "*.pyc", + "*.pyo", + "*.stackdump", + "*.swo", + "*.swp", + "*.temp", + "*.tmp", + "Thumbs.db", + ] + + return !cachePatterns.some((pattern) => { + if (pattern.startsWith("*.")) { + const extension = pattern.slice(1) + return basename.endsWith(extension) + } else { + return basename === pattern + } + }) +} diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts index 0af850cbb1..643233ab6f 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.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` + + `## ${server.name}${config.command ? ` (\`${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/prompts/tools/ask-followup-question.ts b/src/core/prompts/tools/ask-followup-question.ts index 7ece1e311d..c69e5a697f 100644 --- a/src/core/prompts/tools/ask-followup-question.ts +++ b/src/core/prompts/tools/ask-followup-question.ts @@ -7,6 +7,8 @@ Parameters: 1. Be provided in its own tag 2. Be specific, actionable, and directly related to the completed task 3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses. + 4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: suggestion text + - When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge Usage: Your question here @@ -14,6 +16,9 @@ Usage: Your suggested answer here + +Implement the solution + @@ -25,5 +30,15 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil ./config/frontend-config.json ./frontend-config.json + + +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + +Start implementing the solution +Plan the architecture first +Continue with more details + ` } diff --git a/src/core/protect/RooProtectedController.ts b/src/core/protect/RooProtectedController.ts index b74b6a9bb9..86122c7660 100644 --- a/src/core/protect/RooProtectedController.ts +++ b/src/core/protect/RooProtectedController.ts @@ -18,6 +18,7 @@ export class RooProtectedController { ".roorules*", ".clinerules*", ".roo/**", + ".vscode/**", ".rooprotected", // For future use ] diff --git a/src/core/protect/__tests__/RooProtectedController.spec.ts b/src/core/protect/__tests__/RooProtectedController.spec.ts index 63d8809285..6c998e365a 100644 --- a/src/core/protect/__tests__/RooProtectedController.spec.ts +++ b/src/core/protect/__tests__/RooProtectedController.spec.ts @@ -38,6 +38,12 @@ describe("RooProtectedController", () => { expect(controller.isWriteProtected(".clinerules.md")).toBe(true) }) + it("should protect files in .vscode directory", () => { + expect(controller.isWriteProtected(".vscode/settings.json")).toBe(true) + expect(controller.isWriteProtected(".vscode/launch.json")).toBe(true) + expect(controller.isWriteProtected(".vscode/tasks.json")).toBe(true) + }) + it("should not protect other files starting with .roo", () => { expect(controller.isWriteProtected(".roosettings")).toBe(false) expect(controller.isWriteProtected(".rooconfig")).toBe(false) @@ -134,6 +140,7 @@ describe("RooProtectedController", () => { ".roorules*", ".clinerules*", ".roo/**", + ".vscode/**", ".rooprotected", ]) }) diff --git a/src/core/task-persistence/apiMessages.ts b/src/core/task-persistence/apiMessages.ts index d6c17bd9b3..f846aaf13f 100644 --- a/src/core/task-persistence/apiMessages.ts +++ b/src/core/task-persistence/apiMessages.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as fs from "fs/promises" @@ -78,5 +79,5 @@ export async function saveApiMessages({ }) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) const filePath = path.join(taskDir, GlobalFileNames.apiConversationHistory) - await fs.writeFile(filePath, JSON.stringify(messages)) + await safeWriteJson(filePath, messages) } diff --git a/src/core/task-persistence/taskMessages.ts b/src/core/task-persistence/taskMessages.ts index 3ed5c5099e..63a2eefbaa 100644 --- a/src/core/task-persistence/taskMessages.ts +++ b/src/core/task-persistence/taskMessages.ts @@ -1,3 +1,4 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" import * as fs from "fs/promises" @@ -37,5 +38,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 fs.writeFile(filePath, JSON.stringify(messages)) + await safeWriteJson(filePath, messages) } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 46da7485ed..4f0d32c8c1 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -86,6 +86,9 @@ import { ApiMessage } from "../task-persistence/apiMessages" import { getMessagesSinceLastSummary, summarizeConversation } from "../condense" import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning" +// Constants +const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes + export type ClineEvents = { message: [{ action: "created" | "updated"; message: ClineMessage }] taskStarted: [] @@ -1425,10 +1428,15 @@ export class Task extends EventEmitter { // cancel task. this.abortTask() - await abortStream( - "streaming_failed", - error.message ?? JSON.stringify(serializeError(error), null, 2), - ) + // Check if this was a user-initiated cancellation + // If this.abort is 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)) + + await abortStream(cancelReason, streamingFailedMessage) const history = await provider?.getTaskWithId(this.taskId) @@ -1708,6 +1716,8 @@ export class Task extends EventEmitter { 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, @@ -1721,7 +1731,7 @@ export class Task extends EventEmitter { customCondensingPrompt, condensingApiHandler, profileThresholds, - currentProfileId: state?.currentApiConfigName || "default", + currentProfileId, }) if (truncateResult.messages !== this.apiConversationHistory) { await this.overwriteApiConversationHistory(truncateResult.messages) @@ -1792,7 +1802,10 @@ export class Task extends EventEmitter { } const baseDelay = requestDelaySeconds || 5 - let exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) + let exponentialDelay = Math.min( + Math.ceil(baseDelay * Math.pow(2, retryAttempt)), + MAX_EXPONENTIAL_BACKOFF_SECONDS, + ) // 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/__tests__/askFollowupQuestionTool.spec.ts b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts new file mode 100644 index 0000000000..fbb9ef9eb3 --- /dev/null +++ b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, vi } from "vitest" +import { askFollowupQuestionTool } from "../askFollowupQuestionTool" +import { ToolUse } from "../../../shared/tools" + +describe("askFollowupQuestionTool", () => { + let mockCline: any + let mockPushToolResult: any + let toolResult: any + + beforeEach(() => { + vi.clearAllMocks() + + mockCline = { + ask: vi.fn().mockResolvedValue({ text: "Test response" }), + say: vi.fn().mockResolvedValue(undefined), + consecutiveMistakeCount: 0, + } + + mockPushToolResult = vi.fn((result) => { + toolResult = result + }) + }) + + it("should parse suggestions without mode attributes", async () => { + const block: ToolUse = { + type: "tool_use", + name: "ask_followup_question", + params: { + question: "What would you like to do?", + follow_up: "Option 1Option 2", + }, + partial: false, + } + + await askFollowupQuestionTool( + mockCline, + block, + vi.fn(), + vi.fn(), + mockPushToolResult, + vi.fn((tag, content) => content), + ) + + expect(mockCline.ask).toHaveBeenCalledWith( + "followup", + expect.stringContaining('"suggest":[{"answer":"Option 1"},{"answer":"Option 2"}]'), + false, + ) + }) + + it("should parse suggestions with mode attributes", async () => { + const block: ToolUse = { + type: "tool_use", + name: "ask_followup_question", + params: { + question: "What would you like to do?", + follow_up: 'Write codeDebug issue', + }, + partial: false, + } + + await askFollowupQuestionTool( + mockCline, + block, + vi.fn(), + vi.fn(), + mockPushToolResult, + vi.fn((tag, content) => content), + ) + + expect(mockCline.ask).toHaveBeenCalledWith( + "followup", + expect.stringContaining( + '"suggest":[{"answer":"Write code","mode":"code"},{"answer":"Debug issue","mode":"debug"}]', + ), + false, + ) + }) + + it("should handle mixed suggestions with and without mode attributes", async () => { + const block: ToolUse = { + type: "tool_use", + name: "ask_followup_question", + params: { + question: "What would you like to do?", + follow_up: 'Regular optionPlan architecture', + }, + partial: false, + } + + await askFollowupQuestionTool( + mockCline, + block, + vi.fn(), + vi.fn(), + mockPushToolResult, + vi.fn((tag, content) => content), + ) + + expect(mockCline.ask).toHaveBeenCalledWith( + "followup", + expect.stringContaining( + '"suggest":[{"answer":"Regular option"},{"answer":"Plan architecture","mode":"architect"}]', + ), + false, + ) + }) +}) diff --git a/src/core/tools/__tests__/validateToolUse.spec.ts b/src/core/tools/__tests__/validateToolUse.spec.ts index 89d03fea70..7802674605 100644 --- a/src/core/tools/__tests__/validateToolUse.spec.ts +++ b/src/core/tools/__tests__/validateToolUse.spec.ts @@ -7,7 +7,9 @@ import { TOOL_GROUPS } from "../../../shared/tools" import { validateToolUse } from "../validateToolUse" -const [codeMode, architectMode, askMode] = modes.map((mode) => mode.slug) +const codeMode = modes.find((m) => m.slug === "code")?.slug || "code" +const architectMode = modes.find((m) => m.slug === "architect")?.slug || "architect" +const askMode = modes.find((m) => m.slug === "ask")?.slug || "ask" describe("mode-validator", () => { describe("isToolAllowedForMode", () => { diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index d4f7fd883f..c89fe8b735 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -147,9 +147,13 @@ export async function applyDiffToolLegacy( await cline.diffViewProvider.update(diffResult.content, true) await cline.diffViewProvider.scrollToFirstDiff() + // Check if file is write-protected + const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false + const completeMessage = JSON.stringify({ ...sharedMessageProps, diff: diffContent, + isProtected: isWriteProtected, } satisfies ClineSayTool) let toolProgressStatus @@ -158,7 +162,7 @@ export async function applyDiffToolLegacy( toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult) } - const didApprove = await askApproval("tool", completeMessage, toolProgressStatus) + const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected) if (!didApprove) { await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view diff --git a/src/core/tools/askFollowupQuestionTool.ts b/src/core/tools/askFollowupQuestionTool.ts index d5adf6d4cd..e736936887 100644 --- a/src/core/tools/askFollowupQuestionTool.ts +++ b/src/core/tools/askFollowupQuestionTool.ts @@ -26,7 +26,7 @@ export async function askFollowupQuestionTool( return } - type Suggest = { answer: string } + type Suggest = { answer: string; mode?: string } let follow_up_json = { question, @@ -34,12 +34,17 @@ export async function askFollowupQuestionTool( } if (follow_up) { + // Define the actual structure returned by the XML parser + type ParsedSuggestion = string | { "#text": string; "@_mode"?: string } + let parsedSuggest: { - suggest: Suggest[] | Suggest + suggest: ParsedSuggestion[] | ParsedSuggestion } try { - parsedSuggest = parseXml(follow_up, ["suggest"]) as { suggest: Suggest[] | Suggest } + parsedSuggest = parseXml(follow_up, ["suggest"]) as { + suggest: ParsedSuggestion[] | ParsedSuggestion + } } catch (error) { cline.consecutiveMistakeCount++ cline.recordToolError("ask_followup_question") @@ -48,9 +53,24 @@ export async function askFollowupQuestionTool( return } - const normalizedSuggest = Array.isArray(parsedSuggest?.suggest) + const rawSuggestions = Array.isArray(parsedSuggest?.suggest) ? parsedSuggest.suggest - : [parsedSuggest?.suggest].filter((sug): sug is Suggest => sug !== undefined) + : [parsedSuggest?.suggest].filter((sug): sug is ParsedSuggestion => sug !== undefined) + + // Transform parsed XML to our Suggest format + const normalizedSuggest: Suggest[] = rawSuggestions.map((sug) => { + if (typeof sug === "string") { + // Simple string suggestion (no mode attribute) + return { answer: sug } + } else { + // XML object with text content and optional mode attribute + const result: Suggest = { answer: sug["#text"] } + if (sug["@_mode"]) { + result.mode = sug["@_mode"] + } + return result + } + }) follow_up_json.suggest = normalizedSuggest } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 2bed089961..8eb6108a76 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -68,6 +68,7 @@ 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 @@ -109,7 +110,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "jun-17-2025-3-21" // Update for v3.21.0 announcement + public readonly latestAnnouncementId = "jul-02-2025-3-22-6" // Update for v3.22.6 announcement public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager @@ -1303,6 +1304,38 @@ 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[] { + 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("allowedCommands") || [] + + // 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 allowed commands:", error) + // Return empty array as fallback to prevent crashes + return [] + } + } + async getStateToPostToWebview() { const { apiConfiguration, @@ -1314,6 +1347,7 @@ export class ClineProvider alwaysAllowWriteOutsideWorkspace, alwaysAllowWriteProtected, alwaysAllowExecute, + allowedCommands, alwaysAllowBrowser, alwaysAllowMcp, alwaysAllowModeSwitch, @@ -1377,11 +1411,13 @@ export class ClineProvider codebaseIndexConfig, codebaseIndexModels, profileThresholds, + alwaysAllowFollowupQuestions, + followupAutoApproveTimeoutMs, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY const machineId = vscode.env.machineId - const allowedCommands = vscode.workspace.getConfiguration(Package.name).get("allowedCommands") || [] + const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands) const cwd = this.cwd // Check if there's a system prompt override for the current mode @@ -1420,7 +1456,7 @@ export class ClineProvider enableCheckpoints: enableCheckpoints ?? true, shouldShowAnnouncement: telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId, - allowedCommands, + allowedCommands: mergedAllowedCommands, soundVolume: soundVolume ?? 0.5, browserViewportSize: browserViewportSize ?? "900x600", screenshotQuality: screenshotQuality ?? 75, @@ -1487,6 +1523,8 @@ export class ClineProvider profileThresholds: profileThresholds ?? {}, cloudApiUrl: getRooCodeApiUrl(), hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, + alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, + followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, } } @@ -1567,6 +1605,8 @@ export class ClineProvider alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false, alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false, alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false, + alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false, + followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000, allowedMaxRequests: stateValues.allowedMaxRequests, autoCondenseContext: stateValues.autoCondenseContext ?? true, autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100, @@ -1752,7 +1792,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, etc. + * like the current mode, API provider, git repository information, etc. */ public async getTelemetryProperties(): Promise { const { mode, apiConfiguration, language } = await this.getState() @@ -1772,6 +1812,10 @@ export class ClineProvider this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`) } + // Get git repository information + const gitInfo = await getWorkspaceGitInfo() + + // Return all properties including git info - clients will filter as needed return { appName: packageJSON?.name ?? Package.name, appVersion: packageJSON?.version ?? Package.version, @@ -1785,6 +1829,7 @@ export class ClineProvider diffStrategy: task?.diffStrategy?.getName(), isSubtask: task ? !!task.parentTask : undefined, cloudIsAuthenticated, + ...gitInfo, } } } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 72b6bd6a98..801c6c4774 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -13,6 +13,7 @@ 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" @@ -43,6 +44,8 @@ vi.mock("axios", () => ({ post: vi.fn(), })) +vi.mock("../../../utils/safeWriteJson") + vi.mock("@modelcontextprotocol/sdk/types.js", () => ({ CallToolResultSchema: {}, ListResourcesResultSchema: {}, @@ -1989,11 +1992,8 @@ describe("Project MCP Settings", () => { // Check that fs.mkdir was called with the correct path expect(mockedFs.mkdir).toHaveBeenCalledWith("/test/workspace/.roo", { recursive: true }) - // Check that fs.writeFile was called with default content - expect(mockedFs.writeFile).toHaveBeenCalledWith( - "/test/workspace/.roo/mcp.json", - JSON.stringify({ mcpServers: {} }, null, 2), - ) + // Verify file was created with default content + expect(safeWriteJson).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json", { mcpServers: {} }) // Check that openFile was called expect(openFileSpy).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json") diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 663ccec5cc..296f23bc5f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1,7 +1,9 @@ +import { safeWriteJson } from "../../utils/safeWriteJson" import * as path from "path" -import fs from "fs/promises" +import * as 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, TelemetryEventName } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" @@ -27,7 +29,7 @@ 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, importSettings } from "../config/importExport" +import { exportSettings, importSettingsWithFeedback } from "../config/importExport" import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" @@ -324,20 +326,13 @@ export const webviewMessageHandler = async ( provider.exportTaskWithId(message.text!) break case "importSettings": { - const result = await importSettings({ + await importSettingsWithFeedback({ 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": @@ -566,15 +561,22 @@ export const webviewMessageHandler = async ( case "cancelTask": await provider.cancelTask() break - case "allowedCommands": - await provider.context.globalState.update("allowedCommands", message.commands) + 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) // Also update workspace settings. await vscode.workspace .getConfiguration(Package.name) - .update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global) + .update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global) break + } case "openCustomModesSettings": { const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() @@ -608,7 +610,7 @@ export const webviewMessageHandler = async ( const exists = await fileExistsAtPath(mcpPath) if (!exists) { - await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2)) + await safeWriteJson(mcpPath, { mcpServers: {} }) } await openFile(mcpPath) @@ -1099,6 +1101,14 @@ 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() @@ -1500,6 +1510,196 @@ export const webviewMessageHandler = async ( 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"), { diff --git a/src/extension.ts b/src/extension.ts index 9e3daad662..bd43bcbf8a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,6 +28,7 @@ import { McpServerManager } from "./services/mcp/McpServerManager" import { CodeIndexManager } from "./services/code-index/manager" import { MdmService } from "./services/mdm/MdmService" import { migrateSettings } from "./utils/migrateSettings" +import { autoImportSettings } from "./utils/autoImportSettings" import { API } from "./extension/api" import { @@ -120,6 +121,19 @@ export async function activate(context: vscode.ExtensionContext) { }), ) + // Auto-import configuration if specified in settings + try { + await autoImportSettings(outputChannel, { + providerSettingsManager: provider.providerSettingsManager, + contextProxy: provider.contextProxy, + customModesManager: provider.customModesManager, + }) + } catch (error) { + outputChannel.appendLine( + `[AutoImport] Error during auto-import: ${error instanceof Error ? error.message : String(error)}`, + ) + } + registerCommands({ context, outputChannel, provider }) /** diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 04733b0cb6..6ebb4ef36a 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -62,6 +62,13 @@ "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}}", "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.", @@ -73,11 +80,13 @@ "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}}" }, "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?" + "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}}" }, "info": { "no_changes": "No s'han trobat canvis.", @@ -86,11 +95,14 @@ "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!" + "public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!", + "mode_exported": "Mode '{{mode}}' exportat correctament", + "mode_imported": "Mode importat correctament" }, "answers": { "yes": "Sí", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "La teva organització requereix autenticació de Roo Code Cloud. Si us plau, inicia sessió per continuar.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index cf9f363c57..1fceb3bf3f 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Claude Code Prozess wurde mit Code {{exitCode}} beendet.", "errorOutput": "Fehlerausgabe: {{output}}", @@ -73,7 +81,8 @@ }, "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?" + "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}}" }, "info": { "no_changes": "Keine Änderungen gefunden.", @@ -82,11 +91,14 @@ "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!" + "public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!", + "mode_exported": "Modus '{{mode}}' erfolgreich exportiert", + "mode_imported": "Modus erfolgreich importiert" }, "answers": { "yes": "Ja", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Deine Organisation erfordert eine Roo Code Cloud-Authentifizierung. Bitte melde dich an, um fortzufahren.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 63468914ee..b0779cdd89 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Claude Code process exited with code {{exitCode}}.", "errorOutput": "Error output: {{output}}", @@ -73,7 +81,8 @@ }, "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?" + "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}}" }, "info": { "no_changes": "No changes found.", @@ -82,11 +91,14 @@ "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}}" + "image_saved": "Image saved to {{path}}", + "mode_exported": "Mode '{{mode}}' exported successfully", + "mode_imported": "Mode imported successfully" }, "answers": { "yes": "Yes", @@ -111,6 +123,18 @@ "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 97076b442c..316da8d6cc 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "El proceso de Claude Code terminó con código {{exitCode}}.", "errorOutput": "Salida de error: {{output}}", @@ -73,7 +81,8 @@ }, "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?" + "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}}" }, "info": { "no_changes": "No se encontraron cambios.", @@ -82,11 +91,14 @@ "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!" + "public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!", + "mode_exported": "Modo '{{mode}}' exportado correctamente", + "mode_imported": "Modo importado correctamente" }, "answers": { "yes": "Sí", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Tu organización requiere autenticación de Roo Code Cloud. Por favor, inicia sesión para continuar.", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index c9531d07c3..99c511d26a 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Le processus Claude Code s'est terminé avec le code {{exitCode}}.", "errorOutput": "Sortie d'erreur : {{output}}", @@ -73,7 +81,8 @@ }, "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 ?" + "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}}" }, "info": { "no_changes": "Aucun changement trouvé.", @@ -82,11 +91,14 @@ "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 !" + "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" }, "answers": { "yes": "Oui", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Votre organisation nécessite une authentification Roo Code Cloud. Veuillez vous connecter pour continuer.", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 20e0c1f518..fb92bb8f8d 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -58,11 +58,19 @@ "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}}", "share_task_failed": "कार्य साझा करने में विफल। कृपया पुनः प्रयास करें।", "share_no_active_task": "साझा करने के लिए कोई सक्रिय कार्य नहीं", "share_auth_required": "प्रमाणीकरण आवश्यक है। कार्य साझा करने के लिए कृपया साइन इन करें।", "share_not_enabled": "इस संगठन के लिए कार्य साझाकरण सक्षम नहीं है।", "share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।", + "mode_import_failed": "मोड आयात करने में विफल: {{error}}", "claudeCode": { "processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।", "errorOutput": "त्रुटि आउटपुट: {{output}}", @@ -73,7 +81,8 @@ }, "warnings": { "no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं", - "missing_task_files": "इस टास्क की फाइलें गायब हैं। क्या आप इसे टास्क सूची से हटाना चाहते हैं?" + "missing_task_files": "इस टास्क की फाइलें गायब हैं। क्या आप इसे टास्क सूची से हटाना चाहते हैं?", + "auto_import_failed": "RooCode सेटिंग्स का स्वचालित आयात विफल: {{error}}" }, "info": { "no_changes": "कोई परिवर्तन नहीं मिला।", @@ -82,11 +91,14 @@ "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": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!" + "public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!", + "mode_exported": "मोड '{{mode}}' सफलतापूर्वक निर्यात किया गया", + "mode_imported": "मोड सफलतापूर्वक आयात किया गया" }, "answers": { "yes": "हां", @@ -117,11 +129,23 @@ "getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें", "claudeCode": { "pathLabel": "क्लाउड कोड पाथ", - "description": "आपके क्लाउड कोड CLI का वैकल्पिक पाथ। सेट न होने पर डिफ़ॉल्ट रूप से 'claude'。", + "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": "प्रोजेक्ट-विशिष्ट मोड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला" + } + }, "mdm": { "errors": { "cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 319c3f9674..fec0bb863e 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Proses Claude Code keluar dengan kode {{exitCode}}.", "errorOutput": "Output error: {{output}}", @@ -73,7 +81,8 @@ }, "warnings": { "no_terminal_content": "Tidak ada konten terminal yang dipilih", - "missing_task_files": "File tugas ini hilang. Apakah kamu ingin menghapusnya dari daftar tugas?" + "missing_task_files": "File tugas ini hilang. Apakah kamu ingin menghapusnya dari daftar tugas?", + "auto_import_failed": "Gagal mengimpor pengaturan RooCode secara otomatis: {{error}}" }, "info": { "no_changes": "Tidak ada perubahan ditemukan.", @@ -82,11 +91,14 @@ "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!" + "public_share_link_copied": "Tautan berbagi publik disalin ke clipboard!", + "mode_exported": "Mode '{{mode}}' berhasil diekspor", + "mode_imported": "Mode berhasil diimpor" }, "answers": { "yes": "Ya", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index ea919162e4..119b4a6d7a 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Il processo Claude Code è terminato con codice {{exitCode}}.", "errorOutput": "Output di errore: {{output}}", @@ -73,7 +81,8 @@ }, "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à?" + "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}}" }, "info": { "no_changes": "Nessuna modifica trovata.", @@ -82,11 +91,14 @@ "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!" + "public_share_link_copied": "Link di condivisione pubblica copiato negli appunti!", + "mode_exported": "Modalità '{{mode}}' esportata con successo", + "mode_imported": "Modalità importata con successo" }, "answers": { "yes": "Sì", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "La tua organizzazione richiede l'autenticazione Roo Code Cloud. Accedi per continuare.", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 916f9b2f45..39b8948b18 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -58,11 +58,19 @@ "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}}", "share_task_failed": "タスクの共有に失敗しました", "share_no_active_task": "共有するアクティブなタスクがありません", "share_auth_required": "認証が必要です。タスクを共有するにはサインインしてください。", "share_not_enabled": "この組織ではタスク共有が有効になっていません。", "share_task_not_found": "タスクが見つからないか、アクセスが拒否されました。", + "mode_import_failed": "モードのインポートに失敗しました: {{error}}", "claudeCode": { "processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。", "errorOutput": "エラー出力:{{output}}", @@ -73,7 +81,8 @@ }, "warnings": { "no_terminal_content": "選択されたターミナルコンテンツがありません", - "missing_task_files": "このタスクのファイルが見つかりません。タスクリストから削除しますか?" + "missing_task_files": "このタスクのファイルが見つかりません。タスクリストから削除しますか?", + "auto_import_failed": "RooCode設定の自動インポートに失敗しました:{{error}}" }, "info": { "no_changes": "変更は見つかりませんでした。", @@ -82,11 +91,14 @@ "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": "公開共有リンクがクリップボードにコピーされました!" + "public_share_link_copied": "公開共有リンクがクリップボードにコピーされました!", + "mode_exported": "モード「{{mode}}」が正常にエクスポートされました", + "mode_imported": "モードが正常にインポートされました" }, "answers": { "yes": "はい", @@ -122,6 +134,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes ファイルの {{line}} 行目で無効な YAML です。以下を確認してください:\n• 正しいインデント(タブではなくスペースを使用)\n• 引用符と括弧の対応\n• 有効な YAML 構文", + "schemaValidationError": ".roomodes のカスタムモード形式が無効です:\n{{issues}}", + "invalidFormat": "カスタムモード形式が無効です。設定が正しい YAML 形式に従っていることを確認してください。", + "updateFailed": "カスタムモードの更新に失敗しました:{{error}}", + "deleteFailed": "カスタムモードの削除に失敗しました:{{error}}", + "resetFailed": "カスタムモードのリセットに失敗しました:{{error}}", + "modeNotFound": "書き込みエラー:モードが見つかりません", + "noWorkspaceForProject": "プロジェクト固有モード用のワークスペースフォルダーが見つかりません" + } + }, "mdm": { "errors": { "cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index bad3facfd6..f73eb22c2d 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -58,11 +58,19 @@ "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}}", "share_task_failed": "작업 공유에 실패했습니다", "share_no_active_task": "공유할 활성 작업이 없습니다", "share_auth_required": "인증이 필요합니다. 작업을 공유하려면 로그인하세요.", "share_not_enabled": "이 조직에서는 작업 공유가 활성화되지 않았습니다.", "share_task_not_found": "작업을 찾을 수 없거나 액세스가 거부되었습니다.", + "mode_import_failed": "모드 가져오기 실패: {{error}}", "claudeCode": { "processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.", "errorOutput": "오류 출력: {{output}}", @@ -73,7 +81,8 @@ }, "warnings": { "no_terminal_content": "선택된 터미널 내용이 없습니다", - "missing_task_files": "이 작업의 파일이 누락되었습니다. 작업 목록에서 제거하시겠습니까?" + "missing_task_files": "이 작업의 파일이 누락되었습니다. 작업 목록에서 제거하시겠습니까?", + "auto_import_failed": "RooCode 설정 자동 가져오기 실패: {{error}}" }, "info": { "no_changes": "변경 사항이 없습니다.", @@ -82,11 +91,14 @@ "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": "공개 공유 링크가 클립보드에 복사되었습니다!" + "public_share_link_copied": "공개 공유 링크가 클립보드에 복사되었습니다!", + "mode_exported": "'{{mode}}' 모드가 성공적으로 내보내졌습니다", + "mode_imported": "모드를 성공적으로 가져왔습니다" }, "answers": { "yes": "예", @@ -122,6 +134,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes 파일의 {{line}}번째 줄에서 유효하지 않은 YAML입니다. 다음을 확인하세요:\n• 올바른 들여쓰기 (탭이 아닌 공백 사용)\n• 일치하는 따옴표와 괄호\n• 유효한 YAML 구문", + "schemaValidationError": ".roomodes의 사용자 정의 모드 형식이 유효하지 않습니다:\n{{issues}}", + "invalidFormat": "사용자 정의 모드 형식이 유효하지 않습니다. 설정이 올바른 YAML 형식을 따르는지 확인하세요.", + "updateFailed": "사용자 정의 모드 업데이트 실패: {{error}}", + "deleteFailed": "사용자 정의 모드 삭제 실패: {{error}}", + "resetFailed": "사용자 정의 모드 재설정 실패: {{error}}", + "modeNotFound": "쓰기 오류: 모드를 찾을 수 없습니다", + "noWorkspaceForProject": "프로젝트별 모드용 작업 공간 폴더를 찾을 수 없습니다" + } + }, "mdm": { "errors": { "cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index a90ff3d00f..6958ace288 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Claude Code proces beëindigd met code {{exitCode}}.", "errorOutput": "Foutuitvoer: {{output}}", @@ -73,7 +81,8 @@ }, "warnings": { "no_terminal_content": "Geen terminalinhoud geselecteerd", - "missing_task_files": "De bestanden van deze taak ontbreken. Wil je deze uit de takenlijst verwijderen?" + "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}}" }, "info": { "no_changes": "Geen wijzigingen gevonden.", @@ -82,11 +91,14 @@ "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!" + "public_share_link_copied": "Openbare deel-link gekopieerd naar klembord!", + "mode_exported": "Modus '{{mode}}' succesvol geëxporteerd", + "mode_imported": "Modus succesvol geïmporteerd" }, "answers": { "yes": "Ja", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index d6cb5cb809..d9f244cbe3 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Proces Claude Code zakończył się kodem {{exitCode}}.", "errorOutput": "Wyjście błędu: {{output}}", @@ -73,7 +81,8 @@ }, "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ń?" + "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}}" }, "info": { "no_changes": "Nie znaleziono zmian.", @@ -82,11 +91,14 @@ "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!" + "public_share_link_copied": "Publiczny link udostępniania skopiowany do schowka!", + "mode_exported": "Tryb '{{mode}}' pomyślnie wyeksportowany", + "mode_imported": "Tryb pomyślnie zaimportowany" }, "answers": { "yes": "Tak", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Roo Code Cloud. Zaloguj się, aby kontynuować.", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 941dd0f86d..9a9c3de30f 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -62,11 +62,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "O processo Claude Code saiu com código {{exitCode}}.", "errorOutput": "Saída de erro: {{output}}", @@ -77,7 +85,8 @@ }, "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?" + "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}}" }, "info": { "no_changes": "Nenhuma alteração encontrada.", @@ -86,11 +95,14 @@ "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!" + "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" }, "answers": { "yes": "Sim", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Sua organização requer autenticação do Roo Code Cloud. Faça login para continuar.", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 646d5d3b6b..d598d832fb 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -58,11 +58,19 @@ "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}}", "share_task_failed": "Не удалось поделиться задачей", "share_no_active_task": "Нет активной задачи для совместного использования", "share_auth_required": "Требуется аутентификация. Войдите в систему для совместного доступа к задачам.", "share_not_enabled": "Совместный доступ к задачам не включен для этой организации.", "share_task_not_found": "Задача не найдена или доступ запрещен.", + "mode_import_failed": "Не удалось импортировать режим: {{error}}", "claudeCode": { "processExited": "Процесс Claude Code завершился с кодом {{exitCode}}.", "errorOutput": "Вывод ошибки: {{output}}", @@ -73,7 +81,8 @@ }, "warnings": { "no_terminal_content": "Не выбрано содержимое терминала", - "missing_task_files": "Файлы этой задачи отсутствуют. Хотите удалить её из списка задач?" + "missing_task_files": "Файлы этой задачи отсутствуют. Хотите удалить её из списка задач?", + "auto_import_failed": "Не удалось автоматически импортировать настройки RooCode: {{error}}" }, "info": { "no_changes": "Изменения не найдены.", @@ -82,11 +91,14 @@ "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": "Публичная ссылка для совместного доступа скопирована в буфер обмена!" + "public_share_link_copied": "Публичная ссылка для совместного доступа скопирована в буфер обмена!", + "mode_exported": "Режим '{{mode}}' успешно экспортирован", + "mode_imported": "Режим успешно импортирован" }, "answers": { "yes": "Да", @@ -122,6 +134,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": "Недопустимый YAML в файле .roomodes на строке {{line}}. Проверь:\n• Правильные отступы (используй пробелы, не табы)\n• Соответствующие кавычки и скобки\n• Допустимый синтаксис YAML", + "schemaValidationError": "Недопустимый формат пользовательских режимов в .roomodes:\n{{issues}}", + "invalidFormat": "Недопустимый формат пользовательских режимов. Убедись, что твои настройки соответствуют правильному формату YAML.", + "updateFailed": "Не удалось обновить пользовательский режим: {{error}}", + "deleteFailed": "Не удалось удалить пользовательский режим: {{error}}", + "resetFailed": "Не удалось сбросить пользовательские режимы: {{error}}", + "modeNotFound": "Ошибка записи: Режим не найден", + "noWorkspaceForProject": "Не найдена папка рабочего пространства для режима, специфичного для проекта" + } + }, "mdm": { "errors": { "cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 368934fdcc..5fa89fbba5 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Claude Code işlemi {{exitCode}} koduyla çıktı.", "errorOutput": "Hata çıktısı: {{output}}", @@ -73,7 +81,8 @@ }, "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?" + "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}}" }, "info": { "no_changes": "Değişiklik bulunamadı.", @@ -82,11 +91,14 @@ "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ı!" + "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ı" }, "answers": { "yes": "Evet", @@ -122,6 +134,18 @@ } } }, + "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ı" + } + }, "mdm": { "errors": { "cloud_auth_required": "Kuruluşunuz Roo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 9d43acc45a..7f9ec5dc20 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -58,11 +58,19 @@ "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}}", "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}}", "claudeCode": { "processExited": "Tiến trình Claude Code thoát với mã {{exitCode}}.", "errorOutput": "Đầu ra lỗi: {{output}}", @@ -73,7 +81,8 @@ }, "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?" + "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}}" }, "info": { "no_changes": "Không tìm thấy thay đổi nào.", @@ -82,11 +91,14 @@ "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!" + "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" }, "answers": { "yes": "Có", @@ -122,6 +134,18 @@ } } }, + "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" + } + }, "mdm": { "errors": { "cloud_auth_required": "Tổ chức của bạn yêu cầu xác thực Roo Code Cloud. Vui lòng đăng nhập để tiếp tục.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index d21c9ef9b7..1b0a272993 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -63,11 +63,19 @@ "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}}", "share_task_failed": "分享任务失败。请重试。", "share_no_active_task": "没有活跃任务可分享", "share_auth_required": "需要身份验证。请登录以分享任务。", "share_not_enabled": "此组织未启用任务分享功能。", "share_task_not_found": "未找到任务或访问被拒绝。", + "mode_import_failed": "导入模式失败:{{error}}", "claudeCode": { "processExited": "Claude Code 进程退出,退出码:{{exitCode}}。", "errorOutput": "错误输出:{{output}}", @@ -78,7 +86,8 @@ }, "warnings": { "no_terminal_content": "没有选择终端内容", - "missing_task_files": "此任务的文件丢失。您想从任务列表中删除它吗?" + "missing_task_files": "此任务的文件丢失。您想从任务列表中删除它吗?", + "auto_import_failed": "自动导入 RooCode 设置失败:{{error}}" }, "info": { "no_changes": "未找到更改。", @@ -87,11 +96,14 @@ "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": "公开分享链接已复制到剪贴板!" + "public_share_link_copied": "公开分享链接已复制到剪贴板!", + "mode_exported": "模式 '{{mode}}' 已成功导出", + "mode_imported": "模式已成功导入" }, "answers": { "yes": "是", @@ -127,6 +139,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes 文件第 {{line}} 行 YAML 格式无效。请检查:\n• 正确的缩进(使用空格,不要使用制表符)\n• 匹配的引号和括号\n• 有效的 YAML 语法", + "schemaValidationError": ".roomodes 中自定义模式格式无效:\n{{issues}}", + "invalidFormat": "自定义模式格式无效。请确保你的设置遵循正确的 YAML 格式。", + "updateFailed": "更新自定义模式失败:{{error}}", + "deleteFailed": "删除自定义模式失败:{{error}}", + "resetFailed": "重置自定义模式失败:{{error}}", + "modeNotFound": "写入错误:未找到模式", + "noWorkspaceForProject": "未找到项目特定模式的工作区文件夹" + } + }, "mdm": { "errors": { "cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 164df43cc8..c4b90ef1a5 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -58,6 +58,13 @@ "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}}", "share_task_failed": "分享工作失敗。請重試。", "share_no_active_task": "沒有活躍的工作可分享", "share_auth_required": "需要身份驗證。請登入以分享工作。", @@ -69,11 +76,13 @@ "processExitedWithError": "Claude Code 程序退出,退出碼:{{exitCode}}。錯誤輸出:{{output}}", "stoppedWithReason": "Claude Code 停止,原因:{{reason}}", "apiKeyModelPlanMismatch": "API 金鑰和訂閱方案允許不同的模型。請確保所選模型包含在您的方案中。" - } + }, + "mode_import_failed": "匯入模式失敗:{{error}}" }, "warnings": { "no_terminal_content": "沒有選擇終端機內容", - "missing_task_files": "此工作的檔案遺失。您想從工作列表中刪除它嗎?" + "missing_task_files": "此工作的檔案遺失。您想從工作列表中刪除它嗎?", + "auto_import_failed": "自動匯入 RooCode 設定失敗:{{error}}" }, "info": { "no_changes": "沒有找到更改。", @@ -82,11 +91,14 @@ "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": "公開分享連結已複製到剪貼簿!" + "public_share_link_copied": "公開分享連結已複製到剪貼簿!", + "mode_exported": "模式 '{{mode}}' 已成功匯出", + "mode_imported": "模式已成功匯入" }, "answers": { "yes": "是", @@ -122,6 +134,18 @@ } } }, + "customModes": { + "errors": { + "yamlParseError": ".roomodes 檔案第 {{line}} 行 YAML 格式無效。請檢查:\n• 正確的縮排(使用空格,不要使用定位字元)\n• 匹配的引號和括號\n• 有效的 YAML 語法", + "schemaValidationError": ".roomodes 中自訂模式格式無效:\n{{issues}}", + "invalidFormat": "自訂模式格式無效。請確保你的設定遵循正確的 YAML 格式。", + "updateFailed": "更新自訂模式失敗:{{error}}", + "deleteFailed": "刪除自訂模式失敗:{{error}}", + "resetFailed": "重設自訂模式失敗:{{error}}", + "modeNotFound": "寫入錯誤:未找到模式", + "noWorkspaceForProject": "未找到專案特定模式的工作區資料夾" + } + }, "mdm": { "errors": { "cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。", diff --git a/src/integrations/claude-code/__tests__/run.spec.ts b/src/integrations/claude-code/__tests__/run.spec.ts index aa8d9fe8d2..d2fda08fc0 100644 --- a/src/integrations/claude-code/__tests__/run.spec.ts +++ b/src/integrations/claude-code/__tests__/run.spec.ts @@ -13,9 +13,92 @@ vi.mock("vscode", () => ({ }, })) +// Mock execa to test stdin behavior +const mockExeca = vi.fn() +const mockStdin = { + write: vi.fn((data, encoding, callback) => { + // Simulate successful write + if (callback) callback(null) + }), + end: vi.fn(), +} + +// Mock process that simulates successful execution +const createMockProcess = () => { + let resolveProcess: (value: { exitCode: number }) => void + const processPromise = new Promise<{ exitCode: number }>((resolve) => { + resolveProcess = resolve + }) + + const mockProcess = { + stdin: mockStdin, + stdout: { + on: vi.fn(), + }, + stderr: { + on: vi.fn((event, callback) => { + // Don't emit any stderr data in tests + }), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + // Simulate successful process completion after a short delay + setTimeout(() => { + callback(0) + resolveProcess({ exitCode: 0 }) + }, 10) + } + if (event === "error") { + // Don't emit any errors in tests + } + }), + killed: false, + kill: vi.fn(), + then: processPromise.then.bind(processPromise), + catch: processPromise.catch.bind(processPromise), + finally: processPromise.finally.bind(processPromise), + } + return mockProcess +} + +vi.mock("execa", () => ({ + execa: mockExeca, +})) + +// Mock readline with proper interface simulation +let mockReadlineInterface: any = null + +vi.mock("readline", () => ({ + default: { + createInterface: vi.fn(() => { + mockReadlineInterface = { + async *[Symbol.asyncIterator]() { + // Simulate Claude CLI JSON output + yield '{"type":"text","text":"Hello"}' + yield '{"type":"text","text":" world"}' + // Simulate end of stream - must return to terminate the iterator + return + }, + close: vi.fn(), + } + return mockReadlineInterface + }), + }, +})) + describe("runClaudeCode", () => { beforeEach(() => { vi.clearAllMocks() + mockExeca.mockReturnValue(createMockProcess()) + // Mock setImmediate to run synchronously in tests + vi.spyOn(global, "setImmediate").mockImplementation((callback: any) => { + callback() + return {} as any + }) + }) + + afterEach(() => { + vi.restoreAllMocks() }) test("should export runClaudeCode function", async () => { @@ -34,4 +117,174 @@ describe("runClaudeCode", () => { expect(Symbol.asyncIterator in result).toBe(true) expect(typeof result[Symbol.asyncIterator]).toBe("function") }) + + test("should use stdin instead of command line arguments for messages", async () => { + const { runClaudeCode } = await import("../run") + const messages = [{ role: "user" as const, content: "Hello world!" }] + const options = { + systemPrompt: "You are a helpful assistant", + messages, + } + + const generator = runClaudeCode(options) + + // Consume the generator to completion + const results = [] + for await (const chunk of generator) { + results.push(chunk) + } + + // Verify execa was called with correct arguments (no JSON.stringify(messages) in args) + expect(mockExeca).toHaveBeenCalledWith( + "claude", + expect.arrayContaining([ + "-p", + "--system-prompt", + "You are a helpful assistant", + "--verbose", + "--output-format", + "stream-json", + "--disallowedTools", + expect.any(String), + "--max-turns", + "1", + ]), + expect.objectContaining({ + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }), + ) + + // Verify the arguments do NOT contain the stringified messages + const [, args] = mockExeca.mock.calls[0] + expect(args).not.toContain(JSON.stringify(messages)) + + // Verify messages were written to stdin with callback + expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function)) + expect(mockStdin.end).toHaveBeenCalled() + + // Verify we got the expected mock output + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ type: "text", text: "Hello" }) + expect(results[1]).toEqual({ type: "text", text: " world" }) + }) + + test("should include model parameter when provided", async () => { + const { runClaudeCode } = await import("../run") + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + modelId: "claude-3-5-sonnet-20241022", + } + + const generator = runClaudeCode(options) + + // Consume at least one item to trigger process spawn + await generator.next() + + // Clean up the generator + await generator.return(undefined) + + const [, args] = mockExeca.mock.calls[0] + expect(args).toContain("--model") + expect(args).toContain("claude-3-5-sonnet-20241022") + }) + + test("should use custom claude path when provided", async () => { + const { runClaudeCode } = await import("../run") + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + path: "/custom/path/to/claude", + } + + const generator = runClaudeCode(options) + + // Consume at least one item to trigger process spawn + await generator.next() + + // Clean up the generator + await generator.return(undefined) + + const [claudePath] = mockExeca.mock.calls[0] + expect(claudePath).toBe("/custom/path/to/claude") + }) + + test("should handle stdin write errors gracefully", async () => { + const { runClaudeCode } = await import("../run") + + // Create a mock process with stdin that fails + const mockProcessWithError = createMockProcess() + mockProcessWithError.stdin.write = vi.fn((data, encoding, callback) => { + // Simulate write error + if (callback) callback(new Error("EPIPE: broken pipe")) + }) + + // Mock console.error to verify error logging + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + mockExeca.mockReturnValueOnce(mockProcessWithError) + + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + } + + const generator = runClaudeCode(options) + + // Try to consume the generator + try { + await generator.next() + } catch (error) { + // Expected to fail + } + + // Verify error was logged + expect(consoleErrorSpy).toHaveBeenCalledWith("Error writing to Claude Code stdin:", expect.any(Error)) + + // Verify process was killed + expect(mockProcessWithError.kill).toHaveBeenCalled() + + // Clean up + consoleErrorSpy.mockRestore() + await generator.return(undefined) + }) + + test("should handle stdin access errors gracefully", async () => { + const { runClaudeCode } = await import("../run") + + // Create a mock process without stdin + const mockProcessWithoutStdin = createMockProcess() + mockProcessWithoutStdin.stdin = null as any + + // Mock console.error to verify error logging + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + mockExeca.mockReturnValueOnce(mockProcessWithoutStdin) + + const options = { + systemPrompt: "You are a helpful assistant", + messages: [{ role: "user" as const, content: "Hello" }], + } + + const generator = runClaudeCode(options) + + // Try to consume the generator + try { + await generator.next() + } catch (error) { + // Expected to fail + } + + // Verify error was logged + expect(consoleErrorSpy).toHaveBeenCalledWith("Error accessing Claude Code stdin:", expect.any(Error)) + + // Verify process was killed + expect(mockProcessWithoutStdin.kill).toHaveBeenCalled() + + // Clean up + consoleErrorSpy.mockRestore() + await generator.return(undefined) + }) }) diff --git a/src/integrations/claude-code/run.ts b/src/integrations/claude-code/run.ts index 84f1fe0902..e4e9d38df9 100644 --- a/src/integrations/claude-code/run.ts +++ b/src/integrations/claude-code/run.ts @@ -63,10 +63,21 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator } } - // We rely on the assistant message. If the output was truncated, it's better having a poorly formatted message - // from which to extract something, than throwing an error/showing the model didn't return any messages. - if (processState.partialData && processState.partialData.startsWith(`{"type":"assistant"`)) { - yield processState.partialData + // Handle any remaining partial data that could be a valid message + // This helps recover from truncated output and ensures we don't lose reasoning/thinking content + if (processState.partialData) { + // Try to parse the partial data as it might be a complete message + const partialChunk = attemptParseChunk(processState.partialData) + if (partialChunk) { + yield partialChunk + } else { + // If it's not parseable but looks like it could be a message, yield it as raw string + // The provider will handle string messages appropriately + const partialTrimmed = processState.partialData.trim() + if (partialTrimmed.startsWith('{"type":') && partialTrimmed.includes('"message"')) { + yield processState.partialData + } + } } const { exitCode } = await process @@ -112,7 +123,6 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions const args = [ "-p", - JSON.stringify(messages), "--system-prompt", systemPrompt, "--verbose", @@ -129,8 +139,8 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions args.push("--model", modelId) } - return execa(claudePath, args, { - stdin: "ignore", + const child = execa(claudePath, args, { + stdin: "pipe", stdout: "pipe", stderr: "pipe", env: { @@ -142,6 +152,30 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions maxBuffer: 1024 * 1024 * 1000, timeout: CLAUDE_CODE_TIMEOUT, }) + + // Write messages to stdin after process is spawned + // This avoids the E2BIG error on Linux when passing large messages as command line arguments + // Linux has a per-argument limit of ~128KiB for execve() system calls + const messagesJson = JSON.stringify(messages) + + // Use setImmediate to ensure the process has been spawned before writing to stdin + // This prevents potential race conditions where stdin might not be ready + setImmediate(() => { + try { + child.stdin.write(messagesJson, "utf8", (error) => { + if (error) { + console.error("Error writing to Claude Code stdin:", error) + child.kill() + } + }) + child.stdin.end() + } catch (error) { + console.error("Error accessing Claude Code stdin:", error) + child.kill() + } + }) + + return child } function parseChunk(data: string, processState: ProcessState) { @@ -169,9 +203,24 @@ function parseChunk(data: string, processState: ProcessState) { function attemptParseChunk(data: string): ClaudeCodeMessage | null { try { - return JSON.parse(data) + const parsed = JSON.parse(data) + // Log message types for debugging streaming issues + if (parsed?.type && process.env.DEBUG_CLAUDE_CODE) { + console.debug(`Claude Code message type: ${parsed.type}`, { + hasMessage: !!parsed.message, + contentTypes: parsed.message?.content?.map((c: any) => c.type) || [], + }) + } + return parsed } catch (error) { - console.error("Error parsing chunk:", error, data.length) + // Only log errors for non-empty data + if (data.trim()) { + console.error( + "Error parsing chunk:", + error, + `Length: ${data.length}, Preview: ${data.substring(0, 100)}...`, + ) + } return null } } diff --git a/src/package.json b/src/package.json index a78ccf5043..30168c951c 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.21.5", + "version": "3.22.6", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -155,6 +155,11 @@ "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%", @@ -328,6 +333,11 @@ "type": "boolean", "default": true, "description": "%settings.enableCodeActions.description%" + }, + "roo-cline.autoImportSettingsPath": { + "type": "string", + "default": "", + "description": "%settings.autoImportSettingsPath.description%" } } } @@ -394,6 +404,7 @@ "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", @@ -403,6 +414,7 @@ "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", @@ -430,7 +442,9 @@ "@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/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 b20c814724..978caf3df3 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.de.json b/src/package.nls.de.json index c31c8b1629..32b38f7dee 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.es.json b/src/package.nls.es.json index 3bf575427f..dc76f15e94 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.fr.json b/src/package.nls.fr.json index 62103db8b0..621cab976e 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.hi.json b/src/package.nls.hi.json index 022ca3d50d..d54f1d829e 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "नए टैब में खोलें", "command.focusInput.title": "इनपुट फ़ील्ड पर फोकस करें", "command.setCustomStoragePath.title": "कस्टम स्टोरेज पाथ सेट करें", + "command.importSettings.title": "सेटिंग्स इम्पोर्ट करें", "command.terminal.addToContext.title": "टर्मिनल सामग्री को संदर्भ में जोड़ें", "command.terminal.fixCommand.title": "यह कमांड ठीक करें", "command.terminal.explainCommand.title": "यह कमांड समझाएं", @@ -30,5 +31,6 @@ "settings.vsCodeLmModelSelector.vendor.description": "भाषा मॉडल का विक्रेता (उदा. copilot)", "settings.vsCodeLmModelSelector.family.description": "भाषा मॉडल का परिवार (उदा. gpt-4)", "settings.customStoragePath.description": "कस्टम स्टोरेज पाथ। डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ें। पूर्ण पथ का समर्थन करता है (उदा. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Code त्वरित सुधार सक्षम करें" + "settings.enableCodeActions.description": "Roo Code त्वरित सुधार सक्षम करें", + "settings.autoImportSettingsPath.description": "RooCode कॉन्फ़िगरेशन फ़ाइल का पथ जिसे एक्सटेंशन स्टार्टअप पर स्वचालित रूप से आयात किया जाएगा। होम डायरेक्टरी के सापेक्ष पूर्ण पथ और पथों का समर्थन करता है (उदाहरण के लिए '~/Documents/roo-code-settings.json')। ऑटो-इंपोर्ट को अक्षम करने के लिए खाली छोड़ दें।" } diff --git a/src/package.nls.id.json b/src/package.nls.id.json index 0e9ca6a7e4..99313d151e 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -20,6 +20,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.it.json b/src/package.nls.it.json index 1ee2ecc7eb..0fca90870c 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.ja.json b/src/package.nls.ja.json index a7514d0d6f..5fd4ade628 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -20,6 +20,7 @@ "command.addToContext.title": "コンテキストに追加", "command.focusInput.title": "入力フィールドにフォーカス", "command.setCustomStoragePath.title": "カスタムストレージパスの設定", + "command.importSettings.title": "設定をインポート", "command.terminal.addToContext.title": "ターミナルの内容をコンテキストに追加", "command.terminal.fixCommand.title": "このコマンドを修正", "command.terminal.explainCommand.title": "このコマンドを説明", @@ -30,5 +31,6 @@ "settings.vsCodeLmModelSelector.vendor.description": "言語モデルのベンダー(例:copilot)", "settings.vsCodeLmModelSelector.family.description": "言語モデルのファミリー(例:gpt-4)", "settings.customStoragePath.description": "カスタムストレージパス。デフォルトの場所を使用する場合は空のままにします。絶対パスをサポートします(例:'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Codeのクイック修正を有効にする。" + "settings.enableCodeActions.description": "Roo Codeのクイック修正を有効にする。", + "settings.autoImportSettingsPath.description": "拡張機能の起動時に自動的にインポートするRooCode設定ファイルへのパス。絶対パスとホームディレクトリからの相対パスをサポートします(例:'~/Documents/roo-code-settings.json')。自動インポートを無効にするには、空のままにします。" } diff --git a/src/package.nls.json b/src/package.nls.json index b4ccbb0e2e..23885f80be 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -20,6 +20,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.ko.json b/src/package.nls.ko.json index 2dae8aedcb..e6e30c155b 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "새 탭에서 열기", "command.focusInput.title": "입력 필드 포커스", "command.setCustomStoragePath.title": "사용자 지정 저장소 경로 설정", + "command.importSettings.title": "설정 가져오기", "command.terminal.addToContext.title": "터미널 내용을 컨텍스트에 추가", "command.terminal.fixCommand.title": "이 명령어 수정", "command.terminal.explainCommand.title": "이 명령어 설명", @@ -30,5 +31,6 @@ "settings.vsCodeLmModelSelector.vendor.description": "언어 모델 공급자 (예: copilot)", "settings.vsCodeLmModelSelector.family.description": "언어 모델 계열 (예: gpt-4)", "settings.customStoragePath.description": "사용자 지정 저장소 경로. 기본 위치를 사용하려면 비워두세요. 절대 경로를 지원합니다 (예: 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Roo Code 빠른 수정 사용 설정" + "settings.enableCodeActions.description": "Roo Code 빠른 수정 사용 설정", + "settings.autoImportSettingsPath.description": "확장 프로그램 시작 시 자동으로 가져올 RooCode 구성 파일의 경로입니다. 절대 경로 및 홈 디렉토리에 대한 상대 경로를 지원합니다(예: '~/Documents/roo-code-settings.json'). 자동 가져오기를 비활성화하려면 비워 둡니다." } diff --git a/src/package.nls.nl.json b/src/package.nls.nl.json index e9a00f1f3f..095aace43e 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -20,6 +20,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index d1c5bfa39c..9963d96db6 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -9,6 +9,7 @@ "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ę", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.pt-BR.json b/src/package.nls.pt-BR.json index 5b7b5d8a10..aaec3b4533 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.ru.json b/src/package.nls.ru.json index d33c05ed00..632c1e3628 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -20,6 +20,7 @@ "command.addToContext.title": "Добавить в контекст", "command.focusInput.title": "Фокус на поле ввода", "command.setCustomStoragePath.title": "Указать путь хранения", + "command.importSettings.title": "Импортировать настройки", "command.terminal.addToContext.title": "Добавить содержимое терминала в контекст", "command.terminal.fixCommand.title": "Исправить эту команду", "command.terminal.explainCommand.title": "Объяснить эту команду", @@ -30,5 +31,6 @@ "settings.vsCodeLmModelSelector.vendor.description": "Поставщик языковой модели (например, copilot)", "settings.vsCodeLmModelSelector.family.description": "Семейство языковой модели (например, gpt-4)", "settings.customStoragePath.description": "Пользовательский путь хранения. Оставьте пустым для использования пути по умолчанию. Поддерживает абсолютные пути (например, 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Включить быстрые исправления Roo Code." + "settings.enableCodeActions.description": "Включить быстрые исправления Roo Code.", + "settings.autoImportSettingsPath.description": "Путь к файлу конфигурации RooCode для автоматического импорта при запуске расширения. Поддерживает абсолютные пути и пути относительно домашнего каталога (например, '~/Documents/roo-code-settings.json'). Оставьте пустым, чтобы отключить автоматический импорт." } diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index 08a7e2ccca..a0b6c47d4f 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.vi.json b/src/package.nls.vi.json index cc8561b817..2b5013fed0 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -9,6 +9,7 @@ "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", @@ -30,5 +31,6 @@ "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.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." } diff --git a/src/package.nls.zh-CN.json b/src/package.nls.zh-CN.json index fb0cf02b88..76bc991c36 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "在新标签页中打开", "command.focusInput.title": "聚焦输入框", "command.setCustomStoragePath.title": "设置自定义存储路径", + "command.importSettings.title": "导入设置", "command.terminal.addToContext.title": "将终端内容添加到上下文", "command.terminal.fixCommand.title": "修复此命令", "command.terminal.explainCommand.title": "解释此命令", @@ -30,5 +31,6 @@ "settings.vsCodeLmModelSelector.vendor.description": "语言模型的供应商(例如:copilot)", "settings.vsCodeLmModelSelector.family.description": "语言模型的系列(例如:gpt-4)", "settings.customStoragePath.description": "自定义存储路径。留空以使用默认位置。支持绝对路径(例如:'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "启用 Roo Code 快速修复" + "settings.enableCodeActions.description": "启用 Roo Code 快速修复", + "settings.autoImportSettingsPath.description": "RooCode 配置文件的路径,用于在扩展启动时自动导入。支持绝对路径和相对于主目录的路径(例如 '~/Documents/roo-code-settings.json')。留空以禁用自动导入。" } diff --git a/src/package.nls.zh-TW.json b/src/package.nls.zh-TW.json index f9679fbb2e..c833708541 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -9,6 +9,7 @@ "command.openInNewTab.title": "在新分頁中開啟", "command.focusInput.title": "聚焦輸入框", "command.setCustomStoragePath.title": "設定自訂儲存路徑", + "command.importSettings.title": "匯入設定", "command.terminal.addToContext.title": "將終端內容新增到上下文", "command.terminal.fixCommand.title": "修復此命令", "command.terminal.explainCommand.title": "解釋此命令", @@ -30,5 +31,6 @@ "settings.vsCodeLmModelSelector.vendor.description": "語言模型供應商(例如:copilot)", "settings.vsCodeLmModelSelector.family.description": "語言模型系列(例如:gpt-4)", "settings.customStoragePath.description": "自訂儲存路徑。留空以使用預設位置。支援絕對路徑(例如:'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "啟用 Roo Code 快速修復。" + "settings.enableCodeActions.description": "啟用 Roo Code 快速修復。", + "settings.autoImportSettingsPath.description": "RooCode 設定檔案的路徑,用於在擴充功能啟動時自動匯入。支援絕對路徑和相對於主目錄的路徑(例如 '~/Documents/roo-code-settings.json')。留空以停用自動匯入。" } diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 7704caba0c..75b432f01d 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -10,6 +10,9 @@ import { fileExistsAtPath } from "../../utils/fs" import { BrowserActionResult } from "../../shared/ExtensionMessage" import { discoverChromeHostUrl, tryChromeHostUrl } from "./browserDiscovery" +// Timeout constants +const BROWSER_NAVIGATION_TIMEOUT = 15_000 // 15 seconds + interface PCRStats { puppeteer: { launch: typeof launch } executablePath: string @@ -320,7 +323,7 @@ export class BrowserSession { * Navigate to a URL with standard loading options */ private async navigatePageToUrl(page: Page, url: string): Promise { - await page.goto(url, { timeout: 7_000, waitUntil: ["domcontentloaded", "networkidle2"] }) + await page.goto(url, { timeout: BROWSER_NAVIGATION_TIMEOUT, waitUntil: ["domcontentloaded", "networkidle2"] }) await this.waitTillHTMLStable(page) } @@ -403,7 +406,10 @@ export class BrowserSession { console.log(`Root domain: ${this.getRootDomain(currentUrl)}`) console.log(`New URL: ${normalizedNewUrl}`) return this.doAction(async (page) => { - await page.reload({ timeout: 7_000, waitUntil: ["domcontentloaded", "networkidle2"] }) + await page.reload({ + timeout: BROWSER_NAVIGATION_TIMEOUT, + waitUntil: ["domcontentloaded", "networkidle2"], + }) await this.waitTillHTMLStable(page) }) } @@ -476,7 +482,7 @@ export class BrowserSession { await page .waitForNavigation({ waitUntil: ["domcontentloaded", "networkidle2"], - timeout: 7000, + timeout: BROWSER_NAVIGATION_TIMEOUT, }) .catch(() => {}) await this.waitTillHTMLStable(page) diff --git a/src/services/browser/UrlContentFetcher.ts b/src/services/browser/UrlContentFetcher.ts index caf19ee83b..8f4de7fbb4 100644 --- a/src/services/browser/UrlContentFetcher.ts +++ b/src/services/browser/UrlContentFetcher.ts @@ -7,6 +7,11 @@ import TurndownService from "turndown" // @ts-ignore import PCR from "puppeteer-chromium-resolver" import { fileExistsAtPath } from "../../utils/fs" +import { serializeError } from "serialize-error" + +// Timeout constants +const URL_FETCH_TIMEOUT = 30_000 // 30 seconds +const URL_FETCH_FALLBACK_TIMEOUT = 20_000 // 20 seconds for fallback interface PCRStats { puppeteer: { launch: typeof launch } @@ -48,11 +53,24 @@ export class UrlContentFetcher { this.browser = await stats.puppeteer.launch({ args: [ "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", + "--disable-dev-shm-usage", + "--disable-accelerated-2d-canvas", + "--no-first-run", + "--disable-gpu", + "--disable-features=VizDisplayCompositor", ], executablePath: stats.executablePath, }) // (latest version of puppeteer does not add headless to user agent) this.page = await this.browser?.newPage() + + // Set additional page configurations to improve loading success + if (this.page) { + await this.page.setViewport({ width: 1280, height: 720 }) + await this.page.setExtraHTTPHeaders({ + "Accept-Language": "en-US,en;q=0.9", + }) + } } async closeBrowser(): Promise { @@ -71,7 +89,40 @@ export class UrlContentFetcher { - domcontentloaded is when the basic DOM is loaded this should be sufficient for most doc sites */ - await this.page.goto(url, { timeout: 10_000, waitUntil: ["domcontentloaded", "networkidle2"] }) + try { + await this.page.goto(url, { + timeout: URL_FETCH_TIMEOUT, + waitUntil: ["domcontentloaded", "networkidle2"], + }) + } catch (error) { + // Use serialize-error to safely extract error information + const serializedError = serializeError(error) + const errorMessage = serializedError.message || String(error) + const errorName = serializedError.name + + // Only retry for timeout or network-related errors + const shouldRetry = + errorMessage.includes("timeout") || + errorMessage.includes("net::") || + errorMessage.includes("NetworkError") || + errorMessage.includes("ERR_") || + errorName === "TimeoutError" + + if (shouldRetry) { + // If networkidle2 fails due to timeout/network issues, try with just domcontentloaded as fallback + console.warn( + `Failed to load ${url} with networkidle2, retrying with domcontentloaded only: ${errorMessage}`, + ) + await this.page.goto(url, { + timeout: URL_FETCH_FALLBACK_TIMEOUT, + waitUntil: ["domcontentloaded"], + }) + } else { + // For other errors, throw them as-is + throw error + } + } + const content = await this.page.content() // use cheerio to parse and clean up the HTML diff --git a/src/services/browser/__tests__/UrlContentFetcher.spec.ts b/src/services/browser/__tests__/UrlContentFetcher.spec.ts new file mode 100644 index 0000000000..917b27c5f2 --- /dev/null +++ b/src/services/browser/__tests__/UrlContentFetcher.spec.ts @@ -0,0 +1,290 @@ +// npx vitest services/browser/__tests__/UrlContentFetcher.spec.ts + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { UrlContentFetcher } from "../UrlContentFetcher" +import { fileExistsAtPath } from "../../../utils/fs" +import * as path from "path" + +// Mock dependencies +vi.mock("vscode", () => ({ + ExtensionContext: vi.fn(), + Uri: { + file: vi.fn((path) => ({ fsPath: path })), + }, +})) + +// Mock fs/promises +vi.mock("fs/promises", () => ({ + default: { + mkdir: vi.fn().mockResolvedValue(undefined), + }, + mkdir: vi.fn().mockResolvedValue(undefined), +})) + +// Mock utils/fs +vi.mock("../../../utils/fs", () => ({ + fileExistsAtPath: vi.fn().mockResolvedValue(true), +})) + +// Mock cheerio +vi.mock("cheerio", () => ({ + load: vi.fn(() => { + const $ = vi.fn((selector) => ({ + remove: vi.fn().mockReturnThis(), + })) as any + $.html = vi.fn().mockReturnValue("Test content") + return $ + }), +})) + +// Mock turndown +vi.mock("turndown", () => { + return { + default: class MockTurndownService { + turndown = vi.fn().mockReturnValue("# Test content") + }, + } +}) + +// Mock puppeteer-chromium-resolver +vi.mock("puppeteer-chromium-resolver", () => ({ + default: vi.fn().mockResolvedValue({ + puppeteer: { + launch: vi.fn().mockResolvedValue({ + newPage: vi.fn().mockResolvedValue({ + goto: vi.fn(), + content: vi.fn().mockResolvedValue("Test content"), + setViewport: vi.fn().mockResolvedValue(undefined), + setExtraHTTPHeaders: vi.fn().mockResolvedValue(undefined), + }), + close: vi.fn().mockResolvedValue(undefined), + }), + }, + executablePath: "/path/to/chromium", + }), +})) + +// Mock serialize-error +vi.mock("serialize-error", () => ({ + serializeError: vi.fn((error) => { + if (error instanceof Error) { + return { message: error.message, name: error.name } + } else if (typeof error === "string") { + return { message: error } + } else if (error && typeof error === "object" && "message" in error) { + return { message: String(error.message), name: "name" in error ? String(error.name) : undefined } + } else { + return { message: String(error) } + } + }), +})) + +describe("UrlContentFetcher", () => { + let urlContentFetcher: UrlContentFetcher + let mockContext: any + let mockPage: any + let mockBrowser: any + let PCR: any + + beforeEach(async () => { + vi.clearAllMocks() + + mockContext = { + globalStorageUri: { + fsPath: "/test/storage", + }, + } + + mockPage = { + goto: vi.fn(), + content: vi.fn().mockResolvedValue("Test content"), + setViewport: vi.fn().mockResolvedValue(undefined), + setExtraHTTPHeaders: vi.fn().mockResolvedValue(undefined), + } + + mockBrowser = { + newPage: vi.fn().mockResolvedValue(mockPage), + close: vi.fn().mockResolvedValue(undefined), + } + + // Reset PCR mock + // @ts-ignore + PCR = (await import("puppeteer-chromium-resolver")).default + vi.mocked(PCR).mockResolvedValue({ + puppeteer: { + launch: vi.fn().mockResolvedValue(mockBrowser), + }, + executablePath: "/path/to/chromium", + }) + + urlContentFetcher = new UrlContentFetcher(mockContext) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe("launchBrowser", () => { + it("should launch browser with correct arguments", async () => { + await urlContentFetcher.launchBrowser() + + expect(vi.mocked(PCR)).toHaveBeenCalledWith({ + downloadPath: path.join("/test/storage", "puppeteer"), + }) + + const stats = await vi.mocked(PCR).mock.results[0].value + expect(stats.puppeteer.launch).toHaveBeenCalledWith({ + args: [ + "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", + "--disable-dev-shm-usage", + "--disable-accelerated-2d-canvas", + "--no-first-run", + "--disable-gpu", + "--disable-features=VizDisplayCompositor", + ], + executablePath: "/path/to/chromium", + }) + }) + + it("should set viewport and headers after launching", async () => { + await urlContentFetcher.launchBrowser() + + expect(mockPage.setViewport).toHaveBeenCalledWith({ width: 1280, height: 720 }) + expect(mockPage.setExtraHTTPHeaders).toHaveBeenCalledWith({ + "Accept-Language": "en-US,en;q=0.9", + }) + }) + + it("should not launch browser if already launched", async () => { + await urlContentFetcher.launchBrowser() + const initialCallCount = vi.mocked(PCR).mock.calls.length + + await urlContentFetcher.launchBrowser() + expect(vi.mocked(PCR)).toHaveBeenCalledTimes(initialCallCount) + }) + }) + + describe("urlToMarkdown", () => { + beforeEach(async () => { + await urlContentFetcher.launchBrowser() + }) + + it("should successfully fetch and convert URL to markdown", async () => { + mockPage.goto.mockResolvedValueOnce(undefined) + + const result = await urlContentFetcher.urlToMarkdown("https://example.com") + + expect(mockPage.goto).toHaveBeenCalledWith("https://example.com", { + timeout: 30000, + waitUntil: ["domcontentloaded", "networkidle2"], + }) + expect(result).toBe("# Test content") + }) + + it("should retry with domcontentloaded only when networkidle2 fails", async () => { + const timeoutError = new Error("Navigation timeout of 30000 ms exceeded") + mockPage.goto.mockRejectedValueOnce(timeoutError).mockResolvedValueOnce(undefined) + + const result = await urlContentFetcher.urlToMarkdown("https://example.com") + + expect(mockPage.goto).toHaveBeenCalledTimes(2) + expect(mockPage.goto).toHaveBeenNthCalledWith(1, "https://example.com", { + timeout: 30000, + waitUntil: ["domcontentloaded", "networkidle2"], + }) + expect(mockPage.goto).toHaveBeenNthCalledWith(2, "https://example.com", { + timeout: 20000, + waitUntil: ["domcontentloaded"], + }) + expect(result).toBe("# Test content") + }) + + it("should retry for network errors", async () => { + const networkError = new Error("net::ERR_CONNECTION_REFUSED") + mockPage.goto.mockRejectedValueOnce(networkError).mockResolvedValueOnce(undefined) + + const result = await urlContentFetcher.urlToMarkdown("https://example.com") + + expect(mockPage.goto).toHaveBeenCalledTimes(2) + expect(result).toBe("# Test content") + }) + + it("should retry for TimeoutError", async () => { + const timeoutError = new Error("TimeoutError: Navigation timeout") + timeoutError.name = "TimeoutError" + mockPage.goto.mockRejectedValueOnce(timeoutError).mockResolvedValueOnce(undefined) + + const result = await urlContentFetcher.urlToMarkdown("https://example.com") + + expect(mockPage.goto).toHaveBeenCalledTimes(2) + expect(result).toBe("# Test content") + }) + + it("should not retry for non-network/timeout errors", async () => { + const otherError = new Error("Some other error") + mockPage.goto.mockRejectedValueOnce(otherError) + + await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Some other error") + expect(mockPage.goto).toHaveBeenCalledTimes(1) + }) + + it("should throw error if browser not initialized", async () => { + const newFetcher = new UrlContentFetcher(mockContext) + + await expect(newFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Browser not initialized") + }) + + it("should handle errors without message property", async () => { + const errorWithoutMessage = { code: "UNKNOWN_ERROR" } + mockPage.goto.mockRejectedValueOnce(errorWithoutMessage) + + // serialize-error will convert this to a proper error with the object stringified + await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow() + + // Should not retry for non-network errors + expect(mockPage.goto).toHaveBeenCalledTimes(1) + }) + + it("should handle error objects with message property", async () => { + const errorWithMessage = { message: "Custom error", code: "CUSTOM_ERROR" } + mockPage.goto.mockRejectedValueOnce(errorWithMessage) + + await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Custom error") + + // Should not retry for error objects with message property (they're treated as known errors) + expect(mockPage.goto).toHaveBeenCalledTimes(1) + }) + + it("should retry for error objects with network-related messages", async () => { + const errorWithNetworkMessage = { message: "net::ERR_CONNECTION_REFUSED", code: "NETWORK_ERROR" } + mockPage.goto.mockRejectedValueOnce(errorWithNetworkMessage).mockResolvedValueOnce(undefined) + + const result = await urlContentFetcher.urlToMarkdown("https://example.com") + + // Should retry for network-related errors even in non-Error objects + expect(mockPage.goto).toHaveBeenCalledTimes(2) + expect(result).toBe("# Test content") + }) + + it("should handle string errors", async () => { + const stringError = "Simple string error" + mockPage.goto.mockRejectedValueOnce(stringError) + + await expect(urlContentFetcher.urlToMarkdown("https://example.com")).rejects.toThrow("Simple string error") + expect(mockPage.goto).toHaveBeenCalledTimes(1) + }) + }) + + describe("closeBrowser", () => { + it("should close browser and reset state", async () => { + await urlContentFetcher.launchBrowser() + await urlContentFetcher.closeBrowser() + + expect(mockBrowser.close).toHaveBeenCalled() + }) + + it("should handle closing when browser not initialized", async () => { + await expect(urlContentFetcher.closeBrowser()).resolves.not.toThrow() + }) + }) +}) diff --git a/src/services/code-index/__tests__/cache-manager.spec.ts b/src/services/code-index/__tests__/cache-manager.spec.ts index 27408fdf33..e61a92f3cc 100644 --- a/src/services/code-index/__tests__/cache-manager.spec.ts +++ b/src/services/code-index/__tests__/cache-manager.spec.ts @@ -4,6 +4,14 @@ 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: { @@ -89,7 +97,7 @@ describe("CacheManager", () => { cacheManager.updateHash(filePath, hash) expect(cacheManager.getHash(filePath)).toBe(hash) - expect(vscode.workspace.fs.writeFile).toHaveBeenCalled() + expect(safeWriteJson).toHaveBeenCalled() }) it("should delete hash and trigger save", () => { @@ -100,7 +108,7 @@ describe("CacheManager", () => { cacheManager.deleteHash(filePath) expect(cacheManager.getHash(filePath)).toBeUndefined() - expect(vscode.workspace.fs.writeFile).toHaveBeenCalled() + expect(safeWriteJson).toHaveBeenCalled() }) it("should return shallow copy of hashes", () => { @@ -125,18 +133,16 @@ describe("CacheManager", () => { cacheManager.updateHash(filePath, hash) - expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(mockCachePath, expect.any(Uint8Array)) + expect(safeWriteJson).toHaveBeenCalledWith(mockCachePath.fsPath, expect.any(Object)) // Verify the saved data - const savedData = JSON.parse( - Buffer.from((vscode.workspace.fs.writeFile as Mock).mock.calls[0][1]).toString(), - ) + const savedData = (safeWriteJson as Mock).mock.calls[0][1] expect(savedData).toEqual({ [filePath]: hash }) }) it("should handle save errors gracefully", async () => { const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) - ;(vscode.workspace.fs.writeFile as Mock).mockRejectedValue(new Error("Save failed")) + ;(safeWriteJson as Mock).mockRejectedValue(new Error("Save failed")) cacheManager.updateHash("test.ts", "hash") @@ -153,19 +159,19 @@ describe("CacheManager", () => { it("should clear cache file and reset state", async () => { cacheManager.updateHash("test.ts", "hash") - // Reset the mock to ensure writeFile succeeds for clearCacheFile - ;(vscode.workspace.fs.writeFile as Mock).mockClear() - ;(vscode.workspace.fs.writeFile as Mock).mockResolvedValue(undefined) + // Reset the mock to ensure safeWriteJson succeeds for clearCacheFile + ;(safeWriteJson as Mock).mockClear() + ;(safeWriteJson as Mock).mockResolvedValue(undefined) await cacheManager.clearCacheFile() - expect(vscode.workspace.fs.writeFile).toHaveBeenCalledWith(mockCachePath, Buffer.from("{}")) + expect(safeWriteJson).toHaveBeenCalledWith(mockCachePath.fsPath, {}) expect(cacheManager.getAllHashes()).toEqual({}) }) it("should handle clear errors gracefully", async () => { const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {}) - ;(vscode.workspace.fs.writeFile as Mock).mockRejectedValue(new Error("Save failed")) + ;(safeWriteJson as Mock).mockRejectedValue(new Error("Save failed")) await cacheManager.clearCacheFile() diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index f5a759c158..2da19a6a1e 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -709,6 +709,153 @@ describe("CodeIndexConfigManager", () => { const result = await configManager.loadConfiguration() expect(result.requiresRestart).toBe(false) }) + + describe("currentSearchMinScore priority system", () => { + it("should return user-configured score when set", async () => { + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderModelId: "text-embedding-3-small", + codebaseIndexSearchMinScore: 0.8, // User setting + }) + mockContextProxy.getSecret.mockImplementation((key: string) => { + if (key === "codeIndexOpenAiKey") return "test-key" + return undefined + }) + + await configManager.loadConfiguration() + expect(configManager.currentSearchMinScore).toBe(0.8) + }) + + it("should fall back to model-specific threshold when user setting is undefined", async () => { + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "ollama", + codebaseIndexEmbedderModelId: "nomic-embed-code", + // No codebaseIndexSearchMinScore - user hasn't configured it + }) + + await configManager.loadConfiguration() + // nomic-embed-code has a specific threshold of 0.15 + expect(configManager.currentSearchMinScore).toBe(0.15) + }) + + it("should fall back to default SEARCH_MIN_SCORE when neither user setting nor model threshold exists", async () => { + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderModelId: "unknown-model", // Model not in profiles + // No codebaseIndexSearchMinScore + }) + mockContextProxy.getSecret.mockImplementation((key: string) => { + if (key === "codeIndexOpenAiKey") return "test-key" + return undefined + }) + + await configManager.loadConfiguration() + // Should fall back to default SEARCH_MIN_SCORE (0.4) + expect(configManager.currentSearchMinScore).toBe(0.4) + }) + + it("should respect user setting of 0 (edge case)", async () => { + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "ollama", + codebaseIndexEmbedderModelId: "nomic-embed-code", + codebaseIndexSearchMinScore: 0, // User explicitly sets 0 + }) + + await configManager.loadConfiguration() + // Should return 0, not fall back to model threshold (0.15) + expect(configManager.currentSearchMinScore).toBe(0) + }) + + it("should use model-specific threshold with openai-compatible provider", async () => { + mockContextProxy.getGlobalState.mockImplementation((key: string) => { + if (key === "codebaseIndexConfig") { + return { + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "openai-compatible", + codebaseIndexEmbedderModelId: "nomic-embed-code", + // No codebaseIndexSearchMinScore + } + } + if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1" + return undefined + }) + mockContextProxy.getSecret.mockImplementation((key: string) => { + if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-api-key" + return undefined + }) + + await configManager.loadConfiguration() + // openai-compatible provider also has nomic-embed-code with 0.15 threshold + expect(configManager.currentSearchMinScore).toBe(0.15) + }) + + it("should use default model ID when modelId is not specified", async () => { + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "openai", + // No modelId specified + // No codebaseIndexSearchMinScore + }) + mockContextProxy.getSecret.mockImplementation((key: string) => { + if (key === "codeIndexOpenAiKey") return "test-key" + return undefined + }) + + await configManager.loadConfiguration() + // Should use default model (text-embedding-3-small) threshold (0.4) + expect(configManager.currentSearchMinScore).toBe(0.4) + }) + + it("should handle priority correctly: user > model > default", async () => { + // Test 1: User setting takes precedence + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "ollama", + codebaseIndexEmbedderModelId: "nomic-embed-code", // Has 0.15 threshold + codebaseIndexSearchMinScore: 0.9, // User overrides + }) + + await configManager.loadConfiguration() + expect(configManager.currentSearchMinScore).toBe(0.9) // User setting wins + + // Test 2: Model threshold when no user setting + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "ollama", + codebaseIndexEmbedderModelId: "nomic-embed-code", + // No user setting + }) + + const newManager = new CodeIndexConfigManager(mockContextProxy) + await newManager.loadConfiguration() + expect(newManager.currentSearchMinScore).toBe(0.15) // Model threshold + + // Test 3: Default when neither exists + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexQdrantUrl: "http://qdrant.local", + codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderModelId: "custom-unknown-model", + // No user setting, unknown model + }) + + const anotherManager = new CodeIndexConfigManager(mockContextProxy) + await anotherManager.loadConfiguration() + expect(anotherManager.currentSearchMinScore).toBe(0.4) // Default + }) + }) }) describe("empty/missing API key handling", () => { diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index 12583291eb..2aaeb1d374 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -89,7 +89,7 @@ describe("CodeIndexManager - handleExternalSettingsChange regression", () => { expect(manager.isInitialized).toBe(true) // Mock the methods that would be called during restart - const stopWatcherSpy = vitest.spyOn(manager, "stopWatcher").mockImplementation(() => {}) + const recreateServicesSpy = vitest.spyOn(manager as any, "_recreateServices").mockImplementation(() => {}) const startIndexingSpy = vitest.spyOn(manager, "startIndexing").mockResolvedValue() // Mock the feature state @@ -100,7 +100,7 @@ describe("CodeIndexManager - handleExternalSettingsChange regression", () => { // Verify that the restart sequence was called expect(mockConfigManager.loadConfiguration).toHaveBeenCalled() - expect(stopWatcherSpy).toHaveBeenCalled() + expect(recreateServicesSpy).toHaveBeenCalled() expect(startIndexingSpy).toHaveBeenCalled() }) diff --git a/src/services/code-index/cache-manager.ts b/src/services/code-index/cache-manager.ts index f66f933a0b..146db4cd2a 100644 --- a/src/services/code-index/cache-manager.ts +++ b/src/services/code-index/cache-manager.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode" import { createHash } from "crypto" import { ICacheManager } from "./interfaces/cache" import debounce from "lodash.debounce" +import { safeWriteJson } from "../../utils/safeWriteJson" /** * Manages the cache for code indexing @@ -46,7 +47,7 @@ export class CacheManager implements ICacheManager { */ private async _performSave(): Promise { try { - await vscode.workspace.fs.writeFile(this.cachePath, Buffer.from(JSON.stringify(this.fileHashes, null, 2))) + await safeWriteJson(this.cachePath.fsPath, this.fileHashes) } catch (error) { console.error("Failed to save cache:", error) } @@ -57,7 +58,7 @@ export class CacheManager implements ICacheManager { */ async clearCacheFile(): Promise { try { - await vscode.workspace.fs.writeFile(this.cachePath, Buffer.from("{}")) + await safeWriteJson(this.cachePath.fsPath, {}) this.fileHashes = {} } catch (error) { console.error("Failed to clear cache file:", error, this.cachePath) diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 678cec36a1..d0eb9268b2 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -3,7 +3,7 @@ import { ContextProxy } from "../../core/config/ContextProxy" import { EmbedderProvider } from "./interfaces/manager" import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config" import { SEARCH_MIN_SCORE } from "./constants" -import { getDefaultModelId, getModelDimension } from "../../shared/embeddingModels" +import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels" /** * Manages configuration state and validation for the code indexing feature. @@ -34,10 +34,10 @@ export class CodeIndexConfigManager { const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? { codebaseIndexEnabled: false, codebaseIndexQdrantUrl: "http://localhost:6333", - codebaseIndexSearchMinScore: 0.4, codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "", + codebaseIndexSearchMinScore: undefined, } const { @@ -46,6 +46,7 @@ export class CodeIndexConfigManager { codebaseIndexEmbedderProvider, codebaseIndexEmbedderBaseUrl, codebaseIndexEmbedderModelId, + codebaseIndexSearchMinScore, } = codebaseIndexConfig const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? "" @@ -60,8 +61,8 @@ export class CodeIndexConfigManager { this.isEnabled = codebaseIndexEnabled || false this.qdrantUrl = codebaseIndexQdrantUrl this.qdrantApiKey = qdrantApiKey ?? "" + this.searchMinScore = codebaseIndexSearchMinScore this.openAiOptions = { openAiNativeApiKey: openAiKey } - this.searchMinScore = SEARCH_MIN_SCORE // Set embedder provider with support for openai-compatible if (codebaseIndexEmbedderProvider === "ollama") { @@ -139,7 +140,7 @@ export class CodeIndexConfigManager { openAiCompatibleOptions: this.openAiCompatibleOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, - searchMinScore: this.searchMinScore, + searchMinScore: this.currentSearchMinScore, }, requiresRestart, } @@ -294,7 +295,7 @@ export class CodeIndexConfigManager { openAiCompatibleOptions: this.openAiCompatibleOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, - searchMinScore: this.searchMinScore, + searchMinScore: this.currentSearchMinScore, } } @@ -337,9 +338,18 @@ export class CodeIndexConfigManager { } /** - * Gets the configured minimum search score. + * Gets the configured minimum search score based on user setting, model-specific threshold, or fallback. + * Priority: 1) User setting, 2) Model-specific threshold, 3) Default SEARCH_MIN_SCORE constant. */ - public get currentSearchMinScore(): number | undefined { - return this.searchMinScore + public get currentSearchMinScore(): number { + // First check if user has configured a custom score threshold + if (this.searchMinScore !== undefined) { + return this.searchMinScore + } + + // Fall back to model-specific threshold + const currentModelId = this.modelId ?? getDefaultModelId(this.embedderProvider) + const modelSpecificThreshold = getModelScoreThreshold(this.embedderProvider, currentModelId) + return modelSpecificThreshold ?? SEARCH_MIN_SCORE } } diff --git a/src/services/code-index/embedders/ollama.ts b/src/services/code-index/embedders/ollama.ts index 748ed188a4..2f212c7745 100644 --- a/src/services/code-index/embedders/ollama.ts +++ b/src/services/code-index/embedders/ollama.ts @@ -1,5 +1,7 @@ import { ApiHandlerOptions } from "../../../shared/api" import { EmbedderInfo, EmbeddingResponse, IEmbedder } from "../interfaces" +import { getModelQueryPrefix } from "../../../shared/embeddingModels" +import { MAX_ITEM_TOKENS } from "../constants" import { t } from "../../../i18n" /** @@ -25,6 +27,31 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { const modelToUse = model || this.defaultModelId const url = `${this.baseUrl}/api/embed` // Endpoint as specified + // Apply model-specific query prefix if required + const queryPrefix = getModelQueryPrefix("ollama", modelToUse) + const processedTexts = queryPrefix + ? texts.map((text, index) => { + // Prevent double-prefixing + if (text.startsWith(queryPrefix)) { + return text + } + const prefixedText = `${queryPrefix}${text}` + const estimatedTokens = Math.ceil(prefixedText.length / 4) + if (estimatedTokens > MAX_ITEM_TOKENS) { + console.warn( + t("embeddings:textWithPrefixExceedsTokenLimit", { + index, + estimatedTokens, + maxTokens: MAX_ITEM_TOKENS, + }), + ) + // Return original text if adding prefix would exceed limit + return text + } + return prefixedText + }) + : texts + try { // Note: Standard Ollama API uses 'prompt' for single text, not 'input' for array. // Implementing based on user's specific request structure. @@ -35,7 +62,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { }, body: JSON.stringify({ model: modelToUse, - input: texts, // Using 'input' as requested + input: processedTexts, // Using 'input' as requested }), }) diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index 0983cc297f..8bf35eea24 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -6,7 +6,7 @@ import { MAX_BATCH_RETRIES as MAX_RETRIES, INITIAL_RETRY_DELAY_MS as INITIAL_DELAY_MS, } from "../constants" -import { getDefaultModelId } from "../../../shared/embeddingModels" +import { getDefaultModelId, getModelQueryPrefix } from "../../../shared/embeddingModels" import { t } from "../../../i18n" interface EmbeddingItem { @@ -59,9 +59,35 @@ export class OpenAICompatibleEmbedder implements IEmbedder { */ async createEmbeddings(texts: string[], model?: string): Promise { const modelToUse = model || this.defaultModelId + + // Apply model-specific query prefix if required + const queryPrefix = getModelQueryPrefix("openai-compatible", modelToUse) + const processedTexts = queryPrefix + ? texts.map((text, index) => { + // Prevent double-prefixing + if (text.startsWith(queryPrefix)) { + return text + } + const prefixedText = `${queryPrefix}${text}` + const estimatedTokens = Math.ceil(prefixedText.length / 4) + if (estimatedTokens > MAX_ITEM_TOKENS) { + console.warn( + t("embeddings:textWithPrefixExceedsTokenLimit", { + index, + estimatedTokens, + maxTokens: MAX_ITEM_TOKENS, + }), + ) + // Return original text if adding prefix would exceed limit + return text + } + return prefixedText + }) + : texts + const allEmbeddings: number[][] = [] const usage = { promptTokens: 0, totalTokens: 0 } - const remainingTexts = [...texts] + const remainingTexts = [...processedTexts] while (remainingTexts.length > 0) { const currentBatch: string[] = [] diff --git a/src/services/code-index/embedders/openai.ts b/src/services/code-index/embedders/openai.ts index d0dc132df7..667c2f46d4 100644 --- a/src/services/code-index/embedders/openai.ts +++ b/src/services/code-index/embedders/openai.ts @@ -8,6 +8,7 @@ import { MAX_BATCH_RETRIES as MAX_RETRIES, INITIAL_RETRY_DELAY_MS as INITIAL_DELAY_MS, } from "../constants" +import { getModelQueryPrefix } from "../../../shared/embeddingModels" import { t } from "../../../i18n" /** @@ -36,9 +37,35 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder { */ async createEmbeddings(texts: string[], model?: string): Promise { const modelToUse = model || this.defaultModelId + + // Apply model-specific query prefix if required + const queryPrefix = getModelQueryPrefix("openai", modelToUse) + const processedTexts = queryPrefix + ? texts.map((text, index) => { + // Prevent double-prefixing + if (text.startsWith(queryPrefix)) { + return text + } + const prefixedText = `${queryPrefix}${text}` + const estimatedTokens = Math.ceil(prefixedText.length / 4) + if (estimatedTokens > MAX_ITEM_TOKENS) { + console.warn( + t("embeddings:textWithPrefixExceedsTokenLimit", { + index, + estimatedTokens, + maxTokens: MAX_ITEM_TOKENS, + }), + ) + // Return original text if adding prefix would exceed limit + return text + } + return prefixedText + }) + : texts + const allEmbeddings: number[][] = [] const usage = { promptTokens: 0, totalTokens: 0 } - const remainingTexts = [...texts] + const remainingTexts = [...processedTexts] while (remainingTexts.length > 0) { const currentBatch: string[] = [] diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index 465fa95d0c..735bcee670 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -123,54 +123,7 @@ export class CodeIndexManager { const needsServiceRecreation = !this._serviceFactory || requiresRestart if (needsServiceRecreation) { - // Stop watcher if it exists - if (this._orchestrator) { - this.stopWatcher() - } - - // (Re)Initialize service factory - this._serviceFactory = new CodeIndexServiceFactory( - this._configManager, - this.workspacePath, - this._cacheManager, - ) - - const ignoreInstance = ignore() - const ignorePath = path.join(getWorkspacePath(), ".gitignore") - try { - const content = await fs.readFile(ignorePath, "utf8") - ignoreInstance.add(content) - ignoreInstance.add(".gitignore") - } catch (error) { - // Should never happen: reading file failed even though it exists - console.error("Unexpected error loading .gitignore:", error) - } - - // (Re)Create shared service instances - const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices( - this.context, - this._cacheManager, - ignoreInstance, - ) - - // (Re)Initialize orchestrator - this._orchestrator = new CodeIndexOrchestrator( - this._configManager, - this._stateManager, - this.workspacePath, - this._cacheManager, - vectorStore, - scanner, - fileWatcher, - ) - - // (Re)Initialize search service - this._searchService = new CodeIndexSearchService( - this._configManager, - this._stateManager, - embedder, - vectorStore, - ) + await this._recreateServices() } // 5. Handle Indexing Start/Restart @@ -248,6 +201,61 @@ export class CodeIndexManager { return this._searchService!.searchIndex(query, directoryPrefix) } + /** + * Private helper method to recreate services with current configuration. + * Used by both initialize() and handleExternalSettingsChange(). + */ + private async _recreateServices(): Promise { + // Stop watcher if it exists + if (this._orchestrator) { + this.stopWatcher() + } + + // (Re)Initialize service factory + this._serviceFactory = new CodeIndexServiceFactory( + this._configManager!, + this.workspacePath, + this._cacheManager!, + ) + + const ignoreInstance = ignore() + const ignorePath = path.join(getWorkspacePath(), ".gitignore") + try { + const content = await fs.readFile(ignorePath, "utf8") + ignoreInstance.add(content) + ignoreInstance.add(".gitignore") + } catch (error) { + // Should never happen: reading file failed even though it exists + console.error("Unexpected error loading .gitignore:", error) + } + + // (Re)Create shared service instances + const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices( + this.context, + this._cacheManager!, + ignoreInstance, + ) + + // (Re)Initialize orchestrator + this._orchestrator = new CodeIndexOrchestrator( + this._configManager!, + this._stateManager, + this.workspacePath, + this._cacheManager!, + vectorStore, + scanner, + fileWatcher, + ) + + // (Re)Initialize search service + this._searchService = new CodeIndexSearchService( + this._configManager!, + this._stateManager, + embedder, + vectorStore, + ) + } + /** * Handles external settings changes by reloading configuration. * This method should be called when API provider settings are updated @@ -263,7 +271,10 @@ export class CodeIndexManager { // If configuration changes require a restart and the manager is initialized, restart the service if (requiresRestart && isFeatureEnabled && isFeatureConfigured && this.isInitialized) { - this.stopWatcher() + // Recreate services with new configuration + await this._recreateServices() + + // Start indexing with new services await this.startIndexing() } } diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index 75f14b0d4c..91f5bcad26 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -84,7 +84,7 @@ export class SimpleInstaller { // Write back to file await fs.mkdir(path.dirname(filePath), { recursive: true }) - const yamlContent = yaml.stringify(existingData) + const yamlContent = yaml.stringify(existingData, { lineWidth: 0 }) await fs.writeFile(filePath, yamlContent, "utf-8") // Calculate approximate line number where the new mode was added @@ -282,7 +282,7 @@ export class SimpleInstaller { 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") + await fs.writeFile(filePath, yaml.stringify(existingData, { lineWidth: 0 }), "utf-8") } } catch (error: any) { if (error.code === "ENOENT") { diff --git a/src/services/mcp/__tests__/McpHub.spec.ts b/src/services/mcp/__tests__/McpHub.spec.ts index 1ed2993f2a..98ef4514c2 100644 --- a/src/services/mcp/__tests__/McpHub.spec.ts +++ b/src/services/mcp/__tests__/McpHub.spec.ts @@ -5,6 +5,43 @@ 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({ @@ -56,6 +93,7 @@ describe("McpHub", () => { // Mock console.error to suppress error messages during tests console.error = vi.fn() + const mockUri: Uri = { scheme: "file", authority: "", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 73ebf59d4c..031c1f1d6f 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -73,6 +73,9 @@ export interface ExtensionMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" + | "exportModeResult" + | "importModeResult" + | "checkRulesDirectoryResult" | "currentCheckpointUpdated" | "showHumanRelayDialog" | "humanRelayResponse" @@ -141,6 +144,7 @@ export interface ExtensionMessage { error?: string setting?: string value?: any + hasContent?: boolean // For checkRulesDirectoryResult items?: MarketplaceItem[] userInfo?: CloudUserInfo organizationAllowList?: OrganizationAllowList diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 7efc97e8c7..0f1d22c3c7 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -37,6 +37,8 @@ export interface WebviewMessage { | "alwaysAllowWriteOutsideWorkspace" | "alwaysAllowWriteProtected" | "alwaysAllowExecute" + | "alwaysAllowFollowupQuestions" + | "followupAutoApproveTimeoutMs" | "webviewDidLaunch" | "newTask" | "askResponse" @@ -175,6 +177,12 @@ export interface WebviewMessage { | "switchTab" | "profileThresholds" | "shareTaskSuccess" + | "exportMode" + | "exportModeResult" + | "importMode" + | "importModeResult" + | "checkRulesDirectory" + | "checkRulesDirectoryResult" text?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" disabled?: boolean @@ -213,6 +221,7 @@ export interface WebviewMessage { mpInstallOptions?: InstallMarketplaceItemOptions config?: Record // Add config to the payload visibility?: ShareVisibility // For share visibility + hasContent?: boolean // For checkRulesDirectoryResult } export const checkoutDiffPayloadSchema = z.object({ diff --git a/src/shared/__tests__/modes.spec.ts b/src/shared/__tests__/modes.spec.ts index 8ca7eec150..acf7da84c8 100644 --- a/src/shared/__tests__/modes.spec.ts +++ b/src/shared/__tests__/modes.spec.ts @@ -356,7 +356,7 @@ describe("FileRestrictionError", () => { const result = await getFullModeDetails("non-existent") expect(result).toMatchObject({ ...modes[0], - customInstructions: "", + // The first mode (architect) has its own customInstructions }) }) }) diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index cd7c1d4e6b..c78dc6c487 100644 --- a/src/shared/embeddingModels.ts +++ b/src/shared/embeddingModels.ts @@ -6,6 +6,8 @@ export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" // Add export interface EmbeddingModelProfile { dimension: number + scoreThreshold?: number // Model-specific minimum score threshold for semantic search + queryPrefix?: string // Optional prefix required by the model for queries // Add other model-specific properties if needed, e.g., context window size } @@ -18,21 +20,31 @@ export type EmbeddingModelProfiles = { // Example profiles - expand this list as needed export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = { openai: { - "text-embedding-3-small": { dimension: 1536 }, - "text-embedding-3-large": { dimension: 3072 }, - "text-embedding-ada-002": { dimension: 1536 }, + "text-embedding-3-small": { dimension: 1536, scoreThreshold: 0.4 }, + "text-embedding-3-large": { dimension: 3072, scoreThreshold: 0.4 }, + "text-embedding-ada-002": { dimension: 1536, scoreThreshold: 0.4 }, }, ollama: { - "nomic-embed-text": { dimension: 768 }, - "mxbai-embed-large": { dimension: 1024 }, - "all-minilm": { dimension: 384 }, + "nomic-embed-text": { dimension: 768, scoreThreshold: 0.4 }, + "nomic-embed-code": { + dimension: 3584, + scoreThreshold: 0.15, + queryPrefix: "Represent this query for searching relevant code: ", + }, + "mxbai-embed-large": { dimension: 1024, scoreThreshold: 0.4 }, + "all-minilm": { dimension: 384, scoreThreshold: 0.4 }, // Add default Ollama model if applicable, e.g.: // 'default': { dimension: 768 } // Assuming a default dimension }, "openai-compatible": { - "text-embedding-3-small": { dimension: 1536 }, - "text-embedding-3-large": { dimension: 3072 }, - "text-embedding-ada-002": { dimension: 1536 }, + "text-embedding-3-small": { dimension: 1536, scoreThreshold: 0.4 }, + "text-embedding-3-large": { dimension: 3072, scoreThreshold: 0.4 }, + "text-embedding-ada-002": { dimension: 1536, scoreThreshold: 0.4 }, + "nomic-embed-code": { + dimension: 3584, + scoreThreshold: 0.15, + queryPrefix: "Represent this query for searching relevant code: ", + }, }, } @@ -59,6 +71,38 @@ export function getModelDimension(provider: EmbedderProvider, modelId: string): return modelProfile.dimension } +/** + * Retrieves the score threshold for a given provider and model ID. + * @param provider The embedder provider (e.g., "openai"). + * @param modelId The specific model ID (e.g., "text-embedding-3-small"). + * @returns The score threshold or undefined if the model is not found. + */ +export function getModelScoreThreshold(provider: EmbedderProvider, modelId: string): number | undefined { + const providerProfiles = EMBEDDING_MODEL_PROFILES[provider] + if (!providerProfiles) { + return undefined + } + + const modelProfile = providerProfiles[modelId] + return modelProfile?.scoreThreshold +} + +/** + * Retrieves the query prefix for a given provider and model ID. + * @param provider The embedder provider (e.g., "openai"). + * @param modelId The specific model ID (e.g., "nomic-embed-code"). + * @returns The query prefix or undefined if the model doesn't require one. + */ +export function getModelQueryPrefix(provider: EmbedderProvider, modelId: string): string | undefined { + const providerProfiles = EMBEDDING_MODEL_PROFILES[provider] + if (!providerProfiles) { + return undefined + } + + const modelProfile = providerProfiles[modelId] + return modelProfile?.queryPrefix +} + /** * Gets the default *specific* embedding model ID based on the provider. * Does not include the provider prefix. diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 53bc2369db..23efdc45f7 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -60,17 +60,8 @@ 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: "code", - name: "💻 Code", - roleDefinition: - "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", @@ -81,7 +72,17 @@ export const modes: readonly ModeConfig[] = [ description: "Plan and design before implementation", groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], customInstructions: - "1. Do some information gathering (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.", + "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. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Do not provide time estimates for how long tasks will take to complete. Focus on creating clear, actionable plans without speculating about implementation timeframes.**", + }, + { + slug: "code", + name: "💻 Code", + roleDefinition: + "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: "ask", diff --git a/src/utils/__tests__/autoImportSettings.spec.ts b/src/utils/__tests__/autoImportSettings.spec.ts new file mode 100644 index 0000000000..2b9b42293f --- /dev/null +++ b/src/utils/__tests__/autoImportSettings.spec.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" + +// Mock dependencies +vi.mock("vscode", () => ({ + workspace: { + getConfiguration: vi.fn(), + }, + window: { + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + }, +})) + +vi.mock("fs/promises", () => ({ + __esModule: true, + default: { + readFile: vi.fn(), + }, + readFile: vi.fn(), +})) + +vi.mock("path", () => ({ + join: vi.fn((...args: string[]) => args.join("/")), + isAbsolute: vi.fn((p: string) => p.startsWith("/")), + basename: vi.fn((p: string) => p.split("/").pop() || ""), +})) + +vi.mock("os", () => ({ + homedir: vi.fn(() => "/home/user"), +})) + +vi.mock("../fs", () => ({ + fileExistsAtPath: vi.fn(), +})) + +vi.mock("../../core/config/ProviderSettingsManager", async (importOriginal) => { + const originalModule = await importOriginal() + return { + __esModule: true, + // We need to mock the class constructor and its methods, + // but keep other exports (like schemas) as their original values. + ...(originalModule || {}), // Spread original exports + ProviderSettingsManager: vi.fn().mockImplementation(() => ({ + // Mock the class + export: vi.fn().mockResolvedValue({ + apiConfigs: {}, + modeApiConfigs: {}, + currentApiConfigName: "default", + }), + import: vi.fn().mockResolvedValue({ success: true }), + listConfig: vi.fn().mockResolvedValue([]), + })), + } +}) +vi.mock("../../core/config/ContextProxy") +vi.mock("../../core/config/CustomModesManager") + +import { autoImportSettings } from "../autoImportSettings" +import * as vscode from "vscode" +import fsPromises from "fs/promises" +import { fileExistsAtPath } from "../fs" + +describe("autoImportSettings", () => { + let mockProviderSettingsManager: any + let mockContextProxy: any + let mockCustomModesManager: any + let mockOutputChannel: any + let mockProvider: any + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks() + + // Mock output channel + mockOutputChannel = { + appendLine: vi.fn(), + } + + // Mock provider settings manager + mockProviderSettingsManager = { + export: vi.fn().mockResolvedValue({ + apiConfigs: {}, + modeApiConfigs: {}, + currentApiConfigName: "default", + }), + import: vi.fn().mockResolvedValue({ success: true }), + listConfig: vi.fn().mockResolvedValue([]), + } + + // Mock context proxy + mockContextProxy = { + setValues: vi.fn().mockResolvedValue(undefined), + setValue: vi.fn().mockResolvedValue(undefined), + setProviderSettings: vi.fn().mockResolvedValue(undefined), + } + + // Mock custom modes manager + mockCustomModesManager = { + updateCustomMode: vi.fn().mockResolvedValue(undefined), + } + + // mockProvider must be initialized AFTER its dependencies + mockProvider = { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + upsertProviderProfile: vi.fn().mockResolvedValue({ success: true }), + postStateToWebview: vi.fn().mockResolvedValue({ success: true }), + } + + // Reset fs mock + vi.mocked(fsPromises.readFile).mockReset() + vi.mocked(fileExistsAtPath).mockReset() + vi.mocked(vscode.workspace.getConfiguration).mockReset() + vi.mocked(vscode.window.showInformationMessage).mockReset() + vi.mocked(vscode.window.showWarningMessage).mockReset() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should skip auto-import when no settings path is specified", async () => { + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(""), + } as any) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] No auto-import settings path specified, skipping auto-import", + ) + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) + + it("should skip auto-import when settings file does not exist", async () => { + const settingsPath = "~/Documents/roo-config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return false + vi.mocked(fileExistsAtPath).mockResolvedValue(false) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /home/user/Documents/roo-config.json", + ) + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Settings file not found at /home/user/Documents/roo-config.json, skipping auto-import", + ) + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) + + it("should successfully import settings when file exists and is valid", async () => { + const settingsPath = "/absolute/path/to/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return true + vi.mocked(fileExistsAtPath).mockResolvedValue(true) + + // Mock fs.readFile to return valid config + const mockSettings = { + providerProfiles: { + currentApiConfigName: "test-config", + apiConfigs: { + "test-config": { + apiProvider: "anthropic", + anthropicApiKey: "test-key", + }, + }, + }, + globalSettings: { + customInstructions: "Test instructions", + }, + } + + vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify(mockSettings) as any) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /absolute/path/to/config.json", + ) + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Successfully imported settings from /absolute/path/to/config.json", + ) + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("info.auto_import_success") + expect(mockProviderSettingsManager.import).toHaveBeenCalled() + expect(mockContextProxy.setValues).toHaveBeenCalled() + }) + + it("should handle invalid JSON gracefully", async () => { + const settingsPath = "~/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return true + vi.mocked(fileExistsAtPath).mockResolvedValue(true) + + // Mock fs.readFile to return invalid JSON + vi.mocked(fsPromises.readFile).mockResolvedValue("invalid json" as any) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("[AutoImport] Failed to import settings:"), + ) + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + expect.stringContaining("warnings.auto_import_failed"), + ) + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) + + it("should resolve home directory paths correctly", async () => { + const settingsPath = "~/Documents/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return false (so we can check the resolved path) + vi.mocked(fileExistsAtPath).mockResolvedValue(false) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /home/user/Documents/config.json", + ) + }) + + it("should handle relative paths by resolving them to home directory", async () => { + const settingsPath = "Documents/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to return false (so we can check the resolved path) + vi.mocked(fileExistsAtPath).mockResolvedValue(false) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[AutoImport] Checking for settings file at: /home/user/Documents/config.json", + ) + }) + + it("should handle file system errors gracefully", async () => { + const settingsPath = "~/config.json" + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(settingsPath), + } as any) + + // Mock fileExistsAtPath to throw an error + vi.mocked(fileExistsAtPath).mockRejectedValue(new Error("File system error")) + + await autoImportSettings(mockOutputChannel, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("[AutoImport] Unexpected error during auto-import:"), + ) + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + }) +}) diff --git a/src/utils/__tests__/git.spec.ts b/src/utils/__tests__/git.spec.ts index 754d041e29..3ab306feec 100644 --- a/src/utils/__tests__/git.spec.ts +++ b/src/utils/__tests__/git.spec.ts @@ -1,6 +1,20 @@ import { ExecException } from "child_process" - -import { searchCommits, getCommitInfo, getWorkingState } from "../git" +import * as vscode from "vscode" +import * as fs from "fs" +import * as path from "path" + +import { + searchCommits, + getCommitInfo, + getWorkingState, + getGitRepositoryInfo, + sanitizeGitUrl, + extractRepositoryName, + getWorkspaceGitInfo, + GitRepositoryInfo, + convertGitUrlToHttps, +} from "../git" +import { truncateOutput } from "../../integrations/misc/extract-text" type ExecFunction = ( command: string, @@ -15,6 +29,24 @@ 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 => { @@ -169,7 +201,6 @@ describe("git utils", () => { if (command === cmd) { callback(null, response) return {} as any - return {} as any } } callback(new Error("Unexpected command")) @@ -217,7 +248,6 @@ describe("git utils", () => { if (command.startsWith(cmd)) { callback(null, response) return {} as any - return {} as any } } callback(new Error("Unexpected command")) @@ -229,6 +259,7 @@ 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 () => { @@ -297,6 +328,7 @@ 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 () => { @@ -311,7 +343,6 @@ describe("git utils", () => { if (command === cmd) { callback(null, response) return {} as any - return {} as any } } callback(new Error("Unexpected command")) @@ -361,3 +392,398 @@ 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 new file mode 100644 index 0000000000..f3b687595a --- /dev/null +++ b/src/utils/__tests__/safeWriteJson.test.ts @@ -0,0 +1,480 @@ +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/autoImportSettings.ts b/src/utils/autoImportSettings.ts new file mode 100644 index 0000000000..f3ab4080d7 --- /dev/null +++ b/src/utils/autoImportSettings.ts @@ -0,0 +1,84 @@ +import * as vscode from "vscode" +import * as path from "path" +import * as os from "os" + +import { Package } from "../shared/package" +import { fileExistsAtPath } from "./fs" +import { t } from "../i18n" + +import { importSettingsFromPath, ImportOptions } from "../core/config/importExport" + +/** + * Automatically imports RooCode settings from a specified path if it exists. + * This function is called during extension activation to allow users to pre-configure + * their settings by placing a settings file at a predefined location. + */ +export async function autoImportSettings( + outputChannel: vscode.OutputChannel, + { providerSettingsManager, contextProxy, customModesManager }: ImportOptions, +): Promise { + try { + // Get the auto-import settings path from VSCode settings + const settingsPath = vscode.workspace.getConfiguration(Package.name).get("autoImportSettingsPath") + + if (!settingsPath || settingsPath.trim() === "") { + outputChannel.appendLine("[AutoImport] No auto-import settings path specified, skipping auto-import") + return + } + + // Resolve the path (handle ~ for home directory and relative paths) + const resolvedPath = resolvePath(settingsPath.trim()) + outputChannel.appendLine(`[AutoImport] Checking for settings file at: ${resolvedPath}`) + + // Check if the file exists + if (!(await fileExistsAtPath(resolvedPath))) { + outputChannel.appendLine(`[AutoImport] Settings file not found at ${resolvedPath}, skipping auto-import`) + return + } + + // Attempt to import the configuration + const result = await importSettingsFromPath(resolvedPath, { + providerSettingsManager, + contextProxy, + customModesManager, + }) + + if (result.success) { + outputChannel.appendLine(`[AutoImport] Successfully imported settings from ${resolvedPath}`) + + // Show a notification to the user + vscode.window.showInformationMessage( + t("common:info.auto_import_success", { filename: path.basename(resolvedPath) }), + ) + } else { + outputChannel.appendLine(`[AutoImport] Failed to import settings: ${result.error}`) + + // Show a warning but don't fail the extension activation + vscode.window.showWarningMessage(t("common:warnings.auto_import_failed", { error: result.error })) + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + outputChannel.appendLine(`[AutoImport] Unexpected error during auto-import: ${errorMessage}`) + + // Log error but don't fail extension activation + console.warn("Auto-import settings error:", error) + } +} + +/** + * Resolves a file path, handling home directory expansion and relative paths + */ +function resolvePath(settingsPath: string): string { + // Handle home directory expansion + if (settingsPath.startsWith("~/")) { + return path.join(os.homedir(), settingsPath.slice(2)) + } + + // Handle absolute paths + if (path.isAbsolute(settingsPath)) { + return settingsPath + } + + // Handle relative paths (relative to home directory for safety) + return path.join(os.homedir(), settingsPath) +} diff --git a/src/utils/git.ts b/src/utils/git.ts index 640af7fd29..fd6abfa309 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,3 +1,6 @@ +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" @@ -5,6 +8,12 @@ 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 @@ -13,6 +22,185 @@ 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 }) diff --git a/src/utils/migrateSettings.ts b/src/utils/migrateSettings.ts index 406e5bd051..43b1d7291f 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 - const yamlContent = yaml.stringify(customModesData) + // Convert to YAML with no line width limit to prevent line breaks + const yamlContent = yaml.stringify(customModesData, { lineWidth: 0 }) // Write YAML file await fs.writeFile(newYamlPath, yamlContent, "utf-8") diff --git a/src/utils/safeWriteJson.ts b/src/utils/safeWriteJson.ts new file mode 100644 index 0000000000..719bbd7216 --- /dev/null +++ b/src/utils/safeWriteJson.ts @@ -0,0 +1,235 @@ +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/package.json b/webview-ui/package.json index 4d5e5cc821..4c6edc7a2b 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -42,6 +42,7 @@ "debounce": "^2.1.1", "fast-deep-equal": "^3.1.3", "fzf": "^0.5.2", + "hast-util-to-jsx-runtime": "^2.3.6", "i18next": "^25.0.0", "i18next-http-backend": "^3.0.2", "katex": "^0.16.11", diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 6b33a9b7f7..332ef18511 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -20,6 +20,8 @@ import ModesView from "./components/modes/ModesView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" 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" @@ -215,7 +217,9 @@ const AppWithProviders = () => ( - + + + diff --git a/webview-ui/src/__tests__/App.spec.tsx b/webview-ui/src/__tests__/App.spec.tsx index d4e8b26818..2c55d1cf07 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 "@testing-library/react" +import { render, screen, act, cleanup } from "@/utils/test-utils" import AppWithProviders from "../App" diff --git a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx index 5a5ff463ef..0e5d0d6193 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import TaskHeader from "@src/components/chat/TaskHeader" @@ -51,7 +51,6 @@ 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(), @@ -103,18 +102,22 @@ describe("ContextWindowProgress", () => { it("calculates percentages correctly", () => { renderComponent({ contextTokens: 1000, contextWindow: 4000 }) - // 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() + // 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") - // Just verify that the element has a title attribute (the actual text is translated and may vary) - expect(tokenUsageDiv).toHaveAttribute("title") + expect(tokenCount).toBeInTheDocument() + expect(tokenCount).toHaveTextContent("1000") - // 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") + 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() }) }) diff --git a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx index 53b7c0a4ce..d6fd3013e6 100644 --- a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx +++ b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import { describe, it, expect, vi } from "vitest" import { AccountView } from "../AccountView" diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index ac02d6b8c4..8d838f0a18 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -25,6 +25,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { alwaysAllowModeSwitch, alwaysAllowSubtasks, alwaysApproveResubmit, + alwaysAllowFollowupQuestions, allowedMaxRequests, setAlwaysAllowReadOnly, setAlwaysAllowWrite, @@ -34,6 +35,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowModeSwitch, setAlwaysAllowSubtasks, setAlwaysApproveResubmit, + setAlwaysAllowFollowupQuestions, setAllowedMaxRequests, } = useExtensionState() @@ -68,6 +70,9 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { case "alwaysApproveResubmit": setAlwaysApproveResubmit(value) break + case "alwaysAllowFollowupQuestions": + setAlwaysAllowFollowupQuestions(value) + break } }, [ @@ -79,6 +84,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowModeSwitch, setAlwaysAllowSubtasks, setAlwaysApproveResubmit, + setAlwaysAllowFollowupQuestions, ], ) @@ -94,6 +100,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { alwaysAllowModeSwitch: alwaysAllowModeSwitch, alwaysAllowSubtasks: alwaysAllowSubtasks, alwaysApproveResubmit: alwaysApproveResubmit, + alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions, }), [ alwaysAllowReadOnly, @@ -104,6 +111,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { alwaysAllowModeSwitch, alwaysAllowSubtasks, alwaysApproveResubmit, + alwaysAllowFollowupQuestions, ], ) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 43824c5902..f61eb8c5e0 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -10,6 +10,7 @@ import type { ClineMessage } from "@roo-code/types" import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage" import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences" import { safeJsonParse } from "@roo/safeJsonParse" +import { FollowUpData, SuggestionItem } from "@roo-code/types" import { useCopyToClipboard } from "@src/utils/clipboard" import { useExtensionState } from "@src/context/ExtensionStateContext" @@ -48,8 +49,9 @@ interface ChatRowProps { isStreaming: boolean onToggleExpand: (ts: number) => void onHeightChange: (isTaller: boolean) => void - onSuggestionClick?: (answer: string, event?: React.MouseEvent) => void + onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void onBatchFileResponse?: (response: { [key: string]: boolean }) => void + onFollowUpUnmount?: () => void } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -98,6 +100,7 @@ export const ChatRowContent = ({ isStreaming, onToggleExpand, onSuggestionClick, + onFollowUpUnmount, onBatchFileResponse, }: ChatRowContentProps) => { const { t } = useTranslation() @@ -279,7 +282,7 @@ export const ChatRowContent = ({ const followUpData = useMemo(() => { if (message.type === "ask" && message.ask === "followup" && !message.partial) { - return safeJsonParse(message.text) + return safeJsonParse(message.text) } return null }, [message.type, message.ask, message.partial, message.text]) @@ -1215,6 +1218,7 @@ export const ChatRowContent = ({ suggestions={followUpData?.suggest} onSuggestionClick={onSuggestionClick} ts={message?.ts} + onUnmount={onFollowUpUnmount} /> ) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 910b671725..dc07764077 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -19,7 +19,7 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" -import { SelectDropdown, DropdownOptionType, Button } from "@/components/ui" +import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui" import Thumbnails from "../common/Thumbnails" import ModeSelector from "./ModeSelector" @@ -1094,8 +1094,7 @@ const ChatTextArea = forwardRef(
    + })}> {label}
    @@ -1106,21 +1105,25 @@ const ChatTextArea = forwardRef( })}>
    - + + +
  • ) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 77e1d88df7..874f0de3c2 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -15,6 +15,7 @@ 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" @@ -30,8 +31,10 @@ 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 { StandardTooltip } from "@src/components/ui" import TelemetryBanner from "../common/TelemetryBanner" +import VersionIndicator from "../common/VersionIndicator" import { useTaskSearch } from "../history/useTaskSearch" import HistoryPreview from "../history/HistoryPreview" import Announcement from "./Announcement" @@ -86,11 +89,13 @@ 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, @@ -730,7 +736,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { @@ -876,6 +882,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction { return () => { - if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === 'function') { - (scrollToBottomSmooth as any).cancel() + if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") { + ;(scrollToBottomSmooth as any).cancel() } } }, [scrollToBottomSmooth]) @@ -1186,18 +1197,43 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + // 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( - (answer: string, event?: React.MouseEvent) => { + (suggestion: SuggestionItem, event?: React.MouseEvent) => { + // 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) + } + } + if (event?.shiftKey) { // Always append to existing text, don't overwrite setInputValue((currentValue) => { - return currentValue !== "" ? `${currentValue} \n${answer}` : answer + return currentValue !== "" ? `${currentValue} \n${suggestion.answer}` : suggestion.answer }) } else { - handleSendMessage(answer, []) + handleSendMessage(suggestion.answer, []) } }, - [handleSendMessage, setInputValue], // setInputValue is stable, handleSendMessage depends on clineAsk + [handleSendMessage, setInputValue, switchToMode, alwaysAllowModeSwitch], ) const handleBatchFileResponse = useCallback((response: { [key: string]: boolean }) => { @@ -1205,6 +1241,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + // Clear the auto-approve timeout to prevent race conditions + if (autoApproveTimeoutRef.current) { + clearTimeout(autoApproveTimeoutRef.current) + autoApproveTimeoutRef.current = null + } + }, []) + const itemContent = useCallback( (index: number, messageOrGroup: ClineMessage | ClineMessage[]) => { // browser session group @@ -1240,6 +1285,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) }, @@ -1252,6 +1298,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { if (lastMessage?.ask && isAutoApproved(lastMessage)) { - if (lastMessage.ask === "tool" && isWriteToolAction(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) + 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(resolve, followupAutoApproveTimeoutMs) + }) + + // 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(resolve, writeDelayMs) }) } - if (autoApproveTimeoutRef.current === null || autoApproveTimeoutRef.current) { - vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }) + vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }) - setSendingDisabled(true) - setClineAsk(undefined) - setEnableButtons(false) - } + setSendingDisabled(true) + setClineAsk(undefined) + setEnableButtons(false) } } autoApprove() @@ -1300,6 +1369,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction m.slug === mode) const nextModeIndex = (currentModeIndex + 1) % allModes.length // Update local state and notify extension to sync mode change - setMode(allModes[nextModeIndex].slug) - vscode.postMessage({ - type: "mode", - text: allModes[nextModeIndex].slug, - }) - }, [mode, setMode, customModes]) + switchToMode(allModes[nextModeIndex].slug) + }, [mode, customModes, switchToMode]) // Add keyboard event handler const handleKeyDown = useCallback( @@ -1364,14 +1432,24 @@ const ChatViewComponent: React.ForwardRefRenderFunction - {showAnnouncement && } + {(showAnnouncement || showAnnouncementModal) && ( + { + if (showAnnouncementModal) { + setShowAnnouncementModal(false) + } + if (showAnnouncement) { + hideAnnouncement() + } + }} + /> + )} {task ? ( <> ) : ( -
    +
    {/* Moved Task Bar Header Here */} {tasks.length !== 0 && (
    @@ -1410,6 +1488,12 @@ 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" && } {/* Show the task history preview if expanded and tasks exist */} @@ -1477,15 +1561,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction {showScrollToBottom ? (
    -
    { - scrollToBottomSmooth() - disableAutoScrollRef.current = false - }} - title={t("chat:scrollToBottom")}> - -
    + +
    { + scrollToBottomSmooth() + disableAutoScrollRef.current = false + }}> + +
    +
    ) : (
    {primaryButtonText && !isStreaming && ( - handlePrimaryButtonClick(inputValue, selectedImages)}> - {primaryButtonText} - + }> + handlePrimaryButtonClick(inputValue, selectedImages)}> + {primaryButtonText} + + )} {(secondaryButtonText || isStreaming) && ( - handleSecondaryButtonClick(inputValue, selectedImages)}> - {isStreaming ? t("chat:cancel.title") : secondaryButtonText} - + }> + 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 d5a8e6407f..4d48749ecd 100644 --- a/webview-ui/src/components/chat/CodebaseSearchResult.tsx +++ b/webview-ui/src/components/chat/CodebaseSearchResult.tsx @@ -1,5 +1,7 @@ import React from "react" +import { useTranslation } from "react-i18next" import { vscode } from "@src/utils/vscode" +import { StandardTooltip } from "@/components/ui" interface CodebaseSearchResultProps { filePath: string @@ -11,6 +13,8 @@ interface CodebaseSearchResultProps { } const CodebaseSearchResult: React.FC = ({ filePath, score, startLine, endLine }) => { + const { t } = useTranslation("chat") + const handleClick = () => { console.log(filePath) vscode.postMessage({ @@ -23,19 +27,23 @@ const CodebaseSearchResult: React.FC = ({ filePath, s } return ( -
    -
    - - {filePath.split("/").at(-1)}:{startLine}-{endLine} - - - {filePath.split("/").slice(0, -1).join("/")} - + +
    +
    + + {filePath.split("/").at(-1)}:{startLine}-{endLine} + + + {filePath.split("/").slice(0, -1).join("/")} + + + {score.toFixed(3)} + +
    -
    + ) } diff --git a/webview-ui/src/components/chat/ContextWindowProgress.tsx b/webview-ui/src/components/chat/ContextWindowProgress.tsx index a5490d9d4f..1ae80bb3db 100644 --- a/webview-ui/src/components/chat/ContextWindowProgress.tsx +++ b/webview-ui/src/components/chat/ContextWindowProgress.tsx @@ -3,6 +3,7 @@ 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 @@ -26,64 +27,70 @@ 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)}
    -
    - {/* Invisible overlay for hover area */} -
    - - {/* Main progress bar container */} -
    - {/* Current tokens container */} -
    - {/* Invisible overlay for current tokens section */} + +
    + {/* Main progress bar container */} +
    + {/* Current tokens container */}
    - {/* Current tokens used - darkest */} -
    -
    + className="relative h-full" + style={{ width: `${currentPercent}%` }} + data-testid="context-tokens-used"> + {/* Current tokens used - darkest */} +
    +
    - {/* Container for reserved tokens */} -
    - {/* Invisible overlay for reserved section */} + {/* Container for reserved tokens */}
    - {/* Reserved for output section - medium gray */} -
    -
    + className="relative h-full" + style={{ width: `${reservedPercent}%` }} + data-testid="context-reserved-tokens"> + {/* Reserved for output section - medium gray */} +
    +
    - {/* Empty section (if any) */} - {availablePercent > 0 && ( -
    - {/* Invisible overlay for available space */} + {/* Empty section (if any) */} + {availablePercent > 0 && (
    -
    - )} + className="relative h-full" + style={{ width: `${availablePercent}%` }} + data-testid="context-available-space-section"> + {/* Available space - transparent */} +
    + )} +
    -
    +
    {formatLargeNumber(safeContextWindow)}
    diff --git a/webview-ui/src/components/chat/FollowUpSuggest.tsx b/webview-ui/src/components/chat/FollowUpSuggest.tsx index 44a30ca803..5649da744a 100644 --- a/webview-ui/src/components/chat/FollowUpSuggest.tsx +++ b/webview-ui/src/components/chat/FollowUpSuggest.tsx @@ -1,23 +1,85 @@ -import { useCallback } from "react" +import { useCallback, useEffect, useState } from "react" import { Edit } from "lucide-react" -import { Button } from "@/components/ui" +import { Button, StandardTooltip } 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?: string[] - onSuggestionClick?: (answer: string, event?: React.MouseEvent) => void + suggestions?: SuggestionItem[] + onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void ts: number + onUnmount?: () => void } -export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }: FollowUpSuggestProps) => { +export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1, onUnmount }: FollowUpSuggestProps) => { + const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState() + const [countdown, setCountdown] = useState(null) + const [suggestionSelected, setSuggestionSelected] = useState(false) 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 + if (autoApprovalEnabled && alwaysAllowFollowupQuestions && suggestions.length > 0 && !suggestionSelected) { + // 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 + onUnmount?.() + } + } else { + setCountdown(null) + } + }, [ + autoApprovalEnabled, + alwaysAllowFollowupQuestions, + suggestions, + followupAutoApproveTimeoutMs, + suggestionSelected, + onUnmount, + ]) const handleSuggestionClick = useCallback( - (suggestion: string, event: React.MouseEvent) => { + (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 + onUnmount?.() + } + + // Pass the suggestion object to the parent component + // The parent component will handle mode switching if needed onSuggestionClick?.(suggestion, event) }, - [onSuggestionClick], + [onSuggestionClick, onUnmount], ) // Don't render if there are no suggestions or no click handler. @@ -27,29 +89,47 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }: return (
    - {suggestions.map((suggestion) => ( -
    - -
    { - 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 34311b22c6..75d8bc4b0b 100644 --- a/webview-ui/src/components/chat/IconButton.tsx +++ b/webview-ui/src/components/chat/IconButton.tsx @@ -1,4 +1,5 @@ import { cn } from "@/lib/utils" +import { StandardTooltip } from "@/components/ui" interface IconButtonProps extends React.ButtonHTMLAttributes { iconClass: string @@ -35,10 +36,9 @@ export const IconButton: React.FC = ({ const iconClasses = cn("codicon", iconClass, isLoading && "codicon-modifier-spin") - return ( + const button = ( ) + + return {button} } diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index a209ce8723..ba838284d7 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -2,6 +2,7 @@ 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" @@ -34,30 +35,31 @@ 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/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index f066dabfa6..336e9f8357 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -2,13 +2,15 @@ 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 } from "@/components/ui" +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 @@ -37,6 +39,10 @@ export const ModeSelector = ({ 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 }) @@ -55,6 +61,27 @@ export const ModeSelector = ({ // Find the selected mode const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) + const trigger = ( + + + {selectedMode?.name || ""} + + ) + return ( - - - {selectedMode?.name || ""} - + {title ? {trigger} : trigger} { <> {shareButtonState.showPopover ? ( - - - + + + + + {shareSuccess ? (
    @@ -205,15 +207,16 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => { ) : ( - + + + )} {/* Connect to Cloud Modal */} diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index f7b163f720..0d75f8f250 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -10,7 +10,7 @@ import { getModelMaxOutputTokens } from "@roo/api" import { formatLargeNumber } from "@src/utils/format" import { cn } from "@src/lib/utils" -import { Button } from "@src/components/ui" +import { Button, StandardTooltip } from "@src/components/ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" @@ -25,7 +25,6 @@ export interface TaskHeaderProps { task: ClineMessage tokensIn: number tokensOut: number - doesModelSupportPromptCache: boolean cacheWrites?: number cacheReads?: number totalCost: number @@ -39,7 +38,6 @@ const TaskHeader = ({ task, tokensIn, tokensOut, - doesModelSupportPromptCache, cacheWrites, cacheReads, totalCost, @@ -60,13 +58,14 @@ const TaskHeader = ({ const { width: windowWidth } = useWindowSize() const condenseButton = ( - + + + ) return ( @@ -97,14 +96,11 @@ const TaskHeader = ({ )}
    - + + +
    {/* Collapsed state: Track context and cost if we have any */} {!isTaskExpanded && contextWindow > 0 && ( @@ -186,25 +182,24 @@ const TaskHeader = ({ {!totalCost && }
    - {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)} - - )} -
    - )} + {((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 7048022440..42c2d6ff50 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" 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 7aef88d0de..6b2a290c63 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 e31803b99e..63112d12c0 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 "@testing-library/react" +import { render, fireEvent, screen } from "@/utils/test-utils" import { defaultModeSlug } from "@roo/modes" @@ -761,7 +761,7 @@ describe("ChatTextArea", () => { describe("selectApiConfig", () => { // Helper function to get the API config dropdown const getApiConfigDropdown = () => { - return screen.getByTitle("chat:selectApiConfig") + return screen.getByTestId("dropdown-trigger") } 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 3e819904fe..15405396f7 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 "@testing-library/react" +import { render, waitFor } from "@/utils/test-utils" 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 f6ecbaf0e4..0343cb6c96 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 "@testing-library/react" +import { render, waitFor, act } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext" @@ -61,6 +61,49 @@ vi.mock("../AutoApproveMenu", () => ({ default: () => null, })) +// Mock VersionIndicator - returns null by default to prevent rendering in tests +vi.mock("../../common/VersionIndicator", () => ({ + default: vi.fn(() => null), +})) + +// Get the mock function after the module is mocked +const mockVersionIndicator = vi.mocked( + // @ts-expect-error - accessing mocked module + (await import("../../common/VersionIndicator")).default, +) + +vi.mock("@src/components/modals/Announcement", () => ({ + default: function MockAnnouncement({ hideAnnouncement }: { hideAnnouncement: () => void }) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const React = require("react") + return React.createElement( + "div", + { "data-testid": "announcement-modal" }, + React.createElement("div", null, "What's New"), + React.createElement("button", { onClick: hideAnnouncement }, "Close"), + ) + }, +})) + +// Mock i18n +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, options?: any) => { + if (key === "chat:versionIndicator.ariaLabel" && options?.version) { + return `Version ${options.version}` + } + return key + }, + }), + initReactI18next: { + type: "3rdParty", + init: () => {}, + }, + Trans: ({ i18nKey, children }: { i18nKey: string; children?: React.ReactNode }) => { + return <>{children || i18nKey} + }, +})) + interface ChatTextAreaProps { onSend: (value: string) => void inputValue?: string @@ -1068,3 +1111,170 @@ describe("ChatView - Focus Grabbing Tests", () => { expect(mockFocus).toHaveBeenCalledTimes(FOCUS_CALLS_ON_INIT) }) }) + +describe("ChatView - Version Indicator Tests", () => { + beforeEach(() => vi.clearAllMocks()) + + // Helper function to create a mock VersionIndicator implementation + const createMockVersionIndicator = ( + ariaLabel: string = "chat:versionIndicator.ariaLabel", + version: string = "v3.21.5", + ) => { + return (props?: { onClick?: () => void; className?: string }) => { + const { onClick, className } = props || {} + return ( + + ) + } + } + + it("displays version indicator button", () => { + // Temporarily override the mock for this test + mockVersionIndicator.mockImplementation(createMockVersionIndicator()) + + const { getByLabelText } = renderChatView() + + // First hydrate state + mockPostMessage({ + clineMessages: [], + }) + + // Check that version indicator is displayed + const versionButton = getByLabelText(/version/i) + expect(versionButton).toBeInTheDocument() + expect(versionButton).toHaveTextContent(/^v\d+\.\d+\.\d+/) + + // Reset mock + mockVersionIndicator.mockReturnValue(null) + }) + + it("opens announcement modal when version indicator is clicked", () => { + // Temporarily override the mock for this test + mockVersionIndicator.mockImplementation(createMockVersionIndicator("Version 3.22.5", "v3.22.5")) + + const { getByTestId } = renderChatView() + + // First hydrate state + mockPostMessage({ + clineMessages: [], + }) + + // Find version indicator + const versionButton = getByTestId("version-indicator") + expect(versionButton).toBeInTheDocument() + + // Click should trigger modal - we'll just verify the button exists and is clickable + // The actual modal rendering is handled by the component state + expect(versionButton.onclick).toBeDefined() + + // Reset mock + mockVersionIndicator.mockReturnValue(null) + }) + + it("version indicator has correct styling classes", () => { + // Temporarily override the mock for this test + mockVersionIndicator.mockImplementation(createMockVersionIndicator("Version 3.22.5", "v3.22.5")) + + const { getByTestId } = renderChatView() + + // First hydrate state + mockPostMessage({ + clineMessages: [], + }) + + // Check styling classes - the VersionIndicator component receives className prop + const versionButton = getByTestId("version-indicator") + expect(versionButton).toBeInTheDocument() + // The className is passed as a prop to VersionIndicator + expect(versionButton.className).toContain("absolute top-2 right-3 z-10") + + // Reset mock + mockVersionIndicator.mockReturnValue(null) + }) + + it("version indicator has proper accessibility attributes", () => { + // Temporarily override the mock for this test + mockVersionIndicator.mockImplementation(createMockVersionIndicator("Version 3.22.5", "v3.22.5")) + + const { getByTestId } = renderChatView() + + // First hydrate state + mockPostMessage({ + clineMessages: [], + }) + + // Check accessibility + const versionButton = getByTestId("version-indicator") + expect(versionButton).toBeInTheDocument() + expect(versionButton).toHaveAttribute("aria-label", "Version 3.22.5") + + // Reset mock + mockVersionIndicator.mockReturnValue(null) + }) + + it("does not display version indicator when there is an active task", () => { + const { queryByTestId } = renderChatView() + + // Hydrate state with an active task - any message in the array makes task truthy + mockPostMessage({ + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now(), + text: "Active task in progress", + }, + ], + }) + + // Version indicator should not be present during task execution + const versionButton = queryByTestId("version-indicator") + expect(versionButton).not.toBeInTheDocument() + }) + + it("displays version indicator only on welcome screen (no task)", () => { + // Temporarily override the mock for this test + mockVersionIndicator.mockImplementation(createMockVersionIndicator("Version 3.22.5", "v3.22.5")) + + const { queryByTestId, rerender } = renderChatView() + + // First, hydrate with no messages (welcome screen) + mockPostMessage({ + clineMessages: [], + }) + + // Version indicator should be present + let versionButton = queryByTestId("version-indicator") + expect(versionButton).toBeInTheDocument() + + // Reset mock to return null for the second part of the test + mockVersionIndicator.mockReturnValue(null) + + // Now add a task - any message makes task truthy + mockPostMessage({ + clineMessages: [ + { + type: "say", + say: "task", + ts: Date.now(), + text: "Starting a new task", + }, + ], + }) + + // Force a re-render to ensure the component updates + rerender( + + + + + , + ) + + // Version indicator should disappear + versionButton = queryByTestId("version-indicator") + expect(versionButton).not.toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx index e75b16606a..4ef3764841 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 "@testing-library/react" +import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils" 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 index 4d07a6de46..d6fc81368d 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import { describe, test, expect, vi } from "vitest" import ModeSelector from "../ModeSelector" import { Mode } from "@roo/modes" diff --git a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx index f6102dc616..cbe5620264 100644 --- a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx @@ -1,5 +1,5 @@ import { describe, test, expect, vi, beforeEach } from "vitest" -import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { ShareButton } from "../ShareButton" import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" diff --git a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx index c21c5b9331..2d9ea0cac1 100644 --- a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { vi, describe, it, expect, beforeEach } from "vitest" import { TaskActions } from "../TaskActions" import type { HistoryItem } from "@roo-code/types" @@ -89,15 +89,19 @@ describe("TaskActions", () => { it("renders share button when item has id", () => { render() - const shareButton = screen.getByTitle("Share task") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) expect(shareButton).toBeInTheDocument() }) it("does not render share button when item has no id", () => { render() - const shareButton = screen.queryByTitle("Share task") - expect(shareButton).not.toBeInTheDocument() + // Find button by its icon class + const buttons = screen.queryAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).not.toBeDefined() }) it("renders share button even when not authenticated", () => { @@ -108,7 +112,9 @@ describe("TaskActions", () => { render() - const shareButton = screen.getByTitle("Share task") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) expect(shareButton).toBeInTheDocument() }) }) @@ -117,8 +123,11 @@ describe("TaskActions", () => { it("shows organization and public share options when authenticated and sharing enabled", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.getByText("Share with Organization")).toBeInTheDocument() expect(screen.getByText("Share Publicly")).toBeInTheDocument() @@ -127,8 +136,11 @@ describe("TaskActions", () => { it("sends shareCurrentTask message when organization option is selected", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) const orgOption = screen.getByText("Share with Organization") fireEvent.click(orgOption) @@ -142,8 +154,11 @@ describe("TaskActions", () => { it("sends shareCurrentTask message when public option is selected", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) const publicOption = screen.getByText("Share Publicly") fireEvent.click(publicOption) @@ -165,8 +180,11 @@ describe("TaskActions", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() expect(screen.getByText("Share Publicly")).toBeInTheDocument() @@ -184,8 +202,11 @@ describe("TaskActions", () => { it("shows connect to cloud option when not authenticated", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.getByText("Connect to Roo Code Cloud")).toBeInTheDocument() expect(screen.getByText("Sign in to Roo Code Cloud to share tasks")).toBeInTheDocument() @@ -195,8 +216,11 @@ describe("TaskActions", () => { it("does not show organization and public options when not authenticated", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() expect(screen.queryByText("Share Publicly")).not.toBeInTheDocument() @@ -205,8 +229,11 @@ describe("TaskActions", () => { it("sends rooCloudSignIn message when connect to cloud is selected", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) const connectOption = screen.getByText("Connect") fireEvent.click(connectOption) @@ -226,12 +253,14 @@ describe("TaskActions", () => { render() - const shareButton = screen.getByTitle("Sharing disabled by organization") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) expect(shareButton).toBeInTheDocument() expect(shareButton).toBeDisabled() // Should not have a popover when sharing is disabled - fireEvent.click(shareButton) + fireEvent.click(shareButton!) expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() expect(screen.queryByText("Connect to Cloud")).not.toBeInTheDocument() }) @@ -269,14 +298,14 @@ describe("TaskActions", () => { it("renders export button", () => { render() - const exportButton = screen.getByTitle("Export task history") + const exportButton = screen.getByLabelText("Export task history") expect(exportButton).toBeInTheDocument() }) it("sends exportCurrentTask message when export button is clicked", () => { render() - const exportButton = screen.getByTitle("Export task history") + const exportButton = screen.getByLabelText("Export task history") fireEvent.click(exportButton) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -287,7 +316,7 @@ describe("TaskActions", () => { it("renders delete button and file size when item has size", () => { render() - const deleteButton = screen.getByTitle("Delete Task (Shift + Click to skip confirmation)") + const deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)") expect(deleteButton).toBeInTheDocument() expect(screen.getByText("1024 B")).toBeInTheDocument() }) @@ -296,7 +325,7 @@ describe("TaskActions", () => { const itemWithoutSize = { ...mockItem, size: 0 } render() - const deleteButton = screen.queryByTitle("Delete Task (Shift + Click to skip confirmation)") + const deleteButton = screen.queryByLabelText("Delete Task (Shift + Click to skip confirmation)") expect(deleteButton).not.toBeInTheDocument() }) }) @@ -305,8 +334,10 @@ describe("TaskActions", () => { it("disables buttons when buttonsDisabled is true", () => { render() - const shareButton = screen.getByTitle("Share task") - const exportButton = screen.getByTitle("Export task history") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + const exportButton = screen.getByLabelText("Export task history") expect(shareButton).toBeDisabled() expect(exportButton).toBeDisabled() diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 784a263531..c04f7e45e5 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import type { ProviderSettings } from "@roo-code/types" @@ -49,7 +49,6 @@ 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, @@ -94,22 +93,33 @@ describe("TaskHeader", () => { it("should render the condense context button", () => { renderTaskHeader() - expect(screen.getByTitle("chat:task.condenseContext")).toBeInTheDocument() + // 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() }) it("should call handleCondenseContext when condense context button is clicked", () => { const handleCondenseContext = vi.fn() renderTaskHeader({ handleCondenseContext }) - const condenseButton = screen.getByTitle("chat:task.condenseContext") - fireEvent.click(condenseButton) + // 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!) 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 }) - const condenseButton = screen.getByTitle("chat:task.condenseContext") - fireEvent.click(condenseButton) + // 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!) 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 348e230619..21b4f486c7 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 } from "@/components/ui" +import { Button, Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" import { useRooPortal } from "@/components/ui/hooks" import { vscode } from "@src/utils/vscode" @@ -48,13 +48,11 @@ 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 06f929b830..28492acd8b 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -4,8 +4,11 @@ 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 @@ -225,7 +228,7 @@ const CodeBlock = memo( const [windowShade, setWindowShade] = useState(initialWindowShade) const [currentLanguage, setCurrentLanguage] = useState(() => normalizeLanguage(language)) const userChangedLanguageRef = useRef(false) - const [highlightedCode, setHighlightedCode] = useState("") + const [highlightedCode, setHighlightedCode] = useState(null) const [showCollapseButton, setShowCollapseButton] = useState(true) const codeBlockRef = useRef(null) const preRef = useRef(null) @@ -252,7 +255,12 @@ const CodeBlock = memo( // Set mounted state at the beginning of this effect isMountedRef.current = true - const fallback = `
    ${source || ""}
    ` + // Create a safe fallback using React elements instead of HTML string + const fallback = ( +
    +					{source || ""}
    +				
    + ) const highlight = async () => { // Show plain text if language needs to be loaded. @@ -265,7 +273,7 @@ const CodeBlock = memo( const highlighter = await getHighlighter(currentLanguage) if (!isMountedRef.current) return - const html = await highlighter.codeToHtml(source || "", { + const hast = await highlighter.codeToHast(source || "", { lang: currentLanguage || "txt", theme: document.body.className.toLowerCase().includes("light") ? "github-light" : "github-dark", transformers: [ @@ -289,8 +297,25 @@ const CodeBlock = memo( }) if (!isMountedRef.current) return - if (isMountedRef.current) { - setHighlightedCode(html) + // 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) + } } } @@ -725,48 +750,55 @@ const CodeBlock = memo( } {showCollapseButton && ( - { - // Get the current code block element - const codeBlock = codeBlockRef.current // Capture ref early - // Toggle window shade state - setWindowShade(!windowShade) - - // Clear any previous timeouts - if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current) - if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current) - - // 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" }) - - // 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, - ) - }} - title={t(`chat:codeblock.tooltips.${windowShade ? "expand" : "collapse"}`)}> - {windowShade ? : } - + + { + // Get the current code block element + const codeBlock = codeBlockRef.current // Capture ref early + // Toggle window shade state + setWindowShade(!windowShade) + + // Clear any previous timeouts + if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current) + if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current) + + // 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" }) + + // 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 ? : } + + )} - setWordWrap(!wordWrap)} - title={t(`chat:codeblock.tooltips.${wordWrap ? "disable_wrap" : "enable_wrap"}`)}> - {wordWrap ? : } - - - {showCopyFeedback ? : } - + + setWordWrap(!wordWrap)}> + {wordWrap ? : } + + + + + {showCopyFeedback ? : } + + )} @@ -775,7 +807,7 @@ const CodeBlock = memo( ) // Memoized content component to prevent unnecessary re-renders of highlighted code -const MemoizedCodeContent = memo(({ html }: { html: string }) =>
    ) +const MemoizedCodeContent = memo(({ children }: { children: React.ReactNode }) => <>{children}) // Memoized StyledPre component const MemoizedStyledPre = memo( @@ -793,7 +825,7 @@ const MemoizedStyledPre = memo( wordWrap: boolean windowShade: boolean collapsedHeight?: number - highlightedCode: string + highlightedCode: React.ReactNode 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 70a66ba9f1..ac8b7ca1e2 100644 --- a/webview-ui/src/components/common/IconButton.tsx +++ b/webview-ui/src/components/common/IconButton.tsx @@ -1,3 +1,5 @@ +import { StandardTooltip } from "@/components/ui" + interface IconButtonProps { icon: string onClick?: (e: React.MouseEvent) => void @@ -31,15 +33,21 @@ export function IconButton({ const handleClick = onClick || ((_event: React.MouseEvent) => {}) - return ( + const button = ( ) + + 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 46ded57644..79558b9b03 100644 --- a/webview-ui/src/components/common/MermaidActionButtons.tsx +++ b/webview-ui/src/components/common/MermaidActionButtons.tsx @@ -2,6 +2,7 @@ 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 @@ -40,41 +41,51 @@ 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")} - /> - - - ) - } - - return ( - <> - {onZoom && } - { - e.stopPropagation() - onViewCode() - }} - title={t("common:mermaid.buttons.viewCode")} - /> - - {onSave && } - {onClose && } + + + + + {onSave && ( + + + + )} + {onClose && ( + + + + )} ) } diff --git a/webview-ui/src/components/common/MermaidBlock.tsx b/webview-ui/src/components/common/MermaidBlock.tsx index 229b957765..95c795fdc5 100644 --- a/webview-ui/src/components/common/MermaidBlock.tsx +++ b/webview-ui/src/components/common/MermaidBlock.tsx @@ -46,6 +46,7 @@ mermaid.initialize({ startOnLoad: false, securityLevel: "loose", theme: "dark", + suppressErrorRendering: true, themeVariables: { ...MERMAID_THEME, fontSize: "16px", diff --git a/webview-ui/src/components/common/MermaidButton.tsx b/webview-ui/src/components/common/MermaidButton.tsx index 57d4c26b0a..8d77502c6d 100644 --- a/webview-ui/src/components/common/MermaidButton.tsx +++ b/webview-ui/src/components/common/MermaidButton.tsx @@ -7,6 +7,7 @@ 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 @@ -126,7 +127,7 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
    {children} {!isLoading && isHovering && ( -
    +
    - setShowModal(false)} - title={t("common:mermaid.buttons.close")} - /> + + setShowModal(false)} /> +
    + + + + + + + + ) : ( + { + e.stopPropagation() + copyWithFeedback(code, e) + }} /> - - - ) : ( - { - e.stopPropagation() - copyWithFeedback(code, e) - }} - title={t("common:mermaid.buttons.copy")} - /> + )}
    diff --git a/webview-ui/src/components/common/VersionIndicator.tsx b/webview-ui/src/components/common/VersionIndicator.tsx new file mode 100644 index 0000000000..b0c517fdaf --- /dev/null +++ b/webview-ui/src/components/common/VersionIndicator.tsx @@ -0,0 +1,23 @@ +import React from "react" +import { useTranslation } from "react-i18next" +import { Package } from "@roo/package" + +interface VersionIndicatorProps { + onClick: () => void + className?: string +} + +const VersionIndicator: React.FC = ({ onClick, className = "" }) => { + const { t } = useTranslation() + + return ( + + ) +} + +export default VersionIndicator diff --git a/webview-ui/src/components/common/ZoomControls.tsx b/webview-ui/src/components/common/ZoomControls.tsx index 47093ec59f..427a17c7fd 100644 --- a/webview-ui/src/components/common/ZoomControls.tsx +++ b/webview-ui/src/components/common/ZoomControls.tsx @@ -1,5 +1,6 @@ import { IconButton } from "./IconButton" import { useRef, useEffect } from "react" +import { StandardTooltip } from "@/components/ui" interface ZoomControlsProps { zoomLevel: number @@ -67,25 +68,27 @@ export function ZoomControls({ return (
    - 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 a1d3849744..f413745b61 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 "@testing-library/react" +import { render, screen, fireEvent, act } from "@/utils/test-utils" import CodeBlock from "../CodeBlock" @@ -37,6 +37,43 @@ vi.mock("../../../utils/highlighter", () => { const theme = options.theme === "github-light" ? "light" : "dark" return `
    ${code} [${theme}-theme]
    ` }), + codeToHast: vi.fn().mockImplementation((code, options) => { + const theme = options.theme === "github-light" ? "light" : "dark" + // Return a comprehensive HAST node structure that matches Shiki's output + // Apply transformers if provided + const preNode = { + type: "element", + tagName: "pre", + properties: {}, + children: [ + { + type: "element", + tagName: "code", + properties: { className: [`hljs`, `language-${options.lang}`] }, + children: [ + { + type: "text", + value: `${code} [${theme}-theme]`, + }, + ], + }, + ], + } + + // Apply transformers if they exist + if (options.transformers) { + for (const transformer of options.transformers) { + if (transformer.pre) { + transformer.pre(preNode) + } + if (transformer.code && preNode.children[0]) { + transformer.code(preNode.children[0]) + } + } + } + + return preNode + }), } return { @@ -170,9 +207,15 @@ describe("CodeBlock", () => { codeBlock.setAttribute("data-partially-visible", "true") } - const copyButton = screen.getByTitle("Copy code") - await act(async () => { - fireEvent.click(copyButton) - }) + // 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) + }) + } }) }) diff --git a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx index 5190f7fe82..ec97e4e667 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" 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 743b150aae..4243ff8d5a 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 } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@/lib/utils" @@ -25,14 +25,15 @@ 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 b91f13bd50..3e99027546 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 } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { vscode } from "@/utils/vscode" @@ -25,14 +25,15 @@ 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 eeba0ccaf4..fabc8d3d15 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 } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { useCallback } from "react" @@ -15,14 +15,15 @@ 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 2d6ee5fa3d..2f156d0418 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -5,7 +5,16 @@ import { Virtuoso } from "react-virtuoso" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { Button, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui" +import { + Button, + Checkbox, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + StandardTooltip, +} from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { Tab, TabContent, TabHeader } from "../common/Tab" @@ -75,20 +84,22 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {

    {t("history:history")}

    - + +
    diff --git a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx index 9fe49663d1..bdcff23cdd 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 0ba1b27a1f..ac1b39859d 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 42c17f5335..19b333ab44 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 e78101f37d..f8e244e9bf 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 68f4407400..1dda83305c 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 179c51b1c0..6118509702 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" 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 030c36f503..3079844aad 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 9fcc11e572..9d4a939a1e 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 f1390b7d55..661cecf122 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" 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 02f554d697..090bf2521f 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" 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 e047a81cf3..bea79814fa 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 "@testing-library/react" +import { renderHook, act } from "@/utils/test-utils" 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 b469e886ea..b47e1aa875 100644 --- a/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -81,7 +81,7 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace const filteredTags = useMemo(() => allTags, [allTags]) return ( - +
    diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx index 8e62af73c9..06078d638c 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" 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 10290b70b3..95b2dea54b 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import userEvent from "@testing-library/user-event" import { MarketplaceView } from "../MarketplaceView" diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx index b59291a35b..defc138581 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx @@ -7,7 +7,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext" import { isValidUrl } from "../../../utils/url" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { StandardTooltip } from "@/components/ui" import { MarketplaceInstallModal } from "./MarketplaceInstallModal" import { useExtensionState } from "@/context/ExtensionStateContext" @@ -79,37 +79,33 @@ export const MarketplaceItemCard: React.FC = ({ item,
    {isInstalled ? ( /* Single Remove button when installed */ - - - - - - - - {isInstalledInProject + - + : t("marketplace:items.card.removeGlobalTooltip") + }> + + ) : ( /* Single Install button when not installed */ + + ))}
    )} 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 8ffb15abd4..29af469f80 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 "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" 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 6bc9453cd9..e586fbb9e1 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 "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" 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 bd88de7229..1f1ed9030b 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 "@testing-library/react" +import { render, screen } from "@/utils/test-utils" 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 892304d1c6..58b938f9f1 100644 --- a/webview-ui/src/components/mcp/McpToolRow.tsx +++ b/webview-ui/src/components/mcp/McpToolRow.tsx @@ -4,6 +4,7 @@ import { McpTool } from "@roo/mcp" import { useAppTranslation } from "@src/i18n/TranslationContext" import { vscode } from "@src/utils/vscode" +import { StandardTooltip } from "@/components/ui" type McpToolRowProps = { tool: McpTool @@ -46,9 +47,9 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo {/* Tool name section */}
    - - {tool.name} - + + {tool.name} +
    {/* Controls section */} @@ -69,24 +70,25 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo {/* Enabled eye button - only show in settings context */} {!isInChatContext && ( - + + + )}
    )} diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx index a3ccd6a990..2bfe3b1338 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 "@testing-library/react" +import { render, fireEvent, screen } from "@/utils/test-utils" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 42069de8f8..2da6669012 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -8,7 +8,7 @@ import { VSCodeTextField, } from "@vscode/webview-ui-toolkit/react" import { Trans } from "react-i18next" -import { ChevronDown, X } from "lucide-react" +import { ChevronDown, X, Upload, Download } from "lucide-react" import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types" @@ -45,6 +45,7 @@ import { CommandItem, CommandGroup, Input, + StandardTooltip, } from "@src/components/ui" // Get all available groups that should show in prompts view @@ -91,6 +92,10 @@ 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>({}) // State for mode selection popover and search const [open, setOpen] = useState(false) @@ -189,6 +194,22 @@ 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]) + // Helper function to safely access mode properties const getModeProperty = ( mode: ModeConfig | undefined, @@ -396,6 +417,28 @@ 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, + })) } } @@ -431,30 +474,29 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    e.stopPropagation()} className="flex justify-between items-center mb-3">

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

    - -
    - + +
    + + + {showConfigMenu && (
    e.stopPropagation()} @@ -652,18 +694,19 @@ const ModesView = ({ onDone }: ModesViewProps) => { }} className="w-full" /> - + + +
    @@ -674,19 +717,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    {t("prompts:roleDefinition.title")}
    {!findModeBySlug(visualMode, customModes) && ( - + + + )}
    @@ -733,19 +777,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    {t("prompts:description.title")}
    {!findModeBySlug(visualMode, customModes) && ( - + + + )}
    @@ -786,19 +831,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    {t("prompts:whenToUse.title")}
    {!findModeBySlug(visualMode, customModes) && ( - + + + )}
    @@ -843,18 +889,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    {t("prompts:tools.title")}
    {findModeBySlug(visualMode, customModes) && ( - + + )}
    {!findModeBySlug(visualMode, customModes) && ( @@ -936,19 +984,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    {t("prompts:customInstructions.title")}
    {!findModeBySlug(visualMode, customModes) && ( - + + + )}
    @@ -1026,7 +1075,7 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    -
    +
    + + + +
    + + {/* Export/Import Mode Buttons */} +
    + {/* Export button - visible when any mode is selected */} + {getCurrentMode() && ( + + )} + {/* Import button - always visible */}
    - {/* Custom System Prompt Disclosure */} + {/* Advanced Features Disclosure */}
    {isSystemPromptDisclosureOpen && ( -
    - { - const currentMode = getCurrentMode() - if (!currentMode) return - - vscode.postMessage({ - type: "openFile", - text: `./.roo/system-prompt-${currentMode.slug}`, - values: { - create: true, - content: "", - }, - }) - }} - /> - ), - "1": ( - - ), - "2": , - }} - /> +
    + {/* Override System Prompt Section */} +
    +

    + Override System Prompt +

    +
    + { + const currentMode = getCurrentMode() + if (!currentMode) return + + vscode.postMessage({ + type: "openFile", + text: `./.roo/system-prompt-${currentMode.slug}`, + values: { + create: true, + content: "", + }, + }) + }} + /> + ), + "1": ( + + ), + "2": , + }} + /> +
    +
    )}
    @@ -1386,6 +1479,68 @@ const ModesView = ({ onDone }: ModesViewProps) => {
    )} + + {/* Import Mode Dialog */} + {showImportDialog && ( +
    +
    +

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

    +

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

    +
    + + +
    +
    + + +
    +
    +
    + )} ) } diff --git a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx index 4ff7f4cf87..e202114bbb 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 "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import ModesView from "../ModesView" import { ExtensionStateContext } from "@src/context/ExtensionStateContext" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index d0a0a6aa44..edd0a5fe07 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -21,6 +21,7 @@ import { Popover, PopoverContent, PopoverTrigger, + StandardTooltip, } from "@/components/ui" interface ApiConfigManagerProps { @@ -248,23 +249,25 @@ const ApiConfigManager = ({ }} className="grow" /> - - + + + + + +
    {error && (
    @@ -334,13 +337,17 @@ const ApiConfigManager = ({ className={!valid ? "text-vscode-errorForeground" : ""}>
    {!valid && ( - - - + + + + + )} {config.name}
    @@ -360,37 +367,37 @@ const ApiConfigManager = ({ - + + + {currentApiConfigName && ( <> - - + + - - + }> + + )}
    diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index e825ab8d7c..1f9bb282a8 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -25,6 +25,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { alwaysAllowModeSwitch?: boolean alwaysAllowSubtasks?: boolean alwaysAllowExecute?: boolean + alwaysAllowFollowupQuestions?: boolean + followupAutoApproveTimeoutMs?: number allowedCommands?: string[] setCachedStateField: SetCachedStateField< | "alwaysAllowReadOnly" @@ -40,6 +42,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "alwaysAllowModeSwitch" | "alwaysAllowSubtasks" | "alwaysAllowExecute" + | "alwaysAllowFollowupQuestions" + | "followupAutoApproveTimeoutMs" | "allowedCommands" > } @@ -58,6 +62,8 @@ export const AutoApproveSettings = ({ alwaysAllowModeSwitch, alwaysAllowSubtasks, alwaysAllowExecute, + alwaysAllowFollowupQuestions, + followupAutoApproveTimeoutMs = 60000, allowedCommands, setCachedStateField, ...props @@ -95,6 +101,7 @@ export const AutoApproveSettings = ({ alwaysAllowModeSwitch={alwaysAllowModeSwitch} alwaysAllowSubtasks={alwaysAllowSubtasks} alwaysAllowExecute={alwaysAllowExecute} + alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions} onToggle={(key, value) => setCachedStateField(key, value)} /> @@ -202,6 +209,33 @@ export const AutoApproveSettings = ({
    )} + {alwaysAllowFollowupQuestions && ( +
    +
    + +
    {t("settings:autoApprove.followupQuestions.label")}
    +
    +
    +
    + + setCachedStateField("followupAutoApproveTimeoutMs", value) + } + data-testid="followup-timeout-slider" + /> + {followupAutoApproveTimeoutMs / 1000}s +
    +
    + {t("settings:autoApprove.followupQuestions.timeoutLabel")} +
    +
    +
    + )} + {alwaysAllowExecute && (
    diff --git a/webview-ui/src/components/settings/AutoApproveToggle.tsx b/webview-ui/src/components/settings/AutoApproveToggle.tsx index ffad47e2ac..6c82d3c984 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 } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" type AutoApproveToggles = Pick< GlobalSettings, @@ -14,6 +14,7 @@ type AutoApproveToggles = Pick< | "alwaysAllowModeSwitch" | "alwaysAllowSubtasks" | "alwaysAllowExecute" + | "alwaysAllowFollowupQuestions" > export type AutoApproveSetting = keyof AutoApproveToggles @@ -83,6 +84,13 @@ export const autoApproveSettingsConfig: Record {Object.values(autoApproveSettingsConfig).map(({ key, descriptionKey, labelKey, icon, testId }) => ( - + + + ))}
    ) diff --git a/webview-ui/src/components/settings/CodeIndexSettings.tsx b/webview-ui/src/components/settings/CodeIndexSettings.tsx index 13c9524d9f..8aae766b8f 100644 --- a/webview-ui/src/components/settings/CodeIndexSettings.tsx +++ b/webview-ui/src/components/settings/CodeIndexSettings.tsx @@ -7,6 +7,7 @@ import { Trans } from "react-i18next" import { CodebaseIndexConfig, CodebaseIndexModels, ProviderSettings } from "@roo-code/types" import { EmbedderProvider } from "@roo/embeddingModels" +import { SEARCH_MIN_SCORE } from "../../../../src/services/code-index/constants" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" @@ -27,6 +28,12 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, + Slider, + Button, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@src/components/ui" import { SetCachedStateField } from "./types" @@ -51,6 +58,7 @@ export const CodeIndexSettings: React.FC = ({ areSettingsCommitted, }) => { const { t } = useAppTranslation() + const DEFAULT_QDRANT_URL = "http://localhost:6333" const [indexingStatus, setIndexingStatus] = useState({ systemStatus: "Standby", message: "", @@ -58,6 +66,7 @@ export const CodeIndexSettings: React.FC = ({ totalItems: 0, currentItemUnit: "items", }) + const [advancedExpanded, setAdvancedExpanded] = useState(false) // Safely calculate available models for current provider const currentProvider = codebaseIndexConfig?.codebaseIndexEmbedderProvider @@ -425,13 +434,23 @@ export const CodeIndexSettings: React.FC = ({
    setCachedStateField("codebaseIndexConfig", { ...codebaseIndexConfig, codebaseIndexQdrantUrl: e.target.value, }) } + onBlur={(e: any) => { + // Set default value if field is empty on blur + if (!e.target.value) { + setCachedStateField("codebaseIndexConfig", { + ...codebaseIndexConfig, + codebaseIndexQdrantUrl: DEFAULT_QDRANT_URL, + }) + } + }} style={{ width: "100%" }}>
    @@ -495,6 +514,78 @@ export const CodeIndexSettings: React.FC = ({ )}
    + + {/* Advanced Configuration Section */} +
    + + + {advancedExpanded && ( +
    +
    +
    + + {t("settings:codeIndex.searchMinScoreLabel")} + +
    + + setCachedStateField("codebaseIndexConfig", { + ...codebaseIndexConfig, + codebaseIndexSearchMinScore: value, + }) + } + data-testid="search-min-score-slider" + aria-label={t("settings:codeIndex.searchMinScoreLabel")} + /> + + {( + codebaseIndexConfig.codebaseIndexSearchMinScore ?? SEARCH_MIN_SCORE + ).toFixed(2)} + + + + + + + +

    {t("settings:codeIndex.searchMinScoreResetTooltip")}

    +
    +
    +
    +
    +
    + {t("settings:codeIndex.searchMinScoreDescription")} +
    +
    +
    +
    + )} +
    )} diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index 568b8eeee1..160f79dc84 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -6,7 +6,15 @@ 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 } from "@src/components/ui" +import { + Button, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + StandardTooltip, +} from "@src/components/ui" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { MessageSquare } from "lucide-react" @@ -97,15 +105,14 @@ const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts }: Prom
    - + +
    (({ onDone, t codebaseIndexModels, customSupportPrompts, profileThresholds, + alwaysAllowFollowupQuestions, + followupAutoApproveTimeoutMs, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -310,6 +313,8 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) vscode.postMessage({ type: "alwaysAllowSubtasks", bool: alwaysAllowSubtasks }) + vscode.postMessage({ type: "alwaysAllowFollowupQuestions", bool: alwaysAllowFollowupQuestions }) + vscode.postMessage({ type: "followupAutoApproveTimeoutMs", value: followupAutoApproveTimeoutMs }) vscode.postMessage({ type: "condensingApiConfigId", text: condensingApiConfigId || "" }) vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" }) vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} }) @@ -448,27 +453,28 @@ const SettingsView = forwardRef(({ onDone, t

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

    - - + }> + + + + +
    @@ -510,7 +516,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 */} @@ -597,6 +603,8 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowModeSwitch={alwaysAllowModeSwitch} alwaysAllowSubtasks={alwaysAllowSubtasks} alwaysAllowExecute={alwaysAllowExecute} + alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions} + followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs} allowedCommands={allowedCommands} setCachedStateField={setCachedStateField} /> diff --git a/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx index 553f60c79e..194f0ff31e 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 "@testing-library/react" +import { render, screen, fireEvent, within } from "@/utils/test-utils" import ApiConfigManager from "../ApiConfigManager" @@ -41,6 +41,7 @@ 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 31cc2ec82f..1e13323f36 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { type ModelInfo, type ProviderSettings, openAiModelInfoSaneDefaults } from "@roo-code/types" @@ -61,6 +61,7 @@ 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 4442cb9abb..ac8c054f65 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 "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" @@ -25,6 +25,7 @@ describe("AutoApproveToggle", () => { alwaysAllowModeSwitch: true, alwaysAllowSubtasks: false, alwaysAllowExecute: true, + alwaysAllowFollowupQuestions: false, onToggle: mockOnToggle, } diff --git a/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx index 7e591114a8..802d5e8813 100644 --- a/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/CodeIndexSettings.spec.tsx -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import userEvent from "@testing-library/user-event" import { CodeIndexSettings } from "../CodeIndexSettings" @@ -43,6 +43,11 @@ vi.mock("@src/i18n/TranslationContext", () => ({ "settings:codeIndex.clearDataDialog.description": "This will remove all indexed data", "settings:codeIndex.clearDataDialog.cancelButton": "Cancel", "settings:codeIndex.clearDataDialog.confirmButton": "Confirm", + "settings:codeIndex.searchMinScoreLabel": "Search Score Threshold", + "settings:codeIndex.searchMinScoreDescription": + "Minimum similarity score (0.0-1.0) required for search results. Lower values return more results but may be less relevant. Higher values return fewer but more relevant results.", + "settings:codeIndex.searchMinScoreResetTooltip": "Reset to default value (0.4)", + "settings:codeIndex.advancedConfigLabel": "Advanced Configuration", } return translations[key] || key }, @@ -85,6 +90,24 @@ vi.mock("@src/components/ui", () => ({ AlertDialogHeader: ({ children }: any) =>
    {children}
    , AlertDialogTitle: ({ children }: any) =>
    {children}
    , AlertDialogTrigger: ({ children }: any) =>
    {children}
    , + Slider: ({ value, onValueChange, "data-testid": dataTestId }: any) => ( + onValueChange && onValueChange([parseFloat(e.target.value)])} + data-testid={dataTestId} + role="slider" + /> + ), + Button: ({ children, onClick, "data-testid": dataTestId, ...props }: any) => ( + + ), + Tooltip: ({ children }: any) =>
    {children}
    , + TooltipContent: ({ children }: any) =>
    {children}
    , + TooltipProvider: ({ children }: any) =>
    {children}
    , + TooltipTrigger: ({ children }: any) =>
    {children}
    , })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ @@ -158,6 +181,7 @@ describe("CodeIndexSettings", () => { codebaseIndexEmbedderProvider: "openai" as const, codebaseIndexEmbedderModelId: "text-embedding-3-small", codebaseIndexQdrantUrl: "http://localhost:6333", + codebaseIndexSearchMinScore: 0.4, }, apiConfiguration: { codeIndexOpenAiKey: "", @@ -204,7 +228,7 @@ describe("CodeIndexSettings", () => { expect(screen.getByText("Base URL")).toBeInTheDocument() expect(screen.getByText("API Key")).toBeInTheDocument() - expect(screen.getAllByTestId("vscode-textfield")).toHaveLength(6) // Base URL, API Key, Embedding Dimension, Model ID, Qdrant URL, Qdrant Key + expect(screen.getAllByTestId("vscode-textfield")).toHaveLength(6) // Base URL, API Key, Embedding Dimension, Model ID, Qdrant URL, Qdrant Key (Search Min Score is now a slider) }) it("should hide OpenAI Compatible fields when different provider is selected", () => { @@ -817,6 +841,113 @@ describe("CodeIndexSettings", () => { }) }) + describe("Search Minimum Score Slider", () => { + const expandAdvancedConfig = () => { + const advancedButton = screen.getByRole("button", { name: /Advanced Configuration/i }) + fireEvent.click(advancedButton) + } + + it("should render advanced configuration toggle button", () => { + render() + + expect(screen.getByRole("button", { name: /Advanced Configuration/i })).toBeInTheDocument() + expect(screen.getByText("Advanced Configuration")).toBeInTheDocument() + }) + + it("should render search minimum score slider with reset button when expanded", () => { + render() + + expandAdvancedConfig() + + expect(screen.getByTestId("search-min-score-slider")).toBeInTheDocument() + expect(screen.getByTestId("search-min-score-reset-button")).toBeInTheDocument() + expect(screen.getByText("Search Score Threshold")).toBeInTheDocument() + }) + + it("should display current search minimum score value when expanded", () => { + const propsWithScore = { + ...defaultProps, + codebaseIndexConfig: { + ...defaultProps.codebaseIndexConfig, + codebaseIndexSearchMinScore: 0.65, + }, + } + + render() + + expandAdvancedConfig() + + expect(screen.getByText("0.65")).toBeInTheDocument() + }) + + it("should call setCachedStateField when slider value changes", () => { + render() + + expandAdvancedConfig() + + const slider = screen.getByTestId("search-min-score-slider") + fireEvent.change(slider, { target: { value: "0.8" } }) + + expect(mockSetCachedStateField).toHaveBeenCalledWith("codebaseIndexConfig", { + ...defaultProps.codebaseIndexConfig, + codebaseIndexSearchMinScore: 0.8, + }) + }) + + it("should reset to default value when reset button is clicked", () => { + const propsWithScore = { + ...defaultProps, + codebaseIndexConfig: { + ...defaultProps.codebaseIndexConfig, + codebaseIndexSearchMinScore: 0.8, + }, + } + + render() + + expandAdvancedConfig() + + const resetButton = screen.getByTestId("search-min-score-reset-button") + fireEvent.click(resetButton) + + expect(mockSetCachedStateField).toHaveBeenCalledWith("codebaseIndexConfig", { + ...defaultProps.codebaseIndexConfig, + codebaseIndexSearchMinScore: 0.4, + }) + }) + + it("should use default value when no score is set", () => { + const propsWithoutScore = { + ...defaultProps, + codebaseIndexConfig: { + ...defaultProps.codebaseIndexConfig, + codebaseIndexSearchMinScore: undefined, + }, + } + + render() + + expandAdvancedConfig() + + expect(screen.getByText("0.40")).toBeInTheDocument() + }) + + it("should toggle advanced section visibility", () => { + render() + + // Initially collapsed - should not see slider + expect(screen.queryByTestId("search-min-score-slider")).not.toBeInTheDocument() + + // Expand advanced section + expandAdvancedConfig() + expect(screen.getByTestId("search-min-score-slider")).toBeInTheDocument() + + // Collapse again + expandAdvancedConfig() + expect(screen.queryByTestId("search-min-score-slider")).not.toBeInTheDocument() + }) + }) + describe("Error Handling", () => { it("should handle invalid provider gracefully", () => { const propsWithInvalidProvider = { diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 9a7d451d1d..e467ab51d6 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { ContextManagementSettings } from "@src/components/settings/ContextManagementSettings" diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx index 38237dc0e3..82c6da2a3f 100644 --- a/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ModelPicker.spec.tsx -import { screen, fireEvent, render } from "@testing-library/react" +import { screen, fireEvent, render } from "@/utils/test-utils" import { act } from "react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { vi } from "vitest" diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 8d79b998a1..0a76e54ffe 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { vscode } from "@/utils/vscode" @@ -115,6 +115,7 @@ vi.mock("@/components/ui", () => ({ {children} ), + StandardTooltip: ({ children, content }: any) =>
    {children}
    , Input: ({ value, onChange, placeholder, "data-testid": dataTestId }: any) => ( ), diff --git a/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx b/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx index ad4e352126..f9eba66c8f 100644 --- a/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/TemperatureControl.spec.tsx -import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { TemperatureControl } from "../TemperatureControl" diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index f5d1cf29dd..5ca51b4528 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ThinkingBudget.spec.tsx -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import type { ModelInfo } from "@roo-code/types" diff --git a/webview-ui/src/components/settings/providers/Bedrock.tsx b/webview-ui/src/components/settings/providers/Bedrock.tsx index a0ebafd88e..1839298f9b 100644 --- a/webview-ui/src/components/settings/providers/Bedrock.tsx +++ b/webview-ui/src/components/settings/providers/Bedrock.tsx @@ -5,7 +5,7 @@ import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview- import { type ProviderSettings, type ModelInfo, BEDROCK_REGIONS } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui" import { inputEventTransform, noTransform } from "../transforms" @@ -114,11 +114,12 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo onChange={handleInputChange("awsUsePromptCache", noTransform)}>
    {t("settings:providers.enablePromptCaching")} - + + +
    diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index b5f7abc7d4..736b0253c4 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -15,7 +15,7 @@ import { import { ExtensionMessage } from "@roo/ExtensionMessage" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { Button } from "@src/components/ui" +import { Button, StandardTooltip } from "@src/components/ui" import { convertHeadersToObject } from "../utils/headers" import { inputEventTransform, noTransform } from "../transforms" @@ -208,9 +208,11 @@ export const OpenAICompatible = ({
    - - - + + + + +
    {!customHeaders.length ? (
    @@ -231,12 +233,11 @@ export const OpenAICompatible = ({ placeholder={t("settings:providers.headerValue")} onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)} /> - handleRemoveCustomHeader(index)}> - - + + handleRemoveCustomHeader(index)}> + + +
    )) )} @@ -305,7 +306,6 @@ export const OpenAICompatible = ({ return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" })(), }} - title={t("settings:providers.customModel.maxTokens.description")} onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = parseInt((e.target as HTMLInputElement).value) @@ -344,7 +344,6 @@ export const OpenAICompatible = ({ return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" })(), }} - title={t("settings:providers.customModel.contextWindow.description")} onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = (e.target as HTMLInputElement).value const parsed = parseInt(value) @@ -382,11 +381,12 @@ export const OpenAICompatible = ({ {t("settings:providers.customModel.imageSupport.label")} - + + +
    {t("settings:providers.customModel.imageSupport.description")} @@ -405,11 +405,12 @@ export const OpenAICompatible = ({ })}> {t("settings:providers.customModel.computerUse.label")} - + + +
    {t("settings:providers.customModel.computerUse.description")} @@ -428,11 +429,12 @@ export const OpenAICompatible = ({ })}> {t("settings:providers.customModel.promptCache.label")} - + + +
    {t("settings:providers.customModel.promptCache.description")} @@ -473,11 +475,12 @@ export const OpenAICompatible = ({ - + + +
    @@ -516,11 +519,12 @@ export const OpenAICompatible = ({ - + + +
    @@ -559,11 +563,13 @@ export const OpenAICompatible = ({ {t("settings:providers.customModel.pricing.cacheReads.label")} - + + +
    @@ -599,11 +605,13 @@ export const OpenAICompatible = ({ - + + +
    diff --git a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx index ac99a12c6c..b5bb16e975 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { Bedrock } from "../Bedrock" import { ProviderSettings } from "@roo-code/types" diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx index c96de176e6..aba81ec219 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { OpenAICompatible } from "../OpenAICompatible" import { ProviderSettings } from "@roo-code/types" @@ -64,6 +64,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({ // Mock the UI components vi.mock("@src/components/ui", () => ({ Button: ({ children, onClick }: any) => , + StandardTooltip: ({ children, content }: any) =>
    {children}
    , })) // Mock other components diff --git a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx index d4ed834654..2262adb872 100644 --- a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx +++ b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx @@ -1,7 +1,7 @@ // npx vitest run src/components/ui/__tests__/select-dropdown.spec.tsx import { ReactNode } from "react" -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { SelectDropdown, DropdownOptionType } from "../select-dropdown" diff --git a/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx b/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx new file mode 100644 index 0000000000..e1cb619545 --- /dev/null +++ b/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx @@ -0,0 +1,182 @@ +import { render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { describe, it, expect } from "vitest" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip" +import { StandardTooltip } from "../standard-tooltip" + +describe("Tooltip", () => { + it("should render tooltip content on hover", async () => { + const user = userEvent.setup() + + render( + + + Hover me + Tooltip text + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Tooltip text") + expect(tooltips.length).toBeGreaterThan(0) + }, + { timeout: 1000 }, + ) + }) + + it("should apply text wrapping classes", async () => { + const user = userEvent.setup() + + render( + + + Hover me + + This is a very long tooltip text that should wrap when it reaches the maximum width + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText(/This is a very long tooltip text/) + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words") + }, + { timeout: 1000 }, + ) + }) + + it("should not have overflow-hidden class", async () => { + const user = userEvent.setup() + + render( + + + Hover me + Tooltip text + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Tooltip text") + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).not.toHaveClass("overflow-hidden") + }, + { timeout: 1000 }, + ) + }) +}) + +describe("StandardTooltip", () => { + it("should render with default delay", async () => { + const user = userEvent.setup() + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Tooltip text") + expect(tooltips.length).toBeGreaterThan(0) + }, + { timeout: 1000 }, + ) + }) + + it("should apply custom maxWidth", async () => { + const user = userEvent.setup() + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Long tooltip text") + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveStyle({ maxWidth: "200px" }) + }, + { timeout: 1000 }, + ) + }) + + it("should apply custom maxWidth as string", async () => { + const user = userEvent.setup() + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Long tooltip text") + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveStyle({ maxWidth: "15rem" }) + }, + { timeout: 1000 }, + ) + }) + + it("should handle long content with text wrapping", async () => { + const user = userEvent.setup() + const longContent = + "This is a very long tooltip content that should definitely wrap when displayed because it exceeds the maximum width constraint" + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText(longContent) + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words") + }, + { timeout: 1000 }, + ) + }) +}) diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 0d58268d14..5fefabf59e 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -369,4 +369,76 @@ describe("useSelectedModel", () => { expect(result.current.info).toBeUndefined() }) }) + + describe("claude-code provider", () => { + it("should return claude-code model with supportsImages disabled", () => { + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + glama: {}, + unbound: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: "claude-code", + apiModelId: "claude-sonnet-4-20250514", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("claude-code") + expect(result.current.id).toBe("claude-sonnet-4-20250514") + expect(result.current.info).toBeDefined() + expect(result.current.info?.supportsImages).toBe(false) + expect(result.current.info?.supportsPromptCache).toBe(true) // Claude Code now supports prompt cache + // Verify it inherits other properties from anthropic models + expect(result.current.info?.maxTokens).toBe(64_000) + expect(result.current.info?.contextWindow).toBe(200_000) + expect(result.current.info?.supportsComputerUse).toBe(true) + }) + + it("should use default claude-code model when no modelId is specified", () => { + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + glama: {}, + unbound: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: "claude-code", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe("claude-code") + expect(result.current.id).toBe("claude-sonnet-4-20250514") // Default model + expect(result.current.info).toBeDefined() + expect(result.current.info?.supportsImages).toBe(false) + }) + }) }) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 09cae03e5d..40c1ff2431 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -30,6 +30,8 @@ import { glamaDefaultModelId, unboundDefaultModelId, litellmDefaultModelId, + claudeCodeDefaultModelId, + claudeCodeModels, } from "@roo-code/types" import type { RouterModels } from "@roo/api" @@ -199,6 +201,12 @@ function getSelectedModel({ const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels] return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images. } + case "claude-code": { + // Claude Code models extend anthropic models but with images and prompt caching disabled + const id = apiConfiguration.apiModelId ?? claudeCodeDefaultModelId + const info = claudeCodeModels[id as keyof typeof claudeCodeModels] + return { id, info: { ...openAiModelInfoSaneDefaults, ...info } } + } // case "anthropic": // case "human-relay": // case "fake-ai": diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index 69d9c093e0..5bd6b62ee8 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/webview-ui/src/components/ui/index.ts @@ -16,3 +16,4 @@ export * from "./select-dropdown" export * from "./select" export * from "./textarea" export * from "./tooltip" +export * from "./standard-tooltip" diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx index 7fcc6884b7..d9ee99ebbb 100644 --- a/webview-ui/src/components/ui/select-dropdown.tsx +++ b/webview-ui/src/components/ui/select-dropdown.tsx @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" import { useRooPortal } from "./hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui" +import { StandardTooltip } from "@/components/ui" export enum DropdownOptionType { ITEM = "item", @@ -185,25 +186,28 @@ export const SelectDropdown = React.memo( [onChange, options], ) + const triggerContent = ( + + + {displayText} + + ) + return ( - - - {displayText} - + {title ? {triggerContent} : triggerContent} + * + * + * + * // With custom positioning + * + * + * + * + * @note This replaces native HTML title attributes for consistent timing. + * @note Requires a TooltipProvider to be present in the component tree (typically at the app root). + * @note Do not nest StandardTooltip components as this can cause UI issues. + */ +export function StandardTooltip({ + children, + content, + side = "top", + align = "center", + sideOffset = 4, + className, + asChild = true, + maxWidth, +}: StandardTooltipProps) { + // Don't render tooltip if content is empty or only whitespace + if (!content || (typeof content === "string" && !content.trim())) { + return <>{children} + } + + const style = maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : undefined + + return ( + + {children} + + {content} + + + ) +} diff --git a/webview-ui/src/components/ui/tooltip.tsx b/webview-ui/src/components/ui/tooltip.tsx index 7818d9b125..d09d8cec9d 100644 --- a/webview-ui/src/components/ui/tooltip.tsx +++ b/webview-ui/src/components/ui/tooltip.tsx @@ -17,8 +17,11 @@ const TooltipContent = React.forwardRef< void // Setter for the new property + alwaysAllowFollowupQuestions: boolean // New property for follow-up questions auto-approve + setAlwaysAllowFollowupQuestions: (value: boolean) => void // Setter for the new property + followupAutoApproveTimeoutMs: number | undefined // Timeout in ms for auto-approving follow-up questions + setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout condensingApiConfigId?: string setCondensingApiConfigId: (value: string) => void customCondensingPrompt?: string @@ -226,6 +230,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [currentCheckpoint, setCurrentCheckpoint] = useState() const [extensionRouterModels, setExtensionRouterModels] = useState(undefined) const [marketplaceItems, setMarketplaceItems] = useState([]) + const [alwaysAllowFollowupQuestions, setAlwaysAllowFollowupQuestions] = useState(false) // Add state for follow-up questions auto-approve + const [followupAutoApproveTimeoutMs, setFollowupAutoApproveTimeoutMs] = useState(undefined) // Will be set from global settings const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState({ project: {}, global: {}, @@ -255,6 +261,14 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => mergeExtensionState(prevState, newState)) setShowWelcome(!checkExistKey(newState.apiConfiguration)) setDidHydrateState(true) + // Update alwaysAllowFollowupQuestions if present in state message + if ((newState as any).alwaysAllowFollowupQuestions !== undefined) { + setAlwaysAllowFollowupQuestions((newState as any).alwaysAllowFollowupQuestions) + } + // Update followupAutoApproveTimeoutMs if present in state message + if ((newState as any).followupAutoApproveTimeoutMs !== undefined) { + setFollowupAutoApproveTimeoutMs((newState as any).followupAutoApproveTimeoutMs) + } // Handle marketplace data if present in state message if (newState.marketplaceItems !== undefined) { setMarketplaceItems(newState.marketplaceItems) @@ -352,6 +366,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode marketplaceItems, marketplaceInstalledMetadata, profileThresholds: state.profileThresholds ?? {}, + alwaysAllowFollowupQuestions, + followupAutoApproveTimeoutMs, setExperimentEnabled: (id, enabled) => setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })), setApiConfiguration, @@ -367,6 +383,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })), setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })), setAlwaysAllowSubtasks: (value) => setState((prevState) => ({ ...prevState, alwaysAllowSubtasks: value })), + setAlwaysAllowFollowupQuestions, + setFollowupAutoApproveTimeoutMs: (value) => + setState((prevState) => ({ ...prevState, followupAutoApproveTimeoutMs: value })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })), setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), setAllowedMaxRequests: (value) => setState((prevState) => ({ ...prevState, allowedMaxRequests: value })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 085cd5da71..1e5867d3fc 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, act } from "@testing-library/react" +import { render, screen, act } from "@/utils/test-utils" import { ProviderSettings, ExperimentId } from "@roo-code/types" diff --git a/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx b/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx index ee52680201..cdf0c21acf 100644 --- a/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx +++ b/webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx @@ -1,4 +1,4 @@ -import { render } from "@testing-library/react" +import { render } from "@/utils/test-utils" import TranslationProvider, { useAppTranslation } from "../TranslationContext" diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 4db48e40f3..1dd892a39f 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -234,17 +234,19 @@ "tokens": "tokens" }, "followUpSuggest": { - "copyToInput": "Copiar a l'entrada (o Shift + clic)" + "copyToInput": "Copiar a l'entrada (o Shift + clic)", + "autoSelectCountdown": "Selecció automàtica en {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { - "title": "🎉 Roo Code {{version}} publicat", - "description": "Roo Code {{version}} porta noves funcionalitats potents i millores basades en els teus comentaris.", + "title": "🎉 Roo Code {{version}} Llançat", + "description": "Roo Code {{version}} porta noves funcions potents i millores significatives per millorar el vostre flux de treball de desenvolupament.", "whatsNew": "Novetats", - "feature1": "Llançament del Marketplace Roo - El marketplace ja està en funcionament! Descobreix i instal·la modes i MCP més fàcilment que mai.", - "feature2": "Models Gemini 2.5 - S'ha afegit suport per als nous models Gemini 2.5 Pro, Flash i Flash Lite.", - "feature3": "Suport per a fitxers Excel i més - S'ha afegit suport per a fitxers Excel (.xlsx) i nombroses correccions d'errors i millores!", - "hideButton": "Amagar anunci", - "detailsDiscussLinks": "Obtingues més detalls i participa a Discord i Reddit 🚀" + "feature1": "Compartició de Tasques amb 1 Clic: Comparteix instantàniament les vostres tasques amb companys i la comunitat amb un sol clic.", + "feature2": "Suport per a Directori Global .roo: Carrega regles i configuracions des d'un directori global .roo per a configuracions consistents entre projectes.", + "feature3": "Transicions Millorades d'Arquitecte a Codi: Transferències fluides de la planificació en mode Arquitecte a la implementació en mode Codi.", + "hideButton": "Amaga l'anunci", + "detailsDiscussLinks": "Obtén més detalls i uneix-te a les discussions a Discord i Reddit 🚀" }, "browser": { "rooWantsToUse": "Roo vol utilitzar el navegador:", @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo vol cercar a la base de codi {{query}}:", "wantsToSearchWithPath": "Roo vol cercar a la base de codi {{query}} a {{path}}:", - "didSearch": "S'han trobat {{count}} resultat(s) per a {{query}}:" + "didSearch": "S'han trobat {{count}} resultat(s) per a {{query}}:", + "resultTooltip": "Puntuació de similitud: {{score}} (fes clic per obrir el fitxer)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Indexat", "error": "Error d'índex", "status": "Estat de l'índex" + }, + "versionIndicator": { + "ariaLabel": "Versió {{version}} - Feu clic per veure les notes de llançament" } } diff --git a/webview-ui/src/i18n/locales/ca/prompts.json b/webview-ui/src/i18n/locales/ca/prompts.json index 04359f996a..27d74d3d26 100644 --- a/webview-ui/src/i18n/locales/ca/prompts.json +++ b/webview-ui/src/i18n/locales/ca/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modes", "createNewMode": "Crear nou mode", + "importMode": "Importar mode", + "noMatchFound": "No s'han trobat modes", "editModesConfig": "Editar configuració de modes", "editGlobalModes": "Editar modes globals", "editProjectModes": "Editar modes de projecte (.roomodes)", @@ -50,6 +52,28 @@ "description": "Afegiu directrius de comportament específiques per al mode {{modeName}}.", "loadFromFile": "Les instruccions personalitzades específiques per al mode {{mode}} també es poden carregar des de la carpeta .roo/rules-{{slug}}/ al vostre espai de treball (.roorules-{{slug}} i .clinerules-{{slug}} estan obsolets i deixaran de funcionar aviat)." }, + "exportMode": { + "title": "Exportar mode", + "description": "Exporta aquest mode a un fitxer YAML amb totes les regles incloses per compartir fàcilment amb altres.", + "export": "Exportar mode", + "exporting": "Exportant..." + }, + "importMode": { + "selectLevel": "Tria on importar aquest mode:", + "import": "Importar", + "importing": "Important...", + "global": { + "label": "Nivell global", + "description": "Disponible a tots els projectes. Les regles es fusionaran amb les instruccions personalitzades." + }, + "project": { + "label": "Nivell de projecte", + "description": "Només disponible en aquest espai de treball. Si el mode exportat contenia fitxers de regles, es tornaran a crear a la carpeta .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Avançat" + }, "globalCustomInstructions": { "title": "Instruccions personalitzades per a tots els modes", "description": "Aquestes instruccions s'apliquen a tots els modes. Proporcionen un conjunt bàsic de comportaments que es poden millorar amb instruccions específiques de cada mode a continuació. <0>Més informació", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index ea4f974149..3fe9cd78b9 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL d'Ollama:", "qdrantUrlLabel": "URL de Qdrant", "qdrantKeyLabel": "Clau de Qdrant:", + "advancedConfigLabel": "Configuració avançada", + "searchMinScoreLabel": "Llindar de puntuació de cerca", + "searchMinScoreDescription": "Puntuació mínima de similitud (0.0-1.0) requerida per als resultats de la cerca. Valors més baixos retornen més resultats però poden ser menys rellevants. Valors més alts retornen menys resultats però més rellevants.", + "searchMinScoreResetTooltip": "Restablir al valor per defecte (0.4)", "startIndexingButton": "Iniciar indexació", "clearIndexDataButton": "Esborrar dades d'índex", "unsavedSettingsMessage": "Si us plau, deseu la configuració abans d'iniciar el procés d'indexació.", @@ -110,6 +114,11 @@ "label": "Subtasques", "description": "Permetre la creació i finalització de subtasques sense requerir aprovació" }, + "followupQuestions": { + "label": "Pregunta", + "description": "Seleccionar automàticament la primera resposta suggerida per a preguntes de seguiment després del temps d'espera configurat", + "timeoutLabel": "Temps d'espera abans de seleccionar automàticament la primera resposta" + }, "execute": { "label": "Executar", "description": "Executar automàticament comandes de terminal permeses sense requerir aprovació", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 3ee69f9ae4..c62fe9d3bb 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -234,15 +234,17 @@ "tokens": "Tokens" }, "followUpSuggest": { - "copyToInput": "In Eingabefeld kopieren (oder Shift + Klick)" + "copyToInput": "In Eingabefeld kopieren (oder Shift + Klick)", + "autoSelectCountdown": "Automatische Auswahl in {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { "title": "🎉 Roo Code {{version}} veröffentlicht", - "description": "Roo Code {{version}} bringt wichtige neue Funktionen und Verbesserungen basierend auf deinem Feedback.", + "description": "Roo Code {{version}} bringt mächtige neue Funktionen und bedeutende Verbesserungen, um deinen Entwicklungsworkflow zu verbessern.", "whatsNew": "Was ist neu", - "feature1": "Roo Marketplace Launch: Der Marketplace ist jetzt live! Entdecke und installiere Modi und MCPs einfacher denn je.", - "feature2": "Gemini 2.5 Modelle: Unterstützung für neue Gemini 2.5 Pro, Flash und Flash Lite Modelle hinzugefügt.", - "feature3": "Excel-Datei-Unterstützung & mehr: Excel (.xlsx) Datei-Unterstützung hinzugefügt sowie zahlreiche Fehlerbehebungen und Verbesserungen!", + "feature1": "1-Klick-Aufgaben-Teilen: Teile deine Aufgaben sofort mit Kollegen und der Community mit einem einzigen Klick.", + "feature2": "Globale .roo-Verzeichnis-Unterstützung: Lade Regeln und Konfigurationen aus einem globalen .roo-Verzeichnis für konsistente Einstellungen über Projekte hinweg.", + "feature3": "Verbesserte Architect-zu-Code-Übergänge: Nahtlose Übergaben von der Planung im Architect-Modus zur Implementierung im Code-Modus.", "hideButton": "Ankündigung ausblenden", "detailsDiscussLinks": "Erhalte mehr Details und diskutiere auf Discord und Reddit 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo möchte den Codebase nach {{query}} durchsuchen:", "wantsToSearchWithPath": "Roo möchte den Codebase nach {{query}} in {{path}} durchsuchen:", - "didSearch": "{{count}} Ergebnis(se) für {{query}} gefunden:" + "didSearch": "{{count}} Ergebnis(se) für {{query}} gefunden:", + "resultTooltip": "Ähnlichkeitswert: {{score}} (klicken zum Öffnen der Datei)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Indiziert", "error": "Index-Fehler", "status": "Index-Status" + }, + "versionIndicator": { + "ariaLabel": "Version {{version}} - Klicken Sie, um die Versionshinweise anzuzeigen" } } diff --git a/webview-ui/src/i18n/locales/de/prompts.json b/webview-ui/src/i18n/locales/de/prompts.json index 6e9fd0f47b..11c132f1df 100644 --- a/webview-ui/src/i18n/locales/de/prompts.json +++ b/webview-ui/src/i18n/locales/de/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modi", "createNewMode": "Neuen Modus erstellen", + "importMode": "Modus importieren", + "noMatchFound": "Keine Modi gefunden", "editModesConfig": "Moduskonfiguration bearbeiten", "editGlobalModes": "Globale Modi bearbeiten", "editProjectModes": "Projektmodi bearbeiten (.roomodes)", @@ -50,6 +52,28 @@ "description": "Fügen Sie verhaltensspezifische Richtlinien für den Modus {{modeName}} hinzu.", "loadFromFile": "Benutzerdefinierte Anweisungen für den Modus {{mode}} können auch aus dem Ordner .roo/rules-{{slug}}/ in deinem Arbeitsbereich geladen werden (.roorules-{{slug}} und .clinerules-{{slug}} sind veraltet und werden bald nicht mehr funktionieren)." }, + "exportMode": { + "title": "Modus exportieren", + "description": "Exportiert diesen Modus in eine YAML-Datei mit allen enthaltenen Regeln zum einfachen Teilen mit anderen.", + "export": "Modus exportieren", + "exporting": "Exportieren..." + }, + "importMode": { + "selectLevel": "Wähle, wo dieser Modus importiert werden soll:", + "import": "Importieren", + "importing": "Importiere...", + "global": { + "label": "Globale Ebene", + "description": "Verfügbar in allen Projekten. Wenn der exportierte Modus Regeldateien enthielt, werden diese im globalen Ordner .roo/rules-{slug}/ neu erstellt." + }, + "project": { + "label": "Projektebene", + "description": "Nur in diesem Arbeitsbereich verfügbar. Wenn der exportierte Modus Regeldateien enthielt, werden diese im Ordner .roo/rules-{slug}/ neu erstellt." + } + }, + "advanced": { + "title": "Erweitert" + }, "globalCustomInstructions": { "title": "Benutzerdefinierte Anweisungen für alle Modi", "description": "Diese Anweisungen gelten für alle Modi. Sie bieten einen grundlegenden Satz von Verhaltensweisen, die durch modusspezifische Anweisungen unten erweitert werden können. <0>Mehr erfahren", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c5e161bafe..bf33512650 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama-URL:", "qdrantUrlLabel": "Qdrant-URL", "qdrantKeyLabel": "Qdrant-Schlüssel:", + "advancedConfigLabel": "Erweiterte Konfiguration", + "searchMinScoreLabel": "Suchergebnis-Schwellenwert", + "searchMinScoreDescription": "Mindestähnlichkeitswert (0.0-1.0), der für Suchergebnisse erforderlich ist. Niedrigere Werte liefern mehr Ergebnisse, die jedoch möglicherweise weniger relevant sind. Höhere Werte liefern weniger, aber relevantere Ergebnisse.", + "searchMinScoreResetTooltip": "Auf Standardwert zurücksetzen (0.4)", "startIndexingButton": "Indexierung starten", "clearIndexDataButton": "Indexdaten löschen", "unsavedSettingsMessage": "Bitte speichere deine Einstellungen, bevor du den Indexierungsprozess startest.", @@ -110,6 +114,11 @@ "label": "Teilaufgaben", "description": "Erstellung und Abschluss von Unteraufgaben ohne Genehmigung erlauben" }, + "followupQuestions": { + "label": "Frage", + "description": "Automatisch die erste vorgeschlagene Antwort für Folgefragen nach der konfigurierten Zeitüberschreitung auswählen", + "timeoutLabel": "Wartezeit vor der automatischen Auswahl der ersten Antwort" + }, "execute": { "label": "Ausführen", "description": "Erlaubte Terminal-Befehle automatisch ohne Genehmigung ausführen", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 9ee8e167bd..ea4f8920f5 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -201,7 +201,8 @@ "codebaseSearch": { "wantsToSearch": "Roo wants to search the codebase for {{query}}:", "wantsToSearchWithPath": "Roo wants to search the codebase for {{query}} in {{path}}:", - "didSearch": "Found {{count}} result(s) for {{query}}:" + "didSearch": "Found {{count}} result(s) for {{query}}:", + "resultTooltip": "Similarity score: {{score}} (click to open file)" }, "commandOutput": "Command Output", "response": "Response", @@ -244,11 +245,11 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Released", - "description": "Roo Code {{version}} brings major new features and improvements based on your feedback.", + "description": "Roo Code {{version}} brings powerful new features and significant improvements to enhance your development workflow.", "whatsNew": "What's New", - "feature1": "Roo Marketplace Launch: The marketplace is now live! Discover and install modes and MCPs easier than ever before.", - "feature2": "Gemini 2.5 Models: Added support for new Gemini 2.5 Pro, Flash, and Flash Lite models.", - "feature3": "Excel File Support & More: Added Excel (.xlsx) file support and numerous bug fixes and improvements!", + "feature1": "1-Click Task Sharing: Share your tasks instantly with colleagues and the community with a single click.", + "feature2": "Global .roo Directory Support: Load rules and configurations from a global .roo directory for consistent settings across projects.", + "feature3": "Improved Architect to Code Transitions: Seamless handoffs from planning in Architect mode to implementation in Code mode.", "hideButton": "Hide announcement", "detailsDiscussLinks": "Get more details and discuss in Discord and Reddit 🚀" }, @@ -257,7 +258,9 @@ "seconds": "{{count}}s" }, "followUpSuggest": { - "copyToInput": "Copy to input (same as shift + click)" + "copyToInput": "Copy to input (same as shift + click)", + "autoSelectCountdown": "Auto-selecting in {{count}}s", + "countdownDisplay": "{{count}}s" }, "browser": { "rooWantsToUse": "Roo wants to use the browser:", @@ -310,5 +313,8 @@ "indexed": "Indexed", "error": "Index error", "status": "Index status" + }, + "versionIndicator": { + "ariaLabel": "Version {{version}} - Click to view release notes" } } diff --git a/webview-ui/src/i18n/locales/en/prompts.json b/webview-ui/src/i18n/locales/en/prompts.json index 3614d79872..df13c8773f 100644 --- a/webview-ui/src/i18n/locales/en/prompts.json +++ b/webview-ui/src/i18n/locales/en/prompts.json @@ -4,11 +4,13 @@ "modes": { "title": "Modes", "createNewMode": "Create new mode", + "importMode": "Import Mode", "editModesConfig": "Edit modes configuration", "editGlobalModes": "Edit Global Modes", "editProjectModes": "Edit Project Modes (.roomodes)", "createModeHelpText": "Modes are specialized personas that tailor Roo's behavior. <0>Learn about Using Modes or <1>Customizing Modes.", - "selectMode": "Search modes" + "selectMode": "Search modes", + "noMatchFound": "No modes found" }, "apiConfiguration": { "title": "API Configuration", @@ -50,6 +52,27 @@ "description": "Add behavioral guidelines specific to {{modeName}} mode.", "loadFromFile": "Custom instructions specific to {{mode}} mode can also be loaded from the .roo/rules-{{slug}}/ folder in your workspace (.roorules-{{slug}} and .clinerules-{{slug}} are deprecated and will stop working soon)." }, + "exportMode": { + "title": "Export Mode", + "description": "Export this mode with rules from the .roo/rules-{{slug}}/ folder combined into a shareable YAML file. The original files remain unchanged.", + "exporting": "Exporting..." + }, + "importMode": { + "selectLevel": "Choose where to import this mode:", + "import": "Import", + "importing": "Importing...", + "global": { + "label": "Global Level", + "description": "Available across all projects. If the exported mode contained rules files, they will be recreated in the global .roo/rules-{slug}/ folder." + }, + "project": { + "label": "Project Level", + "description": "Only available in this workspace. If the exported mode contained rules files, they will be recreated in .roo/rules-{slug}/ folder." + } + }, + "advanced": { + "title": "Advanced: Override System Prompt" + }, "globalCustomInstructions": { "title": "Custom Instructions for All Modes", "description": "These instructions apply to all modes. They provide a base set of behaviors that can be enhanced by mode-specific instructions below. <0>Learn more", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 14981ea499..b18f526e15 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant Key:", + "advancedConfigLabel": "Advanced Configuration", + "searchMinScoreLabel": "Search Score Threshold", + "searchMinScoreDescription": "Minimum similarity score (0.0-1.0) required for search results. Lower values return more results but may be less relevant. Higher values return fewer but more relevant results.", + "searchMinScoreResetTooltip": "Reset to default value (0.4)", "startIndexingButton": "Start Indexing", "clearIndexDataButton": "Clear Index Data", "unsavedSettingsMessage": "Please save your settings before starting the indexing process.", @@ -110,6 +114,11 @@ "label": "Subtasks", "description": "Allow creation and completion of subtasks without requiring approval" }, + "followupQuestions": { + "label": "Question", + "description": "Automatically select the first suggested answer for follow-up questions after the configured timeout", + "timeoutLabel": "Time to wait before auto-selecting the first answer" + }, "execute": { "label": "Execute", "description": "Automatically execute allowed terminal commands without requiring approval", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 7072d0ea16..a4349b13dc 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -234,15 +234,17 @@ "tokens": "tokens" }, "followUpSuggest": { - "copyToInput": "Copiar a la entrada (o Shift + clic)" + "copyToInput": "Copiar a la entrada (o Shift + clic)", + "autoSelectCountdown": "Selección automática en {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { "title": "🎉 Roo Code {{version}} publicado", - "description": "Roo Code {{version}} trae importantes nuevas funcionalidades y mejoras basadas en tus comentarios.", + "description": "Roo Code {{version}} trae poderosas nuevas funcionalidades y mejoras significativas para mejorar tu flujo de trabajo de desarrollo.", "whatsNew": "Novedades", - "feature1": "Lanzamiento del Marketplace de Roo: ¡El marketplace ya está disponible! Descubre e instala modos y MCPs más fácil que nunca.", - "feature2": "Modelos Gemini 2.5: Se agregó soporte para los nuevos modelos Gemini 2.5 Pro, Flash y Flash Lite.", - "feature3": "Soporte de archivos Excel y más: ¡Se agregó soporte para archivos Excel (.xlsx) y numerosas correcciones de errores y mejoras!", + "feature1": "Compartir tareas con 1 clic: Comparte tus tareas instantáneamente con colegas y la comunidad con un solo clic.", + "feature2": "Soporte de directorio .roo global: Carga reglas y configuraciones desde un directorio .roo global para configuraciones consistentes entre proyectos.", + "feature3": "Transiciones mejoradas de Arquitecto a Código: Transferencias fluidas desde la planificación en modo Arquitecto hasta la implementación en modo Código.", "hideButton": "Ocultar anuncio", "detailsDiscussLinks": "Obtén más detalles y participa en Discord y Reddit 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo quiere buscar en la base de código {{query}}:", "wantsToSearchWithPath": "Roo quiere buscar en la base de código {{query}} en {{path}}:", - "didSearch": "Se encontraron {{count}} resultado(s) para {{query}}:" + "didSearch": "Se encontraron {{count}} resultado(s) para {{query}}:", + "resultTooltip": "Puntuación de similitud: {{score}} (haz clic para abrir el archivo)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Indexado", "error": "Error de índice", "status": "Estado del índice" + }, + "versionIndicator": { + "ariaLabel": "Versión {{version}} - Haz clic para ver las notas de la versión" } } diff --git a/webview-ui/src/i18n/locales/es/prompts.json b/webview-ui/src/i18n/locales/es/prompts.json index 54b5c1bd2d..67a4d25b8a 100644 --- a/webview-ui/src/i18n/locales/es/prompts.json +++ b/webview-ui/src/i18n/locales/es/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modos", "createNewMode": "Crear nuevo modo", + "importMode": "Importar modo", + "noMatchFound": "No se encontraron modos", "editModesConfig": "Editar configuración de modos", "editGlobalModes": "Editar modos globales", "editProjectModes": "Editar modos del proyecto (.roomodes)", @@ -50,6 +52,28 @@ "description": "Agrega directrices de comportamiento específicas para el modo {{modeName}}.", "loadFromFile": "Las instrucciones personalizadas para el modo {{mode}} también se pueden cargar desde la carpeta .roo/rules-{{slug}}/ en tu espacio de trabajo (.roorules-{{slug}} y .clinerules-{{slug}} están obsoletos y dejarán de funcionar pronto)." }, + "exportMode": { + "title": "Exportar modo", + "description": "Exporta este modo a un archivo YAML con todas las reglas incluidas para compartir fácilmente con otros.", + "export": "Exportar modo", + "exporting": "Exportando..." + }, + "importMode": { + "selectLevel": "Elige dónde importar este modo:", + "import": "Importar", + "importing": "Importando...", + "global": { + "label": "Nivel global", + "description": "Disponible en todos los proyectos. Si el modo exportado contenía archivos de reglas, se volverán a crear en la carpeta global .roo/rules-{slug}/." + }, + "project": { + "label": "Nivel de proyecto", + "description": "Solo disponible en este espacio de trabajo. Si el modo exportado contenía archivos de reglas, se volverán a crear en la carpeta .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Avanzado" + }, "globalCustomInstructions": { "title": "Instrucciones personalizadas para todos los modos", "description": "Estas instrucciones se aplican a todos los modos. Proporcionan un conjunto base de comportamientos que pueden ser mejorados por instrucciones específicas de cada modo a continuación. <0>Más información", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 3e3d20cce1..92d12b4e24 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL de Ollama:", "qdrantUrlLabel": "URL de Qdrant", "qdrantKeyLabel": "Clave de Qdrant:", + "advancedConfigLabel": "Configuración avanzada", + "searchMinScoreLabel": "Umbral de puntuación de búsqueda", + "searchMinScoreDescription": "Puntuación mínima de similitud (0.0-1.0) requerida para los resultados de búsqueda. Valores más bajos devuelven más resultados pero pueden ser menos relevantes. Valores más altos devuelven menos resultados pero más relevantes.", + "searchMinScoreResetTooltip": "Restablecer al valor predeterminado (0.4)", "startIndexingButton": "Iniciar indexación", "clearIndexDataButton": "Borrar datos de índice", "unsavedSettingsMessage": "Por favor guarda tus ajustes antes de iniciar el proceso de indexación.", @@ -110,6 +114,11 @@ "label": "Subtareas", "description": "Permitir la creación y finalización de subtareas sin requerir aprobación" }, + "followupQuestions": { + "label": "Pregunta", + "description": "Seleccionar automáticamente la primera respuesta sugerida para preguntas de seguimiento después del tiempo de espera configurado", + "timeoutLabel": "Tiempo de espera antes de seleccionar automáticamente la primera respuesta" + }, "execute": { "label": "Ejecutar", "description": "Ejecutar automáticamente comandos de terminal permitidos sin requerir aprobación", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 25d5074f45..b5f06354bb 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -234,15 +234,17 @@ "tokens": "tokens" }, "followUpSuggest": { - "copyToInput": "Copier vers l'entrée (ou Shift + clic)" + "copyToInput": "Copier vers l'entrée (ou Shift + clic)", + "autoSelectCountdown": "Sélection automatique dans {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { "title": "🎉 Roo Code {{version}} est sortie", - "description": "Roo Code {{version}} apporte de nouvelles fonctionnalités majeures et des améliorations basées sur vos retours.", + "description": "Roo Code {{version}} apporte de puissantes nouvelles fonctionnalités et des améliorations significatives pour améliorer ton flux de travail de développement.", "whatsNew": "Quoi de neuf", - "feature1": "Lancement du Marketplace Roo : Le marketplace est maintenant en ligne ! Découvrez et installez des modes et des MCPs plus facilement que jamais.", - "feature2": "Modèles Gemini 2.5 : Ajout du support pour les nouveaux modèles Gemini 2.5 Pro, Flash et Flash Lite.", - "feature3": "Support des fichiers Excel et plus : Ajout du support des fichiers Excel (.xlsx) et de nombreuses corrections de bugs et améliorations !", + "feature1": "Partage de tâches en 1 clic : Partage tes tâches instantanément avec tes collègues et la communauté en un seul clic.", + "feature2": "Support du répertoire .roo global : Charge les règles et configurations depuis un répertoire .roo global pour des paramètres cohérents entre les projets.", + "feature3": "Transitions Architecte vers Code améliorées : Transferts fluides de la planification en mode Architecte vers l'implémentation en mode Code.", "hideButton": "Masquer l'annonce", "detailsDiscussLinks": "Obtenez plus de détails et participez aux discussions sur Discord et Reddit 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo veut rechercher dans la base de code {{query}} :", "wantsToSearchWithPath": "Roo veut rechercher dans la base de code {{query}} dans {{path}} :", - "didSearch": "{{count}} résultat(s) trouvé(s) pour {{query}} :" + "didSearch": "{{count}} résultat(s) trouvé(s) pour {{query}} :", + "resultTooltip": "Score de similarité : {{score}} (cliquer pour ouvrir le fichier)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Indexé", "error": "Erreur d'index", "status": "Statut de l'index" + }, + "versionIndicator": { + "ariaLabel": "Version {{version}} - Cliquez pour voir les notes de version" } } diff --git a/webview-ui/src/i18n/locales/fr/prompts.json b/webview-ui/src/i18n/locales/fr/prompts.json index 39bc67e482..202e214322 100644 --- a/webview-ui/src/i18n/locales/fr/prompts.json +++ b/webview-ui/src/i18n/locales/fr/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modes", "createNewMode": "Créer un nouveau mode", + "importMode": "Importer le mode", + "noMatchFound": "Aucun mode trouvé", "editModesConfig": "Modifier la configuration des modes", "editGlobalModes": "Modifier les modes globaux", "editProjectModes": "Modifier les modes du projet (.roomodes)", @@ -50,6 +52,28 @@ "description": "Ajoutez des directives comportementales spécifiques au mode {{modeName}}.", "loadFromFile": "Les instructions personnalisées spécifiques au mode {{mode}} peuvent également être chargées depuis le dossier .roo/rules-{{slug}}/ dans votre espace de travail (.roorules-{{slug}} et .clinerules-{{slug}} sont obsolètes et cesseront de fonctionner bientôt)." }, + "exportMode": { + "title": "Exporter le mode", + "description": "Exporte ce mode vers un fichier YAML avec toutes les règles incluses pour un partage facile avec d'autres.", + "export": "Exporter le mode", + "exporting": "Exportation..." + }, + "importMode": { + "selectLevel": "Choisissez où importer ce mode :", + "import": "Importer", + "importing": "Importation...", + "global": { + "label": "Niveau global", + "description": "Disponible dans tous les projets. Si le mode exporté contenait des fichiers de règles, ils seront recréés dans le dossier global .roo/rules-{slug}/." + }, + "project": { + "label": "Niveau projet", + "description": "Disponible uniquement dans cet espace de travail. Si le mode exporté contenait des fichiers de règles, ils seront recréés dans le dossier .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Avancé" + }, "globalCustomInstructions": { "title": "Instructions personnalisées pour tous les modes", "description": "Ces instructions s'appliquent à tous les modes. Elles fournissent un ensemble de comportements de base qui peuvent être améliorés par des instructions spécifiques au mode ci-dessous. <0>En savoir plus", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 5635251876..7c3f38b7b3 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL Ollama :", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Clé Qdrant :", + "advancedConfigLabel": "Configuration avancée", + "searchMinScoreLabel": "Seuil de score de recherche", + "searchMinScoreDescription": "Score de similarité minimum (0.0-1.0) requis pour les résultats de recherche. Des valeurs plus faibles renvoient plus de résultats mais peuvent être moins pertinents. Des valeurs plus élevées renvoient moins de résultats mais plus pertinents.", + "searchMinScoreResetTooltip": "Réinitialiser à la valeur par défaut (0.4)", "startIndexingButton": "Démarrer l'indexation", "clearIndexDataButton": "Effacer les données d'index", "unsavedSettingsMessage": "Merci d'enregistrer tes paramètres avant de démarrer le processus d'indexation.", @@ -110,6 +114,11 @@ "label": "Sous-tâches", "description": "Permettre la création et l'achèvement des sous-tâches sans nécessiter d'approbation" }, + "followupQuestions": { + "label": "Question", + "description": "Sélectionner automatiquement la première réponse suggérée pour les questions de suivi après le délai configuré", + "timeoutLabel": "Temps d'attente avant la sélection automatique de la première réponse" + }, "execute": { "label": "Exécuter", "description": "Exécuter automatiquement les commandes de terminal autorisées sans nécessiter d'approbation", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index e67068d090..9afdcbdd38 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -234,17 +234,19 @@ "tokens": "टोकन" }, "followUpSuggest": { - "copyToInput": "इनपुट में कॉपी करें (या Shift + क्लिक)" + "copyToInput": "इनपुट में कॉपी करें (या Shift + क्लिक)", + "autoSelectCountdown": "{{count}}s में स्वचालित रूप से चयन हो रहा है", + "countdownDisplay": "{{count}}सेकंड" }, "announcement": { "title": "🎉 Roo Code {{version}} रिलीज़ हुआ", - "description": "Roo Code {{version}} आपके फीडबैक के आधार पर शक्तिशाली नई सुविधाएँ और सुधार लाता है।", - "whatsNew": "नई सुविधाएँ", - "feature1": "Roo Marketplace लॉन्च - Marketplace अब लाइव है! पहले से कहीं आसान तरीके से modes और MCP खोजें और इंस्टॉल करें।", - "feature2": "Gemini 2.5 Models - नए Gemini 2.5 Pro, Flash, और Flash Lite models के लिए समर्थन जोड़ा गया।", - "feature3": "Excel File समर्थन और अधिक - Excel (.xlsx) file समर्थन जोड़ा गया और कई bug fixes और सुधार!", - "hideButton": "घोषणा छिपाएँ", - "detailsDiscussLinks": "Discord और Reddit पर अधिक जानकारी प्राप्त करें और चर्चा में भाग लें 🚀" + "description": "Roo Code {{version}} आपके विकास वर्कफ़्लो को बेहतर बनाने के लिए शक्तिशाली नई सुविधाएं और महत्वपूर्ण सुधार लेकर आया है।", + "whatsNew": "नया क्या है", + "feature1": "1-क्लिक टास्क शेयरिंग: अपने टास्क को सहकर्मियों और समुदाय के साथ एक क्लिक में तुरंत साझा करें।", + "feature2": "ग्लोबल .roo डायरेक्टरी समर्थन: प्रोजेक्ट्स में निरंतर सेटिंग्स के लिए ग्लोबल .roo डायरेक्टरी से नियम और कॉन्फ़िगरेशन लोड करें।", + "feature3": "बेहतर आर्किटेक्ट से कोड ट्रांज़िशन: आर्किटेक्ट मोड में प्लानिंग से कोड मोड में इम्प्लीमेंटेशन तक सहज स्थानांतरण।", + "hideButton": "घोषणा छुपाएं", + "detailsDiscussLinks": "Discord और Reddit पर अधिक विवरण प्राप्त करें और चर्चाओं में शामिल हों 🚀" }, "browser": { "rooWantsToUse": "Roo ब्राउज़र का उपयोग करना चाहता है:", @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo कोडबेस में {{query}} खोजना चाहता है:", "wantsToSearchWithPath": "Roo {{path}} में कोडबेस में {{query}} खोजना चाहता है:", - "didSearch": "{{query}} के लिए {{count}} परिणाम मिले:" + "didSearch": "{{query}} के लिए {{count}} परिणाम मिले:", + "resultTooltip": "समानता स्कोर: {{score}} (फ़ाइल खोलने के लिए क्लिक करें)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "इंडेक्स किया गया", "error": "इंडेक्स त्रुटि", "status": "इंडेक्स स्थिति" + }, + "versionIndicator": { + "ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें" } } diff --git a/webview-ui/src/i18n/locales/hi/prompts.json b/webview-ui/src/i18n/locales/hi/prompts.json index 9633b02953..ea1c4b6ae1 100644 --- a/webview-ui/src/i18n/locales/hi/prompts.json +++ b/webview-ui/src/i18n/locales/hi/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "मोड्स", "createNewMode": "नया मोड बनाएँ", + "importMode": "मोड आयात करें", + "noMatchFound": "कोई मोड नहीं मिला", "editModesConfig": "मोड कॉन्फ़िगरेशन संपादित करें", "editGlobalModes": "ग्लोबल मोड्स संपादित करें", "editProjectModes": "प्रोजेक्ट मोड्स संपादित करें (.roomodes)", @@ -50,6 +52,28 @@ "description": "{{modeName}} मोड के लिए विशिष्ट व्यवहार दिशानिर्देश जोड़ें।", "loadFromFile": "{{mode}} मोड के लिए विशिष्ट कस्टम निर्देश आपके वर्कस्पेस में .roo/rules-{{slug}}/ फ़ोल्डर से भी लोड किए जा सकते हैं (.roorules-{{slug}} और .clinerules-{{slug}} पुराने हो गए हैं और जल्द ही काम करना बंद कर देंगे)।" }, + "exportMode": { + "title": "मोड निर्यात करें", + "description": "इस मोड को सभी नियमों के साथ एक YAML फ़ाइल में निर्यात करें ताकि दूसरों के साथ आसानी से साझा किया जा सके।", + "export": "मोड निर्यात करें", + "exporting": "निर्यात हो रहा है..." + }, + "importMode": { + "selectLevel": "चुनें कि इस मोड को कहाँ आयात करना है:", + "import": "आयात करें", + "importing": "आयात कर रहे हैं...", + "global": { + "label": "वैश्विक स्तर", + "description": "सभी परियोजनाओं में उपलब्ध। नियम कस्टम निर्देशों में विलय कर दिए जाएंगे।" + }, + "project": { + "label": "परियोजना स्तर", + "description": "केवल इस कार्यक्षेत्र में उपलब्ध। यदि निर्यात किए गए मोड में नियम फाइलें थीं, तो उन्हें .roo/rules-{slug}/ फ़ोल्डर में फिर से बनाया जाएगा।" + } + }, + "advanced": { + "title": "उन्नत" + }, "globalCustomInstructions": { "title": "सभी मोड्स के लिए कस्टम निर्देश", "description": "ये निर्देश सभी मोड्स पर लागू होते हैं। वे व्यवहारों का एक आधार सेट प्रदान करते हैं जिन्हें नीचे दिए गए मोड-विशिष्ट निर्देशों द्वारा बढ़ाया जा सकता है। <0>और जानें", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 8de5bd1a19..36cfc01896 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant कुंजी:", + "advancedConfigLabel": "उन्नत कॉन्फ़िगरेशन", + "searchMinScoreLabel": "खोज स्कोर थ्रेसहोल्ड", + "searchMinScoreDescription": "खोज परिणामों के लिए आवश्यक न्यूनतम समानता स्कोर (0.0-1.0)। कम मान अधिक परिणाम लौटाते हैं लेकिन कम प्रासंगिक हो सकते हैं। उच्च मान कम लेकिन अधिक प्रासंगिक परिणाम लौटाते हैं।", + "searchMinScoreResetTooltip": "डिफ़ॉल्ट मान पर रीसेट करें (0.4)", "startIndexingButton": "इंडेक्सिंग शुरू करें", "clearIndexDataButton": "इंडेक्स डेटा साफ़ करें", "unsavedSettingsMessage": "इंडेक्सिंग प्रक्रिया शुरू करने से पहले कृपया अपनी सेटिंग्स सहेजें।", @@ -110,6 +114,11 @@ "label": "उप-कार्य", "description": "अनुमोदन की आवश्यकता के बिना उप-कार्यों के निर्माण और पूर्णता की अनुमति दें" }, + "followupQuestions": { + "label": "प्रश्न", + "description": "कॉन्फ़िगर किए गए टाइमआउट के बाद अनुवर्ती प्रश्नों के लिए पहले सुझाए गए उत्तर को स्वचालित रूप से चुनें", + "timeoutLabel": "पहले उत्तर को स्वचालित रूप से चुनने से पहले प्रतीक्षा करने का समय" + }, "execute": { "label": "निष्पादित करें", "description": "अनुमोदन की आवश्यकता के बिना स्वचालित रूप से अनुमत टर्मिनल कमांड निष्पादित करें", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 259665a406..fc0bd5056f 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -207,7 +207,8 @@ "codebaseSearch": { "wantsToSearch": "Roo ingin mencari codebase untuk {{query}}:", "wantsToSearchWithPath": "Roo ingin mencari codebase untuk {{query}} di {{path}}:", - "didSearch": "Ditemukan {{count}} hasil untuk {{query}}:" + "didSearch": "Ditemukan {{count}} hasil untuk {{query}}:", + "resultTooltip": "Skor kemiripan: {{score}} (klik untuk membuka file)" }, "commandOutput": "Output Perintah", "response": "Respons", @@ -250,20 +251,22 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Dirilis", - "description": "Roo Code {{version}} menghadirkan fitur baru yang powerful dan perbaikan berdasarkan feedback kamu.", - "whatsNew": "Apa yang Baru", - "feature1": "Peluncuran Roo Marketplace - Marketplace sekarang sudah live! Temukan dan install mode serta MCP lebih mudah dari sebelumnya.", - "feature2": "Model Gemini 2.5 - Menambahkan dukungan untuk model Gemini 2.5 Pro, Flash, dan Flash Lite yang baru.", - "feature3": "Dukungan File Excel & Lainnya - Menambahkan dukungan file Excel (.xlsx) dan banyak perbaikan bug serta peningkatan!", + "description": "Roo Code {{version}} menghadirkan fitur-fitur baru yang kuat dan peningkatan signifikan untuk meningkatkan alur kerja pengembangan Anda.", + "whatsNew": "Yang Baru", + "feature1": "Berbagi Tugas 1-Klik: Bagikan tugas Anda secara instan dengan rekan kerja dan komunitas hanya dengan satu klik.", + "feature2": "Dukungan Direktori Global .roo: Muat aturan dan konfigurasi dari direktori global .roo untuk pengaturan yang konsisten di seluruh proyek.", + "feature3": "Transisi Arsitektur ke Kode yang Ditingkatkan: Transfer yang mulus dari perencanaan di mode Arsitektur ke implementasi di mode Kode.", "hideButton": "Sembunyikan pengumuman", - "detailsDiscussLinks": "Dapatkan detail lebih lanjut dan diskusi di Discord dan Reddit 🚀" + "detailsDiscussLinks": "Dapatkan detail lebih lanjut dan bergabung dalam diskusi di Discord dan Reddit 🚀" }, "reasoning": { "thinking": "Berpikir", "seconds": "{{count}}d" }, "followUpSuggest": { - "copyToInput": "Salin ke input (sama dengan shift + klik)" + "copyToInput": "Salin ke input (sama dengan shift + klik)", + "autoSelectCountdown": "Pemilihan otomatis dalam {{count}}dtk", + "countdownDisplay": "{{count}}dtk" }, "browser": { "rooWantsToUse": "Roo ingin menggunakan browser:", @@ -316,5 +319,8 @@ "indexed": "Terindeks", "error": "Error indeks", "status": "Status indeks" + }, + "versionIndicator": { + "ariaLabel": "Versi {{version}} - Klik untuk melihat catatan rilis" } } diff --git a/webview-ui/src/i18n/locales/id/prompts.json b/webview-ui/src/i18n/locales/id/prompts.json index a77a6e5376..bdebc4eb73 100644 --- a/webview-ui/src/i18n/locales/id/prompts.json +++ b/webview-ui/src/i18n/locales/id/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Mode", "createNewMode": "Buat mode baru", + "importMode": "Impor mode", + "noMatchFound": "Tidak ada mode yang ditemukan", "editModesConfig": "Edit konfigurasi mode", "editGlobalModes": "Edit Mode Global", "editProjectModes": "Edit Mode Proyek (.roomodes)", @@ -50,6 +52,28 @@ "description": "Tambahkan panduan perilaku khusus untuk mode {{modeName}}.", "loadFromFile": "Instruksi kustom khusus untuk mode {{mode}} juga dapat dimuat dari folder .roo/rules-{{slug}}/ di workspace Anda (.roomodes-{{slug}} dan .clinerules-{{slug}} sudah deprecated dan akan segera berhenti bekerja)." }, + "exportMode": { + "title": "Ekspor Mode", + "description": "Ekspor mode ini ke file YAML dengan semua aturan yang disertakan untuk berbagi dengan mudah dengan orang lain.", + "export": "Ekspor Mode", + "exporting": "Mengekspor..." + }, + "importMode": { + "selectLevel": "Pilih di mana akan mengimpor mode ini:", + "import": "Impor", + "importing": "Mengimpor...", + "global": { + "label": "Tingkat Global", + "description": "Tersedia di semua proyek. Aturan akan digabungkan ke dalam instruksi kustom." + }, + "project": { + "label": "Tingkat Proyek", + "description": "Hanya tersedia di ruang kerja ini. Jika mode yang diekspor berisi file aturan, file tersebut akan dibuat ulang di folder .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Lanjutan" + }, "globalCustomInstructions": { "title": "Instruksi Kustom untuk Semua Mode", "description": "Instruksi ini berlaku untuk semua mode. Mereka menyediakan set dasar perilaku yang dapat ditingkatkan oleh instruksi khusus mode di bawah. <0>Pelajari lebih lanjut", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 8def303c05..08a23cd30c 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant Key:", + "advancedConfigLabel": "Konfigurasi Lanjutan", + "searchMinScoreLabel": "Ambang Batas Skor Pencarian", + "searchMinScoreDescription": "Skor kesamaan minimum (0.0-1.0) yang diperlukan untuk hasil pencarian. Nilai yang lebih rendah mengembalikan lebih banyak hasil tetapi mungkin kurang relevan. Nilai yang lebih tinggi mengembalikan lebih sedikit hasil tetapi lebih relevan.", + "searchMinScoreResetTooltip": "Reset ke nilai default (0.4)", "startIndexingButton": "Mulai Pengindeksan", "clearIndexDataButton": "Hapus Data Indeks", "unsavedSettingsMessage": "Silakan simpan pengaturan kamu sebelum memulai proses pengindeksan.", @@ -110,6 +114,11 @@ "label": "Subtugas", "description": "Izinkan pembuatan dan penyelesaian subtugas tanpa memerlukan persetujuan" }, + "followupQuestions": { + "label": "Pertanyaan", + "description": "Secara otomatis memilih jawaban pertama yang disarankan untuk pertanyaan lanjutan setelah batas waktu yang dikonfigurasi", + "timeoutLabel": "Waktu tunggu sebelum otomatis memilih jawaban pertama" + }, "execute": { "label": "Eksekusi", "description": "Secara otomatis mengeksekusi perintah terminal yang diizinkan tanpa memerlukan persetujuan", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index bbf8c8be6a..f49a25dfa6 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -234,15 +234,17 @@ "tokens": "token" }, "followUpSuggest": { - "copyToInput": "Copia nell'input (o Shift + clic)" + "copyToInput": "Copia nell'input (o Shift + clic)", + "autoSelectCountdown": "Selezione automatica in {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { "title": "🎉 Rilasciato Roo Code {{version}}", - "description": "Roo Code {{version}} introduce importanti nuove funzionalità e miglioramenti basati sui tuoi feedback.", + "description": "Roo Code {{version}} porta nuove potenti funzionalità e miglioramenti significativi per potenziare il tuo flusso di lavoro di sviluppo.", "whatsNew": "Novità", - "feature1": "Lancio del Marketplace Roo: Il marketplace è ora attivo! Scopri e installa modalità e MCP più facilmente che mai.", - "feature2": "Modelli Gemini 2.5: Aggiunto supporto per i nuovi modelli Gemini 2.5 Pro, Flash e Flash Lite.", - "feature3": "Supporto File Excel e altro: Aggiunto supporto per file Excel (.xlsx) e numerose correzioni di bug e miglioramenti!", + "feature1": "Condivisione Task con 1 Clic: Condividi istantaneamente i tuoi task con colleghi e la community con un solo clic.", + "feature2": "Supporto Directory Globale .roo: Carica regole e configurazioni da una directory globale .roo per impostazioni coerenti tra progetti.", + "feature3": "Transizioni Migliorate da Architetto a Codice: Trasferimenti fluidi dalla pianificazione in modalità Architetto all'implementazione in modalità Codice.", "hideButton": "Nascondi annuncio", "detailsDiscussLinks": "Ottieni maggiori dettagli e partecipa alle discussioni su Discord e Reddit 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo vuole cercare nella base di codice {{query}}:", "wantsToSearchWithPath": "Roo vuole cercare nella base di codice {{query}} in {{path}}:", - "didSearch": "Trovato {{count}} risultato/i per {{query}}:" + "didSearch": "Trovato {{count}} risultato/i per {{query}}:", + "resultTooltip": "Punteggio di somiglianza: {{score}} (clicca per aprire il file)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Indicizzato", "error": "Errore indice", "status": "Stato indice" + }, + "versionIndicator": { + "ariaLabel": "Versione {{version}} - Clicca per visualizzare le note di rilascio" } } diff --git a/webview-ui/src/i18n/locales/it/prompts.json b/webview-ui/src/i18n/locales/it/prompts.json index c556a18aac..5f6fd165aa 100644 --- a/webview-ui/src/i18n/locales/it/prompts.json +++ b/webview-ui/src/i18n/locales/it/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modalità", "createNewMode": "Crea nuova modalità", + "importMode": "Importa modalità", + "noMatchFound": "Nessuna modalità trovata", "editModesConfig": "Modifica configurazione modalità", "editGlobalModes": "Modifica modalità globali", "editProjectModes": "Modifica modalità di progetto (.roomodes)", @@ -50,6 +52,28 @@ "description": "Aggiungi linee guida comportamentali specifiche per la modalità {{modeName}}.", "loadFromFile": "Le istruzioni personalizzate specifiche per la modalità {{mode}} possono essere caricate anche dalla cartella .roo/rules-{{slug}}/ nel tuo spazio di lavoro (.roorules-{{slug}} e .clinerules-{{slug}} sono obsoleti e smetteranno di funzionare presto)." }, + "exportMode": { + "title": "Esporta modalità", + "description": "Esporta questa modalità in un file YAML con tutte le regole incluse per una facile condivisione con altri.", + "export": "Esporta modalità", + "exporting": "Esportazione..." + }, + "importMode": { + "selectLevel": "Scegli dove importare questa modalità:", + "import": "Importa", + "importing": "Importazione...", + "global": { + "label": "Livello globale", + "description": "Disponibile in tutti i progetti. Le regole verranno unite nelle istruzioni personalizzate." + }, + "project": { + "label": "Livello di progetto", + "description": "Disponibile solo in questo spazio di lavoro. Se la modalità esportata conteneva file di regole, verranno ricreati nella cartella .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Avanzato" + }, "globalCustomInstructions": { "title": "Istruzioni personalizzate per tutte le modalità", "description": "Queste istruzioni si applicano a tutte le modalità. Forniscono un insieme base di comportamenti che possono essere migliorati dalle istruzioni specifiche per modalità qui sotto. <0>Scopri di più", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 572f99cbb0..4897212d0f 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL Ollama:", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Chiave Qdrant:", + "advancedConfigLabel": "Configurazione avanzata", + "searchMinScoreLabel": "Soglia punteggio di ricerca", + "searchMinScoreDescription": "Punteggio minimo di somiglianza (0.0-1.0) richiesto per i risultati della ricerca. Valori più bassi restituiscono più risultati ma potrebbero essere meno pertinenti. Valori più alti restituiscono meno risultati ma più pertinenti.", + "searchMinScoreResetTooltip": "Ripristina al valore predefinito (0.4)", "startIndexingButton": "Avvia indicizzazione", "clearIndexDataButton": "Cancella dati indice", "unsavedSettingsMessage": "Per favore salva le tue impostazioni prima di avviare il processo di indicizzazione.", @@ -110,6 +114,11 @@ "label": "Sottoattività", "description": "Consenti la creazione e il completamento di attività secondarie senza richiedere approvazione" }, + "followupQuestions": { + "label": "Domanda", + "description": "Seleziona automaticamente la prima risposta suggerita per le domande di follow-up dopo il timeout configurato", + "timeoutLabel": "Tempo di attesa prima di selezionare automaticamente la prima risposta" + }, "execute": { "label": "Esegui", "description": "Esegui automaticamente i comandi del terminale consentiti senza richiedere approvazione", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 0278edd4b6..cb5ebcdafd 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -234,15 +234,17 @@ "tokens": "トークン" }, "followUpSuggest": { - "copyToInput": "入力欄にコピー(またはShift + クリック)" + "copyToInput": "入力欄にコピー(またはShift + クリック)", + "autoSelectCountdown": "{{count}}秒後に自動選択します", + "countdownDisplay": "{{count}}秒" }, "announcement": { "title": "🎉 Roo Code {{version}} リリース", - "description": "Roo Code {{version}}は、あなたのフィードバックに基づく重要な新機能と改善をもたらします。", + "description": "Roo Code {{version}}は、開発ワークフローを向上させる強力な新機能と重要な改善をもたらします。", "whatsNew": "新機能", - "feature1": "Roo マーケットプレイス開始: マーケットプレイスが開始されました!これまで以上に簡単にモードとMCPを発見・インストール。", - "feature2": "Gemini 2.5 モデル: 新しいGemini 2.5 Pro、Flash、Flash Liteモデルのサポートを追加。", - "feature3": "Excelファイルサポートなど: Excel (.xlsx) ファイルサポートと多数のバグ修正・改善を追加!", + "feature1": "1クリックタスク共有: ワンクリックで同僚やコミュニティとタスクを瞬時に共有できます。", + "feature2": "グローバル.rooディレクトリサポート: グローバル.rooディレクトリからルールと設定を読み込み、プロジェクト間で一貫した設定を実現。", + "feature3": "改善されたアーキテクトからコードへの移行: アーキテクトモードでの計画からコードモードでの実装へのシームレスな引き継ぎ。", "hideButton": "通知を非表示", "detailsDiscussLinks": "詳細はDiscordRedditでご確認・ディスカッションください 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Rooはコードベースで {{query}} を検索したい:", "wantsToSearchWithPath": "Rooは {{path}} 内のコードベースで {{query}} を検索したい:", - "didSearch": "{{query}} の検索結果: {{count}} 件" + "didSearch": "{{query}} の検索結果: {{count}} 件", + "resultTooltip": "類似度スコア: {{score}} (クリックしてファイルを開く)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "インデックス作成済み", "error": "インデックスエラー", "status": "インデックス状態" + }, + "versionIndicator": { + "ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示" } } diff --git a/webview-ui/src/i18n/locales/ja/prompts.json b/webview-ui/src/i18n/locales/ja/prompts.json index 8049a82d31..1fed98d194 100644 --- a/webview-ui/src/i18n/locales/ja/prompts.json +++ b/webview-ui/src/i18n/locales/ja/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "モード", "createNewMode": "新しいモードを作成", + "importMode": "モードをインポート", + "noMatchFound": "モードが見つかりません", "editModesConfig": "モード設定を編集", "editGlobalModes": "グローバルモードを編集", "editProjectModes": "プロジェクトモードを編集 (.roomodes)", @@ -50,6 +52,28 @@ "description": "{{modeName}}モードに特化した行動ガイドラインを追加します。", "loadFromFile": "{{mode}}モード固有のカスタム指示は、ワークスペースの.roo/rules-{{slug}}/フォルダからも読み込めます(.roorules-{{slug}}と.clinerules-{{slug}}は非推奨であり、まもなく機能しなくなります)。" }, + "exportMode": { + "title": "モードをエクスポート", + "description": "このモードをすべてのルールを含むYAMLファイルにエクスポートして、他のユーザーと簡単に共有できます。", + "export": "モードをエクスポート", + "exporting": "エクスポート中..." + }, + "importMode": { + "selectLevel": "このモードをインポートする場所を選択してください:", + "import": "インポート", + "importing": "インポート中...", + "global": { + "label": "グローバルレベル", + "description": "すべてのプロジェクトで利用可能です。ルールはカスタム指示にマージされます。" + }, + "project": { + "label": "プロジェクトレベル", + "description": "このワークスペースでのみ利用可能です。エクスポートされたモードにルールファイルが含まれていた場合、それらは.roo/rules-{slug}/フォルダに再作成されます。" + } + }, + "advanced": { + "title": "詳細設定" + }, "globalCustomInstructions": { "title": "すべてのモードのカスタム指示", "description": "これらの指示はすべてのモードに適用されます。モード固有の指示で強化できる基本的な動作セットを提供します。<0>詳細はこちら", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index c6a681e549..4165726ade 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrantキー:", + "advancedConfigLabel": "詳細設定", + "searchMinScoreLabel": "検索スコアのしきい値", + "searchMinScoreDescription": "検索結果に必要な最小類似度スコア(0.0-1.0)。値を低くするとより多くの結果が返されますが、関連性が低くなる可能性があります。値を高くすると返される結果は少なくなりますが、より関連性が高くなります。", + "searchMinScoreResetTooltip": "デフォルト値(0.4)にリセット", "startIndexingButton": "インデックス作成を開始", "clearIndexDataButton": "インデックスデータをクリア", "unsavedSettingsMessage": "インデックス作成プロセスを開始する前に設定を保存してください。", @@ -110,6 +114,11 @@ "label": "サブタスク", "description": "承認なしでサブタスクの作成と完了を許可" }, + "followupQuestions": { + "label": "質問", + "description": "設定された時間が経過すると、フォローアップ質問の最初の提案回答を自動的に選択します", + "timeoutLabel": "最初の回答を自動選択するまでの待機時間" + }, "execute": { "label": "実行", "description": "承認なしで自動的に許可されたターミナルコマンドを実行", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 5373831426..1f86dc8cf4 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -234,17 +234,19 @@ "tokens": "토큰" }, "followUpSuggest": { - "copyToInput": "입력창에 복사 (또는 Shift + 클릭)" + "copyToInput": "입력창에 복사 (또는 Shift + 클릭)", + "autoSelectCountdown": "{{count}}초 후 자동 선택", + "countdownDisplay": "{{count}}초" }, "announcement": { "title": "🎉 Roo Code {{version}} 출시", - "description": "Roo Code {{version}}은 사용자 피드백을 기반으로 중요한 새로운 기능과 개선사항을 제공합니다.", + "description": "Roo Code {{version}}은 개발 워크플로우를 향상시키는 강력한 새 기능과 중요한 개선사항을 제공합니다.", "whatsNew": "새로운 기능", - "feature1": "Roo 마켓플레이스 출시: 마켓플레이스가 이제 라이브입니다! 그 어느 때보다 쉽게 모드와 MCP를 발견하고 설치하세요.", - "feature2": "Gemini 2.5 모델: 새로운 Gemini 2.5 Pro, Flash, Flash Lite 모델 지원을 추가했습니다.", - "feature3": "Excel 파일 지원 및 기타: Excel (.xlsx) 파일 지원과 수많은 버그 수정 및 개선사항 추가!", + "feature1": "원클릭 작업 공유: 한 번의 클릭으로 동료 및 커뮤니티와 작업을 즉시 공유하세요.", + "feature2": "글로벌 .roo 디렉토리 지원: 글로벌 .roo 디렉토리에서 규칙과 구성을 로드하여 프로젝트 간 일관된 설정을 유지하세요.", + "feature3": "개선된 아키텍트에서 코드로의 전환: 아키텍트 모드에서의 계획부터 코드 모드에서의 구현까지 원활한 인수인계.", "hideButton": "공지 숨기기", - "detailsDiscussLinks": "DiscordReddit에서 더 자세한 정보를 확인하고 논의하세요 🚀" + "detailsDiscussLinks": "DiscordReddit에서 자세한 내용을 확인하고 토론에 참여하세요 🚀" }, "browser": { "rooWantsToUse": "Roo가 브라우저를 사용하고 싶어합니다:", @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo가 코드베이스에서 {{query}}을(를) 검색하고 싶어합니다:", "wantsToSearchWithPath": "Roo가 {{path}}에서 {{query}}을(를) 검색하고 싶어합니다:", - "didSearch": "{{query}}에 대한 검색 결과 {{count}}개 찾음:" + "didSearch": "{{query}}에 대한 검색 결과 {{count}}개 찾음:", + "resultTooltip": "유사도 점수: {{score}} (클릭하여 파일 열기)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "인덱싱 완료", "error": "인덱스 오류", "status": "인덱스 상태" + }, + "versionIndicator": { + "ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요" } } diff --git a/webview-ui/src/i18n/locales/ko/prompts.json b/webview-ui/src/i18n/locales/ko/prompts.json index 990ee67f03..6625f2e957 100644 --- a/webview-ui/src/i18n/locales/ko/prompts.json +++ b/webview-ui/src/i18n/locales/ko/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "모드", "createNewMode": "새 모드 만들기", + "importMode": "모드 가져오기", + "noMatchFound": "모드를 찾을 수 없습니다", "editModesConfig": "모드 구성 편집", "editGlobalModes": "전역 모드 편집", "editProjectModes": "프로젝트 모드 편집 (.roomodes)", @@ -50,6 +52,28 @@ "description": "{{modeName}} 모드에 대한 특정 행동 지침을 추가하세요.", "loadFromFile": "{{mode}} 모드에 대한 사용자 지정 지침은 작업 공간의 .roo/rules-{{slug}}/ 폴더에서도 로드할 수 있습니다(.roorules-{{slug}}와 .clinerules-{{slug}}는 더 이상 사용되지 않으며 곧 작동을 중단합니다)." }, + "exportMode": { + "title": "모드 내보내기", + "description": "이 모드를 모든 규칙이 포함된 YAML 파일로 내보내어 다른 사람들과 쉽게 공유할 수 있습니다.", + "export": "모드 내보내기", + "exporting": "내보내는 중..." + }, + "importMode": { + "selectLevel": "이 모드를 가져올 위치를 선택하세요:", + "import": "가져오기", + "importing": "가져오는 중...", + "global": { + "label": "전역 수준", + "description": "모든 프로젝트에서 사용 가능합니다. 규칙은 사용자 지정 지침에 병합됩니다." + }, + "project": { + "label": "프로젝트 수준", + "description": "이 작업 공간에서만 사용할 수 있습니다. 내보낸 모드에 규칙 파일이 포함된 경우 .roo/rules-{slug}/ 폴더에 다시 생성됩니다." + } + }, + "advanced": { + "title": "고급" + }, "globalCustomInstructions": { "title": "모든 모드에 대한 사용자 지정 지침", "description": "이 지침은 모든 모드에 적용됩니다. 아래의 모드별 지침으로 향상될 수 있는 기본 동작 세트를 제공합니다. <0>더 알아보기", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 6ae68428bd..e68297599c 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant 키:", + "advancedConfigLabel": "고급 구성", + "searchMinScoreLabel": "검색 점수 임계값", + "searchMinScoreDescription": "검색 결과에 필요한 최소 유사도 점수(0.0-1.0). 값이 낮을수록 더 많은 결과가 반환되지만 관련성이 떨어질 수 있습니다. 값이 높을수록 결과는 적지만 관련성이 높은 결과가 반환됩니다.", + "searchMinScoreResetTooltip": "기본값(0.4)으로 재설정", "startIndexingButton": "인덱싱 시작", "clearIndexDataButton": "인덱스 데이터 지우기", "unsavedSettingsMessage": "인덱싱 프로세스를 시작하기 전에 설정을 저장해 주세요.", @@ -110,6 +114,11 @@ "label": "하위 작업", "description": "승인 없이 하위 작업 생성 및 완료 허용" }, + "followupQuestions": { + "label": "질문", + "description": "설정된 시간이 지나면 후속 질문에 대한 첫 번째 제안 답변을 자동으로 선택합니다", + "timeoutLabel": "첫 번째 답변을 자동 선택하기 전 대기 시간" + }, "execute": { "label": "실행", "description": "승인 없이 자동으로 허용된 터미널 명령 실행", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 2dc60da09e..d228d7b0c2 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -225,13 +225,13 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} uitgebracht", - "description": "Roo Code {{version}} brengt krachtige nieuwe functies en verbeteringen op basis van jouw feedback.", + "description": "Roo Code {{version}} brengt krachtige nieuwe functies en significante verbeteringen om je ontwikkelingsworkflow te verbeteren.", "whatsNew": "Wat is er nieuw", - "feature1": "Roo Marketplace Launch - De marketplace is nu live! Ontdek en installeer modi en MCP's makkelijker dan ooit tevoren.", - "feature2": "Gemini 2.5 Modellen - Ondersteuning toegevoegd voor nieuwe Gemini 2.5 Pro, Flash en Flash Lite modellen.", - "feature3": "Excel Bestandsondersteuning & Meer - Excel (.xlsx) bestandsondersteuning toegevoegd en talloze bugfixes en verbeteringen!", + "feature1": "1-Klik Taak Delen: Deel je taken direct met collega's en de community met slechts één klik.", + "feature2": "Globale .roo Directory Ondersteuning: Laad regels en configuraties vanuit een globale .roo directory voor consistente instellingen tussen projecten.", + "feature3": "Verbeterde Architect naar Code Overgangen: Soepele overdrachten van planning in Architect modus naar implementatie in Code modus.", "hideButton": "Aankondiging verbergen", - "detailsDiscussLinks": "Meer details en discussie in Discord en Reddit 🚀" + "detailsDiscussLinks": "Krijg meer details en doe mee aan discussies op Discord en Reddit 🚀" }, "reasoning": { "thinking": "Denkt na", @@ -244,7 +244,9 @@ "tokens": "tokens" }, "followUpSuggest": { - "copyToInput": "Kopiëren naar invoer (zelfde als shift + klik)" + "copyToInput": "Kopiëren naar invoer (zelfde als shift + klik)", + "autoSelectCountdown": "Automatische selectie in {{count}}s", + "countdownDisplay": "{{count}}s" }, "browser": { "rooWantsToUse": "Roo wil de browser gebruiken:", @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo wil de codebase doorzoeken op {{query}}:", "wantsToSearchWithPath": "Roo wil de codebase doorzoeken op {{query}} in {{path}}:", - "didSearch": "{{count}} resultaat/resultaten gevonden voor {{query}}:" + "didSearch": "{{count}} resultaat/resultaten gevonden voor {{query}}:", + "resultTooltip": "Gelijkenisscore: {{score}} (klik om bestand te openen)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Geïndexeerd", "error": "Index fout", "status": "Index status" + }, + "versionIndicator": { + "ariaLabel": "Versie {{version}} - Klik om release notes te bekijken" } } diff --git a/webview-ui/src/i18n/locales/nl/prompts.json b/webview-ui/src/i18n/locales/nl/prompts.json index 2aa09a5a15..c472117ea5 100644 --- a/webview-ui/src/i18n/locales/nl/prompts.json +++ b/webview-ui/src/i18n/locales/nl/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modi", "createNewMode": "Nieuwe modus aanmaken", + "importMode": "Modus importeren", + "noMatchFound": "Geen modi gevonden", "editModesConfig": "Modusconfiguratie bewerken", "editGlobalModes": "Globale modi bewerken", "editProjectModes": "Projectmodi bewerken (.roomodes)", @@ -50,6 +52,28 @@ "description": "Voeg gedragsrichtlijnen toe die specifiek zijn voor de modus {{modeName}}.", "loadFromFile": "Modusspecifieke instructies voor {{mode}} kunnen ook worden geladen uit de map .roo/rules-{{slug}}/ in je werkruimte (.roorules-{{slug}} en .clinerules-{{slug}} zijn verouderd en werken binnenkort niet meer)." }, + "exportMode": { + "title": "Modus exporteren", + "description": "Exporteer deze modus naar een YAML-bestand met alle regels inbegrepen voor eenvoudig delen met anderen.", + "export": "Modus exporteren", + "exporting": "Exporteren..." + }, + "importMode": { + "selectLevel": "Kies waar je deze modus wilt importeren:", + "import": "Importeren", + "importing": "Importeren...", + "global": { + "label": "Globaal niveau", + "description": "Beschikbaar in alle projecten. Regels worden samengevoegd in aangepaste instructies." + }, + "project": { + "label": "Projectniveau", + "description": "Alleen beschikbaar in deze werkruimte. Als de geëxporteerde modus regelbestanden bevatte, worden deze opnieuw gemaakt in de map .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Geavanceerd" + }, "globalCustomInstructions": { "title": "Aangepaste instructies voor alle modi", "description": "Deze instructies gelden voor alle modi. Ze bieden een basisset aan gedragingen die kunnen worden uitgebreid met modusspecifieke instructies hieronder. <0>Meer informatie", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index c6d1bb7992..254334df54 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant-sleutel:", + "advancedConfigLabel": "Geavanceerde configuratie", + "searchMinScoreLabel": "Zoekscore drempel", + "searchMinScoreDescription": "Minimale overeenkomstscore (0.0-1.0) vereist voor zoekresultaten. Lagere waarden leveren meer resultaten op, maar zijn mogelijk minder relevant. Hogere waarden leveren minder, maar relevantere resultaten op.", + "searchMinScoreResetTooltip": "Reset naar standaardwaarde (0.4)", "startIndexingButton": "Indexering starten", "clearIndexDataButton": "Indexgegevens wissen", "unsavedSettingsMessage": "Sla je instellingen op voordat je het indexeringsproces start.", @@ -110,6 +114,11 @@ "label": "Subtaken", "description": "Subtaken aanmaken en afronden zonder goedkeuring" }, + "followupQuestions": { + "label": "Vraag", + "description": "Selecteer automatisch het eerste voorgestelde antwoord voor vervolgvragen na de geconfigureerde time-out", + "timeoutLabel": "Wachttijd voordat het eerste antwoord automatisch wordt geselecteerd" + }, "execute": { "label": "Uitvoeren", "description": "Automatisch toegestane terminalcommando's uitvoeren zonder goedkeuring", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 0af70c595b..fdb39b0851 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -234,15 +234,17 @@ "tokens": "tokeny" }, "followUpSuggest": { - "copyToInput": "Kopiuj do pola wprowadzania (lub Shift + kliknięcie)" + "copyToInput": "Kopiuj do pola wprowadzania (lub Shift + kliknięcie)", + "autoSelectCountdown": "Automatyczny wybór za {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { "title": "🎉 Roo Code {{version}} wydany", - "description": "Roo Code {{version}} przynosi potężne nowe funkcje i ulepszenia na podstawie Twoich opinii.", + "description": "Roo Code {{version}} wprowadza potężne nowe funkcje i znaczące ulepszenia, aby ulepszyć Twój przepływ pracy programistycznej.", "whatsNew": "Co nowego", - "feature1": "Uruchomienie Roo Marketplace - Marketplace jest już dostępny! Odkrywaj i instaluj tryby oraz MCP łatwiej niż kiedykolwiek wcześniej.", - "feature2": "Modele Gemini 2.5 - Dodano wsparcie dla nowych modeli Gemini 2.5 Pro, Flash i Flash Lite.", - "feature3": "Wsparcie dla plików Excel i więcej - Dodano wsparcie dla plików Excel (.xlsx) oraz liczne poprawki błędów i ulepszenia!", + "feature1": "Udostępnianie zadań jednym kliknięciem: Natychmiast udostępniaj swoje zadania współpracownikom i społeczności jednym kliknięciem.", + "feature2": "Wsparcie globalnego katalogu .roo: Ładuj reguły i konfiguracje z globalnego katalogu .roo dla spójnych ustawień między projektami.", + "feature3": "Ulepszone przejścia z Architekta do Kodu: Płynne transfery z planowania w trybie Architekta do implementacji w trybie Kodu.", "hideButton": "Ukryj ogłoszenie", "detailsDiscussLinks": "Uzyskaj więcej szczegółów i dołącz do dyskusji na Discord i Reddit 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo chce przeszukać bazę kodu w poszukiwaniu {{query}}:", "wantsToSearchWithPath": "Roo chce przeszukać bazę kodu w poszukiwaniu {{query}} w {{path}}:", - "didSearch": "Znaleziono {{count}} wynik(ów) dla {{query}}:" + "didSearch": "Znaleziono {{count}} wynik(ów) dla {{query}}:", + "resultTooltip": "Wynik podobieństwa: {{score}} (kliknij, aby otworzyć plik)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Zaindeksowane", "error": "Błąd indeksu", "status": "Status indeksu" + }, + "versionIndicator": { + "ariaLabel": "Wersja {{version}} - Kliknij, aby wyświetlić informacje o wydaniu" } } diff --git a/webview-ui/src/i18n/locales/pl/prompts.json b/webview-ui/src/i18n/locales/pl/prompts.json index b4a1bdcc50..3cd76ddd73 100644 --- a/webview-ui/src/i18n/locales/pl/prompts.json +++ b/webview-ui/src/i18n/locales/pl/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Tryby", "createNewMode": "Utwórz nowy tryb", + "importMode": "Importuj tryb", + "noMatchFound": "Nie znaleziono trybów", "editModesConfig": "Edytuj konfigurację trybów", "editGlobalModes": "Edytuj tryby globalne", "editProjectModes": "Edytuj tryby projektu (.roomodes)", @@ -50,6 +52,28 @@ "description": "Dodaj wytyczne dotyczące zachowania specyficzne dla trybu {{modeName}}.", "loadFromFile": "Niestandardowe instrukcje dla trybu {{mode}} mogą być również ładowane z folderu .roo/rules-{{slug}}/ w Twoim obszarze roboczym (.roorules-{{slug}} i .clinerules-{{slug}} są przestarzałe i wkrótce przestaną działać)." }, + "exportMode": { + "title": "Eksportuj tryb", + "description": "Eksportuj ten tryb do pliku YAML ze wszystkimi regułami w celu łatwego udostępniania innym.", + "export": "Eksportuj tryb", + "exporting": "Eksportowanie..." + }, + "importMode": { + "selectLevel": "Wybierz, gdzie zaimportować ten tryb:", + "import": "Importuj", + "importing": "Importowanie...", + "global": { + "label": "Poziom globalny", + "description": "Dostępne we wszystkich projektach. Reguły zostaną scalone z niestandardowymi instrukcjami." + }, + "project": { + "label": "Poziom projektu", + "description": "Dostępne tylko w tym obszarze roboczym. Jeśli wyeksportowany tryb zawierał pliki reguł, zostaną one odtworzone w folderze .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Zaawansowane" + }, "globalCustomInstructions": { "title": "Niestandardowe instrukcje dla wszystkich trybów", "description": "Te instrukcje dotyczą wszystkich trybów. Zapewniają podstawowy zestaw zachowań, które mogą być rozszerzone przez instrukcje specyficzne dla trybów poniżej. <0>Dowiedz się więcej", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index b702d7b5a5..02a7d0d04d 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL Ollama:", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Klucz Qdrant:", + "advancedConfigLabel": "Konfiguracja zaawansowana", + "searchMinScoreLabel": "Próg wyniku wyszukiwania", + "searchMinScoreDescription": "Minimalny wynik podobieństwa (0.0-1.0) wymagany dla wyników wyszukiwania. Niższe wartości zwracają więcej wyników, ale mogą być mniej trafne. Wyższe wartości zwracają mniej wyników, ale bardziej trafnych.", + "searchMinScoreResetTooltip": "Zresetuj do wartości domyślnej (0.4)", "startIndexingButton": "Rozpocznij indeksowanie", "clearIndexDataButton": "Wyczyść dane indeksu", "unsavedSettingsMessage": "Zapisz swoje ustawienia przed rozpoczęciem procesu indeksowania.", @@ -110,6 +114,11 @@ "label": "Podzadania", "description": "Zezwalaj na tworzenie i ukończenie podzadań bez konieczności zatwierdzania" }, + "followupQuestions": { + "label": "Pytanie", + "description": "Automatycznie wybierz pierwszą sugerowaną odpowiedź na pytania uzupełniające po skonfigurowanym limicie czasu", + "timeoutLabel": "Czas oczekiwania przed automatycznym wybraniem pierwszej odpowiedzi" + }, "execute": { "label": "Wykonaj", "description": "Automatycznie wykonuj dozwolone polecenia terminala bez konieczności zatwierdzania", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index a09f8174f6..b37879f2f0 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -234,15 +234,17 @@ "tokens": "tokens" }, "followUpSuggest": { - "copyToInput": "Copiar para entrada (ou Shift + clique)" + "copyToInput": "Copiar para entrada (ou Shift + clique)", + "autoSelectCountdown": "Seleção automática em {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { "title": "🎉 Roo Code {{version}} Lançado", - "description": "Roo Code {{version}} traz importantes novos recursos e melhorias baseados no seu feedback.", + "description": "Roo Code {{version}} traz novos recursos poderosos e melhorias significativas para aprimorar seu fluxo de trabalho de desenvolvimento.", "whatsNew": "O que há de novo", - "feature1": "Lançamento do Marketplace Roo: O marketplace está agora no ar! Descubra e instale modos e MCPs mais facilmente do que nunca.", - "feature2": "Modelos Gemini 2.5: Adicionado suporte para novos modelos Gemini 2.5 Pro, Flash e Flash Lite.", - "feature3": "Suporte a Arquivos Excel e Mais: Adicionado suporte a arquivos Excel (.xlsx) e numerosas correções de bugs e melhorias!", + "feature1": "Compartilhamento de Tarefas com 1 Clique: Compartilhe instantaneamente suas tarefas com colegas e a comunidade com apenas um clique.", + "feature2": "Suporte a Diretório Global .roo: Carregue regras e configurações de um diretório global .roo para configurações consistentes entre projetos.", + "feature3": "Transições Aprimoradas de Arquiteto para Código: Transferências suaves do planejamento no modo Arquiteto para implementação no modo Código.", "hideButton": "Ocultar anúncio", "detailsDiscussLinks": "Obtenha mais detalhes e participe da discussão no Discord e Reddit 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo quer pesquisar na base de código por {{query}}:", "wantsToSearchWithPath": "Roo quer pesquisar na base de código por {{query}} em {{path}}:", - "didSearch": "Encontrado {{count}} resultado(s) para {{query}}:" + "didSearch": "Encontrado {{count}} resultado(s) para {{query}}:", + "resultTooltip": "Pontuação de similaridade: {{score}} (clique para abrir o arquivo)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Indexado", "error": "Erro do índice", "status": "Status do índice" + }, + "versionIndicator": { + "ariaLabel": "Versão {{version}} - Clique para ver as notas de lançamento" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/prompts.json b/webview-ui/src/i18n/locales/pt-BR/prompts.json index c2a88d4eaa..3e456572b7 100644 --- a/webview-ui/src/i18n/locales/pt-BR/prompts.json +++ b/webview-ui/src/i18n/locales/pt-BR/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modos", "createNewMode": "Criar novo modo", + "importMode": "Importar modo", + "noMatchFound": "Nenhum modo encontrado", "editModesConfig": "Editar configuração de modos", "editGlobalModes": "Editar modos globais", "editProjectModes": "Editar modos do projeto (.roomodes)", @@ -50,6 +52,28 @@ "description": "Adicione diretrizes comportamentais específicas para o modo {{modeName}}.", "loadFromFile": "Instruções personalizadas específicas para o modo {{mode}} também podem ser carregadas da pasta .roo/rules-{{slug}}/ no seu espaço de trabalho (.roorules-{{slug}} e .clinerules-{{slug}} estão obsoletos e deixarão de funcionar em breve)." }, + "exportMode": { + "title": "Exportar modo", + "description": "Exporte este modo para um arquivo YAML com todas as regras incluídas para compartilhar facilmente com outros.", + "export": "Exportar modo", + "exporting": "Exportando..." + }, + "importMode": { + "selectLevel": "Escolha onde importar este modo:", + "import": "Importar", + "importing": "Importando...", + "global": { + "label": "Nível global", + "description": "Disponível em todos os projetos. As regras serão mescladas nas instruções personalizadas." + }, + "project": { + "label": "Nível do projeto", + "description": "Disponível apenas neste espaço de trabalho. Se o modo exportado continha arquivos de regras, eles serão recriados na pasta .roo/rules-{slug}/." + } + }, + "advanced": { + "title": "Avançado" + }, "globalCustomInstructions": { "title": "Instruções personalizadas para todos os modos", "description": "Estas instruções se aplicam a todos os modos. Elas fornecem um conjunto base de comportamentos que podem ser aprimorados por instruções específicas do modo abaixo. <0>Saiba mais", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index be7bfe2d0d..8c7b6c9a24 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL Ollama:", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Chave Qdrant:", + "advancedConfigLabel": "Configuração Avançada", + "searchMinScoreLabel": "Limite de pontuação de busca", + "searchMinScoreDescription": "Pontuação mínima de similaridade (0.0-1.0) necessária para os resultados da busca. Valores mais baixos retornam mais resultados, mas podem ser menos relevantes. Valores mais altos retornam menos resultados, mas mais relevantes.", + "searchMinScoreResetTooltip": "Redefinir para o valor padrão (0.4)", "startIndexingButton": "Iniciar Indexação", "clearIndexDataButton": "Limpar Dados de Índice", "unsavedSettingsMessage": "Por favor, salve suas configurações antes de iniciar o processo de indexação.", @@ -110,6 +114,11 @@ "label": "Subtarefas", "description": "Permitir a criação e conclusão de subtarefas sem exigir aprovação" }, + "followupQuestions": { + "label": "Pergunta", + "description": "Selecionar automaticamente a primeira resposta sugerida para perguntas de acompanhamento após o tempo limite configurado", + "timeoutLabel": "Tempo de espera antes de selecionar automaticamente a primeira resposta" + }, "execute": { "label": "Executar", "description": "Executar automaticamente comandos de terminal permitidos sem exigir aprovação", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 89d35f322a..5865e41a53 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -225,11 +225,11 @@ }, "announcement": { "title": "🎉 Выпущен Roo Code {{version}}", - "description": "Roo Code {{version}} приносит важные новые функции и улучшения на основе ваших отзывов.", + "description": "Roo Code {{version}} приносит мощные новые функции и значительные улучшения для совершенствования вашего рабочего процесса разработки.", "whatsNew": "Что нового", - "feature1": "Запуск Roo Marketplace: Маркетплейс теперь в сети! Открывайте и устанавливайте режимы и MCP проще, чем когда-либо.", - "feature2": "Модели Gemini 2.5: Добавлена поддержка новых моделей Gemini 2.5 Pro, Flash и Flash Lite.", - "feature3": "Поддержка файлов Excel и многое другое: Добавлена поддержка файлов Excel (.xlsx) и множество исправлений ошибок и улучшений!", + "feature1": "Обмен задачами в 1 клик: Мгновенно делитесь своими задачами с коллегами и сообществом одним кликом.", + "feature2": "Поддержка глобального каталога .roo: Загружайте правила и конфигурации из глобального каталога .roo для согласованных настроек между проектами.", + "feature3": "Улучшенные переходы от Архитектора к Коду: Плавные переходы от планирования в режиме Архитектора к реализации в режиме Кода.", "hideButton": "Скрыть объявление", "detailsDiscussLinks": "Подробнее и обсуждение в Discord и Reddit 🚀" }, @@ -244,7 +244,9 @@ "tokens": "токены" }, "followUpSuggest": { - "copyToInput": "Скопировать во ввод (то же, что shift + клик)" + "copyToInput": "Скопировать во ввод (то же, что shift + клик)", + "autoSelectCountdown": "Автовыбор через {{count}}с", + "countdownDisplay": "{{count}}с" }, "browser": { "rooWantsToUse": "Roo хочет использовать браузер:", @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo хочет выполнить поиск в кодовой базе по {{query}}:", "wantsToSearchWithPath": "Roo хочет выполнить поиск в кодовой базе по {{query}} в {{path}}:", - "didSearch": "Найдено {{count}} результат(ов) для {{query}}:" + "didSearch": "Найдено {{count}} результат(ов) для {{query}}:", + "resultTooltip": "Оценка схожести: {{score}} (нажмите, чтобы открыть файл)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Проиндексировано", "error": "Ошибка индекса", "status": "Статус индекса" + }, + "versionIndicator": { + "ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску" } } diff --git a/webview-ui/src/i18n/locales/ru/prompts.json b/webview-ui/src/i18n/locales/ru/prompts.json index 07e9f91db8..54fbeb24a6 100644 --- a/webview-ui/src/i18n/locales/ru/prompts.json +++ b/webview-ui/src/i18n/locales/ru/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Режимы", "createNewMode": "Создать новый режим", + "importMode": "Импортировать режим", + "noMatchFound": "Режимы не найдены", "editModesConfig": "Редактировать конфигурацию режимов", "editGlobalModes": "Редактировать глобальные режимы", "editProjectModes": "Редактировать режимы проекта (.roomodes)", @@ -50,11 +52,30 @@ "description": "Добавьте рекомендации по поведению, специфичные для режима {{modeName}}.", "loadFromFile": "Пользовательские инструкции для режима {{mode}} также можно загрузить из папки .roo/rules-{{slug}}/ в вашем рабочем пространстве (.roorules-{{slug}} и .clinerules-{{slug}} устарели и скоро перестанут работать)." }, + "exportMode": { + "title": "Экспортировать режим", + "description": "Экспортировать этот режим в файл YAML со всеми включенными правилами для удобного обмена с другими.", + "export": "Экспортировать режим", + "exporting": "Экспорт..." + }, "globalCustomInstructions": { "title": "Пользовательские инструкции для всех режимов", "description": "Эти инструкции применяются ко всем режимам. Они задают базовое поведение, которое можно расширить с помощью инструкций ниже. <0>Узнать больше", "loadFromFile": "Инструкции также можно загрузить из папки .roo/rules/ в вашем рабочем пространстве (.roorules и .clinerules устарели и скоро перестанут работать)." }, + "importMode": { + "selectLevel": "Выберите, куда импортировать этот режим:", + "import": "Импорт", + "importing": "Импортирование...", + "global": { + "label": "Глобальный уровень", + "description": "Доступно во всех проектах. Правила будут объединены с пользовательскими инструкциями." + }, + "project": { + "label": "Уровень проекта", + "description": "Доступно только в этом рабочем пространстве. Если экспортированный режим содержал файлы правил, они будут воссозданы в папке .roo/rules-{slug}/." + } + }, "systemPrompt": { "preview": "Предпросмотр системного промпта", "copy": "Скопировать системный промпт в буфер обмена", @@ -164,5 +185,8 @@ }, "deleteMode": "Удалить режим" }, - "allFiles": "все файлы" + "allFiles": "все файлы", + "advanced": { + "title": "Дополнительно" + } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index b0693f4532..e60b97f775 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL Ollama:", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Ключ Qdrant:", + "advancedConfigLabel": "Расширенная конфигурация", + "searchMinScoreLabel": "Порог оценки поиска", + "searchMinScoreDescription": "Минимальный балл сходства (0.0-1.0), необходимый для результатов поиска. Более низкие значения возвращают больше результатов, но они могут быть менее релевантными. Более высокие значения возвращают меньше результатов, но более релевантных.", + "searchMinScoreResetTooltip": "Сбросить к значению по умолчанию (0.4)", "startIndexingButton": "Начать индексацию", "clearIndexDataButton": "Очистить данные индекса", "unsavedSettingsMessage": "Пожалуйста, сохрани настройки перед запуском процесса индексации.", @@ -110,6 +114,11 @@ "label": "Подзадачи", "description": "Разрешить создание и выполнение подзадач без необходимости одобрения" }, + "followupQuestions": { + "label": "Вопрос", + "description": "Автоматически выбирать первый предложенный ответ на дополнительные вопросы после настроенного тайм-аута", + "timeoutLabel": "Время ожидания перед автоматическим выбором первого ответа" + }, "execute": { "label": "Выполнение", "description": "Автоматически выполнять разрешённые команды терминала без необходимости одобрения", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index f12fa62bbb..6c5bd20353 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -234,17 +234,19 @@ "tokens": "token" }, "followUpSuggest": { - "copyToInput": "Giriş alanına kopyala (veya Shift + tıklama)" + "copyToInput": "Giriş alanına kopyala (veya Shift + tıklama)", + "autoSelectCountdown": "{{count}}s içinde otomatik seçilecek", + "countdownDisplay": "{{count}}sn" }, "announcement": { "title": "🎉 Roo Code {{version}} Yayınlandı", - "description": "Roo Code {{version}} geri bildirimlerinize dayalı güçlü yeni özellikler ve iyileştirmeler getiriyor.", + "description": "Roo Code {{version}}, geliştirme iş akışınızı geliştirmek için güçlü yeni özellikler ve önemli iyileştirmeler getiriyor.", "whatsNew": "Yenilikler", - "feature1": "Roo Marketplace Lansmanı - Marketplace artık canlı! Modları ve MCP'leri her zamankinden daha kolay keşfedin ve kurun.", - "feature2": "Gemini 2.5 Modelleri - Yeni Gemini 2.5 Pro, Flash ve Flash Lite modelleri için destek eklendi.", - "feature3": "Excel Dosya Desteği ve Daha Fazlası - Excel (.xlsx) dosya desteği eklendi ve sayısız hata düzeltmesi ve iyileştirme!", + "feature1": "Tek Tıkla Görev Paylaşımı: Görevlerinizi meslektaşlarınız ve toplulukla tek tıkla anında paylaşın.", + "feature2": "Global .roo Dizin Desteği: Projeler arası tutarlı ayarlar için global .roo dizininden kurallar ve yapılandırmalar yükleyin.", + "feature3": "Geliştirilmiş Mimar'dan Kod'a Geçişler: Mimar modunda planlamadan Kod modunda uygulamaya sorunsuz aktarımlar.", "hideButton": "Duyuruyu gizle", - "detailsDiscussLinks": "Discord ve Reddit üzerinde daha fazla ayrıntı edinin ve tartışmalara katılın 🚀" + "detailsDiscussLinks": "Discord ve Reddit'te daha fazla ayrıntı alın ve tartışmalara katılın 🚀" }, "browser": { "rooWantsToUse": "Roo tarayıcıyı kullanmak istiyor:", @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo kod tabanında {{query}} aramak istiyor:", "wantsToSearchWithPath": "Roo {{path}} içinde kod tabanında {{query}} aramak istiyor:", - "didSearch": "{{query}} için {{count}} sonuç bulundu:" + "didSearch": "{{query}} için {{count}} sonuç bulundu:", + "resultTooltip": "Benzerlik puanı: {{score}} (dosyayı açmak için tıklayın)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "İndekslendi", "error": "İndeks hatası", "status": "İndeks durumu" + }, + "versionIndicator": { + "ariaLabel": "Sürüm {{version}} - Sürüm notlarını görüntülemek için tıklayın" } } diff --git a/webview-ui/src/i18n/locales/tr/prompts.json b/webview-ui/src/i18n/locales/tr/prompts.json index d091456e43..0da97ec4c6 100644 --- a/webview-ui/src/i18n/locales/tr/prompts.json +++ b/webview-ui/src/i18n/locales/tr/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Modlar", "createNewMode": "Yeni mod oluştur", + "importMode": "Modu içe aktar", + "noMatchFound": "Mod bulunamadı", "editModesConfig": "Mod yapılandırmasını düzenle", "editGlobalModes": "Global modları düzenle", "editProjectModes": "Proje modlarını düzenle (.roomodes)", @@ -50,11 +52,30 @@ "description": "{{modeName}} modu için özel davranış yönergeleri ekleyin.", "loadFromFile": "{{mode}} moduna özgü özel talimatlar ayrıca çalışma alanınızdaki .roo/rules-{{slug}}/ klasöründen yüklenebilir (.roorules-{{slug}} ve .clinerules-{{slug}} kullanımdan kaldırılmıştır ve yakında çalışmayı durduracaklardır)." }, + "exportMode": { + "title": "Modu Dışa Aktar", + "description": "Bu modu tüm kurallar dahil olarak bir YAML dosyasına dışa aktararak başkalarıyla kolayca paylaşın.", + "export": "Modu Dışa Aktar", + "exporting": "Dışa aktarılıyor..." + }, "globalCustomInstructions": { "title": "Tüm Modlar için Özel Talimatlar", "description": "Bu talimatlar tüm modlara uygulanır. Aşağıdaki moda özgü talimatlarla geliştirilebilen temel davranış seti sağlarlar. <0>Daha fazla bilgi edinin", "loadFromFile": "Talimatlar ayrıca çalışma alanınızdaki .roo/rules/ klasöründen de yüklenebilir (.roorules ve .clinerules kullanımdan kaldırılmıştır ve yakında çalışmayı durduracaklardır)." }, + "importMode": { + "selectLevel": "Bu modu nereye içe aktaracağınızı seçin:", + "import": "İçe Aktar", + "importing": "İçe aktarılıyor...", + "global": { + "label": "Genel Seviye", + "description": "Tüm projelerde kullanılabilir. Kurallar özel talimatlarla birleştirilecektir." + }, + "project": { + "label": "Proje Seviyesi", + "description": "Yalnızca bu çalışma alanında kullanılabilir. Dışa aktarılan mod kural dosyaları içeriyorsa, bunlar .roo/rules-{slug}/ klasöründe yeniden oluşturulur." + } + }, "systemPrompt": { "preview": "Sistem promptunu önizle", "copy": "Sistem promptunu panoya kopyala", @@ -164,5 +185,8 @@ }, "deleteMode": "Modu sil" }, - "allFiles": "tüm dosyalar" + "allFiles": "tüm dosyalar", + "advanced": { + "title": "Gelişmiş" + } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 06bd626406..c7ced157f6 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant Anahtarı:", + "advancedConfigLabel": "Gelişmiş Yapılandırma", + "searchMinScoreLabel": "Arama Skoru Eşiği", + "searchMinScoreDescription": "Arama sonuçları için gereken minimum benzerlik puanı (0.0-1.0). Düşük değerler daha fazla sonuç döndürür ancak daha az alakalı olabilir. Yüksek değerler daha az ancak daha alakalı sonuçlar döndürür.", + "searchMinScoreResetTooltip": "Varsayılan değere sıfırla (0.4)", "startIndexingButton": "İndekslemeyi Başlat", "clearIndexDataButton": "İndeks Verilerini Temizle", "unsavedSettingsMessage": "İndeksleme işlemini başlatmadan önce lütfen ayarlarını kaydet.", @@ -110,6 +114,11 @@ "label": "Alt Görevler", "description": "Onay gerektirmeden alt görevlerin oluşturulmasına ve tamamlanmasına izin ver" }, + "followupQuestions": { + "label": "Soru", + "description": "Yapılandırılan zaman aşımından sonra takip sorularına ilişkin ilk önerilen yanıtı otomatik olarak seç", + "timeoutLabel": "İlk yanıtı otomatik olarak seçmeden önce beklenecek süre" + }, "execute": { "label": "Yürüt", "description": "Onay gerektirmeden otomatik olarak izin verilen terminal komutlarını yürüt", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index c2338e33aa..eb7cdc2306 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -234,15 +234,17 @@ "tokens": "token" }, "followUpSuggest": { - "copyToInput": "Sao chép vào ô nhập liệu (hoặc Shift + nhấp chuột)" + "copyToInput": "Sao chép vào ô nhập liệu (hoặc Shift + nhấp chuột)", + "autoSelectCountdown": "Tự động chọn sau {{count}}s", + "countdownDisplay": "{{count}}s" }, "announcement": { "title": "🎉 Roo Code {{version}} Đã phát hành", - "description": "Roo Code {{version}} mang đến các tính năng mạnh mẽ và cải tiến mới dựa trên phản hồi của bạn.", + "description": "Roo Code {{version}} mang đến các tính năng mạnh mẽ mới và cải tiến đáng kể để nâng cao quy trình phát triển của bạn.", "whatsNew": "Có gì mới", - "feature1": "Ra mắt Roo Marketplace - Marketplace hiện đã hoạt động! Khám phá và cài đặt các chế độ và MCP dễ dàng hơn bao giờ hết.", - "feature2": "Các mô hình Gemini 2.5 - Đã thêm hỗ trợ cho các mô hình Gemini 2.5 Pro, Flash và Flash Lite mới.", - "feature3": "Hỗ trợ tệp Excel & Nhiều hơn nữa - Đã thêm hỗ trợ tệp Excel (.xlsx) và vô số sửa lỗi cùng cải tiến!", + "feature1": "Chia sẻ Nhiệm vụ 1-Click: Chia sẻ nhiệm vụ của bạn với đồng nghiệp và cộng đồng ngay lập tức chỉ với một cú nhấp chuột.", + "feature2": "Hỗ trợ Thư mục .roo Toàn cục: Tải quy tắc và cấu hình từ thư mục .roo toàn cục để có cài đặt nhất quán giữa các dự án.", + "feature3": "Cải thiện Chuyển đổi từ Architect sang Code: Chuyển đổi mượt mà từ lập kế hoạch trong chế độ Architect sang triển khai trong chế độ Code.", "hideButton": "Ẩn thông báo", "detailsDiscussLinks": "Nhận thêm chi tiết và thảo luận tại DiscordReddit 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo muốn tìm kiếm trong cơ sở mã cho {{query}}:", "wantsToSearchWithPath": "Roo muốn tìm kiếm trong cơ sở mã cho {{query}} trong {{path}}:", - "didSearch": "Đã tìm thấy {{count}} kết quả cho {{query}}:" + "didSearch": "Đã tìm thấy {{count}} kết quả cho {{query}}:", + "resultTooltip": "Điểm tương tự: {{score}} (nhấp để mở tệp)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "Đã lập chỉ mục", "error": "Lỗi chỉ mục", "status": "Trạng thái chỉ mục" + }, + "versionIndicator": { + "ariaLabel": "Phiên bản {{version}} - Nhấp để xem ghi chú phát hành" } } diff --git a/webview-ui/src/i18n/locales/vi/prompts.json b/webview-ui/src/i18n/locales/vi/prompts.json index 7a0b311a02..1dadec8f02 100644 --- a/webview-ui/src/i18n/locales/vi/prompts.json +++ b/webview-ui/src/i18n/locales/vi/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "Chế độ", "createNewMode": "Tạo chế độ mới", + "importMode": "Nhập chế độ", + "noMatchFound": "Không tìm thấy chế độ nào", "editModesConfig": "Chỉnh sửa cấu hình chế độ", "editGlobalModes": "Chỉnh sửa chế độ toàn cục", "editProjectModes": "Chỉnh sửa chế độ dự án (.roomodes)", @@ -50,11 +52,30 @@ "description": "Thêm hướng dẫn hành vi dành riêng cho chế độ {{modeName}}.", "loadFromFile": "Hướng dẫn tùy chỉnh dành riêng cho chế độ {{mode}} cũng có thể được tải từ thư mục .roo/rules-{{slug}}/ trong không gian làm việc của bạn (.roorules-{{slug}} và .clinerules-{{slug}} đã lỗi thời và sẽ sớm ngừng hoạt động)." }, + "exportMode": { + "title": "Xuất chế độ", + "description": "Xuất chế độ này sang tệp YAML với tất cả các quy tắc được bao gồm để dễ dàng chia sẻ với người khác.", + "export": "Xuất chế độ", + "exporting": "Đang xuất..." + }, "globalCustomInstructions": { "title": "Hướng dẫn tùy chỉnh cho tất cả các chế độ", "description": "Những hướng dẫn này áp dụng cho tất cả các chế độ. Chúng cung cấp một bộ hành vi cơ bản có thể được nâng cao bởi hướng dẫn dành riêng cho chế độ bên dưới. <0>Tìm hiểu thêm", "loadFromFile": "Hướng dẫn cũng có thể được tải từ thư mục .roo/rules/ trong không gian làm việc của bạn (.roorules và .clinerules đã lỗi thời và sẽ sớm ngừng hoạt động)." }, + "importMode": { + "selectLevel": "Chọn nơi để nhập chế độ này:", + "import": "Nhập", + "importing": "Đang nhập...", + "global": { + "label": "Cấp độ toàn cục", + "description": "Có sẵn trong tất cả các dự án. Các quy tắc sẽ được hợp nhất vào hướng dẫn tùy chỉnh." + }, + "project": { + "label": "Cấp độ dự án", + "description": "Chỉ có sẵn trong không gian làm việc này. Nếu chế độ đã xuất có chứa tệp quy tắc, chúng sẽ được tạo lại trong thư mục .roo/rules-{slug}/." + } + }, "systemPrompt": { "preview": "Xem trước lời nhắc hệ thống", "copy": "Sao chép lời nhắc hệ thống vào bộ nhớ tạm", @@ -164,5 +185,8 @@ }, "deleteMode": "Xóa chế độ" }, - "allFiles": "tất cả các tệp" + "allFiles": "tất cả các tệp", + "advanced": { + "title": "Nâng cao" + } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index c434276b3b..c82bfdd7a2 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "URL Ollama:", "qdrantUrlLabel": "URL Qdrant", "qdrantKeyLabel": "Khóa Qdrant:", + "advancedConfigLabel": "Cấu hình nâng cao", + "searchMinScoreLabel": "Ngưỡng điểm tìm kiếm", + "searchMinScoreDescription": "Điểm tương đồng tối thiểu (0.0-1.0) cần thiết cho kết quả tìm kiếm. Giá trị thấp hơn trả về nhiều kết quả hơn nhưng có thể kém liên quan hơn. Giá trị cao hơn trả về ít kết quả hơn nhưng có liên quan hơn.", + "searchMinScoreResetTooltip": "Đặt lại về giá trị mặc định (0.4)", "startIndexingButton": "Bắt đầu lập chỉ mục", "clearIndexDataButton": "Xóa dữ liệu chỉ mục", "unsavedSettingsMessage": "Vui lòng lưu cài đặt của bạn trước khi bắt đầu quá trình lập chỉ mục.", @@ -110,6 +114,11 @@ "label": "Công việc phụ", "description": "Cho phép tạo và hoàn thành các công việc phụ mà không cần phê duyệt" }, + "followupQuestions": { + "label": "Câu hỏi", + "description": "Tự động chọn câu trả lời đầu tiên được đề xuất cho các câu hỏi tiếp theo sau thời gian chờ đã cấu hình", + "timeoutLabel": "Thời gian chờ trước khi tự động chọn câu trả lời đầu tiên" + }, "execute": { "label": "Thực thi", "description": "Tự động thực thi các lệnh terminal được phép mà không cần phê duyệt", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index f3fb857f06..93494e6f50 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -234,15 +234,17 @@ "tokens": "tokens" }, "followUpSuggest": { - "copyToInput": "复制到输入框(或按住Shift点击)" + "copyToInput": "复制到输入框(或按住Shift点击)", + "autoSelectCountdown": "{{count}}秒后自动选择", + "countdownDisplay": "{{count}}秒" }, "announcement": { "title": "🎉 Roo Code {{version}} 已发布", - "description": "Roo Code {{version}} 带来基于您反馈的重要新功能和改进。", + "description": "Roo Code {{version}} 带来强大的新功能和重大改进,提升您的开发工作流程。", "whatsNew": "新特性", - "feature1": "Roo 市场正式上线: 市场现已上线!比以往更轻松地发现和安装模式及 MCP。", - "feature2": "Gemini 2.5 模型: 新增对新版 Gemini 2.5 Pro、Flash 和 Flash Lite 模型的支持。", - "feature3": "Excel 文件支持及更多: 新增 Excel (.xlsx) 文件支持以及大量错误修复和改进!", + "feature1": "一键任务分享: 一键即可与同事和社区分享您的任务。", + "feature2": "全局 .roo 目录支持: 从全局 .roo 目录加载规则和配置,确保项目间设置一致。", + "feature3": "改进的架构师到代码转换: 从架构师模式的规划到代码模式的实现,实现无缝交接。", "hideButton": "隐藏公告", "detailsDiscussLinks": "在 DiscordReddit 获取更多详情并参与讨论 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo 需要搜索代码库: {{query}}", "wantsToSearchWithPath": "Roo 需要在 {{path}} 中搜索: {{query}}", - "didSearch": "找到 {{count}} 个结果: {{query}}" + "didSearch": "找到 {{count}} 个结果: {{query}}", + "resultTooltip": "相似度评分: {{score}} (点击打开文件)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "已索引", "error": "索引错误", "status": "索引状态" + }, + "versionIndicator": { + "ariaLabel": "版本 {{version}} - 点击查看发布说明" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/prompts.json b/webview-ui/src/i18n/locales/zh-CN/prompts.json index 2abf922b14..5b67b41bec 100644 --- a/webview-ui/src/i18n/locales/zh-CN/prompts.json +++ b/webview-ui/src/i18n/locales/zh-CN/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "模式配置", "createNewMode": "新建模式", + "importMode": "导入模式", + "noMatchFound": "未找到任何模式", "editModesConfig": "模式设置", "editGlobalModes": "修改全局模式", "editProjectModes": "编辑项目模式 (.roomodes)", @@ -50,6 +52,25 @@ "description": "{{modeName}}模式的专属规则", "loadFromFile": "支持从.roo/rules-{{slug}}/目录读取配置(.roorules-{{slug}}和.clinerules-{{slug}}已弃用并将很快停止工作)。" }, + "exportMode": { + "title": "导出模式", + "description": "将此模式导出为包含所有规则的 YAML 文件,以便与他人轻松共享。", + "export": "导出模式", + "exporting": "正在导出..." + }, + "importMode": { + "selectLevel": "选择导入模式的位置:", + "import": "导入", + "importing": "导入中...", + "global": { + "label": "全局", + "description": "适用于所有项目。如果导出的模式包含规则文件,则将在全局 .roo/rules-{slug}/ 文件夹中重新创建这些文件。" + }, + "project": { + "label": "项目级", + "description": "仅在此工作区可用。如果导出的模式包含规则文件,则将在 .roo/rules-{slug}/ 文件夹中重新创建这些文件。" + } + }, "globalCustomInstructions": { "title": "所有模式的自定义指令", "description": "这些指令适用于所有模式。它们提供了一套基础行为,可以通过下面的模式特定指令进行增强。<0>了解更多", @@ -164,5 +185,8 @@ }, "deleteMode": "删除模式" }, - "allFiles": "所有文件" + "allFiles": "所有文件", + "advanced": { + "title": "高级" + } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 80bcab26eb..d4ce96e8e4 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant 密钥:", + "advancedConfigLabel": "高级配置", + "searchMinScoreLabel": "搜索分数阈值", + "searchMinScoreDescription": "搜索结果所需的最低相似度分数(0.0-1.0)。较低的值返回更多结果,但可能不太相关。较高的值返回较少但更相关的结果。", + "searchMinScoreResetTooltip": "恢复默认值 (0.4)", "startIndexingButton": "开始索引", "clearIndexDataButton": "清除索引数据", "unsavedSettingsMessage": "请先保存设置再开始索引过程。", @@ -110,6 +114,11 @@ "label": "子任务", "description": "允许创建和完成子任务而无需批准" }, + "followupQuestions": { + "label": "问题", + "description": "在配置的超时时间后自动选择后续问题的第一个建议答案", + "timeoutLabel": "自动选择第一个答案前的等待时间" + }, "execute": { "label": "执行", "description": "自动执行白名单中的命令而无需批准", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 9a20f129a3..e7a476cb37 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -234,15 +234,17 @@ "tokens": "tokens" }, "followUpSuggest": { - "copyToInput": "複製到輸入框(或按住 Shift 並點選)" + "copyToInput": "複製到輸入框(或按住 Shift 並點選)", + "autoSelectCountdown": "{{count}}秒後自動選擇", + "countdownDisplay": "{{count}}秒" }, "announcement": { "title": "🎉 Roo Code {{version}} 已發布", - "description": "Roo Code {{version}} 帶來基於您意見回饋的重要新功能與改進。", + "description": "Roo Code {{version}} 帶來強大的新功能和重大改進,提升您的開發工作流程。", "whatsNew": "新功能", - "feature1": "Roo 市場正式上線: 市場現已上線!比以往更輕鬆地探索並安裝模式和 MCP。", - "feature2": "Gemini 2.5 模型: 新增對新版 Gemini 2.5 Pro、Flash 和 Flash Lite 模型的支援。", - "feature3": "Excel 檔案支援及更多: 新增 Excel (.xlsx) 檔案支援以及大量錯誤修復和改進!", + "feature1": "一鍵分享工作:只需一鍵即可立即與同事和社群分享您的工作。", + "feature2": "全域 .roo 目錄支援:從全域 .roo 目錄載入規則和設定,確保專案間設定一致。", + "feature3": "改進的 Architect 到 Code 轉換:從 Architect 模式的規劃到 Code 模式的實作,轉換更加順暢。", "hideButton": "隱藏公告", "detailsDiscussLinks": "在 DiscordReddit 取得更多詳細資訊並參與討論 🚀" }, @@ -294,7 +296,8 @@ "codebaseSearch": { "wantsToSearch": "Roo 想要搜尋程式碼庫:{{query}}", "wantsToSearchWithPath": "Roo 想要在 {{path}} 中搜尋:{{query}}", - "didSearch": "找到 {{count}} 個結果:{{query}}" + "didSearch": "找到 {{count}} 個結果:{{query}}", + "resultTooltip": "相似度評分:{{score}} (點擊開啟檔案)" }, "read-batch": { "approve": { @@ -310,5 +313,8 @@ "indexed": "已索引", "error": "索引錯誤", "status": "索引狀態" + }, + "versionIndicator": { + "ariaLabel": "版本 {{version}} - 點擊查看發布說明" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/prompts.json b/webview-ui/src/i18n/locales/zh-TW/prompts.json index e853a5d91d..9e03e35147 100644 --- a/webview-ui/src/i18n/locales/zh-TW/prompts.json +++ b/webview-ui/src/i18n/locales/zh-TW/prompts.json @@ -4,6 +4,8 @@ "modes": { "title": "模式", "createNewMode": "建立新模式", + "importMode": "匯入模式", + "noMatchFound": "找不到任何模式", "editModesConfig": "編輯模式設定", "editGlobalModes": "編輯全域模式", "editProjectModes": "編輯專案模式 (.roomodes)", @@ -50,6 +52,25 @@ "description": "為 {{modeName}} 模式新增專屬的行為指南。", "loadFromFile": "{{mode}} 模式的自訂指令也可以從工作區的 .roo/rules-{{slug}}/ 資料夾載入(.roorules-{{slug}} 和 .clinerules-{{slug}} 已棄用並將很快停止運作)。" }, + "exportMode": { + "title": "匯出模式", + "description": "將此模式匯出為包含所有規則的 YAML 檔案,以便與他人輕鬆分享。", + "export": "匯出模式", + "exporting": "正在匯出..." + }, + "importMode": { + "selectLevel": "選擇匯入模式的位置:", + "import": "匯入", + "importing": "匯入中...", + "global": { + "label": "全域", + "description": "適用於所有專案。規則將合併到自訂指令中。" + }, + "project": { + "label": "專案級", + "description": "僅在此工作區可用。如果匯出的模式包含規則檔案,則將在 .roo/rules-{slug}/ 資料夾中重新建立這些檔案。" + } + }, "globalCustomInstructions": { "title": "所有模式的自訂指令", "description": "這些指令適用於所有模式。它們提供了一組基本行為,可以透過下方的模式專屬自訂指令來強化。<0>了解更多", @@ -164,5 +185,8 @@ }, "deleteMode": "刪除模式" }, - "allFiles": "所有檔案" + "allFiles": "所有檔案", + "advanced": { + "title": "進階" + } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 033230ebb4..be6ad5d80c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -56,6 +56,10 @@ "ollamaUrlLabel": "Ollama URL:", "qdrantUrlLabel": "Qdrant URL", "qdrantKeyLabel": "Qdrant 金鑰:", + "advancedConfigLabel": "進階設定", + "searchMinScoreLabel": "搜尋分數閾值", + "searchMinScoreDescription": "搜尋結果所需的最低相似度分數(0.0-1.0)。較低的值會傳回更多結果,但可能較不相關。較高的值會傳回較少但更相關的結果。", + "searchMinScoreResetTooltip": "重設為預設值 (0.4)", "startIndexingButton": "開始索引", "clearIndexDataButton": "清除索引資料", "unsavedSettingsMessage": "請先儲存設定再開始索引程序。", @@ -110,6 +114,11 @@ "label": "子工作", "description": "允許建立和完成子工作而無需核准" }, + "followupQuestions": { + "label": "問題", + "description": "在設定的逾時時間後自動選擇後續問題的第一個建議答案", + "timeoutLabel": "自動選擇第一個答案前的等待時間" + }, "execute": { "label": "執行", "description": "自動執行允許的終端機命令而無需核准", diff --git a/webview-ui/src/utils/test-utils.tsx b/webview-ui/src/utils/test-utils.tsx new file mode 100644 index 0000000000..05560c3065 --- /dev/null +++ b/webview-ui/src/utils/test-utils.tsx @@ -0,0 +1,21 @@ +import React from "react" +import { render, RenderOptions } from "@testing-library/react" +import { TooltipProvider } from "@/components/ui/tooltip" +import { STANDARD_TOOLTIP_DELAY } from "@/components/ui/standard-tooltip" + +interface AllTheProvidersProps { + children: React.ReactNode +} + +const AllTheProviders = ({ children }: AllTheProvidersProps) => { + return {children} +} + +const customRender = (ui: React.ReactElement, options?: Omit) => + render(ui, { wrapper: AllTheProviders, ...options }) + +// re-export everything +export * from "@testing-library/react" + +// override render method +export { customRender as render }