Merge branch 'main' into main

This commit is contained in:
Kevin Zhao 2025-12-10 22:01:00 +08:00 committed by GitHub
commit 941786a2e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
814 changed files with 56844 additions and 15245 deletions

View file

@ -76,7 +76,7 @@ body:
label: API Provider (optional)
options:
- Anthropic
- AWS Bedrock
- Amazon Bedrock
- Chutes AI
- DeepSeek
- Featherless AI

3
.gitignore vendored
View file

@ -49,3 +49,6 @@ logs
# Qdrant
qdrant_storage/
# Architect plans
plans/

View file

@ -16,14 +16,14 @@ argument-hint: 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!)"
- Always include contributor attribution and the PR number: use "(PR #<prNumber> by @username)".
- For PRs that close issues, include both the issue number and the PR number and authors: "- Fix: Description (#123 by @reporter, PR #456 by @contributor)"
- For PRs without linked issues, include the PR number and author: "- Add support for feature (PR #456 by @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!)"
- With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR #789 by @prAuthor)"
- Without issue: "- Add support for Gemini 2.5 Pro caching (PR #789 by @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 the generate_image tool is available, create a release image at `releases/[version]-release.png`

View file

@ -115,7 +115,7 @@
- 保留英文品牌名
- 技术术语保持一致性
- 保留英文专有名词:如"AWS Bedrock ARN"
- 保留英文专有名词:如"Amazon Bedrock ARN"
4. **用户操作**
- 操作动词统一:

View file

@ -1,5 +1,351 @@
# Roo Code Changelog
## [3.36.3] - 2025-12-09
![3.36.3 Release](/releases/3.36.3-release.png)
- Refactor: Unified context-management architecture with improved UX for better context control (PR #9795 by @hannesrudolph)
- Add new `search_replace` native tool for single-replacement operations with improved editing precision (PR #9918 by @hannesrudolph)
- Streaming tool stats and token usage throttling for better real-time feedback during generation (PR #9926 by @hannesrudolph)
- Add versioned settings support with minPluginVersion gating for Roo provider (PR #9934 by @hannesrudolph)
- Make Architect mode save plans to `/plans` directory and gitignore it (PR #9944 by @brunobergher)
- Add announcement support CTA and social icons to UI (PR #9945 by @hannesrudolph)
- Add ability to save screenshots from the browser tool (PR #9963 by @mrubens)
- Refactor: Decouple tools from system prompt for cleaner architecture (PR #9784 by @daniel-lxs)
- Update DeepSeek models to V3.2 with new pricing (PR #9962 by @hannesrudolph)
- Add minimal and medium reasoning effort levels for Gemini models (PR #9973 by @hannesrudolph)
- Update xAI models catalog with latest model options (PR #9872 by @hannesrudolph)
- Add DeepSeek V3-2 support for Baseten provider (PR #9861 by @AlexKer)
- Tweaks to Baseten model definitions for better defaults (PR #9866 by @mrubens)
- Fix: Add xhigh reasoning effort support for gpt-5.1-codex-max (#9891 by @andrewginns, PR #9900 by @andrewginns)
- Fix: Add Kimi, MiniMax, and Qwen model configurations for Bedrock (#9902 by @jbearak, PR #9905 by @app/roomote)
- Configure tool preferences for xAI models (PR #9923 by @hannesrudolph)
- Default to using native tools when supported on OpenRouter (PR #9878 by @mrubens)
- Fix: Exclude apply_diff from native tools when diffEnabled is false (#9919 by @denis-kudelin, PR #9920 by @app/roomote)
- Fix: Always show tool protocol selector for openai-compatible provider (#9965 by @bozoweed, PR #9966 by @hannesrudolph)
- Fix: Respect explicit supportsReasoningEffort array values for proper model configuration (PR #9970 by @hannesrudolph)
- Add timeout configuration to OpenAI Compatible Provider Client (PR #9898 by @dcbartlett)
- Revert default tool protocol change from xml to native for stability (PR #9956 by @mrubens)
- Remove defaultTemperature from Roo provider configuration (PR #9932 by @mrubens)
- Improve OpenAI error messages to be more useful for debugging (PR #9639 by @mrubens)
- Better error logs for parseToolCall exceptions (PR #9857 by @cte)
- Improve cloud job error logging for RCC provider errors (PR #9924 by @cte)
- Fix: Display actual API error message instead of generic text on retry (PR #9954 by @hannesrudolph)
- Add API error telemetry to OpenRouter provider for better diagnostics (PR #9953 by @daniel-lxs)
- Fix: Sanitize removed/invalid API providers to prevent infinite loop (PR #9869 by @hannesrudolph)
- Fix: Use foreground color for context-management icons (PR #9912 by @hannesrudolph)
- Fix: Suppress 'ask promise was ignored' error in handleError (PR #9914 by @daniel-lxs)
- Fix: Process finish_reason to emit tool_call_end events properly (PR #9927 by @daniel-lxs)
- Fix: Add finish_reason processing to xai.ts provider (PR #9929 by @daniel-lxs)
- Fix: Validate and fix tool_result IDs before API requests (PR #9952 by @daniel-lxs)
- Fix: Return undefined instead of 0 for disabled API timeout (PR #9960 by @hannesrudolph)
- Stop making unnecessary count_tokens requests for better performance (PR #9884 by @mrubens)
- Refactor: Consolidate ThinkingBudget components and fix disable handling (PR #9930 by @hannesrudolph)
- Forbid time estimates in architect mode for more focused planning (PR #9931 by @app/roomote)
- Web: Add product pages (PR #9865 by @brunobergher)
- Make eval runs deleteable in the web UI (PR #9909 by @mrubens)
- Feat: Change defaultToolProtocol default from xml to native (later reverted) (PR #9892 by @app/roomote)
## [3.36.2] - 2025-12-04
![3.36.2 Release - Dynamic API Settings](/releases/3.36.2-release.png)
- Restrict GPT-5 tool set to apply_patch for improved compatibility (PR #9853 by @hannesrudolph)
- Add dynamic settings support for Roo models from API, allowing model-specific configurations to be fetched dynamically (PR #9852 by @hannesrudolph)
- Fix: Resolve Chutes provider model fetching issue (PR #9854 by @cte)
## [3.36.1] - 2025-12-04
![3.36.1 Release - Message Management & Stability Improvements](/releases/3.36.1-release.png)
- Add MessageManager layer for centralized history coordination, fixing message synchronization issues (PR #9842 by @hannesrudolph)
- Fix: Prevent cascading truncation loop by only truncating visible messages (PR #9844 by @hannesrudolph)
- Fix: Handle unknown/invalid native tool calls to prevent extension freeze (PR #9834 by @daniel-lxs)
- Always enable reasoning for models that require it (PR #9836 by @cte)
- ChatView: Smoother stick-to-bottom behavior during streaming (PR #8999 by @hannesrudolph)
- UX: Improved error messages and documentation links (PR #9777 by @brunobergher)
- Fix: Overly round follow-up question suggestions styling (PR #9829 by @brunobergher)
- Add symlink support for slash commands in .roo/commands folder (PR #9838 by @mrubens)
- Ignore input to the execa terminal process for safer command execution (PR #9827 by @mrubens)
- Be safer about large file reads (PR #9843 by @jr)
- Add gpt-5.1-codex-max model to OpenAI provider (PR #9848 by @hannesrudolph)
- Evals UI: Add filtering, bulk delete, tool consolidation, and run notes (PR #9837 by @hannesrudolph)
- Evals UI: Add multi-model launch and UI improvements (PR #9845 by @hannesrudolph)
- Web: New pricing page (PR #9821 by @brunobergher)
## [3.36.0] - 2025-12-04
![3.36.0 Release - Rewind Kangaroo](/releases/3.36.0-release.png)
- Fix: Restore context when rewinding after condense (#8295 by @hannesrudolph, PR #9665 by @hannesrudolph)
- Add reasoning_details support to Roo provider for enhanced model reasoning visibility (PR #9796 by @app/roomote)
- Default to native tools for all models in the Roo provider for improved performance (PR #9811 by @mrubens)
- Enable search_and_replace for Minimax models (PR #9780 by @mrubens)
- Fix: Resolve Vercel AI Gateway model fetching issues (PR #9791 by @cte)
- Fix: Apply conservative max tokens for Cerebras provider (PR #9804 by @sebastiand-cerebras)
- Fix: Remove omission detection logic to eliminate false positives (#9785 by @Michaelzag, PR #9787 by @app/roomote)
- Refactor: Remove deprecated insert_content tool (PR #9751 by @daniel-lxs)
- Chore: Hide parallel tool calls experiment and disable feature (PR #9798 by @hannesrudolph)
- Update next.js documentation site dependencies (PR #9799 by @jr)
- Fix: Correct download count display on homepage (PR #9807 by @mrubens)
## [3.35.5] - 2025-12-03
- Feat: Add provider routing selection for OpenRouter embeddings (#9144 by @SannidhyaSah, PR #9693 by @SannidhyaSah)
- Default Minimax M2 to native tool calling (PR #9778 by @mrubens)
- Sanitize the native tool calls to fix a bug with Gemini (PR #9769 by @mrubens)
- UX: Updates to CloudView (PR #9776 by @roomote)
## [3.35.4] - 2025-12-02
- Fix: Handle malformed native tool calls to prevent hanging (PR #9758 by @daniel-lxs)
- Fix: Remove reasoning toggles for GLM-4.5 and GLM-4.6 on z.ai provider (PR #9752 by @roomote)
- Refactor: Remove line_count parameter from write_to_file tool (PR #9667 by @hannesrudolph)
## [3.35.3] - 2025-12-02
- Switch to new welcome view for improved onboarding experience (PR #9741 by @mrubens)
- Update homepage with latest changes (PR #9675 by @brunobergher)
- Improve privacy for stealth models by adding vendor confidentiality section to system prompt (PR #9742 by @mrubens)
## [3.35.2] - 2025-12-01
![3.35.2 Release - Model Default Temperatures](/releases/3.35.2-release.png)
- Allow models to contain default temperature settings for provider-specific optimal defaults (PR #9734 by @mrubens)
- Add tag-based native tool calling detection for Roo provider models (PR #9735 by @mrubens)
- Enable native tool support for all LiteLLM models by default (PR #9736 by @mrubens)
- Pass app version to provider for improved request tracking (PR #9730 by @cte)
## [3.35.1] - 2025-12-01
- Fix: Flush pending tool results before task delegation (PR #9726 by @daniel-lxs)
- Improve: Better IPC error logging for easier debugging (PR #9727 by @cte)
## [3.35.0] - 2025-12-01
![3.35.0 Release - Subtasks & Native Tools](/releases/3.35.0-release.png)
- Metadata-driven subtasks with automatic parent resume and single-open safety for improved task orchestration (#8081 by @hannesrudolph, PR #9090 by @hannesrudolph)
- Native tool calling support expanded across many providers: Bedrock (PR #9698 by @mrubens), Cerebras (PR #9692 by @mrubens), Chutes with auto-detection from API (PR #9715 by @daniel-lxs), DeepInfra (PR #9691 by @mrubens), DeepSeek and Doubao (PR #9671 by @daniel-lxs), Groq (PR #9673 by @daniel-lxs), LiteLLM (PR #9719 by @daniel-lxs), Ollama (PR #9696 by @mrubens), OpenAI-compatible providers (PR #9676 by @daniel-lxs), Requesty (PR #9672 by @daniel-lxs), Unbound (PR #9699 by @mrubens), Vercel AI Gateway (PR #9697 by @mrubens), Vertex Gemini (PR #9678 by @daniel-lxs), and xAI with new Grok 4 Fast and Grok 4.1 Fast models (PR #9690 by @mrubens)
- Fix: Preserve tool_use blocks in summary for parallel tool calls (#9700 by @SilentFlower, PR #9714 by @SilentFlower)
- Default Grok Code Fast to native tools for better performance (PR #9717 by @mrubens)
- UX improvements to the Roo Code Cloud provider-centric onboarding flow (PR #9709 by @brunobergher)
- UX toolbar cleanup and settings consolidation for a cleaner interface (PR #9710 by @brunobergher)
- Add model-specific tool customization via `excludedTools` and `includedTools` configuration (PR #9641 by @daniel-lxs)
- Add new `apply_patch` native tool for more efficient file editing operations (PR #9663 by @hannesrudolph)
- Add new `search_and_replace` tool for batch text replacements across files (PR #9549 by @hannesrudolph)
- Add debug buttons to view API and UI history for troubleshooting (PR #9684 by @hannesrudolph)
- Include tool format in environment details for better context awareness (PR #9661 by @mrubens)
- Fix: Display install count in millions instead of thousands (PR #9677 by @app/roomote)
- Web-evals improvements: add task log viewing, export failed logs, and new run options (PR #9637 by @hannesrudolph)
- Web-evals updates: add kill run functionality (PR #9681 by @hannesrudolph)
- Fix: Prevent navigation buttons from wrapping on smaller screens (PR #9721 by @app/roomote)
## [3.34.8] - 2025-11-27
![3.34.8 Release - Race Condition Fix](/releases/3.34.8-release.png)
- Fix: Race condition in new_task tool for native protocol (PR #9655 by @daniel-lxs)
## [3.34.7] - 2025-11-27
![3.34.7 Release - More Native Tool Integrations](/releases/3.34.7-release.png)
- Support native tools in the Anthropic provider for improved tool calling (PR #9644 by @mrubens)
- Enable native tool calling for z.ai models (PR #9645 by @mrubens)
- Enable native tool calling for Moonshot models (PR #9646 by @mrubens)
- Fix: OpenRouter tool calls handling improvements (PR #9642 by @mrubens)
- Fix: OpenRouter GPT-5 strict schema validation for read_file tool (PR #9633 by @daniel-lxs)
- Fix: Create parent directories early in write_to_file to prevent ENOENT errors (#9634 by @ivanenev, PR #9640 by @daniel-lxs)
- Fix: Disable native tools and temperature support for claude-code provider (PR #9643 by @hannesrudolph)
- Add 'taking you to cloud' screen after provider welcome for improved onboarding (PR #9652 by @mrubens)
## [3.34.6] - 2025-11-26
![3.34.6 Release - Bedrock Embeddings](/releases/3.34.6-release.png)
- Add support for AWS Bedrock embeddings in code indexing (#8658 by @kyle-hobbs, PR #9475 by @ggoranov-smar)
- Add native tool calling support for Mistral provider (PR #9625 by @hannesrudolph)
- Wire MULTIPLE_NATIVE_TOOL_CALLS experiment to OpenAI parallel_tool_calls for parallel tool execution (PR #9621 by @hannesrudolph)
- Add fine grained tool streaming for OpenRouter Anthropic (PR #9629 by @mrubens)
- Allow global inference selection for Bedrock when cross-region is enabled (PR #9616 by @roomote)
- Fix: Filter non-Anthropic content blocks before sending to Vertex API (#9583 by @cardil, PR #9618 by @hannesrudolph)
- Fix: Restore content undefined check in WriteToFileTool.handlePartial() (#9611 by @Lissanro, PR #9614 by @daniel-lxs)
- Fix: Prevent model cache from persisting empty API responses (#9597 by @zx2021210538, PR #9623 by @daniel-lxs)
- Fix: Exclude access_mcp_resource tool when MCP has no resources (PR #9615 by @daniel-lxs)
- Fix: Update default settings for inline terminal and codebase indexing (PR #9622 by @roomote)
- Fix: Convert line_ranges strings to lineRanges objects in native tool calls (PR #9627 by @daniel-lxs)
- Fix: Defer new_task tool_result until subtask completes for native protocol (PR #9628 by @daniel-lxs)
## [3.34.5] - 2025-11-25
![3.34.5 Release - Experimental Parallel Tool Calling](/releases/3.34.5-release.png)
- Experimental feature to enable multiple native tool calls per turn (PR #9273 by @daniel-lxs)
- Add Bedrock Opus 4.5 to global inference model list (PR #9595 by @roomote)
- Fix: Update API handler when toolProtocol changes (PR #9599 by @mrubens)
- Set native tools as default for minimax-m2 and claude-haiku-4.5 (PR #9586 by @daniel-lxs)
- Make single file read only apply to XML tools (PR #9600 by @mrubens)
- Enhance web-evals dashboard with dynamic tool columns and UX improvements (PR #9592 by @hannesrudolph)
- Revert "Add support for Roo Code Cloud as an embeddings provider" while we fix some issues (PR #9602 by @mrubens)
## [3.34.4] - 2025-11-25
![3.34.4 Release - BFL Image Generation](/releases/3.34.4-release.png)
- Add new Black Forest Labs image generation models, free on Roo Code Cloud and also available on OpenRouter (PR #9587 and #9589 by @mrubens)
- Fix: Preserve dynamic MCP tool names in native mode API history to prevent tool name mismatches (PR #9559 by @daniel-lxs)
- Fix: Preserve tool_use blocks in summary message during condensing with native tools to maintain conversation context (PR #9582 by @daniel-lxs)
## [3.34.3] - 2025-11-25
![3.34.3 Release - Streaming and Opus 4.5](/releases/3.34.3-release.png)
- Implement streaming for native tool calls, providing real-time feedback during tool execution (PR #9542 by @daniel-lxs)
- Add Claude Opus 4.5 model to Claude Code provider (PR #9560 by @mrubens)
- Add Claude Opus 4.5 model to Bedrock provider (#9571 by @pisicode, PR #9572 by @roomote)
- Enable caching for Opus 4.5 model to improve performance (#9567 by @iainRedro, PR #9568 by @roomote)
- Add support for Roo Code Cloud as an embeddings provider (PR #9543 by @mrubens)
- Fix ask_followup_question streaming issue and add missing tool cases (PR #9561 by @daniel-lxs)
- Add contact links to About Roo Code settings page (PR #9570 by @roomote)
- Switch from asdf to mise-en-place in bare-metal evals setup script (PR #9548 by @cte)
## [3.34.2] - 2025-11-24
![3.34.2 Release - Opus Conductor](/releases/3.34.2-release.png)
- Add support for Claude Opus 4.5 in Anthropic and Vertex providers (PR #9541 by @daniel-lxs)
- Add support for Claude Opus 4.5 in OpenRouter with prompt caching and reasoning budget (PR #9540 by @daniel-lxs)
- Add Roo Code Cloud as an image generation provider (PR #9528 by @mrubens)
- Fix: Gracefully skip unsupported content blocks in Gemini transformer (PR #9537 by @daniel-lxs)
- Fix: Flush LiteLLM cache when credentials change on refresh (PR #9536 by @daniel-lxs)
- Fix: Ensure XML parser state matches tool protocol on config update (PR #9535 by @daniel-lxs)
- Update Cerebras models (PR #9527 by @sebastiand-cerebras)
- Fix: Support reasoning_details format for Gemini 3 models (PR #9506 by @daniel-lxs)
## [3.34.1] - 2025-11-23
- Show the prompt for image generation in the UI (PR #9505 by @mrubens)
- Fix double todo list display issue (PR #9517 by @mrubens)
- Add tracking for cloud synced messages (PR #9518 by @mrubens)
- Enable the Roo Code Cloud provider in evals (PR #9492 by @cte)
## [3.34.0] - 2025-11-21
![3.34.0 Release - Browser Use 2.0](/releases/3.34.0-release.png)
- Add Browser Use 2.0 with enhanced browser interaction capabilities (PR #8941 by @hannesrudolph)
- Add support for Baseten as a new AI provider (PR #9461 by @AlexKer)
- Improve base OpenAI compatible provider with better error handling and configuration (PR #9462 by @mrubens)
- Add provider-oriented welcome screen to improve onboarding experience (PR #9484 by @mrubens)
- Pin Roo provider to the top of the provider list for better discoverability (PR #9485 by @mrubens)
- Enhance native tool descriptions with examples and clarifications for better AI understanding (PR #9486 by @daniel-lxs)
- Fix: Make cancel button immediately responsive during streaming (#9435 by @jwadow, PR #9448 by @daniel-lxs)
- Fix: Resolve apply_diff performance regression from earlier changes (PR #9474 by @daniel-lxs)
- Fix: Implement model cache refresh to prevent stale disk cache issues (PR #9478 by @daniel-lxs)
- Fix: Copy model-level capabilities to OpenRouter endpoint models correctly (PR #9483 by @daniel-lxs)
- Fix: Add fallback to yield tool calls regardless of finish_reason (PR #9476 by @daniel-lxs)
## [3.33.3] - 2025-11-20
![3.33.3 Release - Gemini 3 Pro Image Preview](/releases/3.33.3-release.png)
- Add Google Gemini 3 Pro Image Preview to image generation models (PR #9440 by @app/roomote)
- Add support for Minimax as Anthropic-compatible provider (PR #9455 by @daniel-lxs)
- Store reasoning in conversation history for all providers (PR #9451 by @daniel-lxs)
- Fix: Improve preserveReasoning flag to control API reasoning inclusion (PR #9453 by @daniel-lxs)
- Fix: Prevent OpenAI Native parallel tool calls for native tool calling (PR #9433 by @hannesrudolph)
- Fix: Improve search and replace symbol parsing (PR #9456 by @daniel-lxs)
- Fix: Send tool_result blocks for skipped tools in native protocol (PR #9457 by @daniel-lxs)
- Fix: Improve markdown formatting and add reasoning support (PR #9458 by @daniel-lxs)
- Fix: Prevent duplicate environment_details when resuming cancelled tasks (PR #9442 by @daniel-lxs)
- Improve read_file tool description with examples (PR #9422 by @daniel-lxs)
- Update glob dependency to ^11.1.0 (PR #9449 by @jr)
- Update tar-fs to 3.1.1 via pnpm override (PR #9450 by @app/roomote)
## [3.33.2] - 2025-11-19
- Enable native tool calling for Gemini provider (PR #9343 by @hannesrudolph)
- Add RCC credit balance display (PR #9386 by @jr)
- Fix: Preserve user images in native tool call results (PR #9401 by @daniel-lxs)
- Perf: Reduce excessive getModel() calls and implement disk cache fallback (PR #9410 by @daniel-lxs)
- Show zero price for free models (PR #9419 by @mrubens)
## [3.33.1] - 2025-11-18
![3.33.1 Release - Native Tool Protocol Fixes](/releases/3.33.1-release.png)
- Add native tool calling support to OpenAI-compatible (PR #9369 by @mrubens)
- Fix: Resolve native tool protocol race condition causing 400 errors (PR #9363 by @daniel-lxs)
- Fix: Update tools to return structured JSON for native protocol (PR #9373 by @daniel-lxs)
- Fix: Include nativeArgs in tool repetition detection (PR #9377 by @daniel-lxs)
- Fix: Ensure no XML parsing when protocol is native (PR #9371 by @daniel-lxs)
- Fix: Gemini maxOutputTokens and reasoning config (PR #9375 by @hannesrudolph)
- Fix: Gemini thought signature validation and token counting errors (PR #9380 by @hannesrudolph)
- Fix: Exclude XML tool examples from MODES section when native protocol enabled (PR #9367 by @daniel-lxs)
- Retry eval tasks if API instability detected (PR #9365 by @cte)
- Add toolProtocol property to PostHog tool usage telemetry (PR #9374 by @app/roomote)
## [3.33.0] - 2025-11-18
![3.33.0 Release - Twin Kangaroos and the Gemini Constellation](/releases/3.33.0-release.png)
- Add Gemini 3 Pro Preview model (PR #9357 by @hannesrudolph)
- Improve Google Gemini defaults with better temperature and cost reporting (PR #9327 by @hannesrudolph)
- Enable native tool calling for openai-native provider (PR #9348 by @hannesrudolph)
- Add git status information to environment details (PR #9310 by @daniel-lxs)
- Add tool protocol selector to advanced settings (PR #9324 by @daniel-lxs)
- Implement dynamic tool protocol resolution with proper precedence hierarchy (PR #9286 by @daniel-lxs)
- Move Import/Export functionality to Modes view toolbar and cleanup Mode Edit view (PR #9077 by @hannesrudolph)
- Update cloud agent CTA to point to setup page (PR #9338 by @app/roomote)
- Fix: Prevent duplicate tool_result blocks in native tool protocol (PR #9248 by @daniel-lxs)
- Fix: Format tool responses properly for native protocol (PR #9270 by @daniel-lxs)
- Fix: Centralize toolProtocol configuration checks (PR #9279 by @daniel-lxs)
- Fix: Preserve tool blocks for native protocol in conversation history (PR #9319 by @daniel-lxs)
- Fix: Prevent infinite loop when task_done succeeds (PR #9325 by @daniel-lxs)
- Fix: Sync parser state with profile/model changes (PR #9355 by @daniel-lxs)
- Fix: Pass tool protocol parameter to lineCountTruncationError (PR #9358 by @daniel-lxs)
- Use VSCode theme color for outline button borders (PR #9336 by @app/roomote)
- Replace broken badgen.net badges with shields.io (PR #9318 by @app/roomote)
- Add max git status files setting to evals (PR #9322 by @mrubens)
- Roo Code Cloud Provider pricing page and changes elsewhere (PR #9195 by @brunobergher)
## [3.32.1] - 2025-11-14
![3.32.1 Release - Bug Fixes](/releases/3.32.1-release.png)
- Fix: Add abort controller for request cancellation in OpenAI native protocol (PR #9276 by @daniel-lxs)
- Fix: Resolve duplicate tool blocks causing 'tool has already been used' error in native protocol mode (PR #9275 by @daniel-lxs)
- Fix: Prevent duplicate tool_result blocks in native protocol mode for read_file (PR #9272 by @daniel-lxs)
- Fix: Correct OpenAI Native handling of encrypted reasoning blocks to prevent errors during condensing (PR #9263 by @hannesrudolph)
- Fix: Disable XML parser for native tool protocol to prevent parsing conflicts (PR #9277 by @daniel-lxs)
## [3.32.0] - 2025-11-14
![3.32.0 Release - GPT-5.1 models and OpenAI prompt caching](/releases/3.32.0-release.png)
- Feature: Add GPT-5.1 models to OpenAI provider (PR #9252 by @hannesrudolph)
- Feature: Support for OpenAI Responses 24 hour prompt caching (PR #9259 by @hannesrudolph)
- Fix: Repair the share button in the UI (PR #9253 by @hannesrudolph)
- Docs: Include PR numbers in the release guide to improve traceability (PR #9236 by @hannesrudolph)
## [3.31.3] - 2025-11-13
![3.31.3 Release - Kangaroo Decrypting a Message](/releases/3.31.3-release.png)
- Fix: OpenAI Native encrypted_content handling and remove gpt-5-chat-latest verbosity flag (#9225 by @politsin, PR by @hannesrudolph)
- Fix: Roo Code Cloud provider Anthropic input token normalization to avoid double-counting (thanks @hannesrudolph!)
- Refactor: Rename sliding-window to context-management and truncateConversationIfNeeded to manageContext (thanks @hannesrudolph!)
## [3.31.2] - 2025-11-12
- Fix: Apply updated API profile settings when provider/model unchanged (#9208 by @hannesrudolph, PR by @hannesrudolph)
- Migrate conversation continuity to plugin-side encrypted reasoning items using Responses API for improved reliability (thanks @hannesrudolph!)
- Fix: Include mcpServers in getState() for auto-approval (#9190 by @bozoweed, PR by @daniel-lxs)
- Batch settings updates from the webview to the extension host for improved performance (thanks @cte!)
- Fix: Replace rate-limited badges with badgen.net to improve README reliability (thanks @daniel-lxs!)
## [3.31.1] - 2025-11-11
![3.31.1 Release - Kangaroo Stuck in the Clouds](/releases/3.31.1-release.png)
@ -206,7 +552,7 @@
## [3.28.11] - 2025-09-29
- Fix: Correct AWS Bedrock Claude Sonnet 4.5 model identifier (#8371 by @sunhyung, PR by @app/roomote)
- Fix: Correct Amazon Bedrock Claude Sonnet 4.5 model identifier (#8371 by @sunhyung, PR by @app/roomote)
- Fix: Correct Claude Sonnet 4.5 model ID format (thanks @daniel-lxs!)
## [3.28.10] - 2025-09-29
@ -538,7 +884,7 @@
## [3.25.14] - 2025-08-13
- Fix: Only include verbosity parameter for models that support it (#7054 by @eastonmeth, PR by @app/roomote)
- Fix: AWS Bedrock 1M context - Move anthropic_beta to additionalModelRequestFields (thanks @daniel-lxs!)
- Fix: Amazon Bedrock 1M context - Move anthropic_beta to additionalModelRequestFields (thanks @daniel-lxs!)
- Fix: Make cancelling requests more responsive by reverting recent changes
## [3.25.13] - 2025-08-12
@ -903,7 +1249,7 @@
- Add user-configurable search score threshold slider for semantic search (thanks @hannesrudolph!)
- Add default headers and testing for litellm fetcher (thanks @andrewshu2000!)
- Fix consistent cancellation error messages for thinking vs streaming phases
- Fix AWS Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
- Fix Amazon Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
- Fix URL loading timeout issues in @ mentions (thanks @MuriloFP!)
- Fix API retry exponential backoff capped at 10 minutes (thanks @MuriloFP!)
- Fix Qdrant URL field auto-filling with default value (thanks @SannidhyaSah!)
@ -917,7 +1263,7 @@
- Suppress Mermaid error rendering
- Improve Mermaid buttons with light background in light mode (thanks @chrarnoldus!)
- Add .vscode/ to write-protected files/directories
- Update AWS Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
- Update Amazon Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
## [3.22.5] - 2025-06-28
@ -1541,7 +1887,7 @@
- Improved display of diff errors + easy copying for investigation
- Fixes to .vscodeignore (thanks @franekp!)
- Fix a zh-CN translation for model capabilities (thanks @zhangtony239!)
- Rename AWS Bedrock to Amazon Bedrock (thanks @ronyblum!)
- Rename Amazon Bedrock to Amazon Bedrock (thanks @ronyblum!)
- Update extension title and description (thanks @StevenTCramer!)
## [3.11.12] - 2025-04-09
@ -1790,12 +2136,12 @@
- PowerShell-specific command handling (thanks @KJ7LNW!)
- OpenAI-compatible DeepSeek/QwQ reasoning support (thanks @lightrabbit!)
- Anthropic-style prompt caching in the OpenAI-compatible provider (thanks @dleen!)
- Add Deepseek R1 for AWS Bedrock (thanks @ATempsch!)
- Add Deepseek R1 for Amazon Bedrock (thanks @ATempsch!)
- Fix MarkdownBlock text color for Dark High Contrast theme (thanks @cannuri!)
- Add gemini-2.0-pro-exp-02-05 model to vertex (thanks @shohei-ihaya!)
- Bring back progress status for multi-diff edits (thanks @qdaxb!)
- Refactor alert dialog styles to use the correct vscode theme (thanks @cannuri!)
- Custom ARNs in AWS Bedrock (thanks @Smartsheet-JB-Brown!)
- Custom ARNs in Amazon Bedrock (thanks @Smartsheet-JB-Brown!)
- Update MCP servers directory path for platform compatibility (thanks @hannesrudolph!)
- Fix browser system prompt inclusion rules (thanks @cannuri!)
- Publish git tags to github from CI (thanks @pdecat!)
@ -1933,7 +2279,7 @@
## [3.7.1] - 2025-02-24
- Add AWS Bedrock support for Sonnet 3.7 and update some defaults to Sonnet 3.7 instead of 3.5
- Add Amazon Bedrock support for Sonnet 3.7 and update some defaults to Sonnet 3.7 instead of 3.5
## [3.7.0] - 2025-02-24
@ -1950,7 +2296,7 @@
## [3.3.24] - 2025-02-20
- Fixed a bug with region selection preventing AWS Bedrock profiles from being saved (thanks @oprstchn!)
- Fixed a bug with region selection preventing Amazon Bedrock profiles from being saved (thanks @oprstchn!)
- Updated the price of gpt-4o (thanks @marvijo-code!)
## [3.3.23] - 2025-02-20
@ -2134,7 +2480,7 @@
- Reverts provider key entry back to checking onInput instead of onChange to hopefully address issues entering API keys (thanks @samhvw8!)
- Added explicit checkbox to use Azure for OpenAI compatible providers (thanks @samhvw8!)
- Fixed Glama usage reporting (thanks @punkpeye!)
- Added Llama 3.3 70B Instruct model to the AWS Bedrock provider options (thanks @Premshay!)
- Added Llama 3.3 70B Instruct model to the Amazon Bedrock provider options (thanks @Premshay!)
## [3.2.7]

View file

@ -1,7 +1,5 @@
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://badgen.net/vs-marketplace/v/RooVeterinaryInc.roo-cline?label=VS%20Code&color=007ACC" alt="VS Code"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://badgen.net/vs-marketplace/i/RooVeterinaryInc.roo-cline?label=Installs&color=007ACC" alt="Installs"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://badgen.net/vs-marketplace/rating/RooVeterinaryInc.roo-cline?label=Rating&color=007ACC" alt="Rating"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
@ -37,7 +35,7 @@
- [简体中文](locales/zh-CN/README.md)
- [繁體中文](locales/zh-TW/README.md)
- ...
</details>
</details>
---
@ -169,16 +167,6 @@ We love community contributions! Get started by reading our [CONTRIBUTING.md](CO
---
## Contributors
Thanks to all our contributors who have helped make Roo Code better!
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
[![Contributors](https://contrib.rocks/image?repo=RooCodeInc/roo-code&max=120&columns=12&cacheBust=0000000000)](https://github.com/RooCodeInc/roo-code/graphs/contributors)
<!-- END CONTRIBUTORS SECTION -->
## License
[Apache 2.0 © 2025 Roo Code, Inc.](./LICENSE)

View file

@ -18,7 +18,7 @@
"@types/vscode": "^1.95.0",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.4.0",
"glob": "^11.0.1",
"glob": "^11.1.0",
"mocha": "^11.1.0",
"rimraf": "^6.0.1",
"typescript": "5.8.3"

View file

@ -15,8 +15,6 @@ suite("Roo Code Extension", function () {
"SidebarProvider.removeView",
"activationCompleted",
"plusButtonClicked",
"mcpButtonClicked",
"promptsButtonClicked",
"popoutButtonClicked",
"openInNewTab",
"settingsButtonClicked",

View file

@ -1,628 +0,0 @@
import * as assert from "assert"
import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
import { waitFor, sleep } from "../utils"
import { setDefaultSuiteTimeout } from "../test-utils"
suite.skip("Roo Code insert_content Tool", function () {
setDefaultSuiteTimeout(this)
let workspaceDir: string
// Pre-created test files that will be used across tests
const testFiles = {
simpleText: {
name: `test-insert-simple-${Date.now()}.txt`,
content: "Line 1\nLine 2\nLine 3",
path: "",
},
jsFile: {
name: `test-insert-js-${Date.now()}.js`,
content: `function hello() {
console.log("Hello World")
}
function goodbye() {
console.log("Goodbye World")
}`,
path: "",
},
emptyFile: {
name: `test-insert-empty-${Date.now()}.txt`,
content: "",
path: "",
},
pythonFile: {
name: `test-insert-python-${Date.now()}.py`,
content: `def main():
print("Start")
print("End")`,
path: "",
},
}
// Get the actual workspace directory that VSCode is using and create all test files
suiteSetup(async function () {
// Get the workspace folder from VSCode
const workspaceFolders = vscode.workspace.workspaceFolders
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error("No workspace folder found")
}
workspaceDir = workspaceFolders[0]!.uri.fsPath
console.log("Using workspace directory:", workspaceDir)
// Create all test files before any tests run
console.log("Creating test files in workspace...")
for (const [key, file] of Object.entries(testFiles)) {
file.path = path.join(workspaceDir, file.name)
await fs.writeFile(file.path, file.content)
console.log(`Created ${key} test file at:`, file.path)
}
// Verify all files exist
for (const [key, file] of Object.entries(testFiles)) {
const exists = await fs
.access(file.path)
.then(() => true)
.catch(() => false)
if (!exists) {
throw new Error(`Failed to create ${key} test file at ${file.path}`)
}
}
})
// Clean up after all tests
suiteTeardown(async () => {
// Cancel any running tasks before cleanup
test("Should insert content at the beginning of a file (line 1)", async function () {
const api = globalThis.api
// Clean up before each test
setup(async () => {
// Cancel any previous task
try {
await globalThis.api.cancelCurrentTask()
} catch {
// Task might not be running
}
// Small delay to ensure clean state
await sleep(100)
})
// Clean up after each test
teardown(async () => {
// Cancel the current task
try {
await globalThis.api.cancelCurrentTask()
} catch {
// Task might not be running
}
// Small delay to ensure clean state
await sleep(100)
})
const messages: ClineMessage[] = []
const testFile = testFiles.simpleText
const insertContent = "New first line"
const expectedContent = `${insertContent}
${testFile.content}`
let taskStarted = false
let taskCompleted = false
let errorOccurred: string | null = null
let insertContentExecuted = false
// Listen for messages
const messageHandler = ({ message }: { message: ClineMessage }) => {
messages.push(message)
// Log important messages for debugging
if (message.type === "say" && message.say === "error") {
errorOccurred = message.text || "Unknown error"
console.error("Error:", message.text)
}
if (message.type === "ask" && message.ask === "tool") {
console.log("Tool request:", message.text?.substring(0, 200))
}
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
console.log("AI response:", message.text?.substring(0, 200))
}
// Check for tool execution
if (message.type === "say" && message.say === "api_req_started" && message.text) {
console.log("API request started:", message.text.substring(0, 200))
try {
const requestData = JSON.parse(message.text)
if (requestData.request && requestData.request.includes("insert_content")) {
insertContentExecuted = true
console.log("insert_content tool executed!")
}
} catch (e) {
console.log("Failed to parse api_req_started message:", e)
}
}
}
api.on(RooCodeEventName.Message, messageHandler)
// Listen for task events
const taskStartedHandler = (id: string) => {
if (id === taskId) {
taskStarted = true
console.log("Task started:", id)
}
}
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
const taskCompletedHandler = (id: string) => {
if (id === taskId) {
taskCompleted = true
console.log("Task completed:", id)
}
}
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
let taskId: string
try {
// Start the task
taskId = await api.startNewTask({
configuration: {
mode: "code",
autoApprovalEnabled: true,
alwaysAllowWrite: true,
alwaysAllowReadOnly: true,
alwaysAllowReadOnlyOutsideWorkspace: true,
},
text: `Use insert_content to add "${insertContent}" at line 1 (beginning) of the file ${testFile.name}. The file already exists with this content:
${testFile.content}
Assume the file exists and you can modify it directly.`,
})
console.log("Task ID:", taskId)
console.log("Test filename:", testFile.name)
// Wait for task to start
await waitFor(() => taskStarted, { timeout: 45_000 })
// Check for early errors
if (errorOccurred) {
console.error("Early error detected:", errorOccurred)
}
// Wait for task completion
await waitFor(() => taskCompleted, { timeout: 45_000 })
// Give extra time for file system operations
await sleep(2000)
// Check if the file was modified correctly
const actualContent = await fs.readFile(testFile.path, "utf-8")
console.log("File content after insertion:", actualContent)
// Verify tool was executed
assert.strictEqual(insertContentExecuted, true, "insert_content tool should have been executed")
// Verify file content
assert.strictEqual(
actualContent.trim(),
expectedContent.trim(),
"Content should be inserted at the beginning of the file",
)
// Verify no errors occurred
assert.strictEqual(
errorOccurred,
null,
`Task should complete without errors, but got: ${errorOccurred}`,
)
console.log("Test passed! insert_content tool executed and content inserted at beginning successfully")
} finally {
api.off(RooCodeEventName.Message, messageHandler)
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
}
})
try {
await globalThis.api.cancelCurrentTask()
} catch {
// Task might not be running
}
// Clean up all test files
console.log("Cleaning up test files...")
for (const [key, file] of Object.entries(testFiles)) {
try {
await fs.unlink(file.path)
console.log(`Cleaned up ${key} test file`)
} catch (error) {
console.log(`Failed to clean up ${key} test file:`, error)
}
}
})
test("Should insert content at the end of a file (line 0)", async function () {
const api = globalThis.api
const messages: ClineMessage[] = []
const testFile = testFiles.simpleText
const insertContent = "New last line"
const expectedContent = `${testFile.content}
${insertContent}`
let taskStarted = false
let taskCompleted = false
let errorOccurred: string | null = null
let insertContentExecuted = false
// Listen for messages
const messageHandler = ({ message }: { message: ClineMessage }) => {
messages.push(message)
// Log important messages for debugging
if (message.type === "say" && message.say === "error") {
errorOccurred = message.text || "Unknown error"
console.error("Error:", message.text)
}
if (message.type === "ask" && message.ask === "tool") {
console.log("Tool request:", message.text?.substring(0, 200))
}
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
console.log("AI response:", message.text?.substring(0, 200))
}
// Check for tool execution
if (message.type === "say" && message.say === "api_req_started" && message.text) {
console.log("API request started:", message.text.substring(0, 200))
try {
const requestData = JSON.parse(message.text)
if (requestData.request && requestData.request.includes("insert_content")) {
insertContentExecuted = true
console.log("insert_content tool executed!")
}
} catch (e) {
console.log("Failed to parse api_req_started message:", e)
}
}
}
api.on(RooCodeEventName.Message, messageHandler)
// Listen for task events
const taskStartedHandler = (id: string) => {
if (id === taskId) {
taskStarted = true
console.log("Task started:", id)
}
}
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
const taskCompletedHandler = (id: string) => {
if (id === taskId) {
taskCompleted = true
console.log("Task completed:", id)
}
}
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
let taskId: string
try {
// Start the task
taskId = await api.startNewTask({
configuration: {
mode: "code",
autoApprovalEnabled: true,
alwaysAllowWrite: true,
alwaysAllowReadOnly: true,
alwaysAllowReadOnlyOutsideWorkspace: true,
},
text: `Use insert_content to add "${insertContent}" at line 0 (end of file) of the file ${testFile.name}. The file already exists with this content:
${testFile.content}
Assume the file exists and you can modify it directly.`,
})
console.log("Task ID:", taskId)
console.log("Test filename:", testFile.name)
// Wait for task to start
await waitFor(() => taskStarted, { timeout: 45_000 })
// Check for early errors
if (errorOccurred) {
console.error("Early error detected:", errorOccurred)
}
// Wait for task completion
await waitFor(() => taskCompleted, { timeout: 45_000 })
// Give extra time for file system operations
await sleep(2000)
// Check if the file was modified correctly
const actualContent = await fs.readFile(testFile.path, "utf-8")
console.log("File content after insertion:", actualContent)
// Verify tool was executed
test("Should insert multiline content into a JavaScript file", async function () {
const api = globalThis.api
const messages: ClineMessage[] = []
const testFile = testFiles.jsFile
const insertContent = `// New import statements
import { utils } from './utils'
import { helpers } from './helpers'`
const expectedContent = `${insertContent}
${testFile.content}`
let taskStarted = false
let taskCompleted = false
let errorOccurred: string | null = null
let insertContentExecuted = false
// Listen for messages
const messageHandler = ({ message }: { message: ClineMessage }) => {
messages.push(message)
// Log important messages for debugging
if (message.type === "say" && message.say === "error") {
errorOccurred = message.text || "Unknown error"
console.error("Error:", message.text)
}
if (message.type === "ask" && message.ask === "tool") {
console.log("Tool request:", message.text?.substring(0, 200))
}
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
console.log("AI response:", message.text?.substring(0, 200))
}
// Check for tool execution
if (message.type === "say" && message.say === "api_req_started" && message.text) {
console.log("API request started:", message.text.substring(0, 200))
try {
const requestData = JSON.parse(message.text)
if (requestData.request && requestData.request.includes("insert_content")) {
insertContentExecuted = true
console.log("insert_content tool executed!")
}
} catch (e) {
console.log("Failed to parse api_req_started message:", e)
}
}
}
api.on(RooCodeEventName.Message, messageHandler)
// Listen for task events
const taskStartedHandler = (id: string) => {
if (id === taskId) {
taskStarted = true
console.log("Task started:", id)
}
}
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
const taskCompletedHandler = (id: string) => {
if (id === taskId) {
taskCompleted = true
console.log("Task completed:", id)
}
}
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
let taskId: string
try {
// Start the task
taskId = await api.startNewTask({
configuration: {
mode: "code",
autoApprovalEnabled: true,
alwaysAllowWrite: true,
alwaysAllowReadOnly: true,
alwaysAllowReadOnlyOutsideWorkspace: true,
},
text: `Use insert_content to add import statements at the beginning (line 1) of the JavaScript file ${testFile.name}. Add these lines:
${insertContent}
The file already exists with this content:
${testFile.content}
Assume the file exists and you can modify it directly.`,
})
console.log("Task ID:", taskId)
console.log("Test filename:", testFile.name)
// Wait for task to start
await waitFor(() => taskStarted, { timeout: 45_000 })
// Check for early errors
if (errorOccurred) {
console.error("Early error detected:", errorOccurred)
}
// Wait for task completion
await waitFor(() => taskCompleted, { timeout: 45_000 })
// Give extra time for file system operations
await sleep(2000)
test("Should insert content into an empty file", async function () {
const api = globalThis.api
const messages: ClineMessage[] = []
const testFile = testFiles.emptyFile
const insertContent = `# My New File
This is the first line of content
And this is the second line`
const expectedContent = insertContent
let taskStarted = false
let taskCompleted = false
let errorOccurred: string | null = null
let insertContentExecuted = false
// Listen for messages
const messageHandler = ({ message }: { message: ClineMessage }) => {
messages.push(message)
// Log important messages for debugging
if (message.type === "say" && message.say === "error") {
errorOccurred = message.text || "Unknown error"
console.error("Error:", message.text)
}
if (message.type === "ask" && message.ask === "tool") {
console.log("Tool request:", message.text?.substring(0, 200))
}
if (
message.type === "say" &&
(message.say === "completion_result" || message.say === "text")
) {
console.log("AI response:", message.text?.substring(0, 200))
}
// Check for tool execution
if (message.type === "say" && message.say === "api_req_started" && message.text) {
console.log("API request started:", message.text.substring(0, 200))
try {
const requestData = JSON.parse(message.text)
if (requestData.request && requestData.request.includes("insert_content")) {
insertContentExecuted = true
console.log("insert_content tool executed!")
}
} catch (e) {
console.log("Failed to parse api_req_started message:", e)
}
}
}
api.on(RooCodeEventName.Message, messageHandler)
// Listen for task events
const taskStartedHandler = (id: string) => {
if (id === taskId) {
taskStarted = true
console.log("Task started:", id)
}
}
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
const taskCompletedHandler = (id: string) => {
if (id === taskId) {
taskCompleted = true
console.log("Task completed:", id)
}
}
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
let taskId: string
try {
// Start the task
taskId = await api.startNewTask({
configuration: {
mode: "code",
autoApprovalEnabled: true,
alwaysAllowWrite: true,
alwaysAllowReadOnly: true,
alwaysAllowReadOnlyOutsideWorkspace: true,
},
text: `Use insert_content to add content to the empty file ${testFile.name}. Add this content at line 0 (end of file):
${insertContent}
The file is currently empty. Assume the file exists and you can modify it directly.`,
})
console.log("Task ID:", taskId)
console.log("Test filename:", testFile.name)
// Wait for task to start
await waitFor(() => taskStarted, { timeout: 45_000 })
// Check for early errors
if (errorOccurred) {
console.error("Early error detected:", errorOccurred)
}
// Wait for task completion
await waitFor(() => taskCompleted, { timeout: 45_000 })
// Give extra time for file system operations
await sleep(2000)
// Check if the file was modified correctly
const actualContent = await fs.readFile(testFile.path, "utf-8")
console.log("File content after insertion:", actualContent)
// Verify tool was executed
assert.strictEqual(
insertContentExecuted,
true,
"insert_content tool should have been executed",
)
// Verify file content
assert.strictEqual(
actualContent.trim(),
expectedContent.trim(),
"Content should be inserted into the empty file",
)
// Verify no errors occurred
assert.strictEqual(
errorOccurred,
null,
`Task should complete without errors, but got: ${errorOccurred}`,
)
console.log(
"Test passed! insert_content tool executed and content inserted into empty file successfully",
)
} finally {
api.off(RooCodeEventName.Message, messageHandler)
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
}
})
// Check if the file was modified correctly
const actualContent = await fs.readFile(testFile.path, "utf-8")
console.log("File content after insertion:", actualContent)
// Verify tool was executed
assert.strictEqual(insertContentExecuted, true, "insert_content tool should have been executed")
// Verify file content
assert.strictEqual(
actualContent.trim(),
expectedContent.trim(),
"Multiline content should be inserted at the beginning of the JavaScript file",
)
// Verify no errors occurred
assert.strictEqual(
errorOccurred,
null,
`Task should complete without errors, but got: ${errorOccurred}`,
)
console.log("Test passed! insert_content tool executed and multiline content inserted successfully")
} finally {
api.off(RooCodeEventName.Message, messageHandler)
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
}
})
assert.strictEqual(insertContentExecuted, true, "insert_content tool should have been executed")
// Verify file content
assert.strictEqual(
actualContent.trim(),
expectedContent.trim(),
"Content should be inserted at the end of the file",
)
// Verify no errors occurred
assert.strictEqual(errorOccurred, null, `Task should complete without errors, but got: ${errorOccurred}`)
console.log("Test passed! insert_content tool executed and content inserted at end successfully")
} finally {
api.off(RooCodeEventName.Message, messageHandler)
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
}
})
// Tests will be added here one by one
})

View file

@ -14,6 +14,7 @@
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-checkbox": "^1.1.5",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.2",
@ -28,12 +29,13 @@
"@roo-code/evals": "workspace:^",
"@roo-code/types": "workspace:^",
"@tanstack/react-query": "^5.69.0",
"archiver": "^7.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.0",
"fuzzysort": "^3.1.0",
"lucide-react": "^0.518.0",
"next": "^15.2.5",
"next": "~15.2.6",
"next-themes": "^0.4.6",
"p-map": "^7.0.3",
"react": "^18.3.1",
@ -51,6 +53,7 @@
"@roo-code/config-eslint": "workspace:^",
"@roo-code/config-typescript": "workspace:^",
"@tailwindcss/postcss": "^4",
"@types/archiver": "^7.0.0",
"@types/ps-tree": "^1.1.6",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.5",

View file

@ -0,0 +1,207 @@
// npx vitest run src/actions/__tests__/killRun.spec.ts
import { execFileSync } from "child_process"
// Mock child_process
vi.mock("child_process", () => ({
execFileSync: vi.fn(),
spawn: vi.fn(),
}))
// Mock next/cache
vi.mock("next/cache", () => ({
revalidatePath: vi.fn(),
}))
// Mock redis client
vi.mock("@/lib/server/redis", () => ({
redisClient: vi.fn().mockResolvedValue({
del: vi.fn().mockResolvedValue(1),
}),
}))
// Mock @roo-code/evals
vi.mock("@roo-code/evals", () => ({
createRun: vi.fn(),
deleteRun: vi.fn(),
createTask: vi.fn(),
exerciseLanguages: [],
getExercisesForLanguage: vi.fn().mockResolvedValue([]),
}))
// Mock timers to speed up tests
vi.useFakeTimers()
// Import after mocks
import { killRun } from "../runs"
const mockExecFileSync = execFileSync as ReturnType<typeof vi.fn>
describe("killRun", () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllTimers()
})
it("should kill controller first, wait, then kill task containers", async () => {
const runId = 123
// execFileSync is used for all docker commands
mockExecFileSync
.mockReturnValueOnce("") // docker kill controller
.mockReturnValueOnce("evals-task-123-456.0\nevals-task-123-789.1\n") // docker ps
.mockReturnValueOnce("") // docker kill evals-task-123-456.0
.mockReturnValueOnce("") // docker kill evals-task-123-789.1
const resultPromise = killRun(runId)
// Fast-forward past the 10 second sleep
await vi.advanceTimersByTimeAsync(10000)
const result = await resultPromise
expect(result.success).toBe(true)
expect(result.killedContainers).toContain("evals-controller-123")
expect(result.killedContainers).toContain("evals-task-123-456.0")
expect(result.killedContainers).toContain("evals-task-123-789.1")
expect(result.errors).toHaveLength(0)
// Verify execFileSync was called for docker kill
expect(mockExecFileSync).toHaveBeenNthCalledWith(
1,
"docker",
["kill", "evals-controller-123"],
expect.any(Object),
)
// Verify execFileSync was called for docker ps with run-specific filter
expect(mockExecFileSync).toHaveBeenNthCalledWith(
2,
"docker",
["ps", "--format", "{{.Names}}", "--filter", "name=evals-task-123-"],
expect.any(Object),
)
})
it("should continue killing runners even if controller is not running", async () => {
const runId = 456
mockExecFileSync
.mockImplementationOnce(() => {
throw new Error("No such container")
}) // controller kill fails
.mockReturnValueOnce("evals-task-456-100.0\n") // docker ps
.mockReturnValueOnce("") // docker kill task
const resultPromise = killRun(runId)
await vi.advanceTimersByTimeAsync(10000)
const result = await resultPromise
expect(result.success).toBe(true)
expect(result.killedContainers).toContain("evals-task-456-100.0")
// Controller not in list since it failed
expect(result.killedContainers).not.toContain("evals-controller-456")
})
it("should clear Redis state after killing containers", async () => {
const runId = 789
const mockDel = vi.fn().mockResolvedValue(1)
const { redisClient } = await import("@/lib/server/redis")
vi.mocked(redisClient).mockResolvedValue({ del: mockDel } as never)
mockExecFileSync
.mockReturnValueOnce("") // controller kill
.mockReturnValueOnce("") // docker ps (no tasks)
const resultPromise = killRun(runId)
await vi.advanceTimersByTimeAsync(10000)
await resultPromise
expect(mockDel).toHaveBeenCalledWith("heartbeat:789")
expect(mockDel).toHaveBeenCalledWith("runners:789")
})
it("should handle docker ps failure gracefully", async () => {
const runId = 111
mockExecFileSync
.mockReturnValueOnce("") // controller kill succeeds
.mockImplementationOnce(() => {
throw new Error("Docker error")
}) // docker ps fails
const resultPromise = killRun(runId)
await vi.advanceTimersByTimeAsync(10000)
const result = await resultPromise
// Should still be successful because controller was killed
expect(result.success).toBe(true)
expect(result.killedContainers).toContain("evals-controller-111")
expect(result.errors).toContain("Failed to list Docker task containers")
})
it("should handle individual task kill failures", async () => {
const runId = 222
mockExecFileSync
.mockReturnValueOnce("") // controller kill
.mockReturnValueOnce("evals-task-222-300.0\nevals-task-222-400.0\n") // docker ps
.mockImplementationOnce(() => {
throw new Error("Kill failed")
}) // first task kill fails
.mockReturnValueOnce("") // second task kill succeeds
const resultPromise = killRun(runId)
await vi.advanceTimersByTimeAsync(10000)
const result = await resultPromise
expect(result.success).toBe(true)
expect(result.killedContainers).toContain("evals-controller-222")
expect(result.killedContainers).toContain("evals-task-222-400.0")
expect(result.errors.length).toBe(1)
expect(result.errors[0]).toContain("evals-task-222-300.0")
})
it("should return success with no containers when nothing is running", async () => {
const runId = 333
mockExecFileSync
.mockImplementationOnce(() => {
throw new Error("No such container")
}) // controller not running
.mockReturnValueOnce("") // no task containers
const resultPromise = killRun(runId)
await vi.advanceTimersByTimeAsync(10000)
const result = await resultPromise
expect(result.success).toBe(true)
expect(result.killedContainers).toHaveLength(0)
expect(result.errors).toHaveLength(0)
})
it("should only kill containers belonging to the specific run", async () => {
const runId = 555
mockExecFileSync
.mockReturnValueOnce("") // controller kill
.mockReturnValueOnce("evals-task-555-100.0\n") // docker ps
.mockReturnValueOnce("") // docker kill task
const resultPromise = killRun(runId)
await vi.advanceTimersByTimeAsync(10000)
const result = await resultPromise
expect(result.success).toBe(true)
// Verify execFileSync was called for docker ps with run-specific filter
expect(mockExecFileSync).toHaveBeenNthCalledWith(
2,
"docker",
["ps", "--format", "{{.Names}}", "--filter", "name=evals-task-555-"],
expect.any(Object),
)
})
})

View file

@ -3,7 +3,7 @@
import * as path from "path"
import fs from "fs"
import { fileURLToPath } from "url"
import { spawn } from "child_process"
import { spawn, execFileSync } from "child_process"
import { revalidatePath } from "next/cache"
import pMap from "p-map"
@ -13,16 +13,22 @@ import {
exerciseLanguages,
createRun as _createRun,
deleteRun as _deleteRun,
updateRun as _updateRun,
getIncompleteRuns as _getIncompleteRuns,
deleteRunsByIds as _deleteRunsByIds,
createTask,
getExercisesForLanguage,
} from "@roo-code/evals"
import { CreateRun } from "@/lib/schemas"
import { redisClient } from "@/lib/server/redis"
// Storage base path for eval logs
const EVALS_STORAGE_PATH = "/tmp/evals/runs"
const EVALS_REPO_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../../evals")
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function createRun({ suite, exercises = [], systemPrompt, timeout, ...values }: CreateRun) {
export async function createRun({ suite, exercises = [], timeout, iterations = 1, ...values }: CreateRun) {
const run = await _createRun({
...values,
timeout,
@ -37,15 +43,34 @@ export async function createRun({ suite, exercises = [], systemPrompt, timeout,
throw new Error("Invalid exercise path: " + path)
}
await createTask({ ...values, runId: run.id, language: language as ExerciseLanguage, exercise })
// Create multiple tasks for each iteration
for (let iteration = 1; iteration <= iterations; iteration++) {
await createTask({
...values,
runId: run.id,
language: language as ExerciseLanguage,
exercise,
iteration,
})
}
}
} else {
for (const language of exerciseLanguages) {
const exercises = await getExercisesForLanguage(EVALS_REPO_PATH, language)
const languageExercises = await getExercisesForLanguage(EVALS_REPO_PATH, language)
await pMap(exercises, (exercise) => createTask({ runId: run.id, language, exercise }), {
concurrency: 10,
})
// Create tasks for all iterations of each exercise
const tasksToCreate: Array<{ language: ExerciseLanguage; exercise: string; iteration: number }> = []
for (const exercise of languageExercises) {
for (let iteration = 1; iteration <= iterations; iteration++) {
tasksToCreate.push({ language, exercise, iteration })
}
}
await pMap(
tasksToCreate,
({ language, exercise, iteration }) => createTask({ runId: run.id, language, exercise, iteration }),
{ concurrency: 10 },
)
}
}
@ -98,3 +123,247 @@ export async function deleteRun(runId: number) {
await _deleteRun(runId)
revalidatePath("/runs")
}
export type KillRunResult = {
success: boolean
killedContainers: string[]
errors: string[]
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
/**
* Kill all Docker containers associated with a run (controller and task runners).
* Kills the controller first, waits 10 seconds, then kills runners.
* Also clears Redis state for heartbeat and runners.
*
* Container naming conventions:
* - Controller: evals-controller-{runId}
* - Task runners: evals-task-{runId}-{taskId}.{attempt}
*/
export async function killRun(runId: number): Promise<KillRunResult> {
const killedContainers: string[] = []
const errors: string[] = []
const controllerPattern = `evals-controller-${runId}`
const taskPattern = `evals-task-${runId}-`
try {
// Step 1: Kill the controller first
console.log(`Killing controller: ${controllerPattern}`)
try {
execFileSync("docker", ["kill", controllerPattern], { encoding: "utf-8", timeout: 10000 })
killedContainers.push(controllerPattern)
console.log(`Killed controller container: ${controllerPattern}`)
} catch (_error) {
// Controller might not be running - that's ok, continue to kill runners
console.log(`Controller ${controllerPattern} not running or already stopped`)
}
// Step 2: Wait 10 seconds before killing runners
console.log("Waiting 10 seconds before killing runners...")
await sleep(10000)
// Step 3: Find and kill all task runner containers for THIS run only
let taskContainerNames: string[] = []
try {
const output = execFileSync("docker", ["ps", "--format", "{{.Names}}", "--filter", `name=${taskPattern}`], {
encoding: "utf-8",
timeout: 10000,
})
taskContainerNames = output
.split("\n")
.map((name) => name.trim())
.filter((name) => name.length > 0 && name.startsWith(taskPattern))
} catch (error) {
console.error("Failed to list task containers:", error)
errors.push("Failed to list Docker task containers")
}
// Kill each task runner container
for (const containerName of taskContainerNames) {
try {
execFileSync("docker", ["kill", containerName], { encoding: "utf-8", timeout: 10000 })
killedContainers.push(containerName)
console.log(`Killed task container: ${containerName}`)
} catch (error) {
// Container might have already stopped
console.error(`Failed to kill container ${containerName}:`, error)
errors.push(`Failed to kill container: ${containerName}`)
}
}
// Step 4: Clear Redis state
try {
const redis = await redisClient()
const heartbeatKey = `heartbeat:${runId}`
const runnersKey = `runners:${runId}`
await redis.del(heartbeatKey)
await redis.del(runnersKey)
console.log(`Cleared Redis keys: ${heartbeatKey}, ${runnersKey}`)
} catch (error) {
console.error("Failed to clear Redis state:", error)
errors.push("Failed to clear Redis state")
}
} catch (error) {
console.error("Error in killRun:", error)
errors.push("Unexpected error while killing containers")
}
revalidatePath(`/runs/${runId}`)
revalidatePath("/runs")
return {
success: killedContainers.length > 0 || errors.length === 0,
killedContainers,
errors,
}
}
export type DeleteIncompleteRunsResult = {
success: boolean
deletedCount: number
deletedRunIds: number[]
storageErrors: string[]
}
/**
* Delete all incomplete runs (runs without a taskMetricsId/final score).
* Removes both database records and storage folders.
*/
export async function deleteIncompleteRuns(): Promise<DeleteIncompleteRunsResult> {
const storageErrors: string[] = []
// Get all incomplete runs
const incompleteRuns = await _getIncompleteRuns()
const runIds = incompleteRuns.map((run) => run.id)
if (runIds.length === 0) {
return {
success: true,
deletedCount: 0,
deletedRunIds: [],
storageErrors: [],
}
}
// Delete storage folders for each run
for (const runId of runIds) {
const storagePath = path.join(EVALS_STORAGE_PATH, String(runId))
try {
if (fs.existsSync(storagePath)) {
fs.rmSync(storagePath, { recursive: true, force: true })
console.log(`Deleted storage folder: ${storagePath}`)
}
} catch (error) {
console.error(`Failed to delete storage folder ${storagePath}:`, error)
storageErrors.push(`Failed to delete storage for run ${runId}`)
}
// Also try to clear Redis state for any potentially running incomplete runs
try {
const redis = await redisClient()
await redis.del(`heartbeat:${runId}`)
await redis.del(`runners:${runId}`)
} catch (error) {
// Non-critical error, just log it
console.error(`Failed to clear Redis state for run ${runId}:`, error)
}
}
// Delete from database
await _deleteRunsByIds(runIds)
revalidatePath("/runs")
return {
success: true,
deletedCount: runIds.length,
deletedRunIds: runIds,
storageErrors,
}
}
/**
* Get count of incomplete runs (for UI display)
*/
export async function getIncompleteRunsCount(): Promise<number> {
const incompleteRuns = await _getIncompleteRuns()
return incompleteRuns.length
}
/**
* Delete all runs older than 30 days.
* Removes both database records and storage folders.
*/
export async function deleteOldRuns(): Promise<DeleteIncompleteRunsResult> {
const storageErrors: string[] = []
// Get all runs older than 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
const { getRuns } = await import("@roo-code/evals")
const allRuns = await getRuns()
const oldRuns = allRuns.filter((run) => run.createdAt < thirtyDaysAgo)
const runIds = oldRuns.map((run) => run.id)
if (runIds.length === 0) {
return {
success: true,
deletedCount: 0,
deletedRunIds: [],
storageErrors: [],
}
}
// Delete storage folders for each run
for (const runId of runIds) {
const storagePath = path.join(EVALS_STORAGE_PATH, String(runId))
try {
if (fs.existsSync(storagePath)) {
fs.rmSync(storagePath, { recursive: true, force: true })
console.log(`Deleted storage folder: ${storagePath}`)
}
} catch (error) {
console.error(`Failed to delete storage folder ${storagePath}:`, error)
storageErrors.push(`Failed to delete storage for run ${runId}`)
}
// Also try to clear Redis state
try {
const redis = await redisClient()
await redis.del(`heartbeat:${runId}`)
await redis.del(`runners:${runId}`)
} catch (error) {
// Non-critical error, just log it
console.error(`Failed to clear Redis state for run ${runId}:`, error)
}
}
// Delete from database
await _deleteRunsByIds(runIds)
revalidatePath("/runs")
return {
success: true,
deletedCount: runIds.length,
deletedRunIds: runIds,
storageErrors,
}
}
/**
* Update the description of a run.
*/
export async function updateRunDescription(runId: number, description: string | null): Promise<{ success: boolean }> {
try {
await _updateRun(runId, { description })
revalidatePath("/runs")
revalidatePath(`/runs/${runId}`)
return { success: true }
} catch (error) {
console.error("Failed to update run description:", error)
return { success: false }
}
}

View file

@ -1,24 +0,0 @@
import { NextResponse } from "next/server"
export async function GET() {
try {
return NextResponse.json(
{
status: "healthy",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || "production",
},
{ status: 200 },
)
} catch (error) {
return NextResponse.json(
{
status: "unhealthy",
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : "Unknown error",
},
{ status: 503 },
)
}
}

View file

@ -0,0 +1,74 @@
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import * as fs from "node:fs/promises"
import * as path from "node:path"
import { findTask, findRun } from "@roo-code/evals"
export const dynamic = "force-dynamic"
const LOG_BASE_PATH = "/tmp/evals/runs"
// Sanitize path components to prevent path traversal attacks
function sanitizePathComponent(component: string): string {
// Remove any path separators, null bytes, and other dangerous characters
return component.replace(/[/\\:\0*?"<>|]/g, "_")
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string; taskId: string }> }) {
const { id, taskId } = await params
try {
const runId = Number(id)
const taskIdNum = Number(taskId)
if (isNaN(runId) || isNaN(taskIdNum)) {
return NextResponse.json({ error: "Invalid run ID or task ID" }, { status: 400 })
}
// Verify the run exists
await findRun(runId)
// Get the task to find its language and exercise
const task = await findTask(taskIdNum)
// Verify the task belongs to this run
if (task.runId !== runId) {
return NextResponse.json({ error: "Task does not belong to this run" }, { status: 404 })
}
// Sanitize language and exercise to prevent path traversal
const safeLanguage = sanitizePathComponent(task.language)
const safeExercise = sanitizePathComponent(task.exercise)
// Construct the log file path
const logFileName = `${safeLanguage}-${safeExercise}.log`
const logFilePath = path.join(LOG_BASE_PATH, String(runId), logFileName)
// Verify the resolved path is within the expected directory (defense in depth)
const resolvedPath = path.resolve(logFilePath)
const expectedBase = path.resolve(LOG_BASE_PATH)
if (!resolvedPath.startsWith(expectedBase)) {
return NextResponse.json({ error: "Invalid log path" }, { status: 400 })
}
// Check if the log file exists and read it (async)
try {
const logContent = await fs.readFile(logFilePath, "utf-8")
return NextResponse.json({ logContent })
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
return NextResponse.json({ error: "Log file not found", logContent: null }, { status: 200 })
}
throw err
}
} catch (error) {
console.error("Error reading task log:", error)
if (error instanceof Error && error.name === "RecordNotFoundError") {
return NextResponse.json({ error: "Task or run not found" }, { status: 404 })
}
return NextResponse.json({ error: "Failed to read log file" }, { status: 500 })
}
}

View file

@ -0,0 +1,129 @@
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import * as fs from "node:fs"
import * as path from "node:path"
import archiver from "archiver"
import { findRun, getTasks } from "@roo-code/evals"
export const dynamic = "force-dynamic"
const LOG_BASE_PATH = "/tmp/evals/runs"
// Sanitize path components to prevent path traversal attacks
function sanitizePathComponent(component: string): string {
// Remove any path separators, null bytes, and other dangerous characters
return component.replace(/[/\\:\0*?"<>|]/g, "_")
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
try {
const runId = Number(id)
if (isNaN(runId)) {
return NextResponse.json({ error: "Invalid run ID" }, { status: 400 })
}
// Verify the run exists
await findRun(runId)
// Get all tasks for this run
const tasks = await getTasks(runId)
// Filter for failed tasks only
const failedTasks = tasks.filter((task) => task.passed === false)
if (failedTasks.length === 0) {
return NextResponse.json({ error: "No failed tasks to export" }, { status: 400 })
}
// Create a zip archive
const archive = archiver("zip", { zlib: { level: 9 } })
// Collect chunks to build the response
const chunks: Buffer[] = []
archive.on("data", (chunk: Buffer) => {
chunks.push(chunk)
})
// Track archive errors
let archiveError: Error | null = null
archive.on("error", (err: Error) => {
archiveError = err
})
// Set up the end promise before finalizing (proper event listener ordering)
const archiveEndPromise = new Promise<void>((resolve, reject) => {
archive.on("end", resolve)
archive.on("error", reject)
})
// Add each failed task's log file to the archive
const logDir = path.join(LOG_BASE_PATH, String(runId))
let filesAdded = 0
for (const task of failedTasks) {
// Sanitize language and exercise to prevent path traversal
const safeLanguage = sanitizePathComponent(task.language)
const safeExercise = sanitizePathComponent(task.exercise)
const logFileName = `${safeLanguage}-${safeExercise}.log`
const logFilePath = path.join(logDir, logFileName)
// Verify the resolved path is within the expected directory (defense in depth)
const resolvedPath = path.resolve(logFilePath)
const expectedBase = path.resolve(LOG_BASE_PATH)
if (!resolvedPath.startsWith(expectedBase)) {
continue // Skip files with suspicious paths
}
if (fs.existsSync(logFilePath)) {
archive.file(logFilePath, { name: logFileName })
filesAdded++
}
}
// Check if any files were actually added
if (filesAdded === 0) {
archive.abort()
return NextResponse.json(
{ error: "No log files found - they may have been cleared from disk" },
{ status: 404 },
)
}
// Finalize the archive
await archive.finalize()
// Wait for all data to be collected
await archiveEndPromise
// Check for archive errors
if (archiveError) {
throw archiveError
}
// Combine all chunks into a single buffer
const zipBuffer = Buffer.concat(chunks)
// Return the zip file
return new NextResponse(zipBuffer, {
status: 200,
headers: {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="run-${runId}-failed-logs.zip"`,
"Content-Length": String(zipBuffer.length),
},
})
} catch (error) {
console.error("Error exporting failed logs:", error)
if (error instanceof Error && error.name === "RecordNotFoundError") {
return NextResponse.json({ error: "Run not found" }, { status: 404 })
}
return NextResponse.json({ error: "Failed to export logs" }, { status: 500 })
}
}

View file

@ -7,7 +7,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
const run = await findRun(Number(id))
return (
<div className="max-w-3xl mx-auto px-12 p-12">
<div className="w-full px-6 py-12">
<Run run={run} />
</div>
)

View file

@ -1,55 +1,79 @@
"use client"
import { Link2, Link2Off, CheckCircle2 } from "lucide-react"
import type { RunStatus as _RunStatus } from "@/hooks/use-run-status"
import { cn } from "@/lib/utils"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui"
export const RunStatus = ({ runStatus: { sseStatus, heartbeat, runners = [] } }: { runStatus: _RunStatus }) => (
<div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<div>Task Stream:</div>
<div className="font-mono text-sm text-muted-foreground">{sseStatus}</div>
</div>
<div className="relative">
<div
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
"bg-green-500": sseStatus === "connected",
"bg-amber-500": sseStatus === "waiting",
"bg-rose-500": sseStatus === "error",
})}
/>
<div
className={cn("size-2.5 rounded-full", {
"bg-green-500": sseStatus === "connected",
"bg-amber-500": sseStatus === "waiting",
"bg-rose-500": sseStatus === "error",
})}
/>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<div>Task Controller:</div>
<div className="font-mono text-sm text-muted-foreground">{heartbeat ?? "dead"}</div>
</div>
<div className="relative">
<div
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
"bg-green-500": !!heartbeat,
"bg-rose-500": !heartbeat,
})}
/>
<div
className={cn("size-2.5 rounded-full", {
"bg-green-500": !!heartbeat,
"bg-rose-500": !heartbeat,
})}
/>
</div>
</div>
<div className="flex items-center gap-2">
<div>Task Runners:</div>
{runners.length > 0 && <div className="font-mono text-sm text-muted-foreground">{runners?.join(", ")}</div>}
</div>
</div>
)
function StreamIcon({ status }: { status: "connected" | "waiting" | "error" }) {
if (status === "connected") {
return <Link2 className="size-4 text-green-500" />
}
return <Link2Off className={cn("size-4", status === "waiting" ? "text-amber-500" : "text-rose-500")} />
}
export const RunStatus = ({
runStatus: { sseStatus, heartbeat, runners = [] },
isComplete = false,
}: {
runStatus: _RunStatus
isComplete?: boolean
}) => {
// For completed runs, show a simple "Complete" badge
if (isComplete) {
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 cursor-default text-muted-foreground">
<CheckCircle2 className="size-4" />
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="font-mono text-xs">
Run complete
</TooltipContent>
</Tooltip>
)
}
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2 cursor-default text-xs font-mono">
{/* Task Stream status icon */}
<StreamIcon status={sseStatus} />
{/* Task Controller ID */}
<span className={heartbeat ? "text-green-500" : "text-rose-500"}>{heartbeat ?? "-"}</span>
{/* Task Runners count */}
<span className={runners.length > 0 ? "text-green-500" : "text-rose-500"}>
{runners.length > 0 ? `${runners.length}r` : "0r"}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="font-mono text-xs max-w-md">
<div className="space-y-1">
<div className="flex items-center gap-2">
<StreamIcon status={sseStatus} />
<span>Task Stream: {sseStatus}</span>
</div>
<div className="flex items-center gap-2">
<span className={heartbeat ? "text-green-500" : "text-rose-500"}></span>
<span>Task Controller: {heartbeat ?? "dead"}</span>
</div>
<div className="flex items-center gap-2">
<span className={runners.length > 0 ? "text-green-500" : "text-rose-500"}></span>
<span>Task Runners: {runners.length > 0 ? runners.length : "none"}</span>
</div>
{runners.length > 0 && (
<div className="mt-2 pt-2 border-t border-border text-muted-foreground space-y-0.5">
{runners.map((runner) => (
<div key={runner}>{runner}</div>
))}
</div>
)}
</div>
</TooltipContent>
</Tooltip>
)
}

View file

@ -1,56 +1,642 @@
"use client"
import { useMemo } from "react"
import { LoaderCircle } from "lucide-react"
import { useMemo, useState, useCallback, useEffect } from "react"
import { toast } from "sonner"
import { LoaderCircle, FileText, Copy, Check, StopCircle } from "lucide-react"
import type { Run, TaskMetrics as _TaskMetrics } from "@roo-code/evals"
import type { Run, TaskMetrics as _TaskMetrics, Task } from "@roo-code/evals"
import type { ToolName } from "@roo-code/types"
import { formatCurrency, formatDuration, formatTokens } from "@/lib/formatters"
import { formatCurrency, formatDuration, formatTokens, formatToolUsageSuccessRate } from "@/lib/formatters"
import { useRunStatus } from "@/hooks/use-run-status"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
import { killRun } from "@/actions/runs"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
ScrollArea,
Button,
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui"
import { TaskStatus } from "./task-status"
import { RunStatus } from "./run-status"
type TaskMetrics = Pick<_TaskMetrics, "tokensIn" | "tokensOut" | "tokensContext" | "duration" | "cost">
type ToolUsageEntry = { attempts: number; failures: number }
type ToolUsage = Record<string, ToolUsageEntry>
// Generate abbreviation from tool name (e.g., "read_file" -> "RF", "list_code_definition_names" -> "LCDN")
function getToolAbbreviation(toolName: string): string {
return toolName
.split("_")
.map((word) => word[0]?.toUpperCase() ?? "")
.join("")
}
// Pattern definitions for syntax highlighting
type HighlightPattern = {
pattern: RegExp
className: string
// If true, wraps the entire match; if a number, wraps that capture group
wrapGroup?: number
}
const HIGHLIGHT_PATTERNS: HighlightPattern[] = [
// Log levels - styled as badges
{ pattern: /\|\s*(INFO)\s*\|/g, className: "text-green-400", wrapGroup: 1 },
{ pattern: /\|\s*(WARN|WARNING)\s*\|/g, className: "text-yellow-400", wrapGroup: 1 },
{ pattern: /\|\s*(ERROR)\s*\|/g, className: "text-red-400 font-semibold", wrapGroup: 1 },
{ pattern: /\|\s*(DEBUG)\s*\|/g, className: "text-gray-400", wrapGroup: 1 },
// Task identifiers - important events
{
pattern: /(taskCreated|taskFocused|taskStarted|taskCompleted|taskAborted|taskResumable)/g,
className: "text-purple-400 font-medium",
},
// Tool failures - highlight in red
{ pattern: /(taskToolFailed)/g, className: "text-red-400 font-bold" },
{ pattern: /(Tool execution failed|tool.*failed|failed.*tool)/gi, className: "text-red-400" },
{ pattern: /(EvalPass)/g, className: "text-green-400 font-bold" },
{ pattern: /(EvalFail)/g, className: "text-red-400 font-bold" },
// Message arrows
{ pattern: /→/g, className: "text-cyan-400" },
// Tool names in quotes
{ pattern: /"(tool)":\s*"([^"]+)"/g, className: "text-orange-400" },
// JSON keys
{ pattern: /"([^"]+)":/g, className: "text-sky-300" },
// Boolean values
{ pattern: /:\s*(true|false)/g, className: "text-amber-400", wrapGroup: 1 },
// Numbers
{ pattern: /:\s*(-?\d+\.?\d*)/g, className: "text-emerald-400", wrapGroup: 1 },
]
// Extract timestamp from a log line and return elapsed time from baseline
function formatElapsedTime(timestamp: string, baselineMs: number): string {
const currentMs = new Date(timestamp).getTime()
const elapsedMs = currentMs - baselineMs
const totalSeconds = Math.floor(elapsedMs / 1000)
const minutes = Math.floor(totalSeconds / 60)
const seconds = totalSeconds % 60
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
}
// Extract the first timestamp from the log to use as baseline
function extractFirstTimestamp(log: string): number | null {
// Match timestamp at start of line: [2025-11-28T09:35:23.187Z | ... or [2025-11-28T09:35:23.187Z]
const match = log.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)[\s|\]]/)
const isoString = match?.[1]
if (!isoString) return null
return new Date(isoString).getTime()
}
// Simplify log line by removing redundant metadata
function simplifyLogLine(line: string, baselineMs: number | null): { timestamp: string; simplified: string } {
// Extract timestamp - matches [2025-11-28T09:35:23.187Z | ... format
const timestampMatch = line.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)[\s|\]]/)
const isoTimestamp = timestampMatch?.[1]
if (!isoTimestamp) {
return { timestamp: "", simplified: line }
}
const timestamp = baselineMs !== null ? formatElapsedTime(isoTimestamp, baselineMs) : isoTimestamp.slice(11, 19)
// Remove the timestamp from the line (handles both [timestamp] and [timestamp | formats)
let simplified = line.replace(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\s*\|?\s*/, "")
// Remove redundant metadata: pid, run, task IDs (they're same for entire log)
simplified = simplified.replace(/\|\s*pid:\d+\s*/g, "")
simplified = simplified.replace(/\|\s*run:\d+\s*/g, "")
simplified = simplified.replace(/\|\s*task:\d+\s*/g, "")
simplified = simplified.replace(/runTask\s*\|\s*/g, "")
// Clean up extra pipes, spaces, and trailing brackets
simplified = simplified.replace(/\|\s*\|/g, "|")
simplified = simplified.replace(/^\s*\|\s*/, "")
simplified = simplified.replace(/\]\s*$/, "") // Remove trailing bracket if present
return { timestamp, simplified }
}
// Format a single line with syntax highlighting using React elements (XSS-safe)
function formatLine(line: string): React.ReactNode[] {
// Find all matches with their positions
type Match = { start: number; end: number; text: string; className: string }
const matches: Match[] = []
for (const { pattern, className, wrapGroup } of HIGHLIGHT_PATTERNS) {
// Reset regex state
pattern.lastIndex = 0
let regexMatch
while ((regexMatch = pattern.exec(line)) !== null) {
const capturedText = wrapGroup !== undefined ? regexMatch[wrapGroup] : regexMatch[0]
// Skip if capture group didn't match
if (!capturedText) continue
const start =
wrapGroup !== undefined ? regexMatch.index + regexMatch[0].indexOf(capturedText) : regexMatch.index
matches.push({
start,
end: start + capturedText.length,
text: capturedText,
className,
})
}
}
// Sort matches by position and filter overlapping ones
matches.sort((a, b) => a.start - b.start)
const filteredMatches: Match[] = []
for (const m of matches) {
const lastMatch = filteredMatches[filteredMatches.length - 1]
if (!lastMatch || m.start >= lastMatch.end) {
filteredMatches.push(m)
}
}
// Build result with highlighted spans
const result: React.ReactNode[] = []
let currentPos = 0
for (const [i, m] of filteredMatches.entries()) {
// Add text before this match
if (m.start > currentPos) {
result.push(line.slice(currentPos, m.start))
}
// Add highlighted match
result.push(
<span key={`${i}-${m.start}`} className={m.className}>
{m.text}
</span>,
)
currentPos = m.end
}
// Add remaining text
if (currentPos < line.length) {
result.push(line.slice(currentPos))
}
return result.length > 0 ? result : [line]
}
// Determine the visual style for a log line based on its content
function getLineStyle(line: string): string {
if (line.includes("ERROR")) return "bg-red-950/30 border-l-2 border-red-500"
if (line.includes("WARN") || line.includes("WARNING")) return "bg-yellow-950/20 border-l-2 border-yellow-500"
if (line.includes("taskToolFailed")) return "bg-red-950/30 border-l-2 border-red-500"
if (line.includes("taskStarted") || line.includes("taskCreated")) return "bg-purple-950/20"
if (line.includes("EvalPass")) return "bg-green-950/30 border-l-2 border-green-500"
if (line.includes("EvalFail")) return "bg-red-950/30 border-l-2 border-red-500"
if (line.includes("taskCompleted") || line.includes("taskAborted")) return "bg-blue-950/20"
return ""
}
// Format log content with basic highlighting (XSS-safe - no dangerouslySetInnerHTML)
function formatLogContent(log: string): React.ReactNode[] {
const lines = log.split("\n")
const baselineMs = extractFirstTimestamp(log)
return lines.map((line, index) => {
if (!line.trim()) {
return (
<div key={index} className="h-2">
{" "}
</div>
)
}
const parsed = simplifyLogLine(line, baselineMs)
const lineStyle = getLineStyle(line)
return (
<div key={index} className={`flex hover:bg-white/10 py-0.5 rounded-sm transition-colors ${lineStyle}`}>
{/* Elapsed time */}
<span className="text-blue-400 font-mono w-12 flex-shrink-0 tabular-nums text-right pr-2">
{parsed.timestamp}
</span>
{/* Log content - pl-12 ensures wrapped lines are indented under the timestamp */}
<span className="flex-1 break-words" style={{ textIndent: "-0.5rem", paddingLeft: "0.5rem" }}>
{formatLine(parsed.simplified)}
</span>
</div>
)
})
}
export function Run({ run }: { run: Run }) {
const runStatus = useRunStatus(run)
const { tasks, tokenUsage, usageUpdatedAt } = runStatus
const { tasks, tokenUsage, toolUsage, usageUpdatedAt, heartbeat, runners } = runStatus
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
const [taskLog, setTaskLog] = useState<string | null>(null)
const [isLoadingLog, setIsLoadingLog] = useState(false)
const [copied, setCopied] = useState(false)
const [showKillDialog, setShowKillDialog] = useState(false)
const [isKilling, setIsKilling] = useState(false)
// Determine if run is still active (has heartbeat or runners)
const isRunActive = !run.taskMetricsId && (!!heartbeat || (runners && runners.length > 0))
const onKillRun = useCallback(async () => {
setIsKilling(true)
try {
const result = await killRun(run.id)
if (result.killedContainers.length > 0) {
toast.success(`Killed ${result.killedContainers.length} container(s)`)
} else if (result.errors.length === 0) {
toast.info("No running containers found")
} else {
toast.error(result.errors.join(", "))
}
} catch (error) {
console.error("Failed to kill run:", error)
toast.error("Failed to kill run")
} finally {
setIsKilling(false)
setShowKillDialog(false)
}
}, [run.id])
const onCopyLog = useCallback(async () => {
if (!taskLog) return
try {
await navigator.clipboard.writeText(taskLog)
setCopied(true)
toast.success("Log copied to clipboard")
setTimeout(() => setCopied(false), 2000)
} catch (error) {
console.error("Failed to copy log:", error)
toast.error("Failed to copy log")
}
}, [taskLog])
// Handle ESC key to close the dialog
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && selectedTask) {
setSelectedTask(null)
}
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [selectedTask])
const onViewTaskLog = useCallback(
async (task: Task) => {
// Only allow viewing logs for tasks that have started
if (!task.startedAt && !tokenUsage.get(task.id)) {
toast.error("Task has not started yet")
return
}
setSelectedTask(task)
setIsLoadingLog(true)
setTaskLog(null)
try {
const response = await fetch(`/api/runs/${run.id}/logs/${task.id}`)
if (!response.ok) {
const error = await response.json()
toast.error(error.error || "Failed to load log")
setSelectedTask(null)
return
}
const data = await response.json()
setTaskLog(data.logContent)
} catch (error) {
console.error("Error loading task log:", error)
toast.error("Failed to load log")
setSelectedTask(null)
} finally {
setIsLoadingLog(false)
}
},
[run.id, tokenUsage],
)
const taskMetrics: Record<number, TaskMetrics> = useMemo(() => {
// Reference usageUpdatedAt to trigger recomputation when Map contents change
void usageUpdatedAt
const metrics: Record<number, TaskMetrics> = {}
tasks?.forEach((task) => {
const usage = tokenUsage.get(task.id)
const streamingUsage = tokenUsage.get(task.id)
const dbMetrics = task.taskMetrics
if (task.finishedAt && task.taskMetrics) {
metrics[task.id] = task.taskMetrics
} else if (usage) {
// For finished tasks, prefer DB values but fall back to streaming values
// This handles race conditions during timeout where DB might not have latest data
if (task.finishedAt) {
// Check if DB metrics have meaningful values (not just default/empty)
const dbHasData = dbMetrics && (dbMetrics.tokensIn > 0 || dbMetrics.tokensOut > 0 || dbMetrics.cost > 0)
if (dbHasData) {
metrics[task.id] = dbMetrics
} else if (streamingUsage) {
// Fall back to streaming values if DB is empty/stale
metrics[task.id] = {
tokensIn: streamingUsage.totalTokensIn,
tokensOut: streamingUsage.totalTokensOut,
tokensContext: streamingUsage.contextTokens,
duration: streamingUsage.duration ?? 0,
cost: streamingUsage.totalCost,
}
}
} else if (streamingUsage) {
// For running tasks, use streaming values
metrics[task.id] = {
tokensIn: usage.totalTokensIn,
tokensOut: usage.totalTokensOut,
tokensContext: usage.contextTokens,
duration: usage.duration ?? 0,
cost: usage.totalCost,
tokensIn: streamingUsage.totalTokensIn,
tokensOut: streamingUsage.totalTokensOut,
tokensContext: streamingUsage.contextTokens,
duration: streamingUsage.duration ?? 0,
cost: streamingUsage.totalCost,
}
}
})
return metrics
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tasks, tokenUsage, usageUpdatedAt])
// Collect all unique tool names from all tasks and sort by total attempts
const toolColumns = useMemo<ToolName[]>(() => {
// Reference usageUpdatedAt to trigger recomputation when Map contents change
void usageUpdatedAt
if (!tasks) return []
const toolTotals = new Map<ToolName, number>()
for (const task of tasks) {
// Get both DB and streaming values
const dbToolUsage = task.taskMetrics?.toolUsage
const streamingToolUsage = toolUsage.get(task.id)
// For finished tasks, prefer DB values but fall back to streaming values
// For running tasks, use streaming values
// This handles race conditions during timeout where DB might not have latest data
const taskToolUsage = task.finishedAt
? dbToolUsage && Object.keys(dbToolUsage).length > 0
? dbToolUsage
: streamingToolUsage
: streamingToolUsage
if (taskToolUsage) {
for (const [toolName, usage] of Object.entries(taskToolUsage)) {
const tool = toolName as ToolName
const current = toolTotals.get(tool) ?? 0
toolTotals.set(tool, current + usage.attempts)
}
}
}
// Sort by total attempts descending
return Array.from(toolTotals.entries())
.sort((a, b) => b[1] - a[1])
.map(([name]): ToolName => name)
// toolUsage ref is stable; usageUpdatedAt triggers recomputation when Map contents change
}, [tasks, toolUsage, usageUpdatedAt])
// Compute aggregate stats
const stats = useMemo(() => {
// Reference usageUpdatedAt to trigger recomputation when Map contents change
void usageUpdatedAt
if (!tasks) return null
const passed = tasks.filter((t) => t.passed === true).length
const failed = tasks.filter((t) => t.passed === false).length
const completed = passed + failed
let totalTokensIn = 0
let totalTokensOut = 0
let totalCost = 0
let totalDuration = 0
// Aggregate tool usage from all tasks (both finished and running)
const toolUsageAggregate: ToolUsage = {}
for (const task of tasks) {
const metrics = taskMetrics[task.id]
if (metrics) {
totalTokensIn += metrics.tokensIn
totalTokensOut += metrics.tokensOut
totalCost += metrics.cost
totalDuration += metrics.duration
}
// Aggregate tool usage: prefer DB values for finished tasks, fall back to streaming values
// This handles race conditions during timeout where DB might not have latest data
const dbToolUsage = task.taskMetrics?.toolUsage
const streamingToolUsage = toolUsage.get(task.id)
const taskToolUsage = task.finishedAt
? dbToolUsage && Object.keys(dbToolUsage).length > 0
? dbToolUsage
: streamingToolUsage
: streamingToolUsage
if (taskToolUsage) {
for (const [key, usage] of Object.entries(taskToolUsage)) {
const tool = key as keyof ToolUsage
if (!toolUsageAggregate[tool]) {
toolUsageAggregate[tool] = { attempts: 0, failures: 0 }
}
toolUsageAggregate[tool].attempts += usage.attempts
toolUsageAggregate[tool].failures += usage.failures
}
}
}
return {
passed,
failed,
completed,
passRate: completed > 0 ? ((passed / completed) * 100).toFixed(1) : null,
totalTokensIn,
totalTokensOut,
totalCost,
totalDuration,
toolUsage: toolUsageAggregate,
}
// Map refs are stable; usageUpdatedAt triggers recomputation when Map contents change
}, [tasks, taskMetrics, toolUsage, usageUpdatedAt])
// Calculate elapsed time (wall-clock time from run creation to completion or now)
const elapsedTime = useMemo(() => {
// Reference usageUpdatedAt to trigger recomputation for live elapsed time updates
void usageUpdatedAt
if (!tasks || tasks.length === 0) return null
const startTime = new Date(run.createdAt).getTime()
// If run is complete, find the latest finishedAt from tasks
if (run.taskMetricsId) {
const latestFinish = tasks.reduce((latest, task) => {
if (task.finishedAt) {
const finishTime = new Date(task.finishedAt).getTime()
return finishTime > latest ? finishTime : latest
}
return latest
}, startTime)
return latestFinish - startTime
}
// If still running, use current time
return Date.now() - startTime
}, [tasks, run.createdAt, run.taskMetricsId, usageUpdatedAt])
return (
<>
<div>
<div className="mb-2">
<div>
<div className="font-mono">{run.model}</div>
{run.description && <div className="text-sm text-muted-foreground">{run.description}</div>}
{stats && (
<div className="mb-4 p-4 border rounded-lg bg-muted sticky top-0 z-10">
{/* Provider, Model title and status */}
<div className="flex items-center justify-center gap-3 mb-3 relative">
{run.settings?.apiProvider && (
<span className="text-sm text-muted-foreground">{run.settings.apiProvider}</span>
)}
<div className="font-mono">{run.model}</div>
<RunStatus runStatus={runStatus} isComplete={!!run.taskMetricsId} />
{run.description && (
<span className="text-sm text-muted-foreground">- {run.description}</span>
)}
{isRunActive && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => setShowKillDialog(true)}
disabled={isKilling}
className="absolute right-0 flex items-center gap-1 text-muted-foreground hover:text-destructive">
{isKilling ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<StopCircle className="size-4" />
)}
Kill
</Button>
</TooltipTrigger>
<TooltipContent>Stop all containers for this run</TooltipContent>
</Tooltip>
)}
</div>
{/* Main Stats Row */}
<div className="flex items-start justify-center gap-x-8 gap-y-3">
{/* Passed/Failed */}
<div className="text-center min-w-[80px]">
<div className="text-2xl font-bold whitespace-nowrap">
<span className="text-green-600">{stats.passed}</span>
<span className="text-muted-foreground mx-1">/</span>
<span className="text-red-600">{stats.failed}</span>
</div>
<div className="text-xs text-muted-foreground">Passed / Failed</div>
</div>
{/* Pass Rate */}
<div className="text-center min-w-[80px]">
<div
className={`text-2xl font-bold ${
stats.passRate === null
? ""
: parseFloat(stats.passRate) === 100
? ""
: parseFloat(stats.passRate) >= 80
? "text-yellow-500"
: "text-red-500"
}`}>
{stats.passRate ? `${stats.passRate}%` : "-"}
</div>
<div className="text-xs text-muted-foreground">Pass Rate</div>
</div>
{/* Tokens */}
<div className="text-center min-w-[140px]">
<div className="text-xl font-bold font-mono whitespace-nowrap">
{formatTokens(stats.totalTokensIn)}
<span className="text-muted-foreground mx-1">/</span>
{formatTokens(stats.totalTokensOut)}
</div>
<div className="text-xs text-muted-foreground">Tokens In / Out</div>
</div>
{/* Cost */}
<div className="text-center min-w-[70px]">
<div className="text-2xl font-bold font-mono">{formatCurrency(stats.totalCost)}</div>
<div className="text-xs text-muted-foreground">Cost</div>
</div>
{/* Duration */}
<div className="text-center min-w-[90px]">
<div className="text-2xl font-bold font-mono whitespace-nowrap">
{stats.totalDuration > 0 ? formatDuration(stats.totalDuration) : "-"}
</div>
<div className="text-xs text-muted-foreground">Duration</div>
</div>
{/* Elapsed Time */}
<div className="text-center min-w-[90px]">
<div className="text-2xl font-bold font-mono whitespace-nowrap">
{elapsedTime !== null ? formatDuration(elapsedTime) : "-"}
</div>
<div className="text-xs text-muted-foreground">Elapsed</div>
</div>
</div>
{/* Tool Usage Row */}
{Object.keys(stats.toolUsage).length > 0 && (
<div className="flex items-center justify-center gap-2 flex-wrap mt-3">
{Object.entries(stats.toolUsage)
.sort(([, a], [, b]) => b.attempts - a.attempts)
.map(([toolName, usage]) => {
const abbr = getToolAbbreviation(toolName)
const successRate =
usage.attempts > 0
? ((usage.attempts - usage.failures) / usage.attempts) * 100
: 100
const rateColor =
successRate === 100
? "text-green-500"
: successRate >= 80
? "text-yellow-500"
: "text-red-500"
return (
<Tooltip key={toolName}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 px-2 py-1 rounded bg-background/50 border border-border/50 hover:border-border transition-colors cursor-default text-xs">
<span className="font-medium text-muted-foreground">
{abbr}
</span>
<span className="font-bold tabular-nums">{usage.attempts}</span>
<span className={`${rateColor}`}>
{formatToolUsageSuccessRate(usage)}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">{toolName}</TooltipContent>
</Tooltip>
)
})}
</div>
)}
</div>
{!run.taskMetricsId && <RunStatus runStatus={runStatus} />}
</div>
)}
{!tasks ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
@ -60,53 +646,213 @@ export function Run({ run }: { run: Run }) {
<TableHead>Exercise</TableHead>
<TableHead className="text-center">Tokens In / Out</TableHead>
<TableHead>Context</TableHead>
{toolColumns.map((toolName) => (
<TableHead key={toolName} className="text-xs text-center">
<Tooltip>
<TooltipTrigger>{getToolAbbreviation(toolName)}</TooltipTrigger>
<TooltipContent>{toolName}</TooltipContent>
</Tooltip>
</TableHead>
))}
<TableHead>Duration</TableHead>
<TableHead>Cost</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tasks.map((task) => (
<TableRow key={task.id}>
<TableCell>
<div className="flex items-center gap-2">
<TaskStatus
task={task}
running={!!task.startedAt || !!tokenUsage.get(task.id)}
/>
<div>
{task.language}/{task.exercise}
</div>
</div>
</TableCell>
{taskMetrics[task.id] ? (
<>
<TableCell className="font-mono text-xs">
<div className="flex items-center justify-evenly">
<div>{formatTokens(taskMetrics[task.id]!.tokensIn)}</div>/
<div>{formatTokens(taskMetrics[task.id]!.tokensOut)}</div>
{tasks.map((task) => {
const hasStarted = !!task.startedAt || !!tokenUsage.get(task.id)
return (
<TableRow
key={task.id}
className={`${hasStarted ? "cursor-pointer hover:bg-muted/50" : ""} ${task.passed === false ? "bg-red-950/30 border-l-2 border-l-red-500" : ""}`}
onClick={() => hasStarted && onViewTaskLog(task)}>
<TableCell>
<div className="flex items-center gap-2">
<TaskStatus task={task} running={hasStarted} />
<div className="flex items-center gap-2">
<span>
{task.language}/{task.exercise}
{task.iteration > 1 && (
<span className="text-muted-foreground ml-1">
(#{task.iteration})
</span>
)}
</span>
{hasStarted && (
<Tooltip>
<TooltipTrigger asChild>
<FileText className="size-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>Click to view log</TooltipContent>
</Tooltip>
)}
</div>
</TableCell>
<TableCell className="font-mono text-xs">
{formatTokens(taskMetrics[task.id]!.tokensContext)}
</TableCell>
<TableCell className="font-mono text-xs">
{taskMetrics[task.id]!.duration
? formatDuration(taskMetrics[task.id]!.duration)
: "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{formatCurrency(taskMetrics[task.id]!.cost)}
</TableCell>
</>
) : (
<TableCell colSpan={4} />
)}
</TableRow>
))}
</div>
</TableCell>
{taskMetrics[task.id] ? (
<>
<TableCell className="font-mono text-xs">
<div className="flex items-center justify-evenly">
<div>{formatTokens(taskMetrics[task.id]!.tokensIn)}</div>/
<div>{formatTokens(taskMetrics[task.id]!.tokensOut)}</div>
</div>
</TableCell>
<TableCell className="font-mono text-xs">
{formatTokens(taskMetrics[task.id]!.tokensContext)}
</TableCell>
{toolColumns.map((toolName) => {
// Use DB values for finished tasks, but fall back to streaming values
// if DB values are missing (handles race condition during timeout)
const dbUsage = task.taskMetrics?.toolUsage?.[toolName]
const streamingUsage = toolUsage.get(task.id)?.[toolName]
const usage = task.finishedAt
? (dbUsage ?? streamingUsage)
: streamingUsage
const successRate =
usage && usage.attempts > 0
? ((usage.attempts - usage.failures) / usage.attempts) * 100
: 100
const rateColor =
successRate === 100
? "text-muted-foreground"
: successRate >= 80
? "text-yellow-500"
: "text-red-500"
return (
<TableCell key={toolName} className="text-xs text-center">
{usage ? (
<div className="flex flex-col items-center">
<span className="font-medium">
{usage.attempts}
</span>
<span className={rateColor}>
{formatToolUsageSuccessRate(usage)}
</span>
</div>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
)
})}
<TableCell className="font-mono text-xs">
{taskMetrics[task.id]!.duration
? formatDuration(taskMetrics[task.id]!.duration)
: "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{formatCurrency(taskMetrics[task.id]!.cost)}
</TableCell>
</>
) : (
<TableCell colSpan={4 + toolColumns.length} />
)}
</TableRow>
)
})}
</TableBody>
</Table>
)}
</div>
{/* Task Log Dialog - Full Screen */}
<Dialog open={!!selectedTask} onOpenChange={() => setSelectedTask(null)}>
<DialogContent className="w-[95vw] !max-w-[95vw] h-[90vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<div className="flex items-center justify-between pr-8">
<DialogTitle className="flex items-center gap-2">
<FileText className="size-4" />
{selectedTask?.language}/{selectedTask?.exercise}
{selectedTask?.iteration && selectedTask.iteration > 1 && (
<span className="text-muted-foreground">(#{selectedTask.iteration})</span>
)}
<span
className={`ml-2 text-sm ${
selectedTask?.passed === true
? "text-green-600"
: selectedTask?.passed === false
? "text-red-600"
: "text-yellow-500"
}`}>
(
{selectedTask?.passed === true
? "Passed"
: selectedTask?.passed === false
? "Failed"
: "Running"}
)
</span>
</DialogTitle>
{taskLog && (
<Button
variant="outline"
size="sm"
onClick={onCopyLog}
className="flex items-center gap-1">
{copied ? (
<>
<Check className="size-4" />
Copied!
</>
) : (
<>
<Copy className="size-4" />
Copy Log
</>
)}
</Button>
)}
</div>
</DialogHeader>
<div className="flex-1 min-h-0 overflow-hidden">
{isLoadingLog ? (
<div className="flex items-center justify-center h-full">
<LoaderCircle className="size-6 animate-spin" />
</div>
) : taskLog ? (
<ScrollArea className="h-full w-full">
<div className="text-xs font-mono bg-muted p-4 rounded-md overflow-x-auto">
{formatLogContent(taskLog)}
</div>
</ScrollArea>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">
Log file not available (may have been cleared)
</div>
)}
</div>
</DialogContent>
</Dialog>
{/* Kill Run Confirmation Dialog */}
<AlertDialog open={showKillDialog} onOpenChange={setShowKillDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Kill Run?</AlertDialogTitle>
<AlertDialogDescription>
This will stop the controller and all task runner containers for this run. Any running tasks
will be terminated immediately. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isKilling}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={onKillRun}
disabled={isKilling}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
{isKilling ? (
<>
<LoaderCircle className="size-4 animate-spin mr-2" />
Killing...
</>
) : (
"Kill Run"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
import { Fragment, HTMLAttributes } from "react"
import { type Keys, type RooCodeSettings, GLOBAL_SETTINGS_KEYS, PROVIDER_SETTINGS_KEYS } from "@roo-code/types"
import { cn } from "@/lib/utils"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
export const ROO_CODE_SETTINGS_KEYS = [...GLOBAL_SETTINGS_KEYS, ...PROVIDER_SETTINGS_KEYS] as Keys<RooCodeSettings>[]
export const ROO_CODE_SETTINGS_KEYS = [
...new Set([...GLOBAL_SETTINGS_KEYS, ...PROVIDER_SETTINGS_KEYS]),
] as Keys<RooCodeSettings>[]
type SettingsDiffProps = HTMLAttributes<HTMLDivElement> & {
type SettingsDiffProps = {
defaultSettings: RooCodeSettings
customSettings: RooCodeSettings
}
@ -14,53 +14,45 @@ type SettingsDiffProps = HTMLAttributes<HTMLDivElement> & {
export function SettingsDiff({
customSettings: { experiments: customExperiments, ...customSettings },
defaultSettings: { experiments: defaultExperiments, ...defaultSettings },
className,
...props
}: SettingsDiffProps) {
const defaults = { ...defaultSettings, ...defaultExperiments }
const custom = { ...customSettings, ...customExperiments }
return (
<div className={cn("grid grid-cols-3 gap-2 text-sm p-2", className)} {...props}>
<div className="font-medium text-muted-foreground">Setting</div>
<div className="font-medium text-muted-foreground">Default</div>
<div className="font-medium text-muted-foreground">Custom</div>
{ROO_CODE_SETTINGS_KEYS.map((key) => {
const defaultValue = defaults[key as keyof typeof defaults]
const customValue = custom[key as keyof typeof custom]
const isDefault = JSON.stringify(defaultValue) === JSON.stringify(customValue)
<div className="border rounded-sm">
<Table>
<TableHeader>
<TableRow className="font-medium text-muted-foreground">
<TableHead>Setting</TableHead>
<TableHead>Default</TableHead>
<TableHead>Custom</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ROO_CODE_SETTINGS_KEYS.map((key) => {
const defaultValue = JSON.stringify(defaults[key as keyof typeof defaults], null, 2)
const customValue = JSON.stringify(custom[key as keyof typeof custom], null, 2)
return isDefault ? null : (
<SettingDiff
key={key}
name={key}
defaultValue={JSON.stringify(defaultValue, null, 2)}
customValue={JSON.stringify(customValue, null, 2)}
/>
)
})}
return defaultValue === customValue ||
(isEmpty(defaultValue) && isEmpty(customValue)) ? null : (
<TableRow key={key}>
<TableCell className="font-mono" title={key}>
{key}
</TableCell>
<TableCell className="font-mono text-rose-500 line-through" title={defaultValue}>
{defaultValue}
</TableCell>
<TableCell className="font-mono text-teal-500" title={customValue}>
{customValue}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
)
}
type SettingDiffProps = HTMLAttributes<HTMLDivElement> & {
name: string
defaultValue?: string
customValue?: string
}
export function SettingDiff({ name, defaultValue, customValue, ...props }: SettingDiffProps) {
return (
<Fragment {...props}>
<div className="font-mono" title={name}>
{name}
</div>
<pre className="inline text-rose-500 line-through" title={defaultValue}>
{defaultValue}
</pre>
<pre className="inline text-teal-500" title={customValue}>
{customValue}
</pre>
</Fragment>
)
}
const isEmpty = (value: string | undefined) =>
value === undefined || value === "" || value === "null" || value === '""' || value === "[]" || value === "{}"

View file

@ -1,16 +1,29 @@
import { useCallback, useState, useRef } from "react"
import Link from "next/link"
import { Ellipsis, ClipboardList, Copy, Check, LoaderCircle, Trash } from "lucide-react"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
import { Ellipsis, ClipboardList, Copy, Check, LoaderCircle, Trash, Settings, FileDown, StickyNote } from "lucide-react"
import type { Run as EvalsRun, TaskMetrics as EvalsTaskMetrics } from "@roo-code/evals"
import type { ToolName } from "@roo-code/types"
import { deleteRun } from "@/actions/runs"
import { formatCurrency, formatDuration, formatTokens, formatToolUsageSuccessRate } from "@/lib/formatters"
import { deleteRun, updateRunDescription } from "@/actions/runs"
import {
formatCurrency,
formatDateTime,
formatDuration,
formatTokens,
formatToolUsageSuccessRate,
} from "@/lib/formatters"
import { useCopyRun } from "@/hooks/use-copy-run"
import {
Button,
TableCell,
TableRow,
Textarea,
Tooltip,
TooltipContent,
TooltipTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@ -23,18 +36,89 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
ScrollArea,
} from "@/components/ui"
type RunProps = {
run: EvalsRun
taskMetrics: EvalsTaskMetrics | null
toolColumns: ToolName[]
consolidatedToolColumns: string[]
}
export function Run({ run, taskMetrics }: RunProps) {
export function Run({ run, taskMetrics, toolColumns, consolidatedToolColumns }: RunProps) {
const router = useRouter()
const [deleteRunId, setDeleteRunId] = useState<number>()
const [showSettings, setShowSettings] = useState(false)
const [isExportingLogs, setIsExportingLogs] = useState(false)
const [showNotesDialog, setShowNotesDialog] = useState(false)
const [editingDescription, setEditingDescription] = useState(run.description ?? "")
const [isSavingNotes, setIsSavingNotes] = useState(false)
const continueRef = useRef<HTMLButtonElement>(null)
const { isPending, copyRun, copied } = useCopyRun(run.id)
const hasDescription = Boolean(run.description && run.description.trim().length > 0)
const handleSaveDescription = useCallback(async () => {
setIsSavingNotes(true)
try {
const result = await updateRunDescription(run.id, editingDescription.trim() || null)
if (result.success) {
toast.success("Description saved")
setShowNotesDialog(false)
router.refresh()
} else {
toast.error("Failed to save description")
}
} catch (error) {
console.error("Error saving description:", error)
toast.error("Failed to save description")
} finally {
setIsSavingNotes(false)
}
}, [run.id, editingDescription, router])
const onExportFailedLogs = useCallback(async () => {
if (run.failed === 0) {
toast.error("No failed tasks to export")
return
}
setIsExportingLogs(true)
try {
const response = await fetch(`/api/runs/${run.id}/logs/failed`)
if (!response.ok) {
const error = await response.json()
toast.error(error.error || "Failed to export logs")
return
}
// Download the zip file
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `run-${run.id}-failed-logs.zip`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
toast.success("Failed logs exported successfully")
} catch (error) {
console.error("Error exporting logs:", error)
toast.error("Failed to export logs")
} finally {
setIsExportingLogs(false)
}
}, [run.id, run.failed])
const onConfirmDelete = useCallback(async () => {
if (!deleteRunId) {
return
@ -48,86 +132,233 @@ export function Run({ run, taskMetrics }: RunProps) {
}
}, [deleteRunId])
const handleRowClick = useCallback(
(e: React.MouseEvent) => {
// Don't navigate if clicking on the dropdown menu
if ((e.target as HTMLElement).closest("[data-dropdown-trigger]")) {
return
}
router.push(`/runs/${run.id}`)
},
[router, run.id],
)
return (
<>
<TableRow>
<TableCell>{run.model}</TableCell>
<TableRow className="cursor-pointer hover:bg-muted/50" onClick={handleRowClick}>
<TableCell className="max-w-[200px] truncate">{run.model}</TableCell>
<TableCell>{run.settings?.apiProvider ?? "-"}</TableCell>
<TableCell className="text-sm text-muted-foreground whitespace-nowrap">
{formatDateTime(run.createdAt)}
</TableCell>
<TableCell>{run.passed}</TableCell>
<TableCell>{run.failed}</TableCell>
<TableCell>
{run.passed + run.failed > 0 && (
<span>{((run.passed / (run.passed + run.failed)) * 100).toFixed(1)}%</span>
)}
{run.passed + run.failed > 0 &&
(() => {
const percent = (run.passed / (run.passed + run.failed)) * 100
const colorClass =
percent === 100 ? "text-green-500" : percent >= 80 ? "text-yellow-500" : "text-red-500"
return <span className={colorClass}>{percent.toFixed(1)}%</span>
})()}
</TableCell>
<TableCell>
{taskMetrics && (
<div className="flex items-center gap-1.5">
<div>{formatTokens(taskMetrics.tokensIn)}</div>/
<div>{formatTokens(taskMetrics.tokensOut)}</div>
</div>
)}
</TableCell>
<TableCell>
{taskMetrics?.toolUsage?.apply_diff && (
<div className="flex flex-row items-center gap-1.5">
<div>{taskMetrics.toolUsage.apply_diff.attempts}</div>
<div>/</div>
<div>{formatToolUsageSuccessRate(taskMetrics.toolUsage.apply_diff)}</div>
<div className="flex items-center gap-1">
<span>{formatTokens(taskMetrics.tokensIn)}</span>/
<span>{formatTokens(taskMetrics.tokensOut)}</span>
</div>
)}
</TableCell>
{consolidatedToolColumns.length > 0 && (
<TableCell className="text-xs text-center">
{taskMetrics?.toolUsage ? (
(() => {
// Calculate aggregated stats for consolidated tools
let totalAttempts = 0
let totalFailures = 0
const breakdown: Array<{ tool: string; attempts: number; rate: string }> = []
for (const toolName of consolidatedToolColumns) {
const usage = taskMetrics.toolUsage[toolName as ToolName]
if (usage) {
totalAttempts += usage.attempts
totalFailures += usage.failures
const rate =
usage.attempts > 0
? `${Math.round(((usage.attempts - usage.failures) / usage.attempts) * 100)}%`
: "0%"
breakdown.push({ tool: toolName, attempts: usage.attempts, rate })
}
}
const consolidatedRate =
totalAttempts > 0 ? ((totalAttempts - totalFailures) / totalAttempts) * 100 : 100
const rateColor =
consolidatedRate === 100
? "text-muted-foreground"
: consolidatedRate >= 80
? "text-yellow-500"
: "text-red-500"
return totalAttempts > 0 ? (
<Tooltip>
<TooltipTrigger>
<div className="flex flex-col items-center">
<span className="font-medium">{totalAttempts}</span>
<span className={rateColor}>{Math.round(consolidatedRate)}%</span>
</div>
</TooltipTrigger>
<TooltipContent>
<div className="text-xs">
<div className="font-semibold mb-1">Consolidated Tools:</div>
{breakdown.map(({ tool, attempts, rate }) => (
<div key={tool} className="flex justify-between gap-4">
<span>{tool}:</span>
<span>
{attempts} ({rate})
</span>
</div>
))}
</div>
</TooltipContent>
</Tooltip>
) : (
<span className="text-muted-foreground">-</span>
)
})()
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
)}
{toolColumns.map((toolName) => {
const usage = taskMetrics?.toolUsage?.[toolName]
const successRate =
usage && usage.attempts > 0 ? ((usage.attempts - usage.failures) / usage.attempts) * 100 : 100
const rateColor =
successRate === 100
? "text-muted-foreground"
: successRate >= 80
? "text-yellow-500"
: "text-red-500"
return (
<TableCell key={toolName} className="text-xs text-center">
{usage ? (
<div className="flex flex-col items-center">
<span className="font-medium">{usage.attempts}</span>
<span className={rateColor}>{formatToolUsageSuccessRate(usage)}</span>
</div>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
)
})}
<TableCell>{taskMetrics && formatCurrency(taskMetrics.cost)}</TableCell>
<TableCell>{taskMetrics && formatDuration(taskMetrics.duration)}</TableCell>
<TableCell>
<DropdownMenu>
<Button variant="ghost" size="icon" asChild>
<DropdownMenuTrigger>
<Ellipsis />
</DropdownMenuTrigger>
</Button>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/runs/${run.id}`}>
<TableCell onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-1">
{/* Note Icon */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={hasDescription ? "" : "opacity-30 hover:opacity-60"}
onClick={(e) => {
e.stopPropagation()
setEditingDescription(run.description ?? "")
setShowNotesDialog(true)
}}>
<StickyNote className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-[300px]">
{hasDescription ? (
<div className="whitespace-pre-wrap">{run.description}</div>
) : (
<div className="text-muted-foreground">No description. Click to add one.</div>
)}
</TooltipContent>
</Tooltip>
{/* More Actions Menu */}
<DropdownMenu>
<Button variant="ghost" size="icon" asChild>
<DropdownMenuTrigger data-dropdown-trigger>
<Ellipsis />
</DropdownMenuTrigger>
</Button>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/runs/${run.id}`}>
<div className="flex items-center gap-1">
<ClipboardList />
<div>View Tasks</div>
</div>
</Link>
</DropdownMenuItem>
{run.settings && (
<DropdownMenuItem onClick={() => setShowSettings(true)}>
<div className="flex items-center gap-1">
<Settings />
<div>View Settings</div>
</div>
</DropdownMenuItem>
)}
{run.taskMetricsId && (
<DropdownMenuItem onClick={() => copyRun()} disabled={isPending || copied}>
<div className="flex items-center gap-1">
{isPending ? (
<>
<LoaderCircle className="animate-spin" />
Copying...
</>
) : copied ? (
<>
<Check />
Copied!
</>
) : (
<>
<Copy />
Copy to Production
</>
)}
</div>
</DropdownMenuItem>
)}
{run.failed > 0 && (
<DropdownMenuItem onClick={onExportFailedLogs} disabled={isExportingLogs}>
<div className="flex items-center gap-1">
{isExportingLogs ? (
<>
<LoaderCircle className="animate-spin" />
Exporting...
</>
) : (
<>
<FileDown />
Export Failed Logs
</>
)}
</div>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setDeleteRunId(run.id)
setTimeout(() => continueRef.current?.focus(), 0)
}}>
<div className="flex items-center gap-1">
<ClipboardList />
<div>View Tasks</div>
</div>
</Link>
</DropdownMenuItem>
{run.taskMetricsId && (
<DropdownMenuItem onClick={() => copyRun()} disabled={isPending || copied}>
<div className="flex items-center gap-1">
{isPending ? (
<>
<LoaderCircle className="animate-spin" />
Copying...
</>
) : copied ? (
<>
<Check />
Copied!
</>
) : (
<>
<Copy />
Copy to Production
</>
)}
<Trash />
<div>Delete</div>
</div>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setDeleteRunId(run.id)
setTimeout(() => continueRef.current?.focus(), 0)
}}>
<div className="flex items-center gap-1">
<Trash />
<div>Delete</div>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
<AlertDialog open={!!deleteRunId} onOpenChange={() => setDeleteRunId(undefined)}>
@ -144,6 +375,51 @@ export function Run({ run, taskMetrics }: RunProps) {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Dialog open={showSettings} onOpenChange={setShowSettings}>
<DialogContent className="max-w-2xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Run Settings</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[60vh]">
<pre className="text-xs font-mono bg-muted p-4 rounded-md overflow-auto">
{JSON.stringify(run.settings, null, 2)}
</pre>
</ScrollArea>
</DialogContent>
</Dialog>
{/* Notes/Description Dialog */}
<Dialog open={showNotesDialog} onOpenChange={setShowNotesDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Run Description</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Textarea
placeholder="Add a description or notes for this run..."
value={editingDescription}
onChange={(e) => setEditingDescription(e.target.value)}
rows={4}
className="resize-none"
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowNotesDialog(false)}>
Cancel
</Button>
<Button onClick={handleSaveDescription} disabled={isSavingNotes}>
{isSavingNotes ? (
<>
<LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
Saving...
</>
) : (
"Save"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}

View file

@ -1,45 +1,656 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import { Rocket } from "lucide-react"
import {
ArrowDown,
ArrowUp,
ArrowUpDown,
Combine,
Ellipsis,
LoaderCircle,
Rocket,
RotateCcw,
Trash2,
X,
} from "lucide-react"
import { toast } from "sonner"
import type { Run, TaskMetrics } from "@roo-code/evals"
import type { ToolName } from "@roo-code/types"
import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
import { deleteIncompleteRuns, deleteOldRuns } from "@/actions/runs"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
MultiSelect,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui"
import { Run as Row } from "@/components/home/run"
type RunWithTaskMetrics = Run & { taskMetrics: TaskMetrics | null }
type SortColumn = "model" | "provider" | "passed" | "failed" | "percent" | "cost" | "duration" | "createdAt"
type SortDirection = "asc" | "desc"
type TimeframeOption = "all" | "24h" | "7d" | "30d" | "90d"
const TIMEFRAME_OPTIONS: { value: TimeframeOption; label: string }[] = [
{ value: "all", label: "All time" },
{ value: "24h", label: "Last 24 hours" },
{ value: "7d", label: "Last 7 days" },
{ value: "30d", label: "Last 30 days" },
{ value: "90d", label: "Last 90 days" },
]
// LocalStorage keys
const STORAGE_KEYS = {
TIMEFRAME: "evals-runs-timeframe",
MODEL_FILTER: "evals-runs-model-filter",
PROVIDER_FILTER: "evals-runs-provider-filter",
CONSOLIDATED_TOOLS: "evals-runs-consolidated-tools",
}
function getTimeframeStartDate(timeframe: TimeframeOption): Date | null {
if (timeframe === "all") return null
const now = new Date()
switch (timeframe) {
case "24h":
return new Date(now.getTime() - 24 * 60 * 60 * 1000)
case "7d":
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
case "30d":
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
case "90d":
return new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
default:
return null
}
}
// Generate abbreviation from tool name (e.g., "read_file" -> "RF", "list_code_definition_names" -> "LCDN")
function getToolAbbreviation(toolName: string): string {
return toolName
.split("_")
.map((word) => word[0]?.toUpperCase() ?? "")
.join("")
}
function SortIcon({
column,
sortColumn,
sortDirection,
}: {
column: SortColumn
sortColumn: SortColumn | null
sortDirection: SortDirection
}) {
if (sortColumn !== column) {
return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />
}
return sortDirection === "asc" ? <ArrowUp className="ml-1 h-3 w-3" /> : <ArrowDown className="ml-1 h-3 w-3" />
}
export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
const router = useRouter()
const [sortColumn, setSortColumn] = useState<SortColumn | null>("createdAt")
const [sortDirection, setSortDirection] = useState<SortDirection>("desc")
// Filter state - initialize from localStorage
const [timeframeFilter, setTimeframeFilter] = useState<TimeframeOption>(() => {
if (typeof window === "undefined") return "all"
const stored = localStorage.getItem(STORAGE_KEYS.TIMEFRAME)
return (stored as TimeframeOption) || "all"
})
const [modelFilter, setModelFilter] = useState<string[]>(() => {
if (typeof window === "undefined") return []
const stored = localStorage.getItem(STORAGE_KEYS.MODEL_FILTER)
return stored ? JSON.parse(stored) : []
})
const [providerFilter, setProviderFilter] = useState<string[]>(() => {
if (typeof window === "undefined") return []
const stored = localStorage.getItem(STORAGE_KEYS.PROVIDER_FILTER)
return stored ? JSON.parse(stored) : []
})
// Tool column consolidation state - initialize from localStorage
const [consolidatedToolColumns, setConsolidatedToolColumns] = useState<string[]>(() => {
if (typeof window === "undefined") return []
const stored = localStorage.getItem(STORAGE_KEYS.CONSOLIDATED_TOOLS)
return stored ? JSON.parse(stored) : []
})
// Delete runs state
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [showDeleteOldConfirm, setShowDeleteOldConfirm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
// Persist filters to localStorage
useEffect(() => {
localStorage.setItem(STORAGE_KEYS.TIMEFRAME, timeframeFilter)
}, [timeframeFilter])
useEffect(() => {
localStorage.setItem(STORAGE_KEYS.MODEL_FILTER, JSON.stringify(modelFilter))
}, [modelFilter])
useEffect(() => {
localStorage.setItem(STORAGE_KEYS.PROVIDER_FILTER, JSON.stringify(providerFilter))
}, [providerFilter])
useEffect(() => {
localStorage.setItem(STORAGE_KEYS.CONSOLIDATED_TOOLS, JSON.stringify(consolidatedToolColumns))
}, [consolidatedToolColumns])
// Count incomplete runs (runs without taskMetricsId)
const incompleteRunsCount = useMemo(() => {
return runs.filter((run) => run.taskMetrics === null).length
}, [runs])
// Count runs older than 30 days
const oldRunsCount = useMemo(() => {
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
return runs.filter((run) => run.createdAt < thirtyDaysAgo).length
}, [runs])
const handleDeleteIncompleteRuns = useCallback(async () => {
setIsDeleting(true)
try {
const result = await deleteIncompleteRuns()
if (result.success) {
toast.success(`Deleted ${result.deletedCount} incomplete run${result.deletedCount !== 1 ? "s" : ""}`)
if (result.storageErrors.length > 0) {
toast.warning(`Some storage folders could not be deleted: ${result.storageErrors.length} errors`)
}
router.refresh()
} else {
toast.error("Failed to delete incomplete runs")
}
} catch (error) {
console.error("Error deleting incomplete runs:", error)
toast.error("Failed to delete incomplete runs")
} finally {
setIsDeleting(false)
setShowDeleteConfirm(false)
}
}, [router])
const handleDeleteOldRuns = useCallback(async () => {
setIsDeleting(true)
try {
const result = await deleteOldRuns()
if (result.success) {
toast.success(
`Deleted ${result.deletedCount} run${result.deletedCount !== 1 ? "s" : ""} older than 30 days`,
)
if (result.storageErrors.length > 0) {
toast.warning(`Some storage folders could not be deleted: ${result.storageErrors.length} errors`)
}
router.refresh()
} else {
toast.error("Failed to delete old runs")
}
} catch (error) {
console.error("Error deleting old runs:", error)
toast.error("Failed to delete old runs")
} finally {
setIsDeleting(false)
setShowDeleteOldConfirm(false)
}
}, [router])
const handleSort = (column: SortColumn) => {
if (sortColumn === column) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc")
} else {
setSortColumn(column)
setSortDirection("desc")
}
}
// Derive unique models and providers from runs
const modelOptions = useMemo(() => {
const models = new Set<string>()
for (const run of runs) {
if (run.model) models.add(run.model)
}
return Array.from(models)
.sort()
.map((model) => ({ label: model, value: model }))
}, [runs])
const providerOptions = useMemo(() => {
const providers = new Set<string>()
for (const run of runs) {
const provider = run.settings?.apiProvider
if (provider) providers.add(provider)
}
return Array.from(providers)
.sort()
.map((provider) => ({ label: provider, value: provider }))
}, [runs])
// Filter runs based on filter state
const filteredRuns = useMemo(() => {
return runs.filter((run) => {
// Timeframe filter
const timeframeStart = getTimeframeStartDate(timeframeFilter)
if (timeframeStart && run.createdAt < timeframeStart) {
return false
}
// Model filter
if (modelFilter.length > 0 && !modelFilter.includes(run.model)) {
return false
}
// Provider filter
if (providerFilter.length > 0) {
const provider = run.settings?.apiProvider
if (!provider || !providerFilter.includes(provider)) {
return false
}
}
return true
})
}, [runs, timeframeFilter, modelFilter, providerFilter])
// Collect all unique tool names from filtered runs and sort by total attempts
const allToolColumns = useMemo<ToolName[]>(() => {
const toolTotals = new Map<ToolName, number>()
for (const run of filteredRuns) {
if (run.taskMetrics?.toolUsage) {
for (const [toolName, usage] of Object.entries(run.taskMetrics.toolUsage)) {
const tool = toolName as ToolName
const current = toolTotals.get(tool) ?? 0
toolTotals.set(tool, current + usage.attempts)
}
}
}
// Sort by total attempts descending
return Array.from(toolTotals.entries())
.sort((a, b) => b[1] - a[1])
.map(([name]): ToolName => name)
}, [filteredRuns])
// Tool column options for the consolidation dropdown
const toolColumnOptions = useMemo(() => {
return allToolColumns.map((tool) => ({
label: tool,
value: tool,
}))
}, [allToolColumns])
// Separate consolidated and individual tool columns
const individualToolColumns = useMemo(() => {
return allToolColumns.filter((tool) => !consolidatedToolColumns.includes(tool))
}, [allToolColumns, consolidatedToolColumns])
// Create a "consolidated" column if any tools are selected for consolidation
const hasConsolidatedColumn = consolidatedToolColumns.length > 0
// Use individualToolColumns for rendering
const toolColumns = individualToolColumns
// Sort filtered runs based on current sort column and direction
const sortedRuns = useMemo(() => {
if (!sortColumn) return filteredRuns
return [...filteredRuns].sort((a, b) => {
let aVal: string | number | Date | null = null
let bVal: string | number | Date | null = null
switch (sortColumn) {
case "model":
aVal = a.model
bVal = b.model
break
case "provider":
aVal = a.settings?.apiProvider ?? ""
bVal = b.settings?.apiProvider ?? ""
break
case "passed":
aVal = a.passed
bVal = b.passed
break
case "failed":
aVal = a.failed
bVal = b.failed
break
case "percent":
aVal = a.passed + a.failed > 0 ? a.passed / (a.passed + a.failed) : 0
bVal = b.passed + b.failed > 0 ? b.passed / (b.passed + b.failed) : 0
break
case "cost":
aVal = a.taskMetrics?.cost ?? 0
bVal = b.taskMetrics?.cost ?? 0
break
case "duration":
aVal = a.taskMetrics?.duration ?? 0
bVal = b.taskMetrics?.duration ?? 0
break
case "createdAt":
aVal = a.createdAt
bVal = b.createdAt
break
}
if (aVal === null || bVal === null) return 0
let comparison = 0
if (typeof aVal === "string" && typeof bVal === "string") {
comparison = aVal.localeCompare(bVal)
} else if (aVal instanceof Date && bVal instanceof Date) {
comparison = aVal.getTime() - bVal.getTime()
} else {
comparison = (aVal as number) - (bVal as number)
}
return sortDirection === "asc" ? comparison : -comparison
})
}, [filteredRuns, sortColumn, sortDirection])
// Calculate colSpan for empty state (7 base columns + dynamic tools + consolidated column + 3 end columns)
const totalColumns = 7 + toolColumns.length + (hasConsolidatedColumn ? 1 : 0) + 3
// Check if any filters or settings are active
const hasActiveFilters = timeframeFilter !== "all" || modelFilter.length > 0 || providerFilter.length > 0
const hasConsolidatedTools = consolidatedToolColumns.length > 0
const hasAnyCustomization = hasActiveFilters || hasConsolidatedTools
const clearAllFilters = () => {
setTimeframeFilter("all")
setModelFilter([])
setProviderFilter([])
}
const resetAll = () => {
setTimeframeFilter("all")
setModelFilter([])
setProviderFilter([])
setConsolidatedToolColumns([])
localStorage.removeItem(STORAGE_KEYS.TIMEFRAME)
localStorage.removeItem(STORAGE_KEYS.MODEL_FILTER)
localStorage.removeItem(STORAGE_KEYS.PROVIDER_FILTER)
localStorage.removeItem(STORAGE_KEYS.CONSOLIDATED_TOOLS)
}
return (
<>
<Table className="border border-t-0">
{/* Filter Controls */}
<div className="flex items-center gap-4 p-4 border border-b-0 rounded-t-md bg-muted/30">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Timeframe:</span>
<Select
value={timeframeFilter}
onValueChange={(value) => setTimeframeFilter(value as TimeframeOption)}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMEFRAME_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Model:</span>
<MultiSelect
options={modelOptions}
value={modelFilter}
onValueChange={setModelFilter}
placeholder="All models"
className="w-[200px]"
maxCount={1}
/>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Provider:</span>
<MultiSelect
options={providerOptions}
value={providerFilter}
onValueChange={setProviderFilter}
placeholder="All providers"
className="w-[180px]"
maxCount={1}
/>
</div>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Combine className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium text-muted-foreground">Consolidate:</span>
</div>
</TooltipTrigger>
<TooltipContent>Select tool columns to consolidate into a combined column</TooltipContent>
</Tooltip>
<div className="relative min-w-[100px] w-fit max-w-[140px]">
<div className={consolidatedToolColumns.length > 0 ? "[&>div>div]:invisible" : ""}>
<MultiSelect
options={toolColumnOptions}
value={consolidatedToolColumns}
onValueChange={setConsolidatedToolColumns}
placeholder="None"
className="w-full min-w-[100px]"
maxCount={0}
popoverAutoWidth
footer={
hasAnyCustomization && (
<Button
variant="ghost"
size="sm"
className="w-full justify-start text-muted-foreground hover:text-foreground"
onClick={resetAll}>
<RotateCcw className="h-4 w-4 mr-2" />
Reset all filters & consolidation
</Button>
)
}
/>
</div>
{consolidatedToolColumns.length > 0 && (
<div className="absolute inset-0 flex items-center px-3 pointer-events-none">
<span className="text-sm font-medium whitespace-nowrap">
{consolidatedToolColumns.length} tool
{consolidatedToolColumns.length !== 1 ? "s" : ""}
</span>
</div>
)}
</div>
</div>
{hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={clearAllFilters}>
<X className="h-4 w-4 mr-1" />
Clear filters
</Button>
)}
<div className="flex items-center gap-2 ml-auto">
{/* Bulk Actions Menu */}
{(incompleteRunsCount > 0 || oldRunsCount > 0) && (
<DropdownMenu>
<Button variant="ghost" size="sm" asChild>
<DropdownMenuTrigger disabled={isDeleting}>
<Ellipsis className="h-4 w-4" />
</DropdownMenuTrigger>
</Button>
<DropdownMenuContent align="end">
{incompleteRunsCount > 0 && (
<DropdownMenuItem
onClick={() => setShowDeleteConfirm(true)}
disabled={isDeleting}
className="text-destructive focus:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
Delete {incompleteRunsCount} incomplete run
{incompleteRunsCount !== 1 ? "s" : ""}
</DropdownMenuItem>
)}
{oldRunsCount > 0 && (
<DropdownMenuItem
onClick={() => setShowDeleteOldConfirm(true)}
disabled={isDeleting}
className="text-destructive focus:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
Delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""} over 30d
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
<div className="text-sm text-muted-foreground">
{filteredRuns.length} of {runs.length} runs
</div>
</div>
</div>
<Table className="border border-t-0 rounded-t-none">
<TableHeader>
<TableRow>
<TableHead>Model</TableHead>
<TableHead>Passed</TableHead>
<TableHead>Failed</TableHead>
<TableHead>% Correct</TableHead>
<TableHead>Tokens In / Out</TableHead>
<TableHead>Diff Edits</TableHead>
<TableHead>Cost</TableHead>
<TableHead>Duration</TableHead>
<TableHead />
<TableHead
className="max-w-[200px] cursor-pointer select-none"
onClick={() => handleSort("model")}>
<div className="flex items-center">
Model
<SortIcon column="model" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("provider")}>
<div className="flex items-center">
Provider
<SortIcon column="provider" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("createdAt")}>
<div className="flex items-center">
Created
<SortIcon column="createdAt" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("passed")}>
<div className="flex items-center">
Passed
<SortIcon column="passed" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("failed")}>
<div className="flex items-center">
Failed
<SortIcon column="failed" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("percent")}>
<div className="flex items-center">
%
<SortIcon column="percent" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead>Tokens</TableHead>
{hasConsolidatedColumn && (
<TableHead className="text-xs text-center">
<Tooltip>
<TooltipTrigger>
<Combine className="h-3 w-3 inline" />
</TooltipTrigger>
<TooltipContent>
<div className="text-xs">
<div className="font-semibold mb-1">Consolidated Tools:</div>
{consolidatedToolColumns.map((tool) => (
<div key={tool}>{tool}</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TableHead>
)}
{toolColumns.map((toolName) => (
<TableHead key={toolName} className="text-xs text-center">
<Tooltip>
<TooltipTrigger>{getToolAbbreviation(toolName)}</TooltipTrigger>
<TooltipContent>{toolName}</TooltipContent>
</Tooltip>
</TableHead>
))}
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("cost")}>
<div className="flex items-center">
Cost
<SortIcon column="cost" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("duration")}>
<div className="flex items-center">
Duration
<SortIcon column="duration" sortColumn={sortColumn} sortDirection={sortDirection} />
</div>
</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{runs.length ? (
runs.map(({ taskMetrics, ...run }) => <Row key={run.id} run={run} taskMetrics={taskMetrics} />)
{sortedRuns.length ? (
sortedRuns.map(({ taskMetrics, ...run }) => (
<Row
key={run.id}
run={run}
taskMetrics={taskMetrics}
toolColumns={toolColumns}
consolidatedToolColumns={consolidatedToolColumns}
/>
))
) : (
<TableRow>
<TableCell colSpan={9} className="text-center">
No eval runs yet.
<Button variant="link" onClick={() => router.push("/runs/new")}>
Launch
</Button>
one now.
<TableCell colSpan={totalColumns} className="text-center py-8">
{runs.length === 0 ? (
<>
No eval runs yet.
<Button variant="link" onClick={() => router.push("/runs/new")}>
Launch
</Button>
one now.
</>
) : (
<>
No runs match the current filters.
<Button variant="link" onClick={clearAllFilters}>
Clear filters
</Button>
to see all runs.
</>
)}
</TableCell>
</TableRow>
)}
@ -51,6 +662,70 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
onClick={() => router.push("/runs/new")}>
<Rocket className="size-6" />
</Button>
{/* Delete Incomplete Runs Confirmation Dialog */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Incomplete Runs</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {incompleteRunsCount} incomplete run
{incompleteRunsCount !== 1 ? "s" : ""}? This will permanently remove all database records
and storage folders for these runs. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteIncompleteRuns}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
{isDeleting ? (
<>
<LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
<>
Delete {incompleteRunsCount} run{incompleteRunsCount !== 1 ? "s" : ""}
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Delete Old Runs Confirmation Dialog */}
<AlertDialog open={showDeleteOldConfirm} onOpenChange={setShowDeleteOldConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Old Runs</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""} older than
30 days? This will permanently remove all database records and storage folders for these
runs. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteOldRuns}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
{isDeleting ? (
<>
<LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
<>
Delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""}
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}

View file

@ -0,0 +1,27 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none">
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View file

@ -1,6 +1,7 @@
export * from "./alert-dialog"
export * from "./badge"
export * from "./button"
export * from "./checkbox"
export * from "./command"
export * from "./dialog"
export * from "./drawer"

View file

@ -48,7 +48,10 @@ interface MultiSelectProps extends React.HTMLAttributes<HTMLDivElement>, Variant
*/
onValueChange: (value: string[]) => void
/** The default selected values when the component mounts. */
/** The controlled selected values. When provided, the component becomes controlled. */
value?: string[]
/** The default selected values when the component mounts (uncontrolled mode). */
defaultValue?: string[]
/**
@ -81,6 +84,18 @@ interface MultiSelectProps extends React.HTMLAttributes<HTMLDivElement>, Variant
* Optional, can be used to add custom styles.
*/
className?: string
/**
* If true, popover width will auto-size to content instead of matching trigger width.
* Optional, defaults to false.
*/
popoverAutoWidth?: boolean
/**
* Optional footer content to render at the bottom of the popover.
* Useful for adding reset buttons or other actions.
*/
footer?: React.ReactNode
}
export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
@ -89,26 +104,42 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
options,
onValueChange,
variant,
value,
defaultValue = [],
placeholder = "Select options",
maxCount = 3,
modalPopover = false,
popoverAutoWidth = false,
footer,
className,
...props
},
ref,
) => {
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
const [internalSelectedValues, setInternalSelectedValues] = React.useState<string[]>(defaultValue)
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false)
// Use controlled value if provided, otherwise use internal state
const isControlled = value !== undefined
const selectedValues = isControlled ? value : internalSelectedValues
const setSelectedValues = React.useCallback(
(newValues: string[]) => {
if (!isControlled) {
setInternalSelectedValues(newValues)
}
onValueChange(newValues)
},
[isControlled, onValueChange],
)
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
setIsPopoverOpen(true)
} else if (event.key === "Backspace" && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues]
newSelectedValues.pop()
if (!selectedValues.length) return
const newSelectedValues = selectedValues.slice(0, -1)
setSelectedValues(newSelectedValues)
onValueChange(newSelectedValues)
}
}
@ -117,7 +148,6 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option]
setSelectedValues(newSelectedValues)
onValueChange(newSelectedValues)
}
const handleTogglePopover = () => {
@ -127,7 +157,6 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
const clearExtraOptions = () => {
const newSelectedValues = selectedValues.slice(0, maxCount)
setSelectedValues(newSelectedValues)
onValueChange(newSelectedValues)
}
const searchResultsRef = React.useRef<Map<string, number>>(new Map())
@ -141,12 +170,10 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
selectedValues.sort().join(",") === values.sort().join(",")
) {
setSelectedValues([])
onValueChange([])
return
}
setSelectedValues(values)
onValueChange(values)
}
const onFilter = React.useCallback(
@ -230,7 +257,7 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
</div>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[var(--radix-popover-trigger-width)]"
className={cn("p-0", popoverAutoWidth ? "w-auto" : "w-[var(--radix-popover-trigger-width)]")}
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}>
<Command filter={onFilter}>
@ -263,6 +290,7 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
</CommandGroup>
</CommandList>
</Command>
{footer && <div className="border-t p-2">{footer}</div>}
</PopoverContent>
</Popover>
)

View file

@ -0,0 +1,37 @@
import { useCallback, useRef, useState } from "react"
import fuzzysort from "fuzzysort"
interface ModelWithId {
id: string
name: string
}
export const useFuzzyModelSearch = <T extends ModelWithId>(data: T[] | undefined) => {
const [searchValue, setSearchValue] = useState("")
const searchResultsRef = useRef<Map<string, number>>(new Map())
const searchValueRef = useRef("")
const onFilter = useCallback(
(value: string, search: string) => {
if (searchValueRef.current !== search) {
searchValueRef.current = search
searchResultsRef.current.clear()
for (const {
obj: { id },
score,
} of fuzzysort.go(search, data || [], {
key: "name",
})) {
searchResultsRef.current.set(id, score)
}
}
return searchResultsRef.current.get(value) ?? 0
},
[data],
)
return { searchValue, setSearchValue, onFilter }
}

View file

@ -1,5 +1,6 @@
import { z } from "zod"
import { useQuery } from "@tanstack/react-query"
import { useFuzzyModelSearch } from "./use-fuzzy-model-search"
export const openRouterModelSchema = z.object({
id: z.string(),
@ -25,8 +26,13 @@ export const getOpenRouterModels = async (): Promise<OpenRouterModel[]> => {
return result.data.data.sort((a, b) => a.name.localeCompare(b.name))
}
export const useOpenRouterModels = () =>
useQuery({
export const useOpenRouterModels = () => {
const query = useQuery({
queryKey: ["getOpenRouterModels"],
queryFn: getOpenRouterModels,
})
const { searchValue, setSearchValue, onFilter } = useFuzzyModelSearch(query.data)
return { ...query, searchValue, setSearchValue, onFilter }
}

View file

@ -0,0 +1,66 @@
import { z } from "zod"
import { useQuery } from "@tanstack/react-query"
import { useFuzzyModelSearch } from "./use-fuzzy-model-search"
export const rooCodeCloudModelSchema = z.object({
object: z.literal("model"),
id: z.string(),
name: z.string(),
description: z.string().optional(),
context_window: z.number(),
max_tokens: z.number(),
supports_images: z.boolean().optional(),
supports_prompt_cache: z.boolean().optional(),
type: z.literal("language"),
tags: z.array(z.string()).optional(),
deprecationMessage: z.string().optional(),
owned_by: z.string(),
pricing: z.object({
input: z.string(),
output: z.string(),
input_cache_read: z.string().optional(),
input_cache_write: z.string().optional(),
}),
evals: z
.object({
score: z.number().min(0).max(100),
})
.optional(),
created: z.number(),
deprecated: z.boolean().optional(),
})
export type RooCodeCloudModel = z.infer<typeof rooCodeCloudModelSchema>
export const getRooCodeCloudModels = async (): Promise<RooCodeCloudModel[]> => {
const response = await fetch("https://api.roocode.com/proxy/v1/models")
if (!response.ok) {
return []
}
const result = z
.object({
object: z.literal("list"),
data: z.array(rooCodeCloudModelSchema),
})
.safeParse(await response.json())
if (!result.success) {
console.error(result.error)
return []
}
return result.data.data.filter((model) => !model.deprecated).sort((a, b) => a.name.localeCompare(b.name))
}
export const useRooCodeCloudModels = () => {
const query = useQuery({
queryKey: ["getRooCodeCloudModels"],
queryFn: getRooCodeCloudModels,
})
const { searchValue, setSearchValue, onFilter } = useFuzzyModelSearch(query.data)
return { ...query, searchValue, setSearchValue, onFilter }
}

View file

@ -1,7 +1,7 @@
import { useState, useCallback, useRef } from "react"
import { useQuery, keepPreviousData } from "@tanstack/react-query"
import { type TokenUsage, RooCodeEventName, taskEventSchema } from "@roo-code/types"
import { type TokenUsage, type ToolUsage, RooCodeEventName, taskEventSchema } from "@roo-code/types"
import type { Run, Task, TaskMetrics } from "@roo-code/evals"
import { getHeartbeat } from "@/actions/heartbeat"
@ -15,6 +15,7 @@ export type RunStatus = {
runners: string[] | undefined
tasks: (Task & { taskMetrics: TaskMetrics | null })[] | undefined
tokenUsage: Map<number, TokenUsage & { duration?: number }>
toolUsage: Map<number, ToolUsage>
usageUpdatedAt: number | undefined
}
@ -23,6 +24,7 @@ export const useRunStatus = (run: Run): RunStatus => {
const [usageUpdatedAt, setUsageUpdatedAt] = useState<number>()
const tokenUsage = useRef<Map<number, TokenUsage & { duration?: number }>>(new Map())
const toolUsage = useRef<Map<number, ToolUsage>>(new Map())
const startTimes = useRef<Map<number, number>>(new Map())
const { data: heartbeat } = useQuery({
@ -78,6 +80,12 @@ export const useRunStatus = (run: Run): RunStatus => {
const startTime = startTimes.current.get(taskId)
const duration = startTime ? Date.now() - startTime : undefined
tokenUsage.current.set(taskId, { ...payload[1], duration })
// Track tool usage from streaming updates
if (payload[2]) {
toolUsage.current.set(taskId, payload[2])
}
setUsageUpdatedAt(Date.now())
break
}
@ -96,6 +104,7 @@ export const useRunStatus = (run: Run): RunStatus => {
runners,
tasks,
tokenUsage: tokenUsage.current,
toolUsage: toolUsage.current,
usageUpdatedAt,
}
}

View file

@ -45,4 +45,14 @@ export const formatTokens = (tokens: number) => {
}
export const formatToolUsageSuccessRate = (usage: { attempts: number; failures: number }) =>
usage.attempts === 0 ? "0%" : `${(((usage.attempts - usage.failures) / usage.attempts) * 100).toFixed(1)}%`
usage.attempts === 0 ? "0%" : `${Math.round(((usage.attempts - usage.failures) / usage.attempts) * 100)}%`
export const formatDateTime = (date: Date) => {
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
}).format(date)
}

View file

@ -6,8 +6,6 @@ import { rooCodeSettingsSchema } from "@roo-code/types"
* CreateRun
*/
export const MODEL_DEFAULT = "anthropic/claude-sonnet-4"
export const CONCURRENCY_MIN = 1
export const CONCURRENCY_MAX = 25
export const CONCURRENCY_DEFAULT = 1
@ -16,6 +14,10 @@ export const TIMEOUT_MIN = 5
export const TIMEOUT_MAX = 10
export const TIMEOUT_DEFAULT = 5
export const ITERATIONS_MIN = 1
export const ITERATIONS_MAX = 10
export const ITERATIONS_DEFAULT = 1
export const createRunSchema = z
.object({
model: z.string().min(1, { message: "Model is required." }),
@ -25,7 +27,8 @@ export const createRunSchema = z
settings: rooCodeSettingsSchema.optional(),
concurrency: z.number().int().min(CONCURRENCY_MIN).max(CONCURRENCY_MAX),
timeout: z.number().int().min(TIMEOUT_MIN).max(TIMEOUT_MAX),
systemPrompt: z.string().optional(),
iterations: z.number().int().min(ITERATIONS_MIN).max(ITERATIONS_MAX),
jobToken: z.string().optional(),
})
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
message: "Exercises are required when running a partial suite.",

View file

@ -1,7 +1,6 @@
# PostHog Analytics Configuration
# Replace these values with your actual PostHog API key and host
NEXT_PUBLIC_POSTHOG_KEY=your_posthog_api_key_here
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# Basin Form Endpoint for Static Form Submissions
# Replace this with your actual Basin form endpoint (e.g., https://usebasin.com/f/your-form-id)

View file

@ -27,6 +27,11 @@ const nextConfig: NextConfig = {
destination: "https://roo-code.notion.site/238fd1401b0a8087b858e1ad431507cf?pvs=105",
permanent: false,
},
{
source: "/provider/pricing",
destination: "/provider",
permanent: true,
},
]
},
}

View file

@ -25,7 +25,7 @@
"embla-carousel-react": "^8.6.0",
"framer-motion": "12.15.0",
"lucide-react": "^0.518.0",
"next": "^15.2.5",
"next": "~15.2.6",
"next-themes": "^0.4.6",
"posthog-js": "^1.248.1",
"react": "^18.3.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bedrock</title><path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z"></path></svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MoonshotAI</title><path d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z"></path></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenRouter</title><path d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z"></path></svg>

After

Width:  |  Height:  |  Size: 906 B

View file

@ -1,29 +1,33 @@
import {
ArrowRight,
Bot,
Brain,
ChartLine,
Cloud,
Lock,
Github,
History,
LucideIcon,
Megaphone,
MessageCircleQuestionMark,
ReplaceAll,
Pencil,
Router,
Share2,
Slack,
Users,
Users2,
} from "lucide-react"
import type { Metadata } from "next"
import Image from "next/image"
import { Button } from "@/components/ui"
import { AnimatedBackground } from "@/components/homepage"
import { AnimatedBackground, UseExamplesSection } from "@/components/homepage"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { EXTERNAL_LINKS } from "@/lib/constants"
import Image from "next/image"
// Workaround for next/image choking on these for some reason
import screenshotDark from "/public/heroes/cloud-screen.png"
const TITLE = "Roo Code Cloud"
const DESCRIPTION =
"Roo Code Cloud gives you and your team the tools to take AI-coding to the next level with cloud agents, remote control, and more."
const OG_DESCRIPTION = "Go way beyond the IDE"
"Your AI Software Engineering Team in the Cloud. Delegate tasks to autonomous agents, review PRs, and collaborate with your team."
const OG_DESCRIPTION = "Your AI Team in the Cloud"
const PATH = "/cloud"
export const metadata: Metadata = {
@ -54,161 +58,171 @@ export const metadata: Metadata = {
description: DESCRIPTION,
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development"],
keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development", "autonomous agents"],
}
const howItWorks = [
{
title: "1. Connect your GitHub account",
description:
"Pick which repos the agents can work with in their isolated containers and choose what model you want to power each of them. You're in control.",
icon: Github,
},
{
title: "2. Set up your agent team",
description:
"Choose the roles you want filled, like Explainer, Planner, Coder, PR Reviewer and PR Fixer. They know how to act in each situation and stay on-task with no deviations.",
icon: Users2,
},
{
title: "3. Start giving them tasks",
description:
"Describe what you want them to do from the web UI, get the Reviewer automatically reviewing PRs, get the Coder building features from Slack threads and much more. They're now part of your team.",
icon: Pencil,
},
]
interface Feature {
icon: LucideIcon
title: string
description: string
logos?: string[]
}
const cloudFeatures: Feature[] = [
const features: Feature[] = [
{
icon: Bot,
title: "Autonomous Cloud Agents",
description:
"Delegate work to specialized agents like the Planner, Coder, Explainer, Reviewer, and Fixer that run 24/7.",
},
{
icon: Brain,
title: "Model Agnostic",
description:
"Bring your own keys or use the Roo Code Cloud Provider with access to all top models with no markup.",
},
{
icon: Github,
title: "GitHub PR Reviews",
description:
"Agents can automatically review Pull Requests, provide feedback, and even push fixes directly to your repository.",
},
{
icon: Slack,
title: "Slack Integration",
description: "Start tasks, get updates, and collaborate with agents directly from your team's Slack channels.",
},
{
icon: Router,
title: "Roomote Control",
description: "Control your IDE from anywhere and keep coding away from your computer.",
},
{
icon: Cloud,
title: "Cloud Agents",
description:
"Specialized agents running in the Cloud to get stuff done while you sleep, with a credit-based system that doesn't lock you in or dumb your models down.",
"Connect to your local VS Code instance and control the extension remotely from the browser or Slack.",
},
{
icon: ReplaceAll,
title: "Still Model-agnostic",
description: "Bring your own provider key — no markup, lock-in, no restrictions.",
logos: ["Anthropic", "OpenAI", "Gemini", "Grok", "Qwen", "Kimi", "Mistral", "Ollama"],
icon: Users,
title: "Team Collaboration",
description:
"Manage your team and their access to tasks and resources, with centralized billing and configuration.",
},
{
icon: ChartLine,
title: "Usage Analytics",
description: "Detailed token analytics to help you optimize your costs and usage.",
description: "Detailed token analytics to help you optimize your costs and usage across your team.",
},
{
icon: Megaphone,
title: "Early Model Access",
description: "Get early, free access to new, stealth coding models as they become available.",
icon: History,
title: "Task History",
description: "Access from anywhere all of your tasks, from the cloud and the extension",
},
{
icon: Share2,
title: "Task Sharing",
description: "Share tasks with friends and co-workers and let them follow your work.",
},
{
icon: Users,
title: "Team Management",
description:
"Manage your team and their access to tasks and resources, with centralized billing, analytics and configuration.",
},
{
icon: Lock,
title: "Secure and Private",
description:
"Your data is never used for training, and we're SOC2 Type 2 and GDPR compliant, following state-of-the-art security practices, with deep respect for your IP.",
},
{
icon: MessageCircleQuestionMark,
title: "Priority support",
description: "Get quick help from the people who know Roo best.",
description: "Share tasks with friends and co-workers and let them follow your work in real-time.",
},
]
// Workaround for next/image choking on these for some reason
import screenshotDark from "/public/heroes/cloud-screen.png"
export default function CloudPage() {
return (
<>
<section className="relative flex md:h-[calc(80vh-theme(spacing.12))] items-center overflow-hidden">
{/* Hero Section */}
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
<AnimatedBackground />
<div className="container relative flex items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid h-full relative gap-4 md:gap-0 lg:grid-cols-2">
<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
<div>
<h1 className="text-4xl font-bold tracking-tight mt-8 text-center md:text-left md:text-4xl lg:text-5xl lg:mt-0">
Go <em>way</em> beyond the IDE
</h1>
<p className="mt-4 max-w-md text-lg text-muted-foreground text-center md:text-left sm:mt-6">
Roo Code Cloud gives you (and your team) the tools to take AI-coding to the next
level
</p>
</div>
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
<Button
variant="outline"
size="lg"
className="w-full sm:w-auto bg-white/20 dark:bg-white/10 backdrop-blur-sm border border-black/40 dark:border-white/30 hover:border-blue-400 hover:bg-white/30 dark:hover:bg-white/20 hover:shadow-[0_0_20px_rgba(59,130,246,0.5)] transition-all duration-300">
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center">
Start Free Trial
<ArrowRight className="ml-2" />
</a>
</Button>
</div>
</div>
<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
<div className="md:w-[900px] md:h-[700px] relative rounded-md overflow-clip">
<div className="block">
<Image
src={screenshotDark}
alt="Screenshot of Roo Code Cloud"
className="max-w-full h-auto"
width={1390}
height={1012}
/>
</div>
</div>
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center max-w-4xl mx-auto mb-12">
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
Your AI Team <span className="text-violet-500">in the Cloud</span>
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
from Web and Slack.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="xl"
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Try Cloud for Free
<ArrowRight className="ml-2 size-5" />
</a>
</Button>
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
<a href="/pricing" className="flex items-center justify-center">
View Pricing
</a>
</Button>
</div>
</div>
{/* Screenshot */}
<div className="relative mx-auto mt-4 md:max-w-[1000px]">
<Image
src={screenshotDark}
alt="Roo Code Cloud Interface"
className="w-full h-auto"
width={1390}
height={1012}
priority
/>
</div>
</div>
</section>
{/* How It Works Section */}
<section className="relative overflow-hidden border-t border-border py-32">
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto mb-12 md:mb-24 max-w-4xl text-center">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
</div>
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
<div>
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">Power and Flexibility</h2>
<p className="mt-6 text-lg text-muted-foreground">
Code in the cloud, access free models, get usage analytics and more
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-4">How it works</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
It only takes 2 minutes to expand your team by 10x.
</p>
</div>
</div>
<div className="relative mx-auto md:max-w-[1200px]">
<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-2 lg:grid-cols-3 lg:gap-8">
{cloudFeatures.map((feature, index) => {
const Icon = feature.icon
<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-3 lg:gap-8">
{howItWorks.map((step, index) => {
const Icon = step.icon
return (
<li
key={index}
className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300">
<Icon className="size-6 text-foreground/80" />
className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300 hover:shadow-lg">
{Icon && <Icon className="size-6 text-foreground/80" />}
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
{feature.title}
{step.title}
</h3>
<p className="leading-relaxed font-light text-muted-foreground">
{feature.description}
</p>
{feature.logos && (
<div className="mt-4 flex flex-wrap items-center gap-4">
{feature.logos.map((logo) => (
<Image
key={logo}
width={20}
height={20}
className="w-5 h-5 overflow-clip opacity-50 dark:invert"
src={`/logos/${logo.toLowerCase()}.svg`}
alt={`${logo} Logo`}
/>
))}
</div>
)}
<div className="leading-relaxed font-light text-muted-foreground">
{step.description}
</div>
</li>
)
})}
@ -217,25 +231,61 @@ export default function CloudPage() {
</div>
</section>
<div id="faq"></div>
{/* Use Cases Section */}
<UseExamplesSection />
{/* Features Grid */}
<section className="py-24 bg-muted/30">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
</div>
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
Powering the next generation of software development
</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Everything you need to scale your development capacity with AI.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto relative">
{features.map((feature, index) => {
const Icon = feature.icon
return (
<div
key={index}
className="bg-background p-8 rounded-2xl border border-border hover:shadow-lg transition-all duration-300">
<div className="bg-violet-100 dark:bg-violet-900/20 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
<Icon className="size-6 text-violet-600 dark:text-violet-400" />
</div>
<h3 className="text-xl font-semibold mb-3">{feature.title}</h3>
<p className="text-muted-foreground leading-relaxed">{feature.description}</p>
</div>
)
})}
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-20">
<section className="py-24">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-blue-500/5 via-cyan-500/5 to-purple-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/20 dark:bg-gradient-to-br dark:from-gray-800 dark:via-gray-900 dark:to-black sm:p-12">
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">Try Roo Code Cloud now</h2>
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">Code from anywhere.</p>
<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
Try a completely new way of working.
</h2>
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">Start for free today.</p>
<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
<Button
size="lg"
className="bg-black text-white hover:bg-gray-800 hover:shadow-lg hover:shadow-black/20 dark:bg-white dark:text-black dark:hover:bg-gray-200 dark:hover:shadow-white/20 transition-all duration-300"
className="bg-foreground text-background hover:bg-foreground/90 transition-all duration-300"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Create a free Cloud account
Sign up now
<ArrowRight className="ml-2 h-4 w-4" />
</a>
</Button>

View file

@ -0,0 +1,84 @@
/* eslint-disable react/jsx-no-target-blank */
import { getVSCodeDownloads } from "@/lib/stats"
import { Button } from "@/components/ui"
import {
AnimatedBackground,
CodeExample,
CompanyLogos,
FAQSection,
Features,
InstallSection,
Testimonials,
} from "@/components/homepage"
import { EXTERNAL_LINKS } from "@/lib/constants"
import { ArrowRight } from "lucide-react"
import { StructuredData } from "@/components/structured-data"
// Invalidate cache when a request comes in, at most once every hour.
export const revalidate = 3600
export default async function ExtensionPage() {
const downloads = await getVSCodeDownloads()
return (
<>
<StructuredData />
<section className="relative flex h-[calc(125vh-theme(spacing.12))] items-center overflow-hidden md:h-[calc(80svh-theme(spacing.12))]">
<AnimatedBackground />
<div className="container relative flex items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid h-full relative gap-8 md:gap-12 lg:grid-cols-2 lg:gap-16">
<div className="flex flex-col justify-center space-y-4 sm:space-y-8">
<div>
<h1 className="text-4xl font-bold tracking-tight mt-8 text-center md:text-left md:text-4xl lg:text-5xl lg:mt-0">
The Open Source AI Coding Assistant for serious work.
</h1>
<p className="mt-4 max-w-lg text-lg text-muted-foreground text-center md:text-left sm:mt-6">
Specialized modes stay on task and ship great code.
<br />
Fully model-agnostic so you can use the best (or most cost-effective) model for
each task.
</p>
<p className="max-w-lg text-lg text-muted-foreground text-center md:text-left sm:mt-6">
Stop chasing this week&apos;s hot new model or CLI tool and go deep with Roo Code.
</p>
</div>
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
<Button
size="lg"
className="w-full hover:bg-gray-200 dark:bg-white dark:text-black sm:w-auto">
<a
href={EXTERNAL_LINKS.MARKETPLACE}
target="_blank"
className="flex w-full items-center justify-center">
Install VS Code Extension
<ArrowRight className="ml-2" />
</a>
</Button>
</div>
<div className="md:max-h-[70px] md:overflow-clip text-center md:text-left pt-6 md:pt-0">
<CompanyLogos />
</div>
</div>
<div className="relative flex items-center mx-auto h-full mt-8 lg:mt-0">
<div className="flex items-center justify-center">
<CodeExample />
</div>
</div>
</div>
</div>
</section>
<div id="product">
<Features />
</div>
<div id="testimonials">
<Testimonials />
</div>
<div id="faq">
<FAQSection />
</div>
<InstallSection downloads={downloads} />
</>
)
}

View file

@ -14,7 +14,7 @@ import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
const OG_TITLE = "Meet Roo Code"
const OG_DESCRIPTION = "The AI dev team that gets things done."
const OG_DESCRIPTION = "Your AI Software Engineering Team in the IDE and the Cloud."
export const metadata: Metadata = {
metadataBase: new URL(SEO.url),

View file

@ -1,16 +1,12 @@
/* eslint-disable react/jsx-no-target-blank */
import { getVSCodeDownloads } from "@/lib/stats"
import { Button } from "@/components/ui"
import {
AnimatedBackground,
CodeExample,
CompanyLogos,
FAQSection,
Features,
InstallSection,
Testimonials,
CTASection,
OptionOverviewSection,
PillarsSection,
UseExamplesSection,
} from "@/components/homepage"
import { EXTERNAL_LINKS } from "@/lib/constants"
import { ArrowRight } from "lucide-react"
@ -20,70 +16,67 @@ import { StructuredData } from "@/components/structured-data"
export const revalidate = 3600
export default async function Home() {
const downloads = await getVSCodeDownloads()
return (
<>
<StructuredData />
<section className="relative flex h-[calc(125vh-theme(spacing.12))] items-center overflow-hidden md:h-[calc(80svh-theme(spacing.12))]">
<AnimatedBackground />
<div className="container relative flex items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid h-full relative gap-8 md:gap-12 lg:grid-cols-2 lg:gap-16">
<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
<div>
<h1 className="text-4xl font-bold tracking-tight mt-8 text-center md:text-left md:text-4xl lg:text-5xl lg:mt-0">
The AI dev team that gets things done.
</h1>
<p className="mt-4 max-w-md text-lg text-muted-foreground text-center md:text-left sm:mt-6">
Roo&apos;s specialized modes stay on task and ship great code. Open source and works
with any model.
</p>
</div>
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
<Button
size="lg"
className="w-full hover:bg-gray-200 dark:bg-white dark:text-black sm:w-auto">
<a
href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"
target="_blank"
className="flex w-full items-center justify-center">
Install VS Code Extension
<ArrowRight className="ml-2" />
</a>
</Button>
<Button
variant="outline"
size="lg"
className="w-full sm:w-auto bg-white/20 dark:bg-white/10 backdrop-blur-sm border border-black/40 dark:border-white/30 hover:border-blue-400 hover:bg-white/30 dark:hover:bg-white/20 hover:shadow-[0_0_20px_rgba(59,130,246,0.5)] transition-all duration-300">
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
className="flex w-full items-center justify-center">
Try Cloud
<ArrowRight className="ml-2" />
</a>
</Button>
</div>
<CompanyLogos />
<section className="relative flex flex-col items-center overflow-hidden pt-20 pb-12 md:pt-32 md:pb-16">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
</div>
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center text-center">
<h1 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground max-w-4xl mb-6">
Your AI Software Engineering Team is here.
<br />
<span className="text-muted-foreground">Interactive in the IDE, autonomous in the cloud.</span>
</h1>
<div className="mt-2 max-w-3xl text-lg text-muted-foreground mb-10 space-y-3">
<p>
Use the <strong className="text-nowrap">Roo Code Extension</strong> on your computer for
full control, or delegate work to your{" "}
<strong className="text-nowrap">Roo Code Cloud Agents</strong> from the web, Slack, Github
or wherever your team is.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 mb-16">
<div className="flex flex-col items-center gap-2">
<Button size="xl" className="w-full">
<a
href={EXTERNAL_LINKS.MARKETPLACE}
target="_blank"
rel="noreferrer"
className="flex items-center justify-center">
Install VS Code Extension
<ArrowRight className="ml-2 size-5" />
</a>
</Button>
<span className="text-xs text-muted-foreground">Free and Open Source</span>
</div>
<div className="relative flex items-center mx-auto h-full mt-8 lg:mt-0">
<div className="flex items-center justify-center">
<CodeExample />
</div>
<div className="flex flex-col items-center gap-2">
<Button size="xl" className="w-full">
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
className="flex items-center justify-center">
Try Cloud for Free
<ArrowRight className="ml-2 size-5" />
</a>
</Button>
<span className="text-xs text-muted-foreground">No credit card needed</span>
</div>
</div>
<div className="mb-12 px-4">
<CompanyLogos />
</div>
</div>
</section>
<div id="product">
<Features />
</div>
<div id="testimonials">
<Testimonials />
</div>
<div id="faq">
<FAQSection />
</div>
<InstallSection downloads={downloads} />
<PillarsSection />
<OptionOverviewSection />
<UseExamplesSection />
<Testimonials />
<FAQSection />
<CTASection />
</>
)
}

View file

@ -33,8 +33,9 @@ export const content: AgentPageContent = {
],
},
cta: {
buttonText: "Start 14-day Free Trial",
disclaimer: "(cancel anytime)",
buttonText: "Try now for free",
disclaimer: "",
tracking: "&agent=pr-fixer",
},
},
howItWorks: {
@ -89,6 +90,6 @@ export const content: AgentPageContent = {
cta: {
heading: "Ship fixes, not follow-ups.",
description: "Let Roo Code's PR Fixer turn your review feedback into clean, ready-to-merge commits.",
buttonText: "Start 14-day Free Trial",
buttonText: "Try now for free",
},
}

View file

@ -1,17 +1,16 @@
import { Users, Building2, ArrowRight, Star, LucideIcon, Check, Cloud } from "lucide-react"
import { Users, ArrowRight, LucideIcon, Check, SquareTerminal, CornerRightDown, Cloud } from "lucide-react"
import type { Metadata } from "next"
import Link from "next/link"
import { Button } from "@/components/ui"
import { AnimatedBackground } from "@/components/homepage"
import { ContactForm } from "@/components/enterprise/contact-form"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { EXTERNAL_LINKS } from "@/lib/constants"
const TITLE = "Roo Code Cloud Pricing"
const TITLE = "Roo Code Pricing"
const DESCRIPTION =
"Simple, transparent pricing for Roo Code Cloud. The VS Code extension is free forever. Choose the cloud plan that fits your needs."
"Simple, transparent pricing for all Roo Code products. The VS Code extension is free forever. Choose the cloud plan that fits your needs."
const OG_DESCRIPTION = ""
const PATH = "/pricing"
@ -61,72 +60,67 @@ interface PricingTier {
name: string
icon: LucideIcon
price: string
priceSuffix: string
period?: string
creditPrice?: string
trial?: string
cancellation?: string
description: string
featuresIntro?: string
features: string[]
cta: {
text: string
href?: string
isContactForm?: boolean
}
}
const pricingTiers: PricingTier[] = [
{
name: "VS Code Extension",
icon: SquareTerminal,
price: "Free",
priceSuffix: "inference",
description: "The best local coding agent",
features: ["Unlimited local use", "Bring your own model", "Powerful, extensible modes", "Community support"],
cta: {
text: "Install Now",
href: EXTERNAL_LINKS.MARKETPLACE,
},
},
{
name: "Cloud Free",
icon: Cloud,
price: "$0",
cancellation: "Cancel anytime",
description: "For folks just getting started",
period: "/mo",
priceSuffix: "credits",
creditPrice: `$${PRICE_CREDITS}`,
description: "For AI-forward engineers",
featuresIntro: "Go beyond the extension with",
features: [
"Token usage analytics",
"Access to Cloud Agents: fully autonomous development you can call from Slack, Github and the web",
"Access to the Roo Code Cloud Provider",
"Follow your tasks from anywhere",
"Share tasks with friends and co-workers",
"Early access to free AI Models",
"Community support",
"Token usage analytics",
"Professional support",
],
cta: {
text: "Get started",
text: "Sign up",
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP,
},
},
{
name: "Pro",
icon: Star,
price: "$20",
period: "/mo",
trial: "Free 14-day trial · ",
creditPrice: `$${PRICE_CREDITS}`,
cancellation: "Cancel anytime",
description: "For pro Roo coders",
featuresIntro: "Everything in Free +",
features: [
"Cloud Agents: PR Reviewer and more",
"Roomote Control: Start, stop and control tasks from anywhere",
"Paid support",
],
cta: {
text: "Get started",
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
},
},
{
name: "Team",
name: "Cloud Team",
icon: Users,
price: "$99",
priceSuffix: "credits",
period: "/mo",
creditPrice: `$${PRICE_CREDITS}`,
trial: "Free 14-day trial · ",
cancellation: "Cancel anytime",
trial: "Free for 14 days, then",
description: "For AI-forward teams",
featuresIntro: "Everything in Pro +",
featuresIntro: "Everything in Free +",
features: ["Unlimited users (no per-seat cost)", "Shared configuration & policies", "Centralized billing"],
cta: {
text: "Get started",
text: "Sign up",
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
},
},
@ -138,52 +132,43 @@ export default function PricingPage() {
<AnimatedBackground />
{/* Hero Section */}
<section className="relative overflow-hidden pt-16 pb-12">
<section className="relative overflow-hidden pt-12 pb-10">
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-5xl font-bold tracking-tight">Roo Code Cloud Pricing</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
Simple, transparent pricing that scales with your needs.
<br />
No inference markups. Free 14-day trials to kick the tires.
<h1 className="text-5xl font-bold tracking-tight">Roo Code Pricing</h1>
<p className="mt-4 text-lg text-muted-foreground">
For all of our products: the Roo Code VS Code Extension, Roo Code Cloud and the Roo Code
Cloud inference Provider.
</p>
</div>
</div>
</section>
{/* Free Extension Notice */}
<div className="mx-auto max-w-6xl">
<div className="rounded-xl p-4 mb-8 text-center bg-gradient-to-r from-blue-500/10 via-cyan-500/10 to-purple-500/10 border border-blue-500/20 dark:border-white/20">
<p className="text-center">
<strong className="font-semibold">The Roo Code extension is free! </strong>
Roo Code Cloud is an optional service which takes it to the next level.
</p>
</div>
</div>
{/* Pricing Tiers */}
<section className="">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto grid max-w-6xl gap-4 lg:grid-cols-3">
<div className="mx-auto grid max-w-6xl gap-4 md:grid-cols-3 md:px-4">
{pricingTiers.map((tier) => {
const Icon = tier.icon
return (
<div
key={tier.name}
className="relative p-6 flex flex-col justify-start bg-background border rounded-2xl transition-all hover:shadow-lg">
className="relative group p-6 flex flex-col justify-start bg-background rounded-2xl outline outline-2 outline-border/50 hover:outline-8 transition-all shadow-xl hover:shadow-2xl hover:outline-6">
<div className="mb-6">
<div className="flex items-center justify-between">
<h3 className="text-2xl font-bold tracking-tight">{tier.name}</h3>
<Icon className="size-6" />
</div>
<p className="text-sm text-muted-foreground">{tier.description}</p>
<p className="text-sm font-medium">{tier.description}</p>
</div>
<div className="absolute -right-2 -top-4 rounded-full bg-card shadow-md p-4 outline outline-2 outline-border/50 group-hover:scale-105 group-hover:outline-8 transition-all">
<Icon className="size-6" strokeWidth={1.5} />
</div>
<div className="grow mb-8">
<p className="text-sm text-muted-foreground font-light mb-2">
{tier.featuresIntro}&nbsp;
</p>
<ul className="space-y-3 my-0 h-[148px]">
<ul className="space-y-3 my-0 md:h-[192px]">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-2">
<Check className="mt-0.5 h-4 w-4 text-muted-foreground shrink-0" />
@ -193,52 +178,67 @@ export default function PricingPage() {
</ul>
</div>
<p className="text-2xl mt-0 mb-1 tracking-tight">
<strong>{tier.price}</strong>
{tier.period}
<p className="text-base font-light">{tier.trial}</p>
<p className="text-xl mb-1 tracking-tight font-light">
<strong className="font-bold">{tier.price}</strong>
{tier.period} + {tier.priceSuffix}
<CornerRightDown className="inline size-4 ml-1 relative top-0.5" />
</p>
{tier.creditPrice && (
<p className="text-sm text-muted-foreground mb-1">
+ {tier.creditPrice}/hour for Cloud tasks
</p>
)}
<p className="text-xs text-muted-foreground mb-4">
{tier.trial}
{tier.cancellation}
<p className="text-sm text-muted-foreground mb-5">
{tier.creditPrice && (
<>
Cloud Agents: {tier.creditPrice}/hour in credits
<br />
</>
)}
Inference:{" "}
<Link href="/provider" className="underline hover:no-underline">
Roo Provider pricing
</Link>{" "}
credits or{" "}
<abbr title="Bring Your Own Model" className="cursor-help">
BYOM
</abbr>
</p>
{tier.cta.isContactForm ? (
<ContactForm
formType="demo"
buttonText={tier.cta.text}
buttonClassName="w-full transition-all duration-300"
/>
) : (
<Button size="lg" className="w-full transition-all duration-300" asChild>
<Link href={tier.cta.href!} className="flex items-center justify-center">
{tier.cta.text}
</Link>
</Button>
)}
<Button size="lg" className="w-full transition-all duration-300" asChild>
<Link href={tier.cta.href!} className="flex items-center justify-center">
{tier.cta.text}
<ArrowRight />
</Link>
</Button>
{/* <div className="bg-foreground/20 h-8 absolute -bottom-8 left-1/2 w-[1px]" /> */}
<div className="h-[28px] absolute bottom-[-31px] left-1/2 w-[4px] transition-colors bg-gradient-to-b from-transparent to-violet-700/20 group-hover:from-violet-500/50 group-hover:to-violet-500/20" />
</div>
)
})}
</div>
</div>
<div className="mx-auto grid max-w-6xl gap-4 mt-4 relative">
<p className="bg-background border rounded-2xl p-6 text-center text-sm text-muted-foreground">
<Building2 className="inline size-4 mr-2 mb-0.5" />
Need SAML, advanced security, custom integrations or terms? Enterprise is for you.
<Link
href="/enterprise#contact"
className="font-medium ml-1 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
Talk to Sales
</Link>
.
</p>
<div className="max-w-6xl mx-auto mt-8 p-7 flex flex-col md:flex-row gap-8 md:gap-4 bg-violet-200/20 outline-violet-700/20 outline outline-1 rounded-2xl transition-all shadow-none">
<div className="md:border-r md:pr-4">
<h3 className="text-lg font-medium mb-1">Roo Code Provider</h3>
<div className="text-sm text-muted-foreground">
<p className="">
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
Cloud provider curated models to work with Roo with no markup, including the
latest Gemini, GPT and Claude. Paid with credits.
<Link href="/provider/pricing" className="underline hover:no-underline ml-1">
See per model pricing.
</Link>
</p>
</div>
</div>
<div className="">
<h3 className="text-lg font-medium mb-1">Credits</h3>
<p className="text-sm text-muted-foreground">
Credits are pre-paid, in dollars, and are deducted with usage for inference and Cloud
Agent runs. You&apos;re always in control of your spend, no surprises.
</p>
</div>
</div>
</div>
</section>
@ -249,7 +249,7 @@ export default function PricingPage() {
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
</div>
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
<div className="rounded-lg border border-border bg-card p-6">
<div className="rounded-xl border border-border bg-card p-6">
<h3 className="font-semibold">Wait, is Roo Code free or not?</h3>
<p className="mt-2 text-sm text-muted-foreground">
Yes! The Roo Code VS Code extension is open source and free forever. The extension acts
@ -257,7 +257,7 @@ export default function PricingPage() {
Code Cloud.
</p>
</div>
<div className="rounded-lg border border-border bg-card p-6">
<div className="rounded-xl border border-border bg-card p-6">
<h3 className="font-semibold">Is there a free trial?</h3>
<p className="mt-2 text-sm text-muted-foreground">
Yes, all paid plans come with a 14-day free trial to try out functionality.
@ -266,12 +266,25 @@ export default function PricingPage() {
To use Cloud Agents, you can buy credits.
</p>
</div>
<div className="rounded-lg border border-border bg-card p-6">
<h3 className="font-semibold">How do Cloud Agent credits work?</h3>
<div className="rounded-xl border border-border bg-card p-6">
<h3 className="font-semibold">How do credits work?</h3>
<p className="mt-2 text-sm text-muted-foreground">
Cloud Agents are a version of Roo running in the cloud without depending on your IDE.
You can run as many as you want, and bring your own inference provider key.
Roo Code Cloud credits can be used in two ways:
</p>
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
<li>To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)</li>
<li>
To pay for AI model inference costs (
<a
href="https://app.roocode.com/provider/pricing"
target="_blank"
rel="noopener noreferrer"
className="underline">
varies by model
</a>
)
</li>
</ul>
<p className="mt-2 text-sm text-muted-foreground">
To cover our infrastructure costs, we charge ${PRICE_CREDITS}/hour while the agent is
running (independent of inference costs).
@ -280,25 +293,38 @@ export default function PricingPage() {
There are no markups, no tiers, no dumbing-down of models to increase our profit.
</p>
</div>
<div className="rounded-lg border border-border bg-card p-6">
<div className="rounded-xl border border-border bg-card p-6">
<h3 className="font-semibold">Do I need a credit card for the free trial?</h3>
<p className="mt-2 text-sm text-muted-foreground">
Yes, but you won&apos;t be charged until your trial ends, except for credit purchases.
</p>
<p className="mt-2 text-sm text-muted-foreground">You can cancel anytime with one click.</p>
</div>
<div className="rounded-lg border border-border bg-card p-6">
<div className="rounded-xl border border-border bg-card p-6">
<h3 className="font-semibold">What payment methods do you accept?</h3>
<p className="mt-2 text-sm text-muted-foreground">
We accept all major credit cards, debit cards, and can arrange invoice billing for
Enterprise customers.
</p>
</div>
<div className="rounded-lg border border-border bg-card p-6">
<h3 className="font-semibold">Can I change plans anytime?</h3>
<div className="rounded-xl border border-border bg-card p-6">
<h3 className="font-semibold">Can I cancel or change plans?</h3>
<p className="mt-2 text-sm text-muted-foreground">
Yes, you can upgrade or downgrade your plan at any time. Changes will be reflected in
your next billing cycle.
Yes, you can upgrade, downgrade or cancel your plan at any time. Changes will be
reflected in your next billing cycle.
</p>
</div>
<div className="rounded-xl border border-border bg-card p-6 md:col-span-2">
<h3 className="font-semibold">
What if I have enterprise-level needs like SAML/SCIM, large-scale deployments, specific
integrations and custom terms?
</h3>
<p className="mt-2 text-sm text-muted-foreground">
We have an Enterprise plan which can be a fit. Please{" "}
<Link href="/enterprise#contact" className="underline hover:no-underline">
reach out to our sales team
</Link>{" "}
to discuss it.
</p>
</div>
</div>

View file

@ -0,0 +1,263 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { ModelCard } from "./pricing/components/model-card"
import { Model, ModelWithTotalPrice, ModelsResponse, SortOption } from "@/lib/types/models"
import Link from "next/link"
import { ChevronDown, CircleX, Cloud, Loader, LoaderCircle, Puzzle, Search } from "lucide-react"
const API_URL = "https://api.roocode.com/proxy/v1/models?include_paid=true"
const faqs = [
{
question: "What are AI model providers?",
answer: "AI model providers offer various language models with different capabilities and pricing.",
},
{
question: "What is the Roo Code Cloud Provider?",
answer: (
<>
<p>This is our very own model provider, optimized to work seamlessly with Roo Code Cloud.</p>
<p>You don&apos;t have to use it to use Roo Code, but it&apos;s the easiest way to do it.</p>
</>
),
},
{
question: "Do I have to use the Roo Code Cloud Provider to use the Roo Code products?",
answer: "Not at all! You can bring your own provider key, no problem. This is just meant to make it easier.",
},
{
question: "How is pricing calculated?",
answer: "Pricing is based on token usage for input and output, measured per million tokens, like pretty much any other provider out there.",
},
{
question: "How is my data treated?",
answer: "The Roo Code Cloud provider doesn't keep any of your data, the service only aims to make it easier to use Roo Code. Each model vendor has their own privacy policy though, and usually free models use your data for training, so keep that in mind.",
},
{
question: "How much does the Roo Code Cloud service cost?",
answer: (
<>
Our{" "}
<Link href="/pricing" className="underline hover:no-underline">
service pricing is here.
</Link>
</>
),
},
]
function calculateTotalPrice(model: Model): number {
return parseFloat(model.pricing.input) + parseFloat(model.pricing.output)
}
function enrichModelWithTotalPrice(model: Model): ModelWithTotalPrice {
return {
...model,
totalPrice: calculateTotalPrice(model),
}
}
export default function ProviderPage() {
const [models, setModels] = useState<ModelWithTotalPrice[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("alphabetical")
useEffect(() => {
async function fetchModels() {
try {
setLoading(true)
setError(null)
const response = await fetch(API_URL)
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.statusText}`)
}
const data: ModelsResponse = await response.json()
const enrichedModels = data.data.map(enrichModelWithTotalPrice)
setModels(enrichedModels)
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred while fetching models")
} finally {
setLoading(false)
}
}
fetchModels()
}, [])
const filteredAndSortedModels = useMemo(() => {
// Filter out deprecated models
let filtered = models.filter((model) => !model.deprecated)
// Filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter((model) => {
return (
model.name.toLowerCase().includes(query) ||
model.owned_by?.toLowerCase().includes(query) ||
model.description.toLowerCase().includes(query)
)
})
}
// Sort filtered results
const sorted = [...filtered]
switch (sortOption) {
case "alphabetical":
sorted.sort((a, b) => a.name.localeCompare(b.name))
break
case "price-asc":
sorted.sort((a, b) => a.totalPrice - b.totalPrice)
break
case "price-desc":
sorted.sort((a, b) => b.totalPrice - a.totalPrice)
break
case "context-window-asc":
sorted.sort((a, b) => a.context_window - b.context_window)
break
case "context-window-desc":
sorted.sort((a, b) => b.context_window - a.context_window)
break
}
return sorted
}, [models, searchQuery, sortOption])
// Count non-deprecated models for the display
const nonDeprecatedCount = useMemo(() => models.filter((model) => !model.deprecated).length, [models])
return (
<>
<section className="relative overflow-hidden py-16">
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">The Roo Code Cloud Provider</h1>
<p className="mx-auto mt-4 max-w-3xl md:text-lg text-muted-foreground">
The easiest way to use Roo Code (in the{" "}
<Link href="/cloud" className="underline hover:no-underline">
<Cloud className="inline size-5 mx-1 -mt-0.5" />
Cloud
</Link>{" "}
or the{" "}
<Link href="/extension" className="underline hover:no-underline">
<Puzzle className="inline size-5 mx-1 -mt-0.5" />
Extension
</Link>
) with the best free and paid models no separate accounts, no messing with API keys.
</p>
<p className="mx-auto mt-2 max-w-2xl md:text-lg text-muted-foreground">
And you can always bring your own key (
<Link href="#faq" className="underline hover:no-underline">
FAQ
</Link>
).
</p>
</div>
</div>
</section>
<section className="py-10 relative border-t border-b">
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/0 via-violet-500/10 to-violet-500/0 dark:from-blue-500/10 dark:via-cyan-500/10 dark:to-purple-500/10" />
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-4xl">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder="Search models..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded-full border border-input bg-background px-10 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<div className="text-sm cursor-default text-muted-foreground absolute bg-background right-0 top-0 m-0.5 px-3 py-2 rounded-full">
{filteredAndSortedModels.length} of {nonDeprecatedCount} models
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className="flex items-center gap-2 relative">
<select
id="sort"
value={sortOption}
onChange={(e) => setSortOption(e.target.value as SortOption)}
className="rounded-full cursor-pointer border border-input bg-background hover:bg-muted pl-4 w-full md:w-auto pr-9 py-2.5 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative appearance-none">
<option value="alphabetical">Alphabetical</option>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="context-window-asc">Context Window: Small to Large</option>
<option value="context-window-desc">Context Window: Large to Small</option>
</select>
<ChevronDown className="size-4 absolute right-3" />
</div>
</div>
</div>
</div>
</div>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 ">
<div className="mx-auto max-w-6xl">
{loading && (
<div className="text-center pt-12 space-y-2 mb-4">
<LoaderCircle className="size-8 text-muted-foreground mx-auto animate-spin" />
<p className="text-lg">Loading model list...</p>
</div>
)}
{error && (
<div className="text-center pt-12 space-y-2">
<CircleX className="size-8 text-muted-foreground mx-auto mb-4" />
<p className="text-lg">Oops, couldn&apos;t load the model list.</p>
<p className="text-muted-foreground">Try again in a bit please.</p>
</div>
)}
{!loading && !error && filteredAndSortedModels.length === 0 && (
<div className="text-center pt-12 space-y-2">
<Loader className="size-8 text-muted-foreground mx-auto mb-4" />
<p className="text-lg">No models match your search.</p>
<p className="text-muted-foreground">
Keep in mind we don&apos;t have every model under the sun only the ones we think
are worth using.
<br />
You can always use a third-party provider to access a wider selection.
</p>
</div>
)}
{!loading && !error && filteredAndSortedModels.length > 0 && (
<div className="grid gap-4 pt-8 md:grid-cols-2 lg:grid-cols-3">
{filteredAndSortedModels.map((model) => (
<ModelCard key={model.id} model={model} />
))}
</div>
)}
</div>
</div>
</section>
{/* FAQ Section */}
<section className="bg-background my-16 relative z-50">
<a id="faq" />
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
</div>
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
{faqs.map((faq, index) => (
<div key={index} className="rounded-2xl border border-border bg-card p-6">
<h3 className="font-semibold">{faq.question}</h3>
<p className="mt-2 text-sm text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
</div>
</section>
</>
)
}

View file

@ -0,0 +1,190 @@
import { ModelWithTotalPrice } from "@/lib/types/models"
import { formatCurrency, formatTokens } from "@/lib/formatters"
import {
ArrowLeftToLine,
ArrowRightToLine,
Building2,
Check,
Expand,
Gift,
HardDriveDownload,
HardDriveUpload,
RulerDimensionLine,
ChevronDown,
ChevronUp,
} from "lucide-react"
import { useState } from "react"
interface ModelCardProps {
model: ModelWithTotalPrice
}
export function ModelCard({ model }: ModelCardProps) {
// Prices are per token, multiply by 1M to get price per million tokens
const inputPrice = parseFloat(model.pricing.input) * 1_000_000
const outputPrice = parseFloat(model.pricing.output) * 1_000_000
const cacheReadPrice = parseFloat(model.pricing.input_cache_read || "0") * 1_000_000
const cacheWritePrice = parseFloat(model.pricing.input_cache_write || "0") * 1_000_000
const free = model.tags.includes("free")
// Filter tags to only show vision and reasoning
const displayTags = model.tags.filter((tag) => tag === "vision" || tag === "reasoning")
// Mobile collapsed/expanded state
const [expanded, setExpanded] = useState(false)
return (
<div
className={[
"relative cursor-default px-8 pt-7 pb-5 flex flex-col justify-start bg-background border rounded-3xl transition-all hover:shadow-xl",
// On mobile, visually hint at expandability
"sm:cursor-default",
].join(" ")}>
{/* Header: always visible */}
<div className="mb-4">
<h3 className="text-xl font-semibold tracking-tight mb-2 flex items-center gap-2 justify-between">
{model.name}
{free && (
<span className="inline-flex items-center text-sm font-medium text-green-500">
<Gift className="size-4 mr-1" />
Free!
</span>
)}
</h3>
<p
className={[
"text-sm text-muted-foreground",
// On mobile + collapsed: clamp description
"sm:line-clamp-none",
!expanded ? "line-clamp-2" : "",
]
.join(" ")
.trim()}>
{model.description}
</p>
</div>
{/* Content - pinned to bottom */}
<div className="overflow-x-auto mt-auto">
<table className="w-full text-xs">
<tbody>
{/* Provider: always visible if present */}
{model.owned_by && (
<tr className="border-b border-border">
<td className="py-1.5 font-medium text-muted-foreground">
<Building2 className="size-4 inline-block mr-1.5" />
Provider
</td>
<td className="py-1.5 text-right">{model.owned_by}</td>
</tr>
)}
{/* Context Window: always visible */}
<tr className="border-b border-border">
<td className="py-1.5 font-medium text-muted-foreground">
<RulerDimensionLine className="size-4 inline-block mr-1.5" />
Context Window
</td>
<td className="py-1.5 text-right font-mono">{formatTokens(model.context_window)}</td>
</tr>
{/* Max Output Tokens: always visible on >=sm, expandable on mobile */}
<tr
className={["border-b border-border", expanded ? "table-row" : "hidden sm:table-row"].join(
" ",
)}>
<td className="py-1.5 font-medium text-muted-foreground">
<Expand className="size-4 inline-block mr-1.5" />
Max Output Tokens
</td>
<td className="py-1.5 text-right font-mono">{formatTokens(model.max_tokens)}</td>
</tr>
{/* Input Price: always visible */}
<tr className="border-b border-border">
<td className="py-1.5 font-medium text-muted-foreground">
<ArrowRightToLine className="size-4 inline-block mr-1.5" />
Input Price
</td>
<td className="py-1.5 text-right">
{inputPrice === 0 ? "Free" : `${formatCurrency(inputPrice)}/1M tokens`}
</td>
</tr>
{/* Output Price: always visible */}
<tr
className={[
"border-b border-border",
// Add subtle separation from toggle on mobile
].join(" ")}>
<td className="py-1.5 font-medium text-muted-foreground">
<ArrowLeftToLine className="size-4 inline-block mr-1.5" />
Output Price
</td>
<td className="py-1.5 text-right">
{outputPrice === 0 ? "Free" : `${formatCurrency(outputPrice)}/1M tokens`}
</td>
</tr>
{/* Cache pricing: only visible on mobile when expanded, always visible on >=sm */}
{cacheReadPrice > 0 && (
<tr
className={[
"border-b border-border",
expanded ? "table-row" : "hidden sm:table-row",
].join(" ")}>
<td className="py-1.5 font-medium text-muted-foreground">
<HardDriveUpload className="size-4 inline-block mr-1.5" />
Cache Read
</td>
<td className="py-1.5 text-right">{formatCurrency(cacheReadPrice)}/1M tokens</td>
</tr>
)}
{cacheWritePrice > 0 && (
<tr
className={[
"border-b border-border",
expanded ? "table-row" : "hidden sm:table-row",
].join(" ")}>
<td className="py-1.5 font-medium text-muted-foreground">
<HardDriveDownload className="size-4 inline-block mr-1.5" />
Cache Write
</td>
<td className="py-1.5 text-right">{formatCurrency(cacheWritePrice)}/1M tokens</td>
</tr>
)}
{/* Tags row: only show if there are vision or reasoning tags */}
{displayTags.length > 0 && (
<tr className={[expanded ? "table-row" : "hidden sm:table-row"].join(" ")}>
<td className="py-1.5 font-medium text-muted-foreground align-top">Features</td>
<td className="py-1.5">
{displayTags.map((tag) => (
<span key={tag} className="flex justify-end items-center text-xs capitalize">
<Check className="size-3 m-1" />
{tag}
</span>
))}
</td>
</tr>
)}
{/* Mobile-only toggle row */}
<tr className="sm:hidden">
<td colSpan={2} className="pt-3">
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="w-full inline-flex items-center justify-center gap-1.5 text-xs font-medium text-primary">
{expanded ? "Less" : "More"}
{expanded ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}

View file

@ -32,8 +32,9 @@ export const content: AgentPageContent = {
],
},
cta: {
buttonText: "Start 14-day Free Trial",
disclaimer: "(cancel anytime)",
buttonText: "Try now for free",
disclaimer: "",
tracking: "&agent=reviewer",
},
},
howItWorks: {
@ -87,6 +88,6 @@ export const content: AgentPageContent = {
cta: {
heading: "Ready for better code reviews?",
description: "Start finding the issues that matter with AI-powered reviews built for depth, not cost-cutting.",
buttonText: "Start 14-day Free Trial",
buttonText: "Try now for free",
},
}

View file

@ -32,8 +32,9 @@ export const content: AgentPageContent = {
],
},
cta: {
buttonText: "Start 14-day Free Trial",
disclaimer: "(cancel anytime)",
buttonText: "Try now for free",
disclaimer: "",
tracking: "&agent=reviewer",
},
},
howItWorks: {
@ -87,6 +88,6 @@ export const content: AgentPageContent = {
cta: {
heading: "Ready for better code reviews?",
description: "Start finding the issues that matter with AI-powered reviews built for depth, not cost-cutting.",
buttonText: "Start 14-day Free Trial",
buttonText: "Try now for free",
},
}

View file

@ -16,8 +16,7 @@ import Image from "next/image"
import Link from "next/link"
import { Button } from "@/components/ui"
import { AnimatedBackground } from "@/components/homepage"
import { AgentCarousel } from "@/components/reviewer/agent-carousel"
import { AnimatedBackground, UseExamplesSection } from "@/components/homepage"
import { EXTERNAL_LINKS } from "@/lib/constants"
import { type AgentPageContent, type IconName } from "./agent-page-content"
@ -93,7 +92,7 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
className="w-full sm:w-auto backdrop-blur-sm border hover:shadow-[0_0_20px_rgba(59,130,246,0.5)] transition-all duration-300"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
href={`${EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}${content.hero.cta.tracking}`}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center">
@ -127,6 +126,9 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
{/* How It Works Section */}
<section className="relative overflow-hidden border-t border-border py-32">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
</div>
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
<div>
@ -161,6 +163,9 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
{/* Why Better Section */}
<section className="relative overflow-hidden border-t border-border py-32">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
</div>
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
<div>
@ -197,8 +202,7 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
</div>
</section>
{/* Agent Carousel */}
<AgentCarousel currentAgent={content.agentName} />
<UseExamplesSection agentTitle={true} />
{/* CTA Section */}
<section className="py-20">
@ -214,11 +218,11 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
className="bg-black text-white hover:bg-gray-800 hover:shadow-lg hover:shadow-black/20 dark:bg-white dark:text-black dark:hover:bg-gray-200 dark:hover:shadow-white/20 transition-all duration-300"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
href={`${EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}${content.hero.cta.tracking}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
{content.cta.buttonText}
{content.hero.cta.buttonText}
<ArrowRight className="ml-2 h-4 w-4" />
</a>
</Button>

View file

@ -21,7 +21,6 @@ export type IconName =
* serialization from Server Components to Client Components.
*/
export interface AgentPageContent {
/** The agent name used for the carousel display */
agentName: string
hero: {
/** Optional icon name to display in the hero section */
@ -45,6 +44,7 @@ export interface AgentPageContent {
cta: {
buttonText: string
disclaimer: string
tracking: string
}
}
howItWorks: {

View file

@ -13,7 +13,7 @@ import { EXTERNAL_LINKS } from "@/lib/constants"
import { useLogoSrc } from "@/lib/hooks/use-logo-src"
import { ScrollButton } from "@/components/ui"
import ThemeToggle from "@/components/chromes/theme-toggle"
import { ChevronDown, Cloud, X } from "lucide-react"
import { Brain, ChevronDown, Cloud, Puzzle, X } from "lucide-react"
interface NavBarProps {
stars: string | null
@ -25,7 +25,7 @@ export function NavBar({ stars, downloads }: NavBarProps) {
const logoSrc = useLogoSrc()
return (
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
<header className="sticky font-light top-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
<div className="container flex h-16 items-center justify-between px-4 sm:px-6 lg:px-8">
<div className="flex items-center">
<Link href="/" className="flex items-center">
@ -34,94 +34,117 @@ export function NavBar({ stars, downloads }: NavBarProps) {
</div>
{/* Desktop Navigation */}
<nav className="grow ml-6 hidden text-sm font-medium md:flex md:items-center">
<ScrollButton
targetId="product"
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground max-lg:hidden">
Extension
</ScrollButton>
<Link
href="/cloud"
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Cloud
</Link>
<a
href={EXTERNAL_LINKS.DOCUMENTATION}
target="_blank"
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Docs
</a>
<Link
href="/pricing"
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Pricing
</Link>
<nav className="grow ml-6 hidden text-sm md:flex md:items-center">
{/* Product Dropdown */}
<div className="relative group">
<button className="flex items-center px-4 py-6 gap-1 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Product
<ChevronDown className="size-3 ml-1 mt-0.5" />
</button>
<div className="absolute left-0 top-12 mt-2 w-[260px] rounded-md border border-border bg-background py-1 shadow-lg opacity-0 -translate-y-2 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-200">
<Link
href="/extension"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
<Puzzle className="size-3 inline mr-2 -mt-0.5" />
Roo Code VS Code Extension
</Link>
<Link
href="/cloud"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
<Cloud className="size-3 inline mr-2 -mt-0.5" />
Roo Code Cloud
</Link>
<Link
href="/provider"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
<Brain className="size-3 inline mr-2 -mt-0.5" />
Roo Code Cloud Provider
</Link>
</div>
</div>
{/* Resources Dropdown */}
<div className="relative group">
<button className="flex items-center px-4 py-6 gap-1 text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
<button className="flex items-center px-4 py-6 gap-1 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Resources
<ChevronDown className="size-3" />
<ChevronDown className="size-3 ml-1 mt-0.5" />
</button>
{/* Dropdown Menu */}
<div className="absolute left-0 top-12 mt-2 w-40 rounded-md border border-border bg-background py-1 shadow-lg opacity-0 -translate-y-2 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-200">
<ScrollButton
targetId="faq"
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground">
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
FAQ
</ScrollButton>
<Link
href="/evals"
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground">
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
Evals
</Link>
<a
href={EXTERNAL_LINKS.DISCORD}
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground">
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
Discord
</a>
<a
href={EXTERNAL_LINKS.SECURITY}
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setIsMenuOpen(false)}>
Trust Center
</a>
</div>
</div>
<a
href={EXTERNAL_LINKS.DOCUMENTATION}
target="_blank"
className="px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Docs
</a>
<Link
href="/pricing"
className="px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Pricing
</Link>
</nav>
<div className="hidden md:flex md:items-center md:space-x-4">
<div className="flex flex-row space-x-2">
<div className="hidden md:flex md:items-center md:space-x-4 flex-shrink-0 font-medium">
<div className="flex flex-row space-x-2 flex-shrink-0">
<ThemeToggle />
<Link
href={EXTERNAL_LINKS.GITHUB}
target="_blank"
className="hidden items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground md:flex">
className="hidden items-center gap-1.5 text-sm hover:text-foreground md:flex whitespace-nowrap">
<RxGithubLogo className="h-4 w-4" />
{stars !== null && <span>{stars}</span>}
</Link>
</div>
<a
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
target="_blank"
rel="noopener noreferrer"
className="hidden items-center gap-1.5 rounded-md py-2 text-sm border border-primary-background px-4 text-primary-background transition-all duration-200 hover:shadow-lg hover:scale-105 lg:flex">
Log in
</a>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
target="_blank"
rel="noopener noreferrer"
className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex">
Sign Up
</a>
<Link
href={EXTERNAL_LINKS.MARKETPLACE}
target="_blank"
className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex">
className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex whitespace-nowrap">
<VscVscode className="-mr-[2px] mt-[1px] h-4 w-4" />
<span>
Install <span className="font-black max-lg:text-xs">&middot;</span>
</span>
{downloads !== null && <span>{downloads}</span>}
</Link>
<a
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
target="_blank"
rel="noopener noreferrer"
className="hidden items-center gap-1.5 rounded-md py-2 text-sm border border-primary-background px-4 font-medium text-primary-background transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex">
<Cloud className="inline h-4 w-4" />
Log in
</a>
</div>
{/* Mobile Menu Button */}
@ -141,18 +164,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
<nav className="flex flex-col justify-between h-full pb-16 overflow-y-auto bg-background pointer-events-auto">
{/* Main navigation items */}
<div className="grow-1 py-4 font-semibold text-lg">
<ScrollButton
targetId="product"
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
onClick={() => setIsMenuOpen(false)}>
Extension
</ScrollButton>
<Link
href="/cloud"
className="block w-full p-5 text-left text-foreground active:opacity-50"
onClick={() => setIsMenuOpen(false)}>
Cloud
</Link>
<a
href={EXTERNAL_LINKS.DOCUMENTATION}
target="_blank"
@ -167,6 +178,31 @@ export function NavBar({ stars, downloads }: NavBarProps) {
Pricing
</Link>
{/* Product Section */}
<div className="mt-4 w-full">
<div className="px-5 pb-2 pt-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Product
</div>
<Link
href="/extension"
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
onClick={() => setIsMenuOpen(false)}>
Roo Code VS Code Extension
</Link>
<Link
href="/cloud"
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
onClick={() => setIsMenuOpen(false)}>
Roo Code Cloud
</Link>
<Link
href="/provider"
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
onClick={() => setIsMenuOpen(false)}>
Roo Code Cloud Provider
</Link>
</div>
{/* Resources Section */}
<div className="mt-4 w-full">
<div className="px-5 pb-2 pt-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
@ -209,7 +245,7 @@ export function NavBar({ stars, downloads }: NavBarProps) {
<Link
href={EXTERNAL_LINKS.GITHUB}
target="_blank"
className="inline-flex items-center gap-2 rounded-md p-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
className="inline-flex items-center gap-2 rounded-md p-3 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setIsMenuOpen(false)}>
<RxGithubLogo className="h-6 w-6" />
{stars !== null && <span>{stars}</span>}
@ -220,21 +256,30 @@ export function NavBar({ stars, downloads }: NavBarProps) {
<Link
href={EXTERNAL_LINKS.MARKETPLACE}
target="_blank"
className="inline-flex items-center gap-2 rounded-md p-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
className="inline-flex items-center gap-2 rounded-md p-3 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setIsMenuOpen(false)}>
<VscVscode className="h-6 w-6" />
{downloads !== null && <span>{downloads}</span>}
</Link>
</div>
<a
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 rounded-lg border border-primary bg-background p-4 mx-4 mb-4 text-base font-semibold text-primary"
onClick={() => setIsMenuOpen(false)}>
<Cloud className="h-5 w-5" />
Log in
</a>
<div className="flex gap-2 px-4 pb-4">
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 rounded-full border border-primary bg-foreground p-4 w-full text-base font-semibold text-background"
onClick={() => setIsMenuOpen(false)}>
Sign up
</a>
<a
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 rounded-full border border-primary bg-background p-4 w-full text-base font-semibold text-primary"
onClick={() => setIsMenuOpen(false)}>
Log in
</a>
</div>
</div>
</nav>
</div>

View file

@ -0,0 +1,109 @@
import { Bot, Settings2, ShieldCheck } from "lucide-react"
export function CloudSection() {
return (
<section className="py-24 bg-muted/30">
<div className="container px-4 mx-auto sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">Asynchronous Engineering.</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Stop watching the cursor. Deploy specialized agents to work while you sleep.
</p>
</div>
{/* Pipeline Diagram Visual */}
<div className="flex flex-wrap items-center justify-center gap-4 mb-20 text-sm font-medium">
<div className="px-4 py-2 bg-background border border-border rounded-lg">Ticket</div>
<div className="w-8 h-[1px] bg-border" />
<div className="px-4 py-2 bg-purple-500/10 border border-purple-500/20 text-purple-500 rounded-lg">
Planner Agent
</div>
<div className="w-8 h-[1px] bg-border" />
<div className="px-4 py-2 bg-blue-500/10 border border-blue-500/20 text-blue-500 rounded-lg">
Coder Agent
</div>
<div className="w-8 h-[1px] bg-border" />
<div className="px-4 py-2 bg-green-500/10 border border-green-500/20 text-green-500 rounded-lg">
GitHub PR
</div>
</div>
<div className="grid md:grid-cols-2 gap-12">
<div className="bg-background p-8 rounded-2xl border border-border shadow-sm">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-500/10 rounded-lg">
<ShieldCheck className="h-6 w-6 text-blue-500" />
</div>
<h3 className="text-xl font-bold">Purpose-Built Agents (Safety)</h3>
</div>
<h4 className="text-lg font-semibold mb-2">Zero Drift via Role Constraints</h4>
<p className="text-muted-foreground mb-6">
Fear of agents going haywire is solved by architecture, not prompt engineering. Cloud Agents
enforce the strict Modes you use locally.
</p>
<ul className="space-y-4">
<li className="flex gap-3 items-start">
<Bot className="h-5 w-5 text-purple-500 mt-0.5" />
<div>
<span className="font-bold">The Planner:</span>
<span className="text-muted-foreground ml-2">
Maps dependencies. Read-Only access.
</span>
</div>
</li>
<li className="flex gap-3 items-start">
<Bot className="h-5 w-5 text-blue-500 mt-0.5" />
<div>
<span className="font-bold">The Builder:</span>
<span className="text-muted-foreground ml-2">
Writes code based on the plan. Scoped file access.
</span>
</div>
</li>
<li className="flex gap-3 items-start">
<Bot className="h-5 w-5 text-green-500 mt-0.5" />
<div>
<span className="font-bold">The Reviewer:</span>
<span className="text-muted-foreground ml-2">
Analyzes diffs. Cannot push to main.
</span>
</div>
</li>
</ul>
</div>
<div className="bg-background p-8 rounded-2xl border border-border shadow-sm">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-purple-500/10 rounded-lg">
<Settings2 className="h-6 w-6 text-purple-500" />
</div>
<h3 className="text-xl font-bold">Orchestrated Configuration</h3>
</div>
<h4 className="text-lg font-semibold mb-2">Optimize Your AI Workforce</h4>
<p className="text-muted-foreground mb-6">
Just as you choose models locally, you configure them for the cloud to balance performance
vs. cost.
</p>
<div className="p-4 bg-muted/50 rounded-lg border border-border">
<div className="text-sm font-mono text-muted-foreground mb-2">Config Example:</div>
<div className="space-y-2">
<div className="flex justify-between items-center p-2 bg-background rounded border border-border">
<span className="font-medium">Planner Agent</span>
<span className="text-xs bg-purple-500/10 text-purple-500 px-2 py-1 rounded">
o1-preview (Reasoning)
</span>
</div>
<div className="flex justify-between items-center p-2 bg-background rounded border border-border">
<span className="font-medium">Unit Test Agent</span>
<span className="text-xs bg-blue-500/10 text-blue-500 px-2 py-1 rounded">
Haiku (Speed/Cost)
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
)
}

View file

@ -7,13 +7,13 @@ const logos = ["Apple", "Netflix", "Microsoft", "Amazon", "ByteDance", "Rakuten"
export function CompanyLogos() {
return (
<div className="mt-14">
<div>
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="text-xs text-muted-foreground text-center mb-2 sm:text-left">
Making devs more productive at
className="text-xs text-muted-foreground mb-2 ">
Helping teams ship more at
</motion.p>
<div className="mt-4 flex flex-wrap items-center gap-6 justify-center sm:justify-start">
{logos.map((logo, index) => (
@ -25,7 +25,7 @@ export function CompanyLogos() {
<Image
width={0}
height={0}
className="h-[18px] w-auto overflow-clip opacity-70 dark:invert"
className="h-[20px] w-auto overflow-clip opacity-70 dark:invert"
src={`/logos/${logo.toLowerCase().replace(/\s+/g, "-")}.svg`}
alt={`${logo} Logo`}
/>

View file

@ -0,0 +1,37 @@
import { Button } from "@/components/ui"
import { ArrowRight, Download } from "lucide-react"
import { EXTERNAL_LINKS } from "@/lib/constants"
export function CTASection() {
return (
<section className="py-24 bg-muted/30 border-t border-border">
<div className="container px-4 mx-auto sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-8">Build faster. Solo or Together.</h2>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Button size="lg" className="w-full sm:w-auto h-12 px-8 text-base">
<a
href={EXTERNAL_LINKS.MARKETPLACE}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2">
<Download className="h-4 w-4" />
Install on VS Code
</a>
</Button>
<Button variant="outline" size="lg" className="w-full sm:w-auto h-12 px-8 text-base">
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2">
Try Cloud for Free
<ArrowRight className="h-4 w-4" />
</a>
</Button>
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,82 @@
import { GitMerge, Terminal, MessageSquare } from "lucide-react"
export function EcosystemSection() {
return (
<section className="py-24 bg-background">
<div className="container px-4 mx-auto sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-16">Integrated into your SDLC.</h2>
<div className="relative max-w-4xl mx-auto">
{/* Triangle Connection Lines - Absolute positioned */}
<div className="absolute inset-0 hidden md:block pointer-events-none">
<svg
className="w-full h-full"
viewBox="0 0 800 400"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M400 50 L150 350"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="2"
strokeDasharray="8 8"
/>
<path
d="M400 50 L650 350"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="2"
strokeDasharray="8 8"
/>
<path
d="M150 350 L650 350"
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="2"
strokeDasharray="8 8"
/>
</svg>
</div>
<div className="grid md:grid-cols-3 gap-8 relative z-10">
{/* Step 1: Dispatch */}
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-2xl bg-blue-500/10 flex items-center justify-center mb-6 border border-blue-500/20">
<MessageSquare className="h-8 w-8 text-blue-500" />
</div>
<div className="text-sm font-bold text-blue-500 mb-2">01. DISPATCH</div>
<h3 className="text-xl font-bold mb-3">Trigger Task</h3>
<p className="text-muted-foreground text-sm max-w-[250px]">
Trigger a task via @Roo in Slack or the VS Code terminal.
</p>
</div>
{/* Step 2: Execute */}
<div className="flex flex-col items-center md:-mt-12">
<div className="w-16 h-16 rounded-2xl bg-purple-500/10 flex items-center justify-center mb-6 border border-purple-500/20">
<Terminal className="h-8 w-8 text-purple-500" />
</div>
<div className="text-sm font-bold text-purple-500 mb-2">02. EXECUTE</div>
<h3 className="text-xl font-bold mb-3">Run Agents</h3>
<p className="text-muted-foreground text-sm max-w-[250px]">
Agents run in isolated, ephemeral docker containers.
</p>
</div>
{/* Step 3: Merge */}
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-2xl bg-green-500/10 flex items-center justify-center mb-6 border border-green-500/20">
<GitMerge className="h-8 w-8 text-green-500" />
</div>
<div className="text-sm font-bold text-green-500 mb-2">03. MERGE</div>
<h3 className="text-xl font-bold mb-3">Review PR</h3>
<p className="text-muted-foreground text-sm max-w-[250px]">
The output is always a standard GitHub Pull Request. You review code, not chat logs.
</p>
</div>
</div>
</div>
</div>
</section>
)
}

View file

@ -1,7 +1,17 @@
"use client"
import { motion } from "framer-motion"
import { Shield, Users2, ReplaceAll, Keyboard, LucideIcon, CheckCheck, GitPullRequest } from "lucide-react"
import {
Shield,
Users2,
ReplaceAll,
Keyboard,
LucideIcon,
CheckCheck,
GitPullRequest,
Boxes,
TextSearch,
} from "lucide-react"
import Image from "next/image"
export interface Feature {
@ -21,7 +31,8 @@ export const features: Feature[] = [
{
icon: ReplaceAll,
title: "Model-agnostic",
description: "Bring your own provider key or even run local inference — no markup, lock-in, no restrictions.",
description:
"Use the Roo Code cloud Provider, bring your own provider key or even run local inference — no markup, lock-in, no restrictions.",
logos: ["Anthropic", "OpenAI", "Gemini", "Grok", "Qwen", "Kimi", "Mistral", "Ollama"],
},
{
@ -31,16 +42,27 @@ export const features: Feature[] = [
"Control each action and make Roo as autonomous as you want as you build confidence. Or go YOLO and let it rip.",
},
{
icon: GitPullRequest,
title: "Proudly open source",
icon: Boxes,
title: "Large task coordination",
description:
"Community-driven and fully auditable: no throttling or surprises about what's happening behind the scenes.",
"Orchestrator mode handles large tasks by coordinating tasks for other agents, running for hours and delivering.",
},
{
icon: TextSearch,
title: "Performant with large codebases",
description: "Configurable integrated semantic search for quicker retrieval in large codebases.",
},
{
icon: Keyboard,
title: "Highly customizable",
description:
"Fine-tune settings for Roo to work for you, like inference context, model properties, slash commands and more.",
"Fine-tune settings for Roo to work for you, like inference context, model properties, slash commands and more. Most settings can be global or serialized in your repository.",
},
{
icon: GitPullRequest,
title: "Proudly open source",
description:
"Community-driven and fully auditable: no throttling or surprises about what's happening behind the scenes.",
},
{
icon: Shield,

View file

@ -6,3 +6,9 @@ export * from "./features"
export * from "./install-section"
export * from "./testimonials"
export * from "./whats-new-button"
export * from "./option-overview-section"
export * from "./pillars-section"
export * from "./cloud-section"
export * from "./ecosystem-section"
export * from "./cta-section"
export * from "./use-examples-section"

View file

@ -0,0 +1,99 @@
import { Laptop, Cloud, ArrowRight } from "lucide-react"
import { Button } from "../ui"
import { EXTERNAL_LINKS } from "@/lib/constants"
export function OptionOverviewSection() {
return (
<section className="py-24 bg-background">
<div className="container px-4 mx-auto sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
Different form factors for different ways of working.
</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Roo&apos;s always there to help you get stuff done.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 max-w-5xl mx-auto relative z-1">
<div className="absolute left-1/2 top-[140px] -translate-x-1/2 z-1">
<div className="rounded-full size-[300px] border-[10px] border-dashed animate-[spin_10s_linear_infinite] blur-[5px]" />
</div>
<div className="rounded-3xl bg-card outline outline-border/50 hover:outline-8 shadow-xl p-8 h-full group transition-all hover:shadow-2xl hover:shadow-blue-800/30 relative">
<div className="size-15 p-3 rounded-full flex items-center justify-center shadow-lg absolute -top-4 -right-2 transition-all outline outline-foreground/5 bg-card group group-hover:outline-3 group-hover:scale-105">
<Laptop className="size-8 text-blue-500" strokeWidth={1.5} />
</div>
<h3 className="text-2xl font-bold tracking-tight mb-2">Roo Code VS Code Extension</h3>
<p className="font-semibold text-blue-500 mb-4">For Individual Work</p>
<div className="text-muted-foreground mb-4">
<p>
Run Roo directly in VS Code (or any fork even Cursor!), stay close to the code and
control everything:
</p>
<ul className="list-inside my-4 space-y-1">
<li className="list-disc">Approve every action (or set it to auto-approve)</li>
<li className="list-disc">Manage the context window</li>
<li className="list-disc">Configure every detail</li>
<li className="list-disc">Preview changes live</li>
<li className="list-disc">Stick to your customized editor</li>
<li className="list-disc">Write code by hand (gasp!)</li>
</ul>
<p>
Ideal for real-time debugging or quick iteration where you need full, immediate control.
</p>
</div>
<Button size="lg" variant="default" className="bg-blue-600 hover:bg-blue-600/80">
<a
href={EXTERNAL_LINKS.MARKETPLACE}
target="_blank"
rel="noreferrer"
className="flex items-center justify-center">
Install now
<ArrowRight className="ml-2" />
</a>
</Button>
</div>
<div className="rounded-3xl bg-card outline outline-border/50 hover:outline-8 shadow-xl p-8 h-full group transition-all hover:shadow-2xl hover:shadow-blue-800/30 relative">
<div className="size-15 p-3 rounded-full flex items-center justify-center shadow-lg absolute -top-4 -right-2 transition-all outline outline-foreground/5 bg-card group group-hover:outline-3 group-hover:scale-105">
<Cloud className="size-8 text-violet-500" strokeWidth={1.5} />
</div>
<h3 className="text-2xl font-bold tracking-tight mb-2">Roo Code Cloud</h3>
<div className="font-semibold text-violet-500 mb-4">For Team Work with Agents</div>
<div className="text-muted-foreground mb-4">
<p>
Create your agent team in the Cloud, give them access to Github and start giving them
tasks:
</p>
<ul className="list-inside my-4 space-y-1">
<li className="list-disc">
Use agents like the Planner, Coder, Explainer, Reviewer and Fixer
</li>
<li className="list-disc">Choose your provider and model</li>
<li className="list-disc">
Create tasks from the Web and Slack (more integrations soon)
</li>
<li className="list-disc">Get PR Reviews (and fixes) directly on Github</li>
<li className="list-disc">Collaborate with co-workers</li>
</ul>
<p>
Ideal for kicking projects off, parallelizing execution and looping in the rest of your
team.
</p>
</div>
<Button size="lg" variant="default" className="bg-violet-600 hover:bg-violet-600/80">
<a href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME} className="flex items-center justify-center">
Try now for free
<ArrowRight className="ml-2" />
</a>
</Button>
</div>
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,202 @@
import { Brain, Keyboard, Shield, Users2, Map, Code, MessageCircleQuestion, Bug, TestTube } from "lucide-react"
import Image from "next/image"
import { Link } from "../ui"
const MODEL_LOGOS = [
"OpenRouter",
"Anthropic",
"OpenAI",
"Gemini",
"Grok",
"Bedrock",
"Moonshot",
"Qwen",
"Kimi",
"Mistral",
"Ollama",
]
const MODE_EXAMPLES = [
{
name: "Architect",
description: "Plans complex changes without making changes.",
icon: Map,
},
{
name: "Code",
description: "Implements, refactors and optimizes code.",
icon: Code,
},
{
name: "Ask",
description: "Explains functionality and program behavior.",
icon: MessageCircleQuestion,
},
{
name: "Debug",
description: "Diagnoses issues, traces failures, and proposes targeted, reliable fixes.",
icon: Bug,
},
{
name: "Test",
description: "Creates and improves performant tests without changing the actual functionality.",
icon: TestTube,
},
]
export function PillarsSection() {
return (
<section className="py-24 bg-muted/30 relative">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
</div>
<div className="container px-4 mx-auto sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
To trust an agent, you have to do it on your own terms.
</h2>
<p className="text-xl text-muted-foreground max-w-xl mx-auto">
Roo is designed from the ground up to give you the confidence to do ever more with AI.
</p>
</div>
<div className="flex flex-col md:grid md:grid-cols-8 gap-8">
<div className="relative md:col-span-3 h-full">
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
<div className="absolute -right-3 -top-5 bg-card border shadow-md rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
<Brain className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
</div>
<div>
<h3 className="text-2xl font-bold mb-1">Model-agnostic by design</h3>
<h4 className="font-semibold text-lg">Flexible and future-proof.</h4>
<div className="text-muted-foreground my-4 space-y-1">
<p>
&quot;The best model in the world&quot; changes every other week. Providers
throttle models with no warning. 1st-party coding agents only work with their
own models.
</p>
<p>Roo doesn&apos;t care.</p>
<p>
It works great with 10s of models, from frontier to open weight. Choose from{" "}
<Link href="/provider">the curated selection we offer at-cost</Link> or bring
your own key.
</p>
</div>
<div className="mt-6">
<span className="text-muted-foreground text-sm">
Compatible with dozens of providers
</span>
<div className="mt-4 flex flex-wrap items-center gap-4">
{MODEL_LOGOS.map((logo, index) => (
<Image
key={logo}
width={20}
height={20}
className="size-6 overflow-clip dark:invert"
style={{ opacity: 1.1 - index / MODEL_LOGOS.length }}
src={`/logos/${logo.toLowerCase()}.svg`}
alt={`${logo} Logo`}
/>
))}
</div>
</div>
</div>
</div>
</div>
<div className="relative col-span-5 h-full">
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
<div className="absolute -right-3 -top-5 bg-card border shadow-lg rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
<Users2 className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
</div>
<div>
<h3 className="text-2xl font-bold mb-1">Role-specific Modes</h3>
<h4 className="font-semibold text-lg">On-task and under control.</h4>
<div className="text-muted-foreground my-4 space-y-1">
<p>
As capable as they are, when let loose, LLMs hallucinate, cheat and can cause
real damage.
</p>
<p>
Roo&apos;s Modes keep models focused on a given task and limit their access to
tools which are relevant to their role, keeping the context window clearer and
avoiding surprises.
</p>
<p>
Modes are even smart enough to ask to switch to another when stepping outside
their responsibilities.
</p>
</div>
<div className="mt-6">
<span className="text-muted-foreground text-sm">Some examples</span>
<ul className="flex gap-2 flex-wrap mt-2">
{MODE_EXAMPLES.map((mode) => {
const Icon = mode.icon
return (
<li
key={mode.name}
className="rounded-lg border bg-border/40 w-full md:w-[30%] min-w-[200px] text-sm px-3 py-2 flex gap-1">
<Icon className="text-muted-foreground size-4 shrink-0 mt-0.5" />
<div>
<p className="font-semibold">{mode.name}</p>
<p className="text-muted-foreground text-xs">
{mode.description}
</p>
</div>
</li>
)
})}
</ul>
</div>
</div>
</div>
</div>
<div className="relative col-span-4 h-full">
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
<div className="absolute -right-3 -top-5 bg-card border shadow-lg rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
<Keyboard className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
</div>
<div>
<h3 className="text-2xl font-bold mb-1">Highly configurable</h3>
<h4 className="font-semibold text-lg">Make it fit your workflow.</h4>
<div className="text-muted-foreground my-4 space-y-1">
<p>
Developer tools need to fit like gloves. Highly tweakable,
keyboard-shortcut-heavy gloves.
</p>
<p>We made Roo thoughtfully configurable to fit your workflow as best it can.</p>
</div>
</div>
</div>
</div>
<div className="relative col-span-4 h-full">
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
<div className="absolute -right-3 -top-5 bg-card border shadow-lg rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
<Shield className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
</div>
<div>
<h3 className="text-2xl font-bold mb-1">Secure and transparent</h3>
<h4 className="font-semibold text-lg">Open source from the get go.</h4>
<div className="text-muted-foreground my-4 space-y-1">
<p>
The Roo Code Extension is{" "}
<Link target="_blank" href="https://github.com/Roo-Code-Inc/Roo-Code">
open source
</Link>{" "}
so you can see for yourself exactly what it&apos;s doing and we don&apos;t use
your data for training.
</p>
<p>
Plus we&apos;re fully SOC2 Type 2 compliant and follow industry-standard
security practices.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
)
}

View file

@ -193,11 +193,9 @@ export function Testimonials() {
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto mb-8 md:max-w-2xl text-center">
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
Developers <em>really</em> shipping with AI are using Roo Code
More than 1 million people are shipping with Roo.
</h2>
<p className="mt-6 text-lg text-muted-foreground">
Join more than 1M people revolutionizing their workflow worldwide
</p>
<p className="mt-6 text-lg text-muted-foreground">And they have some great things to say.</p>
</div>
<motion.div
@ -247,7 +245,11 @@ export function Testimonials() {
{testimonial.name}
</h3>
<p className="text-sm text-muted-foreground transition-colors duration-300 dark:text-muted-foreground/80">
{testimonial.role} at {testimonial.origin}
{testimonial.role !== "Reviewer" && (
<>
{testimonial.role} at {testimonial.origin}
</>
)}
{testimonial.stars && (
<span className="flex items-center mt-1">
{" "}

View file

@ -0,0 +1,438 @@
"use client"
import { useMemo, useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import {
LucideIcon,
Pointer,
Slack,
Github,
Code,
GitPullRequest,
Wrench,
Map,
MessageCircleQuestionMark,
CornerDownRight,
ChevronDown,
} from "lucide-react"
import Image from "next/image"
import { Button } from "../ui"
interface UseCase {
role: string
use: string
agent: UseCaseAgent
context: UseCaseSource
}
interface UseCaseSource {
name: string
icon: LucideIcon
}
interface UseCaseAgent {
name: string
icon: LucideIcon
}
interface PositionedUseCase extends UseCase {
layer: 1 | 2 | 3 | 4
position: { x: number; y: number }
scale: number
zIndex: number
avatar: string
}
const SOURCES = {
slack: {
name: "Slack",
icon: Slack,
},
web: {
name: "Web",
icon: Pointer,
},
github: {
name: "Github",
icon: Github,
},
extension: {
name: "Extension",
icon: Code,
},
}
const AGENTS = {
explainer: {
name: "Explainer",
icon: MessageCircleQuestionMark,
},
planner: {
name: "Planner",
icon: Map,
},
coder: {
name: "Coder",
icon: Code,
},
reviewer: {
name: "Reviewer",
icon: GitPullRequest,
},
fixer: {
name: "Fixer",
icon: Wrench,
},
}
const USE_CASES: UseCase[] = [
{
role: "Frontend Developer",
use: "Take Lisa's feedback above and incorporate it into the landing page.",
agent: AGENTS.coder,
context: SOURCES.slack,
},
{
role: "Customer Success",
use: "What could be causing this bug as described by the customer?",
agent: AGENTS.explainer,
context: SOURCES.web,
},
{
role: "Backend Engineer",
use: "Create a migration denormalizing total_cost calculation and backfill the remainder.",
agent: AGENTS.coder,
context: SOURCES.extension,
},
{
role: "Security Engineer",
use: "Do we use any of the libraries mentioned in the thread?",
agent: AGENTS.explainer,
context: SOURCES.slack,
},
{
role: "Designer",
use: "Refactor the button component to use CSS variables",
agent: AGENTS.coder,
context: SOURCES.slack,
},
{
role: "Product Manager",
use: "How big of a change would it be to turn this from a yes/no to have 4 options?",
agent: AGENTS.coder,
context: SOURCES.web,
},
{
role: "QA Engineer",
use: "Write a Playwright test for the login flow failure case, extract existing mocks into shared.",
agent: AGENTS.coder,
context: SOURCES.github,
},
{
role: "DevOps Engineer",
use: "Update the Dockerfile to use Node 20 Alpine.",
agent: AGENTS.fixer,
context: SOURCES.slack,
},
{
role: "Mobile Developer",
use: "Copy what we did in PR #4253 and apply to this component.",
agent: AGENTS.coder,
context: SOURCES.slack,
},
{
role: "Technical Writer",
use: "Generate JSDoc comments for the auth utility functions.",
agent: AGENTS.coder,
context: SOURCES.github,
},
{
role: "Junior Developer",
use: "Review this pull request for potential performance improvements.",
agent: AGENTS.reviewer,
context: SOURCES.github,
},
{
role: "Engineering Manager",
use: "Break down this user profile feature into technical tasks, grouped by skill.",
agent: AGENTS.planner,
context: SOURCES.web,
},
{
role: "Support Engineer",
use: "What's causing this stack trace? The customer is on MacOS 26.1.",
agent: AGENTS.explainer,
context: SOURCES.web,
},
{
role: "Frontend Developer",
use: "Make the navigation menu responsive on mobile devices.",
agent: AGENTS.coder,
context: SOURCES.web,
},
{
role: "Backend Engineer",
use: "Give me two architecture options for the notification system in this PRD.",
agent: AGENTS.planner,
context: SOURCES.web,
},
{
role: "Designer",
use: "Implement the loading spinner animation in CSS.",
agent: AGENTS.coder,
context: SOURCES.web,
},
{
role: "Customer Success",
use: "Write a script to find patterns in these CPU load logs.",
agent: AGENTS.coder,
context: SOURCES.slack,
},
{
role: "Full Stack Dev",
use: "Refactor user_preferences to use named columns instead of a single JSON blob",
agent: AGENTS.coder,
context: SOURCES.extension,
},
{
role: "QA Engineer",
use: "Automate the regression suite for the checkout process.",
agent: AGENTS.coder,
context: SOURCES.extension,
},
{
role: "DevOps Engineer",
use: "Understand why this build error only happens in prod and fix it.",
agent: AGENTS.coder,
context: SOURCES.extension,
},
{
role: "Product Marketer",
use: "What were the 5 most significant PRs merged in the past week?",
agent: AGENTS.explainer,
context: SOURCES.slack,
},
{
role: "Junior Developer",
use: "Explain how useEffect dependency arrays work here.",
agent: AGENTS.explainer,
context: SOURCES.extension,
},
{
role: "Senior Engineer",
use: "Check if this implementation follows the Single Responsibility Principle.",
agent: AGENTS.reviewer,
context: SOURCES.github,
},
]
// Seeded random number generator for consistent layout
function seededRandom(seed: number) {
let value = seed
return () => {
value = (value * 9301 + 49297) % 233280
return value / 233280
}
}
const LAYER_SCALES = {
1: 0.7,
2: 0.85,
3: 1.0,
4: 1.15,
}
function distributeItems(items: UseCase[]): PositionedUseCase[] {
const rng = seededRandom(Math.random() * 12345)
const zones = { rows: 7, cols: 4 }
const zoneWidth = 100 / zones.cols
const zoneHeight = 100 / zones.rows
// Create array of zone indices [0...19] and shuffle them
const zoneIndices = Array.from({ length: items.length }, (_, i) => i)
for (let i = zoneIndices.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1))
const temp = zoneIndices[i]!
zoneIndices[i] = zoneIndices[j]!
zoneIndices[j] = temp
}
return items.map((item, index) => {
// Assign to a random unique zone
const zoneIndex = zoneIndices[index]!
const row = Math.floor(zoneIndex / zones.cols)
const col = zoneIndex % zones.cols
// Distribute layers evenly
const layer = ((index % 4) + 1) as 1 | 2 | 3 | 4
// Calculate base position (center of zone)
const baseX = col * zoneWidth + zoneWidth / 2
const baseY = row * zoneHeight + zoneHeight / 2
// Add jitter (±35% of zone size to keep somewhat contained but messy)
const jitterX = (rng() - 0.5) * zoneWidth * 0.7
const jitterY = (rng() - 0.5) * zoneHeight * 0.7
return {
...item,
avatar: `/illustrations/user-faces/${index + 1}.jpg`,
layer,
position: {
x: baseX + jitterX,
y: baseY + jitterY,
},
scale: LAYER_SCALES[layer],
zIndex: layer,
}
})
}
function UseCaseCardContent({
item,
opacity = 1,
className = "",
}: {
item: UseCase & { avatar: string }
opacity?: number
className?: string
}) {
const ContextIcon: LucideIcon = item.context.icon
return (
<div
className={`rounded-xl outline outline-border/50 bg-card/80 backdrop-blur-sm p-3 md:p-4 shadow-xl transition-all hover:shadow-xl hover:outline-8 ${className}`}>
<div
className="text-sm flex items-center gap-2 font-medium text-violet-600 mb-1"
style={{ opacity: opacity }}>
<Image
src={item.avatar}
className="size-6 rounded-full outline-1 outline-border"
alt=""
width={18}
height={18}
unoptimized
/>
<span className="text-nowrap">{item.role}</span>
</div>
<div
className="text-[0.7em] flex flex-wrap items-center gap-1 text-muted-foreground mb-1"
style={{ opacity: opacity }}>
<CornerDownRight className="size-4 shrink-0 ml-3 -mt-1" />
<span className="text-nowrap font-mono">To {item.agent.name} Agent</span>
</div>
<div className="text-base font-light leading-tight my-1 ml-8" style={{ opacity: opacity }}>
{item.use}
</div>
<div
className="text-[0.7em] font-light text-muted-foreground leading-tight mt-2 ml-8"
style={{ opacity: opacity }}>
via <ContextIcon strokeWidth={1.5} className="size-3.5 inline ml-1" /> {item.context.name}
</div>
</div>
)
}
function DesktopUseCaseCard({ item }: { item: PositionedUseCase }) {
const opacity = Math.min(1, 0.5 + item.layer / 3)
return (
<motion.div
className="absolute w-[200px] cursor-default group"
style={{
left: `${item.position.x}%`,
top: `${item.position.y}%`,
zIndex: item.zIndex,
width: Math.round(300 + Math.random() * 100),
}}
initial={{ opacity: 0, scale: 0 }}
whileInView={{
opacity: 1,
scale: item.scale,
transition: {
duration: 0.1,
delay: 0, // Stagger by layer
},
}}
whileHover={{
scale: 1.3,
zIndex: 30,
}}
viewport={{ once: true }}
// Use standard CSS transform for the positioning to avoid conflicts with Framer Motion's scale
transformTemplate={({ scale }) => `translate(-50%, -50%) scale(${scale})`}>
<UseCaseCardContent
item={item}
opacity={opacity}
className={item.layer === 4 ? "shadow-lg border-border" : ""}
/>
</motion.div>
)
}
export function UseExamplesSection({ agentTitle = false }: { agentTitle?: boolean }) {
const positionedItems = useMemo(() => distributeItems(USE_CASES), [])
const [showAllMobile, setShowAllMobile] = useState(false)
return (
<section className="pt-24 bg-background overflow-hidden relative">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
<div className="absolute left-1/2 top-1/2 h-[700px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground/10 blur-[140px]" />
</div>
<div className="container px-4 mx-auto sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold tracking-tight mb-4">
{agentTitle ? (
<>
Part of the AI team to help your <em>entire</em> human team
</>
) : (
<>
The AI team to help your <em>entire</em> human team
</>
)}
</h2>
<p className="text-xl font-light text-muted-foreground max-w-2xl mx-auto">
Developers, PMs, Designers, Customer Success: everyone moves faster and more independently with
Roo.
</p>
</div>
{/* Mobile: Vertical Staggered List */}
<div className="md:hidden flex flex-col gap-2 px-2 pb-12 max-w-md mx-auto">
<AnimatePresence mode="popLayout">
{positionedItems.slice(0, showAllMobile ? undefined : 8).map((item, index) => (
<motion.div
key={item.use} // Use a unique key for proper animation tracking
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: (index % 8) * 0.1, duration: 0.4 }}
viewport={{ once: true, margin: "-50px" }}
className={`w-[90%] ${index % 2 === 0 ? "self-start" : "self-end"}`}>
<UseCaseCardContent item={item} />
</motion.div>
))}
</AnimatePresence>
{!showAllMobile && (
<div className="text-center mt-8 z-10">
<Button variant="outline" onClick={() => setShowAllMobile(true)}>
More
<ChevronDown />
</Button>
</div>
)}
</div>
{/* Desktop: Positioned Items Container */}
<div className="hidden md:block relative h-[800px] md:min-h-[800px] w-full max-w-6xl mx-auto">
{positionedItems.map((item, index) => (
<DesktopUseCaseCard key={index} item={item} />
))}
</div>
</div>
</section>
)
}

View file

@ -32,7 +32,6 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
// Initialize PostHog immediately on the client side
if (typeof window !== "undefined" && !posthog.__loaded) {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST
// Check if environment variables are set
if (!posthogKey) {
@ -43,19 +42,13 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
return
}
if (!posthogHost) {
console.warn(
"PostHog host URL is missing. Using default host. " +
"Please set NEXT_PUBLIC_POSTHOG_HOST in your .env file.",
)
}
// Check if user has already consented to cookies
const userHasConsented = hasConsent()
// Initialize PostHog with appropriate persistence based on consent
posthog.init(posthogKey, {
api_host: posthogHost || "https://us.i.posthog.com",
api_host: "https://ph.roocode.com",
ui_host: "https://us.posthog.com",
capture_pageview: false, // We handle pageview tracking manually
loaded: (posthogInstance) => {
if (process.env.NODE_ENV === "development") {

View file

@ -1,172 +0,0 @@
"use client"
import { useEffect } from "react"
import { motion } from "framer-motion"
import useEmblaCarousel from "embla-carousel-react"
import AutoPlay from "embla-carousel-autoplay"
import {
Bug,
FileText,
Gauge,
GitPullRequest,
Languages,
Microscope,
PocketKnife,
TestTube,
Wrench,
type LucideIcon,
} from "lucide-react"
// AI Agent types for the carousel
interface AIAgent {
icon: LucideIcon
name: string
page?: string
}
const aiAgents: AIAgent[] = [
{ icon: GitPullRequest, name: "PR Reviewer", page: "/reviewer" },
{ icon: Wrench, name: "PR Fixer", page: "/pr-fixer" },
{ icon: PocketKnife, name: "Generalist" },
{ icon: Bug, name: "Bug Fixer" },
{ icon: TestTube, name: "Test Engineer" },
{ icon: Microscope, name: "Security Auditor" },
{ icon: Gauge, name: "Performance Optimizer" },
{ icon: FileText, name: "Documentation Writer" },
{ icon: Languages, name: "String Translator" },
]
export function AgentCarousel({ currentAgent = "" }: { currentAgent?: string } = {}) {
const [emblaRef, emblaApi] = useEmblaCarousel(
{
loop: true,
align: "start",
watchDrag: true,
dragFree: false,
containScroll: false,
duration: 10000,
},
[
AutoPlay({
playOnInit: true,
delay: 0,
stopOnInteraction: false,
stopOnMouseEnter: false,
stopOnFocusIn: false,
}),
],
)
// Continuous scrolling effect
useEffect(() => {
if (!emblaApi) return
const autoPlay = emblaApi?.plugins()?.autoPlay as
| {
play?: () => void
}
| undefined
if (autoPlay?.play) {
autoPlay.play()
}
// Set up continuous scrolling
const interval = setInterval(() => {
if (emblaApi) {
emblaApi.scrollNext()
}
}, 30) // Smooth continuous scroll
return () => clearInterval(interval)
}, [emblaApi])
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
duration: 0.6,
ease: [0.21, 0.45, 0.27, 0.9],
},
},
}
// Duplicate the agents array for seamless infinite scroll
const displayAgents = [...aiAgents, ...aiAgents]
return (
<section className="relative overflow-hidden border-t border-border py-32">
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto mb-12 max-w-4xl text-center">
<div>
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
The first members of a whole new team
</h2>
<p className="mt-6 text-lg text-muted-foreground">
Architecture, coding, reviewing, testing, debugging, documenting, designing almost
everything we do today is mostly through our agents. Now we&apos;re bringing them to you.
</p>
<p className="mt-2 text-lg text-muted-foreground">
Roo&apos;s {currentAgent} isn&apos;t yet another single-purpose tool to add to your already
complicated stack. It&apos;s the first member of your AI-powered development team. More
agents are shipping soon.
</p>
</div>
</div>
<div className="relative mx-auto md:max-w-[1200px]">
<motion.div
className="relative -mx-4 md:mx-auto max-w-[1400px]"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}>
{/* Gradient Overlays */}
<div className="absolute inset-y-0 left-0 z-10 w-[10%] bg-gradient-to-r from-background to-transparent pointer-events-none md:w-[15%]" />
<div className="absolute inset-y-0 right-0 z-10 w-[10%] bg-gradient-to-l from-background to-transparent pointer-events-none md:w-[15%]" />
{/* Embla Carousel Container */}
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex pb-4">
{displayAgents.map((agent, index) => {
const Icon = agent.icon
return (
<div
key={`${agent.name}-${index}`}
className="relative min-w-0 flex-[0_0_45%] px-2 md:flex-[0_0_30%] md:px-4 lg:flex-[0_0_15%]">
<div className="group relative py-6 cursor-default">
<div
className="relative flex flex-col items-center justify-center rounded-full w-[150px] h-[150px] border border-border bg-background p-6 transition-all duration-500 ease-out shadow-xl
hover:scale-110 hover:-translate-y-2
hover:shadow-[0_20px_50px_rgba(39,110,226,0.25)] dark:hover:shadow-[0_20px_50px_rgba(59,130,246,0.25)]">
<Icon
strokeWidth={1}
className="size-9 mb-2 text-foreground transition-colors duration-300"
/>
<h3 className="text-center leading-tight tracking-tight transition-colors duration-300 dark:text-foreground">
{agent.page ? (
<a
href={agent.page}
className="text-foreground/90 font-semibold">
{agent.name}
</a>
) : (
<span className="text-foreground/60 font-medium">
{agent.name}
</span>
)}
</h3>
</div>
</div>
</div>
)
})}
</div>
</div>
</motion.div>
</div>
</div>
</section>
)
}

View file

@ -5,12 +5,12 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
default: "bg-primary text-primary-foreground shadow hover:bg-primary/80",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
@ -18,8 +18,9 @@ const buttonVariants = cva(
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
xl: "h-14 px-8 text-lg",
icon: "h-9 w-9",
},
},

View file

@ -3,3 +3,4 @@ export * from "./chart"
export * from "./modal"
export * from "./scroll-button"
export * from "./table"
export * from "./link"

View file

@ -0,0 +1,38 @@
import * as React from "react"
import NextLink from "next/link"
import { cn } from "@/lib/utils"
type BaseLinkProps = React.ComponentPropsWithoutRef<typeof NextLink>
type LinkProps = BaseLinkProps & {
newWindow?: boolean
}
const Link = React.forwardRef<React.ElementRef<typeof NextLink>, LinkProps>(
({ className, newWindow = false, target, rel, ...props }, ref) => {
const computedTarget = newWindow ? "_blank" : target
const computedRel = newWindow
? rel
? rel.includes("noreferrer")
? rel
: `${rel} noreferrer`
: "noreferrer"
: rel
return (
<NextLink
ref={ref}
className={cn("underline hover:no-underline", className)}
target={computedTarget}
rel={computedRel}
{...props}
/>
)
},
)
Link.displayName = "Link"
export { Link }
export type { LinkProps }

View file

@ -26,7 +26,8 @@ export const EXTERNAL_LINKS = {
TESTIMONIALS: "https://roocode.com/#testimonials",
CLOUD_APP_LOGIN: "https://app.roocode.com/sign-in",
CLOUD_APP_SIGNUP: "https://app.roocode.com/sign-up",
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/welcome",
CLOUD_APP_SIGNUP_HOME: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
}
export const INTERNAL_LINKS = {

View file

@ -0,0 +1,22 @@
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
})
export const formatCurrency = (amount: number) => formatter.format(amount)
export const formatTokens = (tokens: number) => {
if (tokens < 1000) {
return tokens.toString()
}
if (tokens < 1000000) {
return `${(tokens / 1000).toFixed(1)}K`
}
if (tokens < 1000000000) {
return `${(tokens / 1000000).toFixed(1)}M`
}
return `${(tokens / 1000000000).toFixed(1)}B`
}

View file

@ -104,13 +104,19 @@ export async function getVSCodeDownloads() {
}
function formatNumber(num: number): string {
// divide by 1000 to convert to "thousands" format,
// multiply by 10, floor the result, then divide by 10 to keep one decimal place.
// if number is 1 million or more, format as millions
if (num >= 1000000) {
const truncated = Math.floor((num / 1000000) * 100) / 100
return truncated.toFixed(2) + "M"
}
// otherwise, format as thousands
const truncated = Math.floor((num / 1000) * 10) / 10
// ensure one decimal is always shown and append "k"
return truncated.toFixed(1) + "k"
// examples:
// console.log(formatNumber(1033400)) -> "1.03M"
// console.log(formatNumber(2500000)) -> "2.50M"
// console.log(formatNumber(337231)) -> "337.2k"
// console.log(formatNumber(23233)) -> "23.2k"
// console.log(formatNumber(2322)) -> "2.3k"

View file

@ -0,0 +1,31 @@
export interface ModelPricing {
input: string
output: string
input_cache_read: string
input_cache_write: string
}
export interface Model {
id: string
object: string
created: number
owned_by: string
name: string
description: string
context_window: number
max_tokens: number
type: string
tags: string[]
pricing: ModelPricing
deprecated?: boolean
}
export interface ModelsResponse {
data: Model[]
}
export interface ModelWithTotalPrice extends Model {
totalPrice: number
}
export type SortOption = "alphabetical" | "price-asc" | "price-desc" | "context-window-asc" | "context-window-desc"

View file

@ -16,7 +16,7 @@
"project": ["**/*.ts"]
},
"webview-ui": {
"entry": ["src/index.tsx"],
"entry": ["src/index.tsx", "src/browser-panel.tsx"],
"project": ["src/**/*.{ts,tsx}", "../src/shared/*.ts"]
},
"packages/{build,cloud,evals,ipc,telemetry,types}": {

12
locales/ca/README.md generated
View file

@ -1,5 +1,5 @@
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
@ -167,16 +167,6 @@ Ens encanten les contribucions de la comunitat! Comença llegint el nostre [CONT
---
## Col·laboradors
Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code!
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
[![Contributors](https://contrib.rocks/image?repo=RooCodeInc/roo-code&max=120&columns=12&cacheBust=0000000000)](https://github.com/RooCodeInc/roo-code/graphs/contributors)
<!-- END CONTRIBUTORS SECTION -->
## Llicència
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)

12
locales/de/README.md generated
View file

@ -1,5 +1,5 @@
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
@ -167,16 +167,6 @@ Wir lieben Community-Beiträge! Lies unsere [CONTRIBUTING.md](CONTRIBUTING.md),
---
## Mitwirkende
Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code besser zu machen!
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
[![Contributors](https://contrib.rocks/image?repo=RooCodeInc/roo-code&max=120&columns=12&cacheBust=0000000000)](https://github.com/RooCodeInc/roo-code/graphs/contributors)
<!-- END CONTRIBUTORS SECTION -->
## Lizenz
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)

12
locales/es/README.md generated
View file

@ -1,5 +1,5 @@
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
@ -167,16 +167,6 @@ Usamos [changesets](https://github.com/changesets/changesets) para el versionado
---
## Colaboradores
¡Gracias a todos nuestros colaboradores que han ayudado a mejorar Roo Code!
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
[![Contributors](https://contrib.rocks/image?repo=RooCodeInc/roo-code&max=120&columns=12&cacheBust=0000000000)](https://github.com/RooCodeInc/roo-code/graphs/contributors)
<!-- END CONTRIBUTORS SECTION -->
## Licencia
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)

Some files were not shown because too many files have changed in this diff Show more