fix: resolve merge conflicts with main, keep new models from main
|
|
@ -76,14 +76,18 @@ src/node_modules
|
|||
!pnpm-workspace.yaml
|
||||
!scripts/bootstrap.mjs
|
||||
!apps/web-evals/
|
||||
!apps/cli/
|
||||
!src/
|
||||
!webview-ui/
|
||||
!packages/evals/.docker/entrypoints/runner.sh
|
||||
!packages/build/
|
||||
!packages/config-eslint/
|
||||
!packages/config-typescript/
|
||||
!packages/core/
|
||||
!packages/evals/
|
||||
!packages/ipc/
|
||||
!packages/telemetry/
|
||||
!packages/types/
|
||||
!packages/vscode-shim/
|
||||
!packages/cloud/
|
||||
!locales/
|
||||
|
|
|
|||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -76,16 +76,14 @@ body:
|
|||
label: API Provider (optional)
|
||||
options:
|
||||
- Anthropic
|
||||
- AWS Bedrock
|
||||
- Amazon Bedrock
|
||||
- Chutes AI
|
||||
- DeepSeek
|
||||
- Featherless AI
|
||||
- Fireworks AI
|
||||
- Glama
|
||||
- Google Gemini
|
||||
- Google Vertex AI
|
||||
- Groq
|
||||
- Human Relay Provider
|
||||
- LiteLLM
|
||||
- LM Studio
|
||||
- Mistral AI
|
||||
|
|
|
|||
3
.gitignore
vendored
|
|
@ -49,3 +49,6 @@ logs
|
|||
|
||||
# Qdrant
|
||||
qdrant_storage/
|
||||
|
||||
# Architect plans
|
||||
plans/
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
description: "Create a new release of the Roo Code extension"
|
||||
argument-hint: patch | minor | major
|
||||
mode: code
|
||||
---
|
||||
|
||||
1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt`
|
||||
|
|
@ -16,14 +17,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`
|
||||
|
|
|
|||
|
|
@ -1,25 +1,6 @@
|
|||
version: "1.0"
|
||||
|
||||
commands:
|
||||
- name: Pull latest changes
|
||||
run: git pull
|
||||
timeout: 60
|
||||
execution_phase: task_run
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
timeout: 60
|
||||
execution_phase: task_run
|
||||
|
||||
github_events:
|
||||
- event: issues.opened
|
||||
action:
|
||||
name: github.issue.fix
|
||||
- event: issue_comment.created
|
||||
action:
|
||||
name: github.issue.comment.respond
|
||||
- event: pull_request.opened
|
||||
action:
|
||||
name: github.pr.review
|
||||
- event: pull_request_review_comment.created
|
||||
action:
|
||||
name: github.pr.comment.respond
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
| Auto-approve | 自动批准 | 始终批准 | 权限相关术语 |
|
||||
| Checkpoint | 存档点 | 检查点/快照 | 技术概念统一 |
|
||||
| MCP Server | MCP 服务 | MCP 服务器 | 技术组件 |
|
||||
| Human Relay | 人工辅助模式 | 人工中继 | 功能描述清晰 |
|
||||
| Network Timeout | 请求超时 | 网络超时 | 更准确描述 |
|
||||
| Terminal | 终端 | 命令行 | 技术术语统一 |
|
||||
| diff | 差异更新 | 差分/补丁 | 代码变更 |
|
||||
|
|
@ -115,7 +114,7 @@
|
|||
|
||||
- 保留英文品牌名
|
||||
- 技术术语保持一致性
|
||||
- 保留英文专有名词:如"AWS Bedrock ARN"
|
||||
- 保留英文专有名词:如"Amazon Bedrock ARN"
|
||||
|
||||
4. **用户操作**
|
||||
- 操作动词统一:
|
||||
|
|
|
|||
572
CHANGELOG.md
|
|
@ -1,5 +1,557 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.38.3] - 2026-01-03
|
||||
|
||||
- Feat: Add option in Context settings to recursively load `.roo/rules` and `AGENTS.md` from subdirectories (PR #10446 by @mrubens)
|
||||
- Fix: Stop frequent Claude Code sign-ins by hardening OAuth refresh token handling (PR #10410 by @hannesrudolph)
|
||||
- Fix: Add `maxConcurrentFileReads` limit to native `read_file` tool schema (PR #10449 by @app/roomote)
|
||||
- Fix: Add type check for `lastMessage.text` in TTS useEffect to prevent runtime errors (PR #10431 by @app/roomote)
|
||||
|
||||
## [3.38.2] - 2025-12-31
|
||||
|
||||

|
||||
|
||||
- Align skills system with Agent Skills specification (PR #10409 by @hannesrudolph)
|
||||
- Prevent write_to_file from creating files at truncated paths (PR #10415 by @mrubens and @daniel-lxs)
|
||||
- Update Cerebras maxTokens to 16384 (PR #10387 by @sebastiand-cerebras)
|
||||
- Fix rate limit wait display (PR #10389 by @hannesrudolph)
|
||||
- Remove human-relay provider (PR #10388 by @hannesrudolph)
|
||||
- Replace Todo Lists video with Context Management video in documentation (PR #10375 by @SannidhyaSah)
|
||||
|
||||
## [3.38.1] - 2025-12-29
|
||||
|
||||

|
||||
|
||||
- Fix: Flush pending tool results before condensing context (PR #10379 by @daniel-lxs)
|
||||
- Fix: Revert mergeToolResultText for OpenAI-compatible providers (PR #10381 by @hannesrudolph)
|
||||
- Fix: Enforce maxConcurrentFileReads limit in read_file tool (PR #10363 by @roomote)
|
||||
- Fix: Improve feedback message when read_file is used on a directory (PR #10371 by @roomote)
|
||||
- Fix: Handle custom tool use similarly to MCP tools for IPC schema purposes (PR #10364 by @jr)
|
||||
- Fix: Correct GitHub repository URL in marketing page (#10376 by @jishnuteegala, PR #10377 by @roomote)
|
||||
- Docs: Clarify path to Security Settings in privacy policy (PR #10367 by @roomote)
|
||||
|
||||
## [3.38.0] - 2025-12-27
|
||||
|
||||

|
||||
|
||||
- Add support for [Agent Skills](https://agentskills.io/), enabling reusable packages of prompts, tools, and resources to extend Roo's capabilities (PR #10335 by @mrubens)
|
||||
- Add optional mode field to slash command front matter, allowing commands to automatically switch to a specific mode when triggered (PR #10344 by @app/roomote)
|
||||
- Add support for npm packages and .env files to custom tools, allowing custom tools to import dependencies and access environment variables (PR #10336 by @cte)
|
||||
- Remove simpleReadFileTool feature, streamlining the file reading experience (PR #10254 by @app/roomote)
|
||||
- Remove OpenRouter Transforms feature (PR #10341 by @app/roomote)
|
||||
- Fix mergeToolResultText handling in Roo provider (PR #10359 by @mrubens)
|
||||
|
||||
## [3.37.1] - 2025-12-23
|
||||
|
||||

|
||||
|
||||
- Fix: Send native tool definitions by default for OpenAI to ensure proper tool usage (PR #10314 by @hannesrudolph)
|
||||
- Fix: Preserve reasoning_details shape to prevent malformed responses when processing model output (PR #10313 by @hannesrudolph)
|
||||
- Fix: Drain queued messages while waiting for ask to prevent message loss (PR #10315 by @hannesrudolph)
|
||||
- Feat: Add grace retry for empty assistant messages to improve reliability (PR #10297 by @hannesrudolph)
|
||||
- Feat: Enable mergeToolResultText for all OpenAI-compatible providers for better tool result handling (PR #10299 by @hannesrudolph)
|
||||
- Feat: Enable mergeToolResultText for Roo Code Cloud provider (PR #10301 by @hannesrudolph)
|
||||
- Feat: Strengthen native tool-use guidance in prompts for improved model behavior (PR #10311 by @hannesrudolph)
|
||||
- UX: Account-centric signup flow for improved onboarding experience (PR #10306 by @brunobergher)
|
||||
|
||||
## [3.37.0] - 2025-12-22
|
||||
|
||||

|
||||
|
||||
- Add MiniMax M2.1 and improve environment_details handling for Minimax thinking models (PR #10284 by @hannesrudolph)
|
||||
- Add GLM-4.7 model with thinking mode support for Zai provider (PR #10282 by @hannesrudolph)
|
||||
- Add experimental custom tool calling - define custom tools that integrate seamlessly with your AI workflow (PR #10083 by @cte)
|
||||
- Deprecate XML tool protocol selection and force native tool format for new tasks (PR #10281 by @daniel-lxs)
|
||||
- Fix: Emit tool_call_end events in OpenAI handler when streaming ends (#10275 by @torxeon, PR #10280 by @daniel-lxs)
|
||||
- Fix: Emit tool_call_end events in BaseOpenAiCompatibleProvider (PR #10293 by @hannesrudolph)
|
||||
- Fix: Disable strict mode for MCP tools to preserve optional parameters (PR #10220 by @daniel-lxs)
|
||||
- Fix: Move array-specific properties into anyOf variant in normalizeToolSchema (PR #10276 by @daniel-lxs)
|
||||
- Fix: Add CRLF line ending normalization to search_replace and search_and_replace tools (PR #10288 by @hannesrudolph)
|
||||
- Fix: Add graceful fallback for model parsing in Chutes provider (PR #10279 by @hannesrudolph)
|
||||
- Fix: Enable Requesty refresh models with credentials (PR #10273 by @daniel-lxs)
|
||||
- Fix: Improve reasoning_details accumulation and serialization (PR #10285 by @hannesrudolph)
|
||||
- Fix: Preserve reasoning_content in condense summary for DeepSeek-reasoner (PR #10292 by @hannesrudolph)
|
||||
- Refactor Zai provider to merge environment_details into tool result instead of system message (PR #10289 by @hannesrudolph)
|
||||
- Remove parallel_tool_calls parameter from litellm provider (PR #10274 by @roomote)
|
||||
- Add Cloud Team page with comprehensive team management features (PR #10267 by @roomote)
|
||||
- Add message log deduper utility for evals (PR #10286 by @hannesrudolph)
|
||||
|
||||
## [3.36.16] - 2025-12-19
|
||||
|
||||
- Fix: Normalize tool schemas for VS Code LM API to resolve error 400 when using VS Code Language Model API providers (PR #10221 by @hannesrudolph)
|
||||
|
||||
## [3.36.15] - 2025-12-19
|
||||
|
||||

|
||||
|
||||
- Add 1M context window beta support for Claude Sonnet 4 on Vertex AI, enabling significantly larger context for complex tasks (PR #10209 by @hannesrudolph)
|
||||
- Add native tool calling support for LM Studio and Qwen-Code providers, improving compatibility with local models (PR #10208 by @hannesrudolph)
|
||||
- Add native tool call defaults for OpenAI-compatible providers, expanding native function calling across more configurations (PR #10213 by @hannesrudolph)
|
||||
- Enable native tool calls for Requesty provider (PR #10211 by @daniel-lxs)
|
||||
- Improve API error handling and visibility with clearer error messages and better user feedback (PR #10204 by @brunobergher)
|
||||
- Add downloadable error diagnostics from chat errors, making it easier to troubleshoot and report issues (PR #10188 by @brunobergher)
|
||||
- Fix refresh models button not properly flushing the cache, ensuring model lists update correctly (#9682 by @tl-hbk, PR #9870 by @pdecat)
|
||||
- Fix additionalProperties handling for strict mode compatibility, resolving schema validation issues with certain providers (PR #10210 by @daniel-lxs)
|
||||
|
||||
## [3.36.14] - 2025-12-18
|
||||
|
||||

|
||||
|
||||
- Add native tool calling support for Claude models on Vertex AI, enabling more efficient and reliable tool interactions (PR #10197 by @hannesrudolph)
|
||||
- Fix JSON Schema format value stripping for OpenAI compatibility, resolving issues with unsupported format values (PR #10198 by @daniel-lxs)
|
||||
- Improve "no tools used" error handling with graceful retry mechanism for better reliability when tools fail to execute (PR #10196 by @hannesrudolph)
|
||||
|
||||
## [3.36.13] - 2025-12-18
|
||||
|
||||

|
||||
|
||||
- Change default tool protocol from XML to native for improved reliability and performance (PR #10186 by @mrubens)
|
||||
- Add native tool support for VS Code Language Model API providers (PR #10191 by @daniel-lxs)
|
||||
- Lock task tool protocol for consistent task resumption, ensuring tasks resume with the same protocol they started with (PR #10192 by @daniel-lxs)
|
||||
- Replace edit_file tool alias with actual edit_file tool for improved diff editing capabilities (PR #9983 by @hannesrudolph)
|
||||
- Fix LiteLLM router models by merging default model info for native tool calling support (PR #10187 by @daniel-lxs)
|
||||
- Add PostHog exception tracking for consecutive mistake errors to improve error monitoring (PR #10193 by @daniel-lxs)
|
||||
|
||||
## [3.36.12] - 2025-12-18
|
||||
|
||||

|
||||
|
||||
- Fix: Add userAgentAppId to Bedrock embedder for code indexing (#10165 by @jackrein, PR #10166 by @roomote)
|
||||
- Update OpenAI and Gemini tool preferences for improved model behavior (PR #10170 by @hannesrudolph)
|
||||
- Extract error messages from JSON payloads for better PostHog error grouping (PR #10163 by @daniel-lxs)
|
||||
|
||||
## [3.36.11] - 2025-12-17
|
||||
|
||||

|
||||
|
||||
- Add support for Claude Code Provider native tool calling, improving tool execution performance and reliability (PR #10077 by @hannesrudolph)
|
||||
- Enable native tool calling by default for Z.ai models for better model compatibility (PR #10158 by @app/roomote)
|
||||
- Enable native tools by default for OpenAI compatible provider to improve tool calling support (PR #10159 by @daniel-lxs)
|
||||
- Fix: Normalize MCP tool schemas for Bedrock and OpenAI strict mode to ensure proper tool compatibility (PR #10148 by @daniel-lxs)
|
||||
- Fix: Remove dots and colons from MCP tool names for Bedrock compatibility (PR #10152 by @daniel-lxs)
|
||||
- Fix: Convert tool_result to XML text when native tools disabled for Bedrock (PR #10155 by @daniel-lxs)
|
||||
- Fix: Refresh Roo models cache with session token on auth state change to resolve model list refresh issues (PR #10156 by @daniel-lxs)
|
||||
- Fix: Support AWS GovCloud and China region ARNs in Bedrock provider for expanded regional support (PR #10157 by @app/roomote)
|
||||
|
||||
## [3.36.10] - 2025-12-17
|
||||
|
||||

|
||||
|
||||
- Add support for Gemini 3 Flash Preview model in the Gemini provider (PR #10151 by @hannesrudolph)
|
||||
- Implement interleaved thinking mode for DeepSeek Reasoner, enabling streaming reasoning output (PR #9969 by @hannesrudolph)
|
||||
- Fix: Preserve reasoning_content during tool call sequences in DeepSeek (PR #10141 by @hannesrudolph)
|
||||
- Fix: Correct token counting for context truncation display (PR #9961 by @hannesrudolph)
|
||||
- Update Next.js dependency to ~15.2.8 (PR #10140 by @jr)
|
||||
|
||||
## [3.36.9] - 2025-12-15
|
||||
|
||||

|
||||
|
||||
- Fix: Normalize tool call IDs for cross-provider compatibility via OpenRouter, ensuring consistent handling across different AI providers (PR #10102 by @daniel-lxs)
|
||||
- Fix: Add additionalProperties: false to nested MCP tool schemas, improving schema validation and preventing unexpected properties (PR #10109 by @daniel-lxs)
|
||||
- Fix: Validate tool_result IDs in delegation resume flow, preventing errors when resuming delegated tasks (PR #10135 by @daniel-lxs)
|
||||
- Feat: Add full error details to streaming failure dialog, providing more comprehensive information for debugging streaming issues (PR #10131 by @roomote)
|
||||
- Feat: Improve evals UI with tool groups and duration fix, enhancing the evaluation interface organization and timing accuracy (PR #10133 by @hannesrudolph)
|
||||
|
||||
## [3.36.8] - 2025-12-16
|
||||
|
||||

|
||||
|
||||
- Implement incremental token-budgeted file reading for smarter, more efficient file content retrieval (PR #10052 by @jr)
|
||||
- Enable native tools by default for multiple providers including OpenAI, Azure, Google, Vertex, and more (PR #10059 by @daniel-lxs)
|
||||
- Enable native tools by default for Anthropic and add telemetry tracking for tool format usage (PR #10021 by @daniel-lxs)
|
||||
- Fix: Prevent race condition from deleting wrong API messages during streaming (PR #10113 by @hannesrudolph)
|
||||
- Fix: Prevent duplicate MCP tools error by deduplicating servers at source (PR #10096 by @daniel-lxs)
|
||||
- Remove strict ARN validation for Bedrock custom ARN users allowing more flexibility (#10108 by @wisestmumbler, PR #10110 by @roomote)
|
||||
- Add metadata to error details dialog for improved debugging (PR #10050 by @roomote)
|
||||
- Add configuration to control public sharing feature (PR #10105 by @mrubens)
|
||||
- Remove description from Bedrock service tiers for cleaner UI (PR #10118 by @mrubens)
|
||||
- Fix: Correct link to provider pricing page on web (PR #10107 by @brunobergher)
|
||||
|
||||
## [3.36.7] - 2025-12-15
|
||||
|
||||
- Improve tool configuration for OpenAI models in OpenRouter (PR #10082 by @hannesrudolph)
|
||||
- Capture more detailed provider-specific error information from OpenRouter for better debugging (PR #10073 by @jr)
|
||||
- Add Amazon Nova 2 Lite model to Bedrock provider (#9802 by @Smartsheet-JB-Brown, PR #9830 by @roomote)
|
||||
- Add AWS Bedrock service tier support (#9874 by @Smartsheet-JB-Brown, PR #9955 by @roomote)
|
||||
- Remove auto-approve toggles for to-do and retry actions to simplify the approval workflow (PR #10062 by @hannesrudolph)
|
||||
- Move isToolAllowedForMode out of shared directory for better code organization (PR #10089 by @cte)
|
||||
- Improve run logs and formatters in web-evals for better evaluation tracking (PR #10081 by @hannesrudolph)
|
||||
|
||||
## [3.36.6] - 2025-12-12
|
||||
|
||||

|
||||
|
||||
- Add tool alias support for model-specific tool customization, allowing users to configure how tools are presented to different AI models (PR #9989 by @daniel-lxs)
|
||||
- Sanitize MCP server and tool names for API compatibility, ensuring special characters don't cause issues with API calls (PR #10054 by @daniel-lxs)
|
||||
- Improve auto-approve timer visibility in follow-up suggestions for better user awareness of pending actions (PR #10048 by @brunobergher)
|
||||
- Fix: Cancel auto-approval timeout when user starts typing, preventing accidental auto-approvals during user interaction (PR #9937 by @roomote)
|
||||
- Add WorkspaceTaskVisibility type for organization cloud settings to support team visibility controls (PR #10020 by @roomote)
|
||||
- Fix: Extract raw error message from OpenRouter metadata for clearer error reporting (PR #10039 by @daniel-lxs)
|
||||
- Fix: Show tool protocol dropdown for LiteLLM provider, restoring missing configuration option (PR #10053 by @daniel-lxs)
|
||||
|
||||
## [3.36.5] - 2025-12-11
|
||||
|
||||

|
||||
|
||||
- Add: GPT-5.2 model to openai-native provider (PR #10024 by @hannesrudolph)
|
||||
- Add: Toggle for Enter key behavior in chat input allowing users to configure whether Enter sends or creates new line (#8555 by @lmtr0, PR #10002 by @hannesrudolph)
|
||||
- Add: App version to telemetry exception captures and filter 402 errors (PR #9996 by @daniel-lxs)
|
||||
- Fix: Handle empty Gemini responses and reasoning loops to prevent infinite retries (PR #10007 by @hannesrudolph)
|
||||
- Fix: Add missing tool_result blocks to prevent API errors when tool results are expected (PR #10015 by @daniel-lxs)
|
||||
- Fix: Filter orphaned tool_results when more results than tool_uses to prevent message validation errors (PR #10027 by @daniel-lxs)
|
||||
- Fix: Add general API endpoints for Z.ai provider (#9879 by @richtong, PR #9894 by @roomote)
|
||||
- Fix: Apply versioned settings on nightly builds (PR #9997 by @hannesrudolph)
|
||||
- Remove: Glama provider (PR #9801 by @hannesrudolph)
|
||||
- Remove: Deprecated list_code_definition_names tool (PR #10005 by @hannesrudolph)
|
||||
|
||||
## [3.36.4] - 2025-12-10
|
||||
|
||||

|
||||
|
||||
- Add error details modal with on-demand display for improved error visibility when debugging issues (PR #9985 by @roomote)
|
||||
- Fix: Prevent premature rawChunkTracker clearing for MCP tools, improving reliability of MCP tool streaming (PR #9993 by @daniel-lxs)
|
||||
- Fix: Filter out 429 rate limit errors from API error telemetry for cleaner metrics (PR #9987 by @daniel-lxs)
|
||||
- Fix: Correct TODO list display order in chat view to show items in proper sequence (PR #9991 by @roomote)
|
||||
|
||||
## [3.36.3] - 2025-12-09
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- Fix: Race condition in new_task tool for native protocol (PR #9655 by @daniel-lxs)
|
||||
|
||||
## [3.34.7] - 2025-11-27
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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)
|
||||
|
|
@ -214,7 +766,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
|
||||
|
|
@ -546,7 +1098,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
|
||||
|
|
@ -911,7 +1463,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!)
|
||||
|
|
@ -925,7 +1477,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
|
||||
|
||||
|
|
@ -1549,7 +2101,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
|
||||
|
|
@ -1798,12 +2350,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!)
|
||||
|
|
@ -1941,7 +2493,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
|
||||
|
||||
|
|
@ -1958,7 +2510,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
|
||||
|
|
@ -2142,7 +2694,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]
|
||||
|
||||
|
|
|
|||
22
README.md
|
|
@ -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>
|
||||
|
|
@ -68,10 +66,10 @@ Learn more: [Using Modes](https://docs.roocode.com/basic-usage/using-modes) •
|
|||
|
||||
<div align="center">
|
||||
|
||||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installing Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configuring Profiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase Indexing</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Custom Modes</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Todo Lists</b> |
|
||||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installing Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configuring Profiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase Indexing</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Custom Modes</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Context Management</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
@ -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 -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](./LICENSE)
|
||||
|
|
|
|||
231
apps/cli/README.md
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
# @roo-code/cli
|
||||
|
||||
Command Line Interface for Roo Code - Run the Roo Code agent from the terminal without VSCode.
|
||||
|
||||
## Overview
|
||||
|
||||
This CLI uses the `@roo-code/vscode-shim` package to provide a VSCode API compatibility layer, allowing the main Roo Code extension to run in a Node.js environment.
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Install (Recommended)
|
||||
|
||||
Install the Roo Code CLI with a single command:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Node.js 20 or higher
|
||||
- macOS (Intel or Apple Silicon) or Linux (x64 or ARM64)
|
||||
|
||||
**Custom installation directory:**
|
||||
|
||||
```bash
|
||||
ROO_INSTALL_DIR=/opt/roo-code ROO_BIN_DIR=/usr/local/bin curl -fsSL ... | sh
|
||||
```
|
||||
|
||||
**Install a specific version:**
|
||||
|
||||
```bash
|
||||
ROO_VERSION=0.1.0 curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
Re-run the install script to update to the latest version:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
```
|
||||
|
||||
### Uninstalling
|
||||
|
||||
```bash
|
||||
rm -rf ~/.roo/cli ~/.local/bin/roo
|
||||
```
|
||||
|
||||
### Development Installation
|
||||
|
||||
For contributing or development:
|
||||
|
||||
```bash
|
||||
# From the monorepo root.
|
||||
pnpm install
|
||||
|
||||
# Build the main extension first.
|
||||
pnpm --filter roo-cline bundle
|
||||
|
||||
# Build the cli.
|
||||
pnpm --filter @roo-code/cli build
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode (Default)
|
||||
|
||||
By default, the CLI prompts for approval before executing actions:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
||||
roo "What is this project?" --workspace ~/Documents/my-project
|
||||
```
|
||||
|
||||
In interactive mode:
|
||||
|
||||
- Tool executions prompt for yes/no approval
|
||||
- Commands prompt for yes/no approval
|
||||
- Followup questions show suggestions and wait for user input
|
||||
- Browser and MCP actions prompt for approval
|
||||
|
||||
### Non-Interactive Mode (`-y`)
|
||||
|
||||
For automation and scripts, use `-y` to auto-approve all actions:
|
||||
|
||||
```bash
|
||||
roo -y "Refactor the utils.ts file" --workspace ~/Documents/my-project
|
||||
```
|
||||
|
||||
In non-interactive mode:
|
||||
|
||||
- Tool, command, browser, and MCP actions are auto-approved
|
||||
- Followup questions show a 10-second timeout, then auto-select the first suggestion
|
||||
- Typing any key cancels the timeout and allows manual input
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------ | ----------------- |
|
||||
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
|
||||
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
|
||||
| `-v, --verbose` | Enable verbose output (show VSCode and extension logs) | `false` |
|
||||
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
|
||||
| `-x, --exit-on-complete` | Exit the process when task completes (useful for testing) | `false` |
|
||||
| `-y, --yes` | Non-interactive mode: auto-approve all actions | `false` |
|
||||
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
|
||||
| `-p, --provider <provider>` | API provider (anthropic, openai, openrouter, etc.) | `openrouter` |
|
||||
| `-m, --model <model>` | Model to use | Provider default |
|
||||
| `-M, --mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
|
||||
| `-r, --reasoning-effort <effort>` | Reasoning effort level (none, minimal, low, medium, high, xhigh) | `medium` |
|
||||
|
||||
By default, the CLI runs in quiet mode (suppressing VSCode/extension logs) and only shows assistant output. Use `-v` to see all logs, or `-d` for detailed debug information.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The CLI will look for API keys in environment variables if not provided via `--api-key`:
|
||||
|
||||
| Provider | Environment Variable |
|
||||
| ------------- | -------------------- |
|
||||
| anthropic | `ANTHROPIC_API_KEY` |
|
||||
| openai | `OPENAI_API_KEY` |
|
||||
| openrouter | `OPENROUTER_API_KEY` |
|
||||
| google/gemini | `GOOGLE_API_KEY` |
|
||||
| mistral | `MISTRAL_API_KEY` |
|
||||
| deepseek | `DEEPSEEK_API_KEY` |
|
||||
| bedrock | `AWS_ACCESS_KEY_ID` |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ CLI Entry │
|
||||
│ (index.ts) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ ExtensionHost │
|
||||
│ (extension- │
|
||||
│ host.ts) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────┐ ┌──────────┐
|
||||
│vscode │ │Extension │
|
||||
│-shim │ │ Bundle │
|
||||
└───────┘ └──────────┘
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **CLI Entry Point** (`index.ts`): Parses command line arguments and initializes the ExtensionHost
|
||||
|
||||
2. **ExtensionHost** (`extension-host.ts`):
|
||||
|
||||
- Creates a VSCode API mock using `@roo-code/vscode-shim`
|
||||
- Intercepts `require('vscode')` to return the mock
|
||||
- Loads and activates the extension bundle
|
||||
- Manages bidirectional message flow
|
||||
|
||||
3. **Message Flow**:
|
||||
- CLI → Extension: `emit("webviewMessage", {...})`
|
||||
- Extension → CLI: `emit("extensionWebviewMessage", {...})`
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- **No TUI**: Output is plain text (no React/Ink UI yet)
|
||||
- **No configuration file**: Settings are passed via command line flags
|
||||
- **No persistence**: Each run is a fresh session
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Watch mode for development
|
||||
pnpm dev
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Type checking
|
||||
pnpm check-types
|
||||
|
||||
# Linting
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
To create a new release, run the release script from the monorepo root:
|
||||
|
||||
```bash
|
||||
# Release using version from package.json
|
||||
./apps/cli/scripts/release.sh
|
||||
|
||||
# Release with a specific version
|
||||
./apps/cli/scripts/release.sh 0.1.0
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
1. Build the extension and CLI
|
||||
2. Create a platform-specific tarball (for your current OS/architecture)
|
||||
3. Create a GitHub release with the tarball attached
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated (`gh auth login`)
|
||||
- pnpm installed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Extension bundle not found
|
||||
|
||||
Make sure you've built the main extension first:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
pnpm bundle
|
||||
```
|
||||
|
||||
### Module resolution errors
|
||||
|
||||
The CLI expects the extension to be a CommonJS bundle. Make sure the extension's esbuild config outputs CommonJS.
|
||||
|
||||
### "vscode" module not found
|
||||
|
||||
The CLI intercepts `require('vscode')` calls. If you see this error, the module resolution interception may have failed.
|
||||
4
apps/cli/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
287
apps/cli/install.sh
Executable file
|
|
@ -0,0 +1,287 @@
|
|||
#!/bin/sh
|
||||
# Roo Code CLI Installer
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
#
|
||||
# Environment variables:
|
||||
# ROO_INSTALL_DIR - Installation directory (default: ~/.roo/cli)
|
||||
# ROO_BIN_DIR - Binary symlink directory (default: ~/.local/bin)
|
||||
# ROO_VERSION - Specific version to install (default: latest)
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
INSTALL_DIR="${ROO_INSTALL_DIR:-$HOME/.roo/cli}"
|
||||
BIN_DIR="${ROO_BIN_DIR:-$HOME/.local/bin}"
|
||||
REPO="RooCodeInc/Roo-Code"
|
||||
MIN_NODE_VERSION=20
|
||||
|
||||
# Color output (only if terminal supports it)
|
||||
if [ -t 1 ]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
BOLD=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
info() { printf "${GREEN}==>${NC} %s\n" "$1"; }
|
||||
warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1"; }
|
||||
error() { printf "${RED}Error:${NC} %s\n" "$1" >&2; exit 1; }
|
||||
|
||||
# Check Node.js version
|
||||
check_node() {
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
error "Node.js is not installed. Please install Node.js $MIN_NODE_VERSION or higher.
|
||||
|
||||
Install Node.js:
|
||||
- macOS: brew install node
|
||||
- Linux: https://nodejs.org/en/download/package-manager
|
||||
- Or use a version manager like fnm, nvm, or mise"
|
||||
fi
|
||||
|
||||
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$NODE_VERSION" -lt "$MIN_NODE_VERSION" ]; then
|
||||
error "Node.js $MIN_NODE_VERSION+ required. Found: $(node -v)
|
||||
|
||||
Please upgrade Node.js to version $MIN_NODE_VERSION or higher."
|
||||
fi
|
||||
|
||||
info "Found Node.js $(node -v)"
|
||||
}
|
||||
|
||||
# Detect OS and architecture
|
||||
detect_platform() {
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS" in
|
||||
darwin) OS="darwin" ;;
|
||||
linux) OS="linux" ;;
|
||||
mingw*|msys*|cygwin*)
|
||||
error "Windows is not supported by this installer. Please use WSL or install manually."
|
||||
;;
|
||||
*) error "Unsupported OS: $OS" ;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH="x64" ;;
|
||||
arm64|aarch64) ARCH="arm64" ;;
|
||||
*) error "Unsupported architecture: $ARCH" ;;
|
||||
esac
|
||||
|
||||
PLATFORM="${OS}-${ARCH}"
|
||||
info "Detected platform: $PLATFORM"
|
||||
}
|
||||
|
||||
# Get latest release version or use specified version
|
||||
get_version() {
|
||||
if [ -n "$ROO_VERSION" ]; then
|
||||
VERSION="$ROO_VERSION"
|
||||
info "Using specified version: $VERSION"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Fetching latest version..."
|
||||
|
||||
# Try to get the latest cli release
|
||||
RELEASES_JSON=$(curl -fsSL "https://api.github.com/repos/$REPO/releases" 2>/dev/null) || {
|
||||
error "Failed to fetch releases from GitHub. Check your internet connection."
|
||||
}
|
||||
|
||||
# Extract the latest cli-v* tag
|
||||
VERSION=$(echo "$RELEASES_JSON" |
|
||||
grep -o '"tag_name": "cli-v[^"]*"' |
|
||||
head -1 |
|
||||
sed 's/"tag_name": "cli-v//' |
|
||||
sed 's/"//')
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
error "Could not find any CLI releases. The CLI may not have been released yet."
|
||||
fi
|
||||
|
||||
info "Latest version: $VERSION"
|
||||
}
|
||||
|
||||
# Download and extract
|
||||
download_and_install() {
|
||||
TARBALL="roo-cli-${PLATFORM}.tar.gz"
|
||||
URL="https://github.com/$REPO/releases/download/cli-v${VERSION}/${TARBALL}"
|
||||
|
||||
info "Downloading from $URL..."
|
||||
|
||||
# Create temp directory
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TMP_DIR" EXIT
|
||||
|
||||
# Download with progress indicator
|
||||
HTTP_CODE=$(curl -fsSL -w "%{http_code}" "$URL" -o "$TMP_DIR/$TARBALL" 2>/dev/null) || {
|
||||
if [ "$HTTP_CODE" = "404" ]; then
|
||||
error "Release not found for platform $PLATFORM version $VERSION.
|
||||
|
||||
Available at: https://github.com/$REPO/releases"
|
||||
fi
|
||||
error "Download failed. HTTP code: $HTTP_CODE"
|
||||
}
|
||||
|
||||
# Verify we got something
|
||||
if [ ! -s "$TMP_DIR/$TARBALL" ]; then
|
||||
error "Downloaded file is empty. Please try again."
|
||||
fi
|
||||
|
||||
# Remove old installation if exists
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
info "Removing previous installation..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Extract
|
||||
info "Extracting to $INSTALL_DIR..."
|
||||
tar -xzf "$TMP_DIR/$TARBALL" -C "$INSTALL_DIR" --strip-components=1 || {
|
||||
error "Failed to extract tarball. The download may be corrupted."
|
||||
}
|
||||
|
||||
# Save ripgrep binary before npm install (npm install will overwrite node_modules)
|
||||
RIPGREP_BIN=""
|
||||
if [ -f "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg" ]; then
|
||||
RIPGREP_BIN="$TMP_DIR/rg"
|
||||
cp "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg" "$RIPGREP_BIN"
|
||||
fi
|
||||
|
||||
# Install npm dependencies
|
||||
info "Installing dependencies..."
|
||||
cd "$INSTALL_DIR"
|
||||
npm install --production --silent 2>/dev/null || {
|
||||
warn "npm install failed, trying with --legacy-peer-deps..."
|
||||
npm install --production --legacy-peer-deps --silent 2>/dev/null || {
|
||||
error "Failed to install dependencies. Make sure npm is available."
|
||||
}
|
||||
}
|
||||
cd - > /dev/null
|
||||
|
||||
# Restore ripgrep binary after npm install
|
||||
if [ -n "$RIPGREP_BIN" ] && [ -f "$RIPGREP_BIN" ]; then
|
||||
mkdir -p "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin"
|
||||
cp "$RIPGREP_BIN" "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg"
|
||||
chmod +x "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg"
|
||||
fi
|
||||
|
||||
# Make executable
|
||||
chmod +x "$INSTALL_DIR/bin/roo"
|
||||
|
||||
# Also make ripgrep executable if it exists
|
||||
if [ -f "$INSTALL_DIR/bin/rg" ]; then
|
||||
chmod +x "$INSTALL_DIR/bin/rg"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create symlink in bin directory
|
||||
setup_bin() {
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
# Remove old symlink if exists
|
||||
if [ -L "$BIN_DIR/roo" ] || [ -f "$BIN_DIR/roo" ]; then
|
||||
rm -f "$BIN_DIR/roo"
|
||||
fi
|
||||
|
||||
ln -sf "$INSTALL_DIR/bin/roo" "$BIN_DIR/roo"
|
||||
info "Created symlink: $BIN_DIR/roo"
|
||||
}
|
||||
|
||||
# Check if bin dir is in PATH and provide instructions
|
||||
check_path() {
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*)
|
||||
# Already in PATH
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
warn "$BIN_DIR is not in your PATH"
|
||||
echo ""
|
||||
echo "Add this line to your shell profile:"
|
||||
echo ""
|
||||
|
||||
# Detect shell and provide specific instructions
|
||||
SHELL_NAME=$(basename "$SHELL")
|
||||
case "$SHELL_NAME" in
|
||||
zsh)
|
||||
echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.zshrc"
|
||||
echo " source ~/.zshrc"
|
||||
;;
|
||||
bash)
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.bashrc"
|
||||
echo " source ~/.bashrc"
|
||||
else
|
||||
echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.bash_profile"
|
||||
echo " source ~/.bash_profile"
|
||||
fi
|
||||
;;
|
||||
fish)
|
||||
echo " set -Ux fish_user_paths $BIN_DIR \$fish_user_paths"
|
||||
;;
|
||||
*)
|
||||
echo " export PATH=\"$BIN_DIR:\$PATH\""
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
verify_install() {
|
||||
if [ -x "$BIN_DIR/roo" ]; then
|
||||
info "Verifying installation..."
|
||||
# Just check if it runs without error
|
||||
"$BIN_DIR/roo" --version >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Print success message
|
||||
print_success() {
|
||||
echo ""
|
||||
printf "${GREEN}${BOLD}✓ Roo Code CLI installed successfully!${NC}\n"
|
||||
echo ""
|
||||
echo " Installation: $INSTALL_DIR"
|
||||
echo " Binary: $BIN_DIR/roo"
|
||||
echo " Version: $VERSION"
|
||||
echo ""
|
||||
echo " ${BOLD}Get started:${NC}"
|
||||
echo " roo --help"
|
||||
echo ""
|
||||
echo " ${BOLD}Example:${NC}"
|
||||
echo " export OPENROUTER_API_KEY=sk-or-v1-..."
|
||||
echo " roo \"What is this project?\" --workspace ~/my-project"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
echo ""
|
||||
printf "${BLUE}${BOLD}"
|
||||
echo " ╭─────────────────────────────────╮"
|
||||
echo " │ Roo Code CLI Installer │"
|
||||
echo " ╰─────────────────────────────────╯"
|
||||
printf "${NC}"
|
||||
echo ""
|
||||
|
||||
check_node
|
||||
detect_platform
|
||||
get_version
|
||||
download_and_install
|
||||
setup_bin
|
||||
check_path
|
||||
verify_install
|
||||
print_success
|
||||
}
|
||||
|
||||
main "$@"
|
||||
35
apps/cli/package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@roo-code/cli",
|
||||
"version": "0.1.0",
|
||||
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"roo": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write 'src/**/*.ts'",
|
||||
"lint": "eslint src --ext .ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"build": "tsup",
|
||||
"start": "node dist/index.js",
|
||||
"clean": "rimraf dist .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@roo-code/types": "workspace:^",
|
||||
"@roo-code/vscode-shim": "workspace:^",
|
||||
"@vscode/ripgrep": "^1.15.9",
|
||||
"commander": "^12.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@types/node": "^24.1.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.8.3",
|
||||
"vitest": "^3.2.3"
|
||||
}
|
||||
}
|
||||
363
apps/cli/scripts/release.sh
Executable file
|
|
@ -0,0 +1,363 @@
|
|||
#!/bin/bash
|
||||
# Roo Code CLI Release Script
|
||||
#
|
||||
# Usage:
|
||||
# ./apps/cli/scripts/release.sh [version]
|
||||
#
|
||||
# Examples:
|
||||
# ./apps/cli/scripts/release.sh # Use version from package.json
|
||||
# ./apps/cli/scripts/release.sh 0.1.0 # Specify version
|
||||
#
|
||||
# This script:
|
||||
# 1. Builds the extension and CLI
|
||||
# 2. Creates a tarball for the current platform
|
||||
# 3. Creates a GitHub release and uploads the tarball
|
||||
#
|
||||
# Prerequisites:
|
||||
# - GitHub CLI (gh) installed and authenticated
|
||||
# - pnpm installed
|
||||
# - Run from the monorepo root directory
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { printf "${GREEN}==>${NC} %s\n" "$1"; }
|
||||
warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1"; }
|
||||
error() { printf "${RED}Error:${NC} %s\n" "$1" >&2; exit 1; }
|
||||
step() { printf "${BLUE}${BOLD}[%s]${NC} %s\n" "$1" "$2"; }
|
||||
|
||||
# Get script directory and repo root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
CLI_DIR="$REPO_ROOT/apps/cli"
|
||||
|
||||
# Detect current platform
|
||||
detect_platform() {
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS" in
|
||||
darwin) OS="darwin" ;;
|
||||
linux) OS="linux" ;;
|
||||
*) error "Unsupported OS: $OS" ;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH="x64" ;;
|
||||
arm64|aarch64) ARCH="arm64" ;;
|
||||
*) error "Unsupported architecture: $ARCH" ;;
|
||||
esac
|
||||
|
||||
PLATFORM="${OS}-${ARCH}"
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
step "1/7" "Checking prerequisites..."
|
||||
|
||||
if ! command -v gh &> /dev/null; then
|
||||
error "GitHub CLI (gh) is not installed. Install it with: brew install gh"
|
||||
fi
|
||||
|
||||
if ! gh auth status &> /dev/null; then
|
||||
error "GitHub CLI is not authenticated. Run: gh auth login"
|
||||
fi
|
||||
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
error "pnpm is not installed."
|
||||
fi
|
||||
|
||||
if ! command -v node &> /dev/null; then
|
||||
error "Node.js is not installed."
|
||||
fi
|
||||
|
||||
info "Prerequisites OK"
|
||||
}
|
||||
|
||||
# Get version
|
||||
get_version() {
|
||||
if [ -n "$1" ]; then
|
||||
VERSION="$1"
|
||||
else
|
||||
VERSION=$(node -p "require('$CLI_DIR/package.json').version")
|
||||
fi
|
||||
|
||||
# Validate semver format
|
||||
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
|
||||
error "Invalid version format: $VERSION (expected semver like 0.1.0)"
|
||||
fi
|
||||
|
||||
TAG="cli-v$VERSION"
|
||||
info "Version: $VERSION (tag: $TAG)"
|
||||
}
|
||||
|
||||
# Build everything
|
||||
build() {
|
||||
step "2/7" "Building extension bundle..."
|
||||
cd "$REPO_ROOT"
|
||||
pnpm bundle
|
||||
|
||||
step "3/7" "Building CLI..."
|
||||
pnpm --filter @roo-code/cli build
|
||||
|
||||
info "Build complete"
|
||||
}
|
||||
|
||||
# Create release tarball
|
||||
create_tarball() {
|
||||
step "4/7" "Creating release tarball for $PLATFORM..."
|
||||
|
||||
RELEASE_DIR="$REPO_ROOT/roo-cli-${PLATFORM}"
|
||||
TARBALL="roo-cli-${PLATFORM}.tar.gz"
|
||||
|
||||
# Clean up any previous build
|
||||
rm -rf "$RELEASE_DIR"
|
||||
rm -f "$REPO_ROOT/$TARBALL"
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$RELEASE_DIR/bin"
|
||||
mkdir -p "$RELEASE_DIR/lib"
|
||||
mkdir -p "$RELEASE_DIR/extension"
|
||||
|
||||
# Copy CLI dist files
|
||||
info "Copying CLI files..."
|
||||
cp -r "$CLI_DIR/dist/"* "$RELEASE_DIR/lib/"
|
||||
|
||||
# Create package.json for npm install (only runtime dependencies)
|
||||
info "Creating package.json..."
|
||||
node -e "
|
||||
const pkg = require('$CLI_DIR/package.json');
|
||||
const newPkg = {
|
||||
name: '@roo-code/cli',
|
||||
version: pkg.version,
|
||||
type: 'module',
|
||||
dependencies: {
|
||||
commander: pkg.dependencies.commander
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(newPkg, null, 2));
|
||||
" > "$RELEASE_DIR/package.json"
|
||||
|
||||
# Copy extension bundle
|
||||
info "Copying extension bundle..."
|
||||
cp -r "$REPO_ROOT/src/dist/"* "$RELEASE_DIR/extension/"
|
||||
|
||||
# Add package.json to extension directory to mark it as CommonJS
|
||||
# This is necessary because the main package.json has "type": "module"
|
||||
# but the extension bundle is CommonJS
|
||||
echo '{"type": "commonjs"}' > "$RELEASE_DIR/extension/package.json"
|
||||
|
||||
# Find and copy ripgrep binary
|
||||
# The extension looks for ripgrep at: appRoot/node_modules/@vscode/ripgrep/bin/rg
|
||||
# The CLI sets appRoot to the CLI package root, so we need to put ripgrep there
|
||||
info "Looking for ripgrep binary..."
|
||||
RIPGREP_PATH=$(find "$REPO_ROOT/node_modules" -path "*/@vscode/ripgrep/bin/rg" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$RIPGREP_PATH" ] && [ -f "$RIPGREP_PATH" ]; then
|
||||
info "Found ripgrep at: $RIPGREP_PATH"
|
||||
# Create the expected directory structure for the extension to find ripgrep
|
||||
mkdir -p "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin"
|
||||
cp "$RIPGREP_PATH" "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/"
|
||||
chmod +x "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/rg"
|
||||
# Also keep a copy in bin/ for direct access
|
||||
mkdir -p "$RELEASE_DIR/bin"
|
||||
cp "$RIPGREP_PATH" "$RELEASE_DIR/bin/"
|
||||
chmod +x "$RELEASE_DIR/bin/rg"
|
||||
else
|
||||
warn "ripgrep binary not found - users will need ripgrep installed"
|
||||
fi
|
||||
|
||||
# Create the wrapper script
|
||||
info "Creating wrapper script..."
|
||||
cat > "$RELEASE_DIR/bin/roo" << 'WRAPPER_EOF'
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Set environment variables for the CLI
|
||||
process.env.ROO_EXTENSION_PATH = join(__dirname, '..', 'extension');
|
||||
process.env.ROO_RIPGREP_PATH = join(__dirname, 'rg');
|
||||
|
||||
// Import and run the actual CLI
|
||||
await import(join(__dirname, '..', 'lib', 'index.js'));
|
||||
WRAPPER_EOF
|
||||
|
||||
chmod +x "$RELEASE_DIR/bin/roo"
|
||||
|
||||
# Create version file
|
||||
echo "$VERSION" > "$RELEASE_DIR/VERSION"
|
||||
|
||||
# Create tarball
|
||||
info "Creating tarball..."
|
||||
cd "$REPO_ROOT"
|
||||
tar -czvf "$TARBALL" "$(basename "$RELEASE_DIR")"
|
||||
|
||||
# Clean up release directory
|
||||
rm -rf "$RELEASE_DIR"
|
||||
|
||||
# Show size
|
||||
TARBALL_PATH="$REPO_ROOT/$TARBALL"
|
||||
TARBALL_SIZE=$(ls -lh "$TARBALL_PATH" | awk '{print $5}')
|
||||
info "Created: $TARBALL ($TARBALL_SIZE)"
|
||||
}
|
||||
|
||||
# Create checksum
|
||||
create_checksum() {
|
||||
step "5/7" "Creating checksum..."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if command -v sha256sum &> /dev/null; then
|
||||
sha256sum "$TARBALL" > "${TARBALL}.sha256"
|
||||
elif command -v shasum &> /dev/null; then
|
||||
shasum -a 256 "$TARBALL" > "${TARBALL}.sha256"
|
||||
else
|
||||
warn "No sha256sum or shasum found, skipping checksum"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Checksum: $(cat "${TARBALL}.sha256")"
|
||||
}
|
||||
|
||||
# Check if release already exists
|
||||
check_existing_release() {
|
||||
step "6/7" "Checking for existing release..."
|
||||
|
||||
if gh release view "$TAG" &> /dev/null; then
|
||||
warn "Release $TAG already exists"
|
||||
read -p "Do you want to delete it and create a new one? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
info "Deleting existing release..."
|
||||
gh release delete "$TAG" --yes
|
||||
# Also delete the tag if it exists
|
||||
git tag -d "$TAG" 2>/dev/null || true
|
||||
git push origin ":refs/tags/$TAG" 2>/dev/null || true
|
||||
else
|
||||
error "Aborted. Use a different version or delete the existing release manually."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Create GitHub release
|
||||
create_release() {
|
||||
step "7/7" "Creating GitHub release..."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
RELEASE_NOTES=$(cat << EOF
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
\`\`\`
|
||||
|
||||
Or install a specific version:
|
||||
\`\`\`bash
|
||||
ROO_VERSION=$VERSION curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
\`\`\`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20 or higher
|
||||
- macOS (Intel or Apple Silicon) or Linux (x64 or ARM64)
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
# Set your API key
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
||||
# Run a task
|
||||
roo "What is this project?" --workspace ~/my-project
|
||||
|
||||
# See all options
|
||||
roo --help
|
||||
\`\`\`
|
||||
|
||||
## Platform Support
|
||||
|
||||
This release includes:
|
||||
- \`roo-cli-${PLATFORM}.tar.gz\` - Built on $(uname -s) $(uname -m)
|
||||
|
||||
> **Note:** Additional platforms will be added as needed. If you need a different platform, please open an issue.
|
||||
|
||||
## Checksum
|
||||
|
||||
\`\`\`
|
||||
$(cat "${TARBALL}.sha256" 2>/dev/null || echo "N/A")
|
||||
\`\`\`
|
||||
EOF
|
||||
)
|
||||
|
||||
# Get the current commit SHA for the release target
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
info "Creating release at commit: ${COMMIT_SHA:0:8}"
|
||||
|
||||
# Create release (gh will create the tag automatically)
|
||||
info "Creating release..."
|
||||
RELEASE_FILES="$TARBALL"
|
||||
if [ -f "${TARBALL}.sha256" ]; then
|
||||
RELEASE_FILES="$RELEASE_FILES ${TARBALL}.sha256"
|
||||
fi
|
||||
|
||||
gh release create "$TAG" \
|
||||
--title "Roo Code CLI v$VERSION" \
|
||||
--notes "$RELEASE_NOTES" \
|
||||
--prerelease \
|
||||
--target "$COMMIT_SHA" \
|
||||
$RELEASE_FILES
|
||||
|
||||
info "Release created!"
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
cleanup() {
|
||||
info "Cleaning up..."
|
||||
cd "$REPO_ROOT"
|
||||
rm -f "$TARBALL" "${TARBALL}.sha256"
|
||||
}
|
||||
|
||||
# Print summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
printf "${GREEN}${BOLD}✓ Release v$VERSION created successfully!${NC}\n"
|
||||
echo ""
|
||||
echo " Release URL: https://github.com/RooCodeInc/Roo-Code/releases/tag/$TAG"
|
||||
echo ""
|
||||
echo " Install with:"
|
||||
echo " curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
echo ""
|
||||
printf "${BLUE}${BOLD}"
|
||||
echo " ╭─────────────────────────────────╮"
|
||||
echo " │ Roo Code CLI Release Script │"
|
||||
echo " ╰─────────────────────────────────╯"
|
||||
printf "${NC}"
|
||||
echo ""
|
||||
|
||||
detect_platform
|
||||
check_prerequisites
|
||||
get_version "$1"
|
||||
build
|
||||
create_tarball
|
||||
create_checksum
|
||||
check_existing_release
|
||||
create_release
|
||||
cleanup
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
1164
apps/cli/src/__tests__/extension-host.test.ts
Normal file
144
apps/cli/src/__tests__/integration.test.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Integration tests for CLI
|
||||
*
|
||||
* These tests require a valid OPENROUTER_API_KEY environment variable.
|
||||
* They will be skipped if the API key is not available.
|
||||
*
|
||||
* Run with: OPENROUTER_API_KEY=sk-or-v1-... pnpm test
|
||||
*/
|
||||
|
||||
import { ExtensionHost } from "../extension-host.js"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import os from "os"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY
|
||||
const hasApiKey = !!OPENROUTER_API_KEY
|
||||
|
||||
// Find the extension path - we need a built extension for integration tests
|
||||
function findExtensionPath(): string | null {
|
||||
// From apps/cli/src/__tests__, go up to monorepo root then to src/dist
|
||||
const monorepoPath = path.resolve(__dirname, "../../../../src/dist")
|
||||
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
|
||||
return monorepoPath
|
||||
}
|
||||
// Also try from the apps/cli level
|
||||
const altPath = path.resolve(__dirname, "../../../src/dist")
|
||||
if (fs.existsSync(path.join(altPath, "extension.js"))) {
|
||||
return altPath
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const extensionPath = findExtensionPath()
|
||||
const hasExtension = !!extensionPath
|
||||
|
||||
// Create a temporary workspace directory for tests
|
||||
function createTempWorkspace(): string {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "roo-cli-test-"))
|
||||
return tempDir
|
||||
}
|
||||
|
||||
// Clean up temporary workspace
|
||||
function cleanupWorkspace(workspacePath: string): void {
|
||||
try {
|
||||
fs.rmSync(workspacePath, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(!hasApiKey || !hasExtension)(
|
||||
"CLI Integration Tests (requires OPENROUTER_API_KEY and built extension)",
|
||||
() => {
|
||||
let workspacePath: string
|
||||
let host: ExtensionHost
|
||||
|
||||
beforeAll(() => {
|
||||
console.log("Integration tests running with:")
|
||||
console.log(` - API Key: ${OPENROUTER_API_KEY?.substring(0, 12)}...`)
|
||||
console.log(` - Extension Path: ${extensionPath}`)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
workspacePath = createTempWorkspace()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (host) {
|
||||
await host.dispose()
|
||||
}
|
||||
cleanupWorkspace(workspacePath)
|
||||
})
|
||||
|
||||
/**
|
||||
* Main integration test - tests the complete end-to-end flow
|
||||
*
|
||||
* NOTE: Due to the extension using singletons (TelemetryService, etc.),
|
||||
* only one integration test can run per process. This single test covers
|
||||
* the main functionality: activation, task execution, completion, and disposal.
|
||||
*/
|
||||
it("should complete end-to-end task execution with proper lifecycle", async () => {
|
||||
host = new ExtensionHost({
|
||||
mode: "code",
|
||||
apiProvider: "openrouter",
|
||||
apiKey: OPENROUTER_API_KEY!,
|
||||
model: "anthropic/claude-haiku-4.5", // Use fast, cheap model for tests.
|
||||
workspacePath,
|
||||
extensionPath: extensionPath!,
|
||||
quiet: true,
|
||||
})
|
||||
|
||||
// Test activation
|
||||
await host.activate()
|
||||
|
||||
// Track state messages
|
||||
const stateMessages: unknown[] = []
|
||||
host.on("extensionWebviewMessage", (msg: Record<string, unknown>) => {
|
||||
if (msg.type === "state") {
|
||||
stateMessages.push(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// Test task execution with completion
|
||||
// Note: runTask internally waits for webview to be ready before sending messages
|
||||
await expect(host.runTask("Say hello in exactly 5 words")).resolves.toBeUndefined()
|
||||
|
||||
// After task completes, webview should have been ready
|
||||
expect(host.isInInitialSetup()).toBe(false)
|
||||
|
||||
// Verify we received state updates
|
||||
expect(stateMessages.length).toBeGreaterThan(0)
|
||||
|
||||
// Test disposal
|
||||
await host.dispose()
|
||||
expect((global as Record<string, unknown>).vscode).toBeUndefined()
|
||||
expect((global as Record<string, unknown>).__extensionHost).toBeUndefined()
|
||||
}, 120000) // 2 minute timeout
|
||||
},
|
||||
)
|
||||
|
||||
// Additional test to verify skip behavior
|
||||
describe("Integration test skip behavior", () => {
|
||||
it("should have OPENROUTER_API_KEY check", () => {
|
||||
if (hasApiKey) {
|
||||
console.log("OPENROUTER_API_KEY is set, integration tests will run")
|
||||
} else {
|
||||
console.log("OPENROUTER_API_KEY is not set, integration tests will be skipped")
|
||||
}
|
||||
expect(true).toBe(true) // Always passes
|
||||
})
|
||||
|
||||
it("should have extension check", () => {
|
||||
if (hasExtension) {
|
||||
console.log(`Extension found at: ${extensionPath}`)
|
||||
} else {
|
||||
console.log("Extension not found, integration tests will be skipped")
|
||||
}
|
||||
expect(true).toBe(true) // Always passes
|
||||
})
|
||||
})
|
||||
119
apps/cli/src/__tests__/utils.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Unit tests for CLI utility functions
|
||||
*/
|
||||
|
||||
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "../utils.js"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
// Mock fs module
|
||||
vi.mock("fs")
|
||||
|
||||
describe("getEnvVarName", () => {
|
||||
it.each([
|
||||
["anthropic", "ANTHROPIC_API_KEY"],
|
||||
["openai", "OPENAI_API_KEY"],
|
||||
["openrouter", "OPENROUTER_API_KEY"],
|
||||
["google", "GOOGLE_API_KEY"],
|
||||
["gemini", "GOOGLE_API_KEY"],
|
||||
["bedrock", "AWS_ACCESS_KEY_ID"],
|
||||
["ollama", "OLLAMA_API_KEY"],
|
||||
["mistral", "MISTRAL_API_KEY"],
|
||||
["deepseek", "DEEPSEEK_API_KEY"],
|
||||
])("should return %s for %s provider", (provider, expectedEnvVar) => {
|
||||
expect(getEnvVarName(provider)).toBe(expectedEnvVar)
|
||||
})
|
||||
|
||||
it("should handle case-insensitive provider names", () => {
|
||||
expect(getEnvVarName("ANTHROPIC")).toBe("ANTHROPIC_API_KEY")
|
||||
expect(getEnvVarName("Anthropic")).toBe("ANTHROPIC_API_KEY")
|
||||
expect(getEnvVarName("OpenRouter")).toBe("OPENROUTER_API_KEY")
|
||||
})
|
||||
|
||||
it("should return uppercase provider name with _API_KEY suffix for unknown providers", () => {
|
||||
expect(getEnvVarName("custom")).toBe("CUSTOM_API_KEY")
|
||||
expect(getEnvVarName("myProvider")).toBe("MYPROVIDER_API_KEY")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getApiKeyFromEnv", () => {
|
||||
const originalEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset process.env before each test
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for anthropic", () => {
|
||||
process.env.ANTHROPIC_API_KEY = "test-anthropic-key"
|
||||
expect(getApiKeyFromEnv("anthropic")).toBe("test-anthropic-key")
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for openrouter", () => {
|
||||
process.env.OPENROUTER_API_KEY = "test-openrouter-key"
|
||||
expect(getApiKeyFromEnv("openrouter")).toBe("test-openrouter-key")
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for openai", () => {
|
||||
process.env.OPENAI_API_KEY = "test-openai-key"
|
||||
expect(getApiKeyFromEnv("openai")).toBe("test-openai-key")
|
||||
})
|
||||
|
||||
it("should return undefined when API key is not set", () => {
|
||||
delete process.env.ANTHROPIC_API_KEY
|
||||
expect(getApiKeyFromEnv("anthropic")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle custom provider names", () => {
|
||||
process.env.CUSTOM_API_KEY = "test-custom-key"
|
||||
expect(getApiKeyFromEnv("custom")).toBe("test-custom-key")
|
||||
})
|
||||
|
||||
it("should handle case-insensitive provider lookup", () => {
|
||||
process.env.ANTHROPIC_API_KEY = "test-key"
|
||||
expect(getApiKeyFromEnv("ANTHROPIC")).toBe("test-key")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDefaultExtensionPath", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("should return monorepo path when extension.js exists there", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
expect(result).toBe(expectedMonorepoPath)
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
|
||||
})
|
||||
|
||||
it("should return package path when extension.js does not exist in monorepo path", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedPackagePath = path.resolve(mockDirname, "../extension")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
expect(result).toBe(expectedPackagePath)
|
||||
})
|
||||
|
||||
it("should check monorepo path first", () => {
|
||||
const mockDirname = "/some/path"
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
|
||||
getDefaultExtensionPath(mockDirname)
|
||||
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
|
||||
})
|
||||
})
|
||||
1663
apps/cli/src/extension-host.ts
Normal file
163
apps/cli/src/index.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* @roo-code/cli - Command Line Interface for Roo Code
|
||||
*/
|
||||
|
||||
import { Command } from "commander"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import {
|
||||
type ProviderName,
|
||||
type ReasoningEffortExtended,
|
||||
isProviderName,
|
||||
reasoningEffortsExtended,
|
||||
} from "@roo-code/types"
|
||||
import { setLogger } from "@roo-code/vscode-shim"
|
||||
|
||||
import { ExtensionHost } from "./extension-host.js"
|
||||
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "./utils.js"
|
||||
|
||||
const DEFAULTS = {
|
||||
mode: "code",
|
||||
reasoningEffort: "medium" as const,
|
||||
model: "anthropic/claude-sonnet-4.5",
|
||||
}
|
||||
|
||||
const REASONING_EFFORTS = [...reasoningEffortsExtended, "unspecified", "disabled"]
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program.name("roo").description("Roo Code CLI - Run the Roo Code agent from the command line").version("0.1.0")
|
||||
|
||||
program
|
||||
.argument("<prompt>", "The prompt/task to execute")
|
||||
.option("-w, --workspace <path>", "Workspace path to operate in", process.cwd())
|
||||
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
||||
.option("-v, --verbose", "Enable verbose output (show VSCode and extension logs)", false)
|
||||
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
|
||||
.option("-x, --exit-on-complete", "Exit the process when the task completes (useful for testing)", false)
|
||||
.option("-y, --yes", "Auto-approve all prompts (non-interactive mode)", false)
|
||||
.option("-k, --api-key <key>", "API key for the LLM provider (defaults to ANTHROPIC_API_KEY env var)")
|
||||
.option("-p, --provider <provider>", "API provider (anthropic, openai, openrouter, etc.)", "openrouter")
|
||||
.option("-m, --model <model>", "Model to use", DEFAULTS.model)
|
||||
.option("-M, --mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULTS.mode)
|
||||
.option(
|
||||
"-r, --reasoning-effort <effort>",
|
||||
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
||||
DEFAULTS.reasoningEffort,
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
prompt: string,
|
||||
options: {
|
||||
workspace: string
|
||||
extension?: string
|
||||
verbose: boolean
|
||||
debug: boolean
|
||||
exitOnComplete: boolean
|
||||
yes: boolean
|
||||
apiKey?: string
|
||||
provider: ProviderName
|
||||
model?: string
|
||||
mode?: string
|
||||
reasoningEffort?: ReasoningEffortExtended | "unspecified" | "disabled"
|
||||
},
|
||||
) => {
|
||||
// Default is quiet mode - suppress VSCode shim logs unless verbose
|
||||
// or debug is specified.
|
||||
if (!options.verbose && !options.debug) {
|
||||
setLogger({
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
const extensionPath = options.extension || getDefaultExtensionPath(__dirname)
|
||||
const apiKey = options.apiKey || getApiKeyFromEnv(options.provider)
|
||||
const workspacePath = path.resolve(options.workspace)
|
||||
|
||||
if (!apiKey) {
|
||||
console.error(
|
||||
`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`,
|
||||
)
|
||||
console.error(`[CLI] For ${options.provider}, set ${getEnvVarName(options.provider)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
console.error(`[CLI] Error: Workspace path does not exist: ${workspacePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!isProviderName(options.provider)) {
|
||||
console.error(`[CLI] Error: Invalid provider: ${options.provider}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (options.reasoningEffort && !REASONING_EFFORTS.includes(options.reasoningEffort)) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid reasoning effort: ${options.reasoningEffort}, must be one of: ${REASONING_EFFORTS.join(", ")}`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`[CLI] Mode: ${options.mode || "default"}`)
|
||||
console.log(`[CLI] Reasoning Effort: ${options.reasoningEffort || "default"}`)
|
||||
console.log(`[CLI] Provider: ${options.provider}`)
|
||||
console.log(`[CLI] Model: ${options.model || "default"}`)
|
||||
console.log(`[CLI] Workspace: ${workspacePath}`)
|
||||
|
||||
const host = new ExtensionHost({
|
||||
mode: options.mode || DEFAULTS.mode,
|
||||
reasoningEffort: options.reasoningEffort === "unspecified" ? undefined : options.reasoningEffort,
|
||||
apiProvider: options.provider,
|
||||
apiKey,
|
||||
model: options.model || DEFAULTS.model,
|
||||
workspacePath,
|
||||
extensionPath: path.resolve(extensionPath),
|
||||
verbose: options.debug,
|
||||
quiet: !options.verbose && !options.debug,
|
||||
nonInteractive: options.yes,
|
||||
})
|
||||
|
||||
// Handle SIGINT (Ctrl+C)
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\n[CLI] Received SIGINT, shutting down...")
|
||||
await host.dispose()
|
||||
process.exit(130)
|
||||
})
|
||||
|
||||
// Handle SIGTERM
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("\n[CLI] Received SIGTERM, shutting down...")
|
||||
await host.dispose()
|
||||
process.exit(143)
|
||||
})
|
||||
|
||||
try {
|
||||
await host.activate()
|
||||
await host.runTask(prompt)
|
||||
await host.dispose()
|
||||
|
||||
if (options.exitOnComplete) {
|
||||
process.exit(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[CLI] Error:", error instanceof Error ? error.message : String(error))
|
||||
|
||||
if (options.debug && error instanceof Error) {
|
||||
console.error(error.stack)
|
||||
}
|
||||
|
||||
await host.dispose()
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
program.parse()
|
||||
62
apps/cli/src/utils.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Utility functions for the Roo Code CLI
|
||||
*/
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
/**
|
||||
* Get the environment variable name for a provider's API key
|
||||
*/
|
||||
export function getEnvVarName(provider: string): string {
|
||||
const envVarMap: Record<string, string> = {
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
openai: "OPENAI_API_KEY",
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
google: "GOOGLE_API_KEY",
|
||||
gemini: "GOOGLE_API_KEY",
|
||||
bedrock: "AWS_ACCESS_KEY_ID",
|
||||
ollama: "OLLAMA_API_KEY",
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
}
|
||||
return envVarMap[provider.toLowerCase()] || `${provider.toUpperCase()}_API_KEY`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from environment variable based on provider
|
||||
*/
|
||||
export function getApiKeyFromEnv(provider: string): string | undefined {
|
||||
const envVar = getEnvVarName(provider)
|
||||
return process.env[envVar]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default path to the extension bundle.
|
||||
* This assumes the CLI is installed alongside the built extension.
|
||||
*
|
||||
* @param dirname - The __dirname equivalent for the calling module
|
||||
*/
|
||||
export function getDefaultExtensionPath(dirname: string): string {
|
||||
// Check for environment variable first (set by install script)
|
||||
if (process.env.ROO_EXTENSION_PATH) {
|
||||
const envPath = process.env.ROO_EXTENSION_PATH
|
||||
if (fs.existsSync(path.join(envPath, "extension.js"))) {
|
||||
return envPath
|
||||
}
|
||||
}
|
||||
|
||||
// __dirname is apps/cli/dist when bundled
|
||||
// The extension is at src/dist (relative to monorepo root)
|
||||
// So from apps/cli/dist, we need to go ../../../src/dist
|
||||
const monorepoPath = path.resolve(dirname, "../../../src/dist")
|
||||
|
||||
// Try monorepo path first (for development)
|
||||
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
|
||||
return monorepoPath
|
||||
}
|
||||
|
||||
// Fallback: when installed via curl script, extension is at ../extension
|
||||
const packagePath = path.resolve(dirname, "../extension")
|
||||
return packagePath
|
||||
}
|
||||
9
apps/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "*.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
24
apps/cli/tsup.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: "node20",
|
||||
platform: "node",
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
// Bundle workspace packages that export TypeScript
|
||||
noExternal: ["@roo-code/types", "@roo-code/vscode-shim"],
|
||||
external: [
|
||||
// Keep native modules external
|
||||
"@anthropic-ai/sdk",
|
||||
"@anthropic-ai/bedrock-sdk",
|
||||
"@anthropic-ai/vertex-sdk",
|
||||
// Keep @vscode/ripgrep external - we bundle the binary separately
|
||||
"@vscode/ripgrep",
|
||||
],
|
||||
})
|
||||
11
apps/cli/vitest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
watch: false,
|
||||
testTimeout: 120_000, // 2m for integration tests.
|
||||
include: ["src/**/*.test.ts"],
|
||||
},
|
||||
})
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -15,16 +15,10 @@ suite("Roo Code Extension", function () {
|
|||
"SidebarProvider.removeView",
|
||||
"activationCompleted",
|
||||
"plusButtonClicked",
|
||||
"mcpButtonClicked",
|
||||
"promptsButtonClicked",
|
||||
"popoutButtonClicked",
|
||||
"openInNewTab",
|
||||
"settingsButtonClicked",
|
||||
"historyButtonClicked",
|
||||
"showHumanRelayDialog",
|
||||
"registerHumanRelayCallback",
|
||||
"unregisterHumanRelayCallback",
|
||||
"handleHumanRelayResponse",
|
||||
"newTask",
|
||||
"setCustomStoragePath",
|
||||
"focusInput",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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.8",
|
||||
"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",
|
||||
|
|
|
|||
207
apps/web-evals/src/actions/__tests__/killRun.spec.ts
Normal 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),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -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,19 +13,33 @@ 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,
|
||||
executionMethod = "vscode",
|
||||
...values
|
||||
}: CreateRun) {
|
||||
const run = await _createRun({
|
||||
...values,
|
||||
timeout,
|
||||
executionMethod,
|
||||
socketPath: "", // TODO: Get rid of this.
|
||||
})
|
||||
|
||||
|
|
@ -37,15 +51,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 +131,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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
74
apps/web-evals/src/app/api/runs/[id]/logs/[taskId]/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
147
apps/web-evals/src/app/api/runs/[id]/logs/failed/route.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
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 and history files 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 expectedBase = path.resolve(LOG_BASE_PATH)
|
||||
|
||||
// Add the log file
|
||||
const logFileName = `${safeLanguage}-${safeExercise}.log`
|
||||
const logFilePath = path.join(logDir, logFileName)
|
||||
|
||||
// Verify the resolved path is within the expected directory (defense in depth)
|
||||
const resolvedLogPath = path.resolve(logFilePath)
|
||||
if (resolvedLogPath.startsWith(expectedBase) && fs.existsSync(logFilePath)) {
|
||||
archive.file(logFilePath, { name: logFileName })
|
||||
filesAdded++
|
||||
}
|
||||
|
||||
// Add the API conversation history file
|
||||
// Format: {language}-{exercise}.{iteration}_api_conversation_history.json
|
||||
const apiHistoryFileName = `${safeLanguage}-${safeExercise}.${task.iteration}_api_conversation_history.json`
|
||||
const apiHistoryFilePath = path.join(logDir, apiHistoryFileName)
|
||||
const resolvedApiHistoryPath = path.resolve(apiHistoryFilePath)
|
||||
if (resolvedApiHistoryPath.startsWith(expectedBase) && fs.existsSync(apiHistoryFilePath)) {
|
||||
archive.file(apiHistoryFilePath, { name: apiHistoryFileName })
|
||||
filesAdded++
|
||||
}
|
||||
|
||||
// Add the UI messages file
|
||||
// Format: {language}-{exercise}.{iteration}_ui_messages.json
|
||||
const uiMessagesFileName = `${safeLanguage}-${safeExercise}.${task.iteration}_ui_messages.json`
|
||||
const uiMessagesFilePath = path.join(logDir, uiMessagesFileName)
|
||||
const resolvedUiMessagesPath = path.resolve(uiMessagesFilePath)
|
||||
if (resolvedUiMessagesPath.startsWith(expectedBase) && fs.existsSync(uiMessagesFilePath)) {
|
||||
archive.file(uiMessagesFilePath, { name: uiMessagesFileName })
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 === "{}"
|
||||
|
|
|
|||
|
|
@ -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,97 @@ import {
|
|||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
ScrollArea,
|
||||
} from "@/components/ui"
|
||||
|
||||
// Tool group type (same as in runs.tsx)
|
||||
type ToolGroup = {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
tools: string[]
|
||||
}
|
||||
|
||||
type RunProps = {
|
||||
run: EvalsRun
|
||||
taskMetrics: EvalsTaskMetrics | null
|
||||
toolColumns: ToolName[]
|
||||
toolGroups: ToolGroup[]
|
||||
}
|
||||
|
||||
export function Run({ run, taskMetrics }: RunProps) {
|
||||
export function Run({ run, taskMetrics, toolColumns, toolGroups }: 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 +140,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],
|
||||
)
|
||||
|
||||
// Helper to render a tool group cell
|
||||
const renderToolGroupCell = (group: ToolGroup) => {
|
||||
if (!taskMetrics?.toolUsage) {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
let totalAttempts = 0
|
||||
let totalFailures = 0
|
||||
const breakdown: Array<{ tool: string; attempts: number; rate: string }> = []
|
||||
|
||||
for (const toolName of group.tools) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
if (totalAttempts === 0) {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
const successRate = ((totalAttempts - totalFailures) / totalAttempts) * 100
|
||||
const rateColor =
|
||||
successRate === 100 ? "text-muted-foreground" : successRate >= 80 ? "text-yellow-500" : "text-red-500"
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">{totalAttempts}</span>
|
||||
<span className={rateColor}>{Math.round(successRate)}%</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-xs">
|
||||
<div className="font-semibold mb-1">{group.name}</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>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
{/* Tool Group Columns */}
|
||||
{toolGroups.map((group) => (
|
||||
<TableCell key={group.id} className="text-xs text-center">
|
||||
{renderToolGroupCell(group)}
|
||||
</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 +383,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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
27
apps/web-evals/src/components/ui/checkbox.tsx
Normal 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 }
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
37
apps/web-evals/src/hooks/use-fuzzy-model-search.ts
Normal 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 }
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
66
apps/web-evals/src/hooks/use-roo-code-cloud-models.ts
Normal 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 }
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
apps/web-evals/src/lib/__tests__/formatters.spec.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { formatDuration, formatTokens } from "../formatters"
|
||||
|
||||
describe("formatDuration()", () => {
|
||||
it("formats as H:MM:SS", () => {
|
||||
expect(formatDuration(0)).toBe("0:00:00")
|
||||
expect(formatDuration(1_000)).toBe("0:00:01")
|
||||
expect(formatDuration(61_000)).toBe("0:01:01")
|
||||
expect(formatDuration(3_661_000)).toBe("1:01:01")
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatTokens()", () => {
|
||||
it("formats small numbers without suffix", () => {
|
||||
expect(formatTokens(0)).toBe("0")
|
||||
expect(formatTokens(999)).toBe("999")
|
||||
})
|
||||
|
||||
it("formats thousands without decimals and clamps to 1.0M at boundary", () => {
|
||||
expect(formatTokens(1_000)).toBe("1k")
|
||||
expect(formatTokens(72_500)).toBe("73k")
|
||||
expect(formatTokens(999_499)).toBe("999k")
|
||||
expect(formatTokens(999_500)).toBe("1.0M")
|
||||
})
|
||||
|
||||
it("formats millions with one decimal and clamps to 1.0B at boundary", () => {
|
||||
expect(formatTokens(1_000_000)).toBe("1.0M")
|
||||
expect(formatTokens(3_240_000)).toBe("3.2M")
|
||||
expect(formatTokens(999_950_000)).toBe("1.0B")
|
||||
})
|
||||
})
|
||||
|
|
@ -11,21 +11,10 @@ export const formatDuration = (durationMs: number) => {
|
|||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
|
||||
const parts = []
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours}h`)
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes}m`)
|
||||
}
|
||||
|
||||
if (remainingSeconds > 0 || parts.length === 0) {
|
||||
parts.push(`${remainingSeconds}s`)
|
||||
}
|
||||
|
||||
return parts.join(" ")
|
||||
// Format as H:MM:SS
|
||||
const mm = minutes.toString().padStart(2, "0")
|
||||
const ss = remainingSeconds.toString().padStart(2, "0")
|
||||
return `${hours}:${mm}:${ss}`
|
||||
}
|
||||
|
||||
export const formatTokens = (tokens: number) => {
|
||||
|
|
@ -34,15 +23,37 @@ export const formatTokens = (tokens: number) => {
|
|||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}k`
|
||||
// No decimal for thousands (e.g., 72k not 72.5k)
|
||||
const rounded = Math.round(tokens / 1000)
|
||||
// If rounding crosses the boundary to 1000k, show as 1.0M instead
|
||||
if (rounded >= 1000) {
|
||||
return "1.0M"
|
||||
}
|
||||
return `${rounded}k`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
// Keep decimal for millions (e.g., 3.2M)
|
||||
const rounded = Math.round(tokens / 100000) / 10 // Round to 1 decimal
|
||||
// If rounding crosses the boundary to 1000M, show as 1.0B instead
|
||||
if (rounded >= 1000) {
|
||||
return "1.0B"
|
||||
}
|
||||
return `${rounded.toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import { z } from "zod"
|
|||
import { rooCodeSettingsSchema } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* CreateRun
|
||||
* ExecutionMethod
|
||||
*/
|
||||
|
||||
export const MODEL_DEFAULT = "anthropic/claude-sonnet-4"
|
||||
export const executionMethodSchema = z.enum(["vscode", "cli"])
|
||||
export type ExecutionMethod = z.infer<typeof executionMethodSchema>
|
||||
|
||||
/**
|
||||
* CreateRun
|
||||
*/
|
||||
|
||||
export const CONCURRENCY_MIN = 1
|
||||
export const CONCURRENCY_MAX = 25
|
||||
|
|
@ -16,6 +21,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 +34,9 @@ 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(),
|
||||
executionMethod: executionMethodSchema,
|
||||
})
|
||||
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
|
||||
message: "Exercises are required when running a partial suite.",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"posthog-js": "^1.248.1",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
apps/web-roo-code/public/illustrations/form-factor-cloud.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
apps/web-roo-code/public/illustrations/form-factor-extension.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/1.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/10.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/11.jpg
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/12.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/13.jpg
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/14.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/15.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/16.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/17.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/18.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/19.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/2.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/20.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/21.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/22.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/23.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/24.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/3.jpg
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/4.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/5.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/6.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/7.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/8.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/9.jpg
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
1
apps/web-roo-code/public/logos/bedrock.svg
Normal 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 |
1
apps/web-roo-code/public/logos/moonshot.svg
Normal 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 |
1
apps/web-roo-code/public/logos/openrouter.svg
Normal 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 |
|
|
@ -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>
|
||||
|
|
|
|||
297
apps/web-roo-code/src/app/cloud/team/page.tsx
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Users,
|
||||
Settings,
|
||||
BarChart3,
|
||||
Lock,
|
||||
Puzzle,
|
||||
ShieldCheck,
|
||||
DollarSign,
|
||||
Share2,
|
||||
LucideIcon,
|
||||
Server,
|
||||
ServerIcon,
|
||||
RefreshCcw,
|
||||
} from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
const TITLE = "Roo Code Cloud Team Plan"
|
||||
const DESCRIPTION =
|
||||
"Scale your development with team collaboration features. Centralized billing, shared configuration, team-wide analytics, and unified GitHub and Slack integrations."
|
||||
const OG_DESCRIPTION = "Team collaboration for AI-powered development"
|
||||
const PATH = "/cloud/team"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"team",
|
||||
"collaboration",
|
||||
"enterprise",
|
||||
"organization",
|
||||
"centralized billing",
|
||||
"team management",
|
||||
],
|
||||
}
|
||||
|
||||
const keyBenefits = [
|
||||
{
|
||||
title: "No Per-Seat Costs",
|
||||
description: "Add unlimited team members without worrying about escalating per-seat charges.",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Centralized Billing",
|
||||
description:
|
||||
"Single billing point for all team members using Cloud Agents and the Roo Code Cloud Provider. No more API key management.",
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
title: "Unified Integrations",
|
||||
description:
|
||||
"Connect GitHub and Slack once for the entire team. No need for each member to set up individual integrations.",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
title: "Team-Wide Visibility",
|
||||
description: "Access task history and usage analytics across your entire team with granular per-user filters.",
|
||||
icon: BarChart3,
|
||||
},
|
||||
{
|
||||
title: "Configuration Enforcement",
|
||||
description:
|
||||
"Set policies for providers, models, and MCP servers to ensure your team follows organizational standards.",
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
title: "Secure Environment Variables",
|
||||
description:
|
||||
"Centrally manage secrets, API keys, and environment variables for Cloud Agents in our encrypted secret store.",
|
||||
icon: Lock,
|
||||
},
|
||||
]
|
||||
|
||||
interface Feature {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Configuration Enforcement",
|
||||
description:
|
||||
"Require team members to log in to the VS Code Extension so policies can be enforced via MDM distribution.",
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: "Provider Management",
|
||||
description:
|
||||
"Configure and manage the model providers your team can access for both the Extension and Cloud Agents, with API-key-free management.",
|
||||
},
|
||||
{
|
||||
icon: Puzzle,
|
||||
title: "Centralized Integration",
|
||||
description:
|
||||
"Centralized GitHub and Slack connection for the entire team. Agents can review PRs, collaborate on your repositories and respond on your team channels.",
|
||||
},
|
||||
{
|
||||
icon: RefreshCcw,
|
||||
title: "Extension Task Sync Config",
|
||||
description:
|
||||
"Require task syncing from VS Code Extension and control visibility settings for who can view each other's tasks.",
|
||||
},
|
||||
{
|
||||
icon: Share2,
|
||||
title: "Task Sharing Controls",
|
||||
description: "Enable per-task sharing with customizable audience controls and link expiration times.",
|
||||
},
|
||||
{
|
||||
icon: ServerIcon,
|
||||
title: "MCP Server Controls",
|
||||
description: "Control access to the Roo MCP Marketplace and what custom MCPs to make available to your team.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function CloudTeamPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<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-6xl">Roo Code Cloud Team</h1>
|
||||
<h2 className="text-2xl font-bold tracking-tight mb-6 md:text-4xl">
|
||||
Built for <span className="text-violet-500">AI-Forward Teams</span>
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-8 mx-auto">
|
||||
Empower your entire team with confidence with team-wide configuration, centralized billing,
|
||||
analytics and more. No per-seat costs, no API key juggling.
|
||||
</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 + "?redirect_url=/checkout/team"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start 14-Day Free Trial
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Key Benefits 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="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 mb-4">Why Teams Choose Roo</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Streamline collaboration and scale your development capacity with team-first features.
|
||||
</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-6">
|
||||
{keyBenefits.map((benefit, index) => {
|
||||
const Icon = benefit.icon
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="rounded-3xl bg-card outline outline-border/50 hover:outline-8 shadow-xl p-8 h-full w-full group transition-all hover:shadow-2xl hover:shadow-violet-800/30 relative">
|
||||
{Icon && (
|
||||
<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-hover:outline-3 group-hover:scale-105">
|
||||
<Icon className="size-5 text-violet-500" />
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-xl font-bold tracking-tight mb-2">{benefit.title}</h3>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed">
|
||||
{benefit.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 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-4xl font-bold tracking-tight mb-4">Complete Team Management</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Access all capabilities from your Organization Settings. Everything you need to manage your
|
||||
team in one place.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto relative">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<div
|
||||
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" />
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">{feature.title}</h3>
|
||||
<p className="leading-relaxed font-light text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<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-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">
|
||||
Ready to scale your team's development?
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">
|
||||
Start your free 14-day trial today. Got questions? Get in touch.
|
||||
</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-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/checkout/team"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start Free Trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" className="backdrop-blur-sm" asChild>
|
||||
<a href={EXTERNAL_LINKS.SUPPORT} className="flex items-center justify-center">
|
||||
Contact Support
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
84
apps/web-roo-code/src/app/extension/page.tsx
Normal 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'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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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'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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,74 +60,71 @@ 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
|
||||
}
|
||||
learnMoreLink?: string
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
learnMoreLink: "/cloud/team",
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -138,52 +134,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">
|
||||
<div className="grow mb-8 md:h-[214px]">
|
||||
<p className="text-sm text-muted-foreground font-light mb-2">
|
||||
{tier.featuresIntro}
|
||||
</p>
|
||||
<ul className="space-y-3 my-0 h-[148px]">
|
||||
<ul className="space-y-3 my-0">
|
||||
{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" />
|
||||
|
|
@ -191,54 +178,78 @@ export default function PricingPage() {
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{tier.learnMoreLink && (
|
||||
<div className="mt-2">
|
||||
<Link
|
||||
href={tier.learnMoreLink}
|
||||
className="text-sm text-violet-600 dark:text-violet-400 hover:underline">
|
||||
Learn more →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</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" 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're always in control of your spend, no surprises.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -249,7 +260,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 +268,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 +277,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 +304,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'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>
|
||||
|
|
|
|||
|
|
@ -283,7 +283,8 @@ export default function Privacy() {
|
|||
</li>
|
||||
<li>
|
||||
<strong>Delete your Cloud account</strong> at any time from{" "}
|
||||
<strong>Security Settings</strong> inside Roo Code Cloud.
|
||||
<strong>Security Settings</strong> inside Roo Code Cloud (User Menu → My Settings
|
||||
→ Open Profile).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Marketing communications:</strong> You can unsubscribe from marketing and
|
||||
|
|
|
|||
263
apps/web-roo-code/src/app/provider/page.tsx
Normal 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't have to use it to use Roo Code, but it'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'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'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,112 @@ 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">
|
||||
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">·</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 +159,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 +173,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 +240,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 +251,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>
|
||||
|
|
|
|||
109
apps/web-roo-code/src/components/homepage/cloud-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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`}
|
||||
/>
|
||||
|
|
|
|||
37
apps/web-roo-code/src/components/homepage/cta-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||