diff --git a/.roo/commands/release.md b/.roo/commands/release.md new file mode 100644 index 0000000000..ec54b804d1 --- /dev/null +++ b/.roo/commands/release.md @@ -0,0 +1,40 @@ +--- +description: "Create a new release of the Roo Code extension" +argument-hint: patch | minor | major +--- + +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,closingIssuesReferences --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt, issues: .closingIssuesReferences}] | sort_by(.number)'` +3. For each PR with linked issues, fetch the issue details to get the issue reporter: `gh issue view ISSUE_NUMBER --json number,author -q '{number, reporter: .author.login}'` +4. Summarize the changes. If the user did not specify, ask them whether this should be a major, minor, or patch release. +5. 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!) +- For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)" +- For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)" +- Provide brief descriptions of each item to explain the change +- Order the list from most important to least important +- Example formats: + - With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)" + - Without issue: "- 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. + +6. If a major or minor release: + - Ask the user what the three most important areas to highlight are in the 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) + - Ask the user to confirm that the English version looks good to them before proceeding + - 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 (The READMEs as well as the translation strings) +7. Create a new branch for the release preparation: `git checkout -b release/v[version]` +8. Commit and push the changeset file and any documentation updates to the repository: `git add . && git commit -m "chore: add changeset for v[version]" && git push origin release/v[version]` +9. Create a pull request for the release: `gh pr create --title "Release v[version]" --body "Release preparation for v[version]. This PR includes the changeset and any necessary documentation updates." --base main --head release/v[version]` +10. 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 diff --git a/.roo/roomotes.yml b/.roo/roomotes.yml new file mode 100644 index 0000000000..ecc4da61ee --- /dev/null +++ b/.roo/roomotes.yml @@ -0,0 +1,29 @@ +version: "1.0" + +commands: + - name: Pull latest changes + run: git pull + timeout: 60 + execution_phase: task_run + - name: Install dependencies + run: pnpm install + timeout: 60 + execution_phase: task_run + +github_events: + - event: issues.opened + action: + name: github.issue.fix + - event: issue_comment.created + action: + name: github.issue.comment.respond + - event: pull_request.opened + action: + name: github.pr.review + - event: pull_request.opened + action: + name: general.task + prompt: "Check this pull request for any missing translations in the codebase. First, run the check-translations script using `node scripts/find-missing-translations.js` and carefully review its output for any missing translations. Then look for hardcoded strings that should be internationalized, but focus only on hardcoded strings that were added in this PR rather than existing strings. Verify that all UI text uses i18n functions, and ensure translation files are updated for all supported languages. If the script reports missing translations or you find other translation issues, use the translator mode to add them." + - event: pull_request_review_comment.created + action: + name: github.pr.comment.respond diff --git a/.roo/rules-docs-extractor/1_extraction_workflow.xml b/.roo/rules-docs-extractor/1_extraction_workflow.xml index 936cba7edd..088fc1ae89 100644 --- a/.roo/rules-docs-extractor/1_extraction_workflow.xml +++ b/.roo/rules-docs-extractor/1_extraction_workflow.xml @@ -1,8 +1,10 @@ - The Docs Extractor mode analyzes features to generate documentation. - It extracts technical details, business logic, and user workflows - for different audiences. + The Docs Extractor mode has two primary functions: + 1. Extract technical and non-technical details about features to provide to documentation teams + 2. Verify existing documentation for factual accuracy against the codebase + + This mode does not generate final documentation but provides detailed analysis and verification. @@ -10,25 +12,23 @@ Parse Request Identify the feature or component in the user's request. - Determine if the request is for a review or to generate new documentation. - Default to user-friendly docs unless technical output is requested. - Note any specific areas to emphasize. + Determine if the request is for extraction or verification. + For extraction: Note what level of detail is needed (technical vs non-technical). + For verification: Identify the documentation to be verified. + Note any specific areas to emphasize or check. - The initial request determines the workflow path (review vs. generation). + The mode branches into extraction or verification based on the request. Discover Feature - Find related code with semantic search. + Locate relevant code using appropriate search methods. Identify entry points and components. Map the high-level architecture. + Use any combination of tools to understand the feature. - -[feature name] implementation main entry point - - ]]> + Use the most effective discovery method for the situation - file exploration, search, or direct navigation. @@ -66,22 +66,68 @@ + + UI/UX and User Experience Analysis + + + Analyze user interface components +
+ - UI components and their interactions + - Forms, buttons, navigation elements + - Visual feedback and loading states + - Responsive design considerations + - Accessibility features +
+
+ + Map user journeys and interactions +
+ - Step-by-step user workflows + - Click paths and navigation flows + - User decision points + - Input validation and error messaging + - Success and failure scenarios +
+
+ + Document user experience elements +
+ - Page layouts and information architecture + - Interactive elements and their behaviors + - Tooltips, help text, and guidance + - Confirmation dialogs and warnings + - Progress indicators and status updates +
+
+ + Capture visual and behavioral patterns +
+ - Color schemes and theming + - Animation and transitions + - Keyboard shortcuts and accessibility + - Mobile vs desktop experiences + - Browser-specific considerations +
+
+
+
+ Business Logic Extraction - Map workflows + Map workflows from user perspective
- - User journey + - User journey through the feature - Decision points and branching - - State transitions - - Roles and permissions + - State transitions visible to users + - Roles and permissions affecting UI
Document business rules
- - Validation logic + - Validation logic and user feedback - Formulas and algorithms - Business process implementations - Compliance requirements @@ -92,8 +138,8 @@
- Primary use cases - Edge cases - - Error scenarios - - Performance factors + - Error scenarios and user recovery + - Performance factors affecting UX
@@ -199,38 +245,117 @@ - - Workflow branches here: review existing docs or generate new docs. - - Path 1: Review and Recommend - Used when a document is provided for review. - - Compare provided docs against codebase analysis. - Identify inaccuracies, omissions, and areas for improvement. - Categorize issues by severity (Critical, Major, Minor). - Formulate a structured recommendation in chat. - Do not write files. - Final output is only the recommendation. - - - - Path 2: Generate Documentation - Used when new documentation is requested. - - Select a template from `2_documentation_patterns.xml`. - Structure the document with clear sections and examples. - Create `DOCS-TEMP-[feature].md` with generated content. - Apply tone and examples from `7_user_friendly_examples.xml`. - - - + + + Extract Feature Details + Analyze and extract comprehensive details for documentation team + + + Compile Technical Details + + List all technical components and their relationships + Document APIs, data structures, and algorithms + Extract configuration options and their impacts + Identify error handling and edge cases + Note performance characteristics and limitations + + + + Extract Non-Technical Information + + Describe complete user experience and workflows + Document UI interactions and visual elements + Explain business logic in plain language + Identify user benefits and use cases + Document common scenarios with UI context + Note prerequisites and user-facing dependencies + Capture error messages and user guidance + + + + Create Extraction Report + + Organize findings into clear categories + Separate technical and non-technical information + Include code snippets and examples where helpful + Create `EXTRACTION-[feature].md` with findings + Highlight areas that need special attention in documentation + + + - Executive summary of the feature + - UI/UX analysis and user experience + - Technical details section + - Non-technical/user-facing details + - User workflows and interactions + - Configuration and setup information + - Common use cases with UI context + - Error handling and user guidance + - Potential documentation considerations + + + + + + + Verify Documentation Accuracy + Check existing documentation against codebase reality + + + Analyze Provided Documentation + + Parse the documentation to identify claims and descriptions + Extract technical specifications mentioned + Note user-facing features and workflows described + Identify configuration options and examples provided + + + + Verify Against Codebase + + Check technical claims against actual implementation + Verify API endpoints, parameters, and responses + Confirm configuration options and defaults + Validate code examples and snippets + Check if described workflows match implementation + + + + Create Verification Report + + Categorize findings by severity (Critical, Major, Minor) + List all inaccuracies with correct information + Identify missing important information + Note outdated or deprecated content + Provide specific corrections and suggestions + Create `VERIFICATION-[feature].md` with findings + + + - Verification summary (Accurate/Needs Updates) + - Critical inaccuracies that could mislead users + - Technical corrections needed + - Missing information that should be added + - Suggestions for clarity improvements + - Overall recommendations + + + + + - Code paths analyzed - Business logic documented - Integration points mapped - Security addressed - Audience needs met - Metadata and links are complete + + All code paths analyzed + Technical details comprehensively extracted + Non-technical information clearly explained + Use cases and examples provided + Report organized for documentation team use + + + All documentation claims verified + Inaccuracies identified and corrected + Missing information noted + Suggestions for improvement provided + Clear verification report created + \ No newline at end of file diff --git a/.roo/rules-docs-extractor/3_analysis_techniques.xml b/.roo/rules-docs-extractor/3_analysis_techniques.xml index 4ab4cb17cc..b9ef93d1f3 100644 --- a/.roo/rules-docs-extractor/3_analysis_techniques.xml +++ b/.roo/rules-docs-extractor/3_analysis_techniques.xml @@ -3,7 +3,191 @@ Techniques for analyzing code to extract documentation. + + + + Find and analyze UI components and their interactions + + + + Search for UI component files + + +src +\.(tsx|jsx|vue)$|@Component|export.*component +*.tsx + + + + +src + + + ]]> + + + + Analyze styling and visual elements + + +src/styles +true + + + + +src +className=|style=|styled\.|makeStyles|@apply + + ]]> + + + + + + + Map user interactions and navigation flows + + + Route definitions and navigation + Form submissions and validations + Button clicks and event handlers + State changes and UI updates + Loading and error states + + + +src +Route.*path=|router\.push|navigate\(|Link.*to= + + + + +src +onClick=|onSubmit=|onChange=|handleClick|handleSubmit + + + + +src +validate|validation|required|pattern=|minLength|maxLength + + ]]> + + + + + Analyze how the system communicates with users + + + Error messages and alerts + Success notifications + Loading indicators + Tooltips and help text + Confirmation dialogs + Progress indicators + + + +src +toast|notification|alert|message|error.*message|success.*message + + + + +src +loading|isLoading|pending|spinner|skeleton|placeholder + + + + +src +modal|dialog|confirm|popup|overlay + + ]]> + + + + + Check for accessibility features and compliance + + + ARIA labels and roles + Keyboard navigation support + Screen reader compatibility + Focus management + Color contrast considerations + + + +src +aria-|role=|tabIndex|alt=|title=|accessibilityLabel + + + + +src +focus\(|blur\(|onFocus|onBlur|autoFocus|focusable + + ]]> + + + + + Analyze responsive design and mobile experience + + + Breakpoint definitions + Mobile-specific components + Touch event handlers + Viewport configurations + Media queries + + + +src +@media|breakpoint|mobile|tablet|desktop|responsive + + + + +src +onTouch|swipe|gesture|tap|press + + ]]> + + + + + + Use semantic search to find conceptually related code when available. + + + Finding code by concept rather than keywords + Discovering implementations across different naming conventions + When pattern-based search isn't finding expected results + + + +user authentication login security JWT token validation + + + + +payment processing transaction billing invoice checkout + + ]]> + This is an optional tool - use when semantic understanding would help find related code that keyword search might miss + + Analyze entry points to understand feature flow. @@ -14,23 +198,60 @@ Map decision branches. Document input validation. - - -main function app.listen server.start router controller handler - - - - -src/controllers/feature.controller.ts - + + + Start by exploring directory structure + + +src +false + + + +src/controllers +true + + ]]> + + + + Search for specific patterns + src (app\.(get|post|put|delete)|@(Get|Post|Put|Delete)|router\.(get|post|put|delete)) - ]]> + ]]> + + + + Read known entry points directly + + +src/app.ts + + + + +src/controllers/feature.controller.ts + + ]]> + + + + Use semantic search as an alternative discovery method + + +main entry point application startup initialization bootstrap + + ]]> + + @@ -76,24 +297,39 @@ type\s+(Query|Mutation|Subscription)\s*{[^}]+}|@(Query|Mutation|Resolver) Message queue integrations Filesystem operations - + + Start with package.json to understand dependencies + + +package.json + + ]]> + + + + Follow import chains to map dependencies + src ^import\s+.*from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\) - - - -package.json - - + ]]> + + + + Find external API integrations + src (fetch|axios|http\.request|request\(|\.get\(|\.post\() - ]]> + ]]> + + diff --git a/.roo/rules-docs-extractor/4_tool_usage_guide.xml b/.roo/rules-docs-extractor/4_tool_usage_guide.xml index d746141daa..5049917273 100644 --- a/.roo/rules-docs-extractor/4_tool_usage_guide.xml +++ b/.roo/rules-docs-extractor/4_tool_usage_guide.xml @@ -3,125 +3,117 @@ Guidance on using tools for documentation extraction. - - - codebase_search - Initial code discovery. - - - Find feature entry points - -authentication login user session JWT token - - ]]> - - - Find business logic - -calculate pricing discount tax invoice billing - - ]]> - - - Find configuration - -config settings environment variables .env process.env - - ]]> - - - + + + Use the most appropriate tools for the situation + + Start with what you know - file names, directory structure, or keywords + Use multiple discovery methods to build understanding + Adapt your approach based on the codebase structure + + - - list_code_definition_names - Understand code structure. - - Use on core feature directories. - Analyze implementation and test directories. - Look for naming patterns. - - -src/features/authentication - - ]]> - + + + Explore directory structure and find relevant files + + - Starting exploration of a feature area + - Understanding project organization + - Finding configuration or test files + + - - read_file - Analyze specific implementations. - - Read main feature files. - Follow imports to find dependencies. - Read test files for expected behavior. - Examine config and type definition files. - - - - - src/controllers/auth.controller.ts - - - src/services/auth.service.ts - - - src/models/user.model.ts - - - src/types/auth.types.ts - - - src/__tests__/auth.test.ts - - - - ]]> - + + Examine specific files in detail + + - Analyzing implementation details + - Understanding configuration + - Reading documentation or comments + + Read multiple related files together for better context + - - search_files - Find specific patterns. - - - Find API endpoints - -src -@(Get|Post|Put|Delete|Patch)\(['"]([^'"]+)['"]|router\.(get|post|put|delete|patch)\(['"]([^'"]+)['"] - - ]]> - - - Find error handling - -src -throw new \w+Error|catch \(|\.catch\(|try \{ - - ]]> - - - Find config usage - -src -process\.env\.\w+|config\.get\(['"]([^'"]+)['"]|getConfig\(\) - - ]]> - - - - + + Find specific patterns or text + + - Locating API endpoints + - Finding configuration usage + - Tracking down error handling + - Discovering cross-references + + + + + Get overview of code structure + + - Understanding module organization + - Identifying main components + - Finding test coverage + + + + + Semantic search when available + + - Finding conceptually related code + - Discovering implementations by functionality + - When keyword search isn't sufficient + + Optional - use when semantic understanding is needed + + + + + + Start from high-level structure and drill down + + List files in feature directory + Identify main entry points + Follow imports and dependencies + Examine implementation details + + + + + Use tests to understand expected behavior + + Find test files for the feature + Read test descriptions and scenarios + Trace back to implementation + Verify behavior matches tests + + + + + Start with configuration to understand setup + + Find configuration files + Identify feature flags and settings + Trace usage in code + Document impacts of each setting + + + + + Map external interfaces first + + Search for route definitions + Find API controllers or handlers + Trace to business logic + Document request/response flow + + + + - Create documentation file for new docs. - Not used for reviews. Feedback for reviews is provided in chat. - DOCS-TEMP-[feature-name].md + Create extraction or verification report files. + Generates reports for documentation teams, not final documentation. + + - For extraction: EXTRACTION-[feature-name].md + - For verification: VERIFICATION-[feature-name].md + Use descriptive feature name in filename. Include table of contents. @@ -130,7 +122,7 @@ -DOCS-TEMP-authentication-system.md +EXTRACTION-authentication-system.md # Authentication System Documentation @@ -184,47 +176,72 @@ The authentication system provides secure user authentication using JWT tokens.. - Find all files related to a feature. + Find all files related to a feature using various methods. - - - Start with semantic search. - -feature implementation main logic - - ]]> - - - List directory structure. + + + Start by exploring likely directories -src/features +src +false + + + + +src/features/[feature-name] true ]]> - - - Find related tests. + + + + Search for feature-related patterns + +src +feature-name|FeatureName + + + src describe\(['"].*Feature.*['"]|test\(['"].*feature.*['"] *.test.ts ]]> - - - Find config files. + + + + Find configuration files + +config +true + + + . feature.*config|settings.*feature *.json ]]> - - + + + + Use semantic search if available and helpful + + +feature implementation main logic + + ]]> + This is optional - use when other methods aren't sufficient + + diff --git a/.roo/rules-docs-extractor/5_complete_extraction_examples.xml b/.roo/rules-docs-extractor/5_complete_extraction_examples.xml index 3975b5b262..8c644e2f03 100644 --- a/.roo/rules-docs-extractor/5_complete_extraction_examples.xml +++ b/.roo/rules-docs-extractor/5_complete_extraction_examples.xml @@ -1,42 +1,54 @@ - Examples of the documentation extraction workflow. + Examples of both documentation extraction and verification workflows demonstrating flexible discovery methods and comprehensive UI/UX analysis. - Extract documentation for a JWT-based authentication system, including login, token refresh, and RBAC. + Extract comprehensive documentation for a JWT-based authentication system, including technical implementation, UI/UX elements, and user workflows. - Discover feature + Initialize and discover feature using flexible methods -authentication JWT login logout token refresh auth middleware - + + +src +false + + ]]> + Look for auth-related directories like auth/, authentication/, or security/ + +src/auth +true + ]]> - - Auth controllers, services, middleware, models, and routes. + - Auth controllers, services, middleware, models, and routes + - Login components and forms + - Session management UI - Analyze structure + Analyze code structure and architecture src/auth ]]> - - Identify main classes/functions. - - Map auth flow. - - Find token generation/validation logic. + - Identify main classes/functions + - Map authentication flow + - Find token generation/validation logic + - Locate UI components - Read core files + Read core implementation files @@ -61,20 +73,74 @@ - Extract API endpoints + UI/UX Analysis - Discover UI components + + +src/components +(Login|Auth|Session|Password).*\.(tsx?|jsx?) +*.tsx + + ]]> + + + + src/components/LoginForm.tsx + + + src/components/SessionManager.tsx + + + src/components/PasswordReset.tsx + + + + ]]> + + + + UI/UX Analysis - Map user interactions + +src/components +onClick|onSubmit|onChange|handleSubmit|validate + + ]]> + + - Form validation patterns + - User feedback mechanisms + - Error handling UI + - Loading states + + + + + UI/UX Analysis - Visual patterns and accessibility + +src +className=|style=|theme\.|aria-|role= +*.tsx + + ]]> + +src/styles +\.login|\.auth|\.session +*.css + + ]]> + + + + Extract API endpoints and configuration src/auth @(Post|Get)\(['"]\/auth\/[^'"]+['"]|router\.(post|get)\(['"]\/auth\/[^'"]+['"] ]]> - - - POST /auth/login, POST /auth/logout, POST /auth/refresh, GET /auth/profile, POST /auth/register - - - - - Extract configuration src @@ -83,542 +149,573 @@ ]]> - - Analyze security + + Optional: Use semantic search for deeper insights -src/auth -bcrypt|hash|salt|encrypt|sanitize|validate|@IsEmail|@MinLength - + + +authentication error handling user feedback messages + ]]> + Used when specific patterns don't capture all relevant error handling - - Generate docs + + Create comprehensive extraction report -DOCS-TEMP-authentication-system.md +EXTRACTION-authentication-system.md -# Authentication System Documentation +# Authentication System - Feature Extraction Report -## Table of Contents -1. [Overview](#overview) -2. [Quick Start](#quick-start) -3. [Architecture](#architecture) -4. [API Reference](#api-reference) -5. [Configuration](#configuration) -6. [Security](#security) -7. [User Guide](#user-guide) -8. [Developer Guide](#developer-guide) -9. [Troubleshooting](#troubleshooting) +## Executive Summary +The authentication system is a JWT-based security implementation that handles user registration, login, session management, and access control. It provides a comprehensive user experience with visual feedback, accessibility features, and robust error handling. ---- +## UI/UX Analysis -## Overview +### User Interface Components -### Purpose -The authentication system provides secure user authentication using JWT (JSON Web Tokens) with support for: -- User registration and login -- Token-based authentication -- Refresh token rotation -- Role-based access control (RBAC) -- Session management -- Password reset functionality +#### 1. Login Page (`src/components/LoginForm.tsx`) +**Visual Layout:** +- Centered card design with shadow (400px width) +- Company logo at top +- Form fields with floating labels +- Primary blue theme (#1976d2) -### Key Features -- **JWT Authentication**: Stateless authentication using signed tokens -- **Refresh Tokens**: Long-lived tokens for seamless user experience -- **Role-Based Access**: Fine-grained permission control -- **Security**: bcrypt password hashing, rate limiting, CSRF protection -- **Multi-device Support**: Token management across devices +**Interactive Elements:** +- Email input field + - Real-time validation (regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/) + - Error state: Red border and helper text + - Success state: Green checkmark icon +- Password field + - Show/hide toggle button (eye icon) + - Minimum 8 characters validation + - Caps lock warning indicator +- "Remember me" checkbox with 30-day persistence +- "Forgot password?" link (underlined on hover) +- Submit button + - Disabled state: Gray background until valid input + - Loading state: Spinner replaces text + - Success state: Checkmark animation -### Version Information -- **Current Version**: 2.1.0 -- **Minimum Node.js**: 14.0.0 -- **Dependencies**: - - jsonwebtoken: ^9.0.0 - - bcrypt: ^5.1.0 - - passport: ^0.6.0 +**User Feedback:** +- Loading overlay with spinner during authentication +- Error messages appear with slide-down animation +- Success toast notification (3s duration) +- Form shake animation on error ---- +#### 2. Registration Form (`src/components/RegisterForm.tsx`) +**Multi-Step Design:** +- Progress bar showing 3 steps +- Smooth slide transitions between steps +- Back/Next navigation buttons -## Quick Start +**Step 1 - Account Info:** +- Email field with async availability check +- Password field with strength meter (5 levels) +- Password confirmation with match validation -### For Users -1. Register a new account: - ```bash - POST /api/auth/register - { - "email": "user@example.com", - "password": "SecurePassword123!", - "name": "John Doe" - } - ``` +**Step 2 - Personal Info:** +- First/Last name fields +- Optional phone with format mask +- Country dropdown with flag icons -2. Login to receive tokens: - ```bash - POST /api/auth/login - { - "email": "user@example.com", - "password": "SecurePassword123!" - } - ``` +**Step 3 - Terms & Submit:** +- Terms of service scrollable text +- Privacy policy link (opens modal) +- Checkbox required for submission +- Review summary before final submit -3. Use the access token in subsequent requests: - ```bash - Authorization: Bearer - ``` +**Visual Feedback:** +- Field validation on blur +- Progress saved in localStorage +- Success confetti animation +- Auto-redirect countdown (5s) -### For Developers -```typescript -// Import authentication module -import { AuthModule } from './auth/auth.module'; +#### 3. Session Management (`src/components/SessionManager.tsx`) +**Device List UI:** +- Card-based layout for each session +- Device icons (FontAwesome) + - fa-mobile for mobile + - fa-desktop for desktop + - fa-tablet for tablet +- Information displayed: + - Device name and browser + - IP address (partially masked) + - Last active (relative time) + - Location (city, country) -// Configure in app module -@Module({ - imports: [ - AuthModule.forRoot({ - jwtSecret: process.env.JWT_SECRET, - jwtExpiration: '15m', - refreshExpiration: '7d' - }) - ] -}) -export class AppModule {} +**Interactive Features:** +- Current device highlighted with blue border +- Hover state shows "Revoke" button +- Confirmation modal with device details +- Bulk selection with checkboxes +- "Revoke All" with double confirmation + +### User Experience Elements + +#### Visual Patterns +**Theme System:** +```css +--primary-color: #1976d2; +--error-color: #d32f2f; +--success-color: #388e3c; +--warning-color: #f57c00; +--text-primary: rgba(0, 0, 0, 0.87); +--text-secondary: rgba(0, 0, 0, 0.6); ``` ---- +**Animations:** +- Page transitions: 300ms ease-in-out +- Button hover: scale(1.02) +- Error shake: 0.5s horizontal +- Success checkmark: SVG path animation +- Loading spinner: 1s rotation -## Architecture +**Responsive Breakpoints:** +- Mobile: < 768px (single column) +- Tablet: 768px - 1024px +- Desktop: > 1024px -### System Overview +#### Accessibility Features +**Keyboard Navigation:** +- Tab order follows visual flow +- Enter key submits forms +- Escape closes modals +- Arrow keys in dropdowns + +**Screen Reader Support:** +- ARIA labels on all inputs +- Live regions for errors +- Role attributes for custom components +- Descriptive button text + +**Visual Accessibility:** +- 4.5:1 contrast ratio minimum +- Focus indicators (2px outline) +- Error icons for colorblind users +- Scalable fonts (rem units) + +### User Workflows + +#### 1. First-Time Registration ``` -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ -│ Client │────▶│ Auth Guard │────▶│ Service │ -└─────────────┘ └──────────────┘ └─────────────┘ - │ │ - ▼ ▼ - ┌──────────────┐ ┌─────────────┐ - │ JWT Strategy │ │ Database │ - └──────────────┘ └─────────────┘ +Start → Landing Page → "Get Started" CTA + ↓ +Registration Form (Step 1) + → Email validation (async) + → Password strength check + → Real-time feedback + ↓ +Personal Info (Step 2) + → Optional fields clearly marked + → Format validation + ↓ +Terms Agreement (Step 3) + → Must scroll to enable checkbox + → Review summary + ↓ +Submit → Loading → Success + → Confetti animation + → Welcome email sent + → Auto-redirect (5s) + ↓ +Dashboard (First-time tour) ``` -### Components -- **AuthController**: Handles HTTP requests for authentication endpoints -- **AuthService**: Core authentication logic and token management -- **JwtStrategy**: Passport strategy for JWT validation -- **AuthGuard**: Route protection middleware -- **UserService**: User management and database operations - -### Token Flow -1. User provides credentials -2. System validates credentials against database -3. Generate access token (short-lived) and refresh token (long-lived) -4. Client stores tokens securely -5. Access token used for API requests -6. Refresh token used to obtain new access token - ---- - -## API Reference - -### Authentication Endpoints - -#### `POST /api/auth/register` -Register a new user account. - -**Request Body**: -```json -{ - "email": "string (required)", - "password": "string (required, min 8 chars)", - "name": "string (required)", - "role": "string (optional, default: 'user')" -} +#### 2. Returning User Login +``` +Start → Login Page + ↓ +Enter Credentials + → Email autocomplete + → Password manager integration + → "Remember me" option + ↓ +Submit → Loading (avg 1.2s) + ↓ +Success → Dashboard + OR +Error → Inline feedback + → Retry with guidance + → "Forgot password?" option ``` -**Response** (201 Created): -```json -{ - "user": { - "id": "uuid", - "email": "user@example.com", - "name": "John Doe", - "role": "user", - "createdAt": "2024-01-01T00:00:00Z" - }, - "tokens": { - "accessToken": "jwt_token", - "refreshToken": "refresh_token", - "expiresIn": 900 - } -} +#### 3. Password Reset Flow +``` +Login Page → "Forgot password?" + ↓ +Modal Dialog + → Email input + → Captcha (if multiple attempts) + ↓ +Submit → "Check email" message + ↓ +Email Received (< 1 min) + → Secure link (1hr expiry) + ↓ +Reset Page + → New password requirements shown + → Strength meter + → Confirmation field + ↓ +Submit → Success → Login redirect ``` -**Error Responses**: -- `400 Bad Request`: Invalid input data -- `409 Conflict`: Email already exists +## Technical Details -#### `POST /api/auth/login` -Authenticate user and receive tokens. +### Core Components +1. **AuthController** (`src/auth/auth.controller.ts`) + - REST endpoints with validation decorators + - Rate limiting middleware + - CORS configuration -**Request Body**: -```json -{ - "email": "string (required)", - "password": "string (required)" -} +2. **AuthService** (`src/auth/auth.service.ts`) + - JWT token generation/validation + - Bcrypt password hashing + - Session management logic + +3. **Security Implementation** + - JWT RS256 algorithm + - Refresh token rotation + - CSRF double-submit cookies + - XSS protection headers + +### API Endpoints +| Method | Endpoint | Description | Rate Limit | +|--------|----------|-------------|------------| +| POST | /auth/register | New user registration | 3/hour | +| POST | /auth/login | User authentication | 5/min | +| POST | /auth/refresh | Token refresh | 10/min | +| POST | /auth/logout | Session termination | None | +| GET | /auth/profile | Current user data | None | +| POST | /auth/reset-password | Password reset | 3/hour | + +### Configuration +```env +# Required +JWT_SECRET=minimum-32-character-secret +DATABASE_URL=postgresql://... + +# Optional with defaults +JWT_EXPIRATION=15m +REFRESH_TOKEN_EXPIRATION=7d +BCRYPT_ROUNDS=10 +SESSION_MAX_AGE=30d +MAX_SESSIONS_PER_USER=5 ``` -**Response** (200 OK): -```json -{ - "user": { - "id": "uuid", - "email": "user@example.com", - "name": "John Doe", - "role": "user" - }, - "tokens": { - "accessToken": "jwt_token", - "refreshToken": "refresh_token", - "expiresIn": 900 - } -} -``` +## Non-Technical Information -**Error Responses**: -- `401 Unauthorized`: Invalid credentials -- `429 Too Many Requests`: Rate limit exceeded +### Business Rules +1. **Account Creation** + - Unique email required + - Password: 8+ chars, mixed case, number, special + - Email verification within 24 hours + - Terms acceptance mandatory -#### `POST /api/auth/refresh` -Refresh access token using refresh token. +2. **Session Management** + - Max 5 concurrent sessions + - Idle timeout: 30 minutes + - Absolute timeout: 7 days + - Device trust for 30 days -**Request Body**: -```json -{ - "refreshToken": "string (required)" -} -``` +3. **Security Policies** + - Account lockout: 5 failed attempts (15 min) + - Password history: Last 3 not reusable + - 2FA optional but recommended + - Suspicious login notifications -**Response** (200 OK): -```json -{ - "accessToken": "new_jwt_token", - "expiresIn": 900 -} -``` +### Common User Scenarios -#### `POST /api/auth/logout` -Invalidate refresh token. +#### Mobile Experience +- Touch-optimized buttons (44px min) +- Biometric login (Face ID/Touch ID) +- Simplified navigation menu +- Offline detection with retry +- Push notification for new sessions -**Headers**: -- `Authorization: Bearer ` +#### Error Recovery +- Network timeout: Auto-retry with backoff +- Session expired: Smooth re-login flow +- Form errors: Contextual help text +- Server errors: Friendly messages with support link -**Request Body**: -```json -{ - "refreshToken": "string (required)" -} -``` +### Performance Metrics +- Login response: 200ms (p50), 500ms (p95) +- Page load: 1.2s (3G), 400ms (4G) +- Token validation: < 10ms +- Session check: < 50ms -**Response** (200 OK): -```json -{ - "message": "Logged out successfully" -} -``` +## Documentation Recommendations ---- +### Critical Areas for User Documentation +1. **Getting Started Guide** + - Screenshots of each registration step + - Common email provider settings + - Password manager setup -## Configuration +2. **Troubleshooting Section** + - "Why can't I log in?" flowchart + - Browser compatibility matrix + - Cookie/JavaScript requirements -### Environment Variables +3. **Security Best Practices** + - How to spot phishing attempts + - Importance of unique passwords + - When to revoke sessions -| Variable | Type | Default | Description | -|----------|------|---------|-------------| -| `JWT_SECRET` | string | - | Secret key for signing JWT tokens (required) | -| `JWT_EXPIRATION` | string | '15m' | Access token expiration time | -| `REFRESH_TOKEN_EXPIRATION` | string | '7d' | Refresh token expiration time | -| `BCRYPT_ROUNDS` | number | 10 | Number of bcrypt hashing rounds | -| `AUTH_RATE_LIMIT` | number | 5 | Max login attempts per minute | -| `ENABLE_2FA` | boolean | false | Enable two-factor authentication | +### Developer Integration Guide +1. **API Authentication** + - Bearer token format + - Refresh token flow diagram + - Error response examples -### Configuration File (auth.config.ts) -```typescript -export const authConfig = { - jwt: { - secret: process.env.JWT_SECRET, - signOptions: { - expiresIn: process.env.JWT_EXPIRATION || '15m', - issuer: 'your-app-name', - audience: 'your-app-users' - } - }, - bcrypt: { - rounds: parseInt(process.env.BCRYPT_ROUNDS || '10') - }, - session: { - maxDevices: 5, - inactivityTimeout: '30d' - } -}; -``` +2. **SDK Examples** + - JavaScript/TypeScript + - Python + - Mobile (iOS/Android) ---- +## Integration Points +- Email service for password reset and notifications +- Session storage (Redis optional, in-memory default) +- Rate limiting middleware +- CORS configuration for cross-origin requests +- Logging service for audit trails -## Security - -### Authentication Flow -1. **Password Storage**: Passwords hashed using bcrypt with configurable rounds -2. **Token Security**: JWT tokens signed with RS256 algorithm -3. **Refresh Token Rotation**: New refresh token issued on each refresh -4. **Rate Limiting**: Prevents brute force attacks on login endpoint - -### Security Best Practices -- Store tokens securely (httpOnly cookies recommended) -- Implement CSRF protection for cookie-based auth -- Use HTTPS in production -- Rotate JWT secrets periodically -- Implement account lockout after failed attempts -- Enable 2FA for sensitive accounts - -### Common Vulnerabilities Addressed -- **SQL Injection**: Parameterized queries -- **XSS**: Input sanitization and validation -- **CSRF**: Token validation -- **Brute Force**: Rate limiting and account lockout -- **Token Hijacking**: Short expiration times and refresh rotation - ---- - -## User Guide - -### Registration Process -1. Navigate to registration page -2. Enter email, password, and name -3. Verify email (if enabled) -4. Login with credentials - -### Managing Sessions -- View active sessions in account settings -- Revoke sessions from other devices -- Set session timeout preferences - -### Password Management -- Change password from profile settings -- Reset forgotten password via email -- Password requirements: - - Minimum 8 characters - - At least one uppercase letter - - At least one number - - At least one special character - ---- - -## Developer Guide - -### Protecting Routes -```typescript -// Use AuthGuard decorator -@UseGuards(AuthGuard('jwt')) -@Get('protected') -async getProtectedData() { - return { data: 'This is protected' }; -} - -// Role-based protection -@UseGuards(AuthGuard('jwt'), RolesGuard) -@Roles('admin') -@Get('admin') -async getAdminData() { - return { data: 'Admin only' }; -} -``` - -### Custom Authentication Logic -```typescript -// Extend AuthService -export class CustomAuthService extends AuthService { - async validateUser(email: string, password: string): Promise { - // Add custom validation logic - const user = await super.validateUser(email, password); - - // Additional checks - if (user.suspended) { - throw new UnauthorizedException('Account suspended'); - } - - return user; - } -} -``` - -### Testing Authentication -```typescript -describe('AuthController', () => { - it('should login user', async () => { - const response = await request(app.getHttpServer()) - .post('/auth/login') - .send({ - email: 'test@example.com', - password: 'TestPass123!' - }) - .expect(200); - - expect(response.body).toHaveProperty('tokens.accessToken'); - }); -}); -``` - ---- - -## Troubleshooting - -### Common Issues - -#### Invalid Token Error -**Problem**: "JsonWebTokenError: invalid token" -**Solutions**: -- Verify token format (Bearer prefix) -- Check token expiration -- Ensure JWT_SECRET matches - -#### Login Rate Limit -**Problem**: "429 Too Many Requests" -**Solutions**: -- Wait for rate limit window to reset -- Check AUTH_RATE_LIMIT configuration -- Implement exponential backoff - -#### CORS Issues -**Problem**: "Access blocked by CORS policy" -**Solutions**: -- Configure CORS middleware -- Add origin to allowed list -- Check preflight requests - -### Debug Mode -Enable debug logging: -```bash -DEBUG=auth:* npm start -``` - -### Support -- GitHub Issues: [github.com/yourapp/issues](https://github.com/yourapp/issues) -- Documentation: [docs.yourapp.com/auth](https://docs.yourapp.com/auth) -- Email: support@yourapp.com - ---- - -## Changelog - -### v2.1.0 (2024-01-15) -- Added refresh token rotation -- Improved rate limiting -- Fixed security vulnerability in password reset - -### v2.0.0 (2023-12-01) -- Breaking: Changed token format -- Added 2FA support -- Improved session management - -### Migration Guide (v1.x to v2.x) -1. Update JWT_SECRET format -2. Run token migration script -3. Update client-side token handling - ---- - -## References -- [JWT.io](https://jwt.io) - JWT Documentation -- [OWASP Authentication Guide](https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet) -- [Passport.js Documentation](http://www.passportjs.org/docs/) +## Summary for Documentation Team +This extraction report provides comprehensive details about the authentication system's current implementation. The system offers a complete user experience with visual feedback, accessibility features, and robust security measures. Key areas for user documentation include the multi-step registration process, session management features, and security policies. The technical implementation uses industry-standard JWT tokens with proper security measures. -450 +354 ]]> - Use semantic search to find related files. - Read multiple files for context. - Extract API docs from route definitions. - Use tests to understand behavior. - Document security measures. - Include troubleshooting for common errors. + Use flexible discovery methods - directory exploration and pattern search are primary tools + Comprehensive UI/UX analysis is essential for complete documentation + Document visual elements, interactions, and user feedback mechanisms + Include accessibility and responsive design considerations + Semantic search (codebase_search) is optional for deeper insights when needed + Organize findings to support documentation team's work - + - Extract documentation for database models, relationships, and migrations. + Verify user-provided documentation about authentication UI/UX against the actual implementation. - Find DB files - -database schema model entity migration table column relationship - - ]]> + User provides documentation to verify + + "Please verify our login documentation: + + 'The login form provides a seamless experience with instant validation. Users see a green checkmark when their email is valid. The password field includes a strength indicator. After 3 failed attempts, users must wait 5 minutes before trying again.'" + - Analyze models - -src/models -@(Entity|Table|Model)|class\s+\w+\s+extends\s+(Model|BaseEntity) - - ]]> + Analyze the documentation claims + + Claims to verify: + - Instant email validation with green checkmark + - Password strength indicator on login + - 3 failed attempts triggers 5-minute wait + - Extract relationships + Find login UI components using directory exploration + +src/components +false + + ]]> -src/models -@(OneToMany|ManyToOne|OneToOne|ManyToMany|BelongsTo|HasMany) +src/components +Login|SignIn|Auth.*Form +*.tsx ]]> - Document migrations + Examine login form implementation + + + + src/components/LoginForm.tsx + + + src/components/LoginForm.css + + + + ]]> + + - Email validation exists but shows red X, not green checkmark + - No password strength indicator on login form (only on registration) + - Account lockout is after 5 attempts, not 3 + - Lockout duration is 15 minutes, not 5 + + + + + Verify validation behavior + +src/components +validateEmail|emailValid|checkmark|strength.*indicator + + ]]> + +src/auth +failed.*attempts|lockout|rate.*limit + + ]]> + + + + Create verification report + +VERIFICATION-login-documentation.md + +# Login Documentation - Verification Report + +## Overall Assessment: Needs Significant Updates + +## Summary +The login documentation contains several inaccuracies about the UI/UX implementation that could confuse users and lead to poor user experience. + +## Critical Inaccuracies + +### 1. Email Validation Feedback +**Documentation States**: "Users see a green checkmark when their email is valid" +**Actual Implementation**: Red X icon for invalid emails, no positive feedback +- Location: `src/components/LoginForm.tsx:45-52` +- Current behavior: Only shows error states +- Missing: Success indicator implementation +- **Impact**: Users don't get positive reinforcement for correct input + +### 2. Password Strength Indicator +**Documentation States**: "The password field includes a strength indicator" +**Actual Implementation**: No strength indicator on login form +- Location: Checked `src/components/LoginForm.tsx` +- Strength indicator only exists in `RegisterForm.tsx` +- Login form has basic password field only +- **Impact**: Sets incorrect user expectations + +### 3. Failed Login Attempts +**Documentation States**: "After 3 failed attempts, users must wait 5 minutes" +**Actual Implementation**: 5 attempts, 15-minute lockout +- Location: `src/auth/services/auth.service.ts:234` +- Code: `MAX_LOGIN_ATTEMPTS = 5` +- Code: `LOCKOUT_DURATION = 15 * 60 * 1000 // 15 minutes` +- **Impact**: Users may be confused about actual security policy + +## UI/UX Elements Found But Not Documented + +### 1. Loading States +- Spinner overlay during authentication +- Button disabled state during submission +- These improve perceived performance + +### 2. Remember Me Feature +- 30-day persistent session option +- Stored in secure cookie +- Not mentioned in documentation + +### 3. Caps Lock Warning +- Visual indicator when caps lock is on +- Helps prevent login failures +- Important accessibility feature + +## Recommendations + +### Immediate Corrections Needed +1. **Email Validation**: + ``` + CORRECT: "The email field shows a red error icon for invalid formats. Valid emails are accepted without visual confirmation." + ``` + +2. **Password Features**: + ``` + CORRECT: "The login form has a standard password field with show/hide toggle. Password strength indicators are only shown during registration." + ``` + +3. **Security Policy**: + ``` + CORRECT: "After 5 failed login attempts, accounts are temporarily locked for 15 minutes to prevent brute force attacks." + ``` + +### Suggested Additions +1. Document the loading states and visual feedback +2. Explain the "Remember Me" functionality +3. Mention the Caps Lock warning feature +4. Add screenshots of actual UI states + +## Technical References +- Login Form Component: `src/components/LoginForm.tsx` +- Styles: `src/components/LoginForm.css` +- Auth Service: `src/auth/services/auth.service.ts` +- Rate Limiting: `src/middleware/rateLimiter.ts` + +## Visual Discrepancies +- Documentation implies more visual feedback than implemented +- Color scheme in docs doesn't match actual theme +- Missing documentation for error message styling + +## Conclusion +The documentation needs updates to accurately reflect the current login experience. The actual implementation is more security-focused (5 attempts, 15-min lockout) but less visually rich (no success indicators) than documented. + +95 + + ]]> + + + + + Always verify UI/UX claims against actual component implementation + Check both visual elements and behavioral logic + Look for undocumented features that users might expect + Provide specific corrections with code references + Consider the impact of inaccuracies on user experience + + + + + + Extract comprehensive API documentation including endpoints, request/response formats, and error handling. + + + + + Discover API structure using flexible methods + + +src +false + + ]]> -migrations +src/api true ]]> - - Generate schema documentation - - - Entity relationship diagrams - - Table schemas with column types - - Index definitions - - Foreign key constraints - - Migration history - - Query patterns and optimizations - - - - - - - - Extract comprehensive API documentation including all endpoints, - request/response formats, authentication, and examples. - - - - - Find all API routes + + Find all API routes using pattern search src @@ -627,8 +724,8 @@ DEBUG=auth:* npm start ]]> - - Extract request validation + + Extract request validation schemas src @@ -637,296 +734,148 @@ DEBUG=auth:* npm start ]]> - - Find response schemas - -src -@ApiResponse|swagger|openapi|response\.json\(|res\.send\( - - ]]> - - - Document authentication requirements + Analyze error handling and responses src -@(UseGuards|Authorized|Public)|passport\.authenticate|requireAuth +@ApiResponse|response\.status\(|res\.status\(|throw new.*Error ]]> - Generate OpenAPI/Swagger documentation - - - OpenAPI 3.0 specification - - Postman collection - - API client examples - - cURL commands - - SDK usage examples - + Optional: Semantic search for middleware and auth + + +API middleware authentication authorization guards + + ]]> + + + + Generate API extraction report + + - Complete endpoint inventory with methods and paths + - Request/response schemas with examples + - Authentication requirements per endpoint + - Rate limiting and throttling rules + - Error response formats and codes + - API versioning strategy + - + - Document React/Vue/Angular components including props, events, - slots, styling, and usage examples. + Document a React component library including props, styling, accessibility, and usage patterns. - Find component files + Discover component structure + +src/components +true + + ]]> + + + + Analyze component interfaces and props src/components -export\s+(default\s+)?(function|class|const)\s+\w+|@Component +interface\s+\w+Props|type\s+\w+Props|export\s+(default\s+)?function|export\s+const *.tsx ]]> - - Extract component props/inputs + + Extract styling and theme usage src/components -interface\s+\w+Props|type\s+\w+Props|@Input\(\)|props:\s*{ - - ]]> - - - - Find component usage examples - -src - +styled\.|makeStyles|className=|sx=|css= ]]> - Document styling and themes + Document accessibility features src/components -styled\.|makeStyles|@apply|className=|style= +aria-|role=|tabIndex|alt=|htmlFor= ]]> - Extract Storybook stories + Find usage examples and stories src -export\s+default\s+{.*title:|\.stories\. -*.stories.tsx +\.stories\.|\.story\.|examples?/|demo/ +*.tsx ]]> - Generate component documentation + Create component library report - - Component API reference - - Props table with types and defaults - - Event documentation - - Styling guidelines - - Usage examples - - Accessibility notes - - Browser compatibility + - Component hierarchy and relationships + - Props documentation with types and defaults + - Styling system and customization options + - Accessibility compliance checklist + - Interactive examples and code snippets + - Best practices and anti-patterns + - Browser compatibility notes - - - Document all configuration options, environment variables, - feature flags, and their impacts on system behavior. - - - - - Find configuration files - -. -false - - ]]> - - - .env.example - - config/ - - settings.json - - app.config.ts - - - - - Extract environment variables - -. -process\.env\.(\w+)|getenv\(['"](\w+)['"]\) - - ]]> - - - - Find feature flags - -src -feature\.|featureFlag|isEnabled\(['"]|flags\. - - ]]> - - - - Document configuration impacts - - - How each config affects behavior - - Dependencies between configs - - Performance implications - - Security considerations - - Default values and overrides - - - - - Generate configuration guide - - - Complete configuration reference - - Environment setup guide - - Docker/Kubernetes configs - - CI/CD pipeline variables - - Production deployment checklist - - - - - - - Review a user-provided documentation section for accuracy against the codebase - and provide recommendations for improvement in the chat. - - - - User provides documentation for review - - "Here's our current draft for the JWT_SECRET setting. Can you check it for accuracy? 'The `JWT_SECRET` is an optional key for enhancing security.'" - - - - Analyze relevant code - -JWT_SECRET configuration implementation - - ]]> - - - Code where `JWT_SECRET` is read (e.g., `process.env.JWT_SECRET`) - - Logic that fails if the secret is missing - - Default values or error handling related to the secret - - - - Formulate and provide feedback in chat - - -I have reviewed the documentation section provided for the `JWT_SECRET` setting. Here are my recommendations for improvement: - -**Overall Assessment**: The current draft is critically inaccurate and could lead to security vulnerabilities if followed. - -**Recommendations for Improvement**: - -* **Critical Inaccuracy**: - * The documentation states that `JWT_SECRET` is optional. However, the application will fail to start if it is not provided. It is a **required** setting for the application to function securely. - * **Suggestion**: "The `JWT_SECRET` is a **required** environment variable used to sign and verify JSON Web Tokens. The application will not run without it." - -This feedback can be copied and pasted for your documentation team. - - - ]]> - No file is written. The feedback is provided directly to the user in the chat interface. - - - - - - - Ensure all aspects are documented - - Technical implementation details - Business logic and rules - User workflows and journeys - API specifications - Configuration options - Security measures - Performance characteristics - Error handling - Testing strategies - Deployment procedures - - - - - Tailor content for different readers - - - Focus on how-to guides and troubleshooting - - - Include code examples and technical details - - - Emphasize configuration and maintenance - - - Highlight business value and metrics - - - - - - Create documentation that's easy to update + + + Use the most appropriate discovery method - Use clear section headers - Include version information - Add last-updated timestamps - Cross-reference related sections - Provide migration guides + Start with directory exploration for well-organized codebases + Use pattern search for specific syntax or naming conventions + Apply file-based search when you know exact locations + Reserve semantic search for complex conceptual queries - - Include practical examples throughout - - Code snippets with syntax highlighting - API request/response pairs - Configuration examples - Command-line usage - Error scenarios and solutions - + + Ensure complete UI/UX documentation + + Visual design and layout + Interactive elements and states + User feedback mechanisms + Accessibility features + Responsive behavior + Animation and transitions + Error states and recovery + Loading and progress indicators + + + + + Verify all aspects of documentation claims + + Technical accuracy of code examples + UI element descriptions match implementation + User workflows reflect actual behavior + Configuration values are current + Error messages match code + Performance claims are realistic + - - - - Table of contents with working links - All sections properly formatted - Code examples are syntactically correct - No placeholder text remaining - Version information included - Cross-references are valid - Metadata is complete - File follows naming convention - - \ No newline at end of file diff --git a/.roo/rules-docs-extractor/6_communication_guidelines.xml b/.roo/rules-docs-extractor/6_communication_guidelines.xml index 908b1fcfb6..8691f2519c 100644 --- a/.roo/rules-docs-extractor/6_communication_guidelines.xml +++ b/.roo/rules-docs-extractor/6_communication_guidelines.xml @@ -61,29 +61,48 @@ Warn about complex dependency chains. - - + @@ -204,43 +223,55 @@ Status: Stable - Summary of documented feature. - Key findings. - File location. - Next step suggestions (if applicable). + Summary of analysis performed. + Key findings or issues identified. + Report file location. + Recommended next steps. - - +The extraction report contains all details needed for comprehensive documentation. + ]]> + + diff --git a/.roo/rules-issue-writer/1_workflow.xml b/.roo/rules-issue-writer/1_workflow.xml index f24b643e3d..99ef7db5d9 100644 --- a/.roo/rules-issue-writer/1_workflow.xml +++ b/.roo/rules-issue-writer/1_workflow.xml @@ -3,14 +3,24 @@ Initialize Issue Creation Process - When the user requests to create an issue, immediately set up a todo list to track the workflow. + IMPORTANT: This mode assumes the first user message is already a request to create an issue. + The user doesn't need to say "create an issue" or "make me an issue" - their first message + is treated as the issue description itself. + + When the session starts, immediately: + 1. Treat the user's first message as the issue description + 2. Initialize the workflow by using the update_todo_list tool + 3. Begin the issue creation process without asking what they want to do + [ ] Detect current repository information + [ ] Determine repository structure (monorepo/standard) + [ ] Perform initial codebase discovery [ ] Analyze user request to determine issue type - [ ] Gather initial information for the issue + [ ] Gather and verify additional information [ ] Determine if user wants to contribute - [ ] Perform technical analysis (if contributing) + [ ] Perform issue scoping (if contributing) [ ] Draft issue content [ ] Review and confirm with user [ ] Create GitHub issue @@ -21,49 +31,47 @@ - Determine Issue Type + Detect current repository information - Analyze the user's initial request to automatically assess whether they're reporting a bug or proposing a feature. - Look for keywords and context clues: + CRITICAL FIRST STEP: Verify we're in a git repository and get repository information. - Bug indicators: - - Words like "error", "broken", "not working", "fails", "crash", "bug" - - Descriptions of unexpected behavior - - Error messages or stack traces - - References to something that used to work + 1. Check if we're in a git repository: + + git rev-parse --is-inside-work-tree 2>/dev/null || echo "not-git-repo" + - Feature indicators: - - Words like "feature", "enhancement", "add", "implement", "would be nice" - - Descriptions of new functionality - - Suggestions for improvements - - "It would be great if..." + If the output is "not-git-repo", immediately stop and inform the user: - Based on your analysis, order the options with the most likely choice first: + + + This mode must be run from within a GitHub repository. Please navigate to a git repository and try again. + + - - Based on your request, what type of issue would you like to create? - - [If bug indicators found:] - Bug Report - Report a problem with existing functionality - Detailed Feature Proposal - Propose a new feature or enhancement + 2. If in a git repository, get the repository information: + + git remote get-url origin 2>/dev/null | sed -E 's/.*[:/]([^/]+)\/([^/]+)(\.git)?$/\1\/\2/' | sed 's/\.git$//' + - [If feature indicators found:] - Detailed Feature Proposal - Propose a new feature or enhancement - Bug Report - Report a problem with existing functionality + Store this as REPO_FULL_NAME for use throughout the workflow. - [If unclear:] - Bug Report - Report a problem with existing functionality - Detailed Feature Proposal - Propose a new feature or enhancement - - + If no origin remote exists, stop with: + + + No GitHub remote found. This mode requires a GitHub repository with an 'origin' remote configured. + + - After determining the type, update the todo list: + Update todo after detecting repository: - [x] Analyze user request to determine issue type - [-] Gather initial information for the issue + [x] Detect current repository information + [-] Determine repository structure (monorepo/standard) + [ ] Perform initial codebase discovery + [ ] Analyze user request to determine issue type + [ ] Gather and verify additional information [ ] Determine if user wants to contribute - [ ] Perform technical analysis (if contributing) + [ ] Perform issue scoping (if contributing) [ ] Draft issue content [ ] Review and confirm with user [ ] Create GitHub issue @@ -73,37 +81,62 @@ - Gather Initial Information + Determine Repository Structure - Based on the user's initial prompt or request, extract key information. - If the user hasn't provided enough detail, use ask_followup_question to gather - the required fields from the appropriate template. + Check if this is a monorepo or standard repository by looking for common patterns. - For Bug Reports, ensure you have: - - App version (ask user to check in VSCode extension panel if unknown) - - API provider being used - - Model being used - - Clear steps to reproduce - - What happened vs what was expected - - Any error messages or logs + First, check for monorepo indicators: + 1. Look for workspace configuration: + - package.json with "workspaces" field + - lerna.json + - pnpm-workspace.yaml + - rush.json - For Feature Requests, ensure you have: - - Specific problem description with impact (who is affected, when it happens, current vs expected behavior, impact) - - Additional context if available (mockups, screenshots, links) + 2. Check for common monorepo directory patterns: + + . + false + - IMPORTANT: Do NOT ask for solution design, acceptance criteria, or technical details - unless the user explicitly states they want to contribute the implementation. + Look for directories like: + - apps/ (application packages) + - packages/ (shared packages) + - services/ (service packages) + - libs/ (library packages) + - modules/ (module packages) + - src/ (main source if not using workspaces) - Use multiple ask_followup_question calls if needed to gather all information. - Be specific in your questions based on what's missing. + If monorepo detected: + - Dynamically discover packages by looking for package.json files in detected directories + - Build a list of available packages with their paths - After gathering information, update the todo: + Based on the user's description, try to identify which package they're referring to. + If unclear, ask for clarification: + + + I see this is a monorepo with multiple packages. Which specific package or application is your issue related to? + + [Dynamically generated list of discovered packages] + Let me describe which package: [specify] + + + + If standard repository: + - Skip package selection + - Use repository root for all searches + + Store the repository context for all future codebase searches and explorations. + + Update todo after determining context: - [x] Analyze user request to determine issue type - [x] Gather initial information for the issue - [-] Determine if user wants to contribute - [ ] Perform technical analysis (if contributing) + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [-] Perform initial codebase discovery + [ ] Analyze user request to determine issue type + [ ] Gather and verify additional information + [ ] Determine if user wants to contribute + [ ] Perform issue scoping (if contributing) [ ] Draft issue content [ ] Review and confirm with user [ ] Create GitHub issue @@ -113,33 +146,50 @@ - Determine if User Wants to Contribute + Perform Initial Codebase Discovery - Before exploring the codebase, determine if the user wants to contribute the implementation: + Now that we know the repository structure, immediately search the codebase to understand + what the user is talking about before determining the issue type. - - Are you interested in implementing this yourself, or are you just reporting the problem for the Roo team to solve? - - Just reporting the problem - the Roo team can design the solution - I want to contribute and implement this myself - I'm not sure yet, but I'd like to provide technical analysis - - + DISCOVERY ACTIVITIES: - Based on their response: - - If just reporting: Skip to step 5 (Draft Issue - Problem Only) - - If contributing: Continue to step 4 (Technical Analysis) - - If providing analysis: Continue to step 4 but make technical sections optional + 1. Extract keywords and concepts from the user's INITIAL MESSAGE (their issue description) + 2. Search the codebase to verify these concepts exist + 3. Build understanding of the actual implementation + 4. Identify relevant files, components, and code patterns - Update the todo based on the decision: + + [Keywords from user's initial message/description] + [Repository or package path from step 2] + + + Additional searches based on initial findings: + - If error mentioned: search for exact error strings + - If feature mentioned: search for related functionality + - If component mentioned: search for implementation details + + + [repository or package path] + [specific patterns found in initial search] + + + Document findings: + - Components/features found that match user's description + - Actual implementation details discovered + - Related code sections identified + - Any discrepancies between user description and code reality + + Update todos: - [x] Analyze user request to determine issue type - [x] Gather initial information for the issue - [x] Determine if user wants to contribute - [If contributing: [ ] Perform technical analysis (if contributing)] - [If not contributing: [-] Perform technical analysis (skipped - not contributing)] - [-] Draft issue content + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery + [-] Analyze user request to determine issue type + [ ] Gather and verify additional information + [ ] Determine if user wants to contribute + [ ] Perform issue scoping (if contributing) + [ ] Draft issue content [ ] Review and confirm with user [ ] Create GitHub issue @@ -148,37 +198,54 @@ - Technical Analysis for Contributors + Analyze Request to Determine Issue Type - ONLY perform this step if the user wants to contribute or provide technical analysis. + Using the codebase discoveries from step 2, analyze the user's request to determine + the appropriate issue type with informed context. - This step uses the comprehensive technical analysis sub-workflow defined in - 6_technical_analysis_workflow.xml. The sub-workflow will: + CRITICAL GUIDANCE FOR ISSUE TYPE SELECTION: + For issues that affect user workflows or require behavior changes: + - PREFER the feature proposal template over bug report + - Focus on explaining WHO is affected and WHEN this happens + - Describe the user impact before diving into technical details - 1. Create its own detailed investigation todo list - 2. Perform exhaustive codebase searches using iterative refinement - 3. Analyze all relevant files and dependencies - 4. Form and validate hypotheses about the implementation - 5. Create a comprehensive technical solution - 6. Define detailed acceptance criteria + Based on your findings, classify the issue: - To execute the technical analysis sub-workflow: - - Follow all phases defined in 6_technical_analysis_workflow.xml - - Use the aggressive investigation approach from issue-investigator mode - - Document all findings in extreme detail - - Ensure the analysis is thorough enough for automated implementation + Bug indicators (verified against code): + - Error messages that match actual error handling in code + - Broken functionality in existing features found in codebase + - Regression from previous behavior documented in code/tests + - Code paths that don't work as documented - The sub-workflow will manage its own todo list for the investigation process - and will produce a comprehensive technical analysis section for the issue. + Feature indicators (verified against code): + - New functionality not found in current codebase + - Enhancement to existing features found in code + - Missing capabilities compared to similar features + - Integration points that could be extended + - WORKFLOW IMPROVEMENTS: When existing behavior works but doesn't meet user needs - After completing the technical analysis: + IMPORTANT: Use your codebase findings to inform the question: + + + Based on your request about [specific feature/component found in code], what type of issue would you like to create? + + [Order based on codebase findings and user description] + Bug Report - [Specific component] is not working as expected + Feature Proposal - Add [specific capability] to [existing component] + + + + Update todos: + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery [x] Analyze user request to determine issue type - [x] Gather initial information for the issue - [x] Determine if user wants to contribute - [x] Perform technical analysis (if contributing) - [-] Draft issue content + [-] Gather and verify additional information + [ ] Determine if user wants to contribute + [ ] Perform issue scoping (if contributing) + [ ] Draft issue content [ ] Review and confirm with user [ ] Create GitHub issue @@ -187,113 +254,709 @@ - Draft Issue Content + Gather and Verify Additional Information - Create the issue body based on whether the user is just reporting or contributing. + Based on the issue type and initial codebase discovery, gather information while + continuously verifying against the actual code implementation. - For Bug Reports, format is the same regardless of contribution intent: - ``` - ## App Version - [version from user] + CRITICAL FOR FEATURE REQUESTS: Be fact-driven and challenge assumptions! + When users describe current behavior as problematic for a feature request, you MUST verify + their claims against the actual code. If their description doesn't match reality, this + might actually be a bug report, not a feature request. - ## API Provider - [provider from dropdown list] + For Bug Reports: + 1. When user describes steps to reproduce: + - Search for the UI components/commands mentioned + - Verify the code paths that would be executed + - Check for existing error handling or known issues + + 2. When user provides error messages: + - Search for exact error strings in codebase + - Find where errors are thrown + - Understand the conditions that trigger them + + 3. For version information: + - Check package.json for actual version + - Look for version-specific code or migrations - ## Model Used - [exact model name] + Example verification searches: + + [repository or package path] + [exact error message from user] + - ## 🔁 Steps to Reproduce + + [feature or component name] implementation + [repository or package path] + - 1. [First step with specific details] - 2. [Second step with exact actions] - 3. [Continue numbering all steps] + For Feature Requests - AGGRESSIVE VERIFICATION WITH CONCRETE EXAMPLES: + 1. When user claims current behavior is X: + - ALWAYS search for the actual implementation + - Read the relevant code to verify their claim + - Check CSS/styling files if UI-related + - Look at configuration files + - Examine test files to understand expected behavior + - TRACE THE DATA FLOW: Follow values from where they're calculated to where they're used + + 2. CRITICAL: Look for existing variables/code that could be reused: + - Search for variables that are calculated but not used where expected + - Identify existing patterns that could be extended + - Find similar features that work correctly for comparison + + 3. If discrepancy found between claim and code: + - Do NOT proceed without clarification + - Present CONCRETE before/after examples with actual values + - Show exactly what happens vs what should happen + - Ask if this might be a bug instead + + Example verification approach: + User says: "Feature X doesn't work properly" - Include: - - Exact button clicks or menu selections - - Specific input text or prompts used - - File names and paths involved - - Any settings or configuration + Your investigation should follow this pattern: + a) What is calculated: Search for where X is computed/defined + b) Where it's stored: Find variables/state holding the value + c) Where it's used: Trace all usages of that value + d) What's missing: Identify gaps in the flow - ## 💥 Outcome Summary + Present findings with concrete examples: - Expected: [what should have happened] - Actual: [what actually happened] + + I investigated the implementation and found something interesting: - ## 📄 Relevant Logs or Errors + Current behavior: + - The value is calculated at [file:line]: `value = computeX()` + - It's stored in variable `calculatedValue` at [file:line] + - BUT it's only used for [purpose A] at [file:line] + - It's NOT used for [purpose B] where you expected it - ```[language] - [paste any error messages or logs] - ``` + Concrete example: + - When you do [action], the system calculates [value] + - This value goes to [location A] + - But [location B] still uses [old/different value] - [If user is contributing, add the comprehensive technical analysis section from step 4] - ``` + Is this the issue you're experiencing? This seems like the calculated value isn't being used where it should be. + + Yes, exactly! The value is calculated but not used in the right place + No, the issue is that the calculation itself is wrong + Actually, I see now that [location B] should use a different value + + - For Feature Requests - PROBLEM REPORTERS (not contributing): - ``` - ## What specific problem does this solve? + 4. Continue verification until facts are established: + - If user confirms it's a bug, switch to bug report workflow + - If user provides more specific context, search again + - Do not accept vague claims without code verification + + 5. For genuine feature requests after verification: + - Document what the code currently does (with evidence and line numbers) + - Show the exact data flow: input → processing → output + - Confirm what the user wants changed with concrete examples + - Ensure the request is based on accurate understanding - [Detailed problem description following the template guidelines] + CRITICAL: For feature requests, if user's description doesn't match codebase reality: + - Challenge the assumption with code evidence AND concrete examples + - Show actual vs expected behavior with specific values + - Suggest it might be a bug if code shows different intent + - Ask for clarification repeatedly if needed + - Do NOT proceed until facts are established - **Who is affected:** [user groups] - **When this happens:** [specific scenarios] - **Current behavior:** [what happens now] - **Expected behavior:** [what should happen] - **Impact:** [time wasted, errors, productivity loss] + Only proceed when you have: + - Verified current behavior in code with line-by-line analysis + - Confirmed user's understanding matches reality + - Determined if it's truly a feature request or actually a bug + - Identified any existing code that could be reused for the fix - ## Additional context - - [Any mockups, screenshots, links, or other supporting information] - ``` - - For Feature Requests - CONTRIBUTORS (implementing the feature): - ``` - ## What specific problem does this solve? - - [Detailed problem description following the template guidelines] - - **Who is affected:** [user groups] - **When this happens:** [specific scenarios] - **Current behavior:** [what happens now] - **Expected behavior:** [what should happen] - **Impact:** [time wasted, errors, productivity loss] - - ## Additional context - - [Any mockups, screenshots, links, or other supporting information] - - --- - - ## 🛠️ Contributing & Technical Analysis - - ✅ **I'm interested in implementing this feature** - ✅ **I understand this needs approval before implementation begins** - - [Insert the comprehensive technical analysis section from step 4, including:] - - Root cause / Implementation target - - Affected components with file paths and line numbers - - Current implementation analysis - - Detailed proposed implementation steps - - Code architecture considerations - - Testing requirements - - Performance impact - - Security considerations - - Migration strategy - - Rollback plan - - Dependencies and breaking changes - - Implementation complexity assessment - - ## Acceptance Criteria - - [Insert the detailed acceptance criteria from the technical analysis] - ``` - - After drafting: + Update todos after verification: + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery [x] Analyze user request to determine issue type - [x] Gather initial information for the issue + [x] Gather and verify additional information + [-] Determine if user wants to contribute + [ ] Perform issue scoping (if contributing) + [ ] Draft issue content + [ ] Review and confirm with user + [ ] Create GitHub issue + + + + + + + Determine Contribution Intent with Context + + Before asking about contribution, perform a quick technical assessment to provide context: + + 1. Search for complexity indicators: + - Number of files that would need changes + - Existing tests that would need updates + - Dependencies and integration points + + 2. Look for contribution helpers: + - CONTRIBUTING.md guidelines + - Existing similar implementations + - Test patterns to follow + + + CONTRIBUTING guide setup development + + + Based on findings, provide informed context in the question: + + + Based on my analysis, this [issue type] involves [brief complexity assessment from code exploration]. Are you interested in implementing this yourself, or are you reporting it for the project team to handle? + + Just reporting the problem - the project team can design the solution + I want to contribute and implement this myself + I'd like to provide issue scoping to help whoever implements it + + + + Update todos based on response: + + + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery + [x] Analyze user request to determine issue type + [x] Gather and verify additional information [x] Determine if user wants to contribute - [x] Perform technical analysis (if contributing) + [If contributing: [-] Perform issue scoping (if contributing)] + [If not contributing: [-] Perform issue scoping (skipped - not contributing)] + [-] Draft issue content + [ ] Review and confirm with user + [ ] Create GitHub issue + + + + + + + Issue Scoping for Contributors + + ONLY perform this step if the user wants to contribute or provide issue scoping. + + This step performs a comprehensive, aggressive investigation to create detailed technical + scoping that can guide implementation. The process involves multiple sub-phases: + + + + Perform an exhaustive investigation to produce a comprehensive technical solution + with extreme detail, suitable for automated fix workflows. + + + + Expand the todo list to include detailed investigation steps + + When starting the issue scoping phase, update the main todo list to include + the detailed investigation steps: + + + + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery + [x] Analyze user request to determine issue type + [x] Gather and verify additional information + [x] Determine if user wants to contribute + [-] Perform issue scoping (if contributing) + [ ] Extract keywords from the issue description + [ ] Perform initial broad codebase search + [ ] Analyze search results and identify key components + [ ] Deep dive into relevant files and implementations + [ ] Form initial hypothesis about the issue/feature + [ ] Attempt to disprove hypothesis through further investigation + [ ] Identify all affected files and dependencies + [ ] Map out the complete implementation approach + [ ] Document technical risks and edge cases + [ ] Formulate comprehensive technical solution + [ ] Create detailed acceptance criteria + [ ] Prepare issue scoping summary + [ ] Draft issue content + [ ] Review and confirm with user + [ ] Create GitHub issue + + + + + + + Extract all relevant keywords, concepts, and technical terms + + - Identify primary technical concepts from user's description + - Extract error messages or specific symptoms + - Note any mentioned file paths or components + - List related features or functionality + - Include synonyms and related terms + + + Update the main todo list to mark "Extract keywords" as complete and move to next phase + + + + + Perform multiple rounds of increasingly focused searches + + + Use codebase_search with all extracted keywords to get an overview of relevant code. + + [Combined keywords from extraction phase] + [Repository or package path] + + + + + Based on initial results, identify key components and search for: + - Related class/function definitions + - Import statements and dependencies + - Configuration files + - Test files that might reveal expected behavior + + + + Search for specific implementation details: + - Error handling patterns + - State management + - API endpoints or routes + - Database queries or models + - UI components and their interactions + + + + Look for: + - Edge cases in the code + - Integration points with other systems + - Configuration options that affect behavior + - Feature flags or conditional logic + + + + After completing all search iterations, update the todo list to show progress + + + + + Thoroughly analyze all relevant files discovered + + - Use list_code_definition_names to understand file structure + - Read complete files to understand full context + - Trace execution paths through the code + - Identify all dependencies and imports + - Map relationships between components + + + Document findings including: + - File paths and their purposes + - Key functions and their responsibilities + - Data flow through the system + - External dependencies + - Potential impact areas + + + + + Form a comprehensive hypothesis about the issue or feature + + - Identify the most likely root cause + - Trace the bug through the execution path + - Determine why the current implementation fails + - Consider environmental factors + + + - Identify the optimal integration points + - Determine required architectural changes + - Plan the implementation approach + - Consider scalability and maintainability + + + + + Aggressively attempt to disprove the hypothesis + + + - Look for similar features implemented differently + - Check for deprecated code that might interfere + + + - Search for configuration that could change behavior + - Look for environment-specific code paths + + + - Find existing tests that might contradict hypothesis + - Look for test cases that reveal edge cases + + + - Search for comments explaining design decisions + - Look for TODO or FIXME comments related to the area + + + + If hypothesis is disproven, return to search phase with new insights. + If hypothesis stands, proceed to solution formulation. + + + + + Create a comprehensive technical solution - PRIORITIZE SIMPLICITY + + CRITICAL: Before proposing any solution, ask yourself: + 1. What existing variables/functions can I reuse? + 2. What's the minimal change that fixes the issue? + 3. Can I leverage existing patterns in the codebase? + 4. Is there a simpler approach I'm overlooking? + + The best solution often reuses existing code rather than creating new complexity. + + + + ALWAYS consider backwards compatibility: + 1. Will existing data/configurations still work with the new code? + 2. Can we detect and handle legacy formats automatically? + 3. What migration paths are needed for existing users? + 4. Are there ways to make changes additive rather than breaking? + 5. Document any compatibility considerations clearly + + + + FIRST, identify what can be reused: + - Variables that are already calculated but not used where needed + - Functions that already do what we need + - Patterns in similar features we can follow + - Configuration that already exists but isn't applied + + Example finding: + "The variable `calculatedValue` already contains what we need at line X, + we just need to use it at line Y instead of recalculating" + + + + - Start with the SIMPLEST possible fix + - Exact files to modify with line numbers + - Prefer changing variable usage over creating new logic + - Specific code changes required (minimal diff) + - Order of implementation steps + - Migration strategy if needed + + + + - All files that import affected code + - API contracts that must be maintained + - Existing tests that validate current behavior + - Configuration changes required (prefer reusing existing) + - Documentation updates needed + + + + - Unit tests to add or modify + - Integration tests required + - Edge cases to test + - Performance testing needs + - Manual testing scenarios + + + + - Breaking changes identified + - Performance implications + - Security considerations + - Backward compatibility issues + - Rollback strategy + + + + + + Create extremely detailed acceptance criteria + + Given [detailed context including system state] + When [specific user or system action] + Then [exact expected outcome] + And [additional verifiable outcomes] + But [what should NOT happen] + + Include: + - Specific UI changes with exact text/behavior + - API response formats + - Database state changes + - Performance requirements + - Error handling scenarios + + + - Each criterion must be independently testable + - Include both positive and negative test cases + - Specify exact error messages and codes + - Define performance thresholds where applicable + + + + + Format the comprehensive issue scoping section + + + + + Additional considerations for monorepo repositories: + - Scope all searches to the identified package (if monorepo) + - Check for cross-package dependencies + - Verify against package-specific conventions + - Look for package-specific configuration + - Check if changes affect multiple packages + - Identify shared dependencies that might be impacted + - Look for workspace-specific scripts or tooling + - Consider package versioning implications + + After completing the comprehensive issue scoping, update the main todo list to show + all investigation steps are complete: + + + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery + [x] Analyze user request to determine issue type + [x] Gather and verify additional information + [x] Determine if user wants to contribute + [x] Perform issue scoping (if contributing) + [x] Extract keywords from the issue description + [x] Perform initial broad codebase search + [x] Analyze search results and identify key components + [x] Deep dive into relevant files and implementations + [x] Form initial hypothesis about the issue/feature + [x] Attempt to disprove hypothesis through further investigation + [x] Identify all affected files and dependencies + [x] Map out the complete implementation approach + [x] Document technical risks and edge cases + [x] Formulate comprehensive technical solution + [x] Create detailed acceptance criteria + [x] Prepare issue scoping summary + [-] Draft issue content + [ ] Review and confirm with user + [ ] Create GitHub issue + + + + + + + Check for Repository Issue Templates + + Check if the repository has custom issue templates and use them. If not, create a simple generic template. + + 1. Check for issue templates in standard locations: + + .github/ISSUE_TEMPLATE + true + + + 2. Also check for single template file: + + .github + false + + + Look for files like: + - .github/ISSUE_TEMPLATE/*.md + - .github/ISSUE_TEMPLATE/*.yml + - .github/ISSUE_TEMPLATE/*.yaml + - .github/issue_template.md + - .github/ISSUE_TEMPLATE.md + + 3. If templates are found: + a. Parse the template files to extract: + - Template name and description + - Required fields + - Template body structure + - Labels to apply + + b. For YAML templates, look for: + - name: Template display name + - description: Template description + - labels: Default labels + - body: Form fields or markdown template + + c. For Markdown templates, look for: + - Front matter with metadata + - Template structure with placeholders + + 4. If multiple templates exist, ask user to choose: + + I found the following issue templates in this repository. Which one would you like to use? + + [Template 1 name]: [Template 1 description] + [Template 2 name]: [Template 2 description] + + + + 5. If no templates are found: + - Create a simple generic template based on issue type + - For bugs: Basic structure with description, steps to reproduce, expected vs actual + - For features: Problem description, proposed solution, impact + + 6. Store the selected/created template information: + - Template content/structure + - Required fields + - Default labels + - Any special formatting requirements + + Update todos: + + + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery + [x] Analyze user request to determine issue type + [x] Gather and verify additional information + [x] Determine if user wants to contribute + [x] Perform issue scoping (if contributing) + [x] Check for repository issue templates + [-] Draft issue content + [ ] Review and confirm with user + [ ] Create GitHub issue + + + + + + + Draft Issue Content + + Create the issue body using the template from step 8 and all verified information from codebase exploration. + + If using a repository template: + - Fill in the template fields with gathered information + - Include code references and findings where appropriate + - Respect the template's structure and formatting + + If using a generated template (no repo templates found): + + For Bug Reports: + ``` + ## Description + [Clear description of the bug with code context] + + ## Steps to Reproduce + 1. [Step with relevant code paths] + 2. [Step with component references] + 3. [Continue with specific details] + + ## Expected Behavior + [What should happen based on code logic] + + ## Actual Behavior + [What actually happens] + + ## Additional Context + - Version: [from package.json if found] + - Environment: [any relevant details] + - Error logs: [if any] + + ## Code Investigation + [Include findings from codebase exploration] + - Relevant files: [list with line numbers] + - Possible cause: [hypothesis from code review] + + [If user is contributing, add the comprehensive issue scoping section from step 7] + ``` + + For Feature Requests: + ``` + ## Problem Description + [What problem does this solve, who is affected, when it happens] + + ## Current Behavior + [How it works now with specific examples] + + ## Proposed Solution + [What should change] + + ## Impact + [Who benefits and how] + + ## Technical Context + [Findings from codebase exploration] + - Similar features: [code references] + - Integration points: [from exploration] + - Architecture considerations: [if any] + + [If contributing, add the comprehensive issue scoping section from step 7] + ``` + + Update todos: + + + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery + [x] Analyze user request to determine issue type + [x] Gather and verify additional information + [x] Determine if user wants to contribute + [x] Perform issue scoping (if contributing) + [x] Check for repository issue templates [x] Draft issue content [-] Review and confirm with user [ ] Create GitHub issue @@ -302,19 +965,26 @@ - + Review and Confirm with User - Present the complete drafted issue to the user for review: + Present the complete drafted issue to the user for review, highlighting the + code-verified information: - I've prepared the following GitHub issue. Please review it carefully: + I've prepared the following GitHub issue based on my analysis of the codebase and your description. I've verified the technical details against the actual implementation. Please review: [Show the complete formatted issue content] + Key verifications made: + - ✓ Component locations confirmed in code + - ✓ Error messages matched to source + - ✓ Architecture compatibility checked + [List other relevant verifications] + Would you like me to create this issue, or would you like to make any changes? - Yes, create this issue in RooCodeInc/Roo-Code + Yes, create this issue in the detected repository Modify the problem description Add more technical details Change the title to: [let me specify] @@ -326,57 +996,164 @@ After confirmation: + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery [x] Analyze user request to determine issue type - [x] Gather initial information for the issue + [x] Gather and verify additional information [x] Determine if user wants to contribute - [x] Perform technical analysis (if contributing) + [x] Perform issue scoping (if contributing) + [x] Check for repository issue templates [x] Draft issue content [x] Review and confirm with user - [-] Create GitHub issue + [-] Prepare issue for submission + [ ] Handle submission choice - - Create GitHub Issue + + Prepare Issue for Submission - Once user confirms, create the issue using the GitHub CLI: + Once user confirms the issue content, prepare it for submission: - First, save the issue body to a temporary file: + First, perform final duplicate check with refined search based on our findings: - cat > /tmp/issue_body.md << 'EOF' -[The complete formatted issue body from step 5] -EOF + gh issue list --repo $REPO_FULL_NAME --search "[key terms from verified analysis]" --state all --limit 10 - Then create the issue: - - gh issue create --repo RooCodeInc/Roo-Code --title "[Create a descriptive title based on the issue content]" --body-file /tmp/issue_body.md --label "bug" - + If no exact duplicates are found, save the issue content to a temporary file within the project: - For feature requests, use labels "proposal,enhancement": - - gh issue create --repo RooCodeInc/Roo-Code --title "[Create a descriptive title based on the issue content]" --body-file /tmp/issue_body.md --label "proposal" --label "enhancement" - + + ./github_issue_draft.md + [The complete formatted issue body from step 8] + [calculated line count] + - The command will return the issue URL. Inform the user of the created issue number and URL. + After saving the issue draft, ask the user how they would like to proceed: - Clean up the temporary file: - - rm /tmp/issue_body.md - + + I've saved the issue draft to ./github_issue_draft.md. The issue is ready for submission with the following details: + + Title: "[Descriptive title with component name]" + Labels: [appropriate labels based on issue type] + Repository: $REPO_FULL_NAME + + How would you like to proceed? + + Submit the issue now to the repository + Let me make some edits to the issue first + I'll submit it manually later + + - Complete the workflow: + Based on the user's response: + + If "Submit the issue now": + - Use gh issue create with the saved file + - Provide the created issue URL and number + - Clean up the temporary file + - Complete the workflow + + If "Let me make some edits": + - Ask what changes they'd like to make + - Update the draft file with their changes + - Return to the submission question + + If "I'll submit it manually": + - Inform them the draft is saved at the configured location + - Provide the gh command they can use later + - Complete the workflow without submission + + Update todos based on the outcome: + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery [x] Analyze user request to determine issue type - [x] Gather initial information for the issue + [x] Gather and verify additional information [x] Determine if user wants to contribute - [x] Perform technical analysis (if contributing) + [x] Perform issue scoping (if contributing) + [x] Check for repository issue templates [x] Draft issue content [x] Review and confirm with user - [x] Create GitHub issue + [x] Prepare issue for submission + [-] Handle submission choice + + + + + + + Handle Submission Choice + + This step handles the user's choice from step 9. + + OPTION 1: Submit the issue now + If the user chooses to submit immediately: + + + gh issue create --repo $REPO_FULL_NAME --title "[Descriptive title]" --body-file ./github_issue_draft.md --label "[appropriate labels]" + + + Label selection based on findings: + - Bug: Use "bug" label + - Feature: Use "enhancement" label + - If affects multiple packages in monorepo: add "affects-multiple" label + + After successful creation: + - Capture and display the issue URL + - Clean up the temporary file: + + rm ./github_issue_draft.md + + - Provide a summary of key findings included + + OPTION 2: Make edits + If the user wants to edit: + + + What changes would you like to make to the issue? + + Update the title + Modify the problem description + Add or remove technical details + Change the labels or other metadata + + + + - Apply the requested changes to the draft + - Update the file with write_to_file + - Return to step 9 to ask about submission again + + OPTION 3: Manual submission + If the user will submit manually: + + Provide clear instructions: + "The issue draft has been saved to ./github_issue_draft.md + + To submit it later, you can use: + gh issue create --repo $REPO_FULL_NAME --title "[Your title]" --body-file ./github_issue_draft.md --label "[labels]" + + Or you can copy the content and create the issue through the GitHub web interface." + + Final todo update: + + + [x] Detect current repository information + [x] Determine repository structure (monorepo/standard) + [x] Perform initial codebase discovery + [x] Analyze user request to determine issue type + [x] Gather and verify additional information + [x] Determine if user wants to contribute + [x] Perform issue scoping (if contributing) + [x] Check for repository issue templates + [x] Draft issue content + [x] Review and confirm with user + [x] Prepare issue for submission + [x] Handle submission choice diff --git a/.roo/rules-issue-writer/2_github_issue_templates.xml b/.roo/rules-issue-writer/2_github_issue_templates.xml index 3130f2026e..36b44125dd 100644 --- a/.roo/rules-issue-writer/2_github_issue_templates.xml +++ b/.roo/rules-issue-writer/2_github_issue_templates.xml @@ -1,219 +1,190 @@ - - Bug Report - Clearly report a bug with detailed repro steps - ["bug"] - - - - What version of Roo Code are you using? (e.g., v3.3.1) - - - - - - Anthropic - - AWS Bedrock - - Chutes AI - - DeepSeek - - Glama - - Google Gemini - - Google Vertex AI - - Groq - - Human Relay Provider - - LiteLLM - - LM Studio - - Mistral AI - - Ollama - - OpenAI - - OpenAI Compatible - - OpenRouter - - Requesty - - Unbound - - VS Code Language Model API - - xAI (Grok) - - Not Applicable / Other - - - - - Exact model name (e.g., Claude 3.7 Sonnet). Use N/A if irrelevant. - - - - - Help us see what you saw. Give clear, numbered steps: - - 1. Setup (OS, extension version, settings) - 2. Exact actions (clicks, input, files, commands) - 3. What happened after each step - - Think like you're writing a recipe. Without this, we can't reproduce the issue. - - - - - - Recap what went wrong in one or two lines. - - Example: "Expected code to run, but got an empty response and no error." - - Expected ___, but got ___. - - - - Paste API logs, terminal output, or errors here. Use triple backticks (```) for code formatting. - shell - - - - - - Detailed Feature Proposal - Report a specific problem that needs solving in Roo Code - ["proposal", "enhancement"] - - - - - **Be concrete and detailed.** Explain the problem from a user's perspective. - - ✅ **Good examples (specific, clear impact):** - - "When running large tasks, users wait 5+ minutes because tasks execute sequentially instead of in parallel, blocking productivity" - - "AI can only read one file per request, forcing users to make multiple requests for multi-file projects, increasing wait time from 30s to 5+ minutes" - - "Dark theme users can't see the submit button because it uses white text on light grey background" - - ❌ **Poor examples (vague, unclear impact):** - - "The UI looks weird" -> What specifically looks weird? On which screen? What's the impact? - - "System prompt is not good" -> What's wrong with it? What behaviour does it cause? What should it do instead? - - "Performance could be better" -> Where? How slow is it currently? What's the user impact? - - **Your problem description should answer:** - - Who is affected? (all users, specific user types, etc.) - - When does this happen? (specific scenarios/steps) - - What's the current behaviour vs expected behaviour? - - What's the impact? (time wasted, errors caused, etc.) - - Be specific about the problem, who it affects, and the impact. Avoid generic statements like "it's slow" or "it's confusing." - - - - Mockups, screenshots, links, user quotes, or other relevant information that supports your proposal. - - + + This mode prioritizes using repository-specific issue templates over hardcoded ones. + If no templates exist in the repository, simple generic templates are created on the fly. + + + + + .github/ISSUE_TEMPLATE/*.yml + .github/ISSUE_TEMPLATE/*.yaml + .github/ISSUE_TEMPLATE/*.md + .github/issue_template.md + .github/ISSUE_TEMPLATE.md + - - - - - **Important:** If you check "Yes" below, the technical sections become REQUIRED. - We need detailed technical analysis from contributors to ensure quality implementation. - - - - - - - - - - - **If you want to implement this feature, this section is REQUIRED.** - - **Describe your solution in detail.** Explain not just what to build, but how it should work. - - ✅ **Good examples:** - - "Add parallel task execution: Allow up to 3 tasks to run simultaneously with a queue system for additional tasks. Show progress for each active task in the UI." - - "Enable multi-file AI processing: Modify the request handler to accept multiple files in a single request and process them together, reducing round trips." - - "Fix button contrast: Change submit button to use primary colour on dark theme (white text on blue background) instead of current grey." - - ❌ **Poor examples:** - - "Make it faster" -> How? What specific changes? - - "Improve the UI" -> Which part? What specific improvements? - - "Fix the prompt" -> What should the new prompt do differently? - - **Your solution should explain:** - - What exactly will change? - - How will users interact with it? - - What will the new behaviour look like? - - Describe the specific changes and how they will work. Include user interaction details if relevant. - - - - - **If you want to implement this feature, this section is REQUIRED.** - - **This is crucial - don't skip it.** Define what "working" looks like with specific, testable criteria. - - **Format suggestion:** - ``` - Given [context/situation] - When [user action] - Then [expected result] - And [additional expectations] - But [what should NOT happen] - ``` - - **Example:** - ``` - Given I have 5 large tasks to run - When I start all of them - Then they execute in parallel (max 3 at once, can be configured) - And I see progress for each active task - And queued tasks show "waiting" status - But the UI doesn't freeze or become unresponsive - ``` - - - Define specific, testable criteria. What should users be able to do? What should happen? What should NOT happen? - Use the Given/When/Then format above or your own clear structure. - - - - - - **If you want to implement this feature, this section is REQUIRED.** - - Share technical insights that could help planning: - - Implementation approach or architecture changes - - Performance implications - - Compatibility concerns - - Systems that might be affected - - Potential blockers you can foresee - - e.g., "Will need to refactor task manager", "Could impact memory usage on large files", "Requires a large portion of code to be rewritten" - - - - - **If you want to implement this feature, this section is REQUIRED.** - - What could go wrong or what alternatives did you consider? - - Alternative approaches and why you chose this one - - Potential negative impacts (performance, UX, etc.) - - Breaking changes or migration concerns - - Edge cases that need careful handling - - e.g., "Alternative: use library X but it is 500KB larger", "Risk: might slow older devices", "Breaking: changes API response format" - - - - - - - Template now focuses on problem reporting first, with solution contribution as optional - - - Only problem description and context are required for basic submission - - - Technical fields (solution, acceptance criteria, etc.) are only required if user wants to contribute - - - Users can submit after describing the problem without technical details - - - Implementation guidance moved to contributor section only - - + + Display name of the template + Brief description of when to use this template + Default issue title (optional) + Array of labels to apply + Array of default assignees + Array of form elements or markdown content + + + + + Static markdown content + + The markdown content to display + + + + + Single-line text input + + Unique identifier + Display label + Help text + Placeholder text + Default value + Boolean + + + + + Multi-line text input + + Unique identifier + Display label + Help text + Placeholder text + Default value + Boolean + Language for syntax highlighting + + + + + Dropdown selection + + Unique identifier + Display label + Help text + Array of options + Boolean + + + + + Multiple checkbox options + + Unique identifier + Display label + Help text + Array of checkbox items + + + + + + + Optional YAML front matter with: + - name: Template name + - about: Template description + - title: Default title + - labels: Comma-separated or array + - assignees: Comma-separated or array + + + Markdown content with sections and placeholders + Common patterns: + - Headers with ## + - Placeholder text in brackets or as comments + - Checklists with - [ ] + - Code blocks with ``` + + + + + + + When no repository templates exist, create simple templates based on issue type. + These should be minimal and focused on gathering essential information. + + + + + - Description: Clear explanation of the bug + - Steps to Reproduce: Numbered list + - Expected Behavior: What should happen + - Actual Behavior: What actually happens + - Additional Context: Version, environment, logs + - Code Investigation: Findings from exploration (if any) + + ["bug"] + + + + + - Problem Description: What problem this solves + - Current Behavior: How it works now + - Proposed Solution: What should change + - Impact: Who benefits and how + - Technical Context: Code findings (if any) + + ["enhancement", "proposal"] + + + + + + When parsing YAML templates: + 1. Use a YAML parser to extract the structure + 2. Convert form elements to markdown sections + 3. Preserve required field indicators + 4. Include descriptions as help text + 5. Maintain the intended flow of the template + + + + When parsing Markdown templates: + 1. Extract front matter if present + 2. Identify section headers + 3. Look for placeholder patterns + 4. Preserve formatting and structure + 5. Replace generic placeholders with user's information + + + + For template selection: + 1. If only one template exists, use it automatically + 2. If multiple exist, let user choose based on name/description + 3. Match template to issue type when possible (bug vs feature) + 4. Respect template metadata (labels, assignees, etc.) + + + + + + Fill templates intelligently using gathered information: + - Map user's description to appropriate sections + - Include code investigation findings where relevant + - Preserve template structure and formatting + - Don't leave placeholder text unfilled + - Add contributor scoping if user is contributing + + + + + + + + + + + + + When no templates exist, create appropriate generic templates on the fly. + Keep them simple and focused on essential information. + + + + - Don't overwhelm with too many fields + - Focus on problem description first + - Include technical details only if user is contributing + - Use clear, simple section headers + - Adapt based on issue type (bug vs feature) + + \ No newline at end of file diff --git a/.roo/rules-issue-writer/3_best_practices.xml b/.roo/rules-issue-writer/3_best_practices.xml index 6d70cba144..f2f149ed26 100644 --- a/.roo/rules-issue-writer/3_best_practices.xml +++ b/.roo/rules-issue-writer/3_best_practices.xml @@ -1,38 +1,172 @@ + + - CRITICAL: This mode assumes the user's FIRST message is already an issue description + - Do NOT ask "What would you like to do?" or "Do you want to create an issue?" + - Immediately start the issue creation workflow when the user begins talking + - Treat their initial message as the problem/feature description + - Begin with repository detection and codebase discovery right away + - The user is already in "issue creation mode" by choosing this mode + + + + - ALWAYS check for repository-specific issue templates before creating issues + - Use templates from .github/ISSUE_TEMPLATE/ directory if they exist + - Parse both YAML (.yml/.yaml) and Markdown (.md) template formats + - If multiple templates exist, let the user choose the appropriate one + - If no templates exist, create a simple generic template on the fly + - NEVER fall back to hardcoded templates - always use repo templates or generate minimal ones + - Respect template metadata like labels, assignees, and title patterns + - Fill templates intelligently using gathered information from codebase exploration + + - Focus on helping users describe problems clearly, not solutions - - The Roo team will design solutions unless the user explicitly wants to contribute + - The project team will design solutions unless the user explicitly wants to contribute - Don't push users to provide technical details they may not have - Make it easy for non-technical users to report issues effectively + + CRITICAL: Lead with user impact: + - Always explain WHO is affected and WHEN the problem occurs + - Use concrete examples with actual values, not abstractions + - Show before/after scenarios with specific data + - Example: "Users trying to [action] see [actual result] instead of [expected result]" + + - ALWAYS verify user claims against actual code implementation + - For feature requests, aggressively check if current behavior matches user's description + - If code shows different intent than user describes, it might be a bug not a feature + - Present code evidence when challenging user assumptions + - Do not be agreeable - be fact-driven and question discrepancies + - Continue verification until facts are established + - A "feature request" where code shows the feature should already work is likely a bug + + CRITICAL additions for thorough analysis: + - Trace data flow from where values are created to where they're used + - Look for existing variables/functions that already contain needed data + - Check if the issue is just missing usage of existing code + - Follow imports and exports to understand data availability + - Identify patterns in similar features that work correctly + + - Always search for existing similar issues before creating a new one - - Search GitHub Discussions (especially feature-requests category) for related topics + - Check for and use repository issue templates before creating content - Include specific version numbers and environment details - Use code blocks with syntax highlighting for code snippets - Make titles descriptive but concise (e.g., "Dark theme: Submit button invisible due to white-on-grey text") - For bugs, always test if the issue is reproducible - Include screenshots or mockups when relevant (ask user to provide) - Link to related issues or PRs if found during exploration - - Add "Closes #[number]" for discussions that would be fully addressed by the issue - - Add "Related to #[number]" for partially related discussions + + CRITICAL: Use concrete examples throughout: + - Show actual data values, not just descriptions + - Include specific file paths and line numbers + - Demonstrate the data flow with real examples + - Bad: "The value is incorrect" + - Good: "The function returns '123' when it should return '456'" - - Only explore codebase if user wants to contribute + - Only perform issue scoping if user wants to contribute - Reference specific files and line numbers from codebase exploration - Ensure technical proposals align with project architecture - - Include implementation steps and technical analysis + - Include implementation steps and issue scoping - Provide clear acceptance criteria in Given/When/Then format - Consider trade-offs and alternative approaches + + CRITICAL: Prioritize simple solutions: + - ALWAYS check if needed functionality already exists before proposing new code + - Look for existing variables that just need to be passed/used differently + - Prefer using existing patterns over creating new ones + - The best fix often involves minimal code changes + - Example: "Use existing `modeInfo` from line 234 in export" vs "Create new mode tracking system" + + ALWAYS consider backwards compatibility: + - Think about existing data/configurations already in use + - Propose solutions that handle both old and new formats gracefully + - Consider migration paths for existing users + - Document any breaking changes clearly + - Prefer additive changes over breaking changes when possible + + - Be supportive and encouraging to problem reporters - Don't overwhelm users with technical questions upfront - Clearly indicate when technical sections are optional - Guide contributors through the additional requirements - Make the "submit now" option clear for problem reporters + - When presenting template choices, include template descriptions to help users choose + - Explain that you're using the repository's own templates for consistency + + + + Always check these locations in order: + 1. .github/ISSUE_TEMPLATE/*.yml or *.yaml (GitHub form syntax) + 2. .github/ISSUE_TEMPLATE/*.md (Markdown templates) + 3. .github/issue_template.md (single template) + 4. .github/ISSUE_TEMPLATE.md (alternate naming) + + + + For YAML templates: + - Extract form elements and convert to appropriate markdown sections + - Preserve required field indicators + - Include field descriptions as context + - Respect dropdown options and checkbox lists + + For Markdown templates: + - Parse front matter for metadata + - Identify section headers and structure + - Replace placeholder text with actual information + - Maintain formatting and hierarchy + + + + - Map gathered information to template sections intelligently + - Don't leave placeholder text in the final issue + - Add code investigation findings to relevant sections + - Include contributor scoping in appropriate section if applicable + - Preserve the template's intended structure and flow + + + + When no templates exist: + - Create minimal, focused templates + - Use simple section headers + - Focus on essential information only + - Adapt structure based on issue type + - Don't overwhelm with unnecessary fields + + + + + Before proposing ANY solution: + 1. Use codebase_search extensively to find all related code + 2. Read multiple files to understand the full context + 3. Trace variable usage from creation to consumption + 4. Look for similar working features to understand patterns + 5. Identify what already exists vs what's actually missing + + + + When designing solutions: + 1. Check if the data/function already exists somewhere + 2. Look for configuration options before code changes + 3. Prefer passing existing variables over creating new ones + 4. Use established patterns from similar features + 5. Aim for minimal diff size + + + + Always include: + - Exact file paths and line numbers + - Variable/function names as they appear in code + - Before/after code snippets showing minimal changes + - Clear explanation of why the simple fix works + + \ No newline at end of file diff --git a/.roo/rules-issue-writer/4_common_mistakes_to_avoid.xml b/.roo/rules-issue-writer/4_common_mistakes_to_avoid.xml index 2013bd73d8..a8dd9b590b 100644 --- a/.roo/rules-issue-writer/4_common_mistakes_to_avoid.xml +++ b/.roo/rules-issue-writer/4_common_mistakes_to_avoid.xml @@ -1,4 +1,13 @@ + + - CRITICAL: Asking "What would you like to do?" when mode starts + - Waiting for user to say "create an issue" or "make me an issue" + - Not treating the first user message as the issue description + - Delaying the workflow start with unnecessary questions + - Asking if they want to create an issue when they've already chosen this mode + - Not immediately beginning repository detection and codebase discovery + + - Vague descriptions like "doesn't work" or "broken" - Missing reproduction steps for bugs @@ -12,19 +21,106 @@ - Asking for technical details from non-contributing users - - Exploring codebase before confirming user wants to contribute + - Performing issue scoping before confirming user wants to contribute - Requiring acceptance criteria from problem reporters - Making the process too complex for simple problem reports - Not clearly indicating the "submit now" option - Overwhelming users with contributor requirements upfront + - Using hardcoded templates instead of repository templates + - Not checking for issue templates before creating content + - Ignoring template metadata like labels and assignees - Starting implementation before approval - - Not providing detailed technical analysis when contributing + - Not providing detailed issue scoping when contributing - Missing acceptance criteria for contributed features - Forgetting to include technical context from code exploration - Not considering trade-offs and alternatives - Proposing solutions without understanding current architecture + + + Not tracing data flow completely through the system + Missing that data already exists leads to proposing unnecessary new code + + - Use codebase_search extensively to find ALL related code + - Trace variables from creation to consumption + - Check if needed data is already calculated but not used + - Look for similar working features as patterns + + + Bad: "Add mode tracking to import function" + Good: "The export already includes mode info at line 234, just use it in import at line 567" + + + + + Proposing complex new systems when simple fixes exist + Creates unnecessary complexity, maintenance burden, and potential bugs + + - ALWAYS check if functionality already exists first + - Look for minimal changes that solve the problem + - Prefer using existing variables/functions differently + - Aim for the smallest possible diff + + + Bad: "Create new state management system for mode tracking" + Good: "Pass existing modeInfo variable from line 45 to the function at line 78" + + + + + Not reading actual code before proposing solutions + Solutions don't match the actual codebase structure + + - Always read the relevant files first + - Verify exact line numbers and content + - Check imports/exports to understand data availability + - Look at similar features that work correctly + + + + + Creating new patterns instead of following existing ones + Inconsistent codebase, harder to maintain + + - Find similar features that work correctly + - Follow the same patterns and structures + - Reuse existing utilities and helpers + - Maintain consistency with the codebase style + + + + + Using hardcoded templates when repository templates exist + Issues don't follow repository conventions, may be rejected or need reformatting + + - Always check .github/ISSUE_TEMPLATE/ directory first + - Parse and use repository templates when available + - Only create generic templates when none exist + + + + + Not properly parsing YAML template structure + Missing required fields, incorrect formatting, lost metadata + + - Parse YAML templates to extract all form elements + - Convert form elements to appropriate markdown sections + - Preserve field requirements and descriptions + - Maintain dropdown options and checkbox lists + + + + + Leaving placeholder text in final issue + Unprofessional appearance, confusion about what information is needed + + - Replace all placeholders with actual information + - Remove instruction text meant for template users + - Fill every section with relevant content + - Add "N/A" for truly inapplicable sections + + \ No newline at end of file diff --git a/.roo/rules-issue-writer/5_github_cli_usage.xml b/.roo/rules-issue-writer/5_github_cli_usage.xml index 8beb024d15..1792be87eb 100644 --- a/.roo/rules-issue-writer/5_github_cli_usage.xml +++ b/.roo/rules-issue-writer/5_github_cli_usage.xml @@ -3,9 +3,8 @@ The GitHub CLI (gh) provides comprehensive tools for interacting with GitHub. Here's when and how to use each command in the issue creation workflow. - Note: Issue body formatting should follow the templates defined in - 2_github_issue_templates.xml, with different formats for problem reporters - vs contributors. + Note: This mode prioritizes using repository-specific issue templates over + hardcoded ones. Templates are detected and used dynamically from the repository. @@ -16,7 +15,7 @@ - gh issue list --repo RooCodeInc/Roo-Code --search "dark theme button visibility" --state all --limit 20 + gh issue list --repo $REPO_FULL_NAME --search "dark theme button visibility" --state all --limit 20 @@ -35,7 +34,7 @@ - gh search issues --repo RooCodeInc/Roo-Code "dark theme button" --limit 10 + gh search issues --repo $REPO_FULL_NAME "dark theme button" --limit 10 @@ -47,7 +46,7 @@ - gh issue view 123 --repo RooCodeInc/Roo-Code --comments + gh issue view 123 --repo $REPO_FULL_NAME --comments @@ -58,6 +57,46 @@ + + + + Use to check for issue templates in the repository before creating issues. + This is not a gh command but necessary for template detection. + + + Check for templates in standard location: + + .github/ISSUE_TEMPLATE + true + + + Check for single template file: + + .github + false + + + + + + + Read template files to parse their structure and content. + Used after detecting template files. + + + Read YAML template: + + .github/ISSUE_TEMPLATE/bug_report.yml + + + Read Markdown template: + + .github/ISSUE_TEMPLATE/feature_request.md + + + + + These commands should ONLY be used if the user has indicated they want to @@ -70,7 +109,7 @@ - gh repo view RooCodeInc/Roo-Code --json defaultBranchRef,description,updatedAt + gh repo view $REPO_FULL_NAME --json defaultBranchRef,description,updatedAt @@ -82,7 +121,7 @@ - gh search prs --repo RooCodeInc/Roo-Code "dark theme" --limit 10 --state all + gh search prs --repo $REPO_FULL_NAME "dark theme" --limit 10 --state all @@ -105,18 +144,19 @@ Only use after: 1. Confirming no duplicates exist - 2. Gathering all required information - 3. Determining if user is contributing or just reporting - 4. Getting user confirmation + 2. Checking for and using repository templates + 3. Gathering all required information + 4. Determining if user is contributing or just reporting + 5. Getting user confirmation - gh issue create --repo RooCodeInc/Roo-Code --title "[Descriptive title of the bug]" --body-file /tmp/issue_body.md --label "bug" + gh issue create --repo $REPO_FULL_NAME --title "[Descriptive title of the bug]" --body-file /tmp/issue_body.md --label "bug" - gh issue create --repo RooCodeInc/Roo-Code --title "[Problem-focused title]" --body-file /tmp/issue_body.md --label "proposal" --label "enhancement" + gh issue create --repo $REPO_FULL_NAME --title "[Problem-focused title]" --body-file /tmp/issue_body.md --label "proposal" --label "enhancement" @@ -138,7 +178,7 @@ - gh issue comment 456 --repo RooCodeInc/Roo-Code --body "Additional context or comments." + gh issue comment 456 --repo $REPO_FULL_NAME --body "Additional context or comments." @@ -150,7 +190,7 @@ - gh issue edit 456 --repo RooCodeInc/Roo-Code --title "[Updated title]" --body "[Updated body]" + gh issue edit 456 --repo $REPO_FULL_NAME --title "[Updated title]" --body "[Updated body]" @@ -164,41 +204,41 @@ 3. Ask if they want to continue or comment on existing issue - - When searching GitHub Discussions: - 1. Note that GitHub CLI doesn't currently have full discussions support - 2. Use web search or instruct user to manually search discussions at: - https://github.com/RooCodeInc/Roo-Code/discussions/categories/feature-requests - 3. Ask user to provide any related discussion numbers they find - 4. Include these in the "Related Discussions" section of the issue - + + Template detection (NEW): + 1. Use list_files to check .github/ISSUE_TEMPLATE/ directory + 2. Read any template files found (YAML or Markdown) + 3. Parse template structure and metadata + 4. If multiple templates, let user choose + 5. If no templates, prepare to create generic one + - + Decision point for contribution: 1. Ask user if they want to contribute implementation 2. If yes: Use contributor commands for codebase investigation 3. If no: Skip directly to creating a problem-focused issue 4. This saves time for problem reporters - + - + During codebase exploration (CONTRIBUTORS ONLY): - 1. Clone repo locally if needed: `gh repo clone RooCodeInc/Roo-Code` + 1. Clone repo locally if needed: `gh repo clone $REPO_FULL_NAME` 2. Use `git log` to find recent changes to affected files 3. Use `gh search prs` for related pull requests 4. Include findings in the technical context section - + - + When creating the issue: - 1. Format differently based on contributor vs problem reporter - 2. Problem reporters: Simple problem description + context - 3. Contributors: Full template with technical sections + 1. Use repository template if found, or generic template if not + 2. Fill template with gathered information + 3. Format differently based on contributor vs problem reporter 4. Save formatted body to temporary file - 5. Use `gh issue create` with appropriate labels + 5. Use `gh issue create` with appropriate labels from template 6. Capture the returned issue URL 7. Show user the created issue URL - + @@ -270,4 +310,33 @@ gh repo clone - Clone repository + + + + When parsing YAML templates: + - Extract 'name' for template identification + - Get 'labels' array for automatic labeling + - Parse 'body' array for form elements + - Convert form elements to markdown sections + - Preserve 'required' field indicators + + + + When parsing Markdown templates: + - Check for YAML front matter + - Extract metadata (labels, assignees) + - Identify section headers + - Replace placeholder text + - Maintain formatting structure + + + + 1. Detect templates with list_files + 2. Read templates with read_file + 3. Parse structure and metadata + 4. Let user choose if multiple exist + 5. Fill template with information + 6. Create issue with template content + + \ No newline at end of file diff --git a/.roo/rules-issue-writer/6_technical_analysis_workflow.xml b/.roo/rules-issue-writer/6_technical_analysis_workflow.xml deleted file mode 100644 index c61d8fc1ca..0000000000 --- a/.roo/rules-issue-writer/6_technical_analysis_workflow.xml +++ /dev/null @@ -1,349 +0,0 @@ - - - This sub-workflow provides an aggressive, thorough, and all-encompassing investigation - process for technical analysis when creating GitHub issues. It employs methods from - the issue-investigator mode to deeply analyze the codebase and formulate comprehensive - technical solutions. This workflow is designed to produce scoped issues that can be - used in automated fix workflows. - - - - - Create Investigation Plan - - When technical analysis is requested, immediately create a comprehensive todo list - to track the investigation progress. - - - -[ ] Extract keywords from the issue description -[ ] Perform initial broad codebase search -[ ] Analyze search results and identify key components -[ ] Deep dive into relevant files and implementations -[ ] Form initial hypothesis about the issue/feature -[ ] Attempt to disprove hypothesis through further investigation -[ ] Identify all affected files and dependencies -[ ] Map out the complete implementation approach -[ ] Document technical risks and edge cases -[ ] Formulate comprehensive technical solution -[ ] Create detailed acceptance criteria -[ ] Prepare technical analysis summary - - - ]]> - - - - - - - Extract all relevant keywords, concepts, and technical terms from the issue description. - Be exhaustive - include function names, error messages, feature names, and related concepts. - - - Identify primary technical concepts - Extract error messages or specific symptoms - Note any mentioned file paths or components - List related features or functionality - Include synonyms and related terms - - Mark "Extract keywords from the issue description" as complete - - - - - Perform multiple rounds of codebase searches, starting broad and progressively - narrowing based on findings. This is an aggressive, exhaustive search process. - - - Initial Broad Search - - Use codebase_search with all extracted keywords to get an overview of relevant code. - -[Combined keywords from extraction phase] - - ]]> - - - - - Component Discovery - - Based on initial results, identify key components and search for: - - Related class/function definitions - - Import statements and dependencies - - Configuration files - - Test files that might reveal expected behavior - - - - - Deep Implementation Search - - Search for specific implementation details: - - Error handling patterns - - State management - - API endpoints or routes - - Database queries or models - - UI components and their interactions - - - - - Edge Case and Integration Search - - Look for: - - Edge cases in the code - - Integration points with other systems - - Configuration options that affect behavior - - Feature flags or conditional logic - - - - Update search-related todos as each iteration completes - - - - - Thoroughly analyze all relevant files discovered during the search phase. - - - Use list_code_definition_names to understand file structure - Read complete files to understand full context - Trace execution paths through the code - Identify all dependencies and imports - Map relationships between components - - - Document findings including: - - File paths and their purposes - - Key functions and their responsibilities - - Data flow through the system - - External dependencies - - Potential impact areas - - Mark file analysis todos as complete - - - - - Form a comprehensive hypothesis about the issue or feature implementation. - - - - Identify the most likely root cause - Trace the bug through the execution path - Determine why the current implementation fails - Consider environmental factors - - - - - Identify the optimal integration points - Determine required architectural changes - Plan the implementation approach - Consider scalability and maintainability - - - Mark hypothesis formation as complete - - - - - Aggressively attempt to disprove the hypothesis by searching for contradictory evidence. - - - - Search for Alternative Implementations - Look for similar features implemented differently - Check for deprecated code that might interfere - - - Configuration and Environment Check - Search for configuration that could change behavior - Look for environment-specific code paths - - - Test Case Analysis - Find existing tests that might contradict hypothesis - Look for test cases that reveal edge cases - - - Historical Context - Search for comments explaining design decisions - Look for TODO or FIXME comments related to the area - - - - If hypothesis is disproven, return to search phase with new insights. - If hypothesis stands, proceed to solution formulation. - - Update hypothesis validation status - - - - - Create a comprehensive technical solution with extreme detail. - - - -
- - Exact files to modify with line numbers - - New files to create with full paths - - Specific code changes required - - Order of implementation steps - - Migration strategy if needed -
-
- - -
- - All files that import affected code - - API contracts that must be maintained - - Database schema changes if any - - Configuration changes required - - Documentation updates needed -
-
- - -
- - Unit tests to add or modify - - Integration tests required - - Edge cases to test - - Performance testing needs - - Manual testing scenarios -
-
- - -
- - Breaking changes identified - - Performance implications - - Security considerations - - Backward compatibility issues - - Rollback strategy -
-
-
- Mark solution formulation as complete -
- - - - Create extremely detailed acceptance criteria that can guide automated implementation. - - - - Each criterion must be independently testable - Include both positive and negative test cases - Specify exact error messages and codes - Define performance thresholds where applicable - - Mark acceptance criteria creation as complete - -
- - - - - - - - All keywords extracted and searched - Multiple search iterations completed - All relevant files analyzed - Hypothesis formed and validated - Comprehensive solution documented - Acceptance criteria defined - All risks and edge cases identified - Technical analysis formatted for issue - - - Mark all investigation todos as complete and update the main workflow todo list - - - \ No newline at end of file diff --git a/.roo/rules-mode-writer/1_mode_creation_workflow.xml b/.roo/rules-mode-writer/1_mode_creation_workflow.xml index 15a48aa804..77a1728599 100644 --- a/.roo/rules-mode-writer/1_mode_creation_workflow.xml +++ b/.roo/rules-mode-writer/1_mode_creation_workflow.xml @@ -1,124 +1,278 @@ - + - This workflow guides you through creating a new custom mode to be used in the Roo Code Software, - from initial requirements gathering to final implementation. + This workflow guides you through creating new custom modes or editing existing modes + for the Roo Code Software, ensuring comprehensive understanding and cohesive implementation. - + - Gather Requirements + Determine User Intent - Understand what the user wants the mode to accomplish + Identify whether the user wants to create a new mode or edit an existing one - - Ask about the mode's primary purpose and use cases - Identify what types of tasks the mode should handle - Determine what tools and file access the mode needs - Clarify any special behaviors or restrictions - - + + + + User mentions a specific mode by name or slug + User references a mode directory path (e.g., .roo/rules-[mode-slug]) + User asks to modify, update, enhance, or fix an existing mode + User says "edit this mode" or "change this mode" + + + + + User asks to create a new mode + User describes a new capability not covered by existing modes + User says "make a mode for" or "create a mode that" + + + + - What is the primary purpose of this new mode? What types of tasks should it handle? + I want to make sure I understand correctly. Are you looking to create a brand new mode or modify an existing one? - A mode for writing and maintaining documentation - A mode for database schema design and migrations - A mode for API endpoint development and testing - A mode for performance optimization and profiling + Create a new mode for a specific purpose + Edit an existing mode to add new capabilities + Fix issues in an existing mode + Enhance an existing mode with better workflows - + + - - Design Mode Configuration - - Create the mode definition with all required fields - - - - Unique identifier (lowercase, hyphens allowed) - Keep it short and descriptive (e.g., "api-dev", "docs-writer") - - - Display name with optional emoji - Use an emoji that represents the mode's purpose - - - Detailed description of the mode's role and expertise - - Start with "You are Roo Code, a [specialist type]..." - List specific areas of expertise - Mention key technologies or methodologies - - - - Tool groups the mode can access - - - - - - - - - - - - Clear description for the Orchestrator - Explain specific scenarios and task types - - - - Do not include customInstructions in the .roomodes configuration. - All detailed instructions should be placed in XML files within - the .roo/rules-[mode-slug]/ directory instead. - - + + + + Gather Requirements for New Mode + + Understand what the user wants the new mode to accomplish + + + Ask about the mode's primary purpose and use cases + Identify what types of tasks the mode should handle + Determine what tools and file access the mode needs + Clarify any special behaviors or restrictions + + + + What is the primary purpose of this new mode? What types of tasks should it handle? + + A mode for writing and maintaining documentation + A mode for database schema design and migrations + A mode for API endpoint development and testing + A mode for performance optimization and profiling + + + + - - Implement File Restrictions - - Configure appropriate file access permissions - - - Restrict edit access to specific file types - + + Design Mode Configuration + + Create the mode definition with all required fields + + + + Unique identifier (lowercase, hyphens allowed) + Keep it short and descriptive (e.g., "api-dev", "docs-writer") + + + Display name with optional emoji + Use an emoji that represents the mode's purpose + + + Detailed description of the mode's role and expertise + + Start with "You are Roo Code, a [specialist type]..." + List specific areas of expertise + Mention key technologies or methodologies + + + + Tool groups the mode can access + + + + + + + + + + + + Clear description for the Orchestrator + Explain specific scenarios and task types + + + + Do not include customInstructions in the .roomodes configuration. + All detailed instructions should be placed in XML files within + the .roo/rules-[mode-slug]/ directory instead. + + + + + Implement File Restrictions + + Configure appropriate file access permissions + + + Restrict edit access to specific file types + groups: - read - - edit - fileRegex: \.(md|txt|rst)$ description: Documentation files only - command - - - - Use regex patterns to limit file editing scope - Provide clear descriptions for restrictions - Consider the principle of least privilege - - + + + + Use regex patterns to limit file editing scope + Provide clear descriptions for restrictions + Consider the principle of least privilege + + - - Create XML Instruction Files + + Create XML Instruction Files + + Design structured instruction files in .roo/rules-[mode-slug]/ + + + Main workflow and step-by-step processes + Guidelines and conventions + Reusable code patterns and examples + Specific tool usage instructions + Complete workflow examples + + + Use semantic tag names that describe content + Nest tags hierarchically for better organization + Include code examples in CDATA sections when needed + Add comments to explain complex sections + + + + + + + Immerse in Existing Mode + + Fully understand the existing mode before making any changes + + + Locate and read the mode configuration in .roomodes + Read all XML instruction files in .roo/rules-[mode-slug]/ + Analyze the mode's current capabilities and limitations + Understand the mode's role in the broader ecosystem + + + + What specific aspects of the mode would you like to change or enhance? + + Add new capabilities or tool permissions + Fix issues with current workflows or instructions + Improve the mode's roleDefinition or whenToUse description + Enhance XML instructions for better clarity + + + + + + + Analyze Change Impact + + Understand how proposed changes will affect the mode + + + Compatibility with existing workflows + Impact on file permissions and tool access + Consistency with mode's core purpose + Integration with other modes + + + + I've analyzed the existing mode. Here's what I understand about your requested changes. Is this correct? + + Yes, that's exactly what I want to change + Mostly correct, but let me clarify some details + No, I meant something different + I'd like to add additional changes + + + + + + + Plan Modifications + + Create a detailed plan for modifying the mode + + + Identify which files need to be modified + Determine if new XML instruction files are needed + Check for potential conflicts or contradictions + Plan the order of changes for minimal disruption + + + + + Implement Changes + + Apply the planned modifications to the mode + + + Update .roomodes configuration if needed + Modify existing XML instruction files + Create new XML instruction files if required + Update examples and documentation + + + + + + + + Validate Cohesion and Consistency - Design structured instruction files in .roo/rules-[mode-slug]/ + Ensure all changes are cohesive and don't contradict each other - - Main workflow and step-by-step processes - Guidelines and conventions - Reusable code patterns and examples - Specific tool usage instructions - Complete workflow examples - - - Use semantic tag names that describe content - Nest tags hierarchically for better organization - Include code examples in CDATA sections when needed - Add comments to explain complex sections - + + + Mode slug follows naming conventions + File restrictions align with mode purpose + Tool permissions are appropriate + whenToUse clearly differentiates from other modes + + + All XML files follow consistent structure + No contradicting instructions between files + Examples align with stated workflows + Tool usage matches granted permissions + + + Mode integrates well with Orchestrator + Clear boundaries with other modes + Handoff points are well-defined + + + + + I've completed the validation checks. Would you like me to review any specific aspect in more detail? + + Review the file permission patterns + Check for workflow contradictions + Verify integration with other modes + Everything looks good, proceed to testing + + + - + Test and Refine Verify the mode works as intended @@ -129,14 +283,19 @@ groups: Instructions are clear and actionable Mode integrates well with Orchestrator All examples are accurate and helpful + Changes don't break existing functionality (for edits) + New capabilities work as expected - + Create mode in .roomodes for project-specific modes Create mode in global custom_modes.yaml for system-wide modes Use list_files to verify .roo folder structure Test file regex patterns with search_files + Use codebase_search to find existing mode implementations + Read all XML files in a mode directory to understand its structure + Always validate changes for cohesion and consistency - \ No newline at end of file + \ No newline at end of file diff --git a/.roo/rules-mode-writer/5_complete_mode_examples.xml b/.roo/rules-mode-writer/5_complete_mode_examples.xml index 45fd577c53..7c8f8a4cde 100644 --- a/.roo/rules-mode-writer/5_complete_mode_examples.xml +++ b/.roo/rules-mode-writer/5_complete_mode_examples.xml @@ -1,96 +1,214 @@ - - - Generate OpenAPI documentation for a REST API with user management endpoints - + + + + Edit the test mode to add support for performance testing and benchmarking + + + + I want to edit the test mode to add benchmark testing capabilities + - - - Discover API endpoints - -express router app.get app.post app.put app.delete @Route @Get @Post - - ]]> - Found routes in src/routes/users.ts - + + + Determine user intent and clarify scope + +I understand you want to edit the test mode to add performance testing capabilities. Before we proceed, let me confirm the scope of changes. What specific performance testing features do you need? + +Add benchmark testing with Vitest's bench API +Include performance profiling and memory usage tracking +Add load testing capabilities for API endpoints +All of the above - comprehensive performance testing suite + + + ]]> + User selected: Add benchmark testing with Vitest's bench API + - - Analyze endpoint implementation - + Immerse in existing test mode configuration + -src/routes/users.ts +.roomodes - ]]> - + ]]> + Found test mode configuration with current roleDefinition and file restrictions + - - Generate OpenAPI specification - + Explore existing test mode instructions + +.roo/rules-test +true + + ]]> + +.roo/rules-test/1_workflow.xml + + ]]> + Understood current test workflows and patterns + + + + Validate changes for cohesion + +I've analyzed the test mode. Adding benchmark testing would require: +1. Updating the roleDefinition to include performance testing expertise +2. Modifying file restrictions to include .bench.ts files +3. Adding new workflow instructions for benchmark creation +4. Including Vitest bench API examples + +This aligns well with the existing testing focus. Should I proceed with all these changes? + +Yes, implement all the changes as described +Start with just the roleDefinition and file restrictions +Focus only on the workflow instructions first +Let me review the specific changes before proceeding + + + ]]> + User selected: Yes, implement all the changes as described + + + + Update mode configuration in .roomodes + +.roomodes + +<<<<<<< SEARCH + 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 +======= + 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 - Benchmark testing with Vitest bench API - Performance profiling and optimization - Memory usage analysis in tests +>>>>>>> REPLACE + + + ]]> + + + + Update file restrictions to include benchmark files + +.roomodes + +<<<<<<< SEARCH + - fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$) + description: Test files, mocks, and Vitest configuration +======= + - fileRegex: (__tests__/.*|__mocks__/.*|\.test\.(ts|tsx|js|jsx)$|\.spec\.(ts|tsx|js|jsx)$|\.bench\.(ts|tsx|js|jsx)$|/test/.*|vitest\.config\.(js|ts)$|vitest\.setup\.(js|ts)$) + description: Test files, benchmarks, mocks, and Vitest configuration +>>>>>>> REPLACE + + + ]]> + + + + Create new benchmark workflow instructions + -docs/api/openapi.yaml +.roo/rules-test/5_benchmark_testing.xml -openapi: 3.0.0 -info: - title: User Management API - version: 1.0.0 - description: API for managing user accounts + + + Guidelines for creating and maintaining performance benchmarks using Vitest bench API + -servers: - - url: https://api.example.com/v1 - description: Production server - - url: http://localhost:3000/v1 - description: Development server + + + Basic benchmark structure + { + bench('Array.push', () => { + const arr = []; + for (let i = 0; i < 1000; i++) { + arr.push(i); + } + }); -components: - schemas: - User: - type: object - required: - - id - - email - properties: - id: - type: string - format: uuid - email: - type: string - format: email - name: - type: string - createdAt: - type: string - format: date-time \ No newline at end of file + bench('Array spread', () => { + let arr = []; + for (let i = 0; i < 1000; i++) { + arr = [...arr, i]; + } + }); +}); + ]]> + + + + + Use meaningful benchmark names that describe what's being measured + Include baseline comparisons when optimizing code + Run benchmarks in isolation to avoid interference + Document performance expectations and thresholds + + + +38 + + ]]> + + + + Validate all changes for consistency + +I've completed all the changes to add benchmark testing capabilities to the test mode: + +1. ✓ Updated roleDefinition to include benchmark expertise +2. ✓ Modified file restrictions to include .bench.ts files +3. ✓ Created new benchmark workflow instructions +4. ✓ Added Vitest bench API examples + +All changes are cohesive and align with the existing test mode structure. Would you like me to test any specific aspect of these changes? + +Test creating a new benchmark file to verify file permissions +Review the complete changes one more time +Everything looks good, the mode is ready +Create a simple benchmark example to test + + + ]]> + + + + + +I've successfully enhanced the test mode with benchmark testing capabilities: + +1. Updated the roleDefinition to include: + - Benchmark testing with Vitest bench API + - Performance profiling and optimization + - Memory usage analysis in tests + +2. Modified file restrictions to include .bench.ts files, allowing the mode to create and edit benchmark files + +3. Created new workflow instructions at .roo/rules-test/5_benchmark_testing.xml with: + - Basic benchmark patterns + - Best practices for performance testing + - Example code using Vitest's bench API + +All changes have been validated for cohesion and consistency with the existing test mode structure. The mode now supports comprehensive performance testing alongside its existing unit and integration testing capabilities. + + + ]]> + + + Always immerse yourself in the existing mode before making changes + Use ask_followup_question aggressively to clarify scope and validate changes + Validate all changes for cohesion and consistency + Update all relevant parts: configuration, file restrictions, and instructions + Test changes to ensure they work as expected + + + \ No newline at end of file diff --git a/.roo/rules-mode-writer/7_validation_cohesion_checking.xml b/.roo/rules-mode-writer/7_validation_cohesion_checking.xml new file mode 100644 index 0000000000..a327a1e465 --- /dev/null +++ b/.roo/rules-mode-writer/7_validation_cohesion_checking.xml @@ -0,0 +1,201 @@ + + + Guidelines for thoroughly validating mode changes to ensure cohesion, + consistency, and prevent contradictions across all mode components. + + + + + + Every change must be reviewed in context of the entire mode + + + Read all existing XML instruction files + Verify new changes align with existing patterns + Check for duplicate or conflicting instructions + Ensure terminology is consistent throughout + + + + + + Use ask_followup_question extensively to clarify ambiguities + + + User's intent is unclear + Multiple interpretations are possible + Changes might conflict with existing functionality + Impact on other modes needs clarification + + +I notice this change might affect how the mode interacts with file permissions. Should we also update the file regex patterns to match? + +Yes, update the file regex to include the new file types +No, keep the current file restrictions as they are +Let me explain what file types I need to work with +Show me the current file restrictions first + + + ]]> + + + + + Actively search for and resolve contradictions + + + + Permission Mismatch + Instructions reference tools the mode doesn't have access to + Either grant the tool permission or update the instructions + + + Workflow Conflicts + Different XML files describe conflicting workflows + Consolidate workflows and ensure single source of truth + + + Role Confusion + Mode's roleDefinition doesn't match its actual capabilities + Update roleDefinition to accurately reflect the mode's purpose + + + + + + + + Before making any changes + + Read and understand all existing mode files + Create a mental model of current mode behavior + Identify potential impact areas + Ask clarifying questions about intended changes + + + + + While making changes + + Document each change and its rationale + Cross-reference with other files after each change + Verify examples still work with new changes + Update related documentation immediately + + + + + After changes are complete + + + All XML files are well-formed and valid + File naming follows established patterns + Tag names are consistent across files + No orphaned or unused instructions + + + + roleDefinition accurately describes the mode + whenToUse is clear and distinguishable + Tool permissions match instruction requirements + File restrictions align with mode purpose + Examples are accurate and functional + + + + Mode boundaries are well-defined + Handoff points to other modes are clear + No overlap with other modes' responsibilities + Orchestrator can correctly route to this mode + + + + + + + + Maintain consistent tone and terminology + + Use the same terms for the same concepts throughout + Keep instruction style consistent across files + Maintain the same level of detail in similar sections + + + + + Ensure instructions flow logically + + Prerequisites come before dependent steps + Complex concepts build on simpler ones + Examples follow the explained patterns + + + + + Ensure all aspects are covered without gaps + + Every mentioned tool has usage instructions + All workflows have complete examples + Error scenarios are addressed + + + + + + + + Before we proceed with changes, I want to ensure I understand the full scope. What is the main goal of these modifications? + + Add new functionality while keeping existing features + Fix issues with current implementation + Refactor for better organization + Expand the mode's capabilities into new areas + + + + + + + This change might affect other parts of the mode. How should we handle the impact on [specific area]? + + Update all affected areas to maintain consistency + Keep the existing behavior for backward compatibility + Create a migration path from old to new behavior + Let me review the impact first + + + + + + + I've completed the changes and validation. Which aspect would you like me to test more thoroughly? + + Test the new workflow end-to-end + Verify file permissions work correctly + Check integration with other modes + Review all changes one more time + + + + + + + + Instructions reference tools not in the mode's groups + Either add the tool group or remove the instruction + + + File regex doesn't match described file types + Update regex pattern to match intended files + + + Examples don't follow stated best practices + Update examples to demonstrate best practices + + + Duplicate instructions in different files + Consolidate to single location and reference + + + \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/1_orchestrator_workflow.xml b/.roo/rules-pr-reviewer/1_orchestrator_workflow.xml deleted file mode 100644 index 8bb94694d6..0000000000 --- a/.roo/rules-pr-reviewer/1_orchestrator_workflow.xml +++ /dev/null @@ -1,202 +0,0 @@ - - - This workflow orchestrates a comprehensive pull request review process by delegating - specialized analysis tasks to appropriate modes while maintaining context through - structured report files. The orchestrator ensures critical review coverage while - avoiding redundant feedback. All GitHub operations are performed using the GitHub CLI. - - - - - Parse PR Information and Initialize Context - - Extract PR information from user input (URL or PR number). - Create context directory and tracking files. - If called by another mode (Issue Fixer, PR Fixer), set calledByMode field. - - - - Parse PR URL or number from user input - - Create directory: .roo/temp/pr-[PR_NUMBER]/ - - Initialize review-context.json with PR metadata - - Check if called by another mode and record it - - - - - - - Fetch PR Details and Context - - Use GitHub CLI to fetch comprehensive PR details. - - - gh pr view [PR_NUMBER] --repo [owner]/[repo] --json number,title,author,state,body,url,headRefName,baseRefName,files,additions,deletions,changedFiles - - .roo/temp/pr-[PR_NUMBER]/pr-metadata.json - - - - Fetch Linked Issue - - If PR references an issue, fetch its details for context. - - - gh issue view [issue_number] --repo [owner]/[repo] --json number,title,body,author,state - - .roo/temp/pr-[PR_NUMBER]/linked-issue.json - - - - Fetch Existing Comments and Reviews - - CRITICAL: Get all existing feedback to avoid redundancy. - - - gh pr view [PR_NUMBER] --repo [owner]/[repo] --json comments --jq '.comments' - gh pr view [PR_NUMBER] --repo [owner]/[repo] --json reviews --jq '.reviews' - - .roo/temp/pr-[PR_NUMBER]/existing-feedback.json - - - - Check Out PR Locally - gh pr checkout [PR_NUMBER] --repo [owner]/[repo] - Enable local code analysis and pattern comparison - - - - - - Delegate Pattern Analysis - - Create a subtask to analyze code patterns and organization. - - - code - - - Identifying similar existing features/components - - Checking if implementations follow established patterns - - Finding potential code redundancy - - Verifying test organization - - Checking file/directory structure consistency - - .roo/temp/pr-[PR_NUMBER]/pattern-analysis.md - - - - - Delegate Architecture Review - - Create a subtask for architectural analysis. - - - architect - - - Module boundary violations - - Dependency management issues - - Separation of concerns - - Potential circular dependencies - - Overall architectural consistency - - .roo/temp/pr-[PR_NUMBER]/architecture-review.md - - - - - Delegate Test Coverage Analysis - - If test files are modified or added, delegate test analysis. - - - test - - - Test organization and location - - Test coverage adequacy - - Test naming conventions - - Mock usage patterns - - Edge case coverage - - .roo/temp/pr-[PR_NUMBER]/test-analysis.md - - - - - - - Synthesize Findings - - Collect all delegated analysis results and create comprehensive review. - - - - Read all analysis files from .roo/temp/pr-[PR_NUMBER]/ - - Identify critical issues vs suggestions - - Check against existing comments to avoid redundancy - - Prioritize findings by impact - - - - - Create Final Review Report - - Generate comprehensive review report with all findings. - - .roo/temp/pr-[PR_NUMBER]/final-review.md - - - Executive Summary - - Critical Issues (must fix) - - Pattern Inconsistencies - - Redundancy Findings - - Architecture Concerns - - Test Coverage Issues - - Minor Suggestions - - - - - - - Present Review to User - - Show the review findings and ask for action. - - - - Only present the analysis report, do not comment on PR - - - Ask user if they want to post the review as a comment - - - - - - Post Review Comment (if approved) - - If user approves and not called by another mode, post review using GitHub CLI. - - - gh pr comment [PR_NUMBER] --repo [owner]/[repo] --body-file .roo/temp/pr-[PR_NUMBER]/final-review.md - - - - - - - - Inform user to run 'gh auth login' and check authentication status - - - Verify PR number and repository, ask user to confirm details - - - Wait briefly and retry, inform user about rate limiting - - - - Continue with available analysis and note limitations - - - Always save intermediate results to temp files - - - \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/1_workflow.xml b/.roo/rules-pr-reviewer/1_workflow.xml new file mode 100644 index 0000000000..1fab579dfe --- /dev/null +++ b/.roo/rules-pr-reviewer/1_workflow.xml @@ -0,0 +1,498 @@ + + + Initialize Review Process + + Create a todo list to track the PR review workflow: + + + + [ ] Fetch pull request information + [ ] Fetch associated issue (if any) + [ ] Fetch pull request diff + [ ] Fetch existing PR comments and reviews + [ ] Check out pull request locally + [ ] Verify existing comments against current code + [ ] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + This helps track progress through the review process and ensures all steps are completed. + + + + + Fetch Pull Request Information + + If the user provides a PR number or URL, extract the necessary information: + - Repository owner and name + - Pull request number + + Use the GitHub CLI to fetch the PR details: + + + gh pr view [PR_NUMBER] --repo [owner]/[repo] --json number,title,body,author,state,url,headRefName,baseRefName,headRefOid,mergeable,isDraft,createdAt,updatedAt + + + Parse the JSON output to understand the PR's current state and metadata. + IMPORTANT: Save the headRefOid value as it will be needed for submitting the review via the API. + + + + [x] Fetch pull request information + [ ] Fetch associated issue (if any) + [ ] Fetch pull request diff + [ ] Fetch existing PR comments and reviews + [ ] Check out pull request locally + [ ] Verify existing comments against current code + [ ] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Fetch Associated Issue (If Any) + + Check the pull request body for a reference to a GitHub issue (e.g., "Fixes #123", "Closes #456"). + If an issue is referenced, use the GitHub CLI to fetch its details: + + + gh issue view [issue_number] --repo [owner]/[repo] --json number,title,body,author,state,url,createdAt,updatedAt,comments + + + The issue description and comments can provide valuable context for the review. + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [ ] Fetch pull request diff + [ ] Fetch existing PR comments and reviews + [ ] Check out pull request locally + [ ] Verify existing comments against current code + [ ] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Fetch Pull Request Diff + + Get the pull request diff to understand the changes: + + + gh pr diff [PR_NUMBER] --repo [owner]/[repo] + + + This will show the complete diff of all changes in the PR. + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [ ] Fetch existing PR comments and reviews + [ ] Check out pull request locally + [ ] Verify existing comments against current code + [ ] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Fetch Existing PR Comments and Reviews + + IMPORTANT: Before reviewing any code, first get all existing comments and reviews to understand what feedback has already been provided: + + Fetch all review comments: + + gh pr view [PR_NUMBER] --repo [owner]/[repo] --comments + + + Also fetch review details: + + gh api repos/[owner]/[repo]/pulls/[PR_NUMBER]/reviews + + + Create a mental or written list of: + - All issues/suggestions that have been raised + - The specific files and line numbers mentioned + - Whether comments appear to be resolved or still pending + + This information will guide your review to avoid duplicate feedback. + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [x] Fetch existing PR comments and reviews + [ ] Check out pull request locally + [ ] Verify existing comments against current code + [ ] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Check Out Pull Request Locally + + Use the GitHub CLI to check out the pull request locally: + + + gh pr checkout [PR_NUMBER] --repo [owner]/[repo] + + + This allows you to: + - Navigate the actual code structure + - Understand how changes interact with existing code + - Get better context for your review + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [x] Fetch existing PR comments and reviews + [x] Check out pull request locally + [ ] Verify existing comments against current code + [ ] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Verify Existing Comments Against Current Code + + Now that you have the code checked out locally and know what comments exist: + + 1. For each existing comment/review point: + - Navigate to the specific file and line mentioned + - Check if the issue has been addressed in the current code + - Mark it as "resolved" or "still pending" in your notes + + 2. Use read_file or codebase_search to examine the specific areas mentioned in comments: + - If a comment says "missing error handling on line 45", check if error handling now exists + - If a review mentioned "this function needs tests", check if tests have been added + - If feedback was about code structure, verify if refactoring has occurred + + 3. Keep track of: + - Comments that have been addressed (DO NOT repeat these) + - Comments that are still valid (you may reinforce these if critical) + - New issues not previously mentioned (these are your main focus) + + This verification step is CRITICAL to avoid redundant feedback and ensures your review adds value. + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [x] Fetch existing PR comments and reviews + [x] Check out pull request locally + [x] Verify existing comments against current code + [ ] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Perform Comprehensive Review + + Review the pull request thoroughly: + - Verify that the changes are directly related to the linked issue and do not include unrelated modifications. + - Focus primarily on the changes made in the PR. + - Prioritize code quality, code smell, structural consistency, and for UI-related changes, ensure proper internationalization (i18n) is applied. + - Watch for signs of technical debt (e.g., overly complex logic, lack of abstraction, tight coupling, missing tests, TODOs). + - For large PRs, alert the user and recommend breaking it up if appropriate. + - NEVER run tests or execute code in PR Reviewer mode. The repository likely has automated testing. Your role is limited to: + - Code review and analysis + - Leaving review comments + - Checking code quality and structure + - Reviewing test coverage and quality (without execution) + + Document your findings in a numbered list format: + 1. Code quality issues + 2. Structural improvements + 3. Missing tests or documentation + 4. Potential bugs or edge cases + 5. Performance concerns + 6. Security considerations + 7. Internationalization (i18n) issues + 8. Technical debt indicators + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [x] Fetch existing PR comments and reviews + [x] Check out pull request locally + [x] Verify existing comments against current code + [x] Perform comprehensive review + [ ] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Prepare Review Comments + + Format your review comments following these guidelines: + + CRITICAL: Before adding any comment, verify it's not already addressed: + - Cross-reference with your notes from Step 7 + - Only comment on NEW issues or UNRESOLVED existing issues + - Never repeat feedback that has been addressed in the current code + + Your suggestions should: + - Use a **friendly, curious tone** — prefer asking: "Is this intentional?" or "Could we approach this differently to improve X?" + - Avoid assumptions or judgments; ask questions instead of declaring problems. + - Skip ALL praise and positive comments. Focus exclusively on issues that need attention. + - Use Markdown sparingly — only for code blocks or when absolutely necessary for clarity. Avoid markdown headings (###, ##, etc.) entirely. + - Avoid including internal evaluation terminology (e.g., scores or internal tags) in public comments. + + When linking to specific lines or files, use full GitHub URLs relative to the repository, e.g. + `https://github.com/[owner]/[repo]/blob/[branch]/[path/to/file]#L[line-number]`. + + Present your findings as a numbered list organized by priority: + + **Critical Issues (Must Fix):** + 1. [Issue description with file/line reference] + 2. [Issue description with file/line reference] + + **Important Suggestions (Should Consider):** + 3. [Suggestion with rationale] + 4. [Suggestion with rationale] + + **Minor Improvements (Nice to Have):** + 5. [Improvement suggestion] + 6. [Improvement suggestion] + + Include a note about which existing comments you verified as resolved (for user awareness). + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [x] Fetch existing PR comments and reviews + [x] Check out pull request locally + [x] Verify existing comments against current code + [x] Perform comprehensive review + [x] Prepare review comments + [ ] Preview review with user + [ ] Submit review or create tasks + + + + + + + Preview Review with User + + Always show the user a preview of your review suggestions and comments before taking any action. + Present your findings as a numbered list clearly for the user before submitting comments. + + + I've completed my review of PR #[number]. Here's what I found: + + [If applicable: **Existing comments that have been resolved:** + - Comment about X on file Y - now addressed + - Suggestion about Z - implemented] + + **Review Findings:** + + **Critical Issues (Must Fix):** + 1. [Specific issue with file/line reference] + 2. [Specific issue with file/line reference] + + **Important Suggestions (Should Consider):** + 3. [Suggestion with rationale] + 4. [Suggestion with rationale] + + **Minor Improvements (Nice to Have):** + 5. [Improvement suggestion] + 6. [Improvement suggestion] + + Would you like me to: + + Create a comprehensive review with all comments + Create individual tasks for each suggestion using new_task + Let me modify the suggestions first + Skip submission - just wanted the analysis + + + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [x] Fetch existing PR comments and reviews + [x] Check out pull request locally + [x] Verify existing comments against current code + [x] Perform comprehensive review + [x] Prepare review comments + [x] Preview review with user + [ ] Submit review or create tasks + + + + + + + Submit Review + + Based on user preference, submit the review using the GitHub API to support inline comments: + + 1. Construct the review payload with inline comments. For each comment, you need: + - The file path (relative to repository root) + - The line number where the comment should appear + - The comment body text + - The side ("RIGHT" for new code, "LEFT" for old code) + + 2. Submit the review using the GitHub API with heredoc syntax: + + gh api -X POST repos/[owner]/[repo]/pulls/[PR_NUMBER]/reviews --input - < + + + The review will be created with all inline comments attached to specific lines of code. + + Note on event types: + - "COMMENT": Submit general feedback without approval/rejection + - "REQUEST_CHANGES": Request changes be made before merging + - "APPROVE": Approve the PR for merging + + Example for a review requesting changes: + + gh api -X POST repos/RooCodeInc/Roo-Code/pulls/6378/reviews --input - < + + + + + [x] Fetch pull request information + [x] Fetch associated issue (if any) + [x] Fetch pull request diff + [x] Fetch existing PR comments and reviews + [x] Check out pull request locally + [x] Verify existing comments against current code + [x] Perform comprehensive review + [x] Prepare review comments + [x] Preview review with user + [x] Submit review or create tasks + + + + + + + Create Tasks for Suggestions (Optional) + + If the user chooses to create individual tasks for each suggestion, use the new_task tool to create separate tasks: + + For each numbered finding from your review: + 1. Determine the appropriate mode based on the type of work needed: + - Use "code" mode for bug fixes, implementation changes, or refactoring + - Use "translate" mode for internationalization (i18n) issues + - Use "test" mode for missing or inadequate test coverage + - Use "docs-extractor" mode for documentation issues + - Use "architect" mode for structural or design improvements + - Use "debug" mode for investigating potential bugs + + 2. Create a clear, actionable task message that includes: + - The specific issue or suggestion + - The file(s) and line numbers affected + - Any relevant context from the PR + - The expected outcome + + 3. Use the new_task tool for each suggestion: + + [appropriate mode based on task type] + Fix [issue type] in [file]: [specific description of what needs to be done] + + + Example task creation: + + code + Fix missing error handling in src/api/users.ts:45-52. The getUserById function should handle cases where the user is not found and return an appropriate error response. + + + + translate + Add missing i18n translations for new user profile fields in src/components/UserProfile.tsx. The fields 'bio', 'location', and 'website' need to be wrapped with translation functions. + + + After creating all tasks, provide a summary: + "I've created [X] individual tasks for the review findings: + - [Y] code fixes/improvements + - [Z] translation/i18n tasks + - [etc.] + + Each task contains the specific context and requirements for addressing the issue." + + + \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/2_best_practices.xml b/.roo/rules-pr-reviewer/2_best_practices.xml new file mode 100644 index 0000000000..1d60ffda38 --- /dev/null +++ b/.roo/rules-pr-reviewer/2_best_practices.xml @@ -0,0 +1,40 @@ + + - ALWAYS create a todo list at the start to track the review workflow (Step 1) + - Use GitHub CLI (`gh`) commands instead of MCP tools for all GitHub operations + - ALWAYS fetch the PR's headRefOid in Step 2 - this is required for API review submission + - ALWAYS fetch existing comments and reviews BEFORE reviewing any code (Step 5) + - Create a list of all existing feedback before starting your review + - Check out the PR locally using `gh pr checkout` for better context understanding + - Systematically verify each existing comment against the current code (Step 7) + - Track which comments are resolved vs still pending + - Only provide feedback on NEW issues or UNRESOLVED existing issues + - Never duplicate feedback that has already been addressed + - Always fetch and review the entire PR diff before commenting + - Check for and review any associated issue for context + - 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 + - Ask questions rather than making assumptions - there may be intentions behind the code choices + - Provide actionable feedback with specific suggestions + - Focus exclusively on issues and improvements - skip all praise or positive comments + - Use minimal markdown - avoid headings (###, ##) and excessive formatting + - Only use markdown for code blocks or when absolutely necessary for clarity + - Consider the PR's scope - suggest breaking up large PRs + - Verify proper i18n implementation for UI changes + - Check for test coverage without executing tests + - Look for signs of technical debt and code smells + - Ensure consistency with existing code patterns + - Link to specific lines using full GitHub URLs + - Present findings in a numbered list format for clarity + - Group feedback by priority (critical, important, minor) + - Always preview comments with the user before submitting + - Use the GitHub API for submitting reviews to support inline comments + - Construct proper JSON payloads with commit_id, body, event, and comments array + - Each inline comment needs: path, body, line number, and side (RIGHT for new code) + - Choose appropriate review event: COMMENT, REQUEST_CHANGES, or APPROVE + - Use heredoc syntax (--input - < \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/2_critical_review_guidelines.xml b/.roo/rules-pr-reviewer/2_critical_review_guidelines.xml deleted file mode 100644 index ebccff3dbc..0000000000 --- a/.roo/rules-pr-reviewer/2_critical_review_guidelines.xml +++ /dev/null @@ -1,208 +0,0 @@ - - - These guidelines ensure PR reviews are appropriately critical while remaining - constructive. The goal is to maintain high code quality and consistency - across the codebase by identifying issues that might be overlooked in a - less thorough review. - - - - - Always support criticism with evidence from the codebase - - Instead of: "This doesn't follow our patterns" - Say: "This implementation differs from the pattern used in src/api/handlers/*.ts - where we consistently use the factory pattern for endpoint creation" - - - - - Reference similar existing implementations - - 1. Find 2-3 examples of similar features - 2. Identify the common patterns they follow - 3. Explain how the PR deviates from these patterns - 4. Suggest alignment with existing approaches - - - - - Challenge architectural choices when appropriate - - - "Why was this implemented as a separate module instead of extending the existing X module?" - - "This introduces a new pattern for Y. Have we considered using the established pattern from Z?" - - "This creates a circular dependency with module A. Could we restructure to maintain cleaner boundaries?" - - - - - - - Do new endpoints follow the same structure as existing ones? - Are error responses consistent with other endpoints? - Is authentication/authorization handled the same way? - Are request validations following established patterns? - - - - Do components follow the same file structure (types, helpers, component)? - Are props interfaces defined consistently? - Is state management approach consistent with similar components? - Are hooks used in the same patterns as elsewhere? - - - - Are test files in the correct directory structure? - Do test descriptions follow the same format? - Are mocking strategies consistent with other tests? - Is test data generation following established patterns? - - - - Could this utility already exist elsewhere? - Should this be added to an existing utility module? - Does the naming convention match other utilities? - Are similar transformations already implemented? - - - - - - - Search for similar functionality by behavior - - If PR adds a "formatDate" function, search for: - - "date format" - - "format.*date" - - "dateFormat" - - Existing date manipulation utilities - - - - - Search for similar code patterns - - If PR adds error handling, search for: - - try/catch patterns in similar contexts - - Error boundary implementations - - Existing error utilities - - - - - Check what similar files import - - Look at imports in files with similar purposes - to discover existing utilities that could be reused - - - - - - - Reimplementing existing utilities - - - String manipulation functions - - Array transformations - - Date formatting - - API response transformations - - - - - Creating similar components - - - Modal variations that could use a base modal - - Form inputs that could extend existing inputs - - List components with slight variations - - - - - Repeating business logic - - - Validation rules implemented multiple times - - Permission checks duplicated across files - - Data transformation logic repeated - - - - - - - - - - - - - - - - - - Issues that should block PR approval - - - Security vulnerabilities - - Breaking changes without migration path - - Significant pattern violations that would confuse future developers - - Major redundancy that adds maintenance burden - - - - - Important issues that need addressing - - - Test files in wrong location - - Inconsistent error handling - - Missing critical test cases - - Code organization that violates module boundaries - - - - - Improvements that would benefit the codebase - - - Minor pattern inconsistencies - - Opportunities for code reuse - - Additional test coverage - - Documentation improvements - - - - \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml b/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml new file mode 100644 index 0000000000..d665f93754 --- /dev/null +++ b/.roo/rules-pr-reviewer/3_common_mistakes_to_avoid.xml @@ -0,0 +1,43 @@ + + - Not creating a todo list at the start to track the review workflow + - Using MCP tools instead of GitHub CLI commands for GitHub operations + - Forgetting to fetch headRefOid in Step 2 - this is REQUIRED for API review submission + - Starting to review code WITHOUT first fetching existing comments and reviews + - Failing to create a list of existing feedback before reviewing + - Not systematically checking each existing comment against the current code + - Repeating feedback that has already been addressed in the current code + - Ignoring existing PR comments or failing to verify if they have already been resolved + - Running tests or executing code during review + - Making judgmental or harsh comments + - Providing feedback on code outside the PR's scope + - Overlooking unrelated changes not tied to the main issue + - Including ANY praise or positive comments - focus only on issues + - Using markdown headings (###, ##, #) in review comments + - Using excessive markdown formatting when plain text would suffice + - Submitting comments without user preview/approval + - Forgetting to check for an associated issue for additional context + - Missing critical security or performance issues + - Not checking for proper i18n in UI changes + - Failing to suggest breaking up large PRs + - Using internal evaluation terminology in public comments + - Not providing actionable suggestions for improvements + - Reviewing only the diff without local context + - Making assumptions instead of asking clarifying questions about potential intentions + - Forgetting to link to specific lines with full GitHub URLs + - Not presenting findings in a clear numbered list format + - Failing to offer the task creation option for addressing suggestions + - Creating tasks without specific context or file references + - Choosing inappropriate modes when creating tasks for suggestions + - Not updating the todo list after completing each step + - Not including --repo flag when using gh commands for non-default repositories + - Using wrong commit_id in review payload (must use headRefOid from PR info) + - Forgetting to specify "side": "RIGHT" for comments on new code + - Using incorrect line numbers that don't match the actual diff + - Not escaping special characters in JSON payload properly + - Using wrong event type (e.g., REQUEST_CHANGES when only commenting) + - Not constructing proper file paths relative to repository root + - Submitting empty comments array when inline comments are needed + - Forgetting to use < \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/3_delegation_patterns.xml b/.roo/rules-pr-reviewer/3_delegation_patterns.xml deleted file mode 100644 index 9632c7dcf7..0000000000 --- a/.roo/rules-pr-reviewer/3_delegation_patterns.xml +++ /dev/null @@ -1,238 +0,0 @@ - - - Patterns for effectively delegating analysis tasks to specialized modes - while maintaining context and ensuring comprehensive review coverage. - - - - - - When PR contains new features or significant code changes - - code - - Analyze the following changed files for pattern consistency: - [List of changed files] - - Please focus on: - 1. Finding similar existing implementations in the codebase - 2. Identifying established patterns for this type of feature - 3. Checking if the new code follows these patterns - 4. Looking for potential code redundancy - 5. Verifying proper file organization - - Use codebase_search and search_files to find similar code. - Document all findings with specific examples and file references. - - Save your analysis to: .roo/temp/pr-[PR_NUMBER]/pattern-analysis.md - - Format the output as: - ## Pattern Analysis for PR #[PR_NUMBER] - ### Similar Existing Implementations - ### Established Patterns - ### Pattern Deviations - ### Redundancy Findings - ### Organization Issues - - - - - - When PR modifies core modules, adds new modules, or changes dependencies - - architect - - Review the architectural implications of PR #[PR_NUMBER]: - - Changed files: - [List of changed files] - - PR Description: - [PR description] - - Please analyze: - 1. Module boundary adherence - 2. Dependency management (new dependencies, circular dependencies) - 3. Separation of concerns - 4. Impact on system architecture - 5. Consistency with architectural patterns - - Save your findings to: .roo/temp/pr-[PR_NUMBER]/architecture-review.md - - Format as: - ## Architecture Review for PR #[PR_NUMBER] - ### Module Boundaries - ### Dependency Analysis - ### Architectural Concerns - ### Recommendations - - - - - - When PR adds or modifies test files - - test - - Analyze test changes in PR #[PR_NUMBER]: - - Test files changed: - [List of test files] - - Please review: - 1. Test file organization and location - 2. Test naming conventions - 3. Coverage of edge cases - 4. Mock usage patterns - 5. Consistency with existing test patterns - - Compare with similar existing tests in the codebase. - - Save analysis to: .roo/temp/pr-[PR_NUMBER]/test-analysis.md - - Format as: - ## Test Analysis for PR #[PR_NUMBER] - ### Test Organization - ### Coverage Assessment - ### Pattern Consistency - ### Recommendations - - - - - - When PR modifies UI components or adds new ones - - design-engineer - - Review UI changes in PR #[PR_NUMBER]: - - UI files changed: - [List of UI files] - - Please analyze: - 1. Component structure consistency - 2. Styling approach (Tailwind usage) - 3. Accessibility considerations - 4. i18n implementation - 5. Component reusability - - Save findings to: .roo/temp/pr-[PR_NUMBER]/ui-review.md - - - - - - - Always save delegation results to temp files - .roo/temp/pr-[PR_NUMBER]/[analysis-type].md - - - - Request structured markdown output from delegates - - - Easy to parse and combine - - Consistent formatting - - Clear section headers - - - - - Include relevant context in delegation requests - - - PR number and description - - List of changed files - - Specific areas of concern - - Output file location - - - - - - - Delegate tasks one at a time, using results to inform next delegation - - 1. Pattern analysis first - 2. If patterns violated, delegate architecture review - 3. If tests affected, delegate test analysis - - - - - Delegate multiple independent analyses simultaneously - - - Pattern analysis (code mode) - - Test analysis (test mode) - - UI review (design-engineer mode) - - - - - Only delegate based on file types changed - - - If *.test.ts changed -> delegate to test mode - - If src/components/* changed -> delegate to design-engineer - - If package.json changed -> delegate to architect - - - - - - - Read all analysis files from temp directory - - - pattern-analysis.md - - architecture-review.md - - test-analysis.md - - ui-review.md - - - - - Find common issues across analyses - - - Pattern violations mentioned multiple times - - Redundancy identified by different modes - - Organizational issues - - - - - Categorize by severity - - - Critical (blocks PR) - - Important (should fix) - - Suggestions (nice to have) - - - - - Combine all findings into final review - - ## PR Review Summary - ### Critical Issues - ### Pattern Inconsistencies - ### Architecture Concerns - ### Test Coverage - ### Suggestions - - - - - - - Continue with available analyses - Document which analyses couldn't be completed - - - - Perform basic analysis in orchestrator mode - Note limitations in final report - - - - Use completed analyses - Set reasonable time limits for delegations - - - \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/4_github_operations.xml b/.roo/rules-pr-reviewer/4_github_operations.xml deleted file mode 100644 index ad1fdc4459..0000000000 --- a/.roo/rules-pr-reviewer/4_github_operations.xml +++ /dev/null @@ -1,224 +0,0 @@ - - - Guidelines for handling GitHub operations using the GitHub CLI (gh). - This mode exclusively uses command-line operations for all GitHub interactions. - - - - - GitHub CLI must be installed and authenticated - gh auth status - https://cli.github.com/ - - - User must be authenticated with appropriate permissions - gh auth login - - - - - - Fetch comprehensive PR metadata - gh pr view [PR_NUMBER] --repo [owner]/[repo] --json number,title,author,state,body,url,headRefName,baseRefName,files,additions,deletions,changedFiles - JSON - .roo/temp/pr-[PR_NUMBER]/pr-metadata.json - - - - Get the full diff of PR changes - gh pr diff [PR_NUMBER] --repo [owner]/[repo] - .roo/temp/pr-[PR_NUMBER]/pr.diff - - - - List all files changed in the PR - gh pr view [PR_NUMBER] --repo [owner]/[repo] --json files --jq '.files[].path' - Line-separated file paths - - - - Get all comments on the PR - gh pr view [PR_NUMBER] --repo [owner]/[repo] --json comments --jq '.comments' - JSON array of comments - - - - Get all reviews on the PR - gh pr view [PR_NUMBER] --repo [owner]/[repo] --json reviews --jq '.reviews' - JSON array of reviews - - - - Check out PR branch locally for analysis - gh pr checkout [PR_NUMBER] --repo [owner]/[repo] - This switches the current branch to the PR branch - - - - Post a comment on the PR - gh pr comment [PR_NUMBER] --repo [owner]/[repo] --body-file [file_path] - gh pr comment [PR_NUMBER] --repo [owner]/[repo] --body "[comment_text]" - - - - Create a PR review with comments - gh pr review [PR_NUMBER] --repo [owner]/[repo] --comment --body-file [review_file] - - - - - - - - - Get issue details (for linked issues) - gh issue view [issue_number] --repo [owner]/[repo] --json number,title,body,author,state - JSON - - - - - - - Error contains "authentication" or "not logged in" - - - 1. Inform user about auth issue - 2. Suggest running: gh auth login - 3. Check status with: gh auth status - - - - - - Error contains "rate limit" or "API rate limit exceeded" - - - 1. Wait 30-60 seconds before retry - 2. Inform user about rate limiting - 3. Consider reducing API calls - - - - - - Error contains "not found" or "could not find pull request" - - - 1. Verify PR number and repository format - 2. Check if repository is accessible - 3. Ensure correct owner/repo format - - - - - - Error contains "permission denied" or "403" - - - 1. Check repository permissions - 2. Verify authentication scope - 3. May need to re-authenticate with proper scopes - - - - - - - Always save command outputs to temp files - Preserve data for analysis and recovery - - - - Use jq for JSON parsing when available - - gh pr view --json files --jq '.files[].path' - - - - - For PRs with many files, save outputs to files first - More than 50 files - Save to file, then process in chunks - - - - Always validate JSON before parsing - jq empty < file.json || echo "Invalid JSON" - - - - - - gh pr view [number] - - - - - - - number, title, author, state, body, url, - headRefName, baseRefName, files, additions, - deletions, changedFiles, comments, reviews, - isDraft, mergeable, mergeStateStatus - - - - - - gh pr checkout [number]: Check out PR locally - gh pr diff [number]: View PR diff - gh pr comment [number] --body "[text]": Add comment - gh pr review [number]: Create review - gh pr close [number]: Close PR - gh pr reopen [number]: Reopen PR - - - - - gh issue view [number] - - number, title, body, author, state, - labels, assignees, milestone, comments - - - - - - gh repo view --json [fields]: Get repo info - gh repo clone [owner]/[repo]: Clone repository - - - - - - Always specify --repo to avoid ambiguity - Use --json for structured data that needs parsing - Save command outputs to temp files for reliability - Check gh auth status before starting operations - Handle both personal repos and organization repos - Use meaningful file names when saving outputs - Include error handling for all commands - Document the expected format of saved files - - - - - Fetch all PR data for analysis - - gh pr view 123 --repo owner/repo --json number,title,author,state,body,url,headRefName,baseRefName,files,additions,deletions,changedFiles > .roo/temp/pr-123/metadata.json - gh pr view 123 --repo owner/repo --json comments > .roo/temp/pr-123/comments.json - gh pr view 123 --repo owner/repo --json reviews > .roo/temp/pr-123/reviews.json - gh pr diff 123 --repo owner/repo > .roo/temp/pr-123/pr.diff - - - - - Post a comprehensive review - - Create review content in .roo/temp/pr-123/review.md - gh pr review 123 --repo owner/repo --comment --body-file .roo/temp/pr-123/review.md - - - - \ No newline at end of file diff --git a/.roo/rules-pr-reviewer/5_context_management.xml b/.roo/rules-pr-reviewer/5_context_management.xml deleted file mode 100644 index 4b55431c74..0000000000 --- a/.roo/rules-pr-reviewer/5_context_management.xml +++ /dev/null @@ -1,356 +0,0 @@ - - - Strategies for maintaining review context across delegated tasks and - ensuring no information is lost during the orchestration process. - - - - - Central tracking file for the entire review process - .roo/temp/pr-[PR_NUMBER]/review-context.json - - { - "prNumber": "string", - "repository": "string", - "reviewStartTime": "ISO timestamp", - "calledByMode": "string or null", - "prMetadata": { - "title": "string", - "author": "string", - "state": "string", - "baseRefName": "string", - "headRefName": "string", - "additions": "number", - "deletions": "number", - "changedFiles": "number" - }, - "linkedIssue": { - "number": "number", - "title": "string", - "body": "string" - }, - "existingComments": [], - "existingReviews": [], - "filesChanged": [], - "delegatedTasks": [ - { - "mode": "string", - "status": "pending|completed|failed", - "outputFile": "string", - "startTime": "ISO timestamp", - "endTime": "ISO timestamp" - } - ], - "findings": { - "critical": [], - "patterns": [], - "redundancy": [], - "architecture": [], - "tests": [] - }, - "reviewStatus": "initialized|analyzing|synthesizing|completed" - } - - - - - Raw PR data from GitHub - .roo/temp/pr-[PR_NUMBER]/pr-metadata.json - - - - All existing comments and reviews - .roo/temp/pr-[PR_NUMBER]/existing-feedback.json - - - - Output from code mode delegation - .roo/temp/pr-[PR_NUMBER]/pattern-analysis.md - - - - Output from architect mode delegation - .roo/temp/pr-[PR_NUMBER]/architecture-review.md - - - - Output from test mode delegation - .roo/temp/pr-[PR_NUMBER]/test-analysis.md - - - - Synthesized review ready for posting - .roo/temp/pr-[PR_NUMBER]/final-review.md - - - - - - Update review-context.json with PR metadata - -.roo/temp/pr-123/review-context.json - - - - - -.roo/temp/pr-123/review-context.json - -{ - ...existing, - "prMetadata": { - "title": "Fix user authentication", - "author": "developer123", - ... - }, - "filesChanged": ["src/auth.ts", "tests/auth.test.ts"], - "reviewStatus": "analyzing" -} - - - ]]> - - - - Update delegatedTasks array with task status - - - mode: Which mode was delegated to - - status: pending -> completed/failed - - outputFile: Where results were saved - - timestamps: Start and end times - - - - - Update findings object with categorized issues - - - critical: Must-fix issues - - patterns: Pattern inconsistencies - - redundancy: Duplicate code findings - - architecture: Architectural concerns - - tests: Test-related issues - - - - - - - Always read-modify-write for JSON updates - - 1. Read current context file - 2. Parse JSON - 3. Update specific fields - 4. Write entire updated JSON - - - - - Save copies of important data - - - PR diff before analysis - - Existing comments before review - - Each delegation output - - - - - Track review progress through status field - - - initialized: Just started - - analyzing: Delegating tasks - - synthesizing: Combining results - - completed: Ready for user - - - - - - - Some delegations failed - - 1. Mark failed tasks in context - 2. Continue with available data - 3. Note limitations in final review - - - - - JSON file becomes invalid - - 1. Try to recover from backups - 2. Reconstruct from individual files - 3. Start fresh if necessary - - - - - Review process interrupted - - 1. Check reviewStatus field - 2. Resume from last completed step - 3. Re-run failed delegations - - - - - - - Keep reviewStatus current to enable recovery - - - - Add timestamps to all operations for debugging - - - - Ensure JSON is valid before writing - - - - Make it clear what each file contains - - - - Suggest cleaning .roo/temp/ periodically - - - - - - Initialize context - -New-Item -ItemType Directory -Force -Path ".roo/temp/pr-123" - - - -.roo/temp/pr-123/review-context.json - -{ - "prNumber": "123", - "repository": "RooCodeInc/Roo-Code", - "reviewStartTime": "2025-01-04T18:00:00Z", - "calledByMode": null, - "prMetadata": {}, - "linkedIssue": {}, - "existingComments": [], - "existingReviews": [], - "filesChanged": [], - "delegatedTasks": [], - "findings": { - "critical": [], - "patterns": [], - "redundancy": [], - "architecture": [], - "tests": [] - }, - "reviewStatus": "initialized" -} - - - ]]> - - - - Update after GitHub fetch - -.roo/temp/pr-123/review-context.json - - - - - -.roo/temp/pr-123/review-context.json - -{ - ...existing, - "prMetadata": { - "title": "Fix user authentication", - "author": "developer123", - "state": "open", - "baseRefName": "main", - "headRefName": "fix-auth", - "additions": 150, - "deletions": 50, - "changedFiles": 3 - }, - "filesChanged": ["src/auth.ts", "tests/auth.test.ts", "docs/auth.md"], - "reviewStatus": "analyzing" -} - - - ]]> - - - - Track delegation - - -.roo/temp/pr-123/review-context.json - - - - -.roo/temp/pr-123/review-context.json - -{ - ...existing, - "delegatedTasks": [ - ...existing, - { - "mode": "code", - "status": "pending", - "outputFile": "pattern-analysis.md", - "startTime": "2025-01-04T18:05:00Z", - "endTime": null - } - ] -} - - - - - - ]]> - - - - Synthesize results - - -.roo/temp/pr-123/pattern-analysis.md - - - -.roo/temp/pr-123/architecture-review.md - - - -.roo/temp/pr-123/test-analysis.md - - - - -.roo/temp/pr-123/review-context.json - -{ - ...existing, - "findings": { - "critical": ["Missing error handling in auth.ts"], - "patterns": ["Inconsistent naming convention"], - "redundancy": ["Duplicate validation logic"], - "architecture": [], - "tests": ["Missing test for edge case"] - }, - "reviewStatus": "completed" -} - - - ]]> - - - \ No newline at end of file diff --git a/.roomodes b/.roomodes index e9cb7d8a94..d027cec83f 100644 --- a/.roomodes +++ b/.roomodes @@ -1,32 +1,4 @@ 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. - description: Create and implement custom modes. - groups: - - read - - - edit - - fileRegex: (\.roomodes$|\.roo/.*\.xml$|\.yaml$) - description: Mode configuration files and XML instructions - - command - - mcp - source: project - slug: test name: 🧪 Test roleDefinition: |- @@ -69,42 +41,6 @@ customModes: - mcp customInstructions: Focus on UI refinement, component creation, and adherence to design best-practices. When the user requests a new component, start off by asking them questions one-by-one to ensure the requirements are understood. Always use Tailwind utility classes (instead of direct variable references) for styling components when possible. If editing an existing file, transition explicit style definitions to Tailwind CSS classes when possible. Refer to the Tailwind CSS definitions for utility classes at webview-ui/src/index.css. Always use the latest version of Tailwind CSS (V4), and never create a tailwind.config.js file. Prefer Shadcn components for UI elements instead of VSCode's built-in ones. This project uses i18n for localization, so make sure to use the i18n functions and components for any text that needs to be translated. Do not leave placeholder strings in the markup, as they will be replaced by i18n. Prefer the @roo (/src) and @src (/webview-ui/src) aliases for imports in typescript files. Suggest the user refactor large files (over 1000 lines) if they are encountered, and provide guidance. Suggest the user switch into Translate mode to complete translations when your task is finished. source: project - - 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. - whenToUse: Automate the release process for software projects. - description: Automate the 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,closingIssuesReferences --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt, issues: .closingIssuesReferences}] | sort_by(.number)'` - 3. For each PR with linked issues, fetch the issue details to get the issue reporter: `gh issue view ISSUE_NUMBER --json number,author -q '{number, reporter: .author.login}'` - 4. Summarize the changes and ask the user whether this should be a major, minor, or patch release - 5. 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!) - For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)" - For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)" - Provide brief descriptions of each item to explain the change - Order the list from most important to least important - Example formats: - - With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)" - - Without issue: "- 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. - 6. 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) - 7. Ask the user to confirm the English version - 8. 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 - 9. Create a new branch for the release preparation: `git checkout -b release/v[version]` - 10. Commit and push the changeset file and any documentation updates to the repository: `git add . && git commit -m "chore: add changeset for v[version]" && git push origin release/v[version]` 11. Create a pull request for the release: `gh pr create --title "Release v[version]" --body "Release preparation for v[version]. This PR includes the changeset and any necessary documentation updates." --base main --head release/v[version]` 12. 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 - groups: - - read - - edit - - command - - browser - source: project - slug: translate name: 🌐 Translate roleDefinition: You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources. @@ -137,18 +73,6 @@ customModes: - edit - command 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 - 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. - description: Create well-structured GitHub issues. - groups: - - read - - command - - mcp - source: project - slug: integration-tester name: 🧪 Integration Tester roleDefinition: |- @@ -164,40 +88,20 @@ customModes: - fileRegex: (apps/vscode-e2e/.*\.(ts|js)$|packages/types/.*\.ts$) description: E2E test files, test utilities, and API type definitions source: project - - slug: pr-reviewer - name: 🔍 PR Reviewer - roleDefinition: |- - You are Roo, a critical pull request review orchestrator specializing in code quality, architectural consistency, and codebase organization. Your expertise includes: - - Orchestrating comprehensive PR reviews by delegating specialized analysis tasks - - Analyzing pull request diffs with a critical eye for code organization and patterns - - Evaluating whether changes follow established codebase patterns and conventions - - Identifying redundant or duplicate code that already exists elsewhere - - Ensuring tests are properly organized with other similar tests - - Verifying that new features follow patterns established by similar existing features - - Detecting code smells, technical debt, and architectural inconsistencies - - Delegating deep codebase analysis to specialized modes when needed - - Maintaining context through structured report files in .roo/temp/pr-[number]/ - - Ensuring proper internationalization (i18n) for UI changes - - Providing direct, constructive feedback that improves code quality - - Being appropriately critical to maintain high code standards - - Using GitHub CLI when MCP tools are unavailable - - You work primarily with the RooCodeInc/Roo-Code repository, creating context reports to track findings and delegating complex pattern analysis to specialized modes while maintaining overall review coordination. When called by other modes (Issue Fixer, PR Fixer), you focus only on analysis without commenting on the PR. - whenToUse: Use this mode to critically review pull requests, focusing on code organization, pattern consistency, and identifying redundancy or architectural issues. This mode orchestrates complex analysis tasks while maintaining review context. - description: Critically review pull requests. - groups: - - read - - - edit - - fileRegex: (\.md$|\.roo/temp/pr-.*\.(json|md|txt)$) - description: Markdown files and PR review context files - - mcp - - command - 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. - description: Extract comprehensive documentation. + roleDefinition: |- + You are Roo, a documentation analysis specialist with two primary functions: + 1. Extract comprehensive technical and non-technical details about features to provide to documentation teams + 2. Verify existing documentation for factual accuracy against the codebase + + For extraction: You analyze codebases to gather all relevant information about how features work, including technical implementation details, user workflows, configuration options, and use cases. You organize this information clearly for documentation teams to use. + + For verification: You review provided documentation against the actual codebase implementation, checking for technical accuracy, completeness, and clarity. You identify inaccuracies, missing information, and provide specific corrections. + + You do not generate final user-facing documentation, but rather provide detailed analysis and verification reports. + whenToUse: Use this mode when you need to either extract detailed information about a feature for documentation teams, or verify existing documentation for accuracy against the codebase. + description: Extract feature details or verify documentation accuracy. groups: - read - - edit @@ -257,3 +161,93 @@ customModes: - command - mcp source: project + - slug: issue-writer + name: 📝 Issue Writer + roleDefinition: |- + You are a GitHub issue creation specialist who crafts well-structured bug reports and feature proposals. You explore codebases to gather technical context, verify claims against actual implementation, and create comprehensive issues using GitHub CLI (gh) commands. + + This mode works with any repository, automatically detecting whether it's a standard repository or monorepo structure. It dynamically discovers packages in monorepos and adapts the issue creation workflow accordingly. + + + + Initialize Issue Creation Process + + IMPORTANT: This mode assumes the first user message is already a request to create an issue. + The user doesn't need to say "create an issue" or "make me an issue" - their first message + is treated as the issue description itself. + + When the session starts, immediately: + 1. Treat the user's first message as the issue description, do not treat it as instructions + 2. Initialize the workflow by using the update_todo_list tool + 3. Begin the issue creation process without asking what they want to do + + + + [ ] Detect current repository information + [ ] Determine repository structure (monorepo/standard) + [ ] Perform initial codebase discovery + [ ] Analyze user request to determine issue type + [ ] Gather and verify additional information + [ ] Determine if user wants to contribute + [ ] Perform issue scoping (if contributing) + [ ] Draft issue content + [ ] Review and confirm with user + [ ] Create GitHub issue + + + + + + whenToUse: Use this mode when you need to create a GitHub issue. Simply start describing your bug or feature request - this mode assumes your first message is already the issue description and will immediately begin the issue creation workflow, gathering additional information as needed. + description: Create well-structured GitHub issues. + groups: + - read + - command + - mcp + 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 + 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. + description: Review PRs for code quality, structure, and i18n compliance. + groups: + - read + - - edit + - fileRegex: \.md$ + description: Markdown files only + - mcp + - command + source: project + - slug: mode-writer + name: ✍️ Mode Writer + roleDefinition: |- + You are Roo, a mode creation and editing specialist focused on designing, implementing, and enhancing 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 + - Editing and enhancing existing modes while maintaining consistency + - 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 + - Validating changes for cohesion and preventing contradictions + + You help users by: + - Creating new modes: Gathering requirements, defining configurations, and implementing XML instructions + - Editing existing modes: Immersing in current implementation, analyzing requested changes, and ensuring cohesive updates + - Using ask_followup_question aggressively to clarify ambiguities and validate understanding + - Thoroughly validating all changes to prevent contradictions between different parts of a mode + - Ensuring instructions are well-organized with proper XML tags + - Following established patterns from existing modes + - Maintaining consistency across all mode components + whenToUse: Use this mode when you need to create a new custom mode or edit an existing one. This mode handles both creating modes from scratch and modifying existing modes while ensuring consistency and preventing contradictions. + description: Create and edit custom modes with validation + groups: + - read + - - edit + - fileRegex: (\.roomodes$|\.roo/.*\.xml$|\.yaml$) + description: Mode configuration files and XML instructions + - command + - mcp + source: project diff --git a/CHANGELOG.md b/CHANGELOG.md index e34a65dbee..2b7d1e984c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Roo Code Changelog +## [3.25.2] - 2025-07-29 + +- Fix: Show diff view before approval when background edits are disabled (thanks @daniel-lxs!) +- Add support for organization-level MCP controls +- Fix zap icon hover state + +## [3.25.1] - 2025-07-29 + +- Add support for GLM-4.5-Air model to Chutes AI provider (#6376 by @matbgn, PR by @app/roomote) +- Improve subshell validation for commands + +## [3.25.0] - 2025-07-29 + +- Add message queueing (thanks @app/roomote!) +- Add custom slash commands +- Add options for URL Context and Grounding with Google Search to the Gemini provider (thanks @HahaBill!) +- Add image support to read_file tool (thanks @samhvw8!) +- Add experimental setting to prevent editor focus disruption (#4784 by @hannesrudolph, PR by @app/roomote) +- Add prompt caching support for LiteLLM (#5791 by @steve-gore-snapdocs, PR by @MuriloFP) +- Add markdown table rendering support +- Fix list_files recursive mode now works for dot directories (#2992 by @avtc, #4807 by @zhang157686, #5409 by @MuriloFP, PR by @MuriloFP) +- Add search functionality to mode selector popup and reorganize layout +- Sync API config selector style with mode selector +- Fix keyboard shortcuts for non-QWERTY layouts (#6161 by @shlgug, PR by @app/roomote) +- Add ESC key handling for modes, API provider, and indexing settings popovers (thanks @app/roomote!) +- Make task mode sticky to task (thanks @app/roomote!) +- Add text wrapping to command patterns in Manage Command Permissions (thanks @app/roomote!) +- Update list-files test for fixed hidden files bug (thanks @daniel-lxs!) +- Fix normalize Windows paths to forward slashes in mode export (#6307 by @hannesrudolph, PR by @app/roomote) +- Ensure form-data >= 4.0.4 +- Fix filter out non-text tab inputs (Kilo-Org/kilocode#712 by @szermatt, PR by @hassoncs) + +## [3.24.0] - 2025-07-25 + +- Add Hugging Face provider with support for open source models (thanks @TGlide!) +- Add terminal command permissions UI to chat interface +- Add support for Agent Rules standard via AGENTS.md (thanks @sgryphon!) +- Add settings to control diagnostic messages +- Fix auto-approve checkbox to be toggled at any time (thanks @KJ7LNW!) +- Add efficiency warning for single SEARCH/REPLACE blocks in apply_diff (thanks @KJ7LNW!) +- Fix respect maxReadFileLine setting for file mentions to prevent context exhaustion (thanks @sebinseban!) +- Fix Ollama API URL normalization by removing trailing slashes (thanks @Naam!) +- Fix restore list styles for markdown lists in chat interface (thanks @village-way!) +- Add support for bedrock api keys +- Add confirmation dialog and proper cleanup for marketplace mode removal +- Fix cancel auto-approve timer when editing follow-up suggestion (thanks @hassoncs!) +- Fix add error message when no workspace folder is open for code indexing + ## [3.23.19] - 2025-07-23 - Add Roo Code Cloud Waitlist CTAs (thanks @brunobergher!) diff --git a/README.md b/README.md index c173cdb3d7..332a26dbba 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,13 @@ Check out the [CHANGELOG](CHANGELOG.md) for detailed updates and fixes. --- -## 🎉 Roo Code 3.23 Released +## 🎉 Roo Code 3.25 Released -Roo Code 3.23 brings powerful new features and significant improvements to enhance your development workflow! +Roo Code 3.25 brings powerful new features and significant improvements to enhance your development workflow! -- **Codebase Indexing Graduated from Experimental** - Full codebase indexing is now stable and ready for production use with improved search and context understanding. -- **New Todo List Feature** - Keep your tasks on track with integrated todo management that helps you stay organized and focused on your development goals. +- **Message Queueing** - Queue multiple messages while Roo is working, allowing you to continue planning your workflow without interruption. +- **Custom Slash Commands** - Create personalized slash commands for quick access to frequently used prompts and workflows, with full UI management. +- **Enhanced Gemini Tools** - New URL context and Google Search grounding capabilities provide Gemini models with real-time web information and enhanced research abilities. --- @@ -207,44 +208,45 @@ Thanks to all our contributors who have helped make Roo Code better! -| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| daniel-lxs
daniel-lxs
| samhvw8
samhvw8
| hannesrudolph
hannesrudolph
| -| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| ColemanRoo
ColemanRoo
| MuriloFP
MuriloFP
| canrobins13
canrobins13
| stea9499
stea9499
| -| joemanley201
joemanley201
| System233
System233
| jr
jr
| nissa-seru
nissa-seru
| jquanton
jquanton
| roomote-agent
roomote-agent
| -| NyxJae
NyxJae
| d-oit
d-oit
| elianiva
elianiva
| wkordalski
wkordalski
| qdaxb
qdaxb
| punkpeye
punkpeye
| -| SannidhyaSah
SannidhyaSah
| xyOz-dev
xyOz-dev
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| monotykamary
monotykamary
| cannuri
cannuri
| -| feifei325
feifei325
| zhangtony239
zhangtony239
| chrarnoldus
chrarnoldus
| shariqriazz
shariqriazz
| vigneshsubbiah16
vigneshsubbiah16
| pugazhendhi-m
pugazhendhi-m
| -| lloydchang
lloydchang
| dtrugman
dtrugman
| Szpadel
Szpadel
| lupuletic
lupuletic
| kiwina
kiwina
| liwilliam2021
liwilliam2021
| -| Premshay
Premshay
| psv2522
psv2522
| olweraltuve
olweraltuve
| diarmidmackenzie
diarmidmackenzie
| ChuKhaLi
ChuKhaLi
| PeterDaveHello
PeterDaveHello
| -| aheizi
aheizi
| hassoncs
hassoncs
| nbihan-mediware
nbihan-mediware
| noritaka1166
noritaka1166
| RaySinner
RaySinner
| afshawnlotfi
afshawnlotfi
| -| dleffel
dleffel
| StevenTCramer
StevenTCramer
| Ruakij
Ruakij
| pdecat
pdecat
| kyle-apex
kyle-apex
| emshvac
emshvac
| -| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dlab-anton
dlab-anton
| arthurauffray
arthurauffray
| -| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| sammcj
sammcj
| p12tic
p12tic
| gtaylor
gtaylor
| -| aitoroses
aitoroses
| anton-otee
anton-otee
| ross
ross
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| taisukeoe
taisukeoe
| -| avtc
avtc
| eonghk
eonghk
| GOODBOY008
GOODBOY008
| kcwhite
kcwhite
| ronyblum
ronyblum
| teddyOOXX
teddyOOXX
| -| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| yt3trees
yt3trees
| -| seedlord
seedlord
| bramburn
bramburn
| benzntech
benzntech
| axkirillov
axkirillov
| olearycrew
olearycrew
| brunobergher
brunobergher
| -| catrielmuller
catrielmuller
| devxpain
devxpain
| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| -| julionav
julionav
| KanTakahiro
KanTakahiro
| SplittyDev
SplittyDev
| mdp
mdp
| napter
napter
| philfung
philfung
| -| chris-garrett
chris-garrett
| dairui1
dairui1
| dqroid
dqroid
| janaki-sasidhar
janaki-sasidhar
| forestyoo
forestyoo
| hatsu38
hatsu38
| -| hongzio
hongzio
| im47cn
im47cn
| shoopapa
shoopapa
| jwcraig
jwcraig
| kinandan
kinandan
| nevermorec
nevermorec
| -| bbenshalom
bbenshalom
| bannzai
bannzai
| axmo
axmo
| asychin
asychin
| amittell
amittell
| Yoshino-Yukitaro
Yoshino-Yukitaro
| -| Yikai-Liao
Yikai-Liao
| zxdvd
zxdvd
| s97712
s97712
| vladstudio
vladstudio
| vivekfyi
vivekfyi
| tmsjngx0
tmsjngx0
| -| Githubguy132010
Githubguy132010
| DeXtroTip
DeXtroTip
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| user202729
user202729
| -| thill2323
thill2323
| takakoutso
takakoutso
| student20880
student20880
| shubhamgupta731
shubhamgupta731
| shohei-ihaya
shohei-ihaya
| shivamd1810
shivamd1810
| -| shaybc
shaybc
| sensei-woo
sensei-woo
| samir-nimbly
samir-nimbly
| robertheadley
robertheadley
| refactorthis
refactorthis
| qingyuan1109
qingyuan1109
| -| pokutuna
pokutuna
| philipnext
philipnext
| village-way
village-way
| oprstchn
oprstchn
| nobu007
nobu007
| mosleyit
mosleyit
| -| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| lightrabbit
lightrabbit
| lhish
lhish
| -| kohii
kohii
| pfitz
pfitz
| ExactDoug
ExactDoug
| celestial-vault
celestial-vault
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| -| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| Deon588
Deon588
| dleen
dleen
| CW-B-W
CW-B-W
| -| chadgauth
chadgauth
| thecolorblue
thecolorblue
| bogdan0083
bogdan0083
| benashby
benashby
| Atlogit
Atlogit
| atlasgong
atlasgong
| -| andrewshu2000
andrewshu2000
| andreastempsch
andreastempsch
| alasano
alasano
| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| alarno
alarno
| -| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| adamhill
adamhill
| AMHesch
AMHesch
| tgfjt
tgfjt
| -| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| RSO
RSO
| RandalSchwartz
RandalSchwartz
| -| SECKainersdorfer
SECKainersdorfer
| R-omk
R-omk
| Sarke
Sarke
| PaperBoardOfficial
PaperBoardOfficial
| OlegOAndreev
OlegOAndreev
| kvokka
kvokka
| -| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| markijbema
markijbema
| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| -| libertyteeth
libertyteeth
| shtse8
shtse8
| Rexarrior
Rexarrior
| kevinvandijk
kevinvandijk
| KevinZhao
KevinZhao
| ksze
ksze
| -| Juice10
Juice10
| Fovty
Fovty
| Jdo300
Jdo300
| hesara
hesara
| | | +| mrubens
mrubens
| saoudrizwan
saoudrizwan
| cte
cte
| daniel-lxs
daniel-lxs
| samhvw8
samhvw8
| hannesrudolph
hannesrudolph
| +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| KJ7LNW
KJ7LNW
| a8trejo
a8trejo
| MuriloFP
MuriloFP
| ColemanRoo
ColemanRoo
| canrobins13
canrobins13
| stea9499
stea9499
| +| joemanley201
joemanley201
| System233
System233
| jr
jr
| nissa-seru
nissa-seru
| jquanton
jquanton
| roomote-agent
roomote-agent
| +| NyxJae
NyxJae
| d-oit
d-oit
| elianiva
elianiva
| qdaxb
qdaxb
| punkpeye
punkpeye
| wkordalski
wkordalski
| +| SannidhyaSah
SannidhyaSah
| xyOz-dev
xyOz-dev
| chrarnoldus
chrarnoldus
| sachasayan
sachasayan
| Smartsheet-JB-Brown
Smartsheet-JB-Brown
| monotykamary
monotykamary
| +| cannuri
cannuri
| feifei325
feifei325
| zhangtony239
zhangtony239
| shariqriazz
shariqriazz
| vigneshsubbiah16
vigneshsubbiah16
| pugazhendhi-m
pugazhendhi-m
| +| lloydchang
lloydchang
| dtrugman
dtrugman
| Szpadel
Szpadel
| lupuletic
lupuletic
| kiwina
kiwina
| liwilliam2021
liwilliam2021
| +| Premshay
Premshay
| psv2522
psv2522
| olweraltuve
olweraltuve
| diarmidmackenzie
diarmidmackenzie
| hassoncs
hassoncs
| ChuKhaLi
ChuKhaLi
| +| PeterDaveHello
PeterDaveHello
| aheizi
aheizi
| nbihan-mediware
nbihan-mediware
| noritaka1166
noritaka1166
| RaySinner
RaySinner
| afshawnlotfi
afshawnlotfi
| +| dleffel
dleffel
| StevenTCramer
StevenTCramer
| Ruakij
Ruakij
| pdecat
pdecat
| kyle-apex
kyle-apex
| emshvac
emshvac
| +| Lunchb0ne
Lunchb0ne
| SmartManoj
SmartManoj
| vagadiya
vagadiya
| slytechnical
slytechnical
| dlab-anton
dlab-anton
| arthurauffray
arthurauffray
| +| upamune
upamune
| NamesMT
NamesMT
| taylorwilsdon
taylorwilsdon
| sammcj
sammcj
| p12tic
p12tic
| gtaylor
gtaylor
| +| brunobergher
brunobergher
| aitoroses
aitoroses
| ross
ross
| mr-ryan-james
mr-ryan-james
| heyseth
heyseth
| taisukeoe
taisukeoe
| +| avtc
avtc
| eonghk
eonghk
| GOODBOY008
GOODBOY008
| kcwhite
kcwhite
| ronyblum
ronyblum
| teddyOOXX
teddyOOXX
| +| vincentsong
vincentsong
| yongjer
yongjer
| zeozeozeo
zeozeozeo
| ashktn
ashktn
| franekp
franekp
| yt3trees
yt3trees
| +| seedlord
seedlord
| bramburn
bramburn
| anton-otee
anton-otee
| benzntech
benzntech
| axkirillov
axkirillov
| olearycrew
olearycrew
| +| catrielmuller
catrielmuller
| devxpain
devxpain
| snoyiatk
snoyiatk
| GitlyHallows
GitlyHallows
| jcbdev
jcbdev
| Chenjiayuan195
Chenjiayuan195
| +| julionav
julionav
| KanTakahiro
KanTakahiro
| SplittyDev
SplittyDev
| mdp
mdp
| napter
napter
| philfung
philfung
| +| bbenshalom
bbenshalom
| chris-garrett
chris-garrett
| dairui1
dairui1
| dqroid
dqroid
| janaki-sasidhar
janaki-sasidhar
| forestyoo
forestyoo
| +| hatsu38
hatsu38
| hongzio
hongzio
| im47cn
im47cn
| shoopapa
shoopapa
| jwcraig
jwcraig
| kinandan
kinandan
| +| bannzai
bannzai
| axmo
axmo
| asychin
asychin
| amittell
amittell
| Yoshino-Yukitaro
Yoshino-Yukitaro
| Yikai-Liao
Yikai-Liao
| +| zxdvd
zxdvd
| s97712
s97712
| vladstudio
vladstudio
| vivekfyi
vivekfyi
| HahaBill
HahaBill
| tmsjngx0
tmsjngx0
| +| TGlide
TGlide
| Githubguy132010
Githubguy132010
| nevermorec
nevermorec
| PretzelVector
PretzelVector
| zetaloop
zetaloop
| cdlliuy
cdlliuy
| +| user202729
user202729
| thill2323
thill2323
| takakoutso
takakoutso
| student20880
student20880
| shubhamgupta731
shubhamgupta731
| shohei-ihaya
shohei-ihaya
| +| shivamd1810
shivamd1810
| shaybc
shaybc
| sensei-woo
sensei-woo
| samir-nimbly
samir-nimbly
| robertheadley
robertheadley
| refactorthis
refactorthis
| +| qingyuan1109
qingyuan1109
| pokutuna
pokutuna
| philipnext
philipnext
| village-way
village-way
| oprstchn
oprstchn
| nobu007
nobu007
| +| mosleyit
mosleyit
| moqimoqidea
moqimoqidea
| mlopezr
mlopezr
| mecab
mecab
| olup
olup
| lightrabbit
lightrabbit
| +| lhish
lhish
| kohii
kohii
| tgfjt
tgfjt
| DeXtroTip
DeXtroTip
| pfitz
pfitz
| ExactDoug
ExactDoug
| +| celestial-vault
celestial-vault
| linegel
linegel
| edwin-truthsearch-io
edwin-truthsearch-io
| EamonNerbonne
EamonNerbonne
| dbasclpy
dbasclpy
| dflatline
dflatline
| +| Deon588
Deon588
| dleen
dleen
| CW-B-W
CW-B-W
| chadgauth
chadgauth
| thecolorblue
thecolorblue
| bogdan0083
bogdan0083
| +| benashby
benashby
| Atlogit
Atlogit
| atlasgong
atlasgong
| andrewshu2000
andrewshu2000
| andreastempsch
andreastempsch
| alasano
alasano
| +| QuinsZouls
QuinsZouls
| HadesArchitect
HadesArchitect
| alarno
alarno
| nexon33
nexon33
| adilhafeez
adilhafeez
| adamwlarson
adamwlarson
| +| adamhill
adamhill
| AMHesch
AMHesch
| maekawataiki
maekawataiki
| AlexandruSmirnov
AlexandruSmirnov
| samsilveira
samsilveira
| 01Rian
01Rian
| +| RSO
RSO
| RandalSchwartz
RandalSchwartz
| SECKainersdorfer
SECKainersdorfer
| R-omk
R-omk
| Sarke
Sarke
| PaperBoardOfficial
PaperBoardOfficial
| +| OlegOAndreev
OlegOAndreev
| Naam
Naam
| kvokka
kvokka
| ecmasx
ecmasx
| mollux
mollux
| marvijo-code
marvijo-code
| +| markijbema
markijbema
| mamertofabian
mamertofabian
| monkeyDluffy6017
monkeyDluffy6017
| libertyteeth
libertyteeth
| shtse8
shtse8
| Rexarrior
Rexarrior
| +| kevinvandijk
kevinvandijk
| KevinZhao
KevinZhao
| ksze
ksze
| Juice10
Juice10
| Fovty
Fovty
| Jdo300
Jdo300
| +| hesara
hesara
| | | | | | diff --git a/README.vscode.md b/README.vscode.md new file mode 100644 index 0000000000..2afed2a9c6 --- /dev/null +++ b/README.vscode.md @@ -0,0 +1 @@ +readme test diff --git a/apps/vscode-e2e/src/suite/markdown-lists.test.ts b/apps/vscode-e2e/src/suite/markdown-lists.test.ts new file mode 100644 index 0000000000..a229d9c270 --- /dev/null +++ b/apps/vscode-e2e/src/suite/markdown-lists.test.ts @@ -0,0 +1,168 @@ +import * as assert from "assert" + +import type { ClineMessage } from "@roo-code/types" + +import { waitUntilCompleted } from "./utils" +import { setDefaultSuiteTimeout } from "./test-utils" + +suite("Markdown List Rendering", function () { + setDefaultSuiteTimeout(this) + + test("Should render unordered lists with bullets in chat", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please show me an example of an unordered list with the following items: Apple, Banana, Orange", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find the message containing the list + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text?.includes("Apple") && + text?.includes("Banana") && + text?.includes("Orange"), + ) + + assert.ok(listMessage, "Should have a message containing the list items") + + // The rendered markdown should contain list markers + const messageText = listMessage?.text || "" + assert.ok( + messageText.includes("- Apple") || messageText.includes("* Apple") || messageText.includes("• Apple"), + "List items should be rendered with bullet points", + ) + }) + + test("Should render ordered lists with numbers in chat", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please show me a numbered list with three steps: First step, Second step, Third step", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find the message containing the numbered list + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text?.includes("First step") && + text?.includes("Second step") && + text?.includes("Third step"), + ) + + assert.ok(listMessage, "Should have a message containing the numbered list") + + // The rendered markdown should contain numbered markers + const messageText = listMessage?.text || "" + assert.ok( + messageText.includes("1. First step") || messageText.includes("1) First step"), + "List items should be rendered with numbers", + ) + }) + + test("Should render nested lists with proper hierarchy", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please create a nested list with 'Main item' having two sub-items: 'Sub-item A' and 'Sub-item B'", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find the message containing the nested list + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text?.includes("Main item") && + text?.includes("Sub-item A") && + text?.includes("Sub-item B"), + ) + + assert.ok(listMessage, "Should have a message containing the nested list") + + // The rendered markdown should show hierarchy through indentation + const messageText = listMessage?.text || "" + + // Check for main item + assert.ok( + messageText.includes("- Main item") || + messageText.includes("* Main item") || + messageText.includes("• Main item"), + "Main list item should be rendered", + ) + + // Check for sub-items with indentation (typically 2-4 spaces or a tab) + assert.ok( + messageText.match(/\s{2,}- Sub-item A/) || + messageText.match(/\s{2,}\* Sub-item A/) || + messageText.match(/\s{2,}• Sub-item A/) || + messageText.includes("\t- Sub-item A") || + messageText.includes("\t* Sub-item A") || + messageText.includes("\t• Sub-item A"), + "Sub-items should be indented", + ) + }) + + test("Should render mixed ordered and unordered lists", async () => { + const api = globalThis.api + + const messages: ClineMessage[] = [] + + api.on("message", ({ message }: { message: ClineMessage }) => { + if (message.type === "say" && message.partial === false) { + messages.push(message) + } + }) + + const taskId = await api.startNewTask({ + configuration: { mode: "ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + text: "Please create a list that has both numbered items and bullet points, mixing ordered and unordered lists", + }) + + await waitUntilCompleted({ api, taskId }) + + // Find a message that contains both types of lists + const listMessage = messages.find( + ({ say, text }) => + (say === "completion_result" || say === "text") && + text && + // Check for numbered list markers + (text.includes("1.") || text.includes("1)")) && + // Check for bullet list markers + (text.includes("-") || text.includes("*") || text.includes("•")), + ) + + assert.ok(listMessage, "Should have a message containing mixed list types") + }) +}) diff --git a/apps/vscode-e2e/src/suite/tools/list-files.test.ts b/apps/vscode-e2e/src/suite/tools/list-files.test.ts index 5340a13a16..c374e79515 100644 --- a/apps/vscode-e2e/src/suite/tools/list-files.test.ts +++ b/apps/vscode-e2e/src/suite/tools/list-files.test.ts @@ -242,8 +242,8 @@ This directory contains various files and subdirectories for testing the list_fi // Verify the tool returned the expected files (non-recursive) assert.ok(listResults, "Tool execution results should be captured") - // Check that expected root-level files are present (excluding hidden files due to current bug) - const expectedFiles = ["root-file-1.txt", "root-file-2.js", "config.yaml", "README.md"] + // Check that expected root-level files are present (including hidden files now that bug is fixed) + const expectedFiles = ["root-file-1.txt", "root-file-2.js", "config.yaml", "README.md", ".hidden-file"] const expectedDirs = ["nested/"] const results = listResults as string @@ -255,13 +255,9 @@ This directory contains various files and subdirectories for testing the list_fi assert.ok(results.includes(dir), `Tool results should include directory ${dir}`) } - // BUG: Hidden files are currently excluded in non-recursive mode - // This should be fixed - hidden files should be included when using --hidden flag - console.log("BUG DETECTED: Hidden files are excluded in non-recursive mode") - assert.ok( - !results.includes(".hidden-file"), - "KNOWN BUG: Hidden files are currently excluded in non-recursive mode", - ) + // Verify hidden files are now included (bug has been fixed) + console.log("Verifying hidden files are included in non-recursive mode") + assert.ok(results.includes(".hidden-file"), "Hidden files should be included in non-recursive mode") // Verify nested files are NOT included (non-recursive) const nestedFiles = ["nested-file-1.md", "nested-file-2.json", "deep-nested-file.ts"] diff --git a/apps/vscode-nightly/esbuild.mjs b/apps/vscode-nightly/esbuild.mjs index e45dbd3c3e..09fa948cd8 100644 --- a/apps/vscode-nightly/esbuild.mjs +++ b/apps/vscode-nightly/esbuild.mjs @@ -63,7 +63,7 @@ async function main() { build.onEnd(() => { copyPaths( [ - ["../README.md", "README.md"], + ["../README.vscode.md", "README.md"], ["../CHANGELOG.md", "CHANGELOG.md"], ["../LICENSE", "LICENSE"], ["../.env", ".env", { optional: true }], diff --git a/locales/ca/README.md b/locales/ca/README.md index c75275881c..05bfc26d07 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -50,12 +50,13 @@ Consulteu el [CHANGELOG](../../CHANGELOG.md) per a actualitzacions i correccions --- -## 🎉 Roo Code 3.23 Llançat +## 🎉 Roo Code 3.25 Llançat -Roo Code 3.23 aporta noves funcionalitats potents i millores significatives per millorar el vostre flux de treball de desenvolupament! +Roo Code 3.25 aporta noves funcionalitats potents i millores significatives per millorar el vostre flux de treball de desenvolupament! -- **Indexació de base de codi graduada d'experimental** - La indexació completa de la base de codi ara és estable i llesta per a ús en producció amb cerca millorada i comprensió del context. -- **Nova funcionalitat de llista de tasques** - Mantingueu les vostres tasques en el bon camí amb gestió integrada de tasques que us ajuda a mantenir-vos organitzats i centrats en els vostres objectius de desenvolupament. +- **Cua de missatges** - Poseu diversos missatges a la cua mentre Roo treballa, permetent-vos continuar planificant el vostre flux de treball sense interrupcions. +- **Comandes slash personalitzades** - Creeu comandes slash personalitzades per a accés ràpid a prompts i fluxos de treball utilitzats freqüentment amb gestió completa de la interfície d'usuari. +- **Eines Gemini avançades** - Noves funcionalitats de context d'URL i fonaments de cerca de Google proporcionen als models Gemini informació web en temps real i capacitats de recerca avançades. --- @@ -182,42 +183,43 @@ Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Llicència diff --git a/locales/de/README.md b/locales/de/README.md index e9357daa47..08dd18e2d7 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -50,12 +50,13 @@ Sehen Sie sich das [CHANGELOG](../../CHANGELOG.md) für detaillierte Updates und --- -## 🎉 Roo Code 3.23 veröffentlicht +## 🎉 Roo Code 3.25 veröffentlicht -Roo Code 3.23 bringt mächtige neue Funktionen und bedeutende Verbesserungen, um deinen Entwicklungsworkflow zu verbessern! +Roo Code 3.25 bringt mächtige neue Funktionen und bedeutende Verbesserungen, um deinen Entwicklungsworkflow zu verbessern! -- **Codebase-Indexierung von experimentell graduiert** - Die vollständige Codebase-Indexierung ist jetzt stabil und bereit für den Produktionseinsatz mit verbesserter Suche und Kontextverständnis. -- **Neue Todo-Listen-Funktion** - Halte deine Aufgaben auf Kurs mit integriertem Aufgabenmanagement, das dir hilft, organisiert zu bleiben und dich auf deine Entwicklungsziele zu konzentrieren. +- **Nachrichten-Warteschlange** - Stelle mehrere Nachrichten in die Warteschlange, während Roo arbeitet, damit du deinen Workflow ohne Unterbrechung weiter planen kannst. +- **Benutzerdefinierte Slash-Befehle** - Erstelle personalisierte Slash-Befehle für schnellen Zugriff auf häufig verwendete Prompts und Workflows mit vollständiger UI-Verwaltung. +- **Erweiterte Gemini-Tools** - Neue URL-Kontext- und Google-Such-Grundlagen-Funktionen bieten Gemini-Modellen Echtzeit-Web-Informationen und erweiterte Recherche-Fähigkeiten. --- @@ -182,42 +183,43 @@ Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code zu verbessern! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Lizenz diff --git a/locales/es/README.md b/locales/es/README.md index 09eb159479..ae2c073387 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -50,12 +50,13 @@ Consulta el [CHANGELOG](../../CHANGELOG.md) para ver actualizaciones detalladas --- -## 🎉 Roo Code 3.23 Lanzado +## 🎉 Roo Code 3.25 Lanzado -¡Roo Code 3.23 trae nuevas funcionalidades poderosas y mejoras significativas para mejorar tu flujo de trabajo de desarrollo! +¡Roo Code 3.25 trae nuevas funcionalidades poderosas y mejoras significativas para mejorar tu flujo de trabajo de desarrollo! -- **Indexación de base de código graduada de experimental** - La indexación completa de la base de código ahora es estable y está lista para uso en producción con búsqueda mejorada y comprensión del contexto. -- **Nueva funcionalidad de lista de tareas** - Mantén tus tareas en el buen camino con gestión integrada de tareas que te ayuda a mantenerte organizado y enfocado en tus objetivos de desarrollo. +- **Cola de mensajes** - Pon varios mensajes en cola mientras Roo trabaja, permitiéndote continuar planificando tu flujo de trabajo sin interrupciones. +- **Comandos slash personalizados** - Crea comandos slash personalizados para acceso rápido a prompts y flujos de trabajo utilizados frecuentemente con gestión completa de la interfaz de usuario. +- **Herramientas Gemini avanzadas** - Nuevas funcionalidades de contexto de URL y fundamentos de búsqueda de Google proporcionan a los modelos Gemini información web en tiempo real y capacidades de búsqueda avanzadas. --- @@ -182,42 +183,43 @@ Usamos [changesets](https://github.com/changesets/changesets) para versionar y p |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Licencia diff --git a/locales/fr/README.md b/locales/fr/README.md index 0ca48da21e..2947eed3d7 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -50,12 +50,13 @@ Consultez le [CHANGELOG](../../CHANGELOG.md) pour des mises à jour détaillées --- -## 🎉 Roo Code 3.23 est sorti +## 🎉 Roo Code 3.25 est sorti -Roo Code 3.23 apporte de puissantes nouvelles fonctionnalités et des améliorations significatives pour améliorer ton flux de travail de développement ! +Roo Code 3.25 apporte de puissantes nouvelles fonctionnalités et des améliorations significatives pour améliorer ton flux de travail de développement ! -- **Indexation de base de code graduée d'expérimentale** - L'indexation complète de la base de code est maintenant stable et prête pour un usage en production avec une recherche améliorée et une compréhension du contexte. -- **Nouvelle fonctionnalité de liste de tâches** - Garde tes tâches sur la bonne voie avec une gestion intégrée des tâches qui t'aide à rester organisé et concentré sur tes objectifs de développement. +- **File d'attente de messages** - Mets plusieurs messages en file d'attente pendant que Roo travaille, te permettant de continuer à planifier ton flux de travail sans interruption. +- **Commandes slash personnalisées** - Crée des commandes slash personnalisées pour un accès rapide aux prompts et flux de travail fréquemment utilisés avec une gestion complète de l'interface utilisateur. +- **Outils Gemini avancés** - De nouvelles fonctionnalités de contexte d'URL et de fondements de recherche Google fournissent aux modèles Gemini des informations web en temps réel et des capacités de recherche avancées. --- @@ -182,42 +183,43 @@ Merci à tous nos contributeurs qui ont aidé à améliorer Roo Code ! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Licence diff --git a/locales/hi/README.md b/locales/hi/README.md index aa3da73d0f..59a1d68a68 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -50,12 +50,13 @@ --- -## 🎉 Roo Code 3.23 जारी +## 🎉 Roo Code 3.25 जारी -Roo Code 3.23 आपके डेवलपमेंट वर्कफ़्लो को बेहतर बनाने के लिए शक्तिशाली नई सुविधाएं और महत्वपूर्ण सुधार लाता है! +Roo Code 3.25 आपके डेवलपमेंट वर्कफ़्लो को बेहतर बनाने के लिए शक्तिशाली नई सुविधाएं और महत्वपूर्ण सुधार लाता है! -- **कोडबेस इंडेक्सिंग एक्सपेरिमेंटल से ग्रेजुएट** - पूर्ण कोडबेस इंडेक्सिंग अब स्थिर है और बेहतर खोज और संदर्भ समझ के साथ प्रोडक्शन उपयोग के लिए तैयार है। -- **नई टूडू लिस्ट सुविधा** - एकीकृत टास्क प्रबंधन के साथ अपने टास्क को ट्रैक पर रखें जो आपको व्यवस्थित रहने और अपने डेवलपमेंट लक्ष्यों पर केंद्रित रहने में मदद करता है। +- **संदेश कतार** - Roo के काम करते समय कई संदेशों को कतार में रखें, जिससे आप बिना रुकावट के अपने वर्कफ़्लो की योजना बना सकते हैं। +- **कस्टम स्लैश कमांड** - पूर्ण UI प्रबंधन के साथ अक्सर उपयोग किए जाने वाले प्रॉम्प्ट और वर्कफ़्लो तक त्वरित पहुंच के लिए व्यक्तिगत स्लैश कमांड बनाएं। +- **उन्नत Gemini उपकरण** - नए URL संदर्भ और Google खोज आधार सुविधाएं Gemini मॉडल को वास्तविक समय वेब जानकारी और उन्नत अनुसंधान क्षमताएं प्रदान करती हैं। --- @@ -182,42 +183,43 @@ Roo Code को बेहतर बनाने में मदद करने |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## लाइसेंस diff --git a/locales/id/README.md b/locales/id/README.md index 2aa0d6b423..098397a71b 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -49,12 +49,13 @@ Lihat [CHANGELOG](../../CHANGELOG.md) untuk update dan perbaikan detail. --- -## 🎉 Roo Code 3.23 Dirilis +## 🎉 Roo Code 3.25 Dirilis -Roo Code 3.23 menghadirkan fitur-fitur baru yang powerful dan peningkatan signifikan untuk meningkatkan workflow development kamu! +Roo Code 3.25 menghadirkan fitur-fitur baru yang powerful dan peningkatan signifikan untuk meningkatkan workflow development kamu! -- **Indexing codebase lulus dari eksperimental** - Indexing codebase lengkap sekarang stabil dan siap untuk penggunaan produksi dengan pencarian yang ditingkatkan dan pemahaman konteks. -- **Fitur daftar todo baru** - Jaga tugas kamu tetap on track dengan manajemen tugas terintegrasi yang membantu kamu tetap terorganisir dan fokus pada tujuan development kamu. +- **Antrian pesan** - Antrikan beberapa pesan saat Roo bekerja, memungkinkan kamu terus merencanakan alur kerja tanpa gangguan. +- **Perintah slash kustom** - Buat perintah slash yang dipersonalisasi untuk akses cepat ke prompt dan alur kerja yang sering digunakan dengan manajemen UI lengkap. +- **Alat Gemini lanjutan** - Fitur konteks URL dan dasar pencarian Google baru memberikan model Gemini informasi web real-time dan kemampuan penelitian lanjutan. --- @@ -176,42 +177,43 @@ Terima kasih kepada semua kontributor kami yang telah membantu membuat Roo Code |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## License diff --git a/locales/it/README.md b/locales/it/README.md index c357f4330d..0f09ec6d3d 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -50,12 +50,13 @@ Consulta il [CHANGELOG](../../CHANGELOG.md) per aggiornamenti dettagliati e corr --- -## 🎉 Roo Code 3.23 Rilasciato +## 🎉 Roo Code 3.25 Rilasciato -Roo Code 3.23 porta nuove funzionalità potenti e miglioramenti significativi per migliorare il tuo flusso di lavoro di sviluppo! +Roo Code 3.25 porta nuove funzionalità potenti e miglioramenti significativi per migliorare il tuo flusso di lavoro di sviluppo! -- **Indicizzazione codebase graduata da sperimentale** - L'indicizzazione completa del codebase è ora stabile e pronta per l'uso in produzione con ricerca migliorata e comprensione del contesto. -- **Nuova funzionalità lista todo** - Mantieni i tuoi task in carreggiata con gestione integrata dei task che ti aiuta a rimanere organizzato e concentrato sui tuoi obiettivi di sviluppo. +- **Coda di messaggi** - Metti in coda più messaggi mentre Roo lavora, permettendoti di continuare a pianificare il tuo flusso di lavoro senza interruzioni. +- **Comandi slash personalizzati** - Crea comandi slash personalizzati per accesso rapido a prompt e flussi di lavoro utilizzati frequentemente con gestione completa dell'interfaccia utente. +- **Strumenti Gemini avanzati** - Nuove funzionalità di contesto URL e fondamenti di ricerca Google forniscono ai modelli Gemini informazioni web in tempo reale e capacità di ricerca avanzate. --- @@ -182,42 +183,43 @@ Grazie a tutti i nostri contributori che hanno aiutato a migliorare Roo Code! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Licenza diff --git a/locales/ja/README.md b/locales/ja/README.md index 2c8b5ecf27..658c87af20 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -50,12 +50,13 @@ --- -## 🎉 Roo Code 3.23 リリース +## 🎉 Roo Code 3.25 リリース -Roo Code 3.23は、開発ワークフローを向上させる強力な新機能と重要な改善をもたらします! +Roo Code 3.25は、開発ワークフローを向上させる強力な新機能と重要な改善をもたらします! -- **コードベースインデックス化が実験的から卒業** - 完全なコードベースインデックス化が安定し、改善された検索とコンテキスト理解でプロダクション使用の準備が整いました。 -- **新しいTodoリスト機能** - 統合されたタスク管理でタスクを軌道に乗せ、整理された状態を保ち、開発目標に集中できるようサポートします。 +- **ブラウザセッション管理** - 複数のブラウザセッションを同時に管理し、異なるタスクやテスト環境を分離できます。 +- **プロンプトキャッシング** - 頻繁に使用されるプロンプトをキャッシュして、応答時間を大幅に短縮し、API使用量を削減します。 +- **コンピューター使用機能** - AIがデスクトップアプリケーションと直接対話し、スクリーンショットを撮影し、クリックやタイピングを実行できます。 --- @@ -182,42 +183,43 @@ Roo Codeの改善に貢献してくれたすべての貢献者に感謝します |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## ライセンス diff --git a/locales/ko/README.md b/locales/ko/README.md index 3b41d7927d..449f5d4600 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -50,12 +50,13 @@ --- -## 🎉 Roo Code 3.23 출시 +## 🎉 Roo Code 3.25 출시 -Roo Code 3.23가 개발 워크플로우를 향상시키는 강력한 새 기능과 중요한 개선사항을 제공합니다! +Roo Code 3.25가 개발 워크플로우를 향상시키는 강력한 새 기능과 중요한 개선사항을 제공합니다! -- **코드베이스 인덱싱이 실험적에서 졸업** - 전체 코드베이스 인덱싱이 이제 안정적이며 향상된 검색과 컨텍스트 이해로 프로덕션 사용 준비가 완료되었습니다. -- **새로운 할 일 목록 기능** - 통합된 작업 관리로 작업을 궤도에 유지하여 체계적으로 정리하고 개발 목표에 집중할 수 있도록 도와줍니다. +- **브라우저 세션 관리** - 여러 브라우저 세션을 동시에 관리하여 다양한 작업과 테스트 환경을 분리할 수 있습니다. +- **프롬프트 캐싱** - 자주 사용되는 프롬프트를 캐시하여 응답 시간을 크게 단축하고 API 사용량을 줄입니다. +- **컴퓨터 사용 기능** - AI가 데스크톱 애플리케이션과 직접 상호작용하고, 스크린샷을 찍고, 클릭과 타이핑을 수행할 수 있습니다. --- @@ -182,42 +183,43 @@ Roo Code를 더 좋게 만드는 데 도움을 준 모든 기여자에게 감사 |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## 라이선스 diff --git a/locales/nl/README.md b/locales/nl/README.md index 77372be7ff..ea8213b782 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -50,12 +50,13 @@ Bekijk de [CHANGELOG](../../CHANGELOG.md) voor gedetailleerde updates en fixes. --- -## 🎉 Roo Code 3.23 Uitgebracht +## 🎉 Roo Code 3.25 Uitgebracht -Roo Code 3.23 brengt krachtige nieuwe functies en significante verbeteringen om je ontwikkelingsworkflow te verbeteren! +Roo Code 3.25 brengt krachtige nieuwe functies en significante verbeteringen om je ontwikkelingsworkflow te verbeteren! -- **Codebase indexering afgestudeerd van experimenteel** - Volledige codebase indexering is nu stabiel en klaar voor productiegebruik met verbeterde zoekfunctionaliteit en contextbegrip. -- **Nieuwe todo-lijst functie** - Houd je taken op koers met geïntegreerd taakbeheer dat je helpt georganiseerd te blijven en gefocust op je ontwikkelingsdoelen. +- **Browser Sessiebeheer** - Beheer meerdere browsersessies tegelijkertijd, waardoor verschillende taken en testomgevingen gescheiden kunnen worden. +- **Prompt Caching** - Cache veelgebruikte prompts om responstijden aanzienlijk te verkorten en API-gebruik te verminderen. +- **Computer Use Functionaliteit** - AI kan direct interacteren met desktoptoepassingen, screenshots maken en klik- en typacties uitvoeren. --- @@ -182,42 +183,43 @@ Dank aan alle bijdragers die Roo Code beter hebben gemaakt! |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Licentie diff --git a/locales/pl/README.md b/locales/pl/README.md index 5a44275d6e..5ccc7c3b71 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -50,12 +50,13 @@ Sprawdź [CHANGELOG](../../CHANGELOG.md), aby uzyskać szczegółowe informacje --- -## 🎉 Roo Code 3.23 został wydany +## 🎉 Roo Code 3.25 został wydany -Roo Code 3.23 wprowadza potężne nowe funkcje i znaczące usprawnienia, aby ulepszyć Twój przepływ pracy deweloperskiej! +Roo Code 3.25 wprowadza potężne nowe funkcje i znaczące usprawnienia, aby ulepszyć Twój przepływ pracy deweloperskiej! -- **Indeksowanie bazy kodu ukończone z eksperymentalnego** - Pełne indeksowanie bazy kodu jest teraz stabilne i gotowe do użytku produkcyjnego z ulepszonymi wyszukiwaniem i rozumieniem kontekstu. -- **Nowa funkcja listy zadań** - Utrzymuj swoje zadania na właściwym torze dzięki zintegrowanemu zarządzaniu zadaniami, które pomaga ci pozostać zorganizowanym i skupionym na celach deweloperskich. +- **Zarządzanie Sesjami Przeglądarki** - Zarządzaj wieloma sesjami przeglądarki jednocześnie, umożliwiając separację różnych zadań i środowisk testowych. +- **Buforowanie Promptów** - Buforuj często używane prompty, aby znacznie skrócić czas odpowiedzi i zmniejszyć użycie API. +- **Funkcjonalność Użycia Komputera** - AI może bezpośrednio wchodzić w interakcje z aplikacjami desktopowymi, robić zrzuty ekranu oraz wykonywać akcje kliknięcia i pisania. --- @@ -182,42 +183,43 @@ Dziękujemy wszystkim naszym współtwórcom, którzy pomogli ulepszyć Roo Code |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Licencja diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index 8412b94ffc..3955e9125c 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -50,12 +50,13 @@ Confira o [CHANGELOG](../../CHANGELOG.md) para atualizações e correções deta --- -## 🎉 Roo Code 3.23 foi lançado +## 🎉 Roo Code 3.25 foi lançado -O Roo Code 3.23 traz novos recursos poderosos e melhorias significativas para aprimorar seu fluxo de trabalho de desenvolvimento! +O Roo Code 3.25 traz novos recursos poderosos e melhorias significativas para aprimorar seu fluxo de trabalho de desenvolvimento! -- **Indexação de base de código graduada do experimental** - A indexação completa da base de código agora é estável e pronta para uso em produção com busca aprimorada e compreensão de contexto. -- **Nova funcionalidade de lista de tarefas** - Mantenha suas tarefas no caminho certo com gerenciamento integrado de tarefas que ajuda você a se manter organizado e focado em seus objetivos de desenvolvimento. +- **Gerenciamento de Sessões do Navegador** - Gerencie múltiplas sessões de navegador simultaneamente, permitindo a separação de diferentes tarefas e ambientes de teste. +- **Cache de Prompts** - Faça cache de prompts frequentemente usados para reduzir significativamente os tempos de resposta e diminuir o uso da API. +- **Funcionalidade de Uso do Computador** - A IA pode interagir diretamente com aplicações desktop, tirar screenshots e executar ações de clique e digitação. --- @@ -182,42 +183,43 @@ Obrigado a todos os nossos contribuidores que ajudaram a tornar o Roo Code melho |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Licença diff --git a/locales/ru/README.md b/locales/ru/README.md index e3161a73d8..2fcd696f58 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -50,12 +50,13 @@ --- -## 🎉 Выпущен Roo Code 3.23 +## 🎉 Выпущен Roo Code 3.25 -Roo Code 3.23 представляет мощные новые функции и значительные улучшения для повышения эффективности вашего рабочего процесса разработки! +Roo Code 3.25 представляет мощные новые функции и значительные улучшения для повышения эффективности вашего рабочего процесса разработки. -- **Индексация кодовой базы выпущена из экспериментальной** - Полная индексация кодовой базы теперь стабильна и готова для производственного использования с улучшенным поиском и пониманием контекста. -- **Новая функция списка дел** - Держите свои задачи на правильном пути с интегрированным управлением задачами, которое помогает вам оставаться организованным и сосредоточенным на ваших целях разработки. +- **Провайдер Hugging Face** - Получите доступ к множеству отличных моделей с открытым исходным кодом напрямую через новый провайдер Hugging Face с бесшовной интеграцией и выбором моделей. +- **Встроенные элементы управления командами** - Новые элементы управления автоматическим подтверждением и отклонением для выполнения команд дают вам точный контроль над операциями терминала с настраиваемыми разрешениями. +- **Поддержка правил AGENTS.md** - Добавляет поддержку стандартного файла AGENTS.md сообщества в корне проекта. --- @@ -182,42 +183,43 @@ code --install-extension bin/roo-cline-.vsix |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Лицензия diff --git a/locales/tr/README.md b/locales/tr/README.md index 7e65ebbafd..ce6e3416f9 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -50,12 +50,13 @@ Detaylı güncellemeler ve düzeltmeler için [CHANGELOG](../../CHANGELOG.md) do --- -## 🎉 Roo Code 3.23 Yayınlandı +## 🎉 Roo Code 3.25 Yayınlandı -Roo Code 3.23 geliştirme iş akışınızı geliştirmek için güçlü yeni özellikler ve önemli iyileştirmeler getiriyor! +Roo Code 3.25 geliştirme iş akışınızı geliştirmek için güçlü yeni özellikler ve önemli iyileştirmeler getiriyor. -- **Kod Tabanı İndeksleme Deneysel Aşamadan Çıktı** - Tam kod tabanı indeksleme artık kararlı ve geliştirilmiş arama ve bağlam anlayışı ile üretim kullanımına hazır. -- **Yeni Yapılacaklar Listesi Özelliği** - Görevlerinizi yolunda tutun, organize kalmanıza ve geliştirme hedeflerinize odaklanmanıza yardımcı olan entegre görev yönetimi ile. +- **Hugging Face Sağlayıcısı** - Yeni Hugging Face sağlayıcısı aracılığıyla sorunsuz entegrasyon ve model seçimi ile doğrudan tonlarca harika açık kaynak modeline erişin. +- **Satır İçi Komut Kontrolleri** - Komut yürütme için yeni otomatik onay ve reddetme kontrolleri, özelleştirilebilir izinlerle terminal işlemleri üzerinde hassas kontrol sağlar. +- **AGENTS.md Kuralları Desteği** - Projenin kök dizininde topluluk standardı AGENTS.md dosyası için destek ekler. --- @@ -182,42 +183,43 @@ Roo Code'u daha iyi hale getirmeye yardımcı olan tüm katkıda bulunanlara te |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Lisans diff --git a/locales/vi/README.md b/locales/vi/README.md index 5483d8c2cf..0d55b025dc 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -50,12 +50,13 @@ Kiểm tra [CHANGELOG](../../CHANGELOG.md) để biết thông tin chi tiết v --- -## 🎉 Đã Phát Hành Roo Code 3.23 +## 🎉 Đã Phát Hành Roo Code 3.25 -Roo Code 3.23 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! +Roo Code 3.25 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. -- **Lập Chỉ Mục Codebase Tốt Nghiệp Khỏi Thử Nghiệm** - Lập chỉ mục codebase đầy đủ hiện đã ổn định và sẵn sàng cho sử dụng sản xuất với khả năng tìm kiếm và hiểu ngữ cảnh được cải thiện. -- **Tính Năng Danh Sách Việc Cần Làm Mới** - Giữ các tác vụ của bạn đúng hướng với quản lý tác vụ tích hợp giúp bạn có tổ chức và tập trung vào mục tiêu phát triển. +- **Nhà Cung Cấp Hugging Face** - Truy cập hàng tấn mô hình nguồn mở tuyệt vời trực tiếp thông qua nhà cung cấp Hugging Face mới với tích hợp liền mạch và lựa chọn mô hình. +- **Điều Khiển Lệnh Nội Tuyến** - Các điều khiển tự động phê duyệt và từ chối mới cho việc thực thi lệnh cung cấp cho bạn quyền kiểm soát chính xác các hoạt động terminal với quyền hạn có thể tùy chỉnh. +- **Hỗ Trợ Quy Tắc AGENTS.md** - Thêm hỗ trợ cho tệp AGENTS.md tiêu chuẩn cộng đồng trong thư mục gốc của dự án. --- @@ -182,42 +183,43 @@ 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
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## Giấy Phép diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index a3af91c935..bea072be43 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -50,12 +50,13 @@ --- -## 🎉 Roo Code 3.23 已发布 +## 🎉 Roo Code 3.25 已发布 -Roo Code 3.23 带来强大的新功能和重大改进,提升您的开发工作流程! +Roo Code 3.25 带来强大的新功能和重大改进,提升您的开发工作流程。 -- **代码库索引从实验阶段毕业** - 完整的代码库索引现已稳定,可用于生产环境,具有改进的搜索和上下文理解能力。 -- **新的待办事项列表功能** - 通过集成的任务管理保持任务进度,帮助您保持组织性并专注于开发目标。 +- **Hugging Face 提供者** - 通过新的 Hugging Face 提供者直接访问大量优秀的开源模型,具有无缝集成和模型选择功能。 +- **内联命令控制** - 新的自动批准和拒绝控制功能为命令执行提供精确控制,具有可自定义的权限设置。 +- **AGENTS.md 规则支持** - 添加对项目根目录中社区标准 AGENTS.md 文件的支持。 --- @@ -182,42 +183,43 @@ code --install-extension bin/roo-cline-.vsix |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## 许可证 diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 16f0f7c936..15f5f78b5f 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -51,12 +51,13 @@ --- -## 🎉 Roo Code 3.23 已發布 +## 🎉 Roo Code 3.25 已發布 -Roo Code 3.23 帶來強大的新功能和重大改進,以提升您的開發工作流程! +Roo Code 3.25 帶來強大的新功能和重大改進,以提升您的開發工作流程。 -- **程式碼庫索引從實驗階段畢業** - 完整的程式碼庫索引現已穩定,可用於生產環境,具有改進的搜尋和上下文理解能力。 -- **新的待辦事項清單功能** - 透過整合的任務管理保持任務進度,幫助您保持組織性並專注於開發目標。 +- **Hugging Face 提供者** - 透過新的 Hugging Face 提供者直接存取大量優秀的開源模型,具有無縫整合和模型選擇功能。 +- **內嵌命令控制** - 新的自動核准和拒絕控制功能為命令執行提供精確控制,具有可自訂的權限設定。 +- **AGENTS.md 規則支援** - 新增對專案根目錄中社群標準 AGENTS.md 檔案的支援。 --- @@ -183,42 +184,43 @@ code --install-extension bin/roo-cline-.vsix |mrubens
mrubens
|saoudrizwan
saoudrizwan
|cte
cte
|daniel-lxs
daniel-lxs
|samhvw8
samhvw8
|hannesrudolph
hannesrudolph
| |:---:|:---:|:---:|:---:|:---:|:---:| -|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|ColemanRoo
ColemanRoo
|MuriloFP
MuriloFP
|canrobins13
canrobins13
|stea9499
stea9499
| +|KJ7LNW
KJ7LNW
|a8trejo
a8trejo
|MuriloFP
MuriloFP
|ColemanRoo
ColemanRoo
|canrobins13
canrobins13
|stea9499
stea9499
| |joemanley201
joemanley201
|System233
System233
|jr
jr
|nissa-seru
nissa-seru
|jquanton
jquanton
|roomote-agent
roomote-agent
| -|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|wkordalski
wkordalski
|qdaxb
qdaxb
|punkpeye
punkpeye
| -|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
|cannuri
cannuri
| -|feifei325
feifei325
|zhangtony239
zhangtony239
|chrarnoldus
chrarnoldus
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| +|NyxJae
NyxJae
|d-oit
d-oit
|elianiva
elianiva
|qdaxb
qdaxb
|punkpeye
punkpeye
|wkordalski
wkordalski
| +|SannidhyaSah
SannidhyaSah
|xyOz-dev
xyOz-dev
|chrarnoldus
chrarnoldus
|sachasayan
sachasayan
|Smartsheet-JB-Brown
Smartsheet-JB-Brown
|monotykamary
monotykamary
| +|cannuri
cannuri
|feifei325
feifei325
|zhangtony239
zhangtony239
|shariqriazz
shariqriazz
|vigneshsubbiah16
vigneshsubbiah16
|pugazhendhi-m
pugazhendhi-m
| |lloydchang
lloydchang
|dtrugman
dtrugman
|Szpadel
Szpadel
|lupuletic
lupuletic
|kiwina
kiwina
|liwilliam2021
liwilliam2021
| -|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|ChuKhaLi
ChuKhaLi
|PeterDaveHello
PeterDaveHello
| -|aheizi
aheizi
|hassoncs
hassoncs
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| +|Premshay
Premshay
|psv2522
psv2522
|olweraltuve
olweraltuve
|diarmidmackenzie
diarmidmackenzie
|hassoncs
hassoncs
|ChuKhaLi
ChuKhaLi
| +|PeterDaveHello
PeterDaveHello
|aheizi
aheizi
|nbihan-mediware
nbihan-mediware
|noritaka1166
noritaka1166
|RaySinner
RaySinner
|afshawnlotfi
afshawnlotfi
| |dleffel
dleffel
|StevenTCramer
StevenTCramer
|Ruakij
Ruakij
|pdecat
pdecat
|kyle-apex
kyle-apex
|emshvac
emshvac
| |Lunchb0ne
Lunchb0ne
|SmartManoj
SmartManoj
|vagadiya
vagadiya
|slytechnical
slytechnical
|dlab-anton
dlab-anton
|arthurauffray
arthurauffray
| |upamune
upamune
|NamesMT
NamesMT
|taylorwilsdon
taylorwilsdon
|sammcj
sammcj
|p12tic
p12tic
|gtaylor
gtaylor
| -|aitoroses
aitoroses
|anton-otee
anton-otee
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| +|brunobergher
brunobergher
|aitoroses
aitoroses
|ross
ross
|mr-ryan-james
mr-ryan-james
|heyseth
heyseth
|taisukeoe
taisukeoe
| |avtc
avtc
|eonghk
eonghk
|GOODBOY008
GOODBOY008
|kcwhite
kcwhite
|ronyblum
ronyblum
|teddyOOXX
teddyOOXX
| |vincentsong
vincentsong
|yongjer
yongjer
|zeozeozeo
zeozeozeo
|ashktn
ashktn
|franekp
franekp
|yt3trees
yt3trees
| -|seedlord
seedlord
|bramburn
bramburn
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
|brunobergher
brunobergher
| +|seedlord
seedlord
|bramburn
bramburn
|anton-otee
anton-otee
|benzntech
benzntech
|axkirillov
axkirillov
|olearycrew
olearycrew
| |catrielmuller
catrielmuller
|devxpain
devxpain
|snoyiatk
snoyiatk
|GitlyHallows
GitlyHallows
|jcbdev
jcbdev
|Chenjiayuan195
Chenjiayuan195
| |julionav
julionav
|KanTakahiro
KanTakahiro
|SplittyDev
SplittyDev
|mdp
mdp
|napter
napter
|philfung
philfung
| -|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
|hatsu38
hatsu38
| -|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
|nevermorec
nevermorec
| -|bbenshalom
bbenshalom
|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
| -|Yikai-Liao
Yikai-Liao
|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|tmsjngx0
tmsjngx0
| -|Githubguy132010
Githubguy132010
|DeXtroTip
DeXtroTip
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
|user202729
user202729
| -|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
|shivamd1810
shivamd1810
| -|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
|qingyuan1109
qingyuan1109
| -|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
|mosleyit
mosleyit
| -|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
|lhish
lhish
| -|kohii
kohii
|pfitz
pfitz
|ExactDoug
ExactDoug
|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
| -|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
| -|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
| -|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
| -|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
|adamhill
adamhill
|AMHesch
AMHesch
|tgfjt
tgfjt
| -|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
|RSO
RSO
|RandalSchwartz
RandalSchwartz
| -|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
|OlegOAndreev
OlegOAndreev
|kvokka
kvokka
| -|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
| -|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
| -|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
|hesara
hesara
| | | +|bbenshalom
bbenshalom
|chris-garrett
chris-garrett
|dairui1
dairui1
|dqroid
dqroid
|janaki-sasidhar
janaki-sasidhar
|forestyoo
forestyoo
| +|hatsu38
hatsu38
|hongzio
hongzio
|im47cn
im47cn
|shoopapa
shoopapa
|jwcraig
jwcraig
|kinandan
kinandan
| +|bannzai
bannzai
|axmo
axmo
|asychin
asychin
|amittell
amittell
|Yoshino-Yukitaro
Yoshino-Yukitaro
|Yikai-Liao
Yikai-Liao
| +|zxdvd
zxdvd
|s97712
s97712
|vladstudio
vladstudio
|vivekfyi
vivekfyi
|HahaBill
HahaBill
|tmsjngx0
tmsjngx0
| +|TGlide
TGlide
|Githubguy132010
Githubguy132010
|nevermorec
nevermorec
|PretzelVector
PretzelVector
|zetaloop
zetaloop
|cdlliuy
cdlliuy
| +|user202729
user202729
|thill2323
thill2323
|takakoutso
takakoutso
|student20880
student20880
|shubhamgupta731
shubhamgupta731
|shohei-ihaya
shohei-ihaya
| +|shivamd1810
shivamd1810
|shaybc
shaybc
|sensei-woo
sensei-woo
|samir-nimbly
samir-nimbly
|robertheadley
robertheadley
|refactorthis
refactorthis
| +|qingyuan1109
qingyuan1109
|pokutuna
pokutuna
|philipnext
philipnext
|village-way
village-way
|oprstchn
oprstchn
|nobu007
nobu007
| +|mosleyit
mosleyit
|moqimoqidea
moqimoqidea
|mlopezr
mlopezr
|mecab
mecab
|olup
olup
|lightrabbit
lightrabbit
| +|lhish
lhish
|kohii
kohii
|tgfjt
tgfjt
|DeXtroTip
DeXtroTip
|pfitz
pfitz
|ExactDoug
ExactDoug
| +|celestial-vault
celestial-vault
|linegel
linegel
|edwin-truthsearch-io
edwin-truthsearch-io
|EamonNerbonne
EamonNerbonne
|dbasclpy
dbasclpy
|dflatline
dflatline
| +|Deon588
Deon588
|dleen
dleen
|CW-B-W
CW-B-W
|chadgauth
chadgauth
|thecolorblue
thecolorblue
|bogdan0083
bogdan0083
| +|benashby
benashby
|Atlogit
Atlogit
|atlasgong
atlasgong
|andrewshu2000
andrewshu2000
|andreastempsch
andreastempsch
|alasano
alasano
| +|QuinsZouls
QuinsZouls
|HadesArchitect
HadesArchitect
|alarno
alarno
|nexon33
nexon33
|adilhafeez
adilhafeez
|adamwlarson
adamwlarson
| +|adamhill
adamhill
|AMHesch
AMHesch
|maekawataiki
maekawataiki
|AlexandruSmirnov
AlexandruSmirnov
|samsilveira
samsilveira
|01Rian
01Rian
| +|RSO
RSO
|RandalSchwartz
RandalSchwartz
|SECKainersdorfer
SECKainersdorfer
|R-omk
R-omk
|Sarke
Sarke
|PaperBoardOfficial
PaperBoardOfficial
| +|OlegOAndreev
OlegOAndreev
|Naam
Naam
|kvokka
kvokka
|ecmasx
ecmasx
|mollux
mollux
|marvijo-code
marvijo-code
| +|markijbema
markijbema
|mamertofabian
mamertofabian
|monkeyDluffy6017
monkeyDluffy6017
|libertyteeth
libertyteeth
|shtse8
shtse8
|Rexarrior
Rexarrior
| +|kevinvandijk
kevinvandijk
|KevinZhao
KevinZhao
|ksze
ksze
|Juice10
Juice10
|Fovty
Fovty
|Jdo300
Jdo300
| +|hesara
hesara
| | | | | | ## 授權 diff --git a/package.json b/package.json index 99becf0a0c..cc917ab7ca 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "tar-fs": ">=2.1.3", "esbuild": ">=0.25.0", "undici": ">=5.29.0", - "brace-expansion": ">=2.0.2" + "brace-expansion": ">=2.0.2", + "form-data": ">=4.0.4" } } } diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 32ea443cd6..9a32a16fcb 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -4,6 +4,7 @@ import type { CloudUserInfo, TelemetryEvent, OrganizationAllowList, + OrganizationSettings, ClineMessage, ShareVisibility, } from "@roo-code/types" @@ -174,6 +175,11 @@ export class CloudService { return this.settingsService!.getAllowList() } + public getOrganizationSettings(): OrganizationSettings | undefined { + this.ensureInitialized() + return this.settingsService!.getSettings() + } + // TelemetryClient public captureEvent(event: TelemetryEvent): void { diff --git a/packages/types/npm/package.json b/packages/types/npm/package.json index e99ffe30c2..3ab21bda2c 100644 --- a/packages/types/npm/package.json +++ b/packages/types/npm/package.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.39.0", + "version": "1.40.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 6df7292dd5..5ef90b6e5a 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { globalSettingsSchema } from "./global-settings.js" +import { mcpMarketplaceItemSchema } from "./marketplace.js" /** * CloudUserInfo @@ -110,6 +111,9 @@ export const organizationSettingsSchema = z.object({ cloudSettings: organizationCloudSettingsSchema.optional(), defaultSettings: organizationDefaultSettingsSchema, allowList: organizationAllowListSchema, + hiddenMcps: z.array(z.string()).optional(), + hideMarketplaceMcps: z.boolean().optional(), + mcps: z.array(mcpMarketplaceItemSchema).optional(), }) export type OrganizationSettings = z.infer diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index 10384db8ed..5424121d67 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -export const experimentIds = ["powerSteering", "multiFileApplyDiff"] as const +export const experimentIds = ["powerSteering", "multiFileApplyDiff", "preventFocusDisruption"] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -19,6 +19,7 @@ export type ExperimentId = z.infer export const experimentsSchema = z.object({ powerSteering: z.boolean().optional(), multiFileApplyDiff: z.boolean().optional(), + preventFocusDisruption: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index d5e76eccea..dc5a9e6744 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -101,6 +101,8 @@ export const globalSettingsSchema = z.object({ maxWorkspaceFiles: z.number().optional(), showRooIgnoredFiles: z.boolean().optional(), maxReadFileLine: z.number().optional(), + maxImageFileSize: z.number().optional(), + maxTotalImageSize: z.number().optional(), terminalOutputLineLimit: z.number().optional(), terminalOutputCharacterLimit: z.number().optional(), diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index 8c75024879..ace134566e 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -16,6 +16,7 @@ export const historyItemSchema = z.object({ totalCost: z.number(), size: z.number().optional(), workspace: z.string().optional(), + mode: z.string().optional(), }) export type HistoryItem = z.infer diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index 0c87655fc0..eaec2ad886 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -174,3 +174,19 @@ export const tokenUsageSchema = z.object({ }) export type TokenUsage = z.infer + +/** + * QueuedMessage + */ + +/** + * Represents a message that is queued to be sent when sending is enabled + */ +export interface QueuedMessage { + /** Unique identifier for the queued message */ + id: string + /** The text content of the message */ + text: string + /** Array of image data URLs attached to the message */ + images: string[] +} diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 682514b0fa..8cdb5296b2 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -169,6 +169,8 @@ const lmStudioSchema = baseProviderSettingsSchema.extend({ const geminiSchema = apiModelIdProviderModelSchema.extend({ geminiApiKey: z.string().optional(), googleGeminiBaseUrl: z.string().optional(), + enableUrlContext: z.boolean().optional(), + enableGrounding: z.boolean().optional(), }) const geminiCliSchema = apiModelIdProviderModelSchema.extend({ @@ -236,6 +238,7 @@ const litellmSchema = baseProviderSettingsSchema.extend({ litellmBaseUrl: z.string().optional(), litellmApiKey: z.string().optional(), litellmModelId: z.string().optional(), + litellmUsePromptCache: z.boolean().optional(), }) const defaultSchema = z.object({ diff --git a/packages/types/src/providers/chutes.ts b/packages/types/src/providers/chutes.ts index 5d81799223..98a2f4f360 100644 --- a/packages/types/src/providers/chutes.ts +++ b/packages/types/src/providers/chutes.ts @@ -25,6 +25,7 @@ export type ChutesModelId = | "Qwen/Qwen3-8B" | "microsoft/MAI-DS-R1-FP8" | "tngtech/DeepSeek-R1T-Chimera" + | "zai-org/GLM-4.5-Air" export const chutesDefaultModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1-0528" @@ -236,4 +237,14 @@ export const chutesModels = { outputPrice: 0, description: "TNGTech DeepSeek R1T Chimera model.", }, + "zai-org/GLM-4.5-Air": { + maxTokens: 32768, + contextWindow: 151329, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: + "GLM-4.5-Air model with 151,329 token context window and 106B total parameters with 12B activated.", + }, } as const satisfies Record diff --git a/packages/types/src/providers/claude-code.ts b/packages/types/src/providers/claude-code.ts index df8b3d302a..6f72baf008 100644 --- a/packages/types/src/providers/claude-code.ts +++ b/packages/types/src/providers/claude-code.ts @@ -22,7 +22,7 @@ export function convertModelNameForVertex(modelName: string): string { // Claude Code export type ClaudeCodeModelId = keyof typeof claudeCodeModels export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514" -export const CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS = 8000 +export const CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS = 16000 /** * Gets the appropriate model ID based on whether Vertex AI is being used. diff --git a/packages/types/src/providers/huggingface.ts b/packages/types/src/providers/huggingface.ts new file mode 100644 index 0000000000..d2571a073e --- /dev/null +++ b/packages/types/src/providers/huggingface.ts @@ -0,0 +1,17 @@ +/** + * HuggingFace provider constants + */ + +// Default values for HuggingFace models +export const HUGGINGFACE_DEFAULT_MAX_TOKENS = 2048 +export const HUGGINGFACE_MAX_TOKENS_FALLBACK = 8192 +export const HUGGINGFACE_DEFAULT_CONTEXT_WINDOW = 128_000 + +// UI constants +export const HUGGINGFACE_SLIDER_STEP = 256 +export const HUGGINGFACE_SLIDER_MIN = 1 +export const HUGGINGFACE_TEMPERATURE_MAX_VALUE = 2 + +// API constants +export const HUGGINGFACE_API_URL = "https://router.huggingface.co/v1/models?collection=roocode" +export const HUGGINGFACE_CACHE_DURATION = 1000 * 60 * 60 // 1 hour diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index e4e506b8a7..f5061f152c 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -6,6 +6,7 @@ export * from "./deepseek.js" export * from "./gemini.js" export * from "./glama.js" export * from "./groq.js" +export * from "./huggingface.js" export * from "./lite-llm.js" export * from "./lm-studio.js" export * from "./mistral.js" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14e33596d1..4134d3945a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ overrides: esbuild: '>=0.25.0' undici: '>=5.29.0' brace-expansion: '>=2.0.2' + form-data: '>=4.0.4' importers: @@ -654,6 +655,9 @@ importers: google-auth-library: specifier: ^9.15.1 version: 9.15.1 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 i18next: specifier: ^25.0.0 version: 25.2.1(typescript@5.8.3) @@ -5644,6 +5648,10 @@ packages: exsolve@1.0.5: resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -5801,8 +5809,8 @@ packages: form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} formatly@0.2.4: @@ -6026,6 +6034,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -6340,6 +6352,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -8432,6 +8448,10 @@ packages: resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} engines: {node: '>=0.10.0'} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + seed-random@2.2.0: resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} @@ -8744,6 +8764,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -13098,7 +13122,7 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: '@types/node': 20.17.57 - form-data: 4.0.2 + form-data: 4.0.4 '@types/node-ipc@9.2.3': dependencies: @@ -13440,7 +13464,7 @@ snapshots: cheerio: 1.0.0 cockatiel: 3.2.1 commander: 12.1.0 - form-data: 4.0.2 + form-data: 4.0.4 glob: 11.0.2 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 @@ -13684,7 +13708,7 @@ snapshots: axios@1.9.0: dependencies: follow-redirects: 1.15.9 - form-data: 4.0.2 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -15174,6 +15198,10 @@ snapshots: exsolve@1.0.5: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -15333,11 +15361,12 @@ snapshots: form-data-encoder@1.7.2: {} - form-data@4.0.2: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 formatly@0.2.4: @@ -15597,6 +15626,13 @@ snapshots: graphemer@1.4.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -15959,6 +15995,8 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -18479,6 +18517,11 @@ snapshots: screenfull@5.2.0: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + seed-random@2.2.0: {} semver@5.7.2: {} @@ -18873,6 +18916,8 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-bom@5.0.0: {} diff --git a/src/__tests__/command-integration.spec.ts b/src/__tests__/command-integration.spec.ts new file mode 100644 index 0000000000..e884325b68 --- /dev/null +++ b/src/__tests__/command-integration.spec.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest" +import { getCommands, getCommand, getCommandNames } from "../services/command/commands" +import * as path from "path" + +describe("Command Integration Tests", () => { + const testWorkspaceDir = path.join(__dirname, "../../") + + it("should discover command files in .roo/commands/", async () => { + const commands = await getCommands(testWorkspaceDir) + + // Should be able to discover commands (may be empty in test environment) + expect(Array.isArray(commands)).toBe(true) + + // If commands exist, verify they have valid properties + commands.forEach((command) => { + expect(command.name).toBeDefined() + expect(typeof command.name).toBe("string") + expect(command.source).toMatch(/^(project|global)$/) + expect(command.content).toBeDefined() + expect(typeof command.content).toBe("string") + }) + }) + + it("should return command names correctly", async () => { + const commandNames = await getCommandNames(testWorkspaceDir) + + // Should return an array (may be empty in test environment) + expect(Array.isArray(commandNames)).toBe(true) + + // If command names exist, they should be strings + commandNames.forEach((name) => { + expect(typeof name).toBe("string") + expect(name.length).toBeGreaterThan(0) + }) + }) + + it("should load command content if commands exist", async () => { + const commands = await getCommands(testWorkspaceDir) + + if (commands.length > 0) { + const firstCommand = commands[0] + const loadedCommand = await getCommand(testWorkspaceDir, firstCommand.name) + + expect(loadedCommand).toBeDefined() + expect(loadedCommand?.name).toBe(firstCommand.name) + expect(loadedCommand?.source).toMatch(/^(project|global)$/) + expect(loadedCommand?.content).toBeDefined() + expect(typeof loadedCommand?.content).toBe("string") + } + }) + + it("should handle non-existent commands gracefully", async () => { + const nonExistentCommand = await getCommand(testWorkspaceDir, "non-existent-command") + expect(nonExistentCommand).toBeUndefined() + }) +}) diff --git a/src/__tests__/command-mentions.spec.ts b/src/__tests__/command-mentions.spec.ts new file mode 100644 index 0000000000..d4de0bbba7 --- /dev/null +++ b/src/__tests__/command-mentions.spec.ts @@ -0,0 +1,307 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import { parseMentions } from "../core/mentions" +import { UrlContentFetcher } from "../services/browser/UrlContentFetcher" +import { getCommand } from "../services/command/commands" + +// Mock the dependencies +vi.mock("../services/command/commands") +vi.mock("../services/browser/UrlContentFetcher") + +const MockedUrlContentFetcher = vi.mocked(UrlContentFetcher) +const mockGetCommand = vi.mocked(getCommand) + +describe("Command Mentions", () => { + let mockUrlContentFetcher: any + + beforeEach(() => { + vi.clearAllMocks() + + // Create a mock UrlContentFetcher instance + mockUrlContentFetcher = { + launchBrowser: vi.fn(), + urlToMarkdown: vi.fn(), + closeBrowser: vi.fn(), + } + + MockedUrlContentFetcher.mockImplementation(() => mockUrlContentFetcher) + }) + + // Helper function to call parseMentions with required parameters + const callParseMentions = async (text: string) => { + return await parseMentions( + text, + "/test/cwd", // cwd + mockUrlContentFetcher, // urlContentFetcher + undefined, // fileContextTracker + undefined, // rooIgnoreController + true, // showRooIgnoredFiles + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages + undefined, // maxReadFileLine + ) + } + + describe("parseMentions with command support", () => { + it("should parse command mentions and include content", async () => { + const commandContent = "# Setup Environment\n\nRun the following commands:\n```bash\nnpm install\n```" + mockGetCommand.mockResolvedValue({ + name: "setup", + content: commandContent, + source: "project", + filePath: "/project/.roo/commands/setup.md", + }) + + const input = "/setup Please help me set up the project" + const result = await callParseMentions(input) + + expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup") + expect(result).toContain('') + expect(result).toContain(commandContent) + expect(result).toContain("") + expect(result).toContain("Please help me set up the project") + }) + + it("should handle multiple commands in message", async () => { + mockGetCommand + .mockResolvedValueOnce({ + name: "setup", + content: "# Setup instructions", + source: "project", + filePath: "/project/.roo/commands/setup.md", + }) + .mockResolvedValueOnce({ + name: "deploy", + content: "# Deploy instructions", + source: "project", + filePath: "/project/.roo/commands/deploy.md", + }) + + // Both commands should be recognized + const input = "/setup the project\nThen /deploy later" + const result = await callParseMentions(input) + + expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup") + expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy") + expect(mockGetCommand).toHaveBeenCalledTimes(2) // Both commands called + expect(result).toContain('') + expect(result).toContain("# Setup instructions") + expect(result).toContain('') + expect(result).toContain("# Deploy instructions") + }) + + it("should handle non-existent command gracefully", async () => { + mockGetCommand.mockResolvedValue(undefined) + + const input = "/nonexistent command" + const result = await callParseMentions(input) + + expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent") + expect(result).toContain('') + expect(result).toContain("Command 'nonexistent' not found") + expect(result).toContain("") + }) + + it("should handle command loading errors", async () => { + mockGetCommand.mockRejectedValue(new Error("Failed to load command")) + + const input = "/error-command test" + const result = await callParseMentions(input) + + expect(result).toContain('') + expect(result).toContain("Error loading command") + expect(result).toContain("") + }) + + it("should handle command names with hyphens and underscores at start", async () => { + mockGetCommand.mockResolvedValue({ + name: "setup-dev", + content: "# Dev setup", + source: "project", + filePath: "/project/.roo/commands/setup-dev.md", + }) + + const input = "/setup-dev for the project" + const result = await callParseMentions(input) + + expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup-dev") + expect(result).toContain('') + expect(result).toContain("# Dev setup") + }) + + it("should preserve command content formatting", async () => { + const commandContent = `# Complex Command + +## Step 1 +Run this command: +\`\`\`bash +npm install +\`\`\` + +## Step 2 +- Check file1.js +- Update file2.ts +- Test everything + +> **Note**: This is important!` + + mockGetCommand.mockResolvedValue({ + name: "complex", + content: commandContent, + source: "project", + filePath: "/project/.roo/commands/complex.md", + }) + + const input = "/complex command" + const result = await callParseMentions(input) + + expect(result).toContain('') + expect(result).toContain("# Complex Command") + expect(result).toContain("```bash") + expect(result).toContain("npm install") + expect(result).toContain("- Check file1.js") + expect(result).toContain("> **Note**: This is important!") + expect(result).toContain("") + }) + + it("should handle empty command content", async () => { + mockGetCommand.mockResolvedValue({ + name: "empty", + content: "", + source: "project", + filePath: "/project/.roo/commands/empty.md", + }) + + const input = "/empty command" + const result = await callParseMentions(input) + + expect(result).toContain('') + expect(result).toContain("") + // Should still include the command tags even with empty content + }) + }) + + describe("command mention regex patterns", () => { + it("should match valid command mention patterns anywhere", () => { + const commandRegex = /\/([a-zA-Z0-9_\.-]+)(?=\s|$)/g + + const validPatterns = ["/setup", "/build-prod", "/test_suite", "/my-command", "/command123"] + + validPatterns.forEach((pattern) => { + const match = pattern.match(commandRegex) + expect(match).toBeTruthy() + expect(match![0]).toBe(pattern) + }) + }) + + it("should match command patterns in middle of text", () => { + const commandRegex = /\/([a-zA-Z0-9_\.-]+)(?=\s|$)/g + + const validPatterns = ["Please /setup", "Run /build now", "Use /deploy here"] + + validPatterns.forEach((pattern) => { + const match = pattern.match(commandRegex) + expect(match).toBeTruthy() + expect(match![0]).toMatch(/^\/[a-zA-Z0-9_\.-]+$/) + }) + }) + + it("should match commands at start of new lines", () => { + const commandRegex = /\/([a-zA-Z0-9_\.-]+)(?=\s|$)/g + + const multilineText = "First line\n/setup the project\nAnother line\n/deploy when ready" + const matches = multilineText.match(commandRegex) + + // Should match both commands now + expect(matches).toBeTruthy() + expect(matches).toHaveLength(2) + expect(matches![0]).toBe("/setup") + expect(matches![1]).toBe("/deploy") + }) + + it("should match multiple commands in message", () => { + const commandRegex = /(?:^|\s)\/([a-zA-Z0-9_\.-]+)(?=\s|$)/g + + const validText = "/setup the project\nThen /deploy later" + const matches = validText.match(commandRegex) + + expect(matches).toBeTruthy() + expect(matches).toHaveLength(2) + expect(matches![0]).toBe("/setup") + expect(matches![1]).toBe(" /deploy") // Note: includes leading space + }) + + it("should not match invalid command patterns", () => { + const commandRegex = /\/([a-zA-Z0-9_\.-]+)(?=\s|$)/g + + const invalidPatterns = ["/ space", "/with space", "/with/slash", "//double", "/with@symbol"] + + invalidPatterns.forEach((pattern) => { + const match = pattern.match(commandRegex) + if (match) { + // If it matches, it should not be the full invalid pattern + expect(match[0]).not.toBe(pattern) + } + }) + }) + }) + + describe("command mention text transformation", () => { + it("should transform command mentions at start of message", async () => { + const input = "/setup the project" + const result = await callParseMentions(input) + + expect(result).toContain("Command 'setup' (see below for command content)") + }) + + it("should process multiple commands in message", async () => { + mockGetCommand + .mockResolvedValueOnce({ + name: "setup", + content: "# Setup instructions", + source: "project", + filePath: "/project/.roo/commands/setup.md", + }) + .mockResolvedValueOnce({ + name: "deploy", + content: "# Deploy instructions", + source: "project", + filePath: "/project/.roo/commands/deploy.md", + }) + + const input = "/setup the project\nThen /deploy later" + const result = await callParseMentions(input) + + expect(result).toContain("Command 'setup' (see below for command content)") + expect(result).toContain("Command 'deploy' (see below for command content)") + }) + + it("should match commands anywhere with proper word boundaries", async () => { + mockGetCommand.mockResolvedValue({ + name: "build", + content: "# Build instructions", + source: "project", + filePath: "/project/.roo/commands/build.md", + }) + + // At the beginning - should match + let input = "/build the project" + let result = await callParseMentions(input) + expect(result).toContain("Command 'build'") + + // After space - should match + input = "Please /build and test" + result = await callParseMentions(input) + expect(result).toContain("Command 'build'") + + // At the end - should match + input = "Run the /build" + result = await callParseMentions(input) + expect(result).toContain("Command 'build'") + + // At start of new line - should match + input = "Some text\n/build the project" + result = await callParseMentions(input) + expect(result).toContain("Command 'build'") + }) + }) +}) diff --git a/src/__tests__/commands.spec.ts b/src/__tests__/commands.spec.ts new file mode 100644 index 0000000000..9401050062 --- /dev/null +++ b/src/__tests__/commands.spec.ts @@ -0,0 +1,97 @@ +import { describe, it, expect } from "vitest" +import { + getCommands, + getCommand, + getCommandNames, + getCommandNameFromFile, + isMarkdownFile, +} from "../services/command/commands" + +describe("Command Utilities", () => { + const testCwd = "/test/project" + + describe("getCommandNameFromFile", () => { + it("should strip .md extension only", () => { + expect(getCommandNameFromFile("my-command.md")).toBe("my-command") + expect(getCommandNameFromFile("test.txt")).toBe("test.txt") + expect(getCommandNameFromFile("no-extension")).toBe("no-extension") + expect(getCommandNameFromFile("multiple.dots.file.md")).toBe("multiple.dots.file") + expect(getCommandNameFromFile("api.config.md")).toBe("api.config") + expect(getCommandNameFromFile("deploy_prod.md")).toBe("deploy_prod") + }) + }) + + describe("isMarkdownFile", () => { + it("should identify markdown files correctly", () => { + // Markdown files + expect(isMarkdownFile("command.md")).toBe(true) + expect(isMarkdownFile("my-command.md")).toBe(true) + expect(isMarkdownFile("README.MD")).toBe(true) + expect(isMarkdownFile("test.Md")).toBe(true) + + // Non-markdown files + expect(isMarkdownFile("command.txt")).toBe(false) + expect(isMarkdownFile("script.sh")).toBe(false) + expect(isMarkdownFile("config.json")).toBe(false) + expect(isMarkdownFile("no-extension")).toBe(false) + expect(isMarkdownFile("file.md.bak")).toBe(false) + }) + }) + + describe("getCommands", () => { + it("should return empty array when no command directories exist", async () => { + // This will fail to find directories but should return empty array gracefully + const commands = await getCommands(testCwd) + expect(Array.isArray(commands)).toBe(true) + }) + }) + + describe("getCommandNames", () => { + it("should return empty array when no commands exist", async () => { + const names = await getCommandNames(testCwd) + expect(Array.isArray(names)).toBe(true) + }) + }) + + describe("getCommand", () => { + it("should return undefined for non-existent command", async () => { + const result = await getCommand(testCwd, "non-existent") + expect(result).toBeUndefined() + }) + }) + + describe("command name extraction edge cases", () => { + it("should handle various filename formats", () => { + // Files without extensions + expect(getCommandNameFromFile("command")).toBe("command") + expect(getCommandNameFromFile("my-command")).toBe("my-command") + + // Files with multiple dots - only strip .md extension + expect(getCommandNameFromFile("my.complex.command.md")).toBe("my.complex.command") + expect(getCommandNameFromFile("v1.2.3.txt")).toBe("v1.2.3.txt") + + // Edge cases + expect(getCommandNameFromFile(".")).toBe(".") + expect(getCommandNameFromFile("..")).toBe("..") + expect(getCommandNameFromFile(".hidden.md")).toBe(".hidden") + }) + }) + + describe("command loading behavior", () => { + it("should handle multiple calls to getCommands", async () => { + const commands1 = await getCommands(testCwd) + const commands2 = await getCommands(testCwd) + expect(Array.isArray(commands1)).toBe(true) + expect(Array.isArray(commands2)).toBe(true) + }) + }) + + describe("error handling", () => { + it("should handle invalid command names gracefully", async () => { + // These should not throw errors + expect(await getCommand(testCwd, "")).toBeUndefined() + expect(await getCommand(testCwd, " ")).toBeUndefined() + expect(await getCommand(testCwd, "non/existent/path")).toBeUndefined() + }) + }) +}) diff --git a/src/api/huggingface-models.ts b/src/api/huggingface-models.ts deleted file mode 100644 index ec1915d0e3..0000000000 --- a/src/api/huggingface-models.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { fetchHuggingFaceModels, type HuggingFaceModel } from "../services/huggingface-models" - -export interface HuggingFaceModelsResponse { - models: HuggingFaceModel[] - cached: boolean - timestamp: number -} - -export async function getHuggingFaceModels(): Promise { - const models = await fetchHuggingFaceModels() - - return { - models, - cached: false, // We could enhance this to track if data came from cache - timestamp: Date.now(), - } -} diff --git a/src/api/providers/__tests__/chutes.spec.ts b/src/api/providers/__tests__/chutes.spec.ts index 419ac50dfd..35cb183dae 100644 --- a/src/api/providers/__tests__/chutes.spec.ts +++ b/src/api/providers/__tests__/chutes.spec.ts @@ -185,6 +185,29 @@ describe("ChutesHandler", () => { ) }) + it("should return zai-org/GLM-4.5-Air model with correct configuration", () => { + const testModelId: ChutesModelId = "zai-org/GLM-4.5-Air" + const handlerWithModel = new ChutesHandler({ + apiModelId: testModelId, + chutesApiKey: "test-chutes-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 32768, + contextWindow: 151329, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: + "GLM-4.5-Air model with 151,329 token context window and 106B total parameters with 12B activated.", + temperature: 0.5, // Default temperature for non-DeepSeek models + }), + ) + }) + it("completePrompt method should return text from Chutes API", async () => { const expectedResponse = "This is a test response from Chutes" mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) diff --git a/src/api/providers/__tests__/gemini-handler.spec.ts b/src/api/providers/__tests__/gemini-handler.spec.ts new file mode 100644 index 0000000000..7c61639cfd --- /dev/null +++ b/src/api/providers/__tests__/gemini-handler.spec.ts @@ -0,0 +1,137 @@ +import { describe, it, expect, vi } from "vitest" +import { t } from "i18next" +import { GeminiHandler } from "../gemini" +import type { ApiHandlerOptions } from "../../../shared/api" + +describe("GeminiHandler backend support", () => { + it("passes tools for URL context and grounding in config", async () => { + const options = { + apiProvider: "gemini", + enableUrlContext: true, + enableGrounding: true, + } as ApiHandlerOptions + const handler = new GeminiHandler(options) + const stub = vi.fn().mockReturnValue((async function* () {})()) + // @ts-ignore access private client + handler["client"].models.generateContentStream = stub + await handler.createMessage("instr", [] as any).next() + const config = stub.mock.calls[0][0].config + expect(config.tools).toEqual([{ urlContext: {} }, { googleSearch: {} }]) + }) + + it("completePrompt passes config overrides without tools when URL context and grounding disabled", async () => { + const options = { + apiProvider: "gemini", + enableUrlContext: false, + enableGrounding: false, + } as ApiHandlerOptions + const handler = new GeminiHandler(options) + const stub = vi.fn().mockResolvedValue({ text: "ok" }) + // @ts-ignore access private client + handler["client"].models.generateContent = stub + const res = await handler.completePrompt("hi") + expect(res).toBe("ok") + const promptConfig = stub.mock.calls[0][0].config + expect(promptConfig.tools).toBeUndefined() + }) + + describe("error scenarios", () => { + it("should handle grounding metadata extraction failure gracefully", async () => { + const options = { + apiProvider: "gemini", + enableGrounding: true, + } as ApiHandlerOptions + const handler = new GeminiHandler(options) + + const mockStream = async function* () { + yield { + candidates: [ + { + groundingMetadata: { + // Invalid structure - missing groundingChunks + }, + content: { parts: [{ text: "test response" }] }, + }, + ], + usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 }, + } + } + + const stub = vi.fn().mockReturnValue(mockStream()) + // @ts-ignore access private client + handler["client"].models.generateContentStream = stub + + const messages = [] + for await (const chunk of handler.createMessage("test", [] as any)) { + messages.push(chunk) + } + + // Should still return the main content without sources + expect(messages.some((m) => m.type === "text" && m.text === "test response")).toBe(true) + expect(messages.some((m) => m.type === "text" && m.text?.includes("Sources:"))).toBe(false) + }) + + it("should handle malformed grounding metadata", async () => { + const options = { + apiProvider: "gemini", + enableGrounding: true, + } as ApiHandlerOptions + const handler = new GeminiHandler(options) + + const mockStream = async function* () { + yield { + candidates: [ + { + groundingMetadata: { + groundingChunks: [ + { web: null }, // Missing URI + { web: { uri: "https://example.com" } }, // Valid + {}, // Missing web property entirely + ], + }, + content: { parts: [{ text: "test response" }] }, + }, + ], + usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 }, + } + } + + const stub = vi.fn().mockReturnValue(mockStream()) + // @ts-ignore access private client + handler["client"].models.generateContentStream = stub + + const messages = [] + for await (const chunk of handler.createMessage("test", [] as any)) { + messages.push(chunk) + } + + // Should only include valid citations + const sourceMessage = messages.find((m) => m.type === "text" && m.text?.includes("[2]")) + expect(sourceMessage).toBeDefined() + if (sourceMessage && "text" in sourceMessage) { + expect(sourceMessage.text).toContain("https://example.com") + expect(sourceMessage.text).not.toContain("[1]") + expect(sourceMessage.text).not.toContain("[3]") + } + }) + + it("should handle API errors when tools are enabled", async () => { + const options = { + apiProvider: "gemini", + enableUrlContext: true, + enableGrounding: true, + } as ApiHandlerOptions + const handler = new GeminiHandler(options) + + const mockError = new Error("API rate limit exceeded") + const stub = vi.fn().mockRejectedValue(mockError) + // @ts-ignore access private client + handler["client"].models.generateContentStream = stub + + await expect(async () => { + const generator = handler.createMessage("test", [] as any) + await generator.next() + }).rejects.toThrow(t("common:errors.gemini.generate_stream", { error: "API rate limit exceeded" })) + }) + }) +}) diff --git a/src/api/providers/__tests__/gemini.spec.ts b/src/api/providers/__tests__/gemini.spec.ts index 8a7fd24fe3..812c1ae1a6 100644 --- a/src/api/providers/__tests__/gemini.spec.ts +++ b/src/api/providers/__tests__/gemini.spec.ts @@ -4,6 +4,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { type ModelInfo, geminiDefaultModelId } from "@roo-code/types" +import { t } from "i18next" import { GeminiHandler } from "../gemini" const GEMINI_20_FLASH_THINKING_NAME = "gemini-2.0-flash-thinking-exp-1219" @@ -129,7 +130,7 @@ describe("GeminiHandler", () => { ;(handler["client"].models.generateContent as any).mockRejectedValue(mockError) await expect(handler.completePrompt("Test prompt")).rejects.toThrow( - "Gemini completion error: Gemini API error", + t("common:errors.gemini.generate_complete_prompt", { error: "Gemini API error" }), ) }) diff --git a/src/api/providers/__tests__/lite-llm.spec.ts b/src/api/providers/__tests__/lite-llm.spec.ts new file mode 100644 index 0000000000..26ebbc3525 --- /dev/null +++ b/src/api/providers/__tests__/lite-llm.spec.ts @@ -0,0 +1,158 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import OpenAI from "openai" +import { Anthropic } from "@anthropic-ai/sdk" + +import { LiteLLMHandler } from "../lite-llm" +import { ApiHandlerOptions } from "../../../shared/api" +import { litellmDefaultModelId, litellmDefaultModelInfo } from "@roo-code/types" + +// Mock vscode first to avoid import errors +vi.mock("vscode", () => ({})) + +// Mock OpenAI +vi.mock("openai", () => { + const mockStream = { + [Symbol.asyncIterator]: vi.fn(), + } + + const mockCreate = vi.fn().mockReturnValue({ + withResponse: vi.fn().mockResolvedValue({ data: mockStream }), + }) + + return { + default: vi.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), + } +}) + +// Mock model fetching +vi.mock("../fetchers/modelCache", () => ({ + getModels: vi.fn().mockImplementation(() => { + return Promise.resolve({ + [litellmDefaultModelId]: litellmDefaultModelInfo, + }) + }), +})) + +describe("LiteLLMHandler", () => { + let handler: LiteLLMHandler + let mockOptions: ApiHandlerOptions + let mockOpenAIClient: any + + beforeEach(() => { + vi.clearAllMocks() + mockOptions = { + litellmApiKey: "test-key", + litellmBaseUrl: "http://localhost:4000", + litellmModelId: litellmDefaultModelId, + } + handler = new LiteLLMHandler(mockOptions) + mockOpenAIClient = new OpenAI() + }) + + describe("prompt caching", () => { + it("should add cache control headers when litellmUsePromptCache is enabled", async () => { + const optionsWithCache: ApiHandlerOptions = { + ...mockOptions, + litellmUsePromptCache: true, + } + handler = new LiteLLMHandler(optionsWithCache) + + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there!" }, + { role: "user", content: "How are you?" }, + ] + + // Mock the stream response + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + choices: [{ delta: { content: "I'm doing well!" } }], + usage: { + prompt_tokens: 100, + completion_tokens: 50, + cache_creation_input_tokens: 20, + cache_read_input_tokens: 30, + }, + } + }, + } + + mockOpenAIClient.chat.completions.create.mockReturnValue({ + withResponse: vi.fn().mockResolvedValue({ data: mockStream }), + }) + + const generator = handler.createMessage(systemPrompt, messages) + const results = [] + for await (const chunk of generator) { + results.push(chunk) + } + + // Verify that create was called with cache control headers + const createCall = mockOpenAIClient.chat.completions.create.mock.calls[0][0] + + // Check system message has cache control in the proper format + expect(createCall.messages[0]).toMatchObject({ + role: "system", + content: [ + { + type: "text", + text: systemPrompt, + cache_control: { type: "ephemeral" }, + }, + ], + }) + + // Check that the last two user messages have cache control + const userMessageIndices = createCall.messages + .map((msg: any, idx: number) => (msg.role === "user" ? idx : -1)) + .filter((idx: number) => idx !== -1) + + const lastUserIdx = userMessageIndices[userMessageIndices.length - 1] + const secondLastUserIdx = userMessageIndices[userMessageIndices.length - 2] + + // Check last user message has proper structure with cache control + expect(createCall.messages[lastUserIdx]).toMatchObject({ + role: "user", + content: [ + { + type: "text", + text: "How are you?", + cache_control: { type: "ephemeral" }, + }, + ], + }) + + // Check second last user message (first user message in this case) + if (secondLastUserIdx !== -1) { + expect(createCall.messages[secondLastUserIdx]).toMatchObject({ + role: "user", + content: [ + { + type: "text", + text: "Hello", + cache_control: { type: "ephemeral" }, + }, + ], + }) + } + + // Verify usage includes cache tokens + const usageChunk = results.find((chunk) => chunk.type === "usage") + expect(usageChunk).toMatchObject({ + type: "usage", + inputTokens: 100, + outputTokens: 50, + cacheWriteTokens: 20, + cacheReadTokens: 30, + }) + }) + }) +}) diff --git a/src/api/providers/__tests__/vertex.spec.ts b/src/api/providers/__tests__/vertex.spec.ts index 8e9add524d..d147e79ba8 100644 --- a/src/api/providers/__tests__/vertex.spec.ts +++ b/src/api/providers/__tests__/vertex.spec.ts @@ -7,6 +7,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { ApiStreamChunk } from "../../transform/stream" +import { t } from "i18next" import { VertexHandler } from "../vertex" describe("VertexHandler", () => { @@ -105,7 +106,7 @@ describe("VertexHandler", () => { ;(handler["client"].models.generateContent as any).mockRejectedValue(mockError) await expect(handler.completePrompt("Test prompt")).rejects.toThrow( - "Gemini completion error: Vertex API error", + t("common:errors.gemini.generate_complete_prompt", { error: "Vertex API error" }), ) }) diff --git a/src/api/providers/fetchers/huggingface.ts b/src/api/providers/fetchers/huggingface.ts new file mode 100644 index 0000000000..7a45c74535 --- /dev/null +++ b/src/api/providers/fetchers/huggingface.ts @@ -0,0 +1,290 @@ +import axios from "axios" +import { z } from "zod" +import type { ModelInfo } from "@roo-code/types" +import { + HUGGINGFACE_API_URL, + HUGGINGFACE_CACHE_DURATION, + HUGGINGFACE_DEFAULT_MAX_TOKENS, + HUGGINGFACE_DEFAULT_CONTEXT_WINDOW, +} from "@roo-code/types" +import type { ModelRecord } from "../../../shared/api" + +/** + * HuggingFace Provider Schema + */ +const huggingFaceProviderSchema = z.object({ + provider: z.string(), + status: z.enum(["live", "staging", "error"]), + supports_tools: z.boolean().optional(), + supports_structured_output: z.boolean().optional(), + context_length: z.number().optional(), + pricing: z + .object({ + input: z.number(), + output: z.number(), + }) + .optional(), +}) + +/** + * Represents a provider that can serve a HuggingFace model + * @property provider - The provider identifier (e.g., "sambanova", "together") + * @property status - The current status of the provider + * @property supports_tools - Whether the provider supports tool/function calling + * @property supports_structured_output - Whether the provider supports structured output + * @property context_length - The maximum context length supported by this provider + * @property pricing - The pricing information for input/output tokens + */ +export type HuggingFaceProvider = z.infer + +/** + * HuggingFace Model Schema + */ +const huggingFaceModelSchema = z.object({ + id: z.string(), + object: z.literal("model"), + created: z.number(), + owned_by: z.string(), + providers: z.array(huggingFaceProviderSchema), +}) + +/** + * Represents a HuggingFace model available through the router API + * @property id - The unique identifier of the model + * @property object - The object type (always "model") + * @property created - Unix timestamp of when the model was created + * @property owned_by - The organization that owns the model + * @property providers - List of providers that can serve this model + */ +export type HuggingFaceModel = z.infer + +/** + * HuggingFace API Response Schema + */ +const huggingFaceApiResponseSchema = z.object({ + object: z.string(), + data: z.array(huggingFaceModelSchema), +}) + +/** + * Represents the response from the HuggingFace router API + * @property object - The response object type + * @property data - Array of available models + */ +type HuggingFaceApiResponse = z.infer + +/** + * Cache entry for storing fetched models + * @property data - The cached model records + * @property timestamp - Unix timestamp of when the cache was last updated + */ +interface CacheEntry { + data: ModelRecord + rawModels?: HuggingFaceModel[] + timestamp: number +} + +let cache: CacheEntry | null = null + +/** + * Parse a HuggingFace model into ModelInfo format + * @param model - The HuggingFace model to parse + * @param provider - Optional specific provider to use for capabilities + * @returns ModelInfo object compatible with the application's model system + */ +function parseHuggingFaceModel(model: HuggingFaceModel, provider?: HuggingFaceProvider): ModelInfo { + // Use provider-specific values if available, otherwise find first provider with values + const contextLength = + provider?.context_length || + model.providers.find((p) => p.context_length)?.context_length || + HUGGINGFACE_DEFAULT_CONTEXT_WINDOW + + const pricing = provider?.pricing || model.providers.find((p) => p.pricing)?.pricing + + // Include provider name in description if specific provider is given + const description = provider ? `${model.id} via ${provider.provider}` : `${model.id} via HuggingFace` + + return { + maxTokens: Math.min(contextLength, HUGGINGFACE_DEFAULT_MAX_TOKENS), + contextWindow: contextLength, + supportsImages: false, // HuggingFace API doesn't provide this info yet + supportsPromptCache: false, + supportsComputerUse: false, + inputPrice: pricing?.input, + outputPrice: pricing?.output, + description, + } +} + +/** + * Fetches available models from HuggingFace + * + * @returns A promise that resolves to a record of model IDs to model info + * @throws Will throw an error if the request fails + */ +export async function getHuggingFaceModels(): Promise { + const now = Date.now() + + // Check cache + if (cache && now - cache.timestamp < HUGGINGFACE_CACHE_DURATION) { + return cache.data + } + + const models: ModelRecord = {} + + try { + const response = await axios.get(HUGGINGFACE_API_URL, { + headers: { + "Upgrade-Insecure-Requests": "1", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + Priority: "u=0, i", + Pragma: "no-cache", + "Cache-Control": "no-cache", + }, + timeout: 10000, // 10 second timeout + }) + + const result = huggingFaceApiResponseSchema.safeParse(response.data) + + if (!result.success) { + console.error("HuggingFace models response validation failed:", result.error.format()) + throw new Error("Invalid response format from HuggingFace API") + } + + const validModels = result.data.data.filter((model) => model.providers.length > 0) + + for (const model of validModels) { + // Add the base model + models[model.id] = parseHuggingFaceModel(model) + + // Add provider-specific variants for all live providers + for (const provider of model.providers) { + if (provider.status === "live") { + const providerKey = `${model.id}:${provider.provider}` + const providerModel = parseHuggingFaceModel(model, provider) + + // Always add provider variants to show all available providers + models[providerKey] = providerModel + } + } + } + + // Update cache + cache = { + data: models, + rawModels: validModels, + timestamp: now, + } + + return models + } catch (error) { + console.error("Error fetching HuggingFace models:", error) + + // Return cached data if available + if (cache) { + return cache.data + } + + // Re-throw with more context + if (axios.isAxiosError(error)) { + if (error.response) { + throw new Error( + `Failed to fetch HuggingFace models: ${error.response.status} ${error.response.statusText}`, + ) + } else if (error.request) { + throw new Error( + "Failed to fetch HuggingFace models: No response from server. Check your internet connection.", + ) + } + } + + throw new Error( + `Failed to fetch HuggingFace models: ${error instanceof Error ? error.message : "Unknown error"}`, + ) + } +} + +/** + * Get cached models without making an API request + */ +export function getCachedHuggingFaceModels(): ModelRecord | null { + return cache?.data || null +} + +/** + * Get cached raw models for UI display + */ +export function getCachedRawHuggingFaceModels(): HuggingFaceModel[] | null { + return cache?.rawModels || null +} + +/** + * Clear the cache + */ +export function clearHuggingFaceCache(): void { + cache = null +} + +/** + * HuggingFace Models Response Interface + */ +export interface HuggingFaceModelsResponse { + models: HuggingFaceModel[] + cached: boolean + timestamp: number +} + +/** + * Get HuggingFace models with response metadata + * This function provides a higher-level API that includes cache status and timestamp + */ +export async function getHuggingFaceModelsWithMetadata(): Promise { + try { + // First, trigger the fetch to populate cache + await getHuggingFaceModels() + + // Get the raw models from cache + const cachedRawModels = getCachedRawHuggingFaceModels() + + if (cachedRawModels) { + return { + models: cachedRawModels, + cached: true, + timestamp: Date.now(), + } + } + + // If no cached raw models, fetch directly from API + const response = await axios.get(HUGGINGFACE_API_URL, { + headers: { + "Upgrade-Insecure-Requests": "1", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + Priority: "u=0, i", + Pragma: "no-cache", + "Cache-Control": "no-cache", + }, + timeout: 10000, + }) + + const models = response.data?.data || [] + + return { + models, + cached: false, + timestamp: Date.now(), + } + } catch (error) { + console.error("Failed to get HuggingFace models:", error) + return { + models: [], + cached: false, + timestamp: Date.now(), + } + } +} diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 6765c8676d..5e547edbdc 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -4,6 +4,7 @@ import { type GenerateContentResponseUsageMetadata, type GenerateContentParameters, type GenerateContentConfig, + type GroundingMetadata, } from "@google/genai" import type { JWTInput } from "google-auth-library" @@ -13,6 +14,7 @@ import type { ApiHandlerOptions } from "../../shared/api" import { safeJsonParse } from "../../shared/safeJsonParse" import { convertAnthropicContentToGemini, convertAnthropicMessageToGemini } from "../transform/gemini-format" +import { t } from "i18next" import type { ApiStream } from "../transform/stream" import { getModelParams } from "../transform/model-params" @@ -67,72 +69,103 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl const contents = messages.map(convertAnthropicMessageToGemini) + const tools: GenerateContentConfig["tools"] = [] + if (this.options.enableUrlContext) { + tools.push({ urlContext: {} }) + } + + if (this.options.enableGrounding) { + tools.push({ googleSearch: {} }) + } + const config: GenerateContentConfig = { systemInstruction, httpOptions: this.options.googleGeminiBaseUrl ? { baseUrl: this.options.googleGeminiBaseUrl } : undefined, thinkingConfig, maxOutputTokens: this.options.modelMaxTokens ?? maxTokens ?? undefined, temperature: this.options.modelTemperature ?? 0, + ...(tools.length > 0 ? { tools } : {}), } const params: GenerateContentParameters = { model, contents, config } - const result = await this.client.models.generateContentStream(params) + try { + const result = await this.client.models.generateContentStream(params) - let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined + let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined + let pendingGroundingMetadata: GroundingMetadata | undefined - for await (const chunk of result) { - // Process candidates and their parts to separate thoughts from content - if (chunk.candidates && chunk.candidates.length > 0) { - const candidate = chunk.candidates[0] - if (candidate.content && candidate.content.parts) { - for (const part of candidate.content.parts) { - if (part.thought) { - // This is a thinking/reasoning part - if (part.text) { - yield { type: "reasoning", text: part.text } - } - } else { - // This is regular content - if (part.text) { - yield { type: "text", text: part.text } + for await (const chunk of result) { + // Process candidates and their parts to separate thoughts from content + if (chunk.candidates && chunk.candidates.length > 0) { + const candidate = chunk.candidates[0] + + if (candidate.groundingMetadata) { + pendingGroundingMetadata = candidate.groundingMetadata + } + + if (candidate.content && candidate.content.parts) { + for (const part of candidate.content.parts) { + if (part.thought) { + // This is a thinking/reasoning part + if (part.text) { + yield { type: "reasoning", text: part.text } + } + } else { + // This is regular content + if (part.text) { + yield { type: "text", text: part.text } + } } } } } + + // Fallback to the original text property if no candidates structure + else if (chunk.text) { + yield { type: "text", text: chunk.text } + } + + if (chunk.usageMetadata) { + lastUsageMetadata = chunk.usageMetadata + } } - // Fallback to the original text property if no candidates structure - else if (chunk.text) { - yield { type: "text", text: chunk.text } + if (pendingGroundingMetadata) { + const citations = this.extractCitationsOnly(pendingGroundingMetadata) + if (citations) { + yield { type: "text", text: `\n\n${t("common:errors.gemini.sources")} ${citations}` } + } } - if (chunk.usageMetadata) { - lastUsageMetadata = chunk.usageMetadata - } - } + if (lastUsageMetadata) { + const inputTokens = lastUsageMetadata.promptTokenCount ?? 0 + const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0 + const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount + const reasoningTokens = lastUsageMetadata.thoughtsTokenCount - if (lastUsageMetadata) { - const inputTokens = lastUsageMetadata.promptTokenCount ?? 0 - const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0 - const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount - const reasoningTokens = lastUsageMetadata.thoughtsTokenCount - - yield { - type: "usage", - inputTokens, - outputTokens, - cacheReadTokens, - reasoningTokens, - totalCost: this.calculateCost({ info, inputTokens, outputTokens, cacheReadTokens }), + yield { + type: "usage", + inputTokens, + outputTokens, + cacheReadTokens, + reasoningTokens, + totalCost: this.calculateCost({ info, inputTokens, outputTokens, cacheReadTokens }), + } } + } catch (error) { + if (error instanceof Error) { + throw new Error(t("common:errors.gemini.generate_stream", { error: error.message })) + } + + throw error } } override getModel() { const modelId = this.options.apiModelId let id = modelId && modelId in geminiModels ? (modelId as GeminiModelId) : geminiDefaultModelId - const info: ModelInfo = geminiModels[id] + let info: ModelInfo = geminiModels[id] const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options }) // The `:thinking` suffix indicates that the model is a "Hybrid" @@ -142,25 +175,69 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl return { id: id.endsWith(":thinking") ? id.replace(":thinking", "") : id, info, ...params } } + private extractCitationsOnly(groundingMetadata?: GroundingMetadata): string | null { + const chunks = groundingMetadata?.groundingChunks + + if (!chunks) { + return null + } + + const citationLinks = chunks + .map((chunk, i) => { + const uri = chunk.web?.uri + if (uri) { + return `[${i + 1}](${uri})` + } + return null + }) + .filter((link): link is string => link !== null) + + if (citationLinks.length > 0) { + return citationLinks.join(", ") + } + + return null + } + async completePrompt(prompt: string): Promise { try { const { id: model } = this.getModel() + const tools: GenerateContentConfig["tools"] = [] + if (this.options.enableUrlContext) { + tools.push({ urlContext: {} }) + } + if (this.options.enableGrounding) { + tools.push({ googleSearch: {} }) + } + const promptConfig: GenerateContentConfig = { + httpOptions: this.options.googleGeminiBaseUrl + ? { baseUrl: this.options.googleGeminiBaseUrl } + : undefined, + temperature: this.options.modelTemperature ?? 0, + ...(tools.length > 0 ? { tools } : {}), + } + const result = await this.client.models.generateContent({ model, contents: [{ role: "user", parts: [{ text: prompt }] }], - config: { - httpOptions: this.options.googleGeminiBaseUrl - ? { baseUrl: this.options.googleGeminiBaseUrl } - : undefined, - temperature: this.options.modelTemperature ?? 0, - }, + config: promptConfig, }) - return result.text ?? "" + let text = result.text ?? "" + + const candidate = result.candidates?.[0] + if (candidate?.groundingMetadata) { + const citations = this.extractCitationsOnly(candidate.groundingMetadata) + if (citations) { + text += `\n\n${t("common:errors.gemini.sources")} ${citations}` + } + } + + return text } catch (error) { if (error instanceof Error) { - throw new Error(`Gemini completion error: ${error.message}`) + throw new Error(t("common:errors.gemini.generate_complete_prompt", { error: error.message })) } throw error diff --git a/src/api/providers/huggingface.ts b/src/api/providers/huggingface.ts index 913605bd92..aa158654c9 100644 --- a/src/api/providers/huggingface.ts +++ b/src/api/providers/huggingface.ts @@ -1,16 +1,18 @@ import OpenAI from "openai" import { Anthropic } from "@anthropic-ai/sdk" -import type { ApiHandlerOptions } from "../../shared/api" +import type { ApiHandlerOptions, ModelRecord } from "../../shared/api" import { ApiStream } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" +import { getHuggingFaceModels, getCachedHuggingFaceModels } from "./fetchers/huggingface" export class HuggingFaceHandler extends BaseProvider implements SingleCompletionHandler { private client: OpenAI private options: ApiHandlerOptions + private modelCache: ModelRecord | null = null constructor(options: ApiHandlerOptions) { super() @@ -25,6 +27,20 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion apiKey: this.options.huggingFaceApiKey, defaultHeaders: DEFAULT_HEADERS, }) + + // Try to get cached models first + this.modelCache = getCachedHuggingFaceModels() + + // Fetch models asynchronously + this.fetchModels() + } + + private async fetchModels() { + try { + this.modelCache = await getHuggingFaceModels() + } catch (error) { + console.error("Failed to fetch HuggingFace models:", error) + } } override async *createMessage( @@ -43,6 +59,11 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion stream_options: { include_usage: true }, } + // Add max_tokens if specified + if (this.options.includeMaxTokens && this.options.modelMaxTokens) { + params.max_tokens = this.options.modelMaxTokens + } + const stream = await this.client.chat.completions.create(params) for await (const chunk of stream) { @@ -86,6 +107,18 @@ export class HuggingFaceHandler extends BaseProvider implements SingleCompletion override getModel() { const modelId = this.options.huggingFaceModelId || "meta-llama/Llama-3.3-70B-Instruct" + + // Try to get model info from cache + const modelInfo = this.modelCache?.[modelId] + + if (modelInfo) { + return { + id: modelId, + info: modelInfo, + } + } + + // Fallback to default values if model not found in cache return { id: modelId, info: { diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index e8cd58b12c..7cea7411fe 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -39,10 +39,70 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa ): ApiStream { const { id: modelId, info } = await this.fetchModel() - const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ - { role: "system", content: systemPrompt }, - ...convertToOpenAiMessages(messages), - ] + const openAiMessages = convertToOpenAiMessages(messages) + + // Prepare messages with cache control if enabled and supported + let systemMessage: OpenAI.Chat.ChatCompletionMessageParam + let enhancedMessages: OpenAI.Chat.ChatCompletionMessageParam[] + + if (this.options.litellmUsePromptCache && info.supportsPromptCache) { + // Create system message with cache control in the proper format + systemMessage = { + role: "system", + content: [ + { + type: "text", + text: systemPrompt, + cache_control: { type: "ephemeral" }, + } as any, + ], + } + + // Find the last two user messages to apply caching + const userMsgIndices = openAiMessages.reduce( + (acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), + [] as number[], + ) + const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1 + const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1 + + // Apply cache_control to the last two user messages + enhancedMessages = openAiMessages.map((message, index) => { + if ((index === lastUserMsgIndex || index === secondLastUserMsgIndex) && message.role === "user") { + // Handle both string and array content types + if (typeof message.content === "string") { + return { + ...message, + content: [ + { + type: "text", + text: message.content, + cache_control: { type: "ephemeral" }, + } as any, + ], + } + } else if (Array.isArray(message.content)) { + // Apply cache control to the last content item in the array + return { + ...message, + content: message.content.map((content, contentIndex) => + contentIndex === message.content.length - 1 + ? ({ + ...content, + cache_control: { type: "ephemeral" }, + } as any) + : content, + ), + } + } + } + return message + }) + } else { + // No cache control - use simple format + systemMessage = { role: "system", content: systemPrompt } + enhancedMessages = openAiMessages + } // Required by some providers; others default to max tokens allowed let maxTokens: number | undefined = info.maxTokens ?? undefined @@ -50,7 +110,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelId, max_tokens: maxTokens, - messages: openAiMessages, + messages: [systemMessage, ...enhancedMessages], stream: true, stream_options: { include_usage: true, @@ -80,20 +140,30 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa } if (lastUsage) { + // Extract cache-related information if available + // LiteLLM may use different field names for cache tokens + const cacheWriteTokens = + lastUsage.cache_creation_input_tokens || (lastUsage as any).prompt_cache_miss_tokens || 0 + const cacheReadTokens = + lastUsage.prompt_tokens_details?.cached_tokens || + (lastUsage as any).cache_read_input_tokens || + (lastUsage as any).prompt_cache_hit_tokens || + 0 + const usageData: ApiStreamUsageChunk = { type: "usage", inputTokens: lastUsage.prompt_tokens || 0, outputTokens: lastUsage.completion_tokens || 0, - cacheWriteTokens: lastUsage.cache_creation_input_tokens || 0, - cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens || 0, + cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined, + cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined, } usageData.totalCost = calculateApiCostOpenAI( info, usageData.inputTokens, usageData.outputTokens, - usageData.cacheWriteTokens, - usageData.cacheReadTokens, + usageData.cacheWriteTokens || 0, + usageData.cacheReadTokens || 0, ) yield usageData diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index c388c1a537..095ed86cb7 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -782,11 +782,12 @@ export class CustomModesManager { const filePath = path.join(modeRulesDir, entry.name) const content = await fs.readFile(filePath, "utf-8") if (content.trim()) { - // Calculate relative path based on mode source - const relativePath = isGlobalMode - ? path.relative(baseDir, filePath) - : path.relative(path.join(baseDir, ".roo"), filePath) - rulesFiles.push({ relativePath, content: content.trim() }) + // Calculate relative path from within the rules directory + // This excludes the rules-{slug} folder from the path + const relativePath = path.relative(modeRulesDir, filePath) + // Normalize path to use forward slashes for cross-platform compatibility + const normalizedRelativePath = relativePath.replace(/\\/g, '/') + rulesFiles.push({ relativePath: normalizedRelativePath, content: content.trim() }) } } } @@ -881,11 +882,21 @@ export class CustomModesManager { continue // Skip this file but continue with others } - const targetPath = path.join(baseDir, normalizedRelativePath) - const normalizedTargetPath = path.normalize(targetPath) - const expectedBasePath = path.normalize(baseDir) + // Check if path starts with a rules-* folder (old export format) + let cleanedRelativePath = normalizedRelativePath + const rulesMatch = normalizedRelativePath.match(/^rules-[^\/\\]+[\/\\]/) + if (rulesMatch) { + // Strip the entire rules-* folder reference for backwards compatibility + cleanedRelativePath = normalizedRelativePath.substring(rulesMatch[0].length) + logger.info(`Detected old export format, stripping ${rulesMatch[0]} from path`) + } - // Ensure the resolved path stays within the base directory + // Use the rules folder path instead of base directory + const targetPath = path.join(rulesFolderPath, cleanedRelativePath) + const normalizedTargetPath = path.normalize(targetPath) + const expectedBasePath = path.normalize(rulesFolderPath) + + // Ensure the resolved path stays within the rules folder if (!normalizedTargetPath.startsWith(expectedBasePath)) { logger.error(`Path traversal attempt detected: ${ruleFile.relativePath}`) continue // Skip this file but continue with others diff --git a/src/core/config/__tests__/CustomModesManager.exportImportSlugChange.spec.ts b/src/core/config/__tests__/CustomModesManager.exportImportSlugChange.spec.ts new file mode 100644 index 0000000000..3b7b0d1d9f --- /dev/null +++ b/src/core/config/__tests__/CustomModesManager.exportImportSlugChange.spec.ts @@ -0,0 +1,437 @@ +// npx vitest core/config/__tests__/CustomModesManager.exportImportSlugChange.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", () => ({ + 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") + +describe("CustomModesManager - Export/Import with Slug Changes", () => { + let manager: CustomModesManager + let mockContext: vscode.ExtensionContext + let mockOnUpdate: Mock + let mockWorkspaceFolders: { uri: { fsPath: string } }[] + + // 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 mockWorkspacePath = path.resolve("/mock/workspace") + const mockRoomodes = path.join(mockWorkspacePath, ".roomodes") + + 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 + + // 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(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: [] }) + } + + throw new Error("File not found") + }) + + manager = new CustomModesManager(mockContext, mockOnUpdate) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe("Export Path Calculation", () => { + it("should exclude rules-{slug} folder from exported relative paths", 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") && path.includes("rule1.md")) { + return "Rule 1 content" + } + if (path.includes("rules-test-mode") && path.includes("subfolder") && path.includes("rule2.md")) { + return "Rule 2 content" + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "rule1.md", isFile: () => true }, + { name: "subfolder", isFile: () => false, isDirectory: () => true }, + ]) + + const result = await manager.exportModeWithRules("test-mode") + + expect(result.success).toBe(true) + const exportData = yaml.parse(result.yaml!) + const rulesFiles = exportData.customModes[0].rulesFiles + + // Verify that paths do NOT include rules-test-mode folder + expect(rulesFiles).toBeDefined() + expect(rulesFiles.length).toBeGreaterThan(0) + + // Check that no path starts with rules-test-mode + rulesFiles.forEach((file: any) => { + expect(file.relativePath).not.toMatch(/^rules-test-mode[\/\\]/) + }) + + // Verify the actual paths are just the file names (without rules folder) + const paths = rulesFiles.map((f: any) => f.relativePath) + expect(paths).toContain("rule1.md") + }) + + it("should handle files at root level correctly", async () => { + const roomodesContent = { + customModes: [ + { + slug: "root-mode", + name: "Root Mode", + roleDefinition: "Root 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-root-mode") && path.includes("file1.md")) { + return "File 1 content" + } + if (path.includes("rules-root-mode") && path.includes("file2.md")) { + return "File 2 content" + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "file1.md", isFile: () => true }, + { name: "file2.md", isFile: () => true }, + { name: "subfolder", isFile: () => false }, // This will be ignored by current implementation + ]) + + const result = await manager.exportModeWithRules("root-mode") + + expect(result.success).toBe(true) + const exportData = yaml.parse(result.yaml!) + const rulesFiles = exportData.customModes[0].rulesFiles + + // Verify files are exported without rules-root-mode prefix + expect(rulesFiles).toBeDefined() + expect(rulesFiles.length).toBe(2) + + const paths = rulesFiles.map((f: any) => f.relativePath) + expect(paths).toContain("file1.md") + expect(paths).toContain("file2.md") + + // Verify no path contains the rules folder name + rulesFiles.forEach((file: any) => { + expect(file.relativePath).not.toContain("rules-root-mode") + }) + }) + }) + + describe("Import with Slug Changes", () => { + it("should place files in rules-{new-slug} folder when slug is changed", async () => { + // Import YAML with new format (no rules folder in path) + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "new-slug-name", // Changed slug + name: "Imported Mode", + roleDefinition: "Imported Role", + groups: ["read"], + rulesFiles: [ + { + relativePath: "rule1.md", // New format without rules folder + content: "Rule 1 content", + }, + { + relativePath: "subfolder/rule2.md", + content: "Rule 2 content", + }, + ], + }, + ], + }) + + let writtenFiles: Record = {} + let createdDirs: 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, content: string) => { + writtenFiles[path] = content + return Promise.resolve() + }) + ;(fs.mkdir as Mock).mockImplementation(async (path: string) => { + createdDirs.push(path) + return Promise.resolve() + }) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + + // Verify files were written to the correct new slug folder + const rule1Path = Object.keys(writtenFiles).find((p) => p.includes("rule1.md") && !p.includes(".roomodes")) + const rule2Path = Object.keys(writtenFiles).find((p) => p.includes("rule2.md") && !p.includes(".roomodes")) + + expect(rule1Path).toBeDefined() + expect(rule2Path).toBeDefined() + + // Check that files are in rules-new-slug-name folder + expect(rule1Path).toContain(path.join(".roo", "rules-new-slug-name", "rule1.md")) + expect(rule2Path).toContain(path.join(".roo", "rules-new-slug-name", "subfolder", "rule2.md")) + + // Verify directories were created with new slug + expect(createdDirs.some((dir) => dir.includes("rules-new-slug-name"))).toBe(true) + }) + + it("should handle old format (with rules-{slug} in path) for backwards compatibility", async () => { + // Import YAML with old format (includes rules folder in path) + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "new-slug-name", // Changed slug + name: "Imported Mode", + roleDefinition: "Imported Role", + groups: ["read"], + rulesFiles: [ + { + relativePath: "rules-old-slug/rule1.md", // Old format with rules folder + content: "Rule 1 content", + }, + { + relativePath: "rules-old-slug/subfolder/rule2.md", + content: "Rule 2 content", + }, + ], + }, + ], + }) + + let writtenFiles: Record = {} + + ;(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, content: string) => { + writtenFiles[path] = content + return Promise.resolve() + }) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + + // Verify files were written to the NEW slug folder, not the old one + const rule1Path = Object.keys(writtenFiles).find((p) => p.includes("rule1.md") && !p.includes(".roomodes")) + const rule2Path = Object.keys(writtenFiles).find((p) => p.includes("rule2.md") && !p.includes(".roomodes")) + + expect(rule1Path).toBeDefined() + expect(rule2Path).toBeDefined() + + // Check that files are in rules-new-slug-name folder (not rules-old-slug) + expect(rule1Path).toContain(path.join(".roo", "rules-new-slug-name", "rule1.md")) + expect(rule2Path).toContain(path.join(".roo", "rules-new-slug-name", "subfolder", "rule2.md")) + + // Ensure old slug folder was NOT created + expect(rule1Path).not.toContain("rules-old-slug") + expect(rule2Path).not.toContain("rules-old-slug") + }) + + it("should handle mixed format paths correctly", async () => { + // Import YAML with mixed formats + const importYaml = yaml.stringify({ + customModes: [ + { + slug: "mixed-mode", + name: "Mixed Mode", + roleDefinition: "Mixed Role", + groups: ["read"], + rulesFiles: [ + { + relativePath: "rules-old-slug/old-format.md", // Old format + content: "Old format content", + }, + { + relativePath: "new-format.md", // New format + content: "New format content", + }, + { + relativePath: "rules-another-old/nested/file.md", // Old format nested + content: "Nested old format", + }, + ], + }, + ], + }) + + let writtenFiles: Record = {} + + ;(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, content: string) => { + writtenFiles[path] = content + return Promise.resolve() + }) + + const result = await manager.importModeWithRules(importYaml) + + expect(result.success).toBe(true) + + // All files should be in rules-mixed-mode folder + const oldFormatPath = Object.keys(writtenFiles).find((p) => p.includes("old-format.md")) + const newFormatPath = Object.keys(writtenFiles).find((p) => p.includes("new-format.md")) + const nestedPath = Object.keys(writtenFiles).find((p) => p.includes(path.join("nested", "file.md"))) + + expect(oldFormatPath).toContain(path.join(".roo", "rules-mixed-mode", "old-format.md")) + expect(newFormatPath).toContain(path.join(".roo", "rules-mixed-mode", "new-format.md")) + expect(nestedPath).toContain(path.join(".roo", "rules-mixed-mode", "nested", "file.md")) + }) + }) + + describe("End-to-End Export/Import with Slug Change", () => { + it("should successfully export and re-import with a different slug", async () => { + // Step 1: Set up a mode with rules + const originalMode = { + slug: "original-mode", + name: "Original Mode", + roleDefinition: "Original 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({ customModes: [originalMode] }) + } + if (path.includes("rules-original-mode") && path.includes("rule.md")) { + return "Original rule content" + } + 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: "rule.md", isFile: () => true }]) + + // Step 2: Export the mode + const exportResult = await manager.exportModeWithRules("original-mode") + expect(exportResult.success).toBe(true) + + // Step 3: Modify the exported YAML to change the slug + const exportData = yaml.parse(exportResult.yaml!) + exportData.customModes[0].slug = "renamed-mode" + exportData.customModes[0].name = "Renamed Mode" + const modifiedYaml = yaml.stringify(exportData) + + // Step 4: Import with the new slug + let writtenFiles: Record = {} + ;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => { + writtenFiles[path] = content + return Promise.resolve() + }) + + const importResult = await manager.importModeWithRules(modifiedYaml) + expect(importResult.success).toBe(true) + + // Step 5: Verify the rule file was placed in the new slug folder + const ruleFilePath = Object.keys(writtenFiles).find( + (p) => p.includes("rule.md") && !p.includes(".roomodes"), + ) + expect(ruleFilePath).toBeDefined() + expect(ruleFilePath).toContain(path.join(".roo", "rules-renamed-mode", "rule.md")) + expect(ruleFilePath).not.toContain("rules-original-mode") + + // Verify content was preserved + expect(writtenFiles[ruleFilePath!]).toBe("Original rule content") + }) + }) +}) diff --git a/src/core/config/__tests__/CustomModesManager.spec.ts b/src/core/config/__tests__/CustomModesManager.spec.ts index 682696fd03..b48ea7b65b 100644 --- a/src/core/config/__tests__/CustomModesManager.spec.ts +++ b/src/core/config/__tests__/CustomModesManager.spec.ts @@ -1699,5 +1699,62 @@ describe("CustomModesManager", () => { expect(result.yaml).toContain("global-test-mode") expect(result.yaml).toContain("Global rule content") }) + + it("should normalize paths to use forward slashes in exported YAML", 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")) { + return "Rule content" + } + throw new Error("File not found") + }) + ;(fs.stat as Mock).mockResolvedValue({ isDirectory: () => true }) + + // Mock readdir to return entries with subdirectories + ;(fs.readdir as Mock).mockResolvedValue([ + { name: "rule1.md", isFile: () => true }, + { name: "rule2.md", isFile: () => true }, + ]) + + const result = await manager.exportModeWithRules("test-mode") + + expect(result.success).toBe(true) + + // Parse the YAML to check the paths + const exportedData = yaml.parse(result.yaml!) + const rulesFiles = exportedData.customModes[0].rulesFiles + + // Verify that all paths use forward slashes + expect(rulesFiles).toBeDefined() + expect(rulesFiles.length).toBe(2) + + // Check that all paths use forward slashes and do NOT include the rules-{slug} prefix + rulesFiles.forEach((file: any) => { + expect(file.relativePath).not.toContain("\\") + // The PR excludes the rules-{slug} folder from paths + expect(file.relativePath).not.toMatch(/^rules-test-mode\//) + // Files should be at the root level now + expect(file.relativePath).toMatch(/^rule\d+\.md$/) + }) + + // Ensure no backslashes in the entire exported YAML + expect(result.yaml).not.toContain("\\") + }) }) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index b83b37c75b..5a0a15962c 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -58,7 +58,8 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo const maxTabs = maxOpenTabsContext ?? 20 const openTabPaths = vscode.window.tabGroups.all .flatMap((group) => group.tabs) - .map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath) + .filter((tab) => tab.input instanceof vscode.TabInputText) + .map((tab) => (tab.input as vscode.TabInputText).uri.fsPath) .filter(Boolean) .map((absolutePath) => path.relative(cline.cwd, absolutePath).toPosix()) .slice(0, maxTabs) diff --git a/src/core/mentions/__tests__/processUserContentMentions.spec.ts b/src/core/mentions/__tests__/processUserContentMentions.spec.ts new file mode 100644 index 0000000000..3aebd66e53 --- /dev/null +++ b/src/core/mentions/__tests__/processUserContentMentions.spec.ts @@ -0,0 +1,353 @@ +// npx vitest core/mentions/__tests__/processUserContentMentions.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" +import { processUserContentMentions } from "../processUserContentMentions" +import { parseMentions } from "../index" +import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher" +import { FileContextTracker } from "../../context-tracking/FileContextTracker" + +// Mock the parseMentions function +vi.mock("../index", () => ({ + parseMentions: vi.fn(), +})) + +describe("processUserContentMentions", () => { + let mockUrlContentFetcher: UrlContentFetcher + let mockFileContextTracker: FileContextTracker + let mockRooIgnoreController: any + + beforeEach(() => { + vi.clearAllMocks() + + mockUrlContentFetcher = {} as UrlContentFetcher + mockFileContextTracker = {} as FileContextTracker + mockRooIgnoreController = {} + + // Default mock implementation + vi.mocked(parseMentions).mockImplementation(async (text) => `parsed: ${text}`) + }) + + describe("maxReadFileLine parameter", () => { + it("should pass maxReadFileLine to parseMentions when provided", async () => { + const userContent = [ + { + type: "text" as const, + text: "Read file with limit", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + rooIgnoreController: mockRooIgnoreController, + maxReadFileLine: 100, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Read file with limit", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + mockRooIgnoreController, + true, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages + 100, + ) + }) + + it("should pass undefined maxReadFileLine when not provided", async () => { + const userContent = [ + { + type: "text" as const, + text: "Read file without limit", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + rooIgnoreController: mockRooIgnoreController, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Read file without limit", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + mockRooIgnoreController, + true, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages + undefined, + ) + }) + + it("should handle UNLIMITED_LINES constant correctly", async () => { + const userContent = [ + { + type: "text" as const, + text: "Read unlimited lines", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + rooIgnoreController: mockRooIgnoreController, + maxReadFileLine: -1, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Read unlimited lines", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + mockRooIgnoreController, + true, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages + -1, + ) + }) + }) + + describe("content processing", () => { + it("should process text blocks with tags", async () => { + const userContent = [ + { + type: "text" as const, + text: "Do something", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalled() + expect(result[0]).toEqual({ + type: "text", + text: "parsed: Do something", + }) + }) + + it("should process text blocks with tags", async () => { + const userContent = [ + { + type: "text" as const, + text: "Fix this issue", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalled() + expect(result[0]).toEqual({ + type: "text", + text: "parsed: Fix this issue", + }) + }) + + it("should not process text blocks without task or feedback tags", async () => { + const userContent = [ + { + type: "text" as const, + text: "Regular text without special tags", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).not.toHaveBeenCalled() + expect(result[0]).toEqual(userContent[0]) + }) + + it("should process tool_result blocks with string content", async () => { + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "123", + content: "Tool feedback", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalled() + expect(result[0]).toEqual({ + type: "tool_result", + tool_use_id: "123", + content: "parsed: Tool feedback", + }) + }) + + it("should process tool_result blocks with array content", async () => { + const userContent = [ + { + type: "tool_result" as const, + tool_use_id: "123", + content: [ + { + type: "text" as const, + text: "Array task", + }, + { + type: "text" as const, + text: "Regular text", + }, + ], + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalledTimes(1) + expect(result[0]).toEqual({ + type: "tool_result", + tool_use_id: "123", + content: [ + { + type: "text", + text: "parsed: Array task", + }, + { + type: "text", + text: "Regular text", + }, + ], + }) + }) + + it("should handle mixed content types", async () => { + const userContent = [ + { + type: "text" as const, + text: "First task", + }, + { + type: "image" as const, + source: { + type: "base64" as const, + media_type: "image/png" as const, + data: "base64data", + }, + }, + { + type: "tool_result" as const, + tool_use_id: "456", + content: "Feedback", + }, + ] + + const result = await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + maxReadFileLine: 50, + }) + + expect(parseMentions).toHaveBeenCalledTimes(2) + expect(result).toHaveLength(3) + expect(result[0]).toEqual({ + type: "text", + text: "parsed: First task", + }) + expect(result[1]).toEqual(userContent[1]) // Image block unchanged + expect(result[2]).toEqual({ + type: "tool_result", + tool_use_id: "456", + content: "parsed: Feedback", + }) + }) + }) + + describe("showRooIgnoredFiles parameter", () => { + it("should default showRooIgnoredFiles to true", async () => { + const userContent = [ + { + type: "text" as const, + text: "Test default", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Test default", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + undefined, + true, // showRooIgnoredFiles should default to true + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages + undefined, + ) + }) + + it("should respect showRooIgnoredFiles when explicitly set to false", async () => { + const userContent = [ + { + type: "text" as const, + text: "Test explicit false", + }, + ] + + await processUserContentMentions({ + userContent, + cwd: "/test", + urlContentFetcher: mockUrlContentFetcher, + fileContextTracker: mockFileContextTracker, + showRooIgnoredFiles: false, + }) + + expect(parseMentions).toHaveBeenCalledWith( + "Test explicit false", + "/test", + mockUrlContentFetcher, + mockFileContextTracker, + undefined, + false, + true, // includeDiagnosticMessages + 50, // maxDiagnosticMessages + undefined, + ) + }) + }) +}) diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 7ce54b984e..b6a9dd4d0d 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -4,7 +4,7 @@ import * as path from "path" import * as vscode from "vscode" import { isBinaryFile } from "isbinaryfile" -import { mentionRegexGlobal, unescapeSpaces } from "../../shared/context-mentions" +import { mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "../../shared/context-mentions" import { getCommitInfo, getWorkingState } from "../../utils/git" import { getWorkspacePath } from "../../utils/path" @@ -18,6 +18,7 @@ import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher" import { FileContextTracker } from "../context-tracking/FileContextTracker" import { RooIgnoreController } from "../ignore/RooIgnoreController" +import { getCommand } from "../../services/command/commands" import { t } from "../../i18n" @@ -82,9 +83,19 @@ export async function parseMentions( showRooIgnoredFiles: boolean = true, includeDiagnosticMessages: boolean = true, maxDiagnosticMessages: number = 50, + maxReadFileLine?: number, ): Promise { const mentions: Set = new Set() - let parsedText = text.replace(mentionRegexGlobal, (match, mention) => { + const commandMentions: Set = new Set() + + // First pass: extract command mentions (starting with /) + let parsedText = text.replace(commandRegexGlobal, (match, commandName) => { + commandMentions.add(commandName) + return `Command '${commandName}' (see below for command content)` + }) + + // Second pass: handle regular mentions + parsedText = parsedText.replace(mentionRegexGlobal, (match, mention) => { mentions.add(mention) if (mention.startsWith("http")) { return `'${mention}' (see below for site content)` @@ -149,7 +160,13 @@ export async function parseMentions( } else if (mention.startsWith("/")) { const mentionPath = mention.slice(1) try { - const content = await getFileOrFolderContent(mentionPath, cwd, rooIgnoreController, showRooIgnoredFiles) + const content = await getFileOrFolderContent( + mentionPath, + cwd, + rooIgnoreController, + showRooIgnoredFiles, + maxReadFileLine, + ) if (mention.endsWith("/")) { parsedText += `\n\n\n${content}\n` } else { @@ -196,6 +213,25 @@ export async function parseMentions( } } + // Process command mentions + for (const commandName of commandMentions) { + try { + const command = await getCommand(cwd, commandName) + if (command) { + let commandOutput = "" + if (command.description) { + commandOutput += `Description: ${command.description}\n\n` + } + commandOutput += command.content + parsedText += `\n\n\n${commandOutput}\n` + } else { + parsedText += `\n\n\nCommand '${commandName}' not found. Available commands can be found in .roo/commands/ or ~/.roo/commands/\n` + } + } catch (error) { + parsedText += `\n\n\nError loading command '${commandName}': ${error.message}\n` + } + } + if (urlMention) { try { await urlContentFetcher.closeBrowser() @@ -212,6 +248,7 @@ async function getFileOrFolderContent( cwd: string, rooIgnoreController?: any, showRooIgnoredFiles: boolean = true, + maxReadFileLine?: number, ): Promise { const unescapedPath = unescapeSpaces(mentionPath) const absPath = path.resolve(cwd, unescapedPath) @@ -224,7 +261,7 @@ async function getFileOrFolderContent( return `(File ${mentionPath} is ignored by .rooignore)` } try { - const content = await extractTextFromFile(absPath) + const content = await extractTextFromFile(absPath, maxReadFileLine) return content } catch (error) { return `(Failed to read contents of ${mentionPath}): ${error.message}` @@ -264,7 +301,7 @@ async function getFileOrFolderContent( if (isBinary) { return undefined } - const content = await extractTextFromFile(absoluteFilePath) + const content = await extractTextFromFile(absoluteFilePath, maxReadFileLine) return `\n${content}\n` } catch (error) { return undefined diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts index 0649c4bc3c..b903e74396 100644 --- a/src/core/mentions/processUserContentMentions.ts +++ b/src/core/mentions/processUserContentMentions.ts @@ -15,6 +15,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles = true, includeDiagnosticMessages = true, maxDiagnosticMessages = 50, + maxReadFileLine, }: { userContent: Anthropic.Messages.ContentBlockParam[] cwd: string @@ -24,6 +25,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles?: boolean includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number + maxReadFileLine?: number }) { // Process userContent array, which contains various block types: // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam. @@ -37,7 +39,11 @@ export async function processUserContentMentions({ // should parse mentions). return Promise.all( userContent.map(async (block) => { - const shouldProcessMentions = (text: string) => text.includes("") || text.includes("") + const shouldProcessMentions = (text: string) => + text.includes("") || + text.includes("") || + text.includes("") || + text.includes("") if (block.type === "text") { if (shouldProcessMentions(block.text)) { @@ -52,6 +58,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ), } } @@ -71,6 +78,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ), } } @@ -91,6 +99,7 @@ export async function processUserContentMentions({ showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, ), } } diff --git a/src/core/sliding-window/__tests__/sliding-window.spec.ts b/src/core/sliding-window/__tests__/sliding-window.spec.ts index 393d50307e..0f2c70c81b 100644 --- a/src/core/sliding-window/__tests__/sliding-window.spec.ts +++ b/src/core/sliding-window/__tests__/sliding-window.spec.ts @@ -250,7 +250,6 @@ describe("Sliding Window", () => { { role: "assistant", content: "Fourth message" }, { role: "user", content: "Fifth message" }, ] - it("should not truncate if tokens are below max tokens threshold", async () => { const modelInfo = createModelInfo(100000, 30000) const dynamicBuffer = modelInfo.contextWindow * TOKEN_BUFFER_PERCENTAGE // 10000 diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index 1759a72f47..7b93b5c14a 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -18,6 +18,7 @@ export type TaskMetadataOptions = { taskNumber: number globalStoragePath: string workspace: string + mode?: string } export async function taskMetadata({ @@ -26,6 +27,7 @@ export async function taskMetadata({ taskNumber, globalStoragePath, workspace, + mode, }: TaskMetadataOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) @@ -92,6 +94,7 @@ export async function taskMetadata({ totalCost: tokenUsage.totalCost, size: taskDirSize, workspace, + mode, } return { historyItem, tokenUsage } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 95d12f66aa..edbde32ea7 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -137,6 +137,49 @@ export class Task extends EventEmitter { readonly parentTask: Task | undefined = undefined readonly taskNumber: number readonly workspacePath: string + /** + * The mode associated with this task. Persisted across sessions + * to maintain user context when reopening tasks from history. + * + * ## Lifecycle + * + * ### For new tasks: + * 1. Initially `undefined` during construction + * 2. Asynchronously initialized from provider state via `initializeTaskMode()` + * 3. Falls back to `defaultModeSlug` if provider state is unavailable + * + * ### For history items: + * 1. Immediately set from `historyItem.mode` during construction + * 2. Falls back to `defaultModeSlug` if mode is not stored in history + * + * ## Important + * This property should NOT be accessed directly until `taskModeReady` promise resolves. + * Use `getTaskMode()` for async access or `taskMode` getter for sync access after initialization. + * + * @private + * @see {@link getTaskMode} - For safe async access + * @see {@link taskMode} - For sync access after initialization + * @see {@link waitForModeInitialization} - To ensure initialization is complete + */ + private _taskMode: string | undefined + + /** + * Promise that resolves when the task mode has been initialized. + * This ensures async mode initialization completes before the task is used. + * + * ## Purpose + * - Prevents race conditions when accessing task mode + * - Ensures provider state is properly loaded before mode-dependent operations + * - Provides a synchronization point for async initialization + * + * ## Resolution timing + * - For history items: Resolves immediately (sync initialization) + * - For new tasks: Resolves after provider state is fetched (async initialization) + * + * @private + * @see {@link waitForModeInitialization} - Public method to await this promise + */ + private taskModeReady: Promise providerRef: WeakRef private readonly globalStoragePath: string @@ -268,9 +311,16 @@ export class Task extends EventEmitter { this.parentTask = parentTask this.taskNumber = taskNumber + // Store the task's mode when it's created + // For history items, use the stored mode; for new tasks, we'll set it after getting state if (historyItem) { + this._taskMode = historyItem.mode || defaultModeSlug + this.taskModeReady = Promise.resolve() TelemetryService.instance.captureTaskRestarted(this.taskId) } else { + // For new tasks, don't set the mode yet - wait for async initialization + this._taskMode = undefined + this.taskModeReady = this.initializeTaskMode(provider) TelemetryService.instance.captureTaskCreated(this.taskId) } @@ -307,6 +357,129 @@ export class Task extends EventEmitter { } } + /** + * Initialize the task mode from the provider state. + * This method handles async initialization with proper error handling. + * + * ## Flow + * 1. Attempts to fetch the current mode from provider state + * 2. Sets `_taskMode` to the fetched mode or `defaultModeSlug` if unavailable + * 3. Handles errors gracefully by falling back to default mode + * 4. Logs any initialization errors for debugging + * + * ## Error handling + * - Network failures when fetching provider state + * - Provider not yet initialized + * - Invalid state structure + * + * All errors result in fallback to `defaultModeSlug` to ensure task can proceed. + * + * @private + * @param provider - The ClineProvider instance to fetch state from + * @returns Promise that resolves when initialization is complete + */ + private async initializeTaskMode(provider: ClineProvider): Promise { + try { + const state = await provider.getState() + this._taskMode = state?.mode || defaultModeSlug + } catch (error) { + // If there's an error getting state, use the default mode + this._taskMode = defaultModeSlug + // Use the provider's log method for better error visibility + const errorMessage = `Failed to initialize task mode: ${error instanceof Error ? error.message : String(error)}` + provider.log(errorMessage) + } + } + + /** + * Wait for the task mode to be initialized before proceeding. + * This method ensures that any operations depending on the task mode + * will have access to the correct mode value. + * + * ## When to use + * - Before accessing mode-specific configurations + * - When switching between tasks with different modes + * - Before operations that depend on mode-based permissions + * + * ## Example usage + * ```typescript + * // Wait for mode initialization before mode-dependent operations + * await task.waitForModeInitialization(); + * const mode = task.taskMode; // Now safe to access synchronously + * + * // Or use with getTaskMode() for a one-liner + * const mode = await task.getTaskMode(); // Internally waits for initialization + * ``` + * + * @returns Promise that resolves when the task mode is initialized + * @public + */ + public async waitForModeInitialization(): Promise { + return this.taskModeReady + } + + /** + * Get the task mode asynchronously, ensuring it's properly initialized. + * This is the recommended way to access the task mode as it guarantees + * the mode is available before returning. + * + * ## Async behavior + * - Internally waits for `taskModeReady` promise to resolve + * - Returns the initialized mode or `defaultModeSlug` as fallback + * - Safe to call multiple times - subsequent calls return immediately if already initialized + * + * ## Example usage + * ```typescript + * // Safe async access + * const mode = await task.getTaskMode(); + * console.log(`Task is running in ${mode} mode`); + * + * // Use in conditional logic + * if (await task.getTaskMode() === 'architect') { + * // Perform architect-specific operations + * } + * ``` + * + * @returns Promise resolving to the task mode string + * @public + */ + public async getTaskMode(): Promise { + await this.taskModeReady + return this._taskMode || defaultModeSlug + } + + /** + * Get the task mode synchronously. This should only be used when you're certain + * that the mode has already been initialized (e.g., after waitForModeInitialization). + * + * ## When to use + * - In synchronous contexts where async/await is not available + * - After explicitly waiting for initialization via `waitForModeInitialization()` + * - In event handlers or callbacks where mode is guaranteed to be initialized + * + * ## Example usage + * ```typescript + * // After ensuring initialization + * await task.waitForModeInitialization(); + * const mode = task.taskMode; // Safe synchronous access + * + * // In an event handler after task is started + * task.on('taskStarted', () => { + * console.log(`Task started in ${task.taskMode} mode`); // Safe here + * }); + * ``` + * + * @throws {Error} If the mode hasn't been initialized yet + * @returns The task mode string + * @public + */ + public get taskMode(): string { + if (this._taskMode === undefined) { + throw new Error("Task mode accessed before initialization. Use getTaskMode() or wait for taskModeReady.") + } + return this._taskMode + } + static create(options: TaskOptions): [Task, Promise] { const instance = new Task({ ...options, startTask: false }) const { images, task, historyItem } = options @@ -411,6 +584,7 @@ export class Task extends EventEmitter { taskNumber: this.taskNumber, globalStoragePath: this.globalStoragePath, workspace: this.cwd, + mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode }) this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage) @@ -1230,6 +1404,7 @@ export class Task extends EventEmitter { showRooIgnoredFiles = true, includeDiagnosticMessages = true, maxDiagnosticMessages = 50, + maxReadFileLine = -1, } = (await this.providerRef.deref()?.getState()) ?? {} const parsedUserContent = await processUserContentMentions({ @@ -1241,6 +1416,7 @@ export class Task extends EventEmitter { showRooIgnoredFiles, includeDiagnosticMessages, maxDiagnosticMessages, + maxReadFileLine, }) const environmentDetails = await getEnvironmentDetails(this, includeFileDetails) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 747e954f85..c15422ae0c 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -11,6 +11,7 @@ import { ReadFileToolUse, ToolParamName, ToolResponse } from "../../../shared/to import { readFileTool } from "../readFileTool" import { formatResponse } from "../../prompts/responses" import * as contextValidatorModule from "../contextValidator" +import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB } from "../helpers/imageHelpers" vi.mock("../../../i18n", () => ({ t: vi.fn((key: string) => key), @@ -25,11 +26,7 @@ vi.mock("path", async () => { } }) -vi.mock("fs/promises", () => ({ - mkdir: vi.fn().mockResolvedValue(undefined), - writeFile: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn().mockResolvedValue("{}"), -})) +// Already mocked above with hoisted fsPromises vi.mock("isbinaryfile") @@ -37,11 +34,22 @@ vi.mock("../../../integrations/misc/line-counter") vi.mock("../../../integrations/misc/read-lines") vi.mock("../contextValidator") +// Mock fs/promises readFile for image tests +const fsPromises = vi.hoisted(() => ({ + readFile: vi.fn(), + stat: vi.fn().mockResolvedValue({ size: 1024 }), +})) +vi.mock("fs/promises", () => fsPromises) + // Mock input content for tests let mockInputContent = "" // First create all the mocks -vi.mock("../../../integrations/misc/extract-text") +vi.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: vi.fn(), + addLineNumbers: vi.fn(), + getSupportedBinaryFormats: vi.fn(() => [".pdf", ".docx", ".ipynb"]), +})) vi.mock("../../../services/tree-sitter") // Then create the mock functions @@ -54,6 +62,53 @@ const addLineNumbersMock = vi.fn().mockImplementation((text, startLine = 1) => { const extractTextFromFileMock = vi.fn() const getSupportedBinaryFormatsMock = vi.fn(() => [".pdf", ".docx", ".ipynb"]) +// Mock formatResponse - use vi.hoisted to ensure mocks are available before vi.mock +const { toolResultMock, imageBlocksMock } = vi.hoisted(() => { + const toolResultMock = vi.fn((text: string, images?: string[]) => { + if (images && images.length > 0) { + return [ + { type: "text", text }, + ...images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }), + ] + } + return text + }) + const imageBlocksMock = vi.fn((images?: string[]) => { + return images + ? images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }) + : [] + }) + return { toolResultMock, imageBlocksMock } +}) + +vi.mock("../../prompts/responses", () => ({ + formatResponse: { + toolDenied: vi.fn(() => "The user denied this operation."), + toolDeniedWithFeedback: vi.fn( + (feedback?: string) => + `The user denied this operation and provided the following feedback:\n\n${feedback}\n`, + ), + toolApprovedWithFeedback: vi.fn( + (feedback?: string) => + `The user approved this operation and provided the following context:\n\n${feedback}\n`, + ), + rooIgnoreError: vi.fn( + (path: string) => + `Access to ${path} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.`, + ), + toolResult: toolResultMock, + imageBlocks: imageBlocksMock, + }, +})) + vi.mock("../../ignore/RooIgnoreController", () => ({ RooIgnoreController: class { initialize() { @@ -69,6 +124,109 @@ vi.mock("../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockReturnValue(true), })) +// Global beforeEach to ensure clean mock state between all test suites +beforeEach(() => { + // NOTE: Removed vi.clearAllMocks() to prevent interference with setImageSupport calls + // Instead, individual suites clear their specific mocks to maintain isolation + + // Explicitly reset the hoisted mock implementations to prevent cross-suite pollution + toolResultMock.mockImplementation((text: string, images?: string[]) => { + if (images && images.length > 0) { + return [ + { type: "text", text }, + ...images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }), + ] + } + return text + }) + + imageBlocksMock.mockImplementation((images?: string[]) => { + return images + ? images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }) + : [] + }) +}) + +// Mock i18n translation function +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string, params?: Record) => { + // Map translation keys to English text + const translations: Record = { + "tools:readFile.imageWithSize": "Image file ({{size}} KB)", + "tools:readFile.imageTooLarge": + "Image file is too large ({{size}}). The maximum allowed size is {{max}} MB.", + "tools:readFile.linesRange": " (lines {{start}}-{{end}})", + "tools:readFile.definitionsOnly": " (definitions only)", + "tools:readFile.maxLines": " (max {{max}} lines)", + } + + let result = translations[key] || key + + // Simple template replacement + if (params) { + Object.entries(params).forEach(([param, value]) => { + result = result.replace(new RegExp(`{{${param}}}`, "g"), String(value)) + }) + } + + return result + }), +})) + +// Shared mock setup function to ensure consistent state across all test suites +function createMockCline(): any { + const mockProvider = { + getState: vi.fn(), + deref: vi.fn().mockReturnThis(), + } + + const mockCline: any = { + cwd: "/", + task: "Test", + providerRef: mockProvider, + rooIgnoreController: { + validateAccess: vi.fn().mockReturnValue(true), + }, + say: vi.fn().mockResolvedValue(undefined), + ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), + presentAssistantMessage: vi.fn(), + handleError: vi.fn().mockResolvedValue(undefined), + pushToolResult: vi.fn(), + removeClosingTag: vi.fn((tag, content) => content), + fileContextTracker: { + trackFileContext: vi.fn().mockResolvedValue(undefined), + }, + recordToolUsage: vi.fn().mockReturnValue(undefined), + recordToolError: vi.fn().mockReturnValue(undefined), + didRejectTool: false, + // CRITICAL: Always ensure image support is enabled + api: { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: true }, + }), + }, + } + + return { mockCline, mockProvider } +} + +// Helper function to set image support without affecting shared state +function setImageSupport(mockCline: any, supportsImages: boolean | undefined): void { + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages }, + }), + } +} + describe("read_file tool with maxReadFileLine setting", () => { // Test data const testFilePath = "test/file.txt" @@ -86,12 +244,27 @@ describe("read_file tool with maxReadFileLine setting", () => { const mockedIsBinaryFile = vi.mocked(isBinaryFile) const mockedPathResolve = vi.mocked(path.resolve) - const mockCline: any = {} + let mockCline: any let mockProvider: any let toolResult: ToolResponse | undefined beforeEach(() => { - vi.clearAllMocks() + // Clear specific mocks (not all mocks to preserve shared state) + mockedCountFileLines.mockClear() + mockedExtractTextFromFile.mockClear() + mockedIsBinaryFile.mockClear() + mockedPathResolve.mockClear() + addLineNumbersMock.mockClear() + extractTextFromFileMock.mockClear() + toolResultMock.mockClear() + + // Use shared mock setup function + const mocks = createMockCline() + mockCline = mocks.mockCline + mockProvider = mocks.mockProvider + + // Explicitly disable image support for text file tests to prevent cross-suite pollution + setImageSupport(mockCline, false) mockedPathResolve.mockReturnValue(absoluteFilePath) mockedIsBinaryFile.mockResolvedValue(false) @@ -114,31 +287,6 @@ describe("read_file tool with maxReadFileLine setting", () => { return Promise.resolve(addLineNumbersMock(mockInputContent)) }) - mockProvider = { - getState: vi.fn(), - deref: vi.fn().mockReturnThis(), - } - - mockCline.cwd = "/" - mockCline.task = "Test" - mockCline.providerRef = mockProvider - mockCline.rooIgnoreController = { - validateAccess: vi.fn().mockReturnValue(true), - } - mockCline.say = vi.fn().mockResolvedValue(undefined) - mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) - mockCline.presentAssistantMessage = vi.fn() - mockCline.handleError = vi.fn().mockResolvedValue(undefined) - mockCline.pushToolResult = vi.fn() - mockCline.removeClosingTag = vi.fn((tag, content) => content) - - mockCline.fileContextTracker = { - trackFileContext: vi.fn().mockResolvedValue(undefined), - } - - mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) - mockCline.recordToolError = vi.fn().mockReturnValue(undefined) - toolResult = undefined }) @@ -160,7 +308,7 @@ describe("read_file tool with maxReadFileLine setting", () => { const maxReadFileLine = options.maxReadFileLine ?? 500 const totalLines = options.totalLines ?? 5 - mockProvider.getState.mockResolvedValue({ maxReadFileLine }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageSize: 20 }) mockedCountFileLines.mockResolvedValue(totalLines) // Reset the spy before each test @@ -351,13 +499,38 @@ describe("read_file tool XML output structure", () => { const mockedExtractTextFromFile = vi.mocked(extractTextFromFile) const mockedIsBinaryFile = vi.mocked(isBinaryFile) const mockedPathResolve = vi.mocked(path.resolve) + const mockedFsReadFile = vi.mocked(fsPromises.readFile) + const imageBuffer = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ) - const mockCline: any = {} + let mockCline: any let mockProvider: any let toolResult: ToolResponse | undefined beforeEach(() => { - vi.clearAllMocks() + // Clear specific mocks (not all mocks to preserve shared state) + mockedCountFileLines.mockClear() + mockedExtractTextFromFile.mockClear() + mockedIsBinaryFile.mockClear() + mockedPathResolve.mockClear() + addLineNumbersMock.mockClear() + extractTextFromFileMock.mockClear() + toolResultMock.mockClear() + + // CRITICAL: Reset fsPromises mocks to prevent cross-test contamination + fsPromises.stat.mockClear() + fsPromises.stat.mockResolvedValue({ size: 1024 }) + fsPromises.readFile.mockClear() + + // Use shared mock setup function + const mocks = createMockCline() + mockCline = mocks.mockCline + mockProvider = mocks.mockProvider + + // Explicitly enable image support for this test suite (contains image memory tests) + setImageSupport(mockCline, true) mockedPathResolve.mockReturnValue(absoluteFilePath) mockedIsBinaryFile.mockResolvedValue(false) @@ -370,30 +543,11 @@ describe("read_file tool XML output structure", () => { mockInputContent = fileContent // Setup mock provider with default maxReadFileLine - mockProvider = { - getState: vi.fn().mockResolvedValue({ maxReadFileLine: -1 }), // Default to full file read - deref: vi.fn().mockReturnThis(), - } + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageSize: 20 }) // Default to full file read - mockCline.cwd = "/" - mockCline.task = "Test" - mockCline.providerRef = mockProvider - mockCline.rooIgnoreController = { - validateAccess: vi.fn().mockReturnValue(true), - } - mockCline.say = vi.fn().mockResolvedValue(undefined) - mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) - mockCline.presentAssistantMessage = vi.fn() + // Add additional properties needed for XML tests mockCline.sayAndCreateMissingParamError = vi.fn().mockResolvedValue("Missing required parameter") - mockCline.fileContextTracker = { - trackFileContext: vi.fn().mockResolvedValue(undefined), - } - - mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) - mockCline.recordToolError = vi.fn().mockReturnValue(undefined) - mockCline.didRejectTool = false - toolResult = undefined }) @@ -414,7 +568,7 @@ describe("read_file tool XML output structure", () => { const isBinary = options.isBinary ?? false const validateAccess = options.validateAccess ?? true - mockProvider.getState.mockResolvedValue({ maxReadFileLine }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageSize: 20 }) mockedCountFileLines.mockResolvedValue(totalLines) mockedIsBinaryFile.mockResolvedValue(isBinary) mockCline.rooIgnoreController.validateAccess = vi.fn().mockReturnValue(validateAccess) @@ -453,7 +607,11 @@ describe("read_file tool XML output structure", () => { addLineNumbersMock(mockInputContent) return Promise.resolve(numberedContent) }) - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size // Execute const result = await executeReadFileTool() @@ -482,7 +640,11 @@ describe("read_file tool XML output structure", () => { // Setup mockedCountFileLines.mockResolvedValue(0) mockedExtractTextFromFile.mockResolvedValue("") - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size // Execute const result = await executeReadFileTool({}, { totalLines: 0 }) @@ -492,6 +654,651 @@ describe("read_file tool XML output structure", () => { `\n${testFilePath}\nFile is empty\n\n`, ) }) + + describe("Total Image Memory Limit", () => { + const testImages = [ + { path: "test/image1.png", sizeKB: 5120 }, // 5MB + { path: "test/image2.jpg", sizeKB: 10240 }, // 10MB + { path: "test/image3.gif", sizeKB: 8192 }, // 8MB + ] + + // Define imageBuffer for this test suite + const imageBuffer = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ) + + beforeEach(() => { + // CRITICAL: Reset fsPromises mocks to prevent cross-test contamination within this suite + fsPromises.stat.mockClear() + fsPromises.readFile.mockClear() + }) + + async function executeReadMultipleImagesTool(imagePaths: string[]): Promise { + // Ensure image support is enabled before calling the tool + setImageSupport(mockCline, true) + + // Create args content for multiple files + const filesXml = imagePaths.map((path) => `${path}`).join("") + const argsContent = filesXml + + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + let localResult: ToolResponse | undefined + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + vi.fn(), + (result: ToolResponse) => { + localResult = result + }, + (_: ToolParamName, content?: string) => content ?? "", + ) + // In multi-image scenarios, the result is pushed to pushToolResult, not returned directly. + // We need to check the mock's calls to get the result. + if (mockCline.pushToolResult.mock.calls.length > 0) { + return mockCline.pushToolResult.mock.calls[0][0] + } + + return localResult + } + + it("should allow multiple images under the total memory limit", async () => { + // Setup required mocks (don't clear all mocks - preserve API setup) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size + + // Setup mockCline properties (preserve existing API) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - images that fit within 20MB limit + const smallImages = [ + { path: "test/small1.png", sizeKB: 2048 }, // 2MB + { path: "test/small2.jpg", sizeKB: 3072 }, // 3MB + { path: "test/small3.gif", sizeKB: 4096 }, // 4MB + ] // Total: 9MB (under 20MB limit) + + // Mock file stats for each image + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const normalizedFilePath = path.normalize(filePath.toString()) + const image = smallImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock path.resolve for each image + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(smallImages.map((img) => img.path)) + + // Verify all images were processed (should be a multi-part response) + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + + // Should have text part and 3 image parts + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + + expect(textPart).toBeDefined() + expect(imageParts).toHaveLength(3) + + // Verify no memory limit notices + expect(textPart).not.toContain("Total image memory would exceed") + }) + + it("should skip images that would exceed the total memory limit", async () => { + // Setup required mocks (don't clear all mocks) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 15, + maxTotalImageSize: 20, + }) // Allow up to 15MB per image and 20MB total size + + // Setup mockCline properties + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - images where later ones would exceed 20MB total limit + // Each must be under 5MB per-file limit (5120KB) + const largeImages = [ + { path: "test/large1.png", sizeKB: 5017 }, // ~4.9MB + { path: "test/large2.jpg", sizeKB: 5017 }, // ~4.9MB + { path: "test/large3.gif", sizeKB: 5017 }, // ~4.9MB + { path: "test/large4.png", sizeKB: 5017 }, // ~4.9MB + { path: "test/large5.jpg", sizeKB: 5017 }, // ~4.9MB - This should be skipped (total would be ~24.5MB > 20MB) + ] + + // Mock file stats for each image + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const normalizedFilePath = path.normalize(filePath.toString()) + const image = largeImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock path.resolve for each image + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(largeImages.map((img) => img.path)) + + // Verify result structure - should be a mix of successful images and skipped notices + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + + const textPart = Array.isArray(result) ? result.find((p) => p.type === "text")?.text : result + const imageParts = Array.isArray(result) ? result.filter((p) => p.type === "image") : [] + + expect(textPart).toBeDefined() + + // Debug: Show what we actually got vs expected + if (imageParts.length !== 4) { + throw new Error( + `Expected 4 images, got ${imageParts.length}. Full result: ${JSON.stringify(result, null, 2)}. Text part: ${textPart}`, + ) + } + expect(imageParts).toHaveLength(4) // First 4 images should be included (~19.6MB total) + + // Verify memory limit notice for the fifth image + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/) + }) + + it("should track memory usage correctly across multiple images", async () => { + // Setup mocks (don't clear all mocks) + + // Setup required mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 15, + maxTotalImageSize: 20, + }) // Allow up to 15MB per image and 20MB total size + + // Setup mockCline properties + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - images that exactly reach the limit + const exactLimitImages = [ + { path: "test/exact1.png", sizeKB: 10240 }, // 10MB + { path: "test/exact2.jpg", sizeKB: 10240 }, // 10MB - Total exactly 20MB + { path: "test/exact3.gif", sizeKB: 1024 }, // 1MB - This should be skipped + ] + + // Mock file stats with simpler logic + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const normalizedFilePath = path.normalize(filePath.toString()) + const image = exactLimitImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) + if (image) { + return Promise.resolve({ size: image.sizeKB * 1024 }) + } + return Promise.resolve({ size: 1024 * 1024 }) // Default 1MB + }) + + // Mock path.resolve + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(exactLimitImages.map((img) => img.path)) + + // Verify + const textPart = Array.isArray(result) ? result.find((p) => p.type === "text")?.text : result + const imageParts = Array.isArray(result) ? result.filter((p) => p.type === "image") : [] + + expect(imageParts).toHaveLength(2) // First 2 images should fit + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/) + }) + + it("should handle individual image size limit and total memory limit together", async () => { + // Setup mocks (don't clear all mocks) + + // Setup required mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size + + // Setup mockCline properties (complete setup) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - mix of images with individual size violations and total memory issues + const mixedImages = [ + { path: "test/ok.png", sizeKB: 3072 }, // 3MB - OK + { path: "test/too-big.jpg", sizeKB: 6144 }, // 6MB - Exceeds individual 5MB limit + { path: "test/ok2.gif", sizeKB: 4096 }, // 4MB - OK individually but might exceed total + ] + + // Mock file stats + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const fileName = path.basename(filePath) + const baseName = path.parse(fileName).name + const image = mixedImages.find((img) => img.path.includes(baseName)) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock provider state with 5MB individual limit + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 5, + maxTotalImageSize: 20, + }) + + // Mock path.resolve + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(mixedImages.map((img) => img.path)) + + // Verify + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + + // Should have 2 images (ok.png and ok2.gif) + expect(imageParts).toHaveLength(2) + + // Should show individual size limit violation + expect(textPart).toMatch( + /Image file is too large \(\d+(\.\d+)? MB\)\. The maximum allowed size is 5 MB\./, + ) + }) + + it("should correctly calculate total memory and skip the last image", async () => { + // Setup + const testImages = [ + { path: "test/image1.png", sizeMB: 8 }, + { path: "test/image2.png", sizeMB: 8 }, + { path: "test/image3.png", sizeMB: 8 }, // This one should be skipped + ] + + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 10, // 10MB per image + maxTotalImageSize: 20, // 20MB total + }) + + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + mockedFsReadFile.mockResolvedValue(imageBuffer) + + fsPromises.stat.mockImplementation(async (filePath) => { + const normalizedFilePath = path.normalize(filePath.toString()) + const file = testImages.find((f) => normalizedFilePath.includes(path.normalize(f.path))) + if (file) { + return { size: file.sizeMB * 1024 * 1024 } + } + return { size: 1024 * 1024 } // Default 1MB + }) + + const imagePaths = testImages.map((img) => img.path) + const result = await executeReadMultipleImagesTool(imagePaths) + + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + + expect(imageParts).toHaveLength(2) // First two images should be processed + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/) + }) + + it("should reset total memory tracking for each tool invocation", async () => { + // Setup mocks (don't clear all mocks) + + // Setup required mocks for first batch + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // Setup mockCline properties (complete setup) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - first call with images that use memory + const firstBatch = [{ path: "test/first.png", sizeKB: 10240 }] // 10MB + + fsPromises.stat = vi.fn().mockResolvedValue({ size: 10240 * 1024 }) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute first batch + await executeReadMultipleImagesTool(firstBatch.map((img) => img.path)) + + // Setup second batch (don't clear all mocks) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // Reset path resolving for second batch + mockedPathResolve.mockClear() + + // Re-setup mockCline properties for second batch (complete setup) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + const secondBatch = [{ path: "test/second.png", sizeKB: 15360 }] // 15MB + + // Clear and reset file system mocks for second batch + fsPromises.stat.mockClear() + fsPromises.readFile.mockClear() + mockedIsBinaryFile.mockClear() + mockedCountFileLines.mockClear() + + // Reset mocks for second batch + fsPromises.stat = vi.fn().mockResolvedValue({ size: 15360 * 1024 }) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute second batch + const result = await executeReadMultipleImagesTool(secondBatch.map((img) => img.path)) + + // Verify second batch is processed successfully (memory tracking was reset) + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + const imageParts = parts.filter((p) => p.type === "image") + + expect(imageParts).toHaveLength(1) // Second image should be processed + }) + + it("should handle a folder with many images just under the individual size limit", async () => { + // Setup - Create many images that are each just under the 5MB individual limit + // but together approach the 20MB total limit + const manyImages = [ + { path: "test/img1.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img2.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img3.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img4.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img5.png", sizeKB: 4900 }, // 4.78MB - This should be skipped (total would be ~23.9MB) + ] + + // Setup mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(imageBuffer) + + // Setup provider with 5MB individual limit and 20MB total limit + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 5, + maxTotalImageSize: 20, + }) + + // Mock file stats for each image + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const normalizedFilePath = path.normalize(filePath.toString()) + const image = manyImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock path.resolve + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(manyImages.map((img) => img.path)) + + // Verify + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + + // Should process first 4 images (total ~19.12MB, under 20MB limit) + expect(imageParts).toHaveLength(4) + + // Should show memory limit notice for the 5th image + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toContain("test/img5.png") + + // Verify memory tracking worked correctly + // The notice should show current memory usage around 20MB (4 * 4900KB ≈ 19.14MB, displayed as 20.1MB) + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + }) + + it("should reset memory tracking between separate tool invocations more explicitly", async () => { + // This test verifies that totalImageMemoryUsed is reset between calls + // by making two separate tool invocations and ensuring the second one + // starts with fresh memory tracking + + // Setup mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(imageBuffer) + + // Setup provider + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // First invocation - use 15MB of memory + const firstBatch = [{ path: "test/large1.png", sizeKB: 15360 }] // 15MB + + fsPromises.stat = vi.fn().mockResolvedValue({ size: 15360 * 1024 }) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute first batch + const result1 = await executeReadMultipleImagesTool(firstBatch.map((img) => img.path)) + + // Verify first batch processed successfully + expect(Array.isArray(result1)).toBe(true) + const parts1 = result1 as any[] + const imageParts1 = parts1.filter((p) => p.type === "image") + expect(imageParts1).toHaveLength(1) + + // Second invocation - should start with 0 memory used, not 15MB + // If memory tracking wasn't reset, this 18MB image would be rejected + const secondBatch = [{ path: "test/large2.png", sizeKB: 18432 }] // 18MB + + // Reset mocks for second invocation + fsPromises.stat.mockClear() + fsPromises.readFile.mockClear() + mockedPathResolve.mockClear() + + fsPromises.stat = vi.fn().mockResolvedValue({ size: 18432 * 1024 }) + fsPromises.readFile.mockResolvedValue(imageBuffer) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute second batch + const result2 = await executeReadMultipleImagesTool(secondBatch.map((img) => img.path)) + + // Verify second batch processed successfully + expect(Array.isArray(result2)).toBe(true) + const parts2 = result2 as any[] + const imageParts2 = parts2.filter((p) => p.type === "image") + const textPart2 = parts2.find((p) => p.type === "text")?.text + + // The 18MB image should be processed successfully because memory was reset + expect(imageParts2).toHaveLength(1) + + // Should NOT contain any memory limit notices + expect(textPart2).not.toContain("Image skipped to avoid memory limit") + + // This proves memory tracking was reset between invocations + }) + }) }) describe("Error Handling Tests", () => { @@ -590,3 +1397,343 @@ describe("read_file tool XML output structure", () => { }) }) }) + +describe("read_file tool with image support", () => { + const testImagePath = "test/image.png" + const absoluteImagePath = "/test/image.png" + const base64ImageData = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + const imageBuffer = Buffer.from(base64ImageData, "base64") + + const mockedCountFileLines = vi.mocked(countFileLines) + const mockedIsBinaryFile = vi.mocked(isBinaryFile) + const mockedPathResolve = vi.mocked(path.resolve) + const mockedFsReadFile = vi.mocked(fsPromises.readFile) + const mockedExtractTextFromFile = vi.mocked(extractTextFromFile) + + let localMockCline: any + let localMockProvider: any + let toolResult: ToolResponse | undefined + + beforeEach(() => { + // Clear specific mocks (not all mocks to preserve shared state) + mockedPathResolve.mockClear() + mockedIsBinaryFile.mockClear() + mockedCountFileLines.mockClear() + mockedFsReadFile.mockClear() + mockedExtractTextFromFile.mockClear() + toolResultMock.mockClear() + + // CRITICAL: Reset fsPromises.stat to prevent cross-test contamination + fsPromises.stat.mockClear() + fsPromises.stat.mockResolvedValue({ size: 1024 }) + + // Use shared mock setup function with local variables + const mocks = createMockCline() + localMockCline = mocks.mockCline + localMockProvider = mocks.mockProvider + + // CRITICAL: Explicitly ensure image support is enabled for all tests in this suite + setImageSupport(localMockCline, true) + + mockedPathResolve.mockReturnValue(absoluteImagePath) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + mockedFsReadFile.mockResolvedValue(imageBuffer) + + // Setup mock provider with default maxReadFileLine + localMockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + + toolResult = undefined + }) + + async function executeReadImageTool(imagePath: string = testImagePath): Promise { + const argsContent = `${imagePath}` + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + // Debug: Check if mock is working + console.log("Mock API:", localMockCline.api) + console.log("Supports images:", localMockCline.api?.getModel?.()?.info?.supportsImages) + + await readFileTool( + localMockCline, + toolUse, + localMockCline.ask, + vi.fn(), + (result: ToolResponse) => { + toolResult = result + }, + (_: ToolParamName, content?: string) => content ?? "", + ) + + console.log("Result type:", Array.isArray(toolResult) ? "array" : typeof toolResult) + console.log("Result:", toolResult) + + return toolResult + } + + describe("Image Format Detection", () => { + it.each([ + [".png", "image.png", "image/png"], + [".jpg", "photo.jpg", "image/jpeg"], + [".jpeg", "picture.jpeg", "image/jpeg"], + [".gif", "animation.gif", "image/gif"], + [".bmp", "bitmap.bmp", "image/bmp"], + [".svg", "vector.svg", "image/svg+xml"], + [".webp", "modern.webp", "image/webp"], + [".ico", "favicon.ico", "image/x-icon"], + [".avif", "new-format.avif", "image/avif"], + ])("should detect %s as an image format", async (ext, filename, expectedMimeType) => { + // Setup + const imagePath = `test/${filename}` + const absolutePath = `/test/${filename}` + mockedPathResolve.mockReturnValue(absolutePath) + + // Ensure API mock supports images + setImageSupport(localMockCline, true) + + // Execute + const result = await executeReadImageTool(imagePath) + + // Verify result is a multi-part response + expect(Array.isArray(result)).toBe(true) + const textPart = (result as any[]).find((p) => p.type === "text")?.text + const imagePart = (result as any[]).find((p) => p.type === "image") + + // Verify text part + expect(textPart).toContain(`${imagePath}`) + expect(textPart).not.toContain("") + expect(textPart).toContain(`Image file`) + + // Verify image part + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe(expectedMimeType) + expect(imagePart.source.data).toBe(base64ImageData) + }) + }) + + describe("Image Reading Functionality", () => { + it("should read image file and return a multi-part response", async () => { + // Execute + const result = await executeReadImageTool() + + // Verify result is a multi-part response + expect(Array.isArray(result)).toBe(true) + const textPart = (result as any[]).find((p) => p.type === "text")?.text + const imagePart = (result as any[]).find((p) => p.type === "image") + + // Verify text part + expect(textPart).toContain(`${testImagePath}`) + expect(textPart).not.toContain(``) + expect(textPart).toContain(`Image file`) + + // Verify image part + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + expect(imagePart.source.data).toBe(base64ImageData) + }) + + it("should call formatResponse.toolResult with text and image data", async () => { + // Execute + await executeReadImageTool() + + // Verify toolResultMock was called correctly + expect(toolResultMock).toHaveBeenCalledTimes(1) + const callArgs = toolResultMock.mock.calls[0] + const textArg = callArgs[0] + const imagesArg = callArgs[1] + + expect(textArg).toContain(`${testImagePath}`) + expect(imagesArg).toBeDefined() + expect(imagesArg).toBeInstanceOf(Array) + expect(imagesArg!.length).toBe(1) + expect(imagesArg![0]).toBe(`data:image/png;base64,${base64ImageData}`) + }) + + it("should handle large image files", async () => { + // Setup - simulate a large image + const largeBase64 = "A".repeat(1000000) // 1MB of base64 data + const largeBuffer = Buffer.from(largeBase64, "base64") + mockedFsReadFile.mockResolvedValue(largeBuffer) + + // Execute + const result = await executeReadImageTool() + + // Verify it still works with large data + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + expect(imagePart.source.data).toBe(largeBase64) + }) + + it("should exclude images when model does not support images", async () => { + // Setup - mock API handler that doesn't support images + setImageSupport(localMockCline, false) + + // Execute + const result = await executeReadImageTool() + + // When images are not supported, the tool should return just XML (not call formatResponse.toolResult) + expect(toolResultMock).not.toHaveBeenCalled() + expect(typeof result).toBe("string") + expect(result).toContain(`${testImagePath}`) + expect(result).toContain(`Image file`) + }) + + it("should include images when model supports images", async () => { + // Setup - mock API handler that supports images + setImageSupport(localMockCline, true) + + // Execute + const result = await executeReadImageTool() + + // Verify toolResultMock was called with images + expect(toolResultMock).toHaveBeenCalledTimes(1) + const callArgs = toolResultMock.mock.calls[0] + const textArg = callArgs[0] + const imagesArg = callArgs[1] + + expect(textArg).toContain(`${testImagePath}`) + expect(imagesArg).toBeDefined() // Images should be included + expect(imagesArg).toBeInstanceOf(Array) + expect(imagesArg!.length).toBe(1) + expect(imagesArg![0]).toBe(`data:image/png;base64,${base64ImageData}`) + }) + + it("should handle undefined supportsImages gracefully", async () => { + // Setup - mock API handler with undefined supportsImages + setImageSupport(localMockCline, undefined) + + // Execute + const result = await executeReadImageTool() + + // When supportsImages is undefined, should default to false and return just XML + expect(toolResultMock).not.toHaveBeenCalled() + expect(typeof result).toBe("string") + expect(result).toContain(`${testImagePath}`) + expect(result).toContain(`Image file`) + }) + + it("should handle errors when reading image files", async () => { + // Setup - simulate read error + mockedFsReadFile.mockRejectedValue(new Error("Failed to read image")) + + // Create a spy for handleError + const handleErrorSpy = vi.fn() + + // Execute with the spy + const argsContent = `${testImagePath}` + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + await readFileTool( + localMockCline, + toolUse, + localMockCline.ask, + handleErrorSpy, // Use our spy here + (result: ToolResponse) => { + toolResult = result + }, + (_: ToolParamName, content?: string) => content ?? "", + ) + + // Verify error handling + expect(toolResult).toContain("Error reading image file: Failed to read image") + expect(handleErrorSpy).toHaveBeenCalled() + }) + }) + + describe("Binary File Handling", () => { + it("should not treat non-image binary files as images", async () => { + // Setup + const binaryPath = "test/document.pdf" + const absolutePath = "/test/document.pdf" + mockedPathResolve.mockReturnValue(absolutePath) + mockedExtractTextFromFile.mockResolvedValue("PDF content extracted") + + // Execute + const result = await executeReadImageTool(binaryPath) + + // Verify it uses extractTextFromFile instead + expect(result).not.toContain("") + // Make the test platform-agnostic by checking the call was made (path normalization can vary) + expect(mockedExtractTextFromFile).toHaveBeenCalledTimes(1) + const callArgs = mockedExtractTextFromFile.mock.calls[0] + expect(callArgs[0]).toMatch(/[\\\/]test[\\\/]document\.pdf$/) + }) + + it("should handle unknown binary formats", async () => { + // Setup + const binaryPath = "test/unknown.bin" + const absolutePath = "/test/unknown.bin" + mockedPathResolve.mockReturnValue(absolutePath) + mockedExtractTextFromFile.mockResolvedValue("") + + // Execute + const result = await executeReadImageTool(binaryPath) + + // Verify + expect(result).not.toContain("") + expect(result).toContain(' { + it("should handle case-insensitive image extensions", async () => { + // Test uppercase extensions + const uppercasePath = "test/IMAGE.PNG" + const absolutePath = "/test/IMAGE.PNG" + mockedPathResolve.mockReturnValue(absolutePath) + + // Execute + const result = await executeReadImageTool(uppercasePath) + + // Verify + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + }) + + it("should handle files with multiple dots in name", async () => { + // Setup + const complexPath = "test/my.photo.backup.png" + const absolutePath = "/test/my.photo.backup.png" + mockedPathResolve.mockReturnValue(absolutePath) + + // Execute + const result = await executeReadImageTool(complexPath) + + // Verify + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + }) + + it("should handle empty image files", async () => { + // Setup - empty buffer + mockedFsReadFile.mockResolvedValue(Buffer.from("")) + + // Execute + const result = await executeReadImageTool() + + // Verify - should still create valid data URL + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + expect(imagePart.source.data).toBe("") + }) + }) +}) diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index f046ba67d2..903e3c846e 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -12,6 +12,7 @@ import { formatResponse } from "../prompts/responses" import { fileExistsAtPath } from "../../utils/fs" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { unescapeHtmlEntities } from "../../utils/text-normalization" +import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" export async function applyDiffToolLegacy( cline: Task, @@ -87,7 +88,7 @@ export async function applyDiffToolLegacy( return } - let originalContent: string | null = await fs.readFile(absolutePath, "utf-8") + const originalContent: string = await fs.readFile(absolutePath, "utf-8") // Apply the diff to the original content const diffResult = (await cline.diffStrategy?.applyDiff( @@ -99,9 +100,6 @@ export async function applyDiffToolLegacy( error: "No diff strategy available", } - // Release the original content from memory as it's no longer needed - originalContent = null - if (!diffResult.success) { cline.consecutiveMistakeCount++ const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1 @@ -142,40 +140,79 @@ export async function applyDiffToolLegacy( cline.consecutiveMistakeCount = 0 cline.consecutiveMistakeCountForApplyDiff.delete(relPath) - // Show diff view before asking for approval - cline.diffViewProvider.editType = "modify" - await cline.diffViewProvider.open(relPath) - await cline.diffViewProvider.update(diffResult.content, true) - 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 - - if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) { - toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult) - } - - const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected) - - if (!didApprove) { - await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view - return - } - - // Call saveChanges to update the DiffViewProvider properties + // Check if preventFocusDisruption experiment is enabled const provider = cline.providerRef.deref() const state = await provider?.getState() const diagnosticsEnabled = state?.diagnosticsEnabled ?? true const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + const isPreventFocusDisruptionEnabled = experiments.isEnabled( + state?.experiments ?? {}, + EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION, + ) + + // Check if file is write-protected + const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false + + if (isPreventFocusDisruptionEnabled) { + // Direct file write without diff view + const completeMessage = JSON.stringify({ + ...sharedMessageProps, + diff: diffContent, + isProtected: isWriteProtected, + } satisfies ClineSayTool) + + let toolProgressStatus + + if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) { + toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult) + } + + const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected) + + if (!didApprove) { + return + } + + // Save directly without showing diff view or opening the file + cline.diffViewProvider.editType = "modify" + cline.diffViewProvider.originalContent = originalContent + await cline.diffViewProvider.saveDirectly( + relPath, + diffResult.content, + false, + diagnosticsEnabled, + writeDelayMs, + ) + } else { + // Original behavior with diff view + // Show diff view before asking for approval + cline.diffViewProvider.editType = "modify" + await cline.diffViewProvider.open(relPath) + await cline.diffViewProvider.update(diffResult.content, true) + cline.diffViewProvider.scrollToFirstDiff() + + const completeMessage = JSON.stringify({ + ...sharedMessageProps, + diff: diffContent, + isProtected: isWriteProtected, + } satisfies ClineSayTool) + + let toolProgressStatus + + if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) { + toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult) + } + + const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected) + + if (!didApprove) { + await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view + return + } + + // Call saveChanges to update the DiffViewProvider properties + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + } // Track file edit operation if (relPath) { diff --git a/src/core/tools/helpers/imageHelpers.ts b/src/core/tools/helpers/imageHelpers.ts new file mode 100644 index 0000000000..a1adb078e6 --- /dev/null +++ b/src/core/tools/helpers/imageHelpers.ts @@ -0,0 +1,192 @@ +import path from "path" +import * as fs from "fs/promises" +import { t } from "../../../i18n" +import prettyBytes from "pretty-bytes" + +/** + * Default maximum allowed image file size in bytes (5MB) + */ +export const DEFAULT_MAX_IMAGE_FILE_SIZE_MB = 5 + +/** + * Default maximum total memory usage for all images in a single read operation (20MB) + * This is a cumulative limit - as each image is processed, its size is added to the total. + * If including another image would exceed this limit, it will be skipped with a notice. + * Example: With a 20MB limit, reading 3 images of 8MB, 7MB, and 10MB would process + * the first two (15MB total) but skip the third to stay under the limit. + */ +export const DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB = 20 + +/** + * Supported image formats that can be displayed + */ +export const SUPPORTED_IMAGE_FORMATS = [ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".svg", + ".bmp", + ".ico", + ".tiff", + ".tif", + ".avif", +] as const + +export const IMAGE_MIME_TYPES: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".svg": "image/svg+xml", + ".bmp": "image/bmp", + ".ico": "image/x-icon", + ".tiff": "image/tiff", + ".tif": "image/tiff", + ".avif": "image/avif", +} + +/** + * Result of image validation + */ +export interface ImageValidationResult { + isValid: boolean + reason?: "size_limit" | "memory_limit" | "unsupported_model" + notice?: string + sizeInMB?: number +} + +/** + * Result of image processing + */ +export interface ImageProcessingResult { + dataUrl: string + buffer: Buffer + sizeInKB: number + sizeInMB: number + notice: string +} + +/** + * Reads an image file and returns both the data URL and buffer + */ +export async function readImageAsDataUrlWithBuffer(filePath: string): Promise<{ dataUrl: string; buffer: Buffer }> { + const fileBuffer = await fs.readFile(filePath) + const base64 = fileBuffer.toString("base64") + const ext = path.extname(filePath).toLowerCase() + + const mimeType = IMAGE_MIME_TYPES[ext] || "image/png" + const dataUrl = `data:${mimeType};base64,${base64}` + + return { dataUrl, buffer: fileBuffer } +} + +/** + * Checks if a file extension is a supported image format + */ +export function isSupportedImageFormat(extension: string): boolean { + return SUPPORTED_IMAGE_FORMATS.includes(extension.toLowerCase() as (typeof SUPPORTED_IMAGE_FORMATS)[number]) +} + +/** + * Validates if an image can be processed based on size limits and model support + */ +export async function validateImageForProcessing( + fullPath: string, + supportsImages: boolean, + maxImageFileSize: number, + maxTotalImageSize: number, + currentTotalMemoryUsed: number, +): Promise { + // Check if model supports images + if (!supportsImages) { + return { + isValid: false, + reason: "unsupported_model", + notice: "Image file detected but current model does not support images. Skipping image processing.", + } + } + + const imageStats = await fs.stat(fullPath) + const imageSizeInMB = imageStats.size / (1024 * 1024) + + // Check individual file size limit + if (imageStats.size > maxImageFileSize * 1024 * 1024) { + const imageSizeFormatted = prettyBytes(imageStats.size) + return { + isValid: false, + reason: "size_limit", + notice: t("tools:readFile.imageTooLarge", { + size: imageSizeFormatted, + max: maxImageFileSize, + }), + sizeInMB: imageSizeInMB, + } + } + + // Check total memory limit + if (currentTotalMemoryUsed + imageSizeInMB > maxTotalImageSize) { + const currentMemoryFormatted = prettyBytes(currentTotalMemoryUsed * 1024 * 1024) + const fileMemoryFormatted = prettyBytes(imageStats.size) + return { + isValid: false, + reason: "memory_limit", + notice: `Image skipped to avoid size limit (${maxTotalImageSize}MB). Current: ${currentMemoryFormatted} + this file: ${fileMemoryFormatted}. Try fewer or smaller images.`, + sizeInMB: imageSizeInMB, + } + } + + return { + isValid: true, + sizeInMB: imageSizeInMB, + } +} + +/** + * Processes an image file and returns the result + */ +export async function processImageFile(fullPath: string): Promise { + const imageStats = await fs.stat(fullPath) + const { dataUrl, buffer } = await readImageAsDataUrlWithBuffer(fullPath) + const imageSizeInKB = Math.round(imageStats.size / 1024) + const imageSizeInMB = imageStats.size / (1024 * 1024) + const noticeText = t("tools:readFile.imageWithSize", { size: imageSizeInKB }) + + return { + dataUrl, + buffer, + sizeInKB: imageSizeInKB, + sizeInMB: imageSizeInMB, + notice: noticeText, + } +} + +/** + * Memory tracker for image processing + */ +export class ImageMemoryTracker { + private totalMemoryUsed: number = 0 + + /** + * Gets the current total memory used in MB + */ + getTotalMemoryUsed(): number { + return this.totalMemoryUsed + } + + /** + * Adds to the total memory used + */ + addMemoryUsage(sizeInMB: number): void { + this.totalMemoryUsed += sizeInMB + } + + /** + * Resets the memory tracker + */ + reset(): void { + this.totalMemoryUsed = 0 + } +} diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index 2b31224400..b5e85dea30 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -11,6 +11,7 @@ import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { fileExistsAtPath } from "../../utils/fs" import { insertGroups } from "../diff/insert-groups" import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" export async function insertContentTool( cline: Task, @@ -107,15 +108,15 @@ export async function insertContentTool( }, ]).join("\n") - // Show changes in diff view - if (!cline.diffViewProvider.isEditing) { - await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {}) - // First open with original content - await cline.diffViewProvider.open(relPath) - await cline.diffViewProvider.update(fileContent, false) - cline.diffViewProvider.scrollToFirstDiff() - await delay(200) - } + // Check if preventFocusDisruption experiment is enabled + const provider = cline.providerRef.deref() + const state = await provider?.getState() + const diagnosticsEnabled = state?.diagnosticsEnabled ?? true + const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS + const isPreventFocusDisruptionEnabled = experiments.isEnabled( + state?.experiments ?? {}, + EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION, + ) // For consistency with writeToFileTool, handle new files differently let diff: string | undefined @@ -135,8 +136,7 @@ export async function insertContentTool( approvalContent = updatedContent } - await cline.diffViewProvider.update(updatedContent, true) - + // Prepare the approval message (same for both flows) const completeMessage = JSON.stringify({ ...sharedMessageProps, diff, @@ -145,22 +145,36 @@ export async function insertContentTool( isProtected: isWriteProtected, } satisfies ClineSayTool) + // Show diff view if focus disruption prevention is disabled + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.open(relPath) + await cline.diffViewProvider.update(updatedContent, true) + cline.diffViewProvider.scrollToFirstDiff() + } + + // Ask for approval (same for both flows) const didApprove = await cline .ask("tool", completeMessage, isWriteProtected) .then((response) => response.response === "yesButtonClicked") if (!didApprove) { - await cline.diffViewProvider.revertChanges() + // Revert changes if diff view was shown + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.revertChanges() + } pushToolResult("Changes were rejected by the user.") + await cline.diffViewProvider.reset() return } - // Call saveChanges to update the DiffViewProvider properties - const provider = cline.providerRef.deref() - const state = await provider?.getState() - const diagnosticsEnabled = state?.diagnosticsEnabled ?? true - const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + // Save the changes + if (isPreventFocusDisruptionEnabled) { + // Direct file write without diff view or opening the file + await cline.diffViewProvider.saveDirectly(relPath, updatedContent, false, diagnosticsEnabled, writeDelayMs) + } else { + // Call saveChanges to update the DiffViewProvider properties + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + } // Track file edit operation if (relPath) { diff --git a/src/core/tools/multiApplyDiffTool.ts b/src/core/tools/multiApplyDiffTool.ts index ec8c77a63b..db514d2b64 100644 --- a/src/core/tools/multiApplyDiffTool.ts +++ b/src/core/tools/multiApplyDiffTool.ts @@ -507,11 +507,15 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} cline.consecutiveMistakeCount = 0 cline.consecutiveMistakeCountForApplyDiff.delete(relPath) - // Show diff view before asking for approval (only for single file or after batch approval) - cline.diffViewProvider.editType = "modify" - await cline.diffViewProvider.open(relPath) - await cline.diffViewProvider.update(originalContent!, true) - cline.diffViewProvider.scrollToFirstDiff() + // Check if preventFocusDisruption experiment is enabled + const provider = cline.providerRef.deref() + const state = await provider?.getState() + const diagnosticsEnabled = state?.diagnosticsEnabled ?? true + const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS + const isPreventFocusDisruptionEnabled = experiments.isEnabled( + state?.experiments ?? {}, + EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION, + ) // For batch operations, we've already gotten approval const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false @@ -521,9 +525,10 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} isProtected: isWriteProtected, } - // If single file, ask for approval + // If single file, handle based on PREVENT_FOCUS_DISRUPTION setting let didApprove = true if (operationsToApprove.length === 1) { + // Prepare common data for single file operation const diffContents = diffItems.map((item) => item.content).join("\n\n") const operationMessage = JSON.stringify({ ...sharedMessageProps, @@ -531,7 +536,6 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} } satisfies ClineSayTool) let toolProgressStatus - if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) { toolProgressStatus = cline.diffStrategy.getProgressStatus( { @@ -542,23 +546,70 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} ) } - // Check if file is write-protected + // Set up diff view + cline.diffViewProvider.editType = "modify" + + // Show diff view if focus disruption prevention is disabled + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.open(relPath) + await cline.diffViewProvider.update(originalContent!, true) + cline.diffViewProvider.scrollToFirstDiff() + } else { + // For direct save, we still need to set originalContent + cline.diffViewProvider.originalContent = await fs.readFile(absolutePath, "utf-8") + } + + // Ask for approval (same for both flows) const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false didApprove = await askApproval("tool", operationMessage, toolProgressStatus, isWriteProtected) - } - if (!didApprove) { - await cline.diffViewProvider.revertChanges() - results.push(`Changes to ${relPath} were not approved by user`) - continue - } + if (!didApprove) { + // Revert changes if diff view was shown + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.revertChanges() + } + results.push(`Changes to ${relPath} were not approved by user`) + continue + } - // Call saveChanges to update the DiffViewProvider properties - const provider = cline.providerRef.deref() - const state = await provider?.getState() - const diagnosticsEnabled = state?.diagnosticsEnabled ?? true - const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + // Save the changes + if (isPreventFocusDisruptionEnabled) { + // Direct file write without diff view or opening the file + await cline.diffViewProvider.saveDirectly( + relPath, + originalContent!, + false, + diagnosticsEnabled, + writeDelayMs, + ) + } else { + // Call saveChanges to update the DiffViewProvider properties + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + } + } else { + // Batch operations - already approved above + if (isPreventFocusDisruptionEnabled) { + // Direct file write without diff view or opening the file + cline.diffViewProvider.editType = "modify" + cline.diffViewProvider.originalContent = await fs.readFile(absolutePath, "utf-8") + await cline.diffViewProvider.saveDirectly( + relPath, + originalContent!, + false, + diagnosticsEnabled, + writeDelayMs, + ) + } else { + // Original behavior with diff view + cline.diffViewProvider.editType = "modify" + await cline.diffViewProvider.open(relPath) + await cline.diffViewProvider.update(originalContent!, true) + cline.diffViewProvider.scrollToFirstDiff() + + // Call saveChanges to update the DiffViewProvider properties + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + } + } // Track file edit operation await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index 7cc7063b49..cc56659d02 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -80,17 +80,19 @@ export async function newTaskTool( // Preserve the current mode so we can resume with it later. cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug - // Switch mode first, then create new task instance. - await provider.handleModeSwitch(mode) - - // Delay to allow mode change to take effect before next tool is executed. - await delay(500) - + // Create new task instance first (this preserves parent's current mode in its history) const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline) if (!newCline) { pushToolResult(t("tools:newTask.errors.policy_restriction")) return } + + // Now switch the newly created task to the desired mode + await provider.handleModeSwitch(mode) + + // Delay to allow mode change to take effect + await delay(500) + cline.emit("taskSpawned", newCline.taskId) pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index f1bbcb9149..e205f4527e 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -15,6 +15,14 @@ import { readLines } from "../../integrations/misc/read-lines" import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text" import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { parseXml } from "../../utils/xml" +import { + DEFAULT_MAX_IMAGE_FILE_SIZE_MB, + DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, + isSupportedImageFormat, + validateImageForProcessing, + processImageFile, + ImageMemoryTracker, +} from "./helpers/imageHelpers" export function getReadFileToolDescription(blockName: string, blockParams: any): string { // Handle both single path and multiple files via args @@ -67,6 +75,7 @@ interface FileResult { notice?: string lineRanges?: LineRange[] xmlContent?: string // Final XML content for this file + imageDataUrl?: string // Image data URL for image files feedbackText?: string // User feedback text from approval/denial feedbackImages?: any[] // User feedback images from approval/denial } @@ -84,6 +93,10 @@ export async function readFileTool( const legacyStartLineStr: string | undefined = block.params.start_line const legacyEndLineStr: string | undefined = block.params.end_line + // Check if the current model supports images at the beginning + const modelInfo = cline.api.getModel().info + const supportsImages = modelInfo.supportsImages ?? false + // Handle partial message first if (block.partial) { let filePath = "" @@ -421,6 +434,15 @@ export async function readFileTool( } } + // Track total image memory usage across all files + const imageMemoryTracker = new ImageMemoryTracker() + const state = await cline.providerRef.deref()?.getState() + const { + maxReadFileLine = -1, + maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, + maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, + } = state ?? {} + // Then process only approved files for (const fileResult of fileResults) { // Skip files that weren't approved @@ -430,7 +452,6 @@ export async function readFileTool( const relPath = fileResult.path const fullPath = path.resolve(cline.cwd, relPath) - const { maxReadFileLine = -1 } = (await cline.providerRef.deref()?.getState()) ?? {} // Process approved files try { @@ -456,14 +477,71 @@ export async function readFileTool( const fileExtension = path.extname(relPath).toLowerCase() const supportedBinaryFormats = getSupportedBinaryFormats() - if (!supportedBinaryFormats.includes(fileExtension)) { + // Check if it's a supported image format + if (isSupportedImageFormat(fileExtension)) { + try { + // Validate image for processing + const validationResult = await validateImageForProcessing( + fullPath, + supportsImages, + maxImageFileSize, + maxTotalImageSize, + imageMemoryTracker.getTotalMemoryUsed(), + ) + + if (!validationResult.isValid) { + // Track file read + await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) + + updateFileResult(relPath, { + xmlContent: `${relPath}\n${validationResult.notice}\n`, + }) + continue + } + + // Process the image + const imageResult = await processImageFile(fullPath) + + // Track memory usage for this image + imageMemoryTracker.addMemoryUsage(imageResult.sizeInMB) + + // Track file read + await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) + + // Store image data URL separately - NOT in XML + updateFileResult(relPath, { + xmlContent: `${relPath}\n${imageResult.notice}\n`, + imageDataUrl: imageResult.dataUrl, + }) + continue + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + updateFileResult(relPath, { + status: "error", + error: `Error reading image file: ${errorMsg}`, + xmlContent: `${relPath}Error reading image file: ${errorMsg}`, + }) + await handleError( + `reading image file ${relPath}`, + error instanceof Error ? error : new Error(errorMsg), + ) + continue + } + } + + // Check if it's a supported binary format that can be processed + if (supportedBinaryFormats && supportedBinaryFormats.includes(fileExtension)) { + // For supported binary formats (.pdf, .docx, .ipynb), continue to extractTextFromFile + // Fall through to the normal extractTextFromFile processing below + } else { + // Handle unknown binary format + const fileFormat = fileExtension.slice(1) || "bin" // Remove the dot, fallback to "bin" updateFileResult(relPath, { - notice: "Binary file", - xmlContent: `${relPath}\nBinary file\n`, + notice: `Binary file format: ${fileFormat}`, + xmlContent: `${relPath}\nBinary file - content not displayed\n`, }) continue } - // For supported binary formats (.pdf, .docx, .ipynb), continue to extractTextFromFile } // Handle range reads (bypass maxReadFileLine) @@ -571,6 +649,11 @@ export async function readFileTool( const xmlResults = fileResults.filter((result) => result.xmlContent).map((result) => result.xmlContent) const filesXml = `\n${xmlResults.join("\n")}\n` + // Collect all image data URLs from file results + const fileImageUrls = fileResults + .filter((result) => result.imageDataUrl) + .map((result) => result.imageDataUrl as string) + // Process all feedback in a unified way without branching let statusMessage = "" let feedbackImages: any[] = [] @@ -598,20 +681,39 @@ export async function readFileTool( } } + // Combine all images: feedback images first, then file images + const allImages = [...feedbackImages, ...fileImageUrls] + + // Re-check if the model supports images before including them, in case it changed during execution. + const finalModelSupportsImages = cline.api.getModel().info.supportsImages ?? false + const imagesToInclude = finalModelSupportsImages ? allImages : [] + // Push the result with appropriate formatting - if (statusMessage) { - const result = formatResponse.toolResult(statusMessage, feedbackImages) + if (statusMessage || imagesToInclude.length > 0) { + // Always use formatResponse.toolResult when we have a status message or images + const result = formatResponse.toolResult( + statusMessage || filesXml, + imagesToInclude.length > 0 ? imagesToInclude : undefined, + ) // Handle different return types from toolResult if (typeof result === "string") { - pushToolResult(`${result}\n${filesXml}`) + if (statusMessage) { + pushToolResult(`${result}\n${filesXml}`) + } else { + pushToolResult(result) + } } else { - // For block-based results, we need to convert the filesXml to a text block and append it - const textBlock = { type: "text" as const, text: filesXml } - pushToolResult([...result, textBlock]) + // For block-based results, append the files XML as a text block if not already included + if (statusMessage) { + const textBlock = { type: "text" as const, text: filesXml } + pushToolResult([...result, textBlock]) + } else { + pushToolResult(result) + } } } else { - // No status message, just push the files XML + // No images or status message, just push the files XML pushToolResult(filesXml) } } catch (error) { diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index b6ec3ed39b..50f4868b50 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -12,6 +12,7 @@ import { getReadablePath } from "../../utils/path" import { fileExistsAtPath } from "../../utils/fs" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" /** * Tool for performing search and replace operations on files @@ -199,40 +200,51 @@ export async function searchAndReplaceTool( return } - // Show changes in diff view - if (!cline.diffViewProvider.isEditing) { - await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {}) - await cline.diffViewProvider.open(validRelPath) - await cline.diffViewProvider.update(fileContent, false) - cline.diffViewProvider.scrollToFirstDiff() - await delay(200) - } + // Check if preventFocusDisruption experiment is enabled + const provider = cline.providerRef.deref() + const state = await provider?.getState() + const diagnosticsEnabled = state?.diagnosticsEnabled ?? true + const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS + const isPreventFocusDisruptionEnabled = experiments.isEnabled( + state?.experiments ?? {}, + EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION, + ) - await cline.diffViewProvider.update(newContent, true) - - // Request user approval for changes const completeMessage = JSON.stringify({ ...sharedMessageProps, diff, isProtected: isWriteProtected, } satisfies ClineSayTool) + + // Show diff view if focus disruption prevention is disabled + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.open(validRelPath) + await cline.diffViewProvider.update(newContent, true) + cline.diffViewProvider.scrollToFirstDiff() + } + const didApprove = await cline .ask("tool", completeMessage, isWriteProtected) .then((response) => response.response === "yesButtonClicked") if (!didApprove) { - await cline.diffViewProvider.revertChanges() + // Revert changes if diff view was shown + if (!isPreventFocusDisruptionEnabled) { + await cline.diffViewProvider.revertChanges() + } pushToolResult("Changes were rejected by the user.") await cline.diffViewProvider.reset() return } - // Call saveChanges to update the DiffViewProvider properties - const provider = cline.providerRef.deref() - const state = await provider?.getState() - const diagnosticsEnabled = state?.diagnosticsEnabled ?? true - const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + // Save the changes + if (isPreventFocusDisruptionEnabled) { + // Direct file write without diff view or opening the file + await cline.diffViewProvider.saveDirectly(validRelPath, newContent, false, diagnosticsEnabled, writeDelayMs) + } else { + // Call saveChanges to update the DiffViewProvider properties + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + } // Track file edit operation if (relPath) { diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index fd9d158f3f..e82eab92bc 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -1,6 +1,7 @@ import path from "path" import delay from "delay" import * as vscode from "vscode" +import fs from "fs/promises" import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" @@ -14,6 +15,7 @@ import { isPathOutsideWorkspace } from "../../utils/pathUtils" import { detectCodeOmission } from "../../integrations/editor/detect-omission" import { unescapeHtmlEntities } from "../../utils/text-normalization" import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" export async function writeToFileTool( cline: Task, @@ -99,22 +101,32 @@ export async function writeToFileTool( try { if (block.partial) { - // update gui message - const partialMessage = JSON.stringify(sharedMessageProps) - await cline.ask("tool", partialMessage, block.partial).catch(() => {}) - - // update editor - if (!cline.diffViewProvider.isEditing) { - // open the editor and prepare to stream content in - await cline.diffViewProvider.open(relPath) - } - - // editor is open, stream content in - await cline.diffViewProvider.update( - everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, - false, + // Check if preventFocusDisruption experiment is enabled + const provider = cline.providerRef.deref() + const state = await provider?.getState() + const isPreventFocusDisruptionEnabled = experiments.isEnabled( + state?.experiments ?? {}, + EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION, ) + if (!isPreventFocusDisruptionEnabled) { + // update gui message + const partialMessage = JSON.stringify(sharedMessageProps) + await cline.ask("tool", partialMessage, block.partial).catch(() => {}) + + // update editor + if (!cline.diffViewProvider.isEditing) { + // open the editor and prepare to stream content in + await cline.diffViewProvider.open(relPath) + } + + // editor is open, stream content in + await cline.diffViewProvider.update( + everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, + false, + ) + } + return } else { if (predictedLineCount === undefined) { @@ -149,76 +161,138 @@ export async function writeToFileTool( cline.consecutiveMistakeCount = 0 - // if isEditingFile false, that means we have the full contents of the file already. - // it's important to note how cline function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So cline part of the logic will always be called. - // in other words, you must always repeat the block.partial logic here - if (!cline.diffViewProvider.isEditing) { - // show gui message before showing edit animation - const partialMessage = JSON.stringify(sharedMessageProps) - await cline.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, cline shows the edit row before the content is streamed into the editor - await cline.diffViewProvider.open(relPath) - } - - await cline.diffViewProvider.update( - everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, - true, - ) - - await delay(300) // wait for diff view to update - cline.diffViewProvider.scrollToFirstDiff() - - // Check for code omissions before proceeding - if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { - if (cline.diffStrategy) { - await cline.diffViewProvider.revertChanges() - - pushToolResult( - formatResponse.toolError( - `Content appears to be truncated (file has ${ - newContent.split("\n").length - } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, - ), - ) - return - } else { - vscode.window - .showWarningMessage( - "Potential code truncation detected. cline happens when the AI reaches its max output limit.", - "Follow cline guide to fix the issue", - ) - .then((selection) => { - if (selection === "Follow cline guide to fix the issue") { - vscode.env.openExternal( - vscode.Uri.parse( - "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments", - ), - ) - } - }) - } - } - - const completeMessage = JSON.stringify({ - ...sharedMessageProps, - content: fileExists ? undefined : newContent, - diff: fileExists - ? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent) - : undefined, - } satisfies ClineSayTool) - - const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected) - - if (!didApprove) { - await cline.diffViewProvider.revertChanges() - return - } - - // Call saveChanges to update the DiffViewProvider properties + // Check if preventFocusDisruption experiment is enabled const provider = cline.providerRef.deref() const state = await provider?.getState() const diagnosticsEnabled = state?.diagnosticsEnabled ?? true const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + const isPreventFocusDisruptionEnabled = experiments.isEnabled( + state?.experiments ?? {}, + EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION, + ) + + if (isPreventFocusDisruptionEnabled) { + // Direct file write without diff view + // Check for code omissions before proceeding + if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { + if (cline.diffStrategy) { + pushToolResult( + formatResponse.toolError( + `Content appears to be truncated (file has ${ + newContent.split("\n").length + } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, + ), + ) + return + } else { + vscode.window + .showWarningMessage( + "Potential code truncation detected. cline happens when the AI reaches its max output limit.", + "Follow cline guide to fix the issue", + ) + .then((selection) => { + if (selection === "Follow cline guide to fix the issue") { + vscode.env.openExternal( + vscode.Uri.parse( + "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments", + ), + ) + } + }) + } + } + + const completeMessage = JSON.stringify({ + ...sharedMessageProps, + content: newContent, + } satisfies ClineSayTool) + + const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected) + + if (!didApprove) { + return + } + + // Set up diffViewProvider properties needed for saveDirectly + cline.diffViewProvider.editType = fileExists ? "modify" : "create" + if (fileExists) { + const absolutePath = path.resolve(cline.cwd, relPath) + cline.diffViewProvider.originalContent = await fs.readFile(absolutePath, "utf-8") + } else { + cline.diffViewProvider.originalContent = "" + } + + // Save directly without showing diff view or opening the file + await cline.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs) + } else { + // Original behavior with diff view + // if isEditingFile false, that means we have the full contents of the file already. + // it's important to note how cline function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So cline part of the logic will always be called. + // in other words, you must always repeat the block.partial logic here + if (!cline.diffViewProvider.isEditing) { + // show gui message before showing edit animation + const partialMessage = JSON.stringify(sharedMessageProps) + await cline.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, cline shows the edit row before the content is streamed into the editor + await cline.diffViewProvider.open(relPath) + } + + await cline.diffViewProvider.update( + everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, + true, + ) + + await delay(300) // wait for diff view to update + cline.diffViewProvider.scrollToFirstDiff() + + // Check for code omissions before proceeding + if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { + if (cline.diffStrategy) { + await cline.diffViewProvider.revertChanges() + + pushToolResult( + formatResponse.toolError( + `Content appears to be truncated (file has ${ + newContent.split("\n").length + } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, + ), + ) + return + } else { + vscode.window + .showWarningMessage( + "Potential code truncation detected. cline happens when the AI reaches its max output limit.", + "Follow cline guide to fix the issue", + ) + .then((selection) => { + if (selection === "Follow cline guide to fix the issue") { + vscode.env.openExternal( + vscode.Uri.parse( + "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments", + ), + ) + } + }) + } + } + + const completeMessage = JSON.stringify({ + ...sharedMessageProps, + content: fileExists ? undefined : newContent, + diff: fileExists + ? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent) + : undefined, + } satisfies ClineSayTool) + + const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected) + + if (!didApprove) { + await cline.diffViewProvider.revertChanges() + return + } + + // Call saveChanges to update the DiffViewProvider properties + await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + } // Track file edit operation if (relPath) { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 905e657b37..280ab61a06 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -40,7 +40,7 @@ import { findLast } from "../../shared/array" import { supportPrompt } from "../../shared/support-prompt" import { GlobalFileNames } from "../../shared/globalFileNames" import { ExtensionMessage, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage" -import { Mode, defaultModeSlug } from "../../shared/modes" +import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes" import { experimentDefault, experiments, EXPERIMENT_IDS } from "../../shared/experiments" import { formatLanguage } from "../../shared/language" import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" @@ -112,7 +112,7 @@ export class ClineProvider public isViewLaunched = false public settingsImportedAt?: number - public readonly latestAnnouncementId = "jul-09-2025-3-23-0" // Update for v3.23.0 announcement + public readonly latestAnnouncementId = "jul-29-2025-3-25-0" // Update for v3.25.0 announcement public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager @@ -578,6 +578,49 @@ export class ClineProvider public async initClineWithHistoryItem(historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task }) { await this.removeClineFromStack() + // If the history item has a saved mode, restore it and its associated API configuration + if (historyItem.mode) { + // Validate that the mode still exists + const customModes = await this.customModesManager.getCustomModes() + const modeExists = getModeBySlug(historyItem.mode, customModes) !== undefined + + if (!modeExists) { + // Mode no longer exists, fall back to default mode + this.log( + `Mode '${historyItem.mode}' from history no longer exists. Falling back to default mode '${defaultModeSlug}'.`, + ) + historyItem.mode = defaultModeSlug + } + + await this.updateGlobalState("mode", historyItem.mode) + + // Load the saved API config for the restored mode if it exists + const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode) + const listApiConfig = await this.providerSettingsManager.listConfig() + + // Update listApiConfigMeta first to ensure UI has latest data + await this.updateGlobalState("listApiConfigMeta", listApiConfig) + + // If this mode has a saved config, use it + if (savedConfigId) { + const profile = listApiConfig.find(({ id }) => id === savedConfigId) + + if (profile?.name) { + try { + await this.activateProviderProfile({ name: profile.name }) + } catch (error) { + // Log the error but continue with task restoration + this.log( + `Failed to restore API configuration for mode '${historyItem.mode}': ${ + error instanceof Error ? error.message : String(error) + }. Continuing with default configuration.`, + ) + // The task will continue with the current/default configuration + } + } + } + } + const { apiConfiguration, diffEnabled: enableDiff, @@ -807,6 +850,31 @@ export class ClineProvider if (cline) { TelemetryService.instance.captureModeSwitch(cline.taskId, newMode) cline.emit("taskModeSwitched", cline.taskId, newMode) + + // Store the current mode in case we need to rollback + const previousMode = (cline as any)._taskMode + + try { + // Update the task history with the new mode first + const history = this.getGlobalState("taskHistory") ?? [] + const taskHistoryItem = history.find((item) => item.id === cline.taskId) + if (taskHistoryItem) { + taskHistoryItem.mode = newMode + await this.updateTaskHistory(taskHistoryItem) + } + + // Only update the task's mode after successful persistence + ;(cline as any)._taskMode = newMode + } catch (error) { + // If persistence fails, log the error but don't update the in-memory state + this.log( + `Failed to persist mode switch for task ${cline.taskId}: ${error instanceof Error ? error.message : String(error)}`, + ) + + // Optionally, we could emit an event to notify about the failure + // This ensures the in-memory state remains consistent with persisted state + throw error + } } await this.updateGlobalState("mode", newMode) @@ -1260,10 +1328,10 @@ export class ClineProvider */ async fetchMarketplaceData() { try { - const [marketplaceItems, marketplaceInstalledMetadata] = await Promise.all([ - this.marketplaceManager.getCurrentItems().catch((error) => { + const [marketplaceResult, marketplaceInstalledMetadata] = await Promise.all([ + this.marketplaceManager.getMarketplaceItems().catch((error) => { console.error("Failed to fetch marketplace items:", error) - return [] as MarketplaceItem[] + return { organizationMcps: [], marketplaceItems: [], errors: [error.message] } }), this.marketplaceManager.getInstallationMetadata().catch((error) => { console.error("Failed to fetch installation metadata:", error) @@ -1274,16 +1342,20 @@ export class ClineProvider // Send marketplace data separately this.postMessageToWebview({ type: "marketplaceData", - marketplaceItems: marketplaceItems || [], + organizationMcps: marketplaceResult.organizationMcps || [], + marketplaceItems: marketplaceResult.marketplaceItems || [], marketplaceInstalledMetadata: marketplaceInstalledMetadata || { project: {}, global: {} }, + errors: marketplaceResult.errors, }) } catch (error) { console.error("Failed to fetch marketplace data:", error) // Send empty data on error to prevent UI from hanging this.postMessageToWebview({ type: "marketplaceData", + organizationMcps: [], marketplaceItems: [], marketplaceInstalledMetadata: { project: {}, global: {} }, + errors: [error instanceof Error ? error.message : String(error)], }) // Show user-friendly error notification for network issues @@ -1425,6 +1497,8 @@ export class ClineProvider showRooIgnoredFiles, language, maxReadFileLine, + maxImageFileSize, + maxTotalImageSize, terminalCompressProgressBar, historyPreviewCollapsed, cloudUserInfo, @@ -1532,6 +1606,8 @@ export class ClineProvider language: language ?? formatLanguage(vscode.env.language), renderContext: this.renderContext, maxReadFileLine: maxReadFileLine ?? -1, + maxImageFileSize: maxImageFileSize ?? 5, + maxTotalImageSize: maxTotalImageSize ?? 20, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, settingsImportedAt: this.settingsImportedAt, terminalCompressProgressBar: terminalCompressProgressBar ?? true, @@ -1702,6 +1778,8 @@ export class ClineProvider telemetrySetting: stateValues.telemetrySetting || "unset", showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, maxReadFileLine: stateValues.maxReadFileLine ?? -1, + maxImageFileSize: stateValues.maxImageFileSize ?? 5, + maxTotalImageSize: stateValues.maxTotalImageSize ?? 20, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, cloudUserInfo, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 344b098816..2e70f80f99 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -533,6 +533,8 @@ describe("ClineProvider", () => { showRooIgnoredFiles: true, renderContext: "sidebar", maxReadFileLine: 500, + maxImageFileSize: 5, + maxTotalImageSize: 20, cloudUserInfo: null, organizationAllowList: ORGANIZATION_ALLOW_ALL, autoCondenseContext: true, @@ -1654,6 +1656,268 @@ describe("ClineProvider", () => { }) }) + describe("initClineWithHistoryItem mode validation", () => { + test("validates and falls back to default mode when restored mode no longer exists", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Mock custom modes that don't include the saved mode + const mockCustomModesManager = { + getCustomModes: vi.fn().mockResolvedValue([ + { + slug: "existing-mode", + name: "Existing Mode", + roleDefinition: "Test role", + groups: ["read"] as const, + }, + ]), + dispose: vi.fn(), + } + ;(provider as any).customModesManager = mockCustomModesManager + + // Mock getModeBySlug to return undefined for non-existent mode + const { getModeBySlug } = await import("../../../shared/modes") + vi.mocked(getModeBySlug) + .mockReturnValueOnce(undefined) // First call returns undefined (mode doesn't exist) + .mockReturnValue({ + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }) // Subsequent calls return default mode + + // Mock provider settings manager + ;(provider as any).providerSettingsManager = { + getModeConfigId: vi.fn().mockResolvedValue(undefined), + listConfig: vi.fn().mockResolvedValue([]), + } + + // Spy on log method to verify warning was logged + const logSpy = vi.spyOn(provider, "log") + + // Create history item with non-existent mode + const historyItem = { + id: "test-id", + ts: Date.now(), + task: "Test task", + mode: "non-existent-mode", // This mode doesn't exist + number: 1, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + // Initialize with history item + await provider.initClineWithHistoryItem(historyItem) + + // Verify mode validation occurred + expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled() + expect(getModeBySlug).toHaveBeenCalledWith("non-existent-mode", expect.any(Array)) + + // Verify fallback to default mode + expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "code") + expect(logSpy).toHaveBeenCalledWith( + "Mode 'non-existent-mode' from history no longer exists. Falling back to default mode 'code'.", + ) + + // Verify history item was updated with default mode + expect(historyItem.mode).toBe("code") + }) + + test("preserves mode when it exists in custom modes", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Mock custom modes that include the saved mode + const mockCustomModesManager = { + getCustomModes: vi.fn().mockResolvedValue([ + { + slug: "custom-mode", + name: "Custom Mode", + roleDefinition: "Custom role", + groups: ["read", "edit"] as const, + }, + ]), + dispose: vi.fn(), + } + ;(provider as any).customModesManager = mockCustomModesManager + + // Mock getModeBySlug to return the custom mode + const { getModeBySlug } = await import("../../../shared/modes") + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "custom-mode", + name: "Custom Mode", + roleDefinition: "Custom role", + groups: ["read", "edit"], + }) + + // Mock provider settings manager + ;(provider as any).providerSettingsManager = { + getModeConfigId: vi.fn().mockResolvedValue("config-id"), + listConfig: vi + .fn() + .mockResolvedValue([{ name: "test-config", id: "config-id", apiProvider: "anthropic" }]), + activateProfile: vi + .fn() + .mockResolvedValue({ name: "test-config", id: "config-id", apiProvider: "anthropic" }), + } + + // Spy on log method to verify no warning was logged + const logSpy = vi.spyOn(provider, "log") + + // Create history item with existing custom mode + const historyItem = { + id: "test-id", + ts: Date.now(), + task: "Test task", + mode: "custom-mode", + number: 1, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + // Initialize with history item + await provider.initClineWithHistoryItem(historyItem) + + // Verify mode validation occurred + expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled() + expect(getModeBySlug).toHaveBeenCalledWith("custom-mode", expect.any(Array)) + + // Verify mode was preserved + expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "custom-mode") + expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining("no longer exists")) + + // Verify history item mode was not changed + expect(historyItem.mode).toBe("custom-mode") + }) + + test("preserves mode when it exists in built-in modes", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Mock no custom modes + const mockCustomModesManager = { + getCustomModes: vi.fn().mockResolvedValue([]), + dispose: vi.fn(), + } + ;(provider as any).customModesManager = mockCustomModesManager + + // Mock getModeBySlug to return built-in architect mode + const { getModeBySlug } = await import("../../../shared/modes") + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "architect", + name: "Architect Mode", + roleDefinition: "You are an architect", + groups: ["read", "edit"], + }) + + // Mock provider settings manager + ;(provider as any).providerSettingsManager = { + getModeConfigId: vi.fn().mockResolvedValue(undefined), + listConfig: vi.fn().mockResolvedValue([]), + } + + // Create history item with built-in mode + const historyItem = { + id: "test-id", + ts: Date.now(), + task: "Test task", + mode: "architect", + number: 1, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + // Initialize with history item + await provider.initClineWithHistoryItem(historyItem) + + // Verify mode was preserved + expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect") + + // Verify history item mode was not changed + expect(historyItem.mode).toBe("architect") + }) + + test("handles history items without mode property", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Mock provider settings manager + ;(provider as any).providerSettingsManager = { + getModeConfigId: vi.fn().mockResolvedValue(undefined), + listConfig: vi.fn().mockResolvedValue([]), + } + + // Create history item without mode + const historyItem = { + id: "test-id", + ts: Date.now(), + task: "Test task", + // No mode property + number: 1, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + // Initialize with history item + await provider.initClineWithHistoryItem(historyItem) + + // Verify no mode validation occurred (mode update not called) + expect(mockContext.globalState.update).not.toHaveBeenCalledWith("mode", expect.any(String)) + }) + + test("continues with task restoration even if mode config loading fails", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Mock custom modes + const mockCustomModesManager = { + getCustomModes: vi.fn().mockResolvedValue([]), + dispose: vi.fn(), + } + ;(provider as any).customModesManager = mockCustomModesManager + + // Mock getModeBySlug to return built-in mode + const { getModeBySlug } = await import("../../../shared/modes") + vi.mocked(getModeBySlug).mockReturnValue({ + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }) + + // Mock provider settings manager to throw error + ;(provider as any).providerSettingsManager = { + getModeConfigId: vi.fn().mockResolvedValue("config-id"), + listConfig: vi + .fn() + .mockResolvedValue([{ name: "test-config", id: "config-id", apiProvider: "anthropic" }]), + activateProfile: vi.fn().mockRejectedValue(new Error("Failed to load config")), + } + + // Spy on log method + const logSpy = vi.spyOn(provider, "log") + + // Create history item + const historyItem = { + id: "test-id", + ts: Date.now(), + task: "Test task", + mode: "code", + number: 1, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + // Initialize with history item - should not throw + await expect(provider.initClineWithHistoryItem(historyItem)).resolves.not.toThrow() + + // Verify error was logged but task restoration continued + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("Failed to restore API configuration for mode 'code'"), + ) + }) + }) + describe("updateCustomMode", () => { test("updates both file and state when updating custom mode", async () => { await provider.resolveWebviewView(mockWebviewView) diff --git a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts new file mode 100644 index 0000000000..6b19b47a38 --- /dev/null +++ b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts @@ -0,0 +1,1170 @@ +// npx vitest core/webview/__tests__/ClineProvider.sticky-mode.spec.ts + +import * as vscode from "vscode" +import { TelemetryService } from "@roo-code/telemetry" +import { ClineProvider } from "../ClineProvider" +import { ContextProxy } from "../../config/ContextProxy" +import { Task } from "../../task/Task" +import type { HistoryItem, ProviderName } from "@roo-code/types" + +// Mock setup +vi.mock("vscode", () => ({ + ExtensionContext: vi.fn(), + OutputChannel: vi.fn(), + WebviewView: vi.fn(), + Uri: { + joinPath: vi.fn(), + file: vi.fn(), + }, + CodeActionKind: { + QuickFix: { value: "quickfix" }, + RefactorRewrite: { value: "refactor.rewrite" }, + }, + commands: { + executeCommand: vi.fn().mockResolvedValue(undefined), + }, + window: { + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + showErrorMessage: vi.fn(), + }, + workspace: { + getConfiguration: vi.fn().mockReturnValue({ + get: vi.fn().mockReturnValue([]), + update: vi.fn(), + }), + onDidChangeConfiguration: vi.fn().mockImplementation(() => ({ + dispose: vi.fn(), + })), + onDidSaveTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidChangeTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + onDidCloseTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + }, + env: { + uriScheme: "vscode", + language: "en", + appName: "Visual Studio Code", + }, + ExtensionMode: { + Production: 1, + Development: 2, + Test: 3, + }, + version: "1.85.0", +})) +// Create a counter for unique task IDs +let taskIdCounter = 0 + +vi.mock("../../task/Task", () => ({ + Task: vi.fn().mockImplementation((options) => ({ + taskId: options.taskId || `test-task-id-${++taskIdCounter}`, + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + overwriteClineMessages: vi.fn(), + overwriteApiConversationHistory: vi.fn(), + abortTask: vi.fn(), + handleWebviewAskResponse: vi.fn(), + getTaskNumber: vi.fn().mockReturnValue(0), + setTaskNumber: vi.fn(), + setParentTask: vi.fn(), + setRootTask: vi.fn(), + emit: vi.fn(), + parentTask: options.parentTask, + })), +})) +vi.mock("../../prompts/sections/custom-instructions") +vi.mock("../../../utils/safeWriteJson") +vi.mock("../../../api", () => ({ + buildApiHandler: vi.fn().mockReturnValue({ + getModel: vi.fn().mockReturnValue({ + id: "claude-3-sonnet", + info: { supportsComputerUse: false }, + }), + }), +})) +vi.mock("../../../integrations/workspace/WorkspaceTracker", () => ({ + default: vi.fn().mockImplementation(() => ({ + initializeFilePaths: vi.fn(), + dispose: vi.fn(), + })), +})) +vi.mock("../../diff/strategies/multi-search-replace", () => ({ + MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(() => ({ + getToolDescription: () => "test", + getName: () => "test-strategy", + applyDiff: vi.fn(), + })), +})) +vi.mock("@roo-code/cloud", () => ({ + CloudService: { + hasInstance: vi.fn().mockReturnValue(true), + get instance() { + return { + isAuthenticated: vi.fn().mockReturnValue(false), + } + }, + }, + getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), +})) +vi.mock("../../../shared/modes", () => ({ + modes: [ + { + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }, + { + slug: "architect", + name: "Architect Mode", + roleDefinition: "You are an architect", + groups: ["read", "edit"], + }, + ], + getModeBySlug: vi.fn().mockReturnValue({ + slug: "code", + name: "Code Mode", + roleDefinition: "You are a code assistant", + groups: ["read", "edit", "browser"], + }), + defaultModeSlug: "code", +})) +vi.mock("../../prompts/system", () => ({ + SYSTEM_PROMPT: vi.fn().mockResolvedValue("mocked system prompt"), + codeMode: "code", +})) +vi.mock("../../../api/providers/fetchers/modelCache", () => ({ + getModels: vi.fn().mockResolvedValue({}), + flushModels: vi.fn(), +})) +vi.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: vi.fn().mockResolvedValue("Mock file content"), +})) +vi.mock("p-wait-for", () => ({ + default: vi.fn().mockImplementation(async () => Promise.resolve()), +})) +vi.mock("fs/promises", () => ({ + mkdir: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue(""), + unlink: vi.fn().mockResolvedValue(undefined), + rmdir: vi.fn().mockResolvedValue(undefined), +})) +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + hasInstance: vi.fn().mockReturnValue(true), + createInstance: vi.fn(), + get instance() { + return { + trackEvent: vi.fn(), + trackError: vi.fn(), + setProvider: vi.fn(), + captureModeSwitch: vi.fn(), + } + }, + }, +})) + +describe("ClineProvider - Sticky Mode", () => { + let provider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockWebviewView: vscode.WebviewView + let mockPostMessage: any + + beforeEach(() => { + vi.clearAllMocks() + + if (!TelemetryService.hasInstance()) { + TelemetryService.createInstance([]) + } + + const globalState: Record = { + mode: "code", + currentApiConfigName: "test-config", + } + + const secrets: Record = {} + + mockContext = { + extensionPath: "/test/path", + extensionUri: {} as vscode.Uri, + globalState: { + get: vi.fn().mockImplementation((key: string) => globalState[key]), + update: vi.fn().mockImplementation((key: string, value: string | undefined) => { + globalState[key] = value + return Promise.resolve() + }), + keys: vi.fn().mockImplementation(() => Object.keys(globalState)), + }, + secrets: { + get: vi.fn().mockImplementation((key: string) => secrets[key]), + store: vi.fn().mockImplementation((key: string, value: string | undefined) => { + secrets[key] = value + return Promise.resolve() + }), + delete: vi.fn().mockImplementation((key: string) => { + delete secrets[key] + return Promise.resolve() + }), + }, + subscriptions: [], + extension: { + packageJSON: { version: "1.0.0" }, + }, + globalStorageUri: { + fsPath: "/test/storage/path", + }, + } as unknown as vscode.ExtensionContext + + mockOutputChannel = { + appendLine: vi.fn(), + clear: vi.fn(), + dispose: vi.fn(), + } as unknown as vscode.OutputChannel + + mockPostMessage = vi.fn() + + mockWebviewView = { + webview: { + postMessage: mockPostMessage, + html: "", + options: {}, + onDidReceiveMessage: vi.fn(), + asWebviewUri: vi.fn(), + cspSource: "vscode-webview://test-csp-source", + }, + visible: true, + onDidDispose: vi.fn().mockImplementation((callback) => { + callback() + return { dispose: vi.fn() } + }), + onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })), + } as unknown as vscode.WebviewView + + provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext)) + + // Mock getMcpHub method + provider.getMcpHub = vi.fn().mockReturnValue({ + listTools: vi.fn().mockResolvedValue([]), + callTool: vi.fn().mockResolvedValue({ content: [] }), + listResources: vi.fn().mockResolvedValue([]), + readResource: vi.fn().mockResolvedValue({ contents: [] }), + getAllServers: vi.fn().mockReturnValue([]), + }) + }) + + describe("handleModeSwitch", () => { + beforeEach(async () => { + await provider.resolveWebviewView(mockWebviewView) + }) + + it("should save mode to task metadata when switching modes", async () => { + // Create a mock task + const mockTask = new Task({ + provider, + apiConfiguration: { apiProvider: "openrouter" }, + }) + + // Get the actual taskId from the mock + const taskId = (mockTask as any).taskId || "test-task-id" + + // Mock getGlobalState to return task history + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + }, + ]) + + // Mock updateTaskHistory to track calls + const updateTaskHistorySpy = vi + .spyOn(provider, "updateTaskHistory") + .mockImplementation(() => Promise.resolve([])) + + // Add task to provider stack + await provider.addClineToStack(mockTask) + + // Switch mode + await provider.handleModeSwitch("architect") + + // Verify mode was updated in global state + expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect") + + // Verify task history was updated with new mode + expect(updateTaskHistorySpy).toHaveBeenCalledWith( + expect.objectContaining({ + id: taskId, + mode: "architect", + }), + ) + }) + + it("should update task's taskMode property when switching modes", async () => { + // Create a mock task with initial mode + const mockTask = { + taskId: "test-task-id", + taskMode: "code", // Initial mode + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + // Add task to provider stack + await provider.addClineToStack(mockTask as any) + + // Mock getGlobalState to return task history + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: mockTask.taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + }, + ]) + + // Mock updateTaskHistory + vi.spyOn(provider, "updateTaskHistory").mockImplementation(() => Promise.resolve([])) + + // Switch mode + await provider.handleModeSwitch("architect") + + // Verify task's _taskMode property was updated (using private property) + expect((mockTask as any)._taskMode).toBe("architect") + + // Verify emit was called with taskModeSwitched event + expect(mockTask.emit).toHaveBeenCalledWith("taskModeSwitched", mockTask.taskId, "architect") + }) + + it("should update task history with new mode when active task exists", async () => { + // Create a mock task with history + const mockTask = new Task({ + provider, + apiConfiguration: { apiProvider: "openrouter" }, + }) + + // Get the actual taskId from the mock + const taskId = (mockTask as any).taskId || "test-task-id" + + // Mock getGlobalState to return task history + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + }, + ]) + + // Mock updateTaskHistory to track calls + const updateTaskHistorySpy = vi + .spyOn(provider, "updateTaskHistory") + .mockImplementation(() => Promise.resolve([])) + + // Add task to provider stack + await provider.addClineToStack(mockTask) + + // Switch mode + await provider.handleModeSwitch("architect") + + // Verify updateTaskHistory was called with mode in the history item + expect(updateTaskHistorySpy).toHaveBeenCalledWith( + expect.objectContaining({ + id: taskId, + mode: "architect", + }), + ) + }) + }) + + describe("initClineWithHistoryItem", () => { + it("should restore mode from history item when reopening task", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a history item with saved mode + const historyItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + mode: "architect", // Saved mode + } + + // Mock updateGlobalState to track mode updates + const updateGlobalStateSpy = vi.spyOn(provider as any, "updateGlobalState").mockResolvedValue(undefined) + + // Initialize task with history item + await provider.initClineWithHistoryItem(historyItem) + + // Verify mode was restored via updateGlobalState + expect(updateGlobalStateSpy).toHaveBeenCalledWith("mode", "architect") + }) + + it("should use current mode if history item has no saved mode", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Set current mode + mockContext.globalState.get = vi.fn().mockImplementation((key: string) => { + if (key === "mode") return "code" + return undefined + }) + + // Create a history item without saved mode + const historyItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + // No mode field + } + + // Mock getTaskWithId + vi.spyOn(provider, "getTaskWithId").mockResolvedValue({ + historyItem, + taskDirPath: "/test/path", + apiConversationHistoryFilePath: "/test/path/api_history.json", + uiMessagesFilePath: "/test/path/ui_messages.json", + apiConversationHistory: [], + }) + + // Mock handleModeSwitch to track calls + const handleModeSwitchSpy = vi.spyOn(provider, "handleModeSwitch").mockResolvedValue() + + // Initialize task with history item + await provider.initClineWithHistoryItem(historyItem) + + // Verify mode was not changed (should use current mode) + expect(handleModeSwitchSpy).not.toHaveBeenCalled() + }) + }) + + describe("Task metadata persistence", () => { + it("should include mode in task metadata when creating history items", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Set current mode + await provider.setValue("mode", "debug") + + // Create a mock task + const mockTask = new Task({ + provider, + apiConfiguration: { apiProvider: "openrouter" }, + }) + + // Get the actual taskId from the mock + const taskId = (mockTask as any).taskId || "test-task-id" + + // Mock getGlobalState to return task history with our task + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + }, + ]) + + // Mock updateTaskHistory to capture the updated history item + let updatedHistoryItem: any + vi.spyOn(provider, "updateTaskHistory").mockImplementation((item) => { + updatedHistoryItem = item + return Promise.resolve([item]) + }) + + // Add task to provider stack + await provider.addClineToStack(mockTask) + + // Trigger a mode switch + await provider.handleModeSwitch("debug") + + // Verify mode was included in the updated history item + expect(updatedHistoryItem).toBeDefined() + expect(updatedHistoryItem.mode).toBe("debug") + }) + }) + + describe("Integration with new_task tool", () => { + it("should preserve parent task mode when creating subtasks", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // This test verifies that when using the new_task tool to create a subtask, + // the parent task's mode is preserved and not changed by the subtask's mode switch + + // Set initial mode to architect + await provider.setValue("mode", "architect") + + // Create parent task + const parentTask = new Task({ + provider, + apiConfiguration: { apiProvider: "openrouter" }, + }) + + // Get the actual taskId from the mock + const parentTaskId = (parentTask as any).taskId || "parent-task-id" + + // Create a simple task history tracking object + const taskModes: Record = { + [parentTaskId]: "architect", // Parent starts with architect mode + } + + // Mock getGlobalState to return task history + const getGlobalStateMock = vi.spyOn(provider as any, "getGlobalState") + getGlobalStateMock.mockImplementation((key) => { + if (key === "taskHistory") { + return Object.entries(taskModes).map(([id, mode]) => ({ + id, + ts: Date.now(), + task: `Task ${id}`, + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + mode, + })) + } + // Return empty array for other keys + return [] + }) + + // Mock updateTaskHistory to track mode changes + const updateTaskHistoryMock = vi.spyOn(provider, "updateTaskHistory") + updateTaskHistoryMock.mockImplementation((item) => { + // The handleModeSwitch method updates the task history for the current task + // We should only update the task that matches the item.id + if (item.id && item.mode !== undefined) { + taskModes[item.id] = item.mode + } + return Promise.resolve([]) + }) + + // Add parent task to stack + await provider.addClineToStack(parentTask) + + // Create a subtask (simulating new_task tool behavior) + const subtask = new Task({ + provider, + apiConfiguration: { apiProvider: "openrouter" }, + parentTask: parentTask, + }) + const subtaskId = (subtask as any).taskId || "subtask-id" + + // Initialize subtask with parent's mode + taskModes[subtaskId] = "architect" + + // Mock getCurrentCline to return the parent task initially + const getCurrentClineMock = vi.spyOn(provider, "getCurrentCline") + getCurrentClineMock.mockReturnValue(parentTask as any) + + // Add subtask to stack + await provider.addClineToStack(subtask) + + // Now mock getCurrentCline to return the subtask (simulating stack behavior) + getCurrentClineMock.mockReturnValue(subtask as any) + + // Switch subtask to code mode - this should only affect the subtask + await provider.handleModeSwitch("code") + + // Verify that the parent task's mode is still architect + expect(taskModes[parentTaskId]).toBe("architect") + + // Verify the subtask has code mode + expect(taskModes[subtaskId]).toBe("code") + }) + }) + + describe("Error handling", () => { + it("should handle errors gracefully when saving mode fails", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a mock task that throws on save + const mockTask = new Task({ + provider, + apiConfiguration: { apiProvider: "openrouter" }, + }) + vi.spyOn(mockTask as any, "saveClineMessages").mockRejectedValue(new Error("Save failed")) + + // Add task to provider stack + await provider.addClineToStack(mockTask) + + // Switch mode - should not throw + await expect(provider.handleModeSwitch("architect")).resolves.not.toThrow() + + // Verify mode was still updated in global state + expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect") + }) + + it("should handle null/undefined mode gracefully", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a history item with null mode + const historyItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + mode: null as any, // Invalid mode + } + + // Mock getTaskWithId + vi.spyOn(provider, "getTaskWithId").mockResolvedValue({ + historyItem, + taskDirPath: "/test/path", + apiConversationHistoryFilePath: "/test/path/api_history.json", + uiMessagesFilePath: "/test/path/ui_messages.json", + apiConversationHistory: [], + }) + + // Mock handleModeSwitch to track calls + const handleModeSwitchSpy = vi.spyOn(provider, "handleModeSwitch").mockResolvedValue() + + // Initialize task with history item - should not throw + await expect(provider.initClineWithHistoryItem(historyItem)).resolves.not.toThrow() + + // Verify mode switch was not called with null + expect(handleModeSwitchSpy).not.toHaveBeenCalledWith(null) + }) + + it("should restore API configuration when restoring task from history with mode", async () => { + // Setup: Configure different API configs for different modes + const codeApiConfig = { apiProvider: "anthropic" as ProviderName, anthropicApiKey: "code-key" } + const architectApiConfig = { apiProvider: "openai" as ProviderName, openAiApiKey: "architect-key" } + + // Save API configs + await provider.upsertProviderProfile("code-config", codeApiConfig) + await provider.upsertProviderProfile("architect-config", architectApiConfig) + + // Get the config IDs + const codeConfigId = provider.getProviderProfileEntry("code-config")?.id + const architectConfigId = provider.getProviderProfileEntry("architect-config")?.id + + // Associate configs with modes + await provider.providerSettingsManager.setModeConfig("code", codeConfigId!) + await provider.providerSettingsManager.setModeConfig("architect", architectConfigId!) + + // Start in code mode with code config + await provider.handleModeSwitch("code") + + // Create a history item with architect mode + const historyItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + mode: "architect", // Task was created in architect mode + } + + // Restore the task from history + await provider.initClineWithHistoryItem(historyItem) + + // Verify that the mode was restored + const state = await provider.getState() + expect(state.mode).toBe("architect") + + // Verify that the API configuration was also restored + expect(state.currentApiConfigName).toBe("architect-config") + expect(state.apiConfiguration.apiProvider).toBe("openai") + }) + + it("should handle mode deletion between sessions", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a history item with a mode that no longer exists + const historyItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + mode: "deleted-mode", // Mode that doesn't exist + } + + // Mock getModeBySlug to return undefined for deleted mode + const { getModeBySlug } = await import("../../../shared/modes") + vi.mocked(getModeBySlug).mockReturnValue(undefined) + + // Mock getTaskWithId + vi.spyOn(provider, "getTaskWithId").mockResolvedValue({ + historyItem, + taskDirPath: "/test/path", + apiConversationHistoryFilePath: "/test/path/api_history.json", + uiMessagesFilePath: "/test/path/ui_messages.json", + apiConversationHistory: [], + }) + + // Mock handleModeSwitch to track calls + const handleModeSwitchSpy = vi.spyOn(provider, "handleModeSwitch").mockResolvedValue() + + // Initialize task with history item - should not throw + await expect(provider.initClineWithHistoryItem(historyItem)).resolves.not.toThrow() + + // Verify mode switch was not called with deleted mode + expect(handleModeSwitchSpy).not.toHaveBeenCalledWith("deleted-mode") + }) + }) + + describe("Concurrent mode switches", () => { + it("should handle concurrent mode switches on the same task", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a mock task + const mockTask = { + taskId: "test-task-id", + _taskMode: "code", + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + // Add task to provider stack + await provider.addClineToStack(mockTask as any) + + // Mock getGlobalState to return task history + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: mockTask.taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + }, + ]) + + // Mock updateTaskHistory + const updateTaskHistorySpy = vi + .spyOn(provider, "updateTaskHistory") + .mockImplementation(() => Promise.resolve([])) + + // Clear previous calls to globalState.update + vi.mocked(mockContext.globalState.update).mockClear() + + // Simulate concurrent mode switches + const switches = [ + provider.handleModeSwitch("architect"), + provider.handleModeSwitch("debug"), + provider.handleModeSwitch("code"), + ] + + await Promise.all(switches) + + // Find the last mode update call + const modeCalls = vi.mocked(mockContext.globalState.update).mock.calls.filter((call) => call[0] === "mode") + const lastModeCall = modeCalls[modeCalls.length - 1] + + // Verify the last mode switch wins + expect(lastModeCall).toEqual(["mode", "code"]) + + // Verify task history was updated with final mode + const lastCall = updateTaskHistorySpy.mock.calls[updateTaskHistorySpy.mock.calls.length - 1] + expect(lastCall[0]).toMatchObject({ + id: mockTask.taskId, + mode: "code", + }) + }) + + it("should handle mode switches during task save operations", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a mock task with slow save operation + const mockTask = { + taskId: "test-task-id", + _taskMode: "code", + emit: vi.fn(), + saveClineMessages: vi.fn().mockImplementation(async () => { + // Simulate slow save + await new Promise((resolve) => setTimeout(resolve, 100)) + }), + clineMessages: [], + apiConversationHistory: [], + } + + // Add task to provider stack + await provider.addClineToStack(mockTask as any) + + // Mock getGlobalState + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: mockTask.taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + mode: "code", + }, + ]) + + // Mock updateTaskHistory + vi.spyOn(provider, "updateTaskHistory").mockImplementation(() => Promise.resolve([])) + + // Start a save operation + const savePromise = mockTask.saveClineMessages() + + // Switch mode during save + await provider.handleModeSwitch("architect") + + // Wait for save to complete + await savePromise + + // Task should have the new mode + expect((mockTask as any)._taskMode).toBe("architect") + }) + }) + + describe("Mode switch failure scenarios", () => { + it("should handle invalid mode gracefully", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // The provider actually does switch to invalid modes + // This test should verify that behavior + const mockTask = { + taskId: "test-task-id", + _taskMode: "code", + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + // Add task to provider stack + await provider.addClineToStack(mockTask as any) + + // Clear previous calls + vi.mocked(mockContext.globalState.update).mockClear() + + // Try to switch to invalid mode - it will actually switch + await provider.handleModeSwitch("invalid-mode" as any) + + // The mode WILL be updated to invalid-mode (this is the actual behavior) + expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "invalid-mode") + }) + + it("should handle errors during mode switch gracefully", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a mock task that throws on emit + const mockTask = { + taskId: "test-task-id", + _taskMode: "code", + emit: vi.fn().mockImplementation(() => { + throw new Error("Emit failed") + }), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + // Add task to provider stack + await provider.addClineToStack(mockTask as any) + + // Mock console.error to suppress error output + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // The handleModeSwitch method doesn't catch errors from emit, so it will throw + // This is the actual behavior based on the test failure + await expect(provider.handleModeSwitch("architect")).rejects.toThrow("Emit failed") + + consoleErrorSpy.mockRestore() + }) + + it("should handle updateTaskHistory failures", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a mock task + const mockTask = { + taskId: "test-task-id", + _taskMode: "code", + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + // Add task to provider stack + await provider.addClineToStack(mockTask as any) + + // Mock getGlobalState + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: mockTask.taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + }, + ]) + + // Mock updateTaskHistory to throw error + vi.spyOn(provider, "updateTaskHistory").mockRejectedValue(new Error("Update failed")) + + // Mock console.error + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // The updateTaskHistory failure will cause handleModeSwitch to throw + // This is the actual behavior based on the test failure + await expect(provider.handleModeSwitch("architect")).rejects.toThrow("Update failed") + + consoleErrorSpy.mockRestore() + }) + }) + + describe("Multiple tasks switching modes simultaneously", () => { + it("should handle multiple tasks switching modes independently", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create multiple mock tasks + const task1 = { + taskId: "task-1", + _taskMode: "code", + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + const task2 = { + taskId: "task-2", + _taskMode: "architect", + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + const task3 = { + taskId: "task-3", + _taskMode: "debug", + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + } + + // Add tasks to provider stack + await provider.addClineToStack(task1 as any) + await provider.addClineToStack(task2 as any) + await provider.addClineToStack(task3 as any) + + // Mock getGlobalState to return all tasks + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: task1.taskId, + ts: Date.now(), + task: "Task 1", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + mode: "code", + }, + { + id: task2.taskId, + ts: Date.now(), + task: "Task 2", + number: 2, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + mode: "architect", + }, + { + id: task3.taskId, + ts: Date.now(), + task: "Task 3", + number: 3, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + mode: "debug", + }, + ]) + + // Mock updateTaskHistory + const updateTaskHistorySpy = vi + .spyOn(provider, "updateTaskHistory") + .mockImplementation(() => Promise.resolve([])) + + // Mock getCurrentCline to return different tasks + const getCurrentClineSpy = vi.spyOn(provider, "getCurrentCline") + + // Simulate simultaneous mode switches for different tasks + getCurrentClineSpy.mockReturnValue(task1 as any) + const switch1 = provider.handleModeSwitch("architect") + + getCurrentClineSpy.mockReturnValue(task2 as any) + const switch2 = provider.handleModeSwitch("debug") + + getCurrentClineSpy.mockReturnValue(task3 as any) + const switch3 = provider.handleModeSwitch("code") + + await Promise.all([switch1, switch2, switch3]) + + // Verify each task was updated with its new mode + expect(task1._taskMode).toBe("architect") + expect(task2._taskMode).toBe("debug") + expect(task3._taskMode).toBe("code") + + // Verify emit was called for each task + expect(task1.emit).toHaveBeenCalledWith("taskModeSwitched", task1.taskId, "architect") + expect(task2.emit).toHaveBeenCalledWith("taskModeSwitched", task2.taskId, "debug") + expect(task3.emit).toHaveBeenCalledWith("taskModeSwitched", task3.taskId, "code") + }) + }) + + describe("Task initialization timing edge cases", () => { + it("should handle mode restoration during slow task initialization", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create a history item with saved mode + const historyItem: HistoryItem = { + id: "test-task-id", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 200, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.001, + mode: "architect", + } + + // Mock getTaskWithId to be slow + vi.spyOn(provider, "getTaskWithId").mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return { + historyItem, + taskDirPath: "/test/path", + apiConversationHistoryFilePath: "/test/path/api_history.json", + uiMessagesFilePath: "/test/path/ui_messages.json", + apiConversationHistory: [], + } + }) + + // Clear any previous calls + vi.clearAllMocks() + + // Start initialization + const initPromise = provider.initClineWithHistoryItem(historyItem) + + // Try to switch mode during initialization + await provider.handleModeSwitch("code") + + // Wait for initialization to complete + await initPromise + + // Check all mode update calls + const modeCalls = vi.mocked(mockContext.globalState.update).mock.calls.filter((call) => call[0] === "mode") + + // Based on the actual behavior, the mode switch to "code" happens and persists + // The history mode restoration doesn't override it + const lastModeCall = modeCalls[modeCalls.length - 1] + expect(lastModeCall).toEqual(["mode", "code"]) + }) + + it("should handle rapid task switches during mode changes", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Create multiple tasks + const tasks = Array.from({ length: 5 }, (_, i) => ({ + taskId: `task-${i}`, + _taskMode: "code", + emit: vi.fn(), + saveClineMessages: vi.fn(), + clineMessages: [], + apiConversationHistory: [], + })) + + // Add all tasks to provider + for (const task of tasks) { + await provider.addClineToStack(task as any) + } + + // Mock getCurrentCline + const getCurrentClineSpy = vi.spyOn(provider, "getCurrentCline") + + // Rapidly switch between tasks and modes + const switches: Promise[] = [] + tasks.forEach((task, index) => { + getCurrentClineSpy.mockReturnValue(task as any) + const mode = ["architect", "debug", "code"][index % 3] + switches.push(provider.handleModeSwitch(mode as any)) + }) + + await Promise.all(switches) + + // Each task should have been updated + tasks.forEach((task) => { + expect(task.emit).toHaveBeenCalled() + }) + }) + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c739c2ade8..763e118125 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -676,8 +676,8 @@ export const webviewMessageHandler = async ( break case "requestHuggingFaceModels": try { - const { getHuggingFaceModels } = await import("../../api/huggingface-models") - const huggingFaceModelsResponse = await getHuggingFaceModels() + const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface") + const huggingFaceModelsResponse = await getHuggingFaceModelsWithMetadata() provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: huggingFaceModelsResponse.models, @@ -1265,6 +1265,14 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxReadFileLine", message.value) await provider.postStateToWebview() break + case "maxImageFileSize": + await updateGlobalState("maxImageFileSize", message.value) + await provider.postStateToWebview() + break + case "maxTotalImageSize": + await updateGlobalState("maxTotalImageSize", message.value) + await provider.postStateToWebview() + break case "maxConcurrentFileReads": const valueToSave = message.value // Capture the value intended for saving await updateGlobalState("maxConcurrentFileReads", valueToSave) @@ -2092,6 +2100,19 @@ export const webviewMessageHandler = async ( } } } + } else { + // No workspace open - send error status + provider.log("Cannot save code index settings: No workspace folder open") + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Error", + message: t("embeddings:orchestrator.indexingRequiresWorkspace"), + processedItems: 0, + totalItems: 0, + currentItemUnit: "items", + }, + }) } } catch (error) { provider.log(`Error saving code index settings: ${error.message || error}`) @@ -2105,7 +2126,22 @@ export const webviewMessageHandler = async ( } case "requestIndexingStatus": { - const status = provider.codeIndexManager!.getCurrentStatus() + const manager = provider.codeIndexManager + if (!manager) { + // No workspace open - send error status + provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Error", + message: t("embeddings:orchestrator.indexingRequiresWorkspace"), + processedItems: 0, + totalItems: 0, + currentItemUnit: "items", + }, + }) + return + } + const status = manager.getCurrentStatus() provider.postMessageToWebview({ type: "indexingStatusUpdate", values: status, @@ -2136,7 +2172,22 @@ export const webviewMessageHandler = async ( } case "startIndexing": { try { - const manager = provider.codeIndexManager! + const manager = provider.codeIndexManager + if (!manager) { + // No workspace open - send error status + provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Error", + message: t("embeddings:orchestrator.indexingRequiresWorkspace"), + processedItems: 0, + totalItems: 0, + currentItemUnit: "items", + }, + }) + provider.log("Cannot start indexing: No workspace folder open") + return + } if (manager.isFeatureEnabled && manager.isFeatureConfigured) { if (!manager.isInitialized) { await manager.initialize(provider.contextProxy) @@ -2151,7 +2202,18 @@ export const webviewMessageHandler = async ( } case "clearIndexData": { try { - const manager = provider.codeIndexManager! + const manager = provider.codeIndexManager + if (!manager) { + provider.log("Cannot clear index data: No workspace folder open") + provider.postMessageToWebview({ + type: "indexCleared", + values: { + success: false, + error: t("embeddings:orchestrator.indexingRequiresWorkspace"), + }, + }) + return + } await manager.clearIndexData() provider.postMessageToWebview({ type: "indexCleared", values: { success: true } }) } catch (error) { @@ -2302,5 +2364,199 @@ export const webviewMessageHandler = async ( } break } + case "requestCommands": { + try { + const { getCommands } = await import("../../services/command/commands") + const commands = await getCommands(provider.cwd || "") + + // Convert to the format expected by the frontend + const commandList = commands.map((command) => ({ + name: command.name, + source: command.source, + filePath: command.filePath, + description: command.description, + argumentHint: command.argumentHint, + })) + + await provider.postMessageToWebview({ + type: "commands", + commands: commandList, + }) + } catch (error) { + provider.log(`Error fetching commands: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + // Send empty array on error + await provider.postMessageToWebview({ + type: "commands", + commands: [], + }) + } + break + } + case "openCommandFile": { + try { + if (message.text) { + const { getCommand } = await import("../../services/command/commands") + const command = await getCommand(provider.cwd || "", message.text) + + if (command && command.filePath) { + openFile(command.filePath) + } else { + vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) + } + } + } catch (error) { + provider.log( + `Error opening command file: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.open_command_file")) + } + break + } + case "deleteCommand": { + try { + if (message.text && message.values?.source) { + const { getCommand } = await import("../../services/command/commands") + const command = await getCommand(provider.cwd || "", message.text) + + if (command && command.filePath) { + // Delete the command file + await fs.unlink(command.filePath) + provider.log(`Deleted command file: ${command.filePath}`) + } else { + vscode.window.showErrorMessage(t("common:errors.command_not_found", { name: message.text })) + } + } + } catch (error) { + provider.log(`Error deleting command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + vscode.window.showErrorMessage(t("common:errors.delete_command")) + } + break + } + case "createCommand": { + try { + const source = message.values?.source as "global" | "project" + const fileName = message.text // Custom filename from user input + + if (!source) { + provider.log("Missing source for createCommand") + break + } + + // Determine the commands directory based on source + let commandsDir: string + if (source === "global") { + const globalConfigDir = path.join(os.homedir(), ".roo") + commandsDir = path.join(globalConfigDir, "commands") + } else { + // Project commands + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + if (!workspaceRoot) { + vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command")) + break + } + commandsDir = path.join(workspaceRoot, ".roo", "commands") + } + + // Ensure the commands directory exists + await fs.mkdir(commandsDir, { recursive: true }) + + // Use provided filename or generate a unique one + let commandName: string + if (fileName && fileName.trim()) { + let cleanFileName = fileName.trim() + + // Strip leading slash if present + if (cleanFileName.startsWith("/")) { + cleanFileName = cleanFileName.substring(1) + } + + // Remove .md extension if present BEFORE slugification + if (cleanFileName.toLowerCase().endsWith(".md")) { + cleanFileName = cleanFileName.slice(0, -3) + } + + // Slugify the command name: lowercase, replace spaces with dashes, remove special characters + commandName = cleanFileName + .toLowerCase() + .replace(/\s+/g, "-") // Replace spaces with dashes + .replace(/[^a-z0-9-]/g, "") // Remove special characters except dashes + .replace(/-+/g, "-") // Replace multiple dashes with single dash + .replace(/^-|-$/g, "") // Remove leading/trailing dashes + + // Ensure we have a valid command name + if (!commandName || commandName.length === 0) { + commandName = "new-command" + } + } else { + // Generate a unique command name + commandName = "new-command" + let counter = 1 + let filePath = path.join(commandsDir, `${commandName}.md`) + + while ( + await fs + .access(filePath) + .then(() => true) + .catch(() => false) + ) { + commandName = `new-command-${counter}` + filePath = path.join(commandsDir, `${commandName}.md`) + counter++ + } + } + + const filePath = path.join(commandsDir, `${commandName}.md`) + + // Check if file already exists + if ( + await fs + .access(filePath) + .then(() => true) + .catch(() => false) + ) { + vscode.window.showErrorMessage(t("common:errors.command_already_exists", { commandName })) + break + } + + // Create the command file with template content + const templateContent = t("common:errors.command_template_content") + + await fs.writeFile(filePath, templateContent, "utf8") + provider.log(`Created new command file: ${filePath}`) + + // Open the new file in the editor + openFile(filePath) + + // Refresh commands list + const { getCommands } = await import("../../services/command/commands") + const commands = await getCommands(provider.cwd || "") + const commandList = commands.map((command) => ({ + name: command.name, + source: command.source, + filePath: command.filePath, + description: command.description, + })) + await provider.postMessageToWebview({ + type: "commands", + commands: commandList, + }) + } catch (error) { + provider.log(`Error creating command: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + vscode.window.showErrorMessage(t("common:errors.create_command_failed")) + } + break + } + + case "insertTextIntoTextarea": { + const text = message.text + if (text) { + // Send message to insert text into the chat textarea + await provider.postMessageToWebview({ + type: "insertTextIntoTextarea", + text: text, + }) + } + break + } } } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 39bc1df8f8..0fba764080 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -74,6 +74,13 @@ "share_not_enabled": "La compartició de tasques no està habilitada per a aquesta organització.", "share_task_not_found": "Tasca no trobada o accés denegat.", "delete_rules_folder_failed": "Error en eliminar la carpeta de regles: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Ordre '{{name}}' no trobada", + "open_command_file": "Error en obrir el fitxer d'ordres", + "delete_command": "Error en eliminar l'ordre", + "no_workspace_for_project_command": "No s'ha trobat cap carpeta d'espai de treball per a l'ordre del projecte", + "command_already_exists": "L'ordre \"{{commandName}}\" ja existeix", + "create_command_failed": "Error en crear l'ordre", + "command_template_content": "---\ndescription: \"Breu descripció del que fa aquesta ordre\"\n---\n\nAquesta és una nova ordre slash. Edita aquest fitxer per personalitzar el comportament de l'ordre.", "claudeCode": { "processExited": "El procés Claude Code ha sortit amb codi {{exitCode}}.", "errorOutput": "Sortida d'error: {{output}}", @@ -81,6 +88,11 @@ "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." }, + "gemini": { + "generate_stream": "Error del flux de context de generació de Gemini: {{error}}", + "generate_complete_prompt": "Error de finalització de Gemini: {{error}}", + "sources": "Fonts:" + }, "mode_import_failed": "Ha fallat la importació del mode: {{error}}" }, "warnings": { diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 651bc2b80f..35ddca3b10 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "No s'ha pogut determinar la dimensió del vector per al model '{{modelId}}' amb el proveïdor '{{provider}}'. Comprova els perfils del model o la configuració.", "qdrantUrlMissing": "Falta l'URL de Qdrant per crear l'emmagatzematge de vectors", "codeIndexingNotConfigured": "No es poden crear serveis: La indexació de codi no està configurada correctament" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indexació fallida: No s'ha indexat cap bloc de codi amb èxit. Això normalment indica un problema de configuració de l'embedder.", + "indexingFailedCritical": "Indexació fallida: No s'ha indexat cap bloc de codi amb èxit malgrat trobar fitxers per processar. Això indica una fallida crítica de l'embedder.", + "fileWatcherStarted": "Monitor de fitxers iniciat.", + "fileWatcherStopped": "Monitor de fitxers aturat.", + "failedDuringInitialScan": "Ha fallat durant l'escaneig inicial: {{errorMessage}}", + "unknownError": "Error desconegut", + "indexingRequiresWorkspace": "Indexació requereix una carpeta de workspace oberta" } } diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 87df4056a2..8a5735f5a1 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (només definicions)", "maxLines": " (màxim {{max}} línies)", "showingOnlyLines": "Mostrant només {{shown}} de {{total}} línies totals. Utilitza line_range si necessites llegir més línies", - "contextLimitInstructions": "Per llegir seccions específiques d'aquest fitxer, utilitza el següent format:\n\n\n \n {{path}}\n inici-final\n \n\n\n\nPer exemple, per llegir les línies 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Per llegir seccions específiques d'aquest fitxer, utilitza el següent format:\n\n\n \n {{path}}\n inici-final\n \n\n\n\nPer exemple, per llegir les línies 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "El fitxer d'imatge és massa gran ({{size}} MB). La mida màxima permesa és {{max}} MB.", + "imageWithSize": "Fitxer d'imatge ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.", "codebaseSearch": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index fbd800f602..1c60189b2f 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert.", "mode_import_failed": "Fehler beim Importieren des Modus: {{error}}", "delete_rules_folder_failed": "Fehler beim Löschen des Regelordners: {{rulesFolderPath}}. Fehler: {{error}}", + "command_not_found": "Befehl '{{name}}' nicht gefunden", + "open_command_file": "Fehler beim Öffnen der Befehlsdatei", + "delete_command": "Fehler beim Löschen des Befehls", + "no_workspace_for_project_command": "Kein Arbeitsbereich-Ordner für Projektbefehl gefunden", + "command_already_exists": "Befehl \"{{commandName}}\" existiert bereits", + "create_command_failed": "Fehler beim Erstellen des Befehls", + "command_template_content": "---\ndescription: \"Kurze Beschreibung dessen, was dieser Befehl macht\"\n---\n\nDies ist ein neuer Slash-Befehl. Bearbeite diese Datei, um das Befehlsverhalten anzupassen.", "claudeCode": { "processExited": "Claude Code Prozess wurde mit Code {{exitCode}} beendet.", "errorOutput": "Fehlerausgabe: {{output}}", "processExitedWithError": "Claude Code Prozess wurde mit Code {{exitCode}} beendet. Fehlerausgabe: {{output}}", "stoppedWithReason": "Claude Code wurde mit Grund gestoppt: {{reason}}", "apiKeyModelPlanMismatch": "API-Schlüssel und Abonnement-Pläne erlauben verschiedene Modelle. Stelle sicher, dass das ausgewählte Modell in deinem Plan enthalten ist." + }, + "gemini": { + "generate_stream": "Fehler beim Generieren des Kontext-Streams von Gemini: {{error}}", + "generate_complete_prompt": "Fehler bei der Vervollständigung durch Gemini: {{error}}", + "sources": "Quellen:" } }, "warnings": { diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index 167abc516c..f5aa7339ef 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Konnte die Vektordimension für Modell '{{modelId}}' mit Anbieter '{{provider}}' nicht bestimmen. Überprüfe die Modellprofile oder Konfiguration.", "qdrantUrlMissing": "Qdrant-URL fehlt für die Erstellung des Vektorspeichers", "codeIndexingNotConfigured": "Kann keine Dienste erstellen: Code-Indizierung ist nicht richtig konfiguriert" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indizierung fehlgeschlagen: Keine Code-Blöcke wurden erfolgreich indiziert. Dies deutet normalerweise auf ein Embedder-Konfigurationsproblem hin.", + "indexingFailedCritical": "Indizierung fehlgeschlagen: Keine Code-Blöcke wurden erfolgreich indiziert, obwohl zu verarbeitende Dateien gefunden wurden. Dies deutet auf einen kritischen Embedder-Fehler hin.", + "fileWatcherStarted": "Datei-Watcher gestartet.", + "fileWatcherStopped": "Datei-Watcher gestoppt.", + "failedDuringInitialScan": "Fehler während des ersten Scans: {{errorMessage}}", + "unknownError": "Unbekannter Fehler", + "indexingRequiresWorkspace": "Indexierung erfordert einen offenen Workspace-Ordner" } } diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index 0e2aa51363..b0ada21bb8 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (nur Definitionen)", "maxLines": " (maximal {{max}} Zeilen)", "showingOnlyLines": "Zeige nur {{shown}} von {{total}} Zeilen insgesamt. Verwende line_range, wenn du mehr Zeilen lesen musst", - "contextLimitInstructions": "Um bestimmte Abschnitte dieser Datei zu lesen, verwende das folgende Format:\n\n\n \n {{path}}\n start-ende\n \n\n\n\nZum Beispiel, um die Zeilen 2001-3000 zu lesen:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Um bestimmte Abschnitte dieser Datei zu lesen, verwende das folgende Format:\n\n\n \n {{path}}\n start-ende\n \n\n\n\nZum Beispiel, um die Zeilen 2001-3000 zu lesen:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Die Bilddatei ist zu groß ({{size}} MB). Die maximal erlaubte Größe beträgt {{max}} MB.", + "imageWithSize": "Bilddatei ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen.", "codebaseSearch": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index db6341c312..114e129f45 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Task not found or access denied.", "mode_import_failed": "Failed to import mode: {{error}}", "delete_rules_folder_failed": "Failed to delete rules folder: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Command '{{name}}' not found", + "open_command_file": "Failed to open command file", + "delete_command": "Failed to delete command", + "no_workspace_for_project_command": "No workspace folder found for project command", + "command_already_exists": "Command \"{{commandName}}\" already exists", + "create_command_failed": "Failed to create command", + "command_template_content": "---\ndescription: \"Brief description of what this command does\"\n---\n\nThis is a new slash command. Edit this file to customize the command behavior.", "claudeCode": { "processExited": "Claude Code process exited with code {{exitCode}}.", "errorOutput": "Error output: {{output}}", "processExitedWithError": "Claude Code process exited with code {{exitCode}}. Error output: {{output}}", "stoppedWithReason": "Claude Code stopped with reason: {{reason}}", "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." + }, + "gemini": { + "generate_stream": "Gemini generate context stream error: {{error}}", + "generate_complete_prompt": "Gemini completion error: {{error}}", + "sources": "Sources:" } }, "warnings": { diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 270a8d193b..66465d8c35 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Check model profiles or configuration.", "qdrantUrlMissing": "Qdrant URL missing for vector store creation", "codeIndexingNotConfigured": "Cannot create services: Code indexing is not properly configured" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indexing failed: No code blocks were successfully indexed. This usually indicates an embedder configuration issue.", + "indexingFailedCritical": "Indexing failed: No code blocks were successfully indexed despite finding files to process. This indicates a critical embedder failure.", + "fileWatcherStarted": "File watcher started.", + "fileWatcherStopped": "File watcher stopped.", + "failedDuringInitialScan": "Failed during initial scan: {{errorMessage}}", + "unknownError": "Unknown error", + "indexingRequiresWorkspace": "Indexing requires an open workspace folder" } } diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 9e4e8daadb..ea5a5ee5d8 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (definitions only)", "maxLines": " (max {{max}} lines)", "showingOnlyLines": "Showing only {{shown}} of {{total}} total lines. Use line_range if you need to read more lines", - "contextLimitInstructions": "To read specific sections of this file, use the following format:\n\n\n \n {{path}}\n start-end\n \n\n\n\nFor example, to read lines 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "To read specific sections of this file, use the following format:\n\n\n \n {{path}}\n start-end\n \n\n\n\nFor example, to read lines 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", + "imageWithSize": "Image file ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach.", "codebaseSearch": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index cc04abfdae..62ab4dcb6e 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Tarea no encontrada o acceso denegado.", "mode_import_failed": "Error al importar el modo: {{error}}", "delete_rules_folder_failed": "Error al eliminar la carpeta de reglas: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Comando '{{name}}' no encontrado", + "open_command_file": "Error al abrir el archivo de comandos", + "delete_command": "Error al eliminar el comando", + "no_workspace_for_project_command": "No se encontró carpeta de espacio de trabajo para comando de proyecto", + "command_already_exists": "El comando \"{{commandName}}\" ya existe", + "create_command_failed": "Error al crear comando", + "command_template_content": "---\ndescription: \"Breve descripción de lo que hace este comando\"\n---\n\nEste es un nuevo comando slash. Edita este archivo para personalizar el comportamiento del comando.", "claudeCode": { "processExited": "El proceso de Claude Code terminó con código {{exitCode}}.", "errorOutput": "Salida de error: {{output}}", "processExitedWithError": "El proceso de Claude Code terminó con código {{exitCode}}. Salida de error: {{output}}", "stoppedWithReason": "Claude Code se detuvo por la razón: {{reason}}", "apiKeyModelPlanMismatch": "Las claves API y los planes de suscripción permiten diferentes modelos. Asegúrate de que el modelo seleccionado esté incluido en tu plan." + }, + "gemini": { + "generate_stream": "Error del stream de contexto de generación de Gemini: {{error}}", + "generate_complete_prompt": "Error de finalización de Gemini: {{error}}", + "sources": "Fuentes:" } }, "warnings": { diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index 06478f1d50..51621b6d17 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "No se pudo determinar la dimensión del vector para el modelo '{{modelId}}' con el proveedor '{{provider}}'. Verifica los perfiles del modelo o la configuración.", "qdrantUrlMissing": "Falta la URL de Qdrant para crear el almacén de vectores", "codeIndexingNotConfigured": "No se pueden crear servicios: La indexación de código no está configurada correctamente" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indexación fallida: No se indexaron exitosamente bloques de código. Esto usualmente indica un problema de configuración del incrustador.", + "indexingFailedCritical": "Indexación fallida: No se indexaron exitosamente bloques de código a pesar de encontrar archivos para procesar. Esto indica una falla crítica del incrustador.", + "fileWatcherStarted": "Monitor de archivos iniciado.", + "fileWatcherStopped": "Monitor de archivos detenido.", + "failedDuringInitialScan": "Falló durante el escaneo inicial: {{errorMessage}}", + "unknownError": "Error desconocido", + "indexingRequiresWorkspace": "La indexación requiere una carpeta de workspace abierta" } } diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 8b197aeec8..203fa920f4 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (solo definiciones)", "maxLines": " (máximo {{max}} líneas)", "showingOnlyLines": "Mostrando solo {{shown}} de {{total}} líneas totales. Usa line_range si necesitas leer más líneas", - "contextLimitInstructions": "Para leer secciones específicas de este archivo, usa el siguiente formato:\n\n\n \n {{path}}\n inicio-fin\n \n\n\n\nPor ejemplo, para leer las líneas 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Para leer secciones específicas de este archivo, usa el siguiente formato:\n\n\n \n {{path}}\n inicio-fin\n \n\n\n\nPor ejemplo, para leer las líneas 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "El archivo de imagen es demasiado grande ({{size}} MB). El tamaño máximo permitido es {{max}} MB.", + "imageWithSize": "Archivo de imagen ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.", "codebaseSearch": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 73f3e3d396..aae4d5d7b1 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Tâche non trouvée ou accès refusé.", "mode_import_failed": "Échec de l'importation du mode : {{error}}", "delete_rules_folder_failed": "Échec de la suppression du dossier de règles : {{rulesFolderPath}}. Erreur : {{error}}", + "command_not_found": "Commande '{{name}}' introuvable", + "open_command_file": "Échec de l'ouverture du fichier de commande", + "delete_command": "Échec de la suppression de la commande", + "no_workspace_for_project_command": "Aucun dossier d'espace de travail trouvé pour la commande de projet", + "command_already_exists": "La commande \"{{commandName}}\" existe déjà", + "create_command_failed": "Échec de la création de la commande", + "command_template_content": "---\ndescription: \"Brève description de ce que fait cette commande\"\n---\n\nCeci est une nouvelle commande slash. Modifie ce fichier pour personnaliser le comportement de la commande.", "claudeCode": { "processExited": "Le processus Claude Code s'est terminé avec le code {{exitCode}}.", "errorOutput": "Sortie d'erreur : {{output}}", "processExitedWithError": "Le processus Claude Code s'est terminé avec le code {{exitCode}}. Sortie d'erreur : {{output}}", "stoppedWithReason": "Claude Code s'est arrêté pour la raison : {{reason}}", "apiKeyModelPlanMismatch": "Les clés API et les plans d'abonnement permettent différents modèles. Assurez-vous que le modèle sélectionné est inclus dans votre plan." + }, + "gemini": { + "generate_stream": "Erreur du flux de contexte de génération Gemini : {{error}}", + "generate_complete_prompt": "Erreur d'achèvement de Gemini : {{error}}", + "sources": "Sources :" } }, "warnings": { diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index 167d093e7a..e3a9227234 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Impossible de déterminer la dimension du vecteur pour le modèle '{{modelId}}' avec le fournisseur '{{provider}}'. Vérifie les profils du modèle ou la configuration.", "qdrantUrlMissing": "URL Qdrant manquante pour la création du stockage de vecteurs", "codeIndexingNotConfigured": "Impossible de créer les services : L'indexation du code n'est pas correctement configurée" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Échec de l'indexation : Aucun bloc de code n'a été indexé avec succès. Cela indique généralement un problème de configuration de l'embedder.", + "indexingFailedCritical": "Échec de l'indexation : Aucun bloc de code n'a été indexé avec succès malgré la découverte de fichiers à traiter. Cela indique une défaillance critique de l'embedder.", + "fileWatcherStarted": "Surveillant de fichiers démarré.", + "fileWatcherStopped": "Surveillant de fichiers arrêté.", + "failedDuringInitialScan": "Échec lors du scan initial : {{errorMessage}}", + "unknownError": "Erreur inconnue", + "indexingRequiresWorkspace": "L'indexation nécessite l'ouverture d'un dossier workspace" } } diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index 8fcd12b6ac..5e2827fc17 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (définitions uniquement)", "maxLines": " (max {{max}} lignes)", "showingOnlyLines": "Affichage de seulement {{shown}} sur {{total}} lignes totales. Utilise line_range si tu as besoin de lire plus de lignes", - "contextLimitInstructions": "Pour lire des sections spécifiques de ce fichier, utilise le format suivant :\n\n\n \n {{path}}\n début-fin\n \n\n\n\nPar exemple, pour lire les lignes 2001-3000 :\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Pour lire des sections spécifiques de ce fichier, utilise le format suivant :\n\n\n \n {{path}}\n début-fin\n \n\n\n\nPar exemple, pour lire les lignes 2001-3000 :\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Le fichier image est trop volumineux ({{size}} MB). La taille maximale autorisée est {{max}} MB.", + "imageWithSize": "Fichier image ({{size}} Ko)" }, "toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.", "codebaseSearch": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 03f74e1af5..fae7c42be9 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।", "mode_import_failed": "मोड आयात करने में विफल: {{error}}", "delete_rules_folder_failed": "नियम फ़ोल्डर हटाने में विफल: {{rulesFolderPath}}। त्रुटि: {{error}}", + "command_not_found": "कमांड '{{name}}' नहीं मिला", + "open_command_file": "कमांड फ़ाइल खोलने में विफल", + "delete_command": "कमांड हटाने में विफल", + "no_workspace_for_project_command": "प्रोजेक्ट कमांड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला", + "command_already_exists": "कमांड \"{{commandName}}\" पहले से मौजूद है", + "create_command_failed": "कमांड बनाने में विफल", + "command_template_content": "---\ndescription: \"इस कमांड के कार्य का संक्षिप्त विवरण\"\n---\n\nयह एक नया स्लैश कमांड है। कमांड व्यवहार को कस्टमाइज़ करने के लिए इस फ़ाइल को संपादित करें।", "claudeCode": { "processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।", "errorOutput": "त्रुटि आउटपुट: {{output}}", "processExitedWithError": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई। त्रुटि आउटपुट: {{output}}", "stoppedWithReason": "Claude Code इस कारण से रुका: {{reason}}", "apiKeyModelPlanMismatch": "API कुंजी और सब्सक्रिप्शन प्लान अलग-अलग मॉडल की अनुमति देते हैं। सुनिश्चित करें कि चयनित मॉडल आपकी योजना में शामिल है।" + }, + "gemini": { + "generate_stream": "जेमिनी जनरेट कॉन्टेक्स्ट स्ट्रीम त्रुटि: {{error}}", + "generate_complete_prompt": "जेमिनी समापन त्रुटि: {{error}}", + "sources": "स्रोत:" } }, "warnings": { diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index ad24cfe9d1..01563e833a 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "प्रदाता '{{provider}}' के साथ मॉडल '{{modelId}}' के लिए वेक्टर आयाम निर्धारित नहीं कर सका। मॉडल प्रोफ़ाइल या कॉन्फ़िगरेशन की जांच करें।", "qdrantUrlMissing": "वेक्टर स्टोर बनाने के लिए Qdrant URL गायब है", "codeIndexingNotConfigured": "सेवाएं नहीं बना सकते: कोड इंडेक्सिंग ठीक से कॉन्फ़िगर नहीं है" + }, + "orchestrator": { + "indexingFailedNoBlocks": "इंडेक्सिंग असफल: कोई भी कोड ब्लॉक सफलतापूर्वक इंडेक्स नहीं हुआ। यह आमतौर पर एम्बेडर कॉन्फ़िगरेशन समस्या को दर्शाता है।", + "indexingFailedCritical": "इंडेक्सिंग असफल: प्रोसेस करने के लिए फाइलें मिलने के बावजूद कोई भी कोड ब्लॉक सफलतापूर्वक इंडेक्स नहीं हुआ। यह एक गंभीर एम्बेडर विफलता को दर्शाता है।", + "fileWatcherStarted": "फाइल वॉचर शुरू हुआ।", + "fileWatcherStopped": "फाइल वॉचर रुक गया।", + "failedDuringInitialScan": "प्रारंभिक स्कैन के दौरान असफल: {{errorMessage}}", + "unknownError": "अज्ञात त्रुटि", + "indexingRequiresWorkspace": "इंडेक्सिंग के लिए एक खुला वर्कस्पेस फ़ोल्डर आवश्यक है" } } diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 8520c6905b..e85c8d70b2 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (केवल परिभाषाएँ)", "maxLines": " (अधिकतम {{max}} पंक्तियाँ)", "showingOnlyLines": "कुल {{total}} पंक्तियों में से केवल {{shown}} दिखा रहे हैं। यदि आपको अधिक पंक्तियाँ पढ़नी हैं तो line_range का उपयोग करें", - "contextLimitInstructions": "इस फ़ाइल के विशिष्ट भागों को पढ़ने के लिए, निम्नलिखित प्रारूप का उपयोग करें:\n\n\n \n {{path}}\n शुरुआत-अंत\n \n\n\n\nउदाहरण के लिए, पंक्ति 2001-3000 पढ़ने के लिए:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "इस फ़ाइल के विशिष्ट भागों को पढ़ने के लिए, निम्नलिखित प्रारूप का उपयोग करें:\n\n\n \n {{path}}\n शुरुआत-अंत\n \n\n\n\nउदाहरण के लिए, पंक्ति 2001-3000 पढ़ने के लिए:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "छवि फ़ाइल बहुत बड़ी है ({{size}} MB)। अधिकतम अनुमतित आकार {{max}} MB है।", + "imageWithSize": "छवि फ़ाइल ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।", "codebaseSearch": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 822341f529..eb2db5ac84 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Tugas tidak ditemukan atau akses ditolak.", "mode_import_failed": "Gagal mengimpor mode: {{error}}", "delete_rules_folder_failed": "Gagal menghapus folder aturan: {{rulesFolderPath}}. Error: {{error}}", + "command_not_found": "Perintah '{{name}}' tidak ditemukan", + "open_command_file": "Gagal membuka file perintah", + "delete_command": "Gagal menghapus perintah", + "no_workspace_for_project_command": "Tidak ditemukan folder workspace untuk perintah proyek", + "command_already_exists": "Perintah \"{{commandName}}\" sudah ada", + "create_command_failed": "Gagal membuat perintah", + "command_template_content": "---\ndescription: \"Deskripsi singkat tentang fungsi perintah ini\"\n---\n\nIni adalah perintah slash baru. Edit file ini untuk menyesuaikan perilaku perintah.", "claudeCode": { "processExited": "Proses Claude Code keluar dengan kode {{exitCode}}.", "errorOutput": "Output error: {{output}}", "processExitedWithError": "Proses Claude Code keluar dengan kode {{exitCode}}. Output error: {{output}}", "stoppedWithReason": "Claude Code berhenti karena alasan: {{reason}}", "apiKeyModelPlanMismatch": "Kunci API dan paket berlangganan memungkinkan model yang berbeda. Pastikan model yang dipilih termasuk dalam paket Anda." + }, + "gemini": { + "generate_stream": "Kesalahan aliran konteks pembuatan Gemini: {{error}}", + "generate_complete_prompt": "Kesalahan penyelesaian Gemini: {{error}}", + "sources": "Sumber:" } }, "warnings": { diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index 997c6e8018..a66c1965ab 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Tidak dapat menentukan dimensi vektor untuk model '{{modelId}}' dengan penyedia '{{provider}}'. Periksa profil model atau konfigurasi.", "qdrantUrlMissing": "URL Qdrant tidak ada untuk membuat penyimpanan vektor", "codeIndexingNotConfigured": "Tidak dapat membuat layanan: Pengindeksan kode tidak dikonfigurasi dengan benar" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Pengindeksan gagal: Tidak ada blok kode yang berhasil diindeks. Ini biasanya menunjukkan masalah konfigurasi embedder.", + "indexingFailedCritical": "Pengindeksan gagal: Tidak ada blok kode yang berhasil diindeks meskipun menemukan file untuk diproses. Ini menunjukkan kegagalan kritis embedder.", + "fileWatcherStarted": "Pemantau file dimulai.", + "fileWatcherStopped": "Pemantau file dihentikan.", + "failedDuringInitialScan": "Gagal selama pemindaian awal: {{errorMessage}}", + "unknownError": "Kesalahan tidak diketahui", + "indexingRequiresWorkspace": "Pengindeksan memerlukan folder workspace yang terbuka" } } diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 8257bc57e3..33902b2044 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (hanya definisi)", "maxLines": " (maks {{max}} baris)", "showingOnlyLines": "Menampilkan hanya {{shown}} dari {{total}} total baris. Gunakan line_range jika kamu perlu membaca lebih banyak baris", - "contextLimitInstructions": "Untuk membaca bagian tertentu dari file ini, gunakan format berikut:\n\n\n \n {{path}}\n awal-akhir\n \n\n\n\nContohnya, untuk membaca baris 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Untuk membaca bagian tertentu dari file ini, gunakan format berikut:\n\n\n \n {{path}}\n awal-akhir\n \n\n\n\nContohnya, untuk membaca baris 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "File gambar terlalu besar ({{size}} MB). Ukuran maksimum yang diizinkan adalah {{max}} MB.", + "imageWithSize": "File gambar ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo tampaknya terjebak dalam loop, mencoba aksi yang sama ({{toolName}}) berulang kali. Ini mungkin menunjukkan masalah dengan strategi saat ini. Pertimbangkan untuk mengubah frasa tugas, memberikan instruksi yang lebih spesifik, atau mengarahkannya ke pendekatan yang berbeda.", "codebaseSearch": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 7ae45cc4c5..a7ef4b075a 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Attività non trovata o accesso negato.", "mode_import_failed": "Importazione della modalità non riuscita: {{error}}", "delete_rules_folder_failed": "Impossibile eliminare la cartella delle regole: {{rulesFolderPath}}. Errore: {{error}}", + "command_not_found": "Comando '{{name}}' non trovato", + "open_command_file": "Impossibile aprire il file di comando", + "delete_command": "Impossibile eliminare il comando", + "no_workspace_for_project_command": "Nessuna cartella workspace trovata per il comando di progetto", + "command_already_exists": "Il comando \"{{commandName}}\" esiste già", + "create_command_failed": "Errore nella creazione del comando", + "command_template_content": "---\ndescription: \"Breve descrizione di cosa fa questo comando\"\n---\n\nQuesto è un nuovo comando slash. Modifica questo file per personalizzare il comportamento del comando.", "claudeCode": { "processExited": "Il processo Claude Code è terminato con codice {{exitCode}}.", "errorOutput": "Output di errore: {{output}}", "processExitedWithError": "Il processo Claude Code è terminato con codice {{exitCode}}. Output di errore: {{output}}", "stoppedWithReason": "Claude Code si è fermato per il motivo: {{reason}}", "apiKeyModelPlanMismatch": "Le chiavi API e i piani di abbonamento consentono modelli diversi. Assicurati che il modello selezionato sia incluso nel tuo piano." + }, + "gemini": { + "generate_stream": "Errore del flusso di contesto di generazione Gemini: {{error}}", + "generate_complete_prompt": "Errore di completamento Gemini: {{error}}", + "sources": "Fonti:" } }, "warnings": { diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index 1bc406aecb..d59bc2c26d 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Impossibile determinare la dimensione del vettore per il modello '{{modelId}}' con il provider '{{provider}}'. Controlla i profili del modello o la configurazione.", "qdrantUrlMissing": "URL Qdrant mancante per la creazione dello storage vettoriale", "codeIndexingNotConfigured": "Impossibile creare i servizi: L'indicizzazione del codice non è configurata correttamente" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indicizzazione fallita: Nessun blocco di codice è stato indicizzato con successo. Questo di solito indica un problema di configurazione dell'embedder.", + "indexingFailedCritical": "Indicizzazione fallita: Nessun blocco di codice è stato indicizzato con successo nonostante siano stati trovati file da elaborare. Questo indica un errore critico dell'embedder.", + "fileWatcherStarted": "Monitoraggio file avviato.", + "fileWatcherStopped": "Monitoraggio file fermato.", + "failedDuringInitialScan": "Fallito durante la scansione iniziale: {{errorMessage}}", + "unknownError": "Errore sconosciuto", + "indexingRequiresWorkspace": "L'indicizzazione richiede una cartella di workspace aperta" } } diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index ff70fac751..468e3cff66 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (solo definizioni)", "maxLines": " (max {{max}} righe)", "showingOnlyLines": "Mostrando solo {{shown}} di {{total}} righe totali. Usa line_range se hai bisogno di leggere più righe", - "contextLimitInstructions": "Per leggere sezioni specifiche di questo file, usa il seguente formato:\n\n\n \n {{path}}\n inizio-fine\n \n\n\n\nAd esempio, per leggere le righe 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Per leggere sezioni specifiche di questo file, usa il seguente formato:\n\n\n \n {{path}}\n inizio-fine\n \n\n\n\nAd esempio, per leggere le righe 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Il file immagine è troppo grande ({{size}} MB). La dimensione massima consentita è {{max}} MB.", + "imageWithSize": "File immagine ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.", "codebaseSearch": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index da8124b48c..6e7e0b8a3e 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "タスクが見つからないか、アクセスが拒否されました。", "mode_import_failed": "モードのインポートに失敗しました:{{error}}", "delete_rules_folder_failed": "ルールフォルダの削除に失敗しました:{{rulesFolderPath}}。エラー:{{error}}", + "command_not_found": "コマンド '{{name}}' が見つかりません", + "open_command_file": "コマンドファイルを開けませんでした", + "delete_command": "コマンドの削除に失敗しました", + "no_workspace_for_project_command": "プロジェクトコマンド用のワークスペースフォルダが見つかりません", + "command_already_exists": "コマンド \"{{commandName}}\" は既に存在します", + "create_command_failed": "コマンドの作成に失敗しました", + "command_template_content": "---\ndescription: \"このコマンドが何をするかの簡潔な説明\"\n---\n\nこれは新しいスラッシュコマンドです。このファイルを編集してコマンドの動作をカスタマイズしてください。", "claudeCode": { "processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。", "errorOutput": "エラー出力:{{output}}", "processExitedWithError": "Claude Code プロセスがコード {{exitCode}} で終了しました。エラー出力:{{output}}", "stoppedWithReason": "Claude Code が理由により停止しました:{{reason}}", "apiKeyModelPlanMismatch": "API キーとサブスクリプションプランでは異なるモデルが利用可能です。選択したモデルがプランに含まれていることを確認してください。" + }, + "gemini": { + "generate_stream": "Gemini 生成コンテキスト ストリーム エラー: {{error}}", + "generate_complete_prompt": "Gemini 完了エラー: {{error}}", + "sources": "ソース:" } }, "warnings": { diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index 7152eb52df..799c6745fa 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "プロバイダー '{{provider}}' のモデル '{{modelId}}' の埋め込み次元を決定できませんでした。モデルプロファイルまたは設定を確認してください。", "qdrantUrlMissing": "ベクターストア作成のためのQdrant URLがありません", "codeIndexingNotConfigured": "サービスを作成できません: コードインデックスが正しく設定されていません" + }, + "orchestrator": { + "indexingFailedNoBlocks": "インデックス作成に失敗しました:コードブロックが正常にインデックス化されませんでした。これは通常、エンベッダーの設定問題を示しています。", + "indexingFailedCritical": "インデックス作成に失敗しました:処理するファイルが見つかったにもかかわらず、コードブロックが正常にインデックス化されませんでした。これは重大なエンベッダーの障害を示しています。", + "fileWatcherStarted": "ファイルウォッチャーが開始されました。", + "fileWatcherStopped": "ファイルウォッチャーが停止されました。", + "failedDuringInitialScan": "初期スキャン中に失敗しました:{{errorMessage}}", + "unknownError": "不明なエラー", + "indexingRequiresWorkspace": "インデックス作成には、開かれたワークスペースフォルダーが必要です" } } diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index cc49a3afb8..4f56582cec 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (定義のみ)", "maxLines": " (最大{{max}}行)", "showingOnlyLines": "全{{total}}行中{{shown}}行のみ表示しています。より多くの行を読む必要がある場合はline_rangeを使用してください", - "contextLimitInstructions": "このファイルの特定のセクションを読むには、以下の形式を使用してください:\n\n\n \n {{path}}\n 開始-終了\n \n\n\n\n例えば、2001-3000行目を読むには:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "このファイルの特定のセクションを読むには、以下の形式を使用してください:\n\n\n \n {{path}}\n 開始-終了\n \n\n\n\n例えば、2001-3000行目を読むには:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "画像ファイルが大きすぎます({{size}} MB)。最大許可サイズは {{max}} MB です。", + "imageWithSize": "画像ファイル({{size}} KB)" }, "toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。", "codebaseSearch": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index a95908ffec..1d0a5f3c4a 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "작업을 찾을 수 없거나 액세스가 거부되었습니다.", "mode_import_failed": "모드 가져오기 실패: {{error}}", "delete_rules_folder_failed": "규칙 폴더 삭제 실패: {{rulesFolderPath}}. 오류: {{error}}", + "command_not_found": "'{{name}}' 명령을 찾을 수 없습니다", + "open_command_file": "명령 파일을 열 수 없습니다", + "delete_command": "명령 삭제 실패", + "no_workspace_for_project_command": "프로젝트 명령용 워크스페이스 폴더를 찾을 수 없습니다", + "command_already_exists": "명령 \"{{commandName}}\"이(가) 이미 존재합니다", + "create_command_failed": "명령 생성에 실패했습니다", + "command_template_content": "---\ndescription: \"이 명령이 수행하는 작업에 대한 간단한 설명\"\n---\n\n이것은 새로운 슬래시 명령입니다. 이 파일을 편집하여 명령 동작을 사용자 정의하세요.", "claudeCode": { "processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.", "errorOutput": "오류 출력: {{output}}", "processExitedWithError": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다. 오류 출력: {{output}}", "stoppedWithReason": "Claude Code가 다음 이유로 중지되었습니다: {{reason}}", "apiKeyModelPlanMismatch": "API 키와 구독 플랜에서 다른 모델을 허용합니다. 선택한 모델이 플랜에 포함되어 있는지 확인하세요." + }, + "gemini": { + "generate_stream": "Gemini 생성 컨텍스트 스트림 오류: {{error}}", + "generate_complete_prompt": "Gemini 완료 오류: {{error}}", + "sources": "출처:" } }, "warnings": { diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index f1c40f66bc..3817135982 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "프로바이더 '{{provider}}'의 모델 '{{modelId}}'에 대한 벡터 차원을 결정할 수 없습니다. 모델 프로필 또는 구성을 확인하세요.", "qdrantUrlMissing": "벡터 저장소 생성을 위한 Qdrant URL이 누락되었습니다", "codeIndexingNotConfigured": "서비스를 생성할 수 없습니다: 코드 인덱싱이 올바르게 구성되지 않았습니다" + }, + "orchestrator": { + "indexingFailedNoBlocks": "인덱싱 실패: 코드 블록이 성공적으로 인덱싱되지 않았습니다. 이는 일반적으로 임베더 구성 문제를 나타냅니다.", + "indexingFailedCritical": "인덱싱 실패: 처리할 파일을 찾았음에도 불구하고 코드 블록이 성공적으로 인덱싱되지 않았습니다. 이는 중요한 임베더 오류를 나타냅니다.", + "fileWatcherStarted": "파일 감시자가 시작되었습니다.", + "fileWatcherStopped": "파일 감시자가 중지되었습니다.", + "failedDuringInitialScan": "초기 스캔 중 실패: {{errorMessage}}", + "unknownError": "알 수 없는 오류", + "indexingRequiresWorkspace": "인덱싱에는 열린 워크스페이스 폴더가 필요합니다" } } diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index 649466f00f..4a0ae39174 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (정의만)", "maxLines": " (최대 {{max}}행)", "showingOnlyLines": "전체 {{total}}행 중 {{shown}}행만 표시하고 있습니다. 더 많은 행을 읽으려면 line_range를 사용하세요", - "contextLimitInstructions": "이 파일의 특정 섹션을 읽으려면 다음 형식을 사용하세요:\n\n\n \n {{path}}\n 시작-끝\n \n\n\n\n예를 들어, 2001-3000행을 읽으려면:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "이 파일의 특정 섹션을 읽으려면 다음 형식을 사용하세요:\n\n\n \n {{path}}\n 시작-끝\n \n\n\n\n예를 들어, 2001-3000행을 읽으려면:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "이미지 파일이 너무 큽니다 ({{size}} MB). 최대 허용 크기는 {{max}} MB입니다.", + "imageWithSize": "이미지 파일 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.", "codebaseSearch": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index ac7df81e42..bb7d3c0f23 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Taak niet gevonden of toegang geweigerd.", "mode_import_failed": "Importeren van modus mislukt: {{error}}", "delete_rules_folder_failed": "Kan regelmap niet verwijderen: {{rulesFolderPath}}. Fout: {{error}}", + "command_not_found": "Opdracht '{{name}}' niet gevonden", + "open_command_file": "Kan opdrachtbestand niet openen", + "delete_command": "Kan opdracht niet verwijderen", + "no_workspace_for_project_command": "Geen werkruimtemap gevonden voor projectopdracht", + "command_already_exists": "Opdracht \"{{commandName}}\" bestaat al", + "create_command_failed": "Kan opdracht niet aanmaken", + "command_template_content": "---\ndescription: \"Korte beschrijving van wat deze opdracht doet\"\n---\n\nDit is een nieuwe slash-opdracht. Bewerk dit bestand om het opdrachtgedrag aan te passen.", "claudeCode": { "processExited": "Claude Code proces beëindigd met code {{exitCode}}.", "errorOutput": "Foutuitvoer: {{output}}", "processExitedWithError": "Claude Code proces beëindigd met code {{exitCode}}. Foutuitvoer: {{output}}", "stoppedWithReason": "Claude Code gestopt om reden: {{reason}}", "apiKeyModelPlanMismatch": "API-sleutels en abonnementsplannen staan verschillende modellen toe. Zorg ervoor dat het geselecteerde model is opgenomen in je plan." + }, + "gemini": { + "generate_stream": "Fout bij het genereren van contextstream door Gemini: {{error}}", + "generate_complete_prompt": "Fout bij het voltooien door Gemini: {{error}}", + "sources": "Bronnen:" } }, "warnings": { diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index 19b7bfeaa2..52d675c890 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Kan de vectordimensie voor model '{{modelId}}' met provider '{{provider}}' niet bepalen. Controleer modelprofielen of configuratie.", "qdrantUrlMissing": "Qdrant URL ontbreekt voor het maken van vectoropslag", "codeIndexingNotConfigured": "Kan geen services maken: Code-indexering is niet correct geconfigureerd" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indexering mislukt: Geen codeblokken werden succesvol geïndexeerd. Dit duidt meestal op een embedder configuratieprobleem.", + "indexingFailedCritical": "Indexering mislukt: Geen codeblokken werden succesvol geïndexeerd ondanks het vinden van bestanden om te verwerken. Dit duidt op een kritieke embedder fout.", + "fileWatcherStarted": "Bestandsmonitor gestart.", + "fileWatcherStopped": "Bestandsmonitor gestopt.", + "failedDuringInitialScan": "Mislukt tijdens initiële scan: {{errorMessage}}", + "unknownError": "Onbekende fout", + "indexingRequiresWorkspace": "Indexering vereist een geopende workspace map" } } diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 4fa03a1d55..5a46f7cdea 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (alleen definities)", "maxLines": " (max {{max}} regels)", "showingOnlyLines": "Toont alleen {{shown}} van {{total}} totale regels. Gebruik line_range als je meer regels wilt lezen", - "contextLimitInstructions": "Om specifieke secties van dit bestand te lezen, gebruik het volgende formaat:\n\n\n \n {{path}}\n start-eind\n \n\n\n\nBijvoorbeeld, om regels 2001-3000 te lezen:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Om specifieke secties van dit bestand te lezen, gebruik het volgende formaat:\n\n\n \n {{path}}\n start-eind\n \n\n\n\nBijvoorbeeld, om regels 2001-3000 te lezen:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Afbeeldingsbestand is te groot ({{size}} MB). De maximaal toegestane grootte is {{max}} MB.", + "imageWithSize": "Afbeeldingsbestand ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden.", "codebaseSearch": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index e24960af89..953f52ea79 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Zadanie nie znalezione lub dostęp odmówiony.", "mode_import_failed": "Import trybu nie powiódł się: {{error}}", "delete_rules_folder_failed": "Nie udało się usunąć folderu reguł: {{rulesFolderPath}}. Błąd: {{error}}", + "command_not_found": "Polecenie '{{name}}' nie zostało znalezione", + "open_command_file": "Nie udało się otworzyć pliku polecenia", + "delete_command": "Nie udało się usunąć polecenia", + "no_workspace_for_project_command": "Nie znaleziono folderu obszaru roboczego dla polecenia projektu", + "command_already_exists": "Polecenie \"{{commandName}}\" już istnieje", + "create_command_failed": "Nie udało się utworzyć polecenia", + "command_template_content": "---\ndescription: \"Krótki opis tego, co robi to polecenie\"\n---\n\nTo jest nowe polecenie slash. Edytuj ten plik, aby dostosować zachowanie polecenia.", "claudeCode": { "processExited": "Proces Claude Code zakończył się kodem {{exitCode}}.", "errorOutput": "Wyjście błędu: {{output}}", "processExitedWithError": "Proces Claude Code zakończył się kodem {{exitCode}}. Wyjście błędu: {{output}}", "stoppedWithReason": "Claude Code zatrzymał się z powodu: {{reason}}", "apiKeyModelPlanMismatch": "Klucze API i plany subskrypcji pozwalają na różne modele. Upewnij się, że wybrany model jest zawarty w twoim planie." + }, + "gemini": { + "generate_stream": "Błąd strumienia kontekstu generowania Gemini: {{error}}", + "generate_complete_prompt": "Błąd uzupełniania Gemini: {{error}}", + "sources": "Źródła:" } }, "warnings": { diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index 46e761cb8b..4d1ad0316c 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Nie można określić wymiaru wektora dla modelu '{{modelId}}' z dostawcą '{{provider}}'. Sprawdź profile modelu lub konfigurację.", "qdrantUrlMissing": "Brak adresu URL Qdrant do utworzenia magazynu wektorów", "codeIndexingNotConfigured": "Nie można utworzyć usług: Indeksowanie kodu nie jest poprawnie skonfigurowane" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indeksowanie nie powiodło się: Żadne bloki kodu nie zostały pomyślnie zaindeksowane. To zwykle wskazuje na problem z konfiguracją embeddera.", + "indexingFailedCritical": "Indeksowanie nie powiodło się: Żadne bloki kodu nie zostały pomyślnie zaindeksowane pomimo znalezienia plików do przetworzenia. To wskazuje na krytyczny błąd embeddera.", + "fileWatcherStarted": "Monitor plików uruchomiony.", + "fileWatcherStopped": "Monitor plików zatrzymany.", + "failedDuringInitialScan": "Niepowodzenie podczas początkowego skanowania: {{errorMessage}}", + "unknownError": "Nieznany błąd", + "indexingRequiresWorkspace": "Indeksowanie wymaga otwartego folderu workspace" } } diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index bbd75e44c3..e872f7b215 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (tylko definicje)", "maxLines": " (maks. {{max}} linii)", "showingOnlyLines": "Pokazuję tylko {{shown}} z {{total}} wszystkich linii. Użyj line_range jeśli potrzebujesz przeczytać więcej linii", - "contextLimitInstructions": "Aby przeczytać określone sekcje tego pliku, użyj następującego formatu:\n\n\n \n {{path}}\n początek-koniec\n \n\n\n\nNa przykład, aby przeczytać linie 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Aby przeczytać określone sekcje tego pliku, użyj następującego formatu:\n\n\n \n {{path}}\n początek-koniec\n \n\n\n\nNa przykład, aby przeczytać linie 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Plik obrazu jest zbyt duży ({{size}} MB). Maksymalny dozwolony rozmiar to {{max}} MB.", + "imageWithSize": "Plik obrazu ({{size}} KB)" }, "toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.", "codebaseSearch": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 6007beb41a..21aca727a1 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -75,12 +75,24 @@ "share_task_not_found": "Tarefa não encontrada ou acesso negado.", "mode_import_failed": "Falha ao importar o modo: {{error}}", "delete_rules_folder_failed": "Falha ao excluir pasta de regras: {{rulesFolderPath}}. Erro: {{error}}", + "command_not_found": "Comando '{{name}}' não encontrado", + "open_command_file": "Falha ao abrir arquivo de comando", + "delete_command": "Falha ao excluir comando", + "no_workspace_for_project_command": "Nenhuma pasta de workspace encontrada para comando de projeto", + "command_already_exists": "Comando \"{{commandName}}\" já existe", + "create_command_failed": "Falha ao criar comando", + "command_template_content": "---\ndescription: \"Breve descrição do que este comando faz\"\n---\n\nEste é um novo comando slash. Edite este arquivo para personalizar o comportamento do comando.", "claudeCode": { "processExited": "O processo Claude Code saiu com código {{exitCode}}.", "errorOutput": "Saída de erro: {{output}}", "processExitedWithError": "O processo Claude Code saiu com código {{exitCode}}. Saída de erro: {{output}}", "stoppedWithReason": "Claude Code parou pela razão: {{reason}}", "apiKeyModelPlanMismatch": "Chaves de API e planos de assinatura permitem modelos diferentes. Certifique-se de que o modelo selecionado esteja incluído no seu plano." + }, + "gemini": { + "generate_stream": "Erro de fluxo de contexto de geração do Gemini: {{error}}", + "generate_complete_prompt": "Erro de conclusão do Gemini: {{error}}", + "sources": "Fontes:" } }, "warnings": { diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index 816b1ecded..875bba95dc 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Não foi possível determinar a dimensão do vetor para o modelo '{{modelId}}' com o provedor '{{provider}}'. Verifique os perfis do modelo ou a configuração.", "qdrantUrlMissing": "URL do Qdrant ausente para criação do armazenamento de vetores", "codeIndexingNotConfigured": "Não é possível criar serviços: A indexação de código não está configurada corretamente" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Indexação falhou: Nenhum bloco de código foi indexado com sucesso. Isso geralmente indica um problema de configuração do embedder.", + "indexingFailedCritical": "Indexação falhou: Nenhum bloco de código foi indexado com sucesso apesar de encontrar arquivos para processar. Isso indica uma falha crítica do embedder.", + "fileWatcherStarted": "Monitor de arquivos iniciado.", + "fileWatcherStopped": "Monitor de arquivos parado.", + "failedDuringInitialScan": "Falhou durante a varredura inicial: {{errorMessage}}", + "unknownError": "Erro desconhecido", + "indexingRequiresWorkspace": "A indexação requer uma pasta de workspace aberta" } } diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index f5306aa71b..b47112aa84 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (apenas definições)", "maxLines": " (máx. {{max}} linhas)", "showingOnlyLines": "Mostrando apenas {{shown}} de {{total}} linhas totais. Use line_range se precisar ler mais linhas", - "contextLimitInstructions": "Para ler seções específicas deste arquivo, use o seguinte formato:\n\n\n \n {{path}}\n início-fim\n \n\n\n\nPor exemplo, para ler as linhas 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Para ler seções específicas deste arquivo, use o seguinte formato:\n\n\n \n {{path}}\n início-fim\n \n\n\n\nPor exemplo, para ler as linhas 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Arquivo de imagem é muito grande ({{size}} MB). O tamanho máximo permitido é {{max}} MB.", + "imageWithSize": "Arquivo de imagem ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.", "codebaseSearch": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 4d3daaf743..30913e16e9 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Задача не найдена или доступ запрещен.", "mode_import_failed": "Не удалось импортировать режим: {{error}}", "delete_rules_folder_failed": "Не удалось удалить папку правил: {{rulesFolderPath}}. Ошибка: {{error}}", + "command_not_found": "Команда '{{name}}' не найдена", + "open_command_file": "Не удалось открыть файл команды", + "delete_command": "Не удалось удалить команду", + "no_workspace_for_project_command": "Не найдена папка рабочего пространства для команды проекта", + "command_already_exists": "Команда \"{{commandName}}\" уже существует", + "create_command_failed": "Не удалось создать команду", + "command_template_content": "---\ndescription: \"Краткое описание того, что делает эта команда\"\n---\n\nЭто новая slash-команда. Отредактируйте этот файл, чтобы настроить поведение команды.", "claudeCode": { "processExited": "Процесс Claude Code завершился с кодом {{exitCode}}.", "errorOutput": "Вывод ошибки: {{output}}", "processExitedWithError": "Процесс Claude Code завершился с кодом {{exitCode}}. Вывод ошибки: {{output}}", "stoppedWithReason": "Claude Code остановился по причине: {{reason}}", "apiKeyModelPlanMismatch": "API-ключи и планы подписки позволяют использовать разные модели. Убедитесь, что выбранная модель включена в ваш план." + }, + "gemini": { + "generate_stream": "Ошибка потока контекста генерации Gemini: {{error}}", + "generate_complete_prompt": "Ошибка завершения Gemini: {{error}}", + "sources": "Источники:" } }, "warnings": { diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index fb1688e2ca..80dfa9a594 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Не удалось определить размерность вектора для модели '{{modelId}}' с провайдером '{{provider}}'. Проверьте профили модели или конфигурацию.", "qdrantUrlMissing": "Отсутствует URL Qdrant для создания векторного хранилища", "codeIndexingNotConfigured": "Невозможно создать сервисы: Индексация кода не настроена должным образом" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Индексация не удалась: Ни один блок кода не был успешно проиндексирован. Это обычно указывает на проблему конфигурации эмбеддера.", + "indexingFailedCritical": "Индексация не удалась: Ни один блок кода не был успешно проиндексирован, несмотря на обнаружение файлов для обработки. Это указывает на критическую ошибку эмбеддера.", + "fileWatcherStarted": "Наблюдатель файлов запущен.", + "fileWatcherStopped": "Наблюдатель файлов остановлен.", + "failedDuringInitialScan": "Ошибка во время первоначального сканирования: {{errorMessage}}", + "unknownError": "Неизвестная ошибка", + "indexingRequiresWorkspace": "Для индексации требуется открытая папка рабочего пространства" } } diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 0096df8a6c..28ddb7d942 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (только определения)", "maxLines": " (макс. {{max}} строк)", "showingOnlyLines": "Показано только {{shown}} из {{total}} общих строк. Используй line_range если нужно прочитать больше строк", - "contextLimitInstructions": "Чтобы прочитать определенные разделы этого файла, используй следующий формат:\n\n\n \n {{path}}\n начало-конец\n \n\n\n\nНапример, чтобы прочитать строки 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Чтобы прочитать определенные разделы этого файла, используй следующий формат:\n\n\n \n {{path}}\n начало-конец\n \n\n\n\nНапример, чтобы прочитать строки 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Файл изображения слишком большой ({{size}} МБ). Максимально допустимый размер {{max}} МБ.", + "imageWithSize": "Файл изображения ({{size}} КБ)" }, "toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.", "codebaseSearch": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index e2dfca734b..6892c7c8f1 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Görev bulunamadı veya erişim reddedildi.", "mode_import_failed": "Mod içe aktarılamadı: {{error}}", "delete_rules_folder_failed": "Kurallar klasörü silinemedi: {{rulesFolderPath}}. Hata: {{error}}", + "command_not_found": "'{{name}}' komutu bulunamadı", + "open_command_file": "Komut dosyası açılamadı", + "delete_command": "Komut silinemedi", + "no_workspace_for_project_command": "Proje komutu için çalışma alanı klasörü bulunamadı", + "command_already_exists": "\"{{commandName}}\" komutu zaten mevcut", + "create_command_failed": "Komut oluşturulamadı", + "command_template_content": "---\ndescription: \"Bu komutun ne yaptığının kısa açıklaması\"\n---\n\nBu yeni bir slash komutudur. Komut davranışını özelleştirmek için bu dosyayı düzenleyin.", "claudeCode": { "processExited": "Claude Code işlemi {{exitCode}} koduyla çıktı.", "errorOutput": "Hata çıktısı: {{output}}", "processExitedWithError": "Claude Code işlemi {{exitCode}} koduyla çıktı. Hata çıktısı: {{output}}", "stoppedWithReason": "Claude Code şu nedenle durdu: {{reason}}", "apiKeyModelPlanMismatch": "API anahtarları ve abonelik planları farklı modellere izin verir. Seçilen modelin planınıza dahil olduğundan emin olun." + }, + "gemini": { + "generate_stream": "Gemini oluşturma bağlam akışı hatası: {{error}}", + "generate_complete_prompt": "Gemini tamamlama hatası: {{error}}", + "sources": "Kaynaklar:" } }, "warnings": { diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index 5023190929..ba717b7c82 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "'{{provider}}' sağlayıcısı ile '{{modelId}}' modeli için vektör boyutu belirlenemedi. Model profillerini veya yapılandırmayı kontrol et.", "qdrantUrlMissing": "Vektör deposu oluşturmak için Qdrant URL'si eksik", "codeIndexingNotConfigured": "Hizmetler oluşturulamıyor: Kod indeksleme düzgün yapılandırılmamış" + }, + "orchestrator": { + "indexingFailedNoBlocks": "İndeksleme başarısız: Hiçbir kod bloğu başarıyla indekslenemedi. Bu genellikle bir embedder yapılandırma sorunu olduğunu gösterir.", + "indexingFailedCritical": "İndeksleme başarısız: İşlenecek dosyalar bulunmasına rağmen hiçbir kod bloğu başarıyla indekslenemedi. Bu kritik bir embedder hatası olduğunu gösterir.", + "fileWatcherStarted": "Dosya izleyici başlatıldı.", + "fileWatcherStopped": "Dosya izleyici durduruldu.", + "failedDuringInitialScan": "İlk tarama sırasında başarısız: {{errorMessage}}", + "unknownError": "Bilinmeyen hata", + "indexingRequiresWorkspace": "İndeksleme açık bir workspace klasörü gerektirir" } } diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 19081b2de6..c576692e48 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (sadece tanımlar)", "maxLines": " (maks. {{max}} satır)", "showingOnlyLines": "Toplam {{total}} satırdan sadece {{shown}} tanesi gösteriliyor. Daha fazla satır okumak için line_range kullan", - "contextLimitInstructions": "Bu dosyanın belirli bölümlerini okumak için aşağıdaki formatı kullan:\n\n\n \n {{path}}\n başlangıç-bitiş\n \n\n\n\nÖrneğin, 2001-3000 satırlarını okumak için:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Bu dosyanın belirli bölümlerini okumak için aşağıdaki formatı kullan:\n\n\n \n {{path}}\n başlangıç-bitiş\n \n\n\n\nÖrneğin, 2001-3000 satırlarını okumak için:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Görüntü dosyası çok büyük ({{size}} MB). İzin verilen maksimum boyut {{max}} MB.", + "imageWithSize": "Görüntü dosyası ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.", "codebaseSearch": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 15e4ef8b77..f88120098d 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -71,12 +71,24 @@ "share_task_not_found": "Không tìm thấy nhiệm vụ hoặc truy cập bị từ chối.", "mode_import_failed": "Nhập chế độ thất bại: {{error}}", "delete_rules_folder_failed": "Không thể xóa thư mục quy tắc: {{rulesFolderPath}}. Lỗi: {{error}}", + "command_not_found": "Không tìm thấy lệnh '{{name}}'", + "open_command_file": "Không thể mở tệp lệnh", + "delete_command": "Không thể xóa lệnh", + "no_workspace_for_project_command": "Không tìm thấy thư mục workspace cho lệnh dự án", + "command_already_exists": "Lệnh \"{{commandName}}\" đã tồn tại", + "create_command_failed": "Không thể tạo lệnh", + "command_template_content": "---\ndescription: \"Mô tả ngắn gọn về chức năng của lệnh này\"\n---\n\nĐây là một lệnh slash mới. Chỉnh sửa tệp này để tùy chỉnh hành vi của lệnh.", "claudeCode": { "processExited": "Tiến trình Claude Code thoát với mã {{exitCode}}.", "errorOutput": "Đầu ra lỗi: {{output}}", "processExitedWithError": "Tiến trình Claude Code thoát với mã {{exitCode}}. Đầu ra lỗi: {{output}}", "stoppedWithReason": "Claude Code dừng lại vì lý do: {{reason}}", "apiKeyModelPlanMismatch": "Khóa API và gói đăng ký cho phép các mô hình khác nhau. Đảm bảo rằng mô hình đã chọn được bao gồm trong gói của bạn." + }, + "gemini": { + "generate_stream": "Lỗi luồng ngữ cảnh tạo Gemini: {{error}}", + "generate_complete_prompt": "Lỗi hoàn thành Gemini: {{error}}", + "sources": "Nguồn:" } }, "warnings": { diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index 626f0f6862..12980b3345 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "Không thể xác định kích thước vector cho mô hình '{{modelId}}' với nhà cung cấp '{{provider}}'. Kiểm tra hồ sơ mô hình hoặc cấu hình.", "qdrantUrlMissing": "Thiếu URL Qdrant để tạo kho lưu trữ vector", "codeIndexingNotConfigured": "Không thể tạo dịch vụ: Lập chỉ mục mã không được cấu hình đúng cách" + }, + "orchestrator": { + "indexingFailedNoBlocks": "Lập chỉ mục thất bại: Không có khối mã nào được lập chỉ mục thành công. Điều này thường cho thấy vấn đề cấu hình embedder.", + "indexingFailedCritical": "Lập chỉ mục thất bại: Không có khối mã nào được lập chỉ mục thành công mặc dù đã tìm thấy tệp để xử lý. Điều này cho thấy lỗi nghiêm trọng của embedder.", + "fileWatcherStarted": "Trình theo dõi tệp đã khởi động.", + "fileWatcherStopped": "Trình theo dõi tệp đã dừng.", + "failedDuringInitialScan": "Thất bại trong quá trình quét ban đầu: {{errorMessage}}", + "unknownError": "Lỗi không xác định", + "indexingRequiresWorkspace": "Lập chỉ mục yêu cầu một thư mục workspace đang mở" } } diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index dd87b7ff65..eeca7a5105 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (chỉ định nghĩa)", "maxLines": " (tối đa {{max}} dòng)", "showingOnlyLines": "Chỉ hiển thị {{shown}} trong tổng số {{total}} dòng. Sử dụng line_range nếu bạn cần đọc thêm dòng", - "contextLimitInstructions": "Để đọc các phần cụ thể của tệp này, hãy sử dụng định dạng sau:\n\n\n \n {{path}}\n bắt đầu-kết thúc\n \n\n\n\nVí dụ, để đọc dòng 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "Để đọc các phần cụ thể của tệp này, hãy sử dụng định dạng sau:\n\n\n \n {{path}}\n bắt đầu-kết thúc\n \n\n\n\nVí dụ, để đọc dòng 2001-3000:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "Tệp hình ảnh quá lớn ({{size}} MB). Kích thước tối đa cho phép là {{max}} MB.", + "imageWithSize": "Tệp hình ảnh ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác.", "codebaseSearch": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index edbbb6ae8c..e81b7d589a 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -76,12 +76,24 @@ "share_task_not_found": "未找到任务或访问被拒绝。", "mode_import_failed": "导入模式失败:{{error}}", "delete_rules_folder_failed": "删除规则文件夹失败:{{rulesFolderPath}}。错误:{{error}}", + "command_not_found": "未找到命令 '{{name}}'", + "open_command_file": "打开命令文件失败", + "delete_command": "删除命令失败", + "no_workspace_for_project_command": "未找到项目命令的工作区文件夹", + "command_already_exists": "命令 \"{{commandName}}\" 已存在", + "create_command_failed": "创建命令失败", + "command_template_content": "---\ndescription: \"此命令功能的简要描述\"\n---\n\n这是一个新的斜杠命令。编辑此文件以自定义命令行为。", "claudeCode": { "processExited": "Claude Code 进程退出,退出码:{{exitCode}}。", "errorOutput": "错误输出:{{output}}", "processExitedWithError": "Claude Code 进程退出,退出码:{{exitCode}}。错误输出:{{output}}", "stoppedWithReason": "Claude Code 停止,原因:{{reason}}", "apiKeyModelPlanMismatch": "API 密钥和订阅计划支持不同的模型。请确保所选模型包含在您的计划中。" + }, + "gemini": { + "generate_stream": "Gemini 生成上下文流错误:{{error}}", + "generate_complete_prompt": "Gemini 完成错误:{{error}}", + "sources": "来源:" } }, "warnings": { diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index 3247631bb2..1589689c06 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "无法确定提供商 '{{provider}}' 的模型 '{{modelId}}' 的向量维度。请检查模型配置文件或配置。", "qdrantUrlMissing": "创建向量存储缺少 Qdrant URL", "codeIndexingNotConfigured": "无法创建服务:代码索引未正确配置" + }, + "orchestrator": { + "indexingFailedNoBlocks": "索引失败:没有代码块被成功索引。这通常表示 Embedder 配置问题。", + "indexingFailedCritical": "索引失败:尽管找到了要处理的文件,但没有代码块被成功索引。这表示 Embedder 出现严重故障。", + "fileWatcherStarted": "文件监控已启动。", + "fileWatcherStopped": "文件监控已停止。", + "failedDuringInitialScan": "初始扫描失败:{{errorMessage}}", + "unknownError": "未知错误", + "indexingRequiresWorkspace": "索引需要打开的工作区文件夹" } } diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 69287f9c5d..a8e0adb81a 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (仅定义)", "maxLines": " (最多 {{max}} 行)", "showingOnlyLines": "仅显示 {{shown}} 行,共 {{total}} 行。如需阅读更多行请使用 line_range", - "contextLimitInstructions": "要阅读此文件的特定部分,请使用以下格式:\n\n\n \n {{path}}\n 开始-结束\n \n\n\n\n例如,要阅读第 2001-3000 行:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "要阅读此文件的特定部分,请使用以下格式:\n\n\n \n {{path}}\n 开始-结束\n \n\n\n\n例如,要阅读第 2001-3000 行:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "图片文件过大 ({{size}} MB)。允许的最大大小为 {{max}} MB。", + "imageWithSize": "图片文件 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。", "codebaseSearch": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index e7887025f1..1c800d4d37 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -70,6 +70,13 @@ "share_not_enabled": "此組織未啟用工作分享功能。", "share_task_not_found": "未找到工作或存取被拒絕。", "delete_rules_folder_failed": "刪除規則資料夾失敗: {{rulesFolderPath}}。錯誤: {{error}}", + "command_not_found": "找不到指令 '{{name}}'", + "open_command_file": "開啟指令檔案失敗", + "delete_command": "刪除指令失敗", + "no_workspace_for_project_command": "找不到專案指令的工作區資料夾", + "command_already_exists": "指令 \"{{commandName}}\" 已存在", + "create_command_failed": "建立指令失敗", + "command_template_content": "---\ndescription: \"此指令功能的簡要描述\"\n---\n\n這是一個新的斜線指令。編輯此檔案以自訂指令行為。", "claudeCode": { "processExited": "Claude Code 程序退出,退出碼:{{exitCode}}。", "errorOutput": "錯誤輸出:{{output}}", @@ -77,6 +84,11 @@ "stoppedWithReason": "Claude Code 停止,原因:{{reason}}", "apiKeyModelPlanMismatch": "API 金鑰和訂閱方案允許不同的模型。請確保所選模型包含在您的方案中。" }, + "gemini": { + "generate_stream": "Gemini 產生內容串流錯誤:{{error}}", + "generate_complete_prompt": "Gemini 完成錯誤:{{error}}", + "sources": "來源:" + }, "mode_import_failed": "匯入模式失敗:{{error}}" }, "warnings": { diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index b3b3231d4a..2dc41221f3 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -52,5 +52,14 @@ "vectorDimensionNotDetermined": "無法確定提供商 '{{provider}}' 的模型 '{{modelId}}' 的向量維度。請檢查模型設定檔或設定。", "qdrantUrlMissing": "建立向量儲存缺少 Qdrant URL", "codeIndexingNotConfigured": "無法建立服務:程式碼索引未正確設定" + }, + "orchestrator": { + "indexingFailedNoBlocks": "索引失敗:沒有程式碼區塊被成功索引。這通常表示 Embedder 設定問題。", + "indexingFailedCritical": "索引失敗:儘管找到了要處理的檔案,但沒有程式碼區塊被成功索引。這表示 Embedder 出現嚴重故障。", + "fileWatcherStarted": "檔案監控已啟動。", + "fileWatcherStopped": "檔案監控已停止。", + "failedDuringInitialScan": "初始掃描失敗:{{errorMessage}}", + "unknownError": "未知錯誤", + "indexingRequiresWorkspace": "索引需要開啟的工作區資料夾" } } diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 4b003b71f4..ab4bd92209 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -4,7 +4,9 @@ "definitionsOnly": " (僅定義)", "maxLines": " (最多 {{max}} 行)", "showingOnlyLines": "僅顯示 {{shown}} 行,共 {{total}} 行。如需閱讀更多行請使用 line_range", - "contextLimitInstructions": "要閱讀此檔案的特定部分,請使用以下格式:\n\n\n \n {{path}}\n 開始-結束\n \n\n\n\n例如,要閱讀第 2001-3000 行:\n\n\n \n {{path}}\n 2001-3000\n \n\n" + "contextLimitInstructions": "要閱讀此檔案的特定部分,請使用以下格式:\n\n\n \n {{path}}\n 開始-結束\n \n\n\n\n例如,要閱讀第 2001-3000 行:\n\n\n \n {{path}}\n 2001-3000\n \n\n", + "imageTooLarge": "圖片檔案過大 ({{size}} MB)。允許的最大大小為 {{max}} MB。", + "imageWithSize": "圖片檔案 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。", "codebaseSearch": { diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 64820beffb..5acf09ea78 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -187,7 +187,10 @@ export class DiffViewProvider { } } - async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{ + async saveChanges( + diagnosticsEnabled: boolean = true, + writeDelayMs: number = DEFAULT_WRITE_DELAY_MS, + ): Promise<{ newProblemsMessage: string | undefined userEdits: string | undefined finalContent: string | undefined @@ -222,22 +225,22 @@ export class DiffViewProvider { // and can address them accordingly. If problems don't change immediately after // applying a fix, won't be notified, which is generally fine since the // initial fix is usually correct and it may just take time for linters to catch up. - + let newProblemsMessage = "" - + if (diagnosticsEnabled) { // Add configurable delay to allow linters time to process and clean up issues // like unused imports (especially important for Go and other languages) // Ensure delay is non-negative const safeDelayMs = Math.max(0, writeDelayMs) - + try { await delay(safeDelayMs) } catch (error) { // Log error but continue - delay failure shouldn't break the save operation console.warn(`Failed to apply write delay: ${error}`) } - + const postDiagnostics = vscode.languages.getDiagnostics() // Get diagnostic settings from state @@ -625,4 +628,99 @@ export class DiffViewProvider { this.streamedLines = [] this.preDiagnostics = [] } + + /** + * Directly save content to a file without showing diff view + * Used when preventFocusDisruption experiment is enabled + * + * @param relPath - Relative path to the file + * @param content - Content to write to the file + * @param openFile - Whether to show the file in editor (false = open in memory only for diagnostics) + * @returns Result of the save operation including any new problems detected + */ + async saveDirectly( + relPath: string, + content: string, + openFile: boolean = true, + diagnosticsEnabled: boolean = true, + writeDelayMs: number = DEFAULT_WRITE_DELAY_MS, + ): Promise<{ + newProblemsMessage: string | undefined + userEdits: string | undefined + finalContent: string | undefined + }> { + const absolutePath = path.resolve(this.cwd, relPath) + + // Get diagnostics before editing the file + this.preDiagnostics = vscode.languages.getDiagnostics() + + // Write the content directly to the file + await createDirectoriesForFile(absolutePath) + await fs.writeFile(absolutePath, content, "utf-8") + + // Open the document to ensure diagnostics are loaded + // When openFile is false (PREVENT_FOCUS_DISRUPTION enabled), we only open in memory + if (openFile) { + // Show the document in the editor + await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { + preview: false, + preserveFocus: true, + }) + } else { + // Just open the document in memory to trigger diagnostics without showing it + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(absolutePath)) + + // Save the document to ensure VSCode recognizes it as saved and triggers diagnostics + if (doc.isDirty) { + await doc.save() + } + + // Force a small delay to ensure diagnostics are triggered + await new Promise((resolve) => setTimeout(resolve, 100)) + } + + let newProblemsMessage = "" + + if (diagnosticsEnabled) { + // Add configurable delay to allow linters time to process + const safeDelayMs = Math.max(0, writeDelayMs) + + try { + await delay(safeDelayMs) + } catch (error) { + console.warn(`Failed to apply write delay: ${error}`) + } + + const postDiagnostics = vscode.languages.getDiagnostics() + + // Get diagnostic settings from state + const task = this.taskRef.deref() + const state = await task?.providerRef.deref()?.getState() + const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true + const maxDiagnosticMessages = state?.maxDiagnosticMessages ?? 50 + + const newProblems = await diagnosticsToProblemsString( + getNewDiagnostics(this.preDiagnostics, postDiagnostics), + [vscode.DiagnosticSeverity.Error], + this.cwd, + includeDiagnosticMessages, + maxDiagnosticMessages, + ) + + newProblemsMessage = + newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : "" + } + + // Store the results for formatFileWriteResponse + this.newProblemsMessage = newProblemsMessage + this.userEdits = undefined + this.relPath = relPath + this.newContent = content + + return { + newProblemsMessage, + userEdits: undefined, + finalContent: content, + } + } } diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index 7159aca57a..0737b143cd 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -30,6 +30,10 @@ vi.mock("vscode", () => ({ workspace: { applyEdit: vi.fn(), onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })), + openTextDocument: vi.fn().mockResolvedValue({ + isDirty: false, + save: vi.fn().mockResolvedValue(undefined), + }), textDocuments: [], fs: { stat: vi.fn(), @@ -353,6 +357,88 @@ describe("DiffViewProvider", () => { }) }) + describe("saveDirectly method", () => { + beforeEach(() => { + // Mock vscode functions + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.languages.getDiagnostics).mockReturnValue([]) + }) + + it("should write content directly to file without opening diff view", async () => { + const mockDelay = vi.mocked(delay) + mockDelay.mockClear() + + const result = await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 2000) + + // Verify file was written + const fs = await import("fs/promises") + expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", "utf-8") + + // Verify file was opened without focus + expect(vscode.window.showTextDocument).toHaveBeenCalledWith( + expect.objectContaining({ fsPath: `${mockCwd}/test.ts` }), + { preview: false, preserveFocus: true }, + ) + + // Verify diagnostics were checked after delay + expect(mockDelay).toHaveBeenCalledWith(2000) + expect(vscode.languages.getDiagnostics).toHaveBeenCalled() + + // Verify result + expect(result.newProblemsMessage).toBe("") + expect(result.userEdits).toBeUndefined() + expect(result.finalContent).toBe("new content") + }) + + it("should not open file when openWithoutFocus is false", async () => { + await diffViewProvider.saveDirectly("test.ts", "new content", false, true, 1000) + + // Verify file was written + const fs = await import("fs/promises") + expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", "utf-8") + + // Verify file was NOT opened + expect(vscode.window.showTextDocument).not.toHaveBeenCalled() + }) + + it("should skip diagnostics when diagnosticsEnabled is false", async () => { + const mockDelay = vi.mocked(delay) + mockDelay.mockClear() + vi.mocked(vscode.languages.getDiagnostics).mockClear() + + await diffViewProvider.saveDirectly("test.ts", "new content", true, false, 1000) + + // Verify file was written + const fs = await import("fs/promises") + expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", "utf-8") + + // Verify delay was NOT called + expect(mockDelay).not.toHaveBeenCalled() + // getDiagnostics is called once for pre-diagnostics, but not for post-diagnostics + expect(vscode.languages.getDiagnostics).toHaveBeenCalledTimes(1) + }) + + it("should handle negative delay values", async () => { + const mockDelay = vi.mocked(delay) + mockDelay.mockClear() + + await diffViewProvider.saveDirectly("test.ts", "new content", true, true, -500) + + // Verify delay was called with 0 (safe minimum) + expect(mockDelay).toHaveBeenCalledWith(0) + }) + + it("should store results for formatFileWriteResponse", async () => { + await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 1000) + + // Verify internal state was updated + expect((diffViewProvider as any).newProblemsMessage).toBe("") + expect((diffViewProvider as any).userEdits).toBeUndefined() + expect((diffViewProvider as any).relPath).toBe("test.ts") + expect((diffViewProvider as any).newContent).toBe("new content") + }) + }) + describe("saveChanges method with diagnostic settings", () => { beforeEach(() => { // Setup common mocks for saveChanges tests diff --git a/src/integrations/misc/__tests__/extract-text-large-files.spec.ts b/src/integrations/misc/__tests__/extract-text-large-files.spec.ts new file mode 100644 index 0000000000..fc2f7f54b6 --- /dev/null +++ b/src/integrations/misc/__tests__/extract-text-large-files.spec.ts @@ -0,0 +1,221 @@ +// npx vitest run integrations/misc/__tests__/extract-text-large-files.spec.ts + +import { describe, it, expect, vi, beforeEach, Mock } from "vitest" +import * as fs from "fs/promises" +import { extractTextFromFile } from "../extract-text" +import { countFileLines } from "../line-counter" +import { readLines } from "../read-lines" +import { isBinaryFile } from "isbinaryfile" + +// Mock all dependencies +vi.mock("fs/promises") +vi.mock("../line-counter") +vi.mock("../read-lines") +vi.mock("isbinaryfile") + +describe("extractTextFromFile - Large File Handling", () => { + // Type the mocks + const mockedFs = vi.mocked(fs) + const mockedCountFileLines = vi.mocked(countFileLines) + const mockedReadLines = vi.mocked(readLines) + const mockedIsBinaryFile = vi.mocked(isBinaryFile) + + beforeEach(() => { + vi.clearAllMocks() + // Set default mock behavior + mockedFs.access.mockResolvedValue(undefined) + mockedIsBinaryFile.mockResolvedValue(false) + }) + + it("should truncate files that exceed maxReadFileLine limit", async () => { + const largeFileContent = Array(150) + .fill(null) + .map((_, i) => `Line ${i + 1}: This is a test line with some content`) + .join("\n") + + mockedCountFileLines.mockResolvedValue(150) + mockedReadLines.mockResolvedValue( + Array(100) + .fill(null) + .map((_, i) => `Line ${i + 1}: This is a test line with some content`) + .join("\n"), + ) + + const result = await extractTextFromFile("/test/large-file.ts", 100) + + // Should only include first 100 lines with line numbers + expect(result).toContain(" 1 | Line 1: This is a test line with some content") + expect(result).toContain("100 | Line 100: This is a test line with some content") + expect(result).not.toContain("101 | Line 101: This is a test line with some content") + + // Should include truncation message + expect(result).toContain( + "[File truncated: showing 100 of 150 total lines. The file is too large and may exhaust the context window if read in full.]", + ) + }) + + it("should not truncate files within the maxReadFileLine limit", async () => { + const smallFileContent = Array(50) + .fill(null) + .map((_, i) => `Line ${i + 1}: This is a test line`) + .join("\n") + + mockedCountFileLines.mockResolvedValue(50) + mockedFs.readFile.mockResolvedValue(smallFileContent as any) + + const result = await extractTextFromFile("/test/small-file.ts", 100) + + // Should include all lines with line numbers + expect(result).toContain(" 1 | Line 1: This is a test line") + expect(result).toContain("50 | Line 50: This is a test line") + + // Should not include truncation message + expect(result).not.toContain("[File truncated:") + }) + + it("should handle files with exactly maxReadFileLine lines", async () => { + const exactFileContent = Array(100) + .fill(null) + .map((_, i) => `Line ${i + 1}`) + .join("\n") + + mockedCountFileLines.mockResolvedValue(100) + mockedFs.readFile.mockResolvedValue(exactFileContent as any) + + const result = await extractTextFromFile("/test/exact-file.ts", 100) + + // Should include all lines with line numbers + expect(result).toContain(" 1 | Line 1") + expect(result).toContain("100 | Line 100") + + // Should not include truncation message + expect(result).not.toContain("[File truncated:") + }) + + it("should handle undefined maxReadFileLine by not truncating", async () => { + const largeFileContent = Array(200) + .fill(null) + .map((_, i) => `Line ${i + 1}`) + .join("\n") + + mockedFs.readFile.mockResolvedValue(largeFileContent as any) + + const result = await extractTextFromFile("/test/large-file.ts", undefined) + + // Should include all lines with line numbers when maxReadFileLine is undefined + expect(result).toContain(" 1 | Line 1") + expect(result).toContain("200 | Line 200") + + // Should not include truncation message + expect(result).not.toContain("[File truncated:") + }) + + it("should handle empty files", async () => { + mockedFs.readFile.mockResolvedValue("" as any) + + const result = await extractTextFromFile("/test/empty-file.ts", 100) + + expect(result).toBe("") + expect(result).not.toContain("[File truncated:") + }) + + it("should handle files with only newlines", async () => { + const newlineOnlyContent = "\n\n\n\n\n" + + mockedCountFileLines.mockResolvedValue(6) // 5 newlines = 6 lines + mockedReadLines.mockResolvedValue("\n\n") + + const result = await extractTextFromFile("/test/newline-file.ts", 3) + + // Should truncate at line 3 + expect(result).toContain("[File truncated: showing 3 of 6 total lines") + }) + + it("should handle very large files efficiently", async () => { + // Simulate a 10,000 line file + mockedCountFileLines.mockResolvedValue(10000) + mockedReadLines.mockResolvedValue( + Array(500) + .fill(null) + .map((_, i) => `Line ${i + 1}: Some content here`) + .join("\n"), + ) + + const result = await extractTextFromFile("/test/very-large-file.ts", 500) + + // Should only include first 500 lines with line numbers + expect(result).toContain(" 1 | Line 1: Some content here") + expect(result).toContain("500 | Line 500: Some content here") + expect(result).not.toContain("501 | Line 501: Some content here") + + // Should show truncation message + expect(result).toContain("[File truncated: showing 500 of 10000 total lines") + }) + + it("should handle maxReadFileLine of 0 by throwing an error", async () => { + const fileContent = "Line 1\nLine 2\nLine 3" + + mockedFs.readFile.mockResolvedValue(fileContent as any) + + // maxReadFileLine of 0 should throw an error + await expect(extractTextFromFile("/test/file.ts", 0)).rejects.toThrow( + "Invalid maxReadFileLine: 0. Must be a positive integer or -1 for unlimited.", + ) + }) + + it("should handle negative maxReadFileLine by treating as undefined", async () => { + const fileContent = "Line 1\nLine 2\nLine 3" + + mockedFs.readFile.mockResolvedValue(fileContent as any) + + const result = await extractTextFromFile("/test/file.ts", -1) + + // Should include all content with line numbers when negative + expect(result).toContain("1 | Line 1") + expect(result).toContain("2 | Line 2") + expect(result).toContain("3 | Line 3") + expect(result).not.toContain("[File truncated:") + }) + + it("should preserve file content structure when truncating", async () => { + const structuredContent = [ + "function example() {", + " const x = 1;", + " const y = 2;", + " return x + y;", + "}", + "", + "// More code below", + ].join("\n") + + mockedCountFileLines.mockResolvedValue(7) + mockedReadLines.mockResolvedValue(["function example() {", " const x = 1;", " const y = 2;"].join("\n")) + + const result = await extractTextFromFile("/test/structured.ts", 3) + + // Should preserve the first 3 lines with line numbers + expect(result).toContain("1 | function example() {") + expect(result).toContain("2 | const x = 1;") + expect(result).toContain("3 | const y = 2;") + expect(result).not.toContain("4 | return x + y;") + + // Should include truncation info + expect(result).toContain("[File truncated: showing 3 of 7 total lines") + }) + + it("should handle binary files by throwing an error", async () => { + mockedIsBinaryFile.mockResolvedValue(true) + + await expect(extractTextFromFile("/test/binary.bin", 100)).rejects.toThrow( + "Cannot read text for file type: .bin", + ) + }) + + it("should handle file not found errors", async () => { + mockedFs.access.mockRejectedValue(new Error("ENOENT")) + + await expect(extractTextFromFile("/test/nonexistent.ts", 100)).rejects.toThrow( + "File not found: /test/nonexistent.ts", + ) + }) +}) diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index eb02f63b95..8231c609be 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -5,6 +5,8 @@ import mammoth from "mammoth" import fs from "fs/promises" import { isBinaryFile } from "isbinaryfile" import { extractTextFromXLSX } from "./extract-text-from-xlsx" +import { countFileLines } from "./line-counter" +import { readLines } from "./read-lines" async function extractTextFromPDF(filePath: string): Promise { const dataBuffer = await fs.readFile(filePath) @@ -48,7 +50,27 @@ export function getSupportedBinaryFormats(): string[] { return Object.keys(SUPPORTED_BINARY_FORMATS) } -export async function extractTextFromFile(filePath: string): Promise { +/** + * Extracts text content from a file, with support for various formats including PDF, DOCX, XLSX, and plain text. + * For large text files, can limit the number of lines read to prevent context exhaustion. + * + * @param filePath - Path to the file to extract text from + * @param maxReadFileLine - Maximum number of lines to read from text files. + * Use UNLIMITED_LINES (-1) or undefined for no limit. + * Must be a positive integer or UNLIMITED_LINES. + * @returns Promise resolving to the extracted text content with line numbers + * @throws {Error} If file not found, unsupported format, or invalid parameters + */ +export async function extractTextFromFile(filePath: string, maxReadFileLine?: number): Promise { + // Validate maxReadFileLine parameter + if (maxReadFileLine !== undefined && maxReadFileLine !== -1) { + if (!Number.isInteger(maxReadFileLine) || maxReadFileLine < 1) { + throw new Error( + `Invalid maxReadFileLine: ${maxReadFileLine}. Must be a positive integer or -1 for unlimited.`, + ) + } + } + try { await fs.access(filePath) } catch (error) { @@ -67,6 +89,20 @@ export async function extractTextFromFile(filePath: string): Promise { const isBinary = await isBinaryFile(filePath).catch(() => false) if (!isBinary) { + // Check if we need to apply line limit + if (maxReadFileLine !== undefined && maxReadFileLine !== -1) { + const totalLines = await countFileLines(filePath) + if (totalLines > maxReadFileLine) { + // Read only up to maxReadFileLine (endLine is 0-based and inclusive) + const content = await readLines(filePath, maxReadFileLine - 1, 0) + const numberedContent = addLineNumbers(content) + return ( + numberedContent + + `\n\n[File truncated: showing ${maxReadFileLine} of ${totalLines} total lines. The file is too large and may exhaust the context window if read in full.]` + ) + } + } + // Read the entire file if no limit or file is within limit return addLineNumbers(await fs.readFile(filePath, "utf8")) } else { throw new Error(`Cannot read text for file type: ${fileExtension}`) diff --git a/src/package.json b/src/package.json index c816837bb7..e1dc8ae72e 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.23.19", + "version": "3.25.2", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -442,6 +442,7 @@ "fzf": "^0.5.2", "get-folder-size": "^5.0.0", "google-auth-library": "^9.15.1", + "gray-matter": "^4.0.3", "i18next": "^25.0.0", "ignore": "^7.0.3", "isbinaryfile": "^5.0.2", diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index 505aee7668..fbc4a24118 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -7,6 +7,7 @@ import { DirectoryScanner } from "./processors" import { CacheManager } from "./cache-manager" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" +import { t } from "../../i18n" /** * Manages the code indexing workflow, coordinating between different services and managers. @@ -94,6 +95,13 @@ export class CodeIndexOrchestrator { * Initiates the indexing process (initial scan and starts watcher). */ public async startIndexing(): Promise { + // Check if workspace is available first + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + this.stateManager.setSystemState("Error", t("embeddings:orchestrator.indexingRequiresWorkspace")) + console.warn("[CodeIndexOrchestrator] Start rejected: No workspace folder open.") + return + } + if (!this.configManager.isFeatureConfigured) { this.stateManager.setSystemState("Standby", "Missing configuration. Save your settings to start indexing.") console.warn("[CodeIndexOrchestrator] Start rejected: Missing configuration.") @@ -165,9 +173,7 @@ export class CodeIndexOrchestrator { const firstError = batchErrors[0] throw new Error(`Indexing failed: ${firstError.message}`) } else { - throw new Error( - "Indexing failed: No code blocks were successfully indexed. This usually indicates an embedder configuration issue.", - ) + throw new Error(t("embeddings:orchestrator.indexingFailedNoBlocks")) } } @@ -191,14 +197,12 @@ export class CodeIndexOrchestrator { // Final sanity check: If we found blocks but indexed none and somehow no errors were reported, // this is still a failure if (cumulativeBlocksFoundSoFar > 0 && cumulativeBlocksIndexed === 0) { - throw new Error( - "Indexing failed: No code blocks were successfully indexed despite finding files to process. This indicates a critical embedder failure.", - ) + throw new Error(t("embeddings:orchestrator.indexingFailedCritical")) } await this._startWatcher() - this.stateManager.setSystemState("Indexed", "File watcher started.") + this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted")) } catch (error: any) { console.error("[CodeIndexOrchestrator] Error during indexing:", error) TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { @@ -219,7 +223,12 @@ export class CodeIndexOrchestrator { await this.cacheManager.clearCacheFile() - this.stateManager.setSystemState("Error", `Failed during initial scan: ${error.message || "Unknown error"}`) + this.stateManager.setSystemState( + "Error", + t("embeddings:orchestrator.failedDuringInitialScan", { + errorMessage: error.message || t("embeddings:orchestrator.unknownError"), + }), + ) this.stopWatcher() } finally { this._isProcessing = false @@ -235,7 +244,7 @@ export class CodeIndexOrchestrator { this._fileWatcherSubscriptions = [] if (this.stateManager.state !== "Error") { - this.stateManager.setSystemState("Standby", "File watcher stopped.") + this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.fileWatcherStopped")) } this._isProcessing = false } diff --git a/src/services/command/__tests__/frontmatter-commands.spec.ts b/src/services/command/__tests__/frontmatter-commands.spec.ts new file mode 100644 index 0000000000..1171a4b24c --- /dev/null +++ b/src/services/command/__tests__/frontmatter-commands.spec.ts @@ -0,0 +1,391 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import fs from "fs/promises" +import * as path from "path" +import { getCommand, getCommands } from "../commands" + +// Mock fs and path modules +vi.mock("fs/promises") +vi.mock("../roo-config", () => ({ + getGlobalRooDirectory: vi.fn(() => "/mock/global/.roo"), + getProjectRooDirectoryForCwd: vi.fn(() => "/mock/project/.roo"), +})) + +const mockFs = vi.mocked(fs) + +describe("Command loading with frontmatter", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe("getCommand with frontmatter", () => { + it("should load command with description from frontmatter", async () => { + const commandContent = `--- +description: Sets up the development environment +author: John Doe +--- + +# Setup Command + +Run the following commands: +\`\`\`bash +npm install +npm run build +\`\`\`` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```", + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), + description: "Sets up the development environment", + argumentHint: undefined, + }) + }) + + it("should load command without frontmatter", async () => { + const commandContent = `# Setup Command + +Run the following commands: +\`\`\`bash +npm install +npm run build +\`\`\`` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Setup Command\n\nRun the following commands:\n```bash\nnpm install\nnpm run build\n```", + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), + description: undefined, + argumentHint: undefined, + }) + }) + + it("should handle empty description in frontmatter", async () => { + const commandContent = `--- +description: "" +author: John Doe +--- + +# Setup Command + +Command content here.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result?.description).toBeUndefined() + }) + + it("should handle malformed frontmatter gracefully", async () => { + const commandContent = `--- +description: Test +invalid: yaml: [ +--- + +# Setup Command + +Command content here.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: commandContent.trim(), + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), + description: undefined, + argumentHint: undefined, + }) + }) + + it("should prioritize project commands over global commands", async () => { + const projectCommandContent = `--- +description: Project-specific setup +--- + +# Project Setup + +Project-specific setup instructions.` + + const globalCommandContent = `--- +description: Global setup +--- + +# Global Setup + +Global setup instructions.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi + .fn() + .mockResolvedValueOnce(projectCommandContent) // First call for project + .mockResolvedValueOnce(globalCommandContent) // Second call for global (shouldn't be used) + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Project Setup\n\nProject-specific setup instructions.", + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "setup.md"), + description: "Project-specific setup", + argumentHint: undefined, + }) + }) + + it("should fall back to global command if project command doesn't exist", async () => { + const globalCommandContent = `--- +description: Global setup command +--- + +# Global Setup + +Global setup instructions.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi + .fn() + .mockRejectedValueOnce(new Error("File not found")) // Project command doesn't exist + .mockResolvedValueOnce(globalCommandContent) // Global command exists + + const result = await getCommand("/test/cwd", "setup") + + expect(result).toEqual({ + name: "setup", + content: "# Global Setup\n\nGlobal setup instructions.", + source: "global", + filePath: expect.stringContaining(path.join(".roo", "commands", "setup.md")), + description: "Global setup command", + argumentHint: undefined, + }) + }) + }) + + describe("argument-hint functionality", () => { + it("should load command with argument-hint from frontmatter", async () => { + const commandContent = `--- +description: Create a new release of the Roo Code extension +argument-hint: patch | minor | major +--- + +# Release Command + +Create a new release.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "release") + + expect(result).toEqual({ + name: "release", + content: "# Release Command\n\nCreate a new release.", + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "release.md"), + description: "Create a new release of the Roo Code extension", + argumentHint: "patch | minor | major", + }) + }) + + it("should handle command with both description and argument-hint", async () => { + const commandContent = `--- +description: Deploy application to environment +argument-hint: staging | production +author: DevOps Team +--- + +# Deploy Command + +Deploy the application.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "deploy") + + expect(result).toEqual({ + name: "deploy", + content: "# Deploy Command\n\nDeploy the application.", + source: "project", + filePath: path.join("/test/cwd", ".roo", "commands", "deploy.md"), + description: "Deploy application to environment", + argumentHint: "staging | production", + }) + }) + + it("should handle empty argument-hint in frontmatter", async () => { + const commandContent = `--- +description: Test command +argument-hint: "" +--- + +# Test Command + +Test content.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "test") + + expect(result?.argumentHint).toBeUndefined() + }) + + it("should handle whitespace-only argument-hint in frontmatter", async () => { + const commandContent = `--- +description: Test command +argument-hint: " " +--- + +# Test Command + +Test content.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "test") + + expect(result?.argumentHint).toBeUndefined() + }) + + it("should handle non-string argument-hint in frontmatter", async () => { + const commandContent = `--- +description: Test command +argument-hint: 123 +--- + +# Test Command + +Test content.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readFile = vi.fn().mockResolvedValue(commandContent) + + const result = await getCommand("/test/cwd", "test") + + expect(result?.argumentHint).toBeUndefined() + }) + }) + + describe("getCommands with frontmatter", () => { + it("should load multiple commands with descriptions", async () => { + const setupContent = `--- +description: Sets up the development environment +--- + +# Setup Command + +Setup instructions.` + + const deployContent = `--- +description: Deploys the application to production +--- + +# Deploy Command + +Deploy instructions.` + + const buildContent = `# Build Command + +Build instructions without frontmatter.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readdir = vi.fn().mockResolvedValue([ + { name: "setup.md", isFile: () => true }, + { name: "deploy.md", isFile: () => true }, + { name: "build.md", isFile: () => true }, + { name: "not-markdown.txt", isFile: () => true }, // Should be ignored + ]) + mockFs.readFile = vi + .fn() + .mockResolvedValueOnce(setupContent) + .mockResolvedValueOnce(deployContent) + .mockResolvedValueOnce(buildContent) + + const result = await getCommands("/test/cwd") + + expect(result).toHaveLength(3) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "setup", + description: "Sets up the development environment", + argumentHint: undefined, + }), + expect.objectContaining({ + name: "deploy", + description: "Deploys the application to production", + argumentHint: undefined, + }), + expect.objectContaining({ + name: "build", + description: undefined, + argumentHint: undefined, + }), + ]), + ) + }) + + it("should load multiple commands with argument hints", async () => { + const releaseContent = `--- +description: Create a new release +argument-hint: patch | minor | major +--- + +# Release Command + +Create a release.` + + const deployContent = `--- +description: Deploy to environment +argument-hint: staging | production +--- + +# Deploy Command + +Deploy the app.` + + mockFs.stat = vi.fn().mockResolvedValue({ isDirectory: () => true }) + mockFs.readdir = vi.fn().mockResolvedValue([ + { name: "release.md", isFile: () => true }, + { name: "deploy.md", isFile: () => true }, + ]) + mockFs.readFile = vi.fn().mockResolvedValueOnce(releaseContent).mockResolvedValueOnce(deployContent) + + const result = await getCommands("/test/cwd") + + expect(result).toHaveLength(2) + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "release", + description: "Create a new release", + argumentHint: "patch | minor | major", + }), + expect.objectContaining({ + name: "deploy", + description: "Deploy to environment", + argumentHint: "staging | production", + }), + ]), + ) + }) + }) +}) diff --git a/src/services/command/commands.ts b/src/services/command/commands.ts new file mode 100644 index 0000000000..452269511c --- /dev/null +++ b/src/services/command/commands.ts @@ -0,0 +1,206 @@ +import fs from "fs/promises" +import * as path from "path" +import matter from "gray-matter" +import { getGlobalRooDirectory, getProjectRooDirectoryForCwd } from "../roo-config" + +export interface Command { + name: string + content: string + source: "global" | "project" + filePath: string + description?: string + argumentHint?: string +} + +/** + * Get all available commands from both global and project directories + */ +export async function getCommands(cwd: string): Promise { + const commands = new Map() + + // Scan global commands first + const globalDir = path.join(getGlobalRooDirectory(), "commands") + await scanCommandDirectory(globalDir, "global", commands) + + // Scan project commands (these override global ones) + const projectDir = path.join(getProjectRooDirectoryForCwd(cwd), "commands") + await scanCommandDirectory(projectDir, "project", commands) + + return Array.from(commands.values()) +} + +/** + * Get a specific command by name (optimized to avoid scanning all commands) + */ +export async function getCommand(cwd: string, name: string): Promise { + // Try to find the command directly without scanning all commands + const projectDir = path.join(getProjectRooDirectoryForCwd(cwd), "commands") + const globalDir = path.join(getGlobalRooDirectory(), "commands") + + // Check project directory first (project commands override global ones) + const projectCommand = await tryLoadCommand(projectDir, name, "project") + if (projectCommand) { + return projectCommand + } + + // Check global directory if not found in project + const globalCommand = await tryLoadCommand(globalDir, name, "global") + return globalCommand +} + +/** + * Try to load a specific command from a directory + */ +async function tryLoadCommand( + dirPath: string, + name: string, + source: "global" | "project", +): Promise { + try { + const stats = await fs.stat(dirPath) + if (!stats.isDirectory()) { + return undefined + } + + // Try to find the command file directly + const commandFileName = `${name}.md` + const filePath = path.join(dirPath, commandFileName) + + try { + const content = await fs.readFile(filePath, "utf-8") + + let parsed + let description: string | undefined + let argumentHint: string | undefined + let commandContent: string + + try { + // Try to parse frontmatter with gray-matter + parsed = matter(content) + description = + typeof parsed.data.description === "string" && parsed.data.description.trim() + ? parsed.data.description.trim() + : undefined + argumentHint = + typeof parsed.data["argument-hint"] === "string" && parsed.data["argument-hint"].trim() + ? parsed.data["argument-hint"].trim() + : undefined + commandContent = parsed.content.trim() + } catch (frontmatterError) { + // If frontmatter parsing fails, treat the entire content as command content + description = undefined + argumentHint = undefined + commandContent = content.trim() + } + + return { + name, + content: commandContent, + source, + filePath, + description, + argumentHint, + } + } catch (error) { + // File doesn't exist or can't be read + return undefined + } + } catch (error) { + // Directory doesn't exist or can't be read + return undefined + } +} + +/** + * Get command names for autocomplete + */ +export async function getCommandNames(cwd: string): Promise { + const commands = await getCommands(cwd) + return commands.map((cmd) => cmd.name) +} + +/** + * Scan a specific command directory + */ +async function scanCommandDirectory( + dirPath: string, + source: "global" | "project", + commands: Map, +): Promise { + try { + const stats = await fs.stat(dirPath) + if (!stats.isDirectory()) { + return + } + + const entries = await fs.readdir(dirPath, { withFileTypes: true }) + + for (const entry of entries) { + if (entry.isFile() && isMarkdownFile(entry.name)) { + const filePath = path.join(dirPath, entry.name) + const commandName = getCommandNameFromFile(entry.name) + + try { + const content = await fs.readFile(filePath, "utf-8") + + let parsed + let description: string | undefined + let argumentHint: string | undefined + let commandContent: string + + try { + // Try to parse frontmatter with gray-matter + parsed = matter(content) + description = + typeof parsed.data.description === "string" && parsed.data.description.trim() + ? parsed.data.description.trim() + : undefined + argumentHint = + typeof parsed.data["argument-hint"] === "string" && parsed.data["argument-hint"].trim() + ? parsed.data["argument-hint"].trim() + : undefined + commandContent = parsed.content.trim() + } catch (frontmatterError) { + // If frontmatter parsing fails, treat the entire content as command content + description = undefined + argumentHint = undefined + commandContent = content.trim() + } + + // Project commands override global ones + if (source === "project" || !commands.has(commandName)) { + commands.set(commandName, { + name: commandName, + content: commandContent, + source, + filePath, + description, + argumentHint, + }) + } + } catch (error) { + console.warn(`Failed to read command file ${filePath}:`, error) + } + } + } + } catch (error) { + // Directory doesn't exist or can't be read - this is fine + } +} + +/** + * Extract command name from filename (strip .md extension only) + */ +export function getCommandNameFromFile(filename: string): string { + if (filename.toLowerCase().endsWith(".md")) { + return filename.slice(0, -3) + } + return filename +} + +/** + * Check if a file is a markdown file + */ +export function isMarkdownFile(filename: string): boolean { + return filename.toLowerCase().endsWith(".md") +} diff --git a/src/services/glob/__tests__/list-files.spec.ts b/src/services/glob/__tests__/list-files.spec.ts index 6c133a732a..d855388002 100644 --- a/src/services/glob/__tests__/list-files.spec.ts +++ b/src/services/glob/__tests__/list-files.spec.ts @@ -3,6 +3,18 @@ import * as path from "path" import { listFiles } from "../list-files" import * as childProcess from "child_process" +vi.mock("child_process") +vi.mock("fs") +vi.mock("vscode", () => ({ + env: { + appRoot: "/mock/vscode/app/root", + }, +})) + +vi.mock("../../ripgrep", () => ({ + getBinPath: vi.fn().mockResolvedValue("/mock/path/to/rg"), +})) + vi.mock("../list-files", async () => { const actual = await vi.importActual("../list-files") return { @@ -83,8 +95,11 @@ describe("list-files symlink support", () => { mockSpawn.mockReturnValue(mockProcess as any) + // Use a test directory path + const testDir = "/test/dir" + // Call listFiles to trigger ripgrep execution - await listFiles("/test/dir", false, 100) + await listFiles(testDir, false, 100) // Verify that spawn was called with --follow flag (the critical fix) const [rgPath, args] = mockSpawn.mock.calls[0] @@ -93,9 +108,12 @@ describe("list-files symlink support", () => { expect(args).toContain("--hidden") expect(args).toContain("--follow") // This is the critical assertion - the fix should add this flag - // Platform-agnostic path check - verify the last argument is the resolved path - const expectedPath = path.resolve("/test/dir") - expect(args[args.length - 1]).toBe(expectedPath) + // Platform-agnostic path check - verify the last argument ends with the expected path + const lastArg = args[args.length - 1] + // On Windows, the path might be resolved to something like D:\test\dir + // On Unix, it would be /test/dir + // So we just check that it ends with the expected segments + expect(lastArg).toMatch(/[/\\]test[/\\]dir$/) }) it("should include --follow flag for recursive listings too", async () => { @@ -124,8 +142,11 @@ describe("list-files symlink support", () => { mockSpawn.mockReturnValue(mockProcess as any) + // Use a test directory path + const testDir = "/test/dir" + // Call listFiles with recursive=true - await listFiles("/test/dir", true, 100) + await listFiles(testDir, true, 100) // Verify that spawn was called with --follow flag (the critical fix) const [rgPath, args] = mockSpawn.mock.calls[0] @@ -134,9 +155,12 @@ describe("list-files symlink support", () => { expect(args).toContain("--hidden") expect(args).toContain("--follow") // This should be present in recursive mode too - // Platform-agnostic path check - verify the last argument is the resolved path - const expectedPath = path.resolve("/test/dir") - expect(args[args.length - 1]).toBe(expectedPath) + // Platform-agnostic path check - verify the last argument ends with the expected path + const lastArg = args[args.length - 1] + // On Windows, the path might be resolved to something like D:\test\dir + // On Unix, it would be /test/dir + // So we just check that it ends with the expected segments + expect(lastArg).toMatch(/[/\\]test[/\\]dir$/) }) it("should ensure first-level directories are included when limit is reached", async () => { @@ -159,18 +183,19 @@ describe("list-files symlink support", () => { on: vi.fn((event, callback) => { if (event === "data") { // Return many file paths to trigger the limit + // Note: ripgrep returns relative paths const paths = [ - "/test/dir/a_dir/", - "/test/dir/a_dir/subdir1/", - "/test/dir/a_dir/subdir1/file1.txt", - "/test/dir/a_dir/subdir1/file2.txt", - "/test/dir/a_dir/subdir2/", - "/test/dir/a_dir/subdir2/file3.txt", - "/test/dir/a_dir/file4.txt", - "/test/dir/a_dir/file5.txt", - "/test/dir/file1.txt", - "/test/dir/file2.txt", + "a_dir/", + "a_dir/subdir1/", + "a_dir/subdir1/file1.txt", + "a_dir/subdir1/file2.txt", + "a_dir/subdir2/", + "a_dir/subdir2/file3.txt", + "a_dir/file4.txt", + "a_dir/file5.txt", + "file1.txt", + "file2.txt", // Note: b_dir and c_dir are missing from ripgrep output ].join("\n") + "\n" setTimeout(() => callback(paths), 10) @@ -216,3 +241,321 @@ describe("list-files symlink support", () => { expect(hasCDir).toBe(true) }) }) + +describe("hidden directory exclusion", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should exclude .git subdirectories from recursive directory listing", async () => { + // Mock filesystem structure with .git subdirectories + const mockReaddir = vi.fn() + vi.mocked(fs.promises).readdir = mockReaddir + + // Mock the directory structure: + // /test/ + // .git/ + // hooks/ + // objects/ + // src/ + // components/ + mockReaddir + .mockResolvedValueOnce([ + { name: ".git", isDirectory: () => true, isSymbolicLink: () => false }, + { name: "src", isDirectory: () => true, isSymbolicLink: () => false }, + ]) + .mockResolvedValueOnce([ + // src subdirectories (should be included) + { name: "components", isDirectory: () => true, isSymbolicLink: () => false }, + ]) + .mockResolvedValueOnce([]) // components/ is empty + + // Mock ripgrep to return no files + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + // No files returned + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 10) + } + }), + kill: vi.fn(), + } + mockSpawn.mockReturnValue(mockProcess as any) + + // Call listFiles with recursive=true + const [result] = await listFiles("/test", true, 100) + + // Verify that .git subdirectories are NOT included + const directories = result.filter((item) => item.endsWith("/")) + + // More specific checks - look for exact paths + const hasSrcDir = directories.some((dir) => dir.endsWith("/test/src/") || dir.endsWith("src/")) + const hasComponentsDir = directories.some( + (dir) => + dir.endsWith("/test/src/components/") || dir.endsWith("src/components/") || dir.includes("components/"), + ) + const hasGitDir = directories.some((dir) => dir.includes(".git/")) + + // Should include src/ and src/components/ but NOT .git/ or its subdirectories + expect(hasSrcDir).toBe(true) + expect(hasComponentsDir).toBe(true) + + // Should NOT include .git (hidden directories are excluded) + expect(hasGitDir).toBe(false) + }) + + it("should allow explicit targeting of hidden directories", async () => { + // Mock filesystem structure for explicit .roo-memory targeting + const mockReaddir = vi.fn() + vi.mocked(fs.promises).readdir = mockReaddir + + // Mock .roo-memory directory contents + mockReaddir.mockResolvedValueOnce([ + { name: "tasks", isDirectory: () => true, isSymbolicLink: () => false }, + { name: "context", isDirectory: () => true, isSymbolicLink: () => false }, + ]) + + // Mock ripgrep to return no files + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + // No files returned + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 10) + } + }), + kill: vi.fn(), + } + mockSpawn.mockReturnValue(mockProcess as any) + + // Call listFiles explicitly targeting .roo-memory directory + const [result] = await listFiles("/test/.roo-memory", true, 100) + + // When explicitly targeting a hidden directory, its subdirectories should be included + const directories = result.filter((item) => item.endsWith("/")) + + const hasTasksDir = directories.some((dir) => dir.includes(".roo-memory/tasks/") || dir.includes("tasks/")) + const hasContextDir = directories.some( + (dir) => dir.includes(".roo-memory/context/") || dir.includes("context/"), + ) + + expect(hasTasksDir).toBe(true) + expect(hasContextDir).toBe(true) + }) + + it("should include top-level files when recursively listing a hidden directory that's also in DIRS_TO_IGNORE", async () => { + // This test specifically addresses the bug where files at the root level of .roo/temp + // were being excluded when using recursive listing + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + // Simulate files that should be found in .roo/temp + // Note: ripgrep returns relative paths + setTimeout(() => { + callback("teste1.md\n") + callback("22/test2.md\n") + }, 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + + mockSpawn.mockReturnValue(mockProcess as any) + + // Mock directory listing for .roo/temp + const mockReaddir = vi.fn() + vi.mocked(fs.promises).readdir = mockReaddir + mockReaddir.mockResolvedValueOnce([{ name: "22", isDirectory: () => true, isSymbolicLink: () => false }]) + + // Call listFiles targeting .roo/temp (which is both hidden and in DIRS_TO_IGNORE) + const [files] = await listFiles("/test/.roo/temp", true, 100) + + // Verify ripgrep was called with correct arguments + const [rgPath, args] = mockSpawn.mock.calls[0] + expect(args).toContain("--no-ignore-vcs") + expect(args).toContain("--no-ignore") + + // Check for the inclusion patterns that should be added + expect(args).toContain("-g") + const gIndex = args.indexOf("-g") + expect(args[gIndex + 1]).toBe("*") + + // Verify that both top-level and nested files are included + const fileNames = files.map((f) => path.basename(f)) + expect(fileNames).toContain("teste1.md") + expect(fileNames).toContain("test2.md") + + // Ensure the top-level file is actually included + const topLevelFile = files.find((f) => f.endsWith("teste1.md")) + expect(topLevelFile).toBeTruthy() + }) +}) + +describe("buildRecursiveArgs edge cases", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should correctly detect hidden directories with trailing slashes", async () => { + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + setTimeout(() => callback("file.txt\n"), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + mockSpawn.mockReturnValue(mockProcess as any) + + // Test with trailing slash on hidden directory + await listFiles("/test/.hidden/", true, 100) + + const [rgPath, args] = mockSpawn.mock.calls[0] + // When targeting a hidden directory, these flags should be present + expect(args).toContain("--no-ignore-vcs") + expect(args).toContain("--no-ignore") + expect(args).toContain("-g") + const gIndex = args.indexOf("-g") + expect(args[gIndex + 1]).toBe("*") + }) + + it("should correctly detect hidden directories with redundant separators", async () => { + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + setTimeout(() => callback("file.txt\n"), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + mockSpawn.mockReturnValue(mockProcess as any) + + // Test with redundant separators before hidden directory + await listFiles("/test//.hidden", true, 100) + + const [rgPath, args] = mockSpawn.mock.calls[0] + // When targeting a hidden directory, these flags should be present + expect(args).toContain("--no-ignore-vcs") + expect(args).toContain("--no-ignore") + expect(args).toContain("-g") + const gIndex = args.indexOf("-g") + expect(args[gIndex + 1]).toBe("*") + }) + + it("should correctly detect nested hidden directories with mixed separators", async () => { + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + setTimeout(() => callback("file.txt\n"), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + mockSpawn.mockReturnValue(mockProcess as any) + + // Test with complex path including hidden directory + await listFiles("/test//normal/.hidden//subdir/", true, 100) + + const [rgPath, args] = mockSpawn.mock.calls[0] + // When targeting a path containing a hidden directory, these flags should be present + expect(args).toContain("--no-ignore-vcs") + expect(args).toContain("--no-ignore") + expect(args).toContain("-g") + const gIndex = args.indexOf("-g") + expect(args[gIndex + 1]).toBe("*") + }) + + it("should not detect hidden directories when path only has dots in filenames", async () => { + const mockSpawn = vi.mocked(childProcess.spawn) + const mockProcess = { + stdout: { + on: vi.fn((event, callback) => { + if (event === "data") { + setTimeout(() => callback("file.txt\n"), 10) + } + }), + }, + stderr: { + on: vi.fn(), + }, + on: vi.fn((event, callback) => { + if (event === "close") { + setTimeout(() => callback(0), 20) + } + }), + kill: vi.fn(), + } + mockSpawn.mockReturnValue(mockProcess as any) + + // Test with a path that has dots but no hidden directories + await listFiles("/test/file.with.dots/normal", true, 100) + + const [rgPath, args] = mockSpawn.mock.calls[0] + // Should NOT have the special flags for hidden directories + expect(args).not.toContain("--no-ignore-vcs") + expect(args).not.toContain("--no-ignore") + }) +}) diff --git a/src/services/glob/constants.ts b/src/services/glob/constants.ts index 1ddcc37df9..380e4afaf3 100644 --- a/src/services/glob/constants.ts +++ b/src/services/glob/constants.ts @@ -20,5 +20,6 @@ export const DIRS_TO_IGNORE = [ "deps", "pkg", "Pods", + ".git", ".*", ] diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 05fa8a1d7b..7347515784 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -8,6 +8,20 @@ import { arePathsEqual } from "../../utils/path" import { getBinPath } from "../../services/ripgrep" import { DIRS_TO_IGNORE } from "./constants" +/** + * Context object for directory scanning operations + */ +interface ScanContext { + /** Whether this is the explicitly targeted directory */ + isTargetDir: boolean + /** Whether we're inside an explicitly targeted hidden directory */ + insideExplicitHiddenTarget: boolean + /** The base path for the scan operation */ + basePath: string + /** The ignore instance for gitignore handling */ + ignoreInstance: ReturnType +} + /** * List files in a directory, with optional recursive traversal * @@ -70,7 +84,13 @@ async function getFirstLevelDirectories(dirPath: string, ignoreInstance: ReturnT for (const entry of entries) { if (entry.isDirectory() && !entry.isSymbolicLink()) { const fullDirPath = path.join(absolutePath, entry.name) - if (shouldIncludeDirectory(entry.name, fullDirPath, dirPath, ignoreInstance)) { + const context: ScanContext = { + isTargetDir: false, + insideExplicitHiddenTarget: false, + basePath: dirPath, + ignoreInstance, + } + if (shouldIncludeDirectory(entry.name, fullDirPath, context)) { const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/` directories.push(formattedPath) } @@ -179,9 +199,14 @@ async function listFilesWithRipgrep( recursive: boolean, limit: number, ): Promise { + const rgArgs = buildRipgrepArgs(dirPath, recursive) + + const relativePaths = await execRipgrep(rgPath, rgArgs, limit) + + // Convert relative paths from ripgrep to absolute paths + // Resolve dirPath once here for the mapping operation const absolutePath = path.resolve(dirPath) - const rgArgs = buildRipgrepArgs(absolutePath, recursive) - return execRipgrep(rgPath, rgArgs, limit) + return relativePaths.map((relativePath) => path.resolve(absolutePath, relativePath)) } /** @@ -192,7 +217,7 @@ function buildRipgrepArgs(dirPath: string, recursive: boolean): string[] { const args = ["--files", "--hidden", "--follow"] if (recursive) { - return [...args, ...buildRecursiveArgs(), dirPath] + return [...args, ...buildRecursiveArgs(dirPath), dirPath] } else { return [...args, ...buildNonRecursiveArgs(), dirPath] } @@ -201,14 +226,62 @@ function buildRipgrepArgs(dirPath: string, recursive: boolean): string[] { /** * Build ripgrep arguments for recursive directory traversal */ -function buildRecursiveArgs(): string[] { +function buildRecursiveArgs(dirPath: string): string[] { const args: string[] = [] // In recursive mode, respect .gitignore by default // (ripgrep does this automatically) + // Check if we're explicitly targeting a hidden directory + // Normalize the path first to handle edge cases + const normalizedPath = path.normalize(dirPath) + // Split by separator and filter out empty parts + // This handles cases like trailing slashes, multiple separators, etc. + const pathParts = normalizedPath.split(path.sep).filter((part) => part.length > 0) + const isTargetingHiddenDir = pathParts.some((part) => part.startsWith(".")) + + // Get the target directory name to check if it's in the ignore list + const targetDirName = path.basename(dirPath) + const isTargetInIgnoreList = DIRS_TO_IGNORE.includes(targetDirName) + + // If targeting a hidden directory or a directory in the ignore list, + // use special handling to ensure all files are shown + if (isTargetingHiddenDir || isTargetInIgnoreList) { + args.push("--no-ignore-vcs") + args.push("--no-ignore") + + // When targeting an ignored directory, we need to be careful with glob patterns + // Add a pattern to explicitly include files at the root level + args.push("-g", "*") + args.push("-g", "**/*") + } + // Apply directory exclusions for recursive searches for (const dir of DIRS_TO_IGNORE) { + // Special handling for hidden directories pattern + if (dir === ".*") { + // If we're explicitly targeting a hidden directory, don't exclude hidden files/dirs + // This allows the target hidden directory and all its contents to be listed + if (!isTargetingHiddenDir) { + // Not targeting hidden dir: exclude all hidden directories + args.push("-g", `!**/.*/**`) + } + // If targeting hidden dir: don't add any exclusion for hidden directories + continue + } + + // When explicitly targeting a directory that's in the ignore list (e.g., "temp"), + // we need special handling: + // - Don't add any exclusion pattern for the target directory itself + // - Only exclude nested subdirectories with the same name + // This ensures all files in the target directory are listed, while still + // preventing recursion into nested directories with the same ignored name + if (dir === targetDirName && isTargetInIgnoreList) { + // Skip adding any exclusion pattern - we want to see everything in the target directory + continue + } + + // For all other cases, exclude the directory pattern globally args.push("-g", `!**/${dir}/**`) } @@ -231,8 +304,11 @@ function buildNonRecursiveArgs(): string[] { // Apply directory exclusions for non-recursive searches for (const dir of DIRS_TO_IGNORE) { if (dir === ".*") { - // For hidden files/dirs in non-recursive mode - args.push("-g", "!.*") + // For hidden directories in non-recursive mode, we want to show the directories + // themselves but not their contents. Since we're using --maxdepth 1, this + // naturally happens - we just need to avoid excluding the directories entirely. + // We'll let the directory scanning logic handle the visibility. + continue } else { // Direct children only args.push("-g", `!${dir}`) @@ -261,7 +337,7 @@ async function createIgnoreInstance(dirPath: string): Promise { + // For environment details generation, we don't want to treat the root as a "target" + // if we're doing a general recursive scan, as this would include hidden directories + // Only treat as target if we're explicitly scanning a single hidden directory + const isExplicitHiddenTarget = path.basename(absolutePath).startsWith(".") + + // Create initial context for the scan + const initialContext: ScanContext = { + isTargetDir: isExplicitHiddenTarget, + insideExplicitHiddenTarget: isExplicitHiddenTarget, + basePath: dirPath, + ignoreInstance, + } + + async function scanDirectory(currentPath: string, context: ScanContext): Promise { try { // List all entries in the current directory const entries = await fs.promises.readdir(currentPath, { withFileTypes: true }) @@ -323,61 +412,155 @@ async function listFilteredDirectories( const dirName = entry.name const fullDirPath = path.join(currentPath, dirName) + // Create context for subdirectory checks + // Subdirectories found during scanning are never target directories themselves + const subdirContext: ScanContext = { + ...context, + isTargetDir: false, + } + // Check if this directory should be included - if (shouldIncludeDirectory(dirName, fullDirPath, dirPath, ignoreInstance)) { + if (shouldIncludeDirectory(dirName, fullDirPath, subdirContext)) { // Add the directory to our results (with trailing slash) + // fullDirPath is already absolute since it's built with path.join from absolutePath const formattedPath = fullDirPath.endsWith("/") ? fullDirPath : `${fullDirPath}/` directories.push(formattedPath) + } - // If recursive mode and not a ignored directory, scan subdirectories - if (recursive && !isDirectoryExplicitlyIgnored(dirName)) { - await scanDirectory(fullDirPath) + // If recursive mode and not a ignored directory, scan subdirectories + // Don't recurse into hidden directories unless they are the explicit target + // or we're already inside an explicitly targeted hidden directory + const isHiddenDir = dirName.startsWith(".") + + // Use the same logic as shouldIncludeDirectory for recursion decisions + // When inside an explicitly targeted hidden directory, only block critical directories + let shouldRecurseIntoDir = true + if (context.insideExplicitHiddenTarget) { + // Only apply the most critical ignore patterns when inside explicit hidden target + shouldRecurseIntoDir = !CRITICAL_IGNORE_PATTERNS.has(dirName) + } else { + shouldRecurseIntoDir = !isDirectoryExplicitlyIgnored(dirName) + } + + const shouldRecurse = + recursive && + shouldRecurseIntoDir && + !( + isHiddenDir && + DIRS_TO_IGNORE.includes(".*") && + !context.isTargetDir && + !context.insideExplicitHiddenTarget + ) + if (shouldRecurse) { + // If we're entering a hidden directory that's the target, or we're already inside one, + // mark that we're inside an explicitly targeted hidden directory + const newInsideExplicitHiddenTarget = + context.insideExplicitHiddenTarget || (isHiddenDir && context.isTargetDir) + const newContext: ScanContext = { + ...context, + isTargetDir: false, + insideExplicitHiddenTarget: newInsideExplicitHiddenTarget, } + await scanDirectory(fullDirPath, newContext) } } } } catch (err) { - // Silently continue if we can't read a directory + // Continue if we can't read a directory console.warn(`Could not read directory ${currentPath}: ${err}`) } } // Start scanning from the root directory - await scanDirectory(absolutePath) + await scanDirectory(absolutePath, initialContext) return directories } /** - * Determine if a directory should be included in results based on filters + * Critical directories that should always be ignored, even inside explicitly targeted hidden directories */ -function shouldIncludeDirectory( - dirName: string, +const CRITICAL_IGNORE_PATTERNS = new Set(["node_modules", ".git", "__pycache__", "venv", "env"]) + +/** + * Check if a directory matches any of the given patterns + */ +function matchesIgnorePattern(dirName: string, patterns: string[]): boolean { + for (const pattern of patterns) { + if (pattern === dirName || (pattern.includes("/") && pattern.split("/")[0] === dirName)) { + return true + } + } + return false +} + +/** + * Check if a directory is ignored by gitignore + */ +function isIgnoredByGitignore( fullDirPath: string, basePath: string, ignoreInstance: ReturnType, ): boolean { - // Skip hidden directories if configured to ignore them - if (dirName.startsWith(".") && DIRS_TO_IGNORE.includes(".*")) { - return false - } - - // Check against explicit ignore patterns - if (isDirectoryExplicitlyIgnored(dirName)) { - return false - } - - // Check against gitignore patterns using the ignore library - // Calculate relative path from the base directory const relativePath = path.relative(basePath, fullDirPath) const normalizedPath = relativePath.replace(/\\/g, "/") + return ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/") +} - // Check if the directory is ignored by .gitignore - if (ignoreInstance.ignores(normalizedPath) || ignoreInstance.ignores(normalizedPath + "/")) { +/** + * Check if a target directory should be included + */ +function shouldIncludeTargetDirectory(dirName: string): boolean { + // Only apply non-hidden-directory ignore rules to target directories + const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*") + return !matchesIgnorePattern(dirName, nonHiddenIgnorePatterns) +} + +/** + * Check if a directory inside an explicitly targeted hidden directory should be included + */ +function shouldIncludeInsideHiddenTarget(dirName: string, fullDirPath: string, context: ScanContext): boolean { + // Only apply the most critical ignore patterns when inside explicit hidden target + if (CRITICAL_IGNORE_PATTERNS.has(dirName)) { return false } - return true + // Check against gitignore patterns + return !isIgnoredByGitignore(fullDirPath, context.basePath, context.ignoreInstance) +} + +/** + * Check if a regular directory should be included + */ +function shouldIncludeRegularDirectory(dirName: string, fullDirPath: string, context: ScanContext): boolean { + // Check against explicit ignore patterns (excluding the ".*" pattern) + const nonHiddenIgnorePatterns = DIRS_TO_IGNORE.filter((pattern) => pattern !== ".*") + if (matchesIgnorePattern(dirName, nonHiddenIgnorePatterns)) { + return false + } + + // Check against gitignore patterns + return !isIgnoredByGitignore(fullDirPath, context.basePath, context.ignoreInstance) +} + +/** + * Determine if a directory should be included in results based on filters + */ +function shouldIncludeDirectory(dirName: string, fullDirPath: string, context: ScanContext): boolean { + // If this is the explicitly targeted directory, allow it even if it's hidden + // This preserves the ability to explicitly target hidden directories like .roo-memory + if (context.isTargetDir) { + return shouldIncludeTargetDirectory(dirName) + } + + // If we're inside an explicitly targeted hidden directory, allow subdirectories + // even if they would normally be filtered out by the ".*" pattern or other ignore rules + if (context.insideExplicitHiddenTarget) { + return shouldIncludeInsideHiddenTarget(dirName, fullDirPath, context) + } + + // Regular directory inclusion logic + return shouldIncludeRegularDirectory(dirName, fullDirPath, context) } /** @@ -390,6 +573,11 @@ function isDirectoryExplicitlyIgnored(dirName: string): boolean { return true } + // Skip the ".*" pattern - it's handled specially to allow top-level visibility + if (pattern === ".*") { + continue + } + // Path patterns that contain / if (pattern.includes("/")) { const pathParts = pattern.split("/") @@ -432,6 +620,9 @@ function formatAndCombineResults(files: string[], directories: string[], limit: */ async function execRipgrep(rgPath: string, args: string[], limit: number): Promise { return new Promise((resolve, reject) => { + // Extract the directory path from args (it's the last argument) + const searchDir = args[args.length - 1] + const rgProcess = childProcess.spawn(rgPath, args) let output = "" let results: string[] = [] @@ -497,6 +688,7 @@ async function execRipgrep(rgPath: string, args: string[], limit: number): Promi // Process each complete line for (const line of lines) { if (line.trim() && results.length < limit) { + // Keep the relative path as returned by ripgrep results.push(line) } else if (results.length >= limit) { break diff --git a/src/services/huggingface-models.ts b/src/services/huggingface-models.ts deleted file mode 100644 index 9c0bc406f9..0000000000 --- a/src/services/huggingface-models.ts +++ /dev/null @@ -1,171 +0,0 @@ -export interface HuggingFaceModel { - _id: string - id: string - inferenceProviderMapping: InferenceProviderMapping[] - trendingScore: number - config: ModelConfig - tags: string[] - pipeline_tag: "text-generation" | "image-text-to-text" - library_name?: string -} - -export interface InferenceProviderMapping { - provider: string - providerId: string - status: "live" | "staging" | "error" - task: "conversational" -} - -export interface ModelConfig { - architectures: string[] - model_type: string - tokenizer_config?: { - chat_template?: string | Array<{ name: string; template: string }> - model_max_length?: number - } -} - -interface HuggingFaceApiParams { - pipeline_tag?: "text-generation" | "image-text-to-text" - filter: string - inference_provider: string - limit: number - expand: string[] -} - -const DEFAULT_PARAMS: HuggingFaceApiParams = { - filter: "conversational", - inference_provider: "all", - limit: 100, - expand: [ - "inferenceProviderMapping", - "config", - "library_name", - "pipeline_tag", - "tags", - "mask_token", - "trendingScore", - ], -} - -const BASE_URL = "https://huggingface.co/api/models" -const CACHE_DURATION = 1000 * 60 * 60 // 1 hour - -interface CacheEntry { - data: HuggingFaceModel[] - timestamp: number - status: "success" | "partial" | "error" -} - -let cache: CacheEntry | null = null - -function buildApiUrl(params: HuggingFaceApiParams): string { - const url = new URL(BASE_URL) - - // Add simple params - Object.entries(params).forEach(([key, value]) => { - if (!Array.isArray(value)) { - url.searchParams.append(key, String(value)) - } - }) - - // Handle array params specially - params.expand.forEach((item) => { - url.searchParams.append("expand[]", item) - }) - - return url.toString() -} - -const headers: HeadersInit = { - "Upgrade-Insecure-Requests": "1", - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "none", - "Sec-Fetch-User": "?1", - Priority: "u=0, i", - Pragma: "no-cache", - "Cache-Control": "no-cache", -} - -const requestInit: RequestInit = { - credentials: "include", - headers, - method: "GET", - mode: "cors", -} - -export async function fetchHuggingFaceModels(): Promise { - const now = Date.now() - - // Check cache - if (cache && now - cache.timestamp < CACHE_DURATION) { - console.log("Using cached Hugging Face models") - return cache.data - } - - try { - console.log("Fetching Hugging Face models from API...") - - // Fetch both text-generation and image-text-to-text models in parallel - const [textGenResponse, imgTextResponse] = await Promise.allSettled([ - fetch(buildApiUrl({ ...DEFAULT_PARAMS, pipeline_tag: "text-generation" }), requestInit), - fetch(buildApiUrl({ ...DEFAULT_PARAMS, pipeline_tag: "image-text-to-text" }), requestInit), - ]) - - let textGenModels: HuggingFaceModel[] = [] - let imgTextModels: HuggingFaceModel[] = [] - let hasErrors = false - - // Process text-generation models - if (textGenResponse.status === "fulfilled" && textGenResponse.value.ok) { - textGenModels = await textGenResponse.value.json() - } else { - console.error("Failed to fetch text-generation models:", textGenResponse) - hasErrors = true - } - - // Process image-text-to-text models - if (imgTextResponse.status === "fulfilled" && imgTextResponse.value.ok) { - imgTextModels = await imgTextResponse.value.json() - } else { - console.error("Failed to fetch image-text-to-text models:", imgTextResponse) - hasErrors = true - } - - // Combine and filter models - const allModels = [...textGenModels, ...imgTextModels] - .filter((model) => model.inferenceProviderMapping.length > 0) - .sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase())) - - // Update cache - cache = { - data: allModels, - timestamp: now, - status: hasErrors ? "partial" : "success", - } - - console.log(`Fetched ${allModels.length} Hugging Face models (status: ${cache.status})`) - return allModels - } catch (error) { - console.error("Error fetching Hugging Face models:", error) - - // Return cached data if available - if (cache) { - console.log("Using stale cached data due to fetch error") - cache.status = "error" - return cache.data - } - - // No cache available, return empty array - return [] - } -} - -export function getCachedModels(): HuggingFaceModel[] | null { - return cache?.data || null -} - -export function clearCache(): void { - cache = null -} diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index 5c5b9f6d61..6cd174a577 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -4,12 +4,19 @@ import * as path from "path" import * as yaml from "yaml" import { RemoteConfigLoader } from "./RemoteConfigLoader" import { SimpleInstaller } from "./SimpleInstaller" -import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types" +import type { MarketplaceItem, MarketplaceItemType, McpMarketplaceItem, OrganizationSettings } from "@roo-code/types" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import { t } from "../../i18n" import { TelemetryService } from "@roo-code/telemetry" import type { CustomModesManager } from "../../core/config/CustomModesManager" +import { CloudService } from "@roo-code/cloud" + +export interface MarketplaceItemsResponse { + organizationMcps: MarketplaceItem[] + marketplaceItems: MarketplaceItem[] + errors?: string[] +} export class MarketplaceManager { private configLoader: RemoteConfigLoader @@ -23,17 +30,55 @@ export class MarketplaceManager { this.installer = new SimpleInstaller(context, customModesManager) } - async getMarketplaceItems(): Promise<{ items: MarketplaceItem[]; errors?: string[] }> { + async getMarketplaceItems(): Promise { try { - const items = await this.configLoader.loadAllItems() + const errors: string[] = [] - return { items } + let orgSettings: OrganizationSettings | undefined + try { + if (CloudService.hasInstance() && CloudService.instance.isAuthenticated()) { + orgSettings = CloudService.instance.getOrganizationSettings() + } + } catch (orgError) { + console.warn("Failed to load organization settings:", orgError) + const orgErrorMessage = orgError instanceof Error ? orgError.message : String(orgError) + errors.push(`Organization settings: ${orgErrorMessage}`) + } + + const allMarketplaceItems = await this.configLoader.loadAllItems(orgSettings?.hideMarketplaceMcps) + let organizationMcps: MarketplaceItem[] = [] + let marketplaceItems = allMarketplaceItems + + if (orgSettings) { + if (orgSettings.mcps && orgSettings.mcps.length > 0) { + organizationMcps = orgSettings.mcps.map( + (mcp: McpMarketplaceItem): MarketplaceItem => ({ + ...mcp, + type: "mcp" as const, + }), + ) + } + + if (orgSettings.hiddenMcps && orgSettings.hiddenMcps.length > 0) { + const hiddenMcpIds = new Set(orgSettings.hiddenMcps) + marketplaceItems = allMarketplaceItems.filter( + (item) => item.type !== "mcp" || !hiddenMcpIds.has(item.id), + ) + } + } + + return { + organizationMcps, + marketplaceItems, + errors: errors.length > 0 ? errors : undefined, + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) console.error("Failed to load marketplace items:", error) return { - items: [], + organizationMcps: [], + marketplaceItems: [], errors: [errorMessage], } } @@ -41,7 +86,7 @@ export class MarketplaceManager { async getCurrentItems(): Promise { const result = await this.getMarketplaceItems() - return result.items + return [...result.organizationMcps, ...result.marketplaceItems] } filterItems( diff --git a/src/services/marketplace/RemoteConfigLoader.ts b/src/services/marketplace/RemoteConfigLoader.ts index a37f619b4d..fe66b32be0 100644 --- a/src/services/marketplace/RemoteConfigLoader.ts +++ b/src/services/marketplace/RemoteConfigLoader.ts @@ -23,10 +23,13 @@ export class RemoteConfigLoader { this.apiBaseUrl = getRooCodeApiUrl() } - async loadAllItems(): Promise { + async loadAllItems(hideMarketplaceMcps = false): Promise { const items: MarketplaceItem[] = [] - const [modes, mcps] = await Promise.all([this.fetchModes(), this.fetchMcps()]) + const modesPromise = this.fetchModes() + const mcpsPromise = hideMarketplaceMcps ? Promise.resolve([]) : this.fetchMcps() + + const [modes, mcps] = await Promise.all([modesPromise, mcpsPromise]) items.push(...modes, ...mcps) return items diff --git a/src/services/marketplace/__tests__/MarketplaceManager.spec.ts b/src/services/marketplace/__tests__/MarketplaceManager.spec.ts index 8962f43c5f..59809b2917 100644 --- a/src/services/marketplace/__tests__/MarketplaceManager.spec.ts +++ b/src/services/marketplace/__tests__/MarketplaceManager.spec.ts @@ -4,14 +4,21 @@ import type { MarketplaceItem } from "@roo-code/types" import { MarketplaceManager } from "../MarketplaceManager" -// Mock axios -vi.mock("axios") - -// Mock the cloud config +// Mock CloudService vi.mock("@roo-code/cloud", () => ({ getRooCodeApiUrl: () => "https://test.api.com", + CloudService: { + hasInstance: vi.fn(), + instance: { + isAuthenticated: vi.fn(), + getOrganizationSettings: vi.fn(), + }, + }, })) +// Mock axios +vi.mock("axios") + // Mock TelemetryService vi.mock("../../../../packages/telemetry/src/TelemetryService", () => ({ TelemetryService: { @@ -165,8 +172,9 @@ describe("MarketplaceManager", () => { const result = await manager.getMarketplaceItems() - expect(result.items).toHaveLength(1) - expect(result.items[0].name).toBe("Test Mode") + expect(result.marketplaceItems).toHaveLength(1) + expect(result.marketplaceItems[0].name).toBe("Test Mode") + expect(result.organizationMcps).toHaveLength(0) }) it("should handle API errors gracefully", async () => { @@ -175,9 +183,124 @@ describe("MarketplaceManager", () => { const result = await manager.getMarketplaceItems() - expect(result.items).toHaveLength(0) + expect(result.marketplaceItems).toHaveLength(0) + expect(result.organizationMcps).toHaveLength(0) expect(result.errors).toEqual(["API request failed"]) }) + + it("should return organization MCPs when available", async () => { + const { CloudService } = await import("@roo-code/cloud") + + // Mock CloudService to return organization settings + vi.mocked(CloudService.hasInstance).mockReturnValue(true) + vi.mocked(CloudService.instance.isAuthenticated).mockReturnValue(true) + vi.mocked(CloudService.instance.getOrganizationSettings).mockReturnValue({ + version: 1, + mcps: [ + { + id: "org-mcp-1", + name: "Organization MCP", + description: "An organization MCP", + url: "https://example.com/org-mcp", + content: '{"command": "node", "args": ["org-server.js"]}', + }, + ], + hiddenMcps: [], + allowList: { allowAll: true, providers: {} }, + defaultSettings: {}, + }) + + // Mock the config loader to return test data + const mockItems: MarketplaceItem[] = [ + { + id: "test-mcp", + name: "Test MCP", + description: "A test MCP", + type: "mcp", + url: "https://example.com/test-mcp", + content: '{"command": "node", "args": ["server.js"]}', + }, + ] + + vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems) + + const result = await manager.getMarketplaceItems() + + expect(result.organizationMcps).toHaveLength(1) + expect(result.organizationMcps[0].name).toBe("Organization MCP") + expect(result.marketplaceItems).toHaveLength(1) + expect(result.marketplaceItems[0].name).toBe("Test MCP") + }) + + it("should filter out hidden MCPs from marketplace results", async () => { + const { CloudService } = await import("@roo-code/cloud") + + // Mock CloudService to return organization settings with hidden MCPs + vi.mocked(CloudService.hasInstance).mockReturnValue(true) + vi.mocked(CloudService.instance.isAuthenticated).mockReturnValue(true) + vi.mocked(CloudService.instance.getOrganizationSettings).mockReturnValue({ + version: 1, + mcps: [], + hiddenMcps: ["hidden-mcp"], + allowList: { allowAll: true, providers: {} }, + defaultSettings: {}, + }) + + // Mock the config loader to return test data including a hidden MCP + const mockItems: MarketplaceItem[] = [ + { + id: "visible-mcp", + name: "Visible MCP", + description: "A visible MCP", + type: "mcp", + url: "https://example.com/visible-mcp", + content: '{"command": "node", "args": ["visible.js"]}', + }, + { + id: "hidden-mcp", + name: "Hidden MCP", + description: "A hidden MCP", + type: "mcp", + url: "https://example.com/hidden-mcp", + content: '{"command": "node", "args": ["hidden.js"]}', + }, + ] + + vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems) + + const result = await manager.getMarketplaceItems() + + expect(result.marketplaceItems).toHaveLength(1) + expect(result.marketplaceItems[0].name).toBe("Visible MCP") + expect(result.organizationMcps).toHaveLength(0) + }) + + it("should handle CloudService not being available", async () => { + const { CloudService } = await import("@roo-code/cloud") + + // Mock CloudService to not be available + vi.mocked(CloudService.hasInstance).mockReturnValue(false) + + // Mock the config loader to return test data + const mockItems: MarketplaceItem[] = [ + { + id: "test-mcp", + name: "Test MCP", + description: "A test MCP", + type: "mcp", + url: "https://example.com/test-mcp", + content: '{"command": "node", "args": ["server.js"]}', + }, + ] + + vi.spyOn(manager["configLoader"], "loadAllItems").mockResolvedValue(mockItems) + + const result = await manager.getMarketplaceItems() + + expect(result.organizationMcps).toHaveLength(0) + expect(result.marketplaceItems).toHaveLength(1) + expect(result.marketplaceItems[0].name).toBe("Test MCP") + }) }) describe("installMarketplaceItem", () => { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 000762e317..67f8782e19 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -19,6 +19,15 @@ import { Mode } from "./modes" import { RouterModels } from "./api" import type { MarketplaceItem } from "@roo-code/types" +// Command interface for frontend/backend communication +export interface Command { + name: string + source: "global" | "project" + filePath?: string + description?: string + argumentHint?: string +} + // Type for marketplace installed metadata export interface MarketplaceInstalledMetadata { project: Record @@ -109,6 +118,8 @@ export interface ExtensionMessage { | "codeIndexSecretStatus" | "showDeleteMessageDialog" | "showEditMessageDialog" + | "commands" + | "insertTextIntoTextarea" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -138,26 +149,21 @@ export interface ExtensionMessage { lmStudioModels?: string[] vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[] huggingFaceModels?: Array<{ - _id: string id: string - inferenceProviderMapping: Array<{ + object: string + created: number + owned_by: string + providers: Array<{ provider: string - providerId: string status: "live" | "staging" | "error" - task: "conversational" - }> - trendingScore: number - config: { - architectures: string[] - model_type: string - tokenizer_config?: { - chat_template?: string | Array<{ name: string; template: string }> - model_max_length?: number + supports_tools?: boolean + supports_structured_output?: boolean + context_length?: number + pricing?: { + input: number + output: number } - } - tags: string[] - pipeline_tag: "text-generation" | "image-text-to-text" - library_name?: string + }> }> mcpServers?: McpServer[] commits?: GitCommit[] @@ -179,12 +185,15 @@ export interface ExtensionMessage { organizationAllowList?: OrganizationAllowList tab?: string marketplaceItems?: MarketplaceItem[] + organizationMcps?: MarketplaceItem[] marketplaceInstalledMetadata?: MarketplaceInstalledMetadata + errors?: string[] visibility?: ShareVisibility rulesFolderPath?: string settings?: any messageTs?: number context?: string + commands?: Command[] } export type ExtensionState = Pick< @@ -278,6 +287,8 @@ export type ExtensionState = Pick< maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500) showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings maxReadFileLine: number // Maximum number of lines to read from a file before truncating + maxImageFileSize: number // Maximum size of image files to process in MB + maxTotalImageSize: number // Maximum total size for all images in a single read operation in MB experiments: Experiments // Map of experiment IDs to their enabled state diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 795e276522..a91d1af7ba 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -162,6 +162,8 @@ export interface WebviewMessage { | "remoteBrowserEnabled" | "language" | "maxReadFileLine" + | "maxImageFileSize" + | "maxTotalImageSize" | "maxConcurrentFileReads" | "includeDiagnosticMessages" | "maxDiagnosticMessages" @@ -201,6 +203,11 @@ export interface WebviewMessage { | "checkRulesDirectoryResult" | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" + | "requestCommands" + | "openCommandFile" + | "deleteCommand" + | "createCommand" + | "insertTextIntoTextarea" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" diff --git a/src/shared/__tests__/experiments-preventFocusDisruption.spec.ts b/src/shared/__tests__/experiments-preventFocusDisruption.spec.ts new file mode 100644 index 0000000000..7e5389c286 --- /dev/null +++ b/src/shared/__tests__/experiments-preventFocusDisruption.spec.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest" +import { EXPERIMENT_IDS, experimentConfigsMap, experimentDefault, experiments } from "../experiments" + +describe("PREVENT_FOCUS_DISRUPTION experiment", () => { + it("should include PREVENT_FOCUS_DISRUPTION in EXPERIMENT_IDS", () => { + expect(EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION).toBe("preventFocusDisruption") + }) + + it("should have PREVENT_FOCUS_DISRUPTION in experimentConfigsMap", () => { + expect(experimentConfigsMap.PREVENT_FOCUS_DISRUPTION).toBeDefined() + expect(experimentConfigsMap.PREVENT_FOCUS_DISRUPTION.enabled).toBe(false) + }) + + it("should have PREVENT_FOCUS_DISRUPTION in experimentDefault", () => { + expect(experimentDefault.preventFocusDisruption).toBe(false) + }) + + it("should correctly check if PREVENT_FOCUS_DISRUPTION is enabled", () => { + // Test when experiment is disabled (default) + const disabledConfig = { preventFocusDisruption: false } + expect(experiments.isEnabled(disabledConfig, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) + + // Test when experiment is enabled + const enabledConfig = { preventFocusDisruption: true } + expect(experiments.isEnabled(enabledConfig, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(true) + + // Test when experiment is not in config (should use default) + const emptyConfig = {} + expect(experiments.isEnabled(emptyConfig, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) + }) +}) diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 4a8f06d62a..607c1e0b04 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -28,6 +28,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: false, multiFileApplyDiff: false, + preventFocusDisruption: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) @@ -36,6 +37,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: true, multiFileApplyDiff: false, + preventFocusDisruption: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) @@ -44,6 +46,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: false, multiFileApplyDiff: false, + preventFocusDisruption: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts index 2edb99de6a..d7e59a77dd 100644 --- a/src/shared/context-mentions.ts +++ b/src/shared/context-mentions.ts @@ -57,6 +57,9 @@ export const mentionRegex = /(? type _AssertExperimentIds = AssertEqual>> @@ -16,6 +17,7 @@ interface ExperimentConfig { export const experimentConfigsMap: Record = { MULTI_FILE_APPLY_DIFF: { enabled: false }, POWER_STEERING: { enabled: false }, + PREVENT_FOCUS_DISRUPTION: { enabled: false }, } export const experimentDefault = Object.fromEntries( diff --git a/webview-ui/src/__tests__/SearchableSelect.spec.tsx b/webview-ui/src/__tests__/SearchableSelect.spec.tsx index bcc0a2c8b7..a05acbf914 100644 --- a/webview-ui/src/__tests__/SearchableSelect.spec.tsx +++ b/webview-ui/src/__tests__/SearchableSelect.spec.tsx @@ -253,4 +253,67 @@ describe("SearchableSelect", () => { expect(within(dropdown).queryByText("Option 3")).not.toBeInTheDocument() }) }) + + it("closes the dropdown when ESC key is pressed", async () => { + const user = userEvent.setup() + render() + + // Open the dropdown + const trigger = screen.getByRole("combobox") + await user.click(trigger) + + // Verify dropdown is open + expect(trigger).toHaveAttribute("aria-expanded", "true") + expect(screen.getByRole("listbox")).toBeInTheDocument() + + // Press ESC key + fireEvent.keyDown(window, { key: "Escape" }) + + // Verify dropdown is closed + await waitFor(() => { + expect(trigger).toHaveAttribute("aria-expanded", "false") + expect(screen.queryByRole("listbox")).not.toBeInTheDocument() + }) + }) + + it("does not close the dropdown when ESC is pressed while dropdown is closed", async () => { + render() + + const trigger = screen.getByRole("combobox") + + // Ensure dropdown is closed + expect(trigger).toHaveAttribute("aria-expanded", "false") + + // Press ESC key + fireEvent.keyDown(window, { key: "Escape" }) + + // Verify dropdown remains closed + expect(trigger).toHaveAttribute("aria-expanded", "false") + expect(screen.queryByRole("listbox")).not.toBeInTheDocument() + }) + + it("prevents default and stops propagation when ESC is pressed", async () => { + const user = userEvent.setup() + render() + + // Open the dropdown + const trigger = screen.getByRole("combobox") + await user.click(trigger) + + // Create a mock event to track preventDefault and stopPropagation + const escapeEvent = new KeyboardEvent("keydown", { + key: "Escape", + bubbles: true, + cancelable: true, + }) + const preventDefaultSpy = vi.spyOn(escapeEvent, "preventDefault") + const stopPropagationSpy = vi.spyOn(escapeEvent, "stopPropagation") + + // Dispatch the event + window.dispatchEvent(escapeEvent) + + // Verify preventDefault and stopPropagation were called + expect(preventDefaultSpy).toHaveBeenCalled() + expect(stopPropagationSpy).toHaveBeenCalled() + }) }) diff --git a/webview-ui/src/__tests__/command-autocomplete.spec.ts b/webview-ui/src/__tests__/command-autocomplete.spec.ts new file mode 100644 index 0000000000..be87f3586b --- /dev/null +++ b/webview-ui/src/__tests__/command-autocomplete.spec.ts @@ -0,0 +1,264 @@ +import { describe, it, expect } from "vitest" +import { getContextMenuOptions, ContextMenuOptionType } from "../utils/context-mentions" +import type { Command } from "@roo/ExtensionMessage" + +describe("Command Autocomplete", () => { + const mockCommands: Command[] = [ + { name: "setup", source: "project" }, + { name: "build", source: "project" }, + { name: "deploy", source: "global" }, + { name: "test-suite", source: "project" }, + { name: "cleanup_old", source: "global" }, + { name: "release", source: "project", argumentHint: "patch | minor | major" }, + ] + + const mockQueryItems = [ + { type: ContextMenuOptionType.File, value: "/src/app.ts" }, + { type: ContextMenuOptionType.Problems, value: "problems" }, + ] + + describe("slash command command suggestions", () => { + it('should return all commands when query is just "/"', () => { + const options = getContextMenuOptions("/", null, mockQueryItems, [], [], mockCommands) + + // Should have 7 items: 1 section header + 6 commands + expect(options).toHaveLength(7) + + // Filter out section headers to check commands + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions).toHaveLength(6) + + const commandNames = commandOptions.map((option) => option.value) + expect(commandNames).toContain("setup") + expect(commandNames).toContain("build") + expect(commandNames).toContain("deploy") + expect(commandNames).toContain("test-suite") + expect(commandNames).toContain("cleanup_old") + expect(commandNames).toContain("release") + }) + + it("should filter commands based on fuzzy search", () => { + const options = getContextMenuOptions("/set", null, mockQueryItems, [], [], mockCommands) + + // Should match 'setup' (fuzzy search behavior may vary) + expect(options.length).toBeGreaterThan(0) + const commandNames = options.map((option) => option.value) + expect(commandNames).toContain("setup") + // Note: fuzzy search may not match 'test-suite' for 'set' query + }) + + it("should return commands with correct format", () => { + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands) + + const setupOption = options.find((option) => option.value === "setup") + expect(setupOption).toBeDefined() + expect(setupOption!.type).toBe(ContextMenuOptionType.Command) + expect(setupOption!.slashCommand).toBe("/setup") + expect(setupOption!.value).toBe("setup") + }) + + it("should handle empty command list", () => { + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], []) + + // Should return NoResults when no commands match + expect(options).toHaveLength(1) + expect(options[0].type).toBe(ContextMenuOptionType.NoResults) + }) + + it("should handle no matching commands", () => { + const options = getContextMenuOptions("/nonexistent", null, mockQueryItems, [], [], mockCommands) + + // Should return NoResults when no commands match + expect(options).toHaveLength(1) + expect(options[0].type).toBe(ContextMenuOptionType.NoResults) + }) + + it("should not return command suggestions for non-slash queries", () => { + const options = getContextMenuOptions("setup", null, mockQueryItems, [], [], mockCommands) + + // Should not contain command options for non-slash queries + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions).toHaveLength(0) + }) + + it("should handle commands with special characters in names", () => { + const specialCommands: Command[] = [ + { name: "setup-dev", source: "project" }, + { name: "test_suite", source: "project" }, + { name: "deploy.prod", source: "global" }, + ] + + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], specialCommands) + + const setupDevOption = options.find((option) => option.value === "setup-dev") + expect(setupDevOption).toBeDefined() + expect(setupDevOption!.slashCommand).toBe("/setup-dev") + }) + + it("should handle case-insensitive fuzzy matching", () => { + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands) + + const commandNames = options.map((option) => option.value) + expect(commandNames).toContain("setup") + }) + + it("should prioritize exact matches in fuzzy search", () => { + const commandsWithSimilarNames: Command[] = [ + { name: "test", source: "project" }, + { name: "test-suite", source: "project" }, + { name: "integration-test", source: "project" }, + ] + + const options = getContextMenuOptions("/test", null, mockQueryItems, [], [], commandsWithSimilarNames) + + // Filter out section headers and check the first command + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions[0].value).toBe("test") + }) + + it("should handle partial matches correctly", () => { + const options = getContextMenuOptions("/te", null, mockQueryItems, [], [], mockCommands) + + // Should match 'test-suite' + const commandNames = options.map((option) => option.value) + expect(commandNames).toContain("test-suite") + }) + }) + + describe("command integration with modes", () => { + const mockModes = [ + { + name: "Code", + slug: "code", + description: "Write and edit code", + roleDefinition: "You are a code assistant", + groups: ["read", "edit"], + }, + { + name: "Debug", + slug: "debug", + description: "Debug applications", + roleDefinition: "You are a debug assistant", + groups: ["read", "edit"], + }, + ] as any[] + + it("should return both modes and commands for slash commands", () => { + const options = getContextMenuOptions("/", null, mockQueryItems, [], mockModes, mockCommands) + + const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode) + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + + expect(modeOptions.length).toBe(2) + expect(commandOptions.length).toBe(6) + }) + + it("should filter both modes and commands based on query", () => { + const options = getContextMenuOptions("/co", null, mockQueryItems, [], mockModes, mockCommands) + + // Should match 'code' mode and possibly some commands (fuzzy search may match) + const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode) + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + + expect(modeOptions.length).toBe(1) + expect(modeOptions[0].value).toBe("code") + // Fuzzy search might match some commands, so we just check it's a reasonable number + expect(commandOptions.length).toBeGreaterThanOrEqual(0) + }) + }) + + describe("command source indication", () => { + it("should not expose source information in autocomplete", () => { + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands) + + const setupOption = options.find((option) => option.value === "setup") + expect(setupOption).toBeDefined() + + // Source should not be exposed in the UI + if (setupOption!.description) { + expect(setupOption!.description).not.toContain("project") + expect(setupOption!.description).not.toContain("global") + expect(setupOption!.description).toBe("Trigger the setup command") + } + }) + }) + + describe("argument hint functionality", () => { + it("should include argumentHint in command options when present", () => { + const options = getContextMenuOptions("/release", null, mockQueryItems, [], [], mockCommands) + + const releaseOption = options.find((option) => option.value === "release") + expect(releaseOption).toBeDefined() + expect(releaseOption!.argumentHint).toBe("patch | minor | major") + }) + + it("should handle commands without argumentHint", () => { + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands) + + const setupOption = options.find((option) => option.value === "setup") + expect(setupOption).toBeDefined() + expect(setupOption!.argumentHint).toBeUndefined() + }) + + it("should preserve argumentHint through fuzzy search", () => { + const options = getContextMenuOptions("/rel", null, mockQueryItems, [], [], mockCommands) + + const releaseOption = options.find((option) => option.value === "release") + expect(releaseOption).toBeDefined() + expect(releaseOption!.argumentHint).toBe("patch | minor | major") + }) + + it("should handle commands with empty argumentHint", () => { + const commandsWithEmptyHint: Command[] = [{ name: "test-command", source: "project", argumentHint: "" }] + + const options = getContextMenuOptions("/test", null, mockQueryItems, [], [], commandsWithEmptyHint) + + const testOption = options.find((option) => option.value === "test-command") + expect(testOption).toBeDefined() + expect(testOption!.argumentHint).toBe("") + }) + }) + + describe("edge cases", () => { + it("should handle undefined commands gracefully", () => { + const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], undefined) + + expect(options).toHaveLength(1) + expect(options[0].type).toBe(ContextMenuOptionType.NoResults) + }) + + it("should handle empty query with commands", () => { + const options = getContextMenuOptions("", null, mockQueryItems, [], [], mockCommands) + + // Should not return command options for empty query + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions).toHaveLength(0) + }) + + it("should handle very long command names", () => { + const longNameCommands: Command[] = [ + { name: "very-long-command-name-that-exceeds-normal-length", source: "project" }, + ] + + const options = getContextMenuOptions("/very", null, mockQueryItems, [], [], longNameCommands) + + // Should have 2 items: 1 section header + 1 command + expect(options.length).toBe(2) + const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command) + expect(commandOptions[0].value).toBe("very-long-command-name-that-exceeds-normal-length") + }) + + it("should handle commands with numeric names", () => { + const numericCommands: Command[] = [ + { name: "command1", source: "project" }, + { name: "v2-setup", source: "project" }, + { name: "123test", source: "project" }, + ] + + const options = getContextMenuOptions("/v", null, mockQueryItems, [], [], numericCommands) + + const commandNames = options.map((option) => option.value) + expect(commandNames).toContain("v2-setup") + }) + }) +}) diff --git a/webview-ui/src/components/ErrorBoundary.tsx b/webview-ui/src/components/ErrorBoundary.tsx index 283171ea6f..c1281963ba 100644 --- a/webview-ui/src/components/ErrorBoundary.tsx +++ b/webview-ui/src/components/ErrorBoundary.tsx @@ -17,8 +17,6 @@ class ErrorBoundary extends Component { constructor(props: ErrorProps) { super(props) this.state = {} - - this.state = {} } static getDerivedStateFromError(error: unknown) { diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index fa0781c865..65b78c3cd6 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -82,6 +82,16 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => { }} /> +
  • + •{" "} + , + code: , + }} + /> +
  • void + triggerClassName?: string + listApiConfigMeta: Array<{ id: string; name: string }> + pinnedApiConfigs?: Record + togglePinnedApiConfig: (id: string) => void +} + +export const ApiConfigSelector = ({ + value, + displayName, + disabled = false, + title = "", + onChange, + triggerClassName = "", + listApiConfigMeta, + pinnedApiConfigs, + togglePinnedApiConfig, +}: ApiConfigSelectorProps) => { + const { t } = useAppTranslation() + const [open, setOpen] = useState(false) + const [searchValue, setSearchValue] = useState("") + const portalContainer = useRooPortal("roo-portal") + + // Create searchable items for fuzzy search + const searchableItems = useMemo(() => { + return listApiConfigMeta.map((config) => ({ + original: config, + searchStr: config.name, + })) + }, [listApiConfigMeta]) + + // Create Fzf instance + const fzfInstance = useMemo(() => { + return new Fzf(searchableItems, { + selector: (item) => item.searchStr, + }) + }, [searchableItems]) + + // Filter configs based on search + const filteredConfigs = useMemo(() => { + if (!searchValue) return listApiConfigMeta + + const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) + return matchingItems + }, [listApiConfigMeta, searchValue, fzfInstance]) + + // Separate pinned and unpinned configs + const { pinnedConfigs, unpinnedConfigs } = useMemo(() => { + const pinned = filteredConfigs.filter((config) => pinnedApiConfigs?.[config.id]) + const unpinned = filteredConfigs.filter((config) => !pinnedApiConfigs?.[config.id]) + return { pinnedConfigs: pinned, unpinnedConfigs: unpinned } + }, [filteredConfigs, pinnedApiConfigs]) + + const handleSelect = useCallback( + (configId: string) => { + onChange(configId) + setOpen(false) + setSearchValue("") + }, + [onChange], + ) + + const handleEditClick = useCallback(() => { + vscode.postMessage({ + type: "switchTab", + tab: "settings", + }) + setOpen(false) + }, []) + + const renderConfigItem = useCallback( + (config: { id: string; name: string }, isPinned: boolean) => { + const isCurrentConfig = config.id === value + + return ( +
    handleSelect(config.id)} + className={cn( + "px-3 py-1.5 text-sm cursor-pointer flex items-center group", + "hover:bg-vscode-list-hoverBackground", + isCurrentConfig && + "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground", + )}> + {config.name} +
    + {isCurrentConfig && ( +
    + +
    + )} + + + +
    +
    + ) + }, + [value, handleSelect, t, togglePinnedApiConfig], + ) + + const triggerContent = ( + + + {displayName} + + ) + + return ( + + {title ? {triggerContent} : triggerContent} + +
    + {/* Search input or info blurb */} + {listApiConfigMeta.length > 6 ? ( +
    + setSearchValue(e.target.value)} + placeholder={t("common:ui.search_placeholder")} + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + autoFocus + /> + {searchValue.length > 0 && ( +
    + setSearchValue("")} + /> +
    + )} +
    + ) : ( +
    +

    + {t("prompts:apiConfiguration.select")} +

    +
    + )} + + {/* Config list */} +
    + {filteredConfigs.length === 0 && searchValue ? ( +
    + {t("common:ui.no_results")} +
    + ) : ( +
    + {/* Pinned configs */} + {pinnedConfigs.map((config) => renderConfigItem(config, true))} + + {/* Separator between pinned and unpinned */} + {pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && ( +
    + )} + + {/* Unpinned configs */} + {unpinnedConfigs.map((config) => renderConfigItem(config, false))} +
    + )} +
    + + {/* Bottom bar with buttons on left and title on right */} +
    +
    + +
    + + {/* Info icon and title on the right with matching spacing */} +
    + {listApiConfigMeta.length > 6 && ( + + + + )} +

    + {t("prompts:apiConfiguration.title")} +

    +
    +
    +
    + + + ) +} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6c541353eb..a52902f1e5 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -2,7 +2,7 @@ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, us import { useEvent } from "react-use" import DynamicTextArea from "react-textarea-autosize" -import { mentionRegex, mentionRegexGlobal, unescapeSpaces } from "@roo/context-mentions" +import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions" import { WebviewMessage } from "@roo/WebviewMessage" import { Mode, getAllModes } from "@roo/modes" import { ExtensionMessage } from "@roo/ExtensionMessage" @@ -19,14 +19,16 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" -import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui" +import { StandardTooltip } from "@/components/ui" import Thumbnails from "../common/Thumbnails" import ModeSelector from "./ModeSelector" +import { ApiConfigSelector } from "./ApiConfigSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide-react" +import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react" import { IndexingStatusBadge } from "./IndexingStatusBadge" +import { SlashCommandsPopover } from "./SlashCommandsPopover" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" import { EditModeControls } from "./EditModeControls" @@ -86,6 +88,7 @@ const ChatTextArea = forwardRef( togglePinnedApiConfig, taskHistory, clineMessages, + commands, } = useExtensionState() // Find the ID and display text for the currently selected API configuration @@ -143,6 +146,36 @@ const ChatTextArea = forwardRef( } setIsEnhancingPrompt(false) + } else if (message.type === "insertTextIntoTextarea") { + if (message.text && textAreaRef.current) { + // Insert the command text at the current cursor position + const textarea = textAreaRef.current + const currentValue = inputValue + const cursorPos = textarea.selectionStart || 0 + + // Check if we need to add a space before the command + const textBefore = currentValue.slice(0, cursorPos) + const needsSpaceBefore = textBefore.length > 0 && !textBefore.endsWith(" ") + const prefix = needsSpaceBefore ? " " : "" + + // Insert the text at cursor position + const newValue = + currentValue.slice(0, cursorPos) + + prefix + + message.text + + " " + + currentValue.slice(cursorPos) + setInputValue(newValue) + + // Set cursor position after the inserted text + const newCursorPos = cursorPos + prefix.length + message.text.length + 1 + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.focus() + textAreaRef.current.setSelectionRange(newCursorPos, newCursorPos) + } + }, 0) + } } else if (message.type === "commitSearchResults") { const commits = message.commits.map((commit: any) => ({ type: ContextMenuOptionType.Git, @@ -163,7 +196,7 @@ const ChatTextArea = forwardRef( window.addEventListener("message", messageHandler) return () => window.removeEventListener("message", messageHandler) - }, [setInputValue, searchRequestId]) + }, [setInputValue, searchRequestId, inputValue]) const [isDraggingOver, setIsDraggingOver] = useState(false) const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined) @@ -202,10 +235,6 @@ const ChatTextArea = forwardRef( }, [selectedType, searchQuery]) const handleEnhancePrompt = useCallback(() => { - if (sendingDisabled) { - return - } - const trimmedInput = inputValue.trim() if (trimmedInput) { @@ -214,7 +243,7 @@ const ChatTextArea = forwardRef( } else { setInputValue(t("chat:enhancePromptDescription")) } - }, [inputValue, sendingDisabled, setInputValue, t]) + }, [inputValue, setInputValue, t]) const allModes = useMemo(() => getAllModes(customModes), [customModes]) @@ -273,6 +302,27 @@ const ChatTextArea = forwardRef( return } + if (type === ContextMenuOptionType.Command && value) { + // Handle command selection. + setSelectedMenuIndex(-1) + setInputValue("") + setShowContextMenu(false) + + // Insert the command mention into the textarea + const commandMention = `/${value}` + setInputValue(commandMention + " ") + setCursorPosition(commandMention.length + 1) + setIntendedCursorPosition(commandMention.length + 1) + + // Focus the textarea + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.focus() + } + }, 0) + return + } + if ( type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder || @@ -302,12 +352,18 @@ const ChatTextArea = forwardRef( insertValue = "terminal" } else if (type === ContextMenuOptionType.Git) { insertValue = value || "" + } else if (type === ContextMenuOptionType.Command) { + insertValue = value ? `/${value}` : "" } + // Determine if this is a slash command selection + const isSlashCommand = type === ContextMenuOptionType.Mode || type === ContextMenuOptionType.Command + const { newValue, mentionIndex } = insertMention( textAreaRef.current.value, cursorPosition, insertValue, + isSlashCommand, ) setInputValue(newValue) @@ -343,11 +399,11 @@ const ChatTextArea = forwardRef( const direction = event.key === "ArrowUp" ? -1 : 1 const options = getContextMenuOptions( searchQuery, - inputValue, selectedType, queryItems, fileSearchResults, allModes, + commands, ) const optionsLength = options.length @@ -357,7 +413,8 @@ const ChatTextArea = forwardRef( const selectableOptions = options.filter( (option) => option.type !== ContextMenuOptionType.URL && - option.type !== ContextMenuOptionType.NoResults, + option.type !== ContextMenuOptionType.NoResults && + option.type !== ContextMenuOptionType.SectionHeader, ) if (selectableOptions.length === 0) return -1 // No selectable options @@ -380,16 +437,17 @@ const ChatTextArea = forwardRef( event.preventDefault() const selectedOption = getContextMenuOptions( searchQuery, - inputValue, selectedType, queryItems, fileSearchResults, allModes, + commands, )[selectedMenuIndex] if ( selectedOption && selectedOption.type !== ContextMenuOptionType.URL && - selectedOption.type !== ContextMenuOptionType.NoResults + selectedOption.type !== ContextMenuOptionType.NoResults && + selectedOption.type !== ContextMenuOptionType.SectionHeader ) { handleMentionSelect(selectedOption.type, selectedOption.value) } @@ -407,11 +465,9 @@ const ChatTextArea = forwardRef( if (event.key === "Enter" && !event.shiftKey && !isComposing) { event.preventDefault() - if (!sendingDisabled) { - // Reset history navigation state when sending - resetHistoryNavigation() - onSend() - } + // Always call onSend - let ChatView handle queueing when disabled + resetHistoryNavigation() + onSend() } if (event.key === "Backspace" && !isComposing) { @@ -459,7 +515,6 @@ const ChatTextArea = forwardRef( } }, [ - sendingDisabled, onSend, showContextMenu, searchQuery, @@ -475,6 +530,7 @@ const ChatTextArea = forwardRef( fileSearchResults, handleHistoryNavigation, resetHistoryNavigation, + commands, ], ) @@ -503,11 +559,14 @@ const ChatTextArea = forwardRef( setShowContextMenu(showMenu) if (showMenu) { - if (newValue.startsWith("/")) { - // Handle slash command. + if (newValue.startsWith("/") && !newValue.includes(" ")) { + // Handle slash command - request fresh commands const query = newValue setSearchQuery(query) - setSelectedMenuIndex(0) + // Set to first selectable item (skip section headers) + setSelectedMenuIndex(1) // Section header is at 0, first command is at 1 + // Request commands fresh each time slash menu is shown + vscode.postMessage({ type: "requestCommands" }) } else { // Existing @ mention handling. const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1) @@ -655,14 +714,41 @@ const ChatTextArea = forwardRef( const text = textAreaRef.current.value - highlightLayerRef.current.innerHTML = text + // Helper function to check if a command is valid + const isValidCommand = (commandName: string): boolean => { + return commands?.some((cmd) => cmd.name === commandName) || false + } + + // Process the text to highlight mentions and valid commands + let processedText = text .replace(/\n$/, "\n\n") .replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c] || c) .replace(mentionRegexGlobal, '$&') + // Custom replacement for commands - only highlight valid ones + processedText = processedText.replace(commandRegexGlobal, (match, commandName) => { + // Only highlight if the command exists in the valid commands list + if (isValidCommand(commandName)) { + // Check if the match starts with a space + const startsWithSpace = match.startsWith(" ") + const commandPart = `/${commandName}` + + if (startsWithSpace) { + // Keep the space but only highlight the command part + return ` ${commandPart}` + } else { + // Highlight the entire command (starts at beginning of line) + return `${commandPart}` + } + } + return match // Return unhighlighted if command is not valid + }) + + highlightLayerRef.current.innerHTML = processedText + highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft - }, []) + }, [commands]) useLayoutEffect(() => { updateHighlights() @@ -824,122 +910,11 @@ const ChatTextArea = forwardRef( /> ) - // Helper function to get API config dropdown options - const getApiConfigOptions = useMemo(() => { - const pinnedConfigs = (listApiConfigMeta || []) - .filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: true, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - const unpinnedConfigs = (listApiConfigMeta || []) - .filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id]) - .map((config) => ({ - value: config.id, - label: config.name, - name: config.name, - type: DropdownOptionType.ITEM, - pinned: false, - })) - .sort((a, b) => a.label.localeCompare(b.label)) - - const hasPinnedAndUnpinned = pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 - - return [ - ...pinnedConfigs, - ...(hasPinnedAndUnpinned - ? [ - { - value: "sep-pinned", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - ] - : []), - ...unpinnedConfigs, - { - value: "sep-2", - label: t("chat:separator"), - type: DropdownOptionType.SEPARATOR, - }, - { - value: "settingsButtonClicked", - label: t("chat:edit"), - type: DropdownOptionType.ACTION, - }, - ] - }, [listApiConfigMeta, pinnedApiConfigs, t]) - // Helper function to handle API config change const handleApiConfigChange = useCallback((value: string) => { - if (value === "settingsButtonClicked") { - vscode.postMessage({ - type: "loadApiConfiguration", - text: value, - values: { section: "providers" }, - }) - } else { - vscode.postMessage({ type: "loadApiConfigurationById", text: value }) - } + vscode.postMessage({ type: "loadApiConfigurationById", text: value }) }, []) - // Helper function to render API config item - const renderApiConfigItem = useCallback( - ({ type, value, label, pinned }: any) => { - if (type !== DropdownOptionType.ITEM) { - return label - } - - const config = listApiConfigMeta?.find((c) => c.id === value) - const isCurrentConfig = config?.name === currentApiConfigName - - return ( -
    -
    - {label} -
    -
    -
    - -
    - - - -
    -
    - ) - }, - [listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig], - ) - // Helper function to render non-edit mode controls const renderNonEditModeControls = () => (
    @@ -947,17 +922,16 @@ const ChatTextArea = forwardRef(
    {renderModeSelector()}
    -
    @@ -983,6 +957,7 @@ const ChatTextArea = forwardRef( )} + @@ -1138,8 +1112,8 @@ const ChatTextArea = forwardRef( @@ -1161,10 +1133,10 @@ const ChatTextArea = forwardRef( {!inputValue && !isEditMode && (
    @@ -1245,6 +1217,7 @@ const ChatTextArea = forwardRef( modes={allModes} loading={searchLoading} dynamicSearchResults={fileSearchResults} + commands={commands} />
    )} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index efd2db856c..46790b41ef 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -54,7 +54,9 @@ import AutoApproveMenu from "./AutoApproveMenu" import SystemPromptWarning from "./SystemPromptWarning" import ProfileViolationWarning from "./ProfileViolationWarning" import { CheckpointWarning } from "./CheckpointWarning" +import QueuedMessages from "./QueuedMessages" import { getLatestTodo } from "@roo/todo" +import { QueuedMessage } from "@roo-code/types" export interface ChatViewProps { isHidden: boolean @@ -154,6 +156,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction(null) const [sendingDisabled, setSendingDisabled] = useState(false) const [selectedImages, setSelectedImages] = useState([]) + const [messageQueue, setMessageQueue] = useState([]) + const isProcessingQueueRef = useRef(false) + const retryCountRef = useRef>(new Map()) + const MAX_RETRY_ATTEMPTS = 3 // we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed) const [clineAsk, setClineAsk] = useState(undefined) @@ -439,6 +445,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction { @@ -538,47 +549,133 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - text = text.trim() + (text: string, images: string[], fromQueue = false) => { + try { + text = text.trim() - if (text || images.length > 0) { - // Mark that user has responded - this prevents any pending auto-approvals - userRespondedRef.current = true + if (text || images.length > 0) { + if (sendingDisabled && !fromQueue) { + // Generate a more unique ID using timestamp + random component + const messageId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + setMessageQueue((prev) => [...prev, { id: messageId, text, images }]) + setInputValue("") + setSelectedImages([]) + return + } + // Mark that user has responded - this prevents any pending auto-approvals + userRespondedRef.current = true - if (messagesRef.current.length === 0) { - vscode.postMessage({ type: "newTask", text, images }) - } else if (clineAskRef.current) { - if (clineAskRef.current === "followup") { - markFollowUpAsAnswered() + if (messagesRef.current.length === 0) { + vscode.postMessage({ type: "newTask", text, images }) + } else if (clineAskRef.current) { + if (clineAskRef.current === "followup") { + markFollowUpAsAnswered() + } + + // Use clineAskRef.current + switch ( + clineAskRef.current // Use clineAskRef.current + ) { + case "followup": + case "tool": + case "browser_action_launch": + case "command": // User can provide feedback to a tool or command use. + case "command_output": // User can send input to command stdin. + case "use_mcp_server": + case "completion_result": // If this happens then the user has feedback for the completion result. + case "resume_task": + case "resume_completed_task": + case "mistake_limit_reached": + vscode.postMessage({ + type: "askResponse", + askResponse: "messageResponse", + text, + images, + }) + break + // There is no other case that a textfield should be enabled. + } + } else { + // This is a new message in an ongoing task. + vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images }) } - // Use clineAskRef.current - switch ( - clineAskRef.current // Use clineAskRef.current - ) { - case "followup": - case "tool": - case "browser_action_launch": - case "command": // User can provide feedback to a tool or command use. - case "command_output": // User can send input to command stdin. - case "use_mcp_server": - case "completion_result": // If this happens then the user has feedback for the completion result. - case "resume_task": - case "resume_completed_task": - case "mistake_limit_reached": - vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images }) - break - // There is no other case that a textfield should be enabled. - } + handleChatReset() } - - handleChatReset() + } catch (error) { + console.error("Error in handleSendMessage:", error) + // If this was a queued message, we should handle it differently + if (fromQueue) { + throw error // Re-throw to be caught by the queue processor + } + // For direct sends, we could show an error to the user + // but for now we'll just log it } }, - [handleChatReset, markFollowUpAsAnswered], // messagesRef and clineAskRef are stable + [handleChatReset, markFollowUpAsAnswered, sendingDisabled], // messagesRef and clineAskRef are stable ) + useEffect(() => { + // Early return if conditions aren't met + // Also don't process queue if there's an API error (clineAsk === "api_req_failed") + if ( + sendingDisabled || + messageQueue.length === 0 || + isProcessingQueueRef.current || + clineAsk === "api_req_failed" + ) { + return + } + + // Mark as processing immediately to prevent race conditions + isProcessingQueueRef.current = true + + // Process the first message in the queue + const [nextMessage, ...remaining] = messageQueue + + // Update queue immediately to prevent duplicate processing + setMessageQueue(remaining) + + // Process the message + Promise.resolve() + .then(() => { + handleSendMessage(nextMessage.text, nextMessage.images, true) + // Clear retry count on success + retryCountRef.current.delete(nextMessage.id) + }) + .catch((error) => { + console.error("Failed to send queued message:", error) + + // Get current retry count + const retryCount = retryCountRef.current.get(nextMessage.id) || 0 + + // Only re-add if under retry limit + if (retryCount < MAX_RETRY_ATTEMPTS) { + retryCountRef.current.set(nextMessage.id, retryCount + 1) + // Re-add the message to the end of the queue + setMessageQueue((current) => [...current, nextMessage]) + } else { + console.error(`Message ${nextMessage.id} failed after ${MAX_RETRY_ATTEMPTS} attempts, discarding`) + retryCountRef.current.delete(nextMessage.id) + } + }) + .finally(() => { + isProcessingQueueRef.current = false + }) + + // Cleanup function to handle component unmount + return () => { + isProcessingQueueRef.current = false + } + }, [sendingDisabled, messageQueue, handleSendMessage, clineAsk]) + const handleSetChatBoxMessage = useCallback( (text: string, images: string[]) => { // Avoid nested template literals by breaking down the logic @@ -594,6 +691,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + // Store refs in variables to avoid stale closure issues + const retryCountMap = retryCountRef.current + const isProcessingRef = isProcessingQueueRef + + return () => { + retryCountMap.clear() + isProcessingRef.current = false + } + }, []) + const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), []) // This logic depends on the useEffect[messages] above to set clineAsk, @@ -702,8 +811,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction vscode.postMessage({ type: "selectImages" }), []) - const shouldDisableImages = - !model?.supportsImages || sendingDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE + const shouldDisableImages = !model?.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE const handleMessage = useCallback( (e: MessageEvent) => { @@ -1584,8 +1692,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction { // Check for Command/Ctrl + Period (with or without Shift) - // Using event.code for better cross-platform compatibility - if ((event.metaKey || event.ctrlKey) && event.code === "Period") { + // Using event.key to respect keyboard layouts (e.g., Dvorak) + if ((event.metaKey || event.ctrlKey) && event.key === ".") { event.preventDefault() // Prevent default browser behavior if (event.shiftKey) { @@ -1630,7 +1738,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction +
    {(showAnnouncement || showAnnouncementModal) && ( { @@ -1836,6 +1946,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction )} + setMessageQueue((prev) => prev.filter((_, i) => i !== index))} + onUpdate={(index, newText) => { + setMessageQueue((prev) => prev.map((msg, i) => (i === index ? { ...msg, text: newText } : msg))) + }} + /> = ({ }) }, [checkUnsavedChanges]) + // Use the shared ESC key handler hook - respects unsaved changes logic + useEscapeKey(open, handlePopoverClose) + const handleSaveSettings = () => { // Validate settings before saving if (!validateSettings()) { diff --git a/webview-ui/src/components/chat/CommandExecution.tsx b/webview-ui/src/components/chat/CommandExecution.tsx index 8c92ec7e7b..23d60a7a99 100644 --- a/webview-ui/src/components/chat/CommandExecution.tsx +++ b/webview-ui/src/components/chat/CommandExecution.tsx @@ -6,6 +6,7 @@ import { CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/ import { ExtensionMessage } from "@roo/ExtensionMessage" import { safeJsonParse } from "@roo/safeJsonParse" + import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences" import { vscode } from "@src/utils/vscode" @@ -13,6 +14,14 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { cn } from "@src/lib/utils" import { Button } from "@src/components/ui" import CodeBlock from "../common/CodeBlock" +import { CommandPatternSelector } from "./CommandPatternSelector" +import { parseCommand } from "../../utils/command-validation" +import { extractPatternsFromCommand } from "../../utils/command-parser" + +interface CommandPattern { + pattern: string + description?: string +} interface CommandExecutionProps { executionId: string @@ -22,7 +31,13 @@ interface CommandExecutionProps { } export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => { - const { terminalShellIntegrationDisabled = false } = useExtensionState() + const { + terminalShellIntegrationDisabled = false, + allowedCommands = [], + deniedCommands = [], + setAllowedCommands, + setDeniedCommands, + } = useExtensionState() const { command, output: parsedOutput } = useMemo(() => parseCommandAndOutput(text), [text]) @@ -37,6 +52,55 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec // streaming output (this is the case for running commands). const output = streamingOutput || parsedOutput + // Extract command patterns from the actual command that was executed + const commandPatterns = useMemo(() => { + // First get all individual commands (including subshell commands) using parseCommand + const allCommands = parseCommand(command) + + // Then extract patterns from each command using the existing pattern extraction logic + const allPatterns = new Set() + + // Add all individual commands first + allCommands.forEach((cmd) => { + if (cmd.trim()) { + allPatterns.add(cmd.trim()) + } + }) + + // Then add extracted patterns for each command + allCommands.forEach((cmd) => { + const patterns = extractPatternsFromCommand(cmd) + patterns.forEach((pattern) => allPatterns.add(pattern)) + }) + + return Array.from(allPatterns).map((pattern) => ({ + pattern, + })) + }, [command]) + + // Handle pattern changes + const handleAllowPatternChange = (pattern: string) => { + const isAllowed = allowedCommands.includes(pattern) + const newAllowed = isAllowed ? allowedCommands.filter((p) => p !== pattern) : [...allowedCommands, pattern] + const newDenied = deniedCommands.filter((p) => p !== pattern) + + setAllowedCommands(newAllowed) + setDeniedCommands(newDenied) + vscode.postMessage({ type: "allowedCommands", commands: newAllowed }) + vscode.postMessage({ type: "deniedCommands", commands: newDenied }) + } + + const handleDenyPatternChange = (pattern: string) => { + const isDenied = deniedCommands.includes(pattern) + const newDenied = isDenied ? deniedCommands.filter((p) => p !== pattern) : [...deniedCommands, pattern] + const newAllowed = allowedCommands.filter((p) => p !== pattern) + + setAllowedCommands(newAllowed) + setDeniedCommands(newDenied) + vscode.postMessage({ type: "allowedCommands", commands: newAllowed }) + vscode.postMessage({ type: "deniedCommands", commands: newDenied }) + } + const onMessage = useCallback( (event: MessageEvent) => { const message: ExtensionMessage = event.data @@ -121,9 +185,21 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
    -
    - - +
    +
    + + +
    + {command && command.trim() && ( + + )}
    ) diff --git a/webview-ui/src/components/chat/CommandPatternSelector.tsx b/webview-ui/src/components/chat/CommandPatternSelector.tsx new file mode 100644 index 0000000000..87ccb1bab7 --- /dev/null +++ b/webview-ui/src/components/chat/CommandPatternSelector.tsx @@ -0,0 +1,193 @@ +import React, { useState, useMemo } from "react" +import { Check, ChevronDown, Info, X } from "lucide-react" +import { cn } from "../../lib/utils" +import { useTranslation, Trans } from "react-i18next" +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { StandardTooltip } from "../ui/standard-tooltip" + +interface CommandPattern { + pattern: string + description?: string +} + +interface CommandPatternSelectorProps { + command: string + patterns: CommandPattern[] + allowedCommands: string[] + deniedCommands: string[] + onAllowPatternChange: (pattern: string) => void + onDenyPatternChange: (pattern: string) => void +} + +export const CommandPatternSelector: React.FC = ({ + command, + patterns, + allowedCommands, + deniedCommands, + onAllowPatternChange, + onDenyPatternChange, +}) => { + const { t } = useTranslation() + const [isExpanded, setIsExpanded] = useState(false) + const [editingStates, setEditingStates] = useState>({}) + + const handleOpenSettings = () => { + window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "autoApprove" } }) + } + + // Create a combined list with full command first, then patterns + const allPatterns = useMemo(() => { + // Trim the command to ensure consistency with extracted patterns + const trimmedCommand = command.trim() + const fullCommandPattern: CommandPattern = { pattern: trimmedCommand } + + // Create a set to track unique patterns we've already seen + const seenPatterns = new Set() + seenPatterns.add(trimmedCommand) // Add the trimmed full command first + + // Filter out any patterns that are duplicates or are the same as the full command + const uniquePatterns = patterns.filter((p) => { + if (seenPatterns.has(p.pattern)) { + return false + } + seenPatterns.add(p.pattern) + return true + }) + + return [fullCommandPattern, ...uniquePatterns] + }, [command, patterns]) + + const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => { + if (allowedCommands.includes(pattern)) return "allowed" + if (deniedCommands.includes(pattern)) return "denied" + return "none" + } + + const getEditState = (pattern: string) => { + return editingStates[pattern] || { isEditing: false, value: pattern } + } + + const setEditState = (pattern: string, isEditing: boolean, value?: string) => { + setEditingStates((prev) => ({ + ...prev, + [pattern]: { isEditing, value: value ?? pattern }, + })) + } + + return ( +
    +
    + + + {isExpanded && ( +
    + {allPatterns.map((item) => { + const editState = getEditState(item.pattern) + const status = getPatternStatus(editState.value) + + return ( +
    +
    + {editState.isEditing ? ( + setEditState(item.pattern, true, e.target.value)} + onBlur={() => setEditState(item.pattern, false)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setEditState(item.pattern, false) + } + if (e.key === "Escape") { + setEditState(item.pattern, false, item.pattern) + } + }} + className="font-mono text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1.5 w-full focus:outline-0 focus:ring-1 focus:ring-vscode-focusBorder" + placeholder={item.pattern} + autoFocus + /> + ) : ( +
    setEditState(item.pattern, true)} + className="font-mono text-xs text-vscode-foreground cursor-pointer hover:bg-vscode-list-hoverBackground px-2 py-1.5 rounded transition-colors border border-transparent break-all" + title="Click to edit pattern"> + {editState.value} + {item.description && ( + + - {item.description} + + )} +
    + )} +
    +
    + + +
    +
    + ) + })} +
    + )} +
    + ) +} diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 1672c35ee3..86965fcb11 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react" import { getIconForFilePath, getIconUrlByName, getIconForDirectoryPath } from "vscode-material-icons" import type { ModeConfig } from "@roo-code/types" +import type { Command } from "@roo/ExtensionMessage" import { ContextMenuOptionType, @@ -23,12 +24,12 @@ interface ContextMenuProps { modes?: ModeConfig[] loading?: boolean dynamicSearchResults?: SearchResult[] + commands?: Command[] } const ContextMenu: React.FC = ({ onSelect, searchQuery, - inputValue, onMouseDown, selectedIndex, setSelectedIndex, @@ -36,13 +37,14 @@ const ContextMenu: React.FC = ({ queryItems, modes, dynamicSearchResults = [], + commands = [], }) => { const [materialIconsBaseUri, setMaterialIconsBaseUri] = useState("") const menuRef = useRef(null) const filteredOptions = useMemo(() => { - return getContextMenuOptions(searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes) - }, [searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes]) + return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes, commands) + }, [searchQuery, selectedType, queryItems, dynamicSearchResults, modes, commands]) useEffect(() => { if (menuRef.current) { @@ -68,10 +70,56 @@ const ContextMenu: React.FC = ({ const renderOptionContent = (option: ContextMenuQueryItem) => { switch (option.type) { + case ContextMenuOptionType.SectionHeader: + return ( + + {option.label} + + ) case ContextMenuOptionType.Mode: return (
    - {option.label} +
    + {option.slashCommand} +
    + {option.description && ( + + {option.description} + + )} +
    + ) + case ContextMenuOptionType.Command: + return ( +
    +
    + {option.slashCommand} + {option.argumentHint && ( + + {option.argumentHint} + + )} +
    {option.description && ( = ({ switch (option.type) { case ContextMenuOptionType.Mode: return "symbol-misc" + case ContextMenuOptionType.Command: + return "play" case ContextMenuOptionType.OpenedFile: return "window" case ContextMenuOptionType.File: @@ -194,7 +244,11 @@ const ContextMenu: React.FC = ({ } const isOptionSelectable = (option: ContextMenuQueryItem): boolean => { - return option.type !== ContextMenuOptionType.NoResults && option.type !== ContextMenuOptionType.URL + return ( + option.type !== ContextMenuOptionType.NoResults && + option.type !== ContextMenuOptionType.URL && + option.type !== ContextMenuOptionType.SectionHeader + ) } return ( @@ -217,8 +271,9 @@ const ContextMenu: React.FC = ({ zIndex: 1000, display: "flex", flexDirection: "column", - maxHeight: "200px", + maxHeight: "300px", overflowY: "auto", + overflowX: "hidden", }}> {filteredOptions && filteredOptions.length > 0 ? ( filteredOptions.map((option, index) => ( @@ -226,12 +281,20 @@ const ContextMenu: React.FC = ({ key={`${option.type}-${option.value || index}`} onClick={() => isOptionSelectable(option) && onSelect(option.type, option.value)} style={{ - padding: "4px 6px", + padding: + option.type === ContextMenuOptionType.SectionHeader ? "8px 6px 4px 6px" : "4px 6px", cursor: isOptionSelectable(option) ? "pointer" : "default", color: "var(--vscode-dropdown-foreground)", display: "flex", alignItems: "center", justifyContent: "space-between", + position: "relative", + ...(option.type === ContextMenuOptionType.SectionHeader + ? { + borderBottom: "1px solid var(--vscode-editorGroup-border)", + marginBottom: "2px", + } + : {}), ...(index === selectedIndex && isOptionSelectable(option) ? { backgroundColor: "var(--vscode-list-activeSelectionBackground)", @@ -248,6 +311,7 @@ const ContextMenu: React.FC = ({ minWidth: 0, overflow: "hidden", paddingTop: 0, + position: "relative", }}> {(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder || @@ -264,9 +328,11 @@ const ContextMenu: React.FC = ({ /> )} {option.type !== ContextMenuOptionType.Mode && + option.type !== ContextMenuOptionType.Command && option.type !== ContextMenuOptionType.File && option.type !== ContextMenuOptionType.Folder && option.type !== ContextMenuOptionType.OpenedFile && + option.type !== ContextMenuOptionType.SectionHeader && getIconForOption(option) && ( { e.stopPropagation() + // Cancel the auto-approve timer when edit button is clicked + setSuggestionSelected(true) + onCancelAutoApproval?.() // Simulate shift-click by directly calling the handler with shiftKey=true. onSuggestionClick?.(suggestion, { ...e, shiftKey: true }) }}> diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index ba838284d7..87780d5df8 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -21,7 +21,7 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} style={{ position: "relative" }}> -
    +
    {markdown && !partial && isHovering && ( diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 336e9f8357..93dd2f1f4f 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -1,5 +1,5 @@ import React from "react" -import { ChevronUp, Check } from "lucide-react" +import { ChevronUp, Check, X } from "lucide-react" import { cn } from "@/lib/utils" import { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" @@ -11,6 +11,10 @@ import { Mode, getAllModes } from "@roo/modes" import { ModeConfig, CustomModePrompts } from "@roo-code/types" import { telemetryClient } from "@/utils/TelemetryClient" import { TelemetryEventName } from "@roo-code/types" +import { Fzf } from "fzf" + +// Minimum number of modes required to show search functionality +const SEARCH_THRESHOLD = 6 interface ModeSelectorProps { value: Mode @@ -21,6 +25,7 @@ interface ModeSelectorProps { modeShortcutText: string customModes?: ModeConfig[] customModePrompts?: CustomModePrompts + disableSearch?: boolean } export const ModeSelector = ({ @@ -32,13 +37,16 @@ export const ModeSelector = ({ modeShortcutText, customModes, customModePrompts, + disableSearch = false, }: ModeSelectorProps) => { const [open, setOpen] = React.useState(false) + const [searchValue, setSearchValue] = React.useState("") + const searchInputRef = React.useRef(null) const portalContainer = useRooPortal("roo-portal") const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() const { t } = useAppTranslation() - const trackModeSelectorOpened = () => { + const trackModeSelectorOpened = React.useCallback(() => { // Track telemetry every time the mode selector is opened telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED) @@ -47,7 +55,7 @@ export const ModeSelector = ({ setHasOpenedModeSelector(true) vscode.postMessage({ type: "hasOpenedModeSelector", bool: true }) } - } + }, [hasOpenedModeSelector, setHasOpenedModeSelector]) // Get all modes including custom modes and merge custom prompt descriptions const modes = React.useMemo(() => { @@ -61,6 +69,96 @@ export const ModeSelector = ({ // Find the selected mode const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) + // Memoize searchable items for fuzzy search with separate name and description search + const nameSearchItems = React.useMemo(() => { + return modes.map((mode) => ({ + original: mode, + searchStr: [mode.name, mode.slug].filter(Boolean).join(" "), + })) + }, [modes]) + + const descriptionSearchItems = React.useMemo(() => { + return modes.map((mode) => ({ + original: mode, + searchStr: mode.description || "", + })) + }, [modes]) + + // Create memoized Fzf instances for name and description searches + const nameFzfInstance = React.useMemo(() => { + return new Fzf(nameSearchItems, { + selector: (item) => item.searchStr, + }) + }, [nameSearchItems]) + + const descriptionFzfInstance = React.useMemo(() => { + return new Fzf(descriptionSearchItems, { + selector: (item) => item.searchStr, + }) + }, [descriptionSearchItems]) + + // Filter modes based on search value using fuzzy search with priority + const filteredModes = React.useMemo(() => { + if (!searchValue) return modes + + // First search in names/slugs + const nameMatches = nameFzfInstance.find(searchValue) + const nameMatchedModes = new Set(nameMatches.map((result) => result.item.original.slug)) + + // Then search in descriptions + const descriptionMatches = descriptionFzfInstance.find(searchValue) + + // Combine results: name matches first, then description matches + const combinedResults = [ + ...nameMatches.map((result) => result.item.original), + ...descriptionMatches + .filter((result) => !nameMatchedModes.has(result.item.original.slug)) + .map((result) => result.item.original), + ] + + return combinedResults + }, [modes, searchValue, nameFzfInstance, descriptionFzfInstance]) + + const onClearSearch = React.useCallback(() => { + setSearchValue("") + searchInputRef.current?.focus() + }, []) + + const handleSelect = React.useCallback( + (modeSlug: string) => { + onChange(modeSlug as Mode) + setOpen(false) + // Clear search after selection + setSearchValue("") + }, + [onChange], + ) + + const onOpenChange = React.useCallback( + (isOpen: boolean) => { + if (isOpen) trackModeSelectorOpened() + setOpen(isOpen) + // Clear search when closing + if (!isOpen) { + setSearchValue("") + } + }, + [trackModeSelectorOpened], + ) + + // Auto-focus search input when popover opens + React.useEffect(() => { + if (open && searchInputRef.current) { + searchInputRef.current.focus() + } + }, [open]) + + // Determine if search should be shown + const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD + + // Combine instruction text for tooltip + const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}` + const trigger = ( { - if (isOpen) trackModeSelectorOpened() - setOpen(isOpen) - }} - data-testid="mode-selector-root"> + {title ? {trigger} : trigger}
    -
    -
    -

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

    -
    - { - window.postMessage( - { - type: "action", - action: "marketplaceButtonClicked", - values: { marketplaceTab: "mode" }, - }, - "*", - ) - - setOpen(false) - }} - /> - { - vscode.postMessage({ - type: "switchTab", - tab: "modes", - }) - setOpen(false) - }} - /> -
    + {/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */} + {showSearch ? ( +
    + setSearchValue(e.target.value)} + placeholder={t("chat:modeSelector.searchPlaceholder")} + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + data-testid="mode-search-input" + /> + {searchValue.length > 0 && ( +
    + +
    + )}
    -

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

    -
    + ) : ( +
    +

    {instructionText}

    +
    + )} {/* Mode List */} -
    - {modes.map((mode) => ( -
    + {filteredModes.length === 0 && searchValue ? ( +
    + {t("chat:modeSelector.noResults")} +
    + ) : ( +
    + {filteredModes.map((mode) => ( +
    handleSelect(mode.slug)} + className={cn( + "px-3 py-1.5 text-sm cursor-pointer flex items-center", + "hover:bg-vscode-list-hoverBackground", + mode.slug === value + ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" + : "", + )} + data-testid="mode-selector-item"> +
    +
    {mode.name}
    + {mode.description && ( +
    + {mode.description} +
    + )} +
    + {mode.slug === value && } +
    + ))} +
    + )} +
    + + {/* Bottom bar with buttons on left and title on right */} +
    +
    + { - onChange(mode.slug as Mode) + window.postMessage( + { + type: "action", + action: "marketplaceButtonClicked", + values: { marketplaceTab: "mode" }, + }, + "*", + ) setOpen(false) }} - data-testid="mode-selector-item"> -
    -

    {mode.name}

    - {mode.description && ( -

    - {mode.description} -

    - )} -
    - {mode.slug === value ? ( - - ) : ( -
    - )} -
    - ))} + /> + { + vscode.postMessage({ + type: "switchTab", + tab: "modes", + }) + setOpen(false) + }} + /> +
    + + {/* Info icon and title on the right - only show info icon when search bar is visible */} +
    + {showSearch && ( + + + + )} +

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

    +
    diff --git a/webview-ui/src/components/chat/QueuedMessages.tsx b/webview-ui/src/components/chat/QueuedMessages.tsx new file mode 100644 index 0000000000..cd3ee6d896 --- /dev/null +++ b/webview-ui/src/components/chat/QueuedMessages.tsx @@ -0,0 +1,112 @@ +import React, { useState } from "react" +import { useTranslation } from "react-i18next" +import Thumbnails from "../common/Thumbnails" +import { QueuedMessage } from "@roo-code/types" +import { Mention } from "./Mention" +import { Button } from "@src/components/ui" + +interface QueuedMessagesProps { + queue: QueuedMessage[] + onRemove: (index: number) => void + onUpdate: (index: number, newText: string) => void +} + +const QueuedMessages: React.FC = ({ queue, onRemove, onUpdate }) => { + const { t } = useTranslation("chat") + const [editingStates, setEditingStates] = useState>({}) + + if (queue.length === 0) { + return null + } + + const getEditState = (messageId: string, currentText: string) => { + return editingStates[messageId] || { isEditing: false, value: currentText } + } + + const setEditState = (messageId: string, isEditing: boolean, value?: string) => { + setEditingStates((prev) => ({ + ...prev, + [messageId]: { isEditing, value: value ?? prev[messageId]?.value ?? "" }, + })) + } + + const handleSaveEdit = (index: number, messageId: string, newValue: string) => { + onUpdate(index, newValue) + setEditState(messageId, false) + } + + return ( +
    +
    {t("queuedMessages.title")}
    +
    + {queue.map((message, index) => { + const editState = getEditState(message.id, message.text) + + return ( +
    +
    +
    + {editState.isEditing ? ( +