Merge branch 'main' into main
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -76,7 +76,7 @@ body:
|
|||
label: API Provider (optional)
|
||||
options:
|
||||
- Anthropic
|
||||
- AWS Bedrock
|
||||
- Amazon Bedrock
|
||||
- Chutes AI
|
||||
- DeepSeek
|
||||
- Featherless AI
|
||||
|
|
|
|||
3
.gitignore
vendored
|
|
@ -49,3 +49,6 @@ logs
|
|||
|
||||
# Qdrant
|
||||
qdrant_storage/
|
||||
|
||||
# Architect plans
|
||||
plans/
|
||||
|
|
@ -16,14 +16,14 @@ argument-hint: patch | minor | major
|
|||
[list of changes]
|
||||
```
|
||||
|
||||
- Always include contributor attribution using format: (thanks @username!)
|
||||
- For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)"
|
||||
- For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)"
|
||||
- Always include contributor attribution and the PR number: use "(PR #<prNumber> by @username)".
|
||||
- For PRs that close issues, include both the issue number and the PR number and authors: "- Fix: Description (#123 by @reporter, PR #456 by @contributor)"
|
||||
- For PRs without linked issues, include the PR number and author: "- Add support for feature (PR #456 by @contributor)"
|
||||
- Provide brief descriptions of each item to explain the change
|
||||
- Order the list from most important to least important
|
||||
- Example formats:
|
||||
- With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)"
|
||||
- Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)"
|
||||
- With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR #789 by @prAuthor)"
|
||||
- Without issue: "- Add support for Gemini 2.5 Pro caching (PR #789 by @contributor)"
|
||||
- CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed.
|
||||
|
||||
6. If the generate_image tool is available, create a release image at `releases/[version]-release.png`
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@
|
|||
|
||||
- 保留英文品牌名
|
||||
- 技术术语保持一致性
|
||||
- 保留英文专有名词:如"AWS Bedrock ARN"
|
||||
- 保留英文专有名词:如"Amazon Bedrock ARN"
|
||||
|
||||
4. **用户操作**
|
||||
- 操作动词统一:
|
||||
|
|
|
|||
366
CHANGELOG.md
|
|
@ -1,5 +1,351 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [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)
|
||||
- Migrate conversation continuity to plugin-side encrypted reasoning items using Responses API for improved reliability (thanks @hannesrudolph!)
|
||||
- Fix: Include mcpServers in getState() for auto-approval (#9190 by @bozoweed, PR by @daniel-lxs)
|
||||
- Batch settings updates from the webview to the extension host for improved performance (thanks @cte!)
|
||||
- Fix: Replace rate-limited badges with badgen.net to improve README reliability (thanks @daniel-lxs!)
|
||||
|
||||
## [3.31.1] - 2025-11-11
|
||||
|
||||

|
||||
|
|
@ -206,7 +552,7 @@
|
|||
|
||||
## [3.28.11] - 2025-09-29
|
||||
|
||||
- Fix: Correct AWS Bedrock Claude Sonnet 4.5 model identifier (#8371 by @sunhyung, PR by @app/roomote)
|
||||
- Fix: Correct Amazon Bedrock Claude Sonnet 4.5 model identifier (#8371 by @sunhyung, PR by @app/roomote)
|
||||
- Fix: Correct Claude Sonnet 4.5 model ID format (thanks @daniel-lxs!)
|
||||
|
||||
## [3.28.10] - 2025-09-29
|
||||
|
|
@ -538,7 +884,7 @@
|
|||
## [3.25.14] - 2025-08-13
|
||||
|
||||
- Fix: Only include verbosity parameter for models that support it (#7054 by @eastonmeth, PR by @app/roomote)
|
||||
- Fix: AWS Bedrock 1M context - Move anthropic_beta to additionalModelRequestFields (thanks @daniel-lxs!)
|
||||
- Fix: Amazon Bedrock 1M context - Move anthropic_beta to additionalModelRequestFields (thanks @daniel-lxs!)
|
||||
- Fix: Make cancelling requests more responsive by reverting recent changes
|
||||
|
||||
## [3.25.13] - 2025-08-12
|
||||
|
|
@ -903,7 +1249,7 @@
|
|||
- Add user-configurable search score threshold slider for semantic search (thanks @hannesrudolph!)
|
||||
- Add default headers and testing for litellm fetcher (thanks @andrewshu2000!)
|
||||
- Fix consistent cancellation error messages for thinking vs streaming phases
|
||||
- Fix AWS Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
|
||||
- Fix Amazon Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
|
||||
- Fix URL loading timeout issues in @ mentions (thanks @MuriloFP!)
|
||||
- Fix API retry exponential backoff capped at 10 minutes (thanks @MuriloFP!)
|
||||
- Fix Qdrant URL field auto-filling with default value (thanks @SannidhyaSah!)
|
||||
|
|
@ -917,7 +1263,7 @@
|
|||
- Suppress Mermaid error rendering
|
||||
- Improve Mermaid buttons with light background in light mode (thanks @chrarnoldus!)
|
||||
- Add .vscode/ to write-protected files/directories
|
||||
- Update AWS Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
|
||||
- Update Amazon Bedrock cross-region inference profile mapping (thanks @KevinZhao!)
|
||||
|
||||
## [3.22.5] - 2025-06-28
|
||||
|
||||
|
|
@ -1541,7 +1887,7 @@
|
|||
- Improved display of diff errors + easy copying for investigation
|
||||
- Fixes to .vscodeignore (thanks @franekp!)
|
||||
- Fix a zh-CN translation for model capabilities (thanks @zhangtony239!)
|
||||
- Rename AWS Bedrock to Amazon Bedrock (thanks @ronyblum!)
|
||||
- Rename Amazon Bedrock to Amazon Bedrock (thanks @ronyblum!)
|
||||
- Update extension title and description (thanks @StevenTCramer!)
|
||||
|
||||
## [3.11.12] - 2025-04-09
|
||||
|
|
@ -1790,12 +2136,12 @@
|
|||
- PowerShell-specific command handling (thanks @KJ7LNW!)
|
||||
- OpenAI-compatible DeepSeek/QwQ reasoning support (thanks @lightrabbit!)
|
||||
- Anthropic-style prompt caching in the OpenAI-compatible provider (thanks @dleen!)
|
||||
- Add Deepseek R1 for AWS Bedrock (thanks @ATempsch!)
|
||||
- Add Deepseek R1 for Amazon Bedrock (thanks @ATempsch!)
|
||||
- Fix MarkdownBlock text color for Dark High Contrast theme (thanks @cannuri!)
|
||||
- Add gemini-2.0-pro-exp-02-05 model to vertex (thanks @shohei-ihaya!)
|
||||
- Bring back progress status for multi-diff edits (thanks @qdaxb!)
|
||||
- Refactor alert dialog styles to use the correct vscode theme (thanks @cannuri!)
|
||||
- Custom ARNs in AWS Bedrock (thanks @Smartsheet-JB-Brown!)
|
||||
- Custom ARNs in Amazon Bedrock (thanks @Smartsheet-JB-Brown!)
|
||||
- Update MCP servers directory path for platform compatibility (thanks @hannesrudolph!)
|
||||
- Fix browser system prompt inclusion rules (thanks @cannuri!)
|
||||
- Publish git tags to github from CI (thanks @pdecat!)
|
||||
|
|
@ -1933,7 +2279,7 @@
|
|||
|
||||
## [3.7.1] - 2025-02-24
|
||||
|
||||
- Add AWS Bedrock support for Sonnet 3.7 and update some defaults to Sonnet 3.7 instead of 3.5
|
||||
- Add Amazon Bedrock support for Sonnet 3.7 and update some defaults to Sonnet 3.7 instead of 3.5
|
||||
|
||||
## [3.7.0] - 2025-02-24
|
||||
|
||||
|
|
@ -1950,7 +2296,7 @@
|
|||
|
||||
## [3.3.24] - 2025-02-20
|
||||
|
||||
- Fixed a bug with region selection preventing AWS Bedrock profiles from being saved (thanks @oprstchn!)
|
||||
- Fixed a bug with region selection preventing Amazon Bedrock profiles from being saved (thanks @oprstchn!)
|
||||
- Updated the price of gpt-4o (thanks @marvijo-code!)
|
||||
|
||||
## [3.3.23] - 2025-02-20
|
||||
|
|
@ -2134,7 +2480,7 @@
|
|||
- Reverts provider key entry back to checking onInput instead of onChange to hopefully address issues entering API keys (thanks @samhvw8!)
|
||||
- Added explicit checkbox to use Azure for OpenAI compatible providers (thanks @samhvw8!)
|
||||
- Fixed Glama usage reporting (thanks @punkpeye!)
|
||||
- Added Llama 3.3 70B Instruct model to the AWS Bedrock provider options (thanks @Premshay!)
|
||||
- Added Llama 3.3 70B Instruct model to the Amazon Bedrock provider options (thanks @Premshay!)
|
||||
|
||||
## [3.2.7]
|
||||
|
||||
|
|
|
|||
16
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>
|
||||
|
|
@ -37,7 +35,7 @@
|
|||
- [简体中文](locales/zh-CN/README.md)
|
||||
- [繁體中文](locales/zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -169,16 +167,6 @@ We love community contributions! Get started by reading our [CONTRIBUTING.md](CO
|
|||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all our contributors who have helped make Roo Code better!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](./LICENSE)
|
||||
|
|
|
|||
|
|
@ -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,8 +15,6 @@ suite("Roo Code Extension", function () {
|
|||
"SidebarProvider.removeView",
|
||||
"activationCompleted",
|
||||
"plusButtonClicked",
|
||||
"mcpButtonClicked",
|
||||
"promptsButtonClicked",
|
||||
"popoutButtonClicked",
|
||||
"openInNewTab",
|
||||
"settingsButtonClicked",
|
||||
|
|
|
|||
|
|
@ -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.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"p-map": "^7.0.3",
|
||||
"react": "^18.3.1",
|
||||
|
|
@ -51,6 +53,7 @@
|
|||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/ps-tree": "^1.1.6",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
|
|
|
|||
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,16 +13,22 @@ import {
|
|||
exerciseLanguages,
|
||||
createRun as _createRun,
|
||||
deleteRun as _deleteRun,
|
||||
updateRun as _updateRun,
|
||||
getIncompleteRuns as _getIncompleteRuns,
|
||||
deleteRunsByIds as _deleteRunsByIds,
|
||||
createTask,
|
||||
getExercisesForLanguage,
|
||||
} from "@roo-code/evals"
|
||||
|
||||
import { CreateRun } from "@/lib/schemas"
|
||||
import { redisClient } from "@/lib/server/redis"
|
||||
|
||||
// Storage base path for eval logs
|
||||
const EVALS_STORAGE_PATH = "/tmp/evals/runs"
|
||||
|
||||
const EVALS_REPO_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../../evals")
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export async function createRun({ suite, exercises = [], systemPrompt, timeout, ...values }: CreateRun) {
|
||||
export async function createRun({ suite, exercises = [], timeout, iterations = 1, ...values }: CreateRun) {
|
||||
const run = await _createRun({
|
||||
...values,
|
||||
timeout,
|
||||
|
|
@ -37,15 +43,34 @@ export async function createRun({ suite, exercises = [], systemPrompt, timeout,
|
|||
throw new Error("Invalid exercise path: " + path)
|
||||
}
|
||||
|
||||
await createTask({ ...values, runId: run.id, language: language as ExerciseLanguage, exercise })
|
||||
// Create multiple tasks for each iteration
|
||||
for (let iteration = 1; iteration <= iterations; iteration++) {
|
||||
await createTask({
|
||||
...values,
|
||||
runId: run.id,
|
||||
language: language as ExerciseLanguage,
|
||||
exercise,
|
||||
iteration,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const language of exerciseLanguages) {
|
||||
const exercises = await getExercisesForLanguage(EVALS_REPO_PATH, language)
|
||||
const languageExercises = await getExercisesForLanguage(EVALS_REPO_PATH, language)
|
||||
|
||||
await pMap(exercises, (exercise) => createTask({ runId: run.id, language, exercise }), {
|
||||
concurrency: 10,
|
||||
})
|
||||
// Create tasks for all iterations of each exercise
|
||||
const tasksToCreate: Array<{ language: ExerciseLanguage; exercise: string; iteration: number }> = []
|
||||
for (const exercise of languageExercises) {
|
||||
for (let iteration = 1; iteration <= iterations; iteration++) {
|
||||
tasksToCreate.push({ language, exercise, iteration })
|
||||
}
|
||||
}
|
||||
|
||||
await pMap(
|
||||
tasksToCreate,
|
||||
({ language, exercise, iteration }) => createTask({ runId: run.id, language, exercise, iteration }),
|
||||
{ concurrency: 10 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,3 +123,247 @@ export async function deleteRun(runId: number) {
|
|||
await _deleteRun(runId)
|
||||
revalidatePath("/runs")
|
||||
}
|
||||
|
||||
export type KillRunResult = {
|
||||
success: boolean
|
||||
killedContainers: string[]
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
/**
|
||||
* Kill all Docker containers associated with a run (controller and task runners).
|
||||
* Kills the controller first, waits 10 seconds, then kills runners.
|
||||
* Also clears Redis state for heartbeat and runners.
|
||||
*
|
||||
* Container naming conventions:
|
||||
* - Controller: evals-controller-{runId}
|
||||
* - Task runners: evals-task-{runId}-{taskId}.{attempt}
|
||||
*/
|
||||
export async function killRun(runId: number): Promise<KillRunResult> {
|
||||
const killedContainers: string[] = []
|
||||
const errors: string[] = []
|
||||
const controllerPattern = `evals-controller-${runId}`
|
||||
const taskPattern = `evals-task-${runId}-`
|
||||
|
||||
try {
|
||||
// Step 1: Kill the controller first
|
||||
console.log(`Killing controller: ${controllerPattern}`)
|
||||
try {
|
||||
execFileSync("docker", ["kill", controllerPattern], { encoding: "utf-8", timeout: 10000 })
|
||||
killedContainers.push(controllerPattern)
|
||||
console.log(`Killed controller container: ${controllerPattern}`)
|
||||
} catch (_error) {
|
||||
// Controller might not be running - that's ok, continue to kill runners
|
||||
console.log(`Controller ${controllerPattern} not running or already stopped`)
|
||||
}
|
||||
|
||||
// Step 2: Wait 10 seconds before killing runners
|
||||
console.log("Waiting 10 seconds before killing runners...")
|
||||
await sleep(10000)
|
||||
|
||||
// Step 3: Find and kill all task runner containers for THIS run only
|
||||
let taskContainerNames: string[] = []
|
||||
|
||||
try {
|
||||
const output = execFileSync("docker", ["ps", "--format", "{{.Names}}", "--filter", `name=${taskPattern}`], {
|
||||
encoding: "utf-8",
|
||||
timeout: 10000,
|
||||
})
|
||||
taskContainerNames = output
|
||||
.split("\n")
|
||||
.map((name) => name.trim())
|
||||
.filter((name) => name.length > 0 && name.startsWith(taskPattern))
|
||||
} catch (error) {
|
||||
console.error("Failed to list task containers:", error)
|
||||
errors.push("Failed to list Docker task containers")
|
||||
}
|
||||
|
||||
// Kill each task runner container
|
||||
for (const containerName of taskContainerNames) {
|
||||
try {
|
||||
execFileSync("docker", ["kill", containerName], { encoding: "utf-8", timeout: 10000 })
|
||||
killedContainers.push(containerName)
|
||||
console.log(`Killed task container: ${containerName}`)
|
||||
} catch (error) {
|
||||
// Container might have already stopped
|
||||
console.error(`Failed to kill container ${containerName}:`, error)
|
||||
errors.push(`Failed to kill container: ${containerName}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Clear Redis state
|
||||
try {
|
||||
const redis = await redisClient()
|
||||
const heartbeatKey = `heartbeat:${runId}`
|
||||
const runnersKey = `runners:${runId}`
|
||||
|
||||
await redis.del(heartbeatKey)
|
||||
await redis.del(runnersKey)
|
||||
console.log(`Cleared Redis keys: ${heartbeatKey}, ${runnersKey}`)
|
||||
} catch (error) {
|
||||
console.error("Failed to clear Redis state:", error)
|
||||
errors.push("Failed to clear Redis state")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in killRun:", error)
|
||||
errors.push("Unexpected error while killing containers")
|
||||
}
|
||||
|
||||
revalidatePath(`/runs/${runId}`)
|
||||
revalidatePath("/runs")
|
||||
|
||||
return {
|
||||
success: killedContainers.length > 0 || errors.length === 0,
|
||||
killedContainers,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
export type DeleteIncompleteRunsResult = {
|
||||
success: boolean
|
||||
deletedCount: number
|
||||
deletedRunIds: number[]
|
||||
storageErrors: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all incomplete runs (runs without a taskMetricsId/final score).
|
||||
* Removes both database records and storage folders.
|
||||
*/
|
||||
export async function deleteIncompleteRuns(): Promise<DeleteIncompleteRunsResult> {
|
||||
const storageErrors: string[] = []
|
||||
|
||||
// Get all incomplete runs
|
||||
const incompleteRuns = await _getIncompleteRuns()
|
||||
const runIds = incompleteRuns.map((run) => run.id)
|
||||
|
||||
if (runIds.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: 0,
|
||||
deletedRunIds: [],
|
||||
storageErrors: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Delete storage folders for each run
|
||||
for (const runId of runIds) {
|
||||
const storagePath = path.join(EVALS_STORAGE_PATH, String(runId))
|
||||
try {
|
||||
if (fs.existsSync(storagePath)) {
|
||||
fs.rmSync(storagePath, { recursive: true, force: true })
|
||||
console.log(`Deleted storage folder: ${storagePath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete storage folder ${storagePath}:`, error)
|
||||
storageErrors.push(`Failed to delete storage for run ${runId}`)
|
||||
}
|
||||
|
||||
// Also try to clear Redis state for any potentially running incomplete runs
|
||||
try {
|
||||
const redis = await redisClient()
|
||||
await redis.del(`heartbeat:${runId}`)
|
||||
await redis.del(`runners:${runId}`)
|
||||
} catch (error) {
|
||||
// Non-critical error, just log it
|
||||
console.error(`Failed to clear Redis state for run ${runId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await _deleteRunsByIds(runIds)
|
||||
|
||||
revalidatePath("/runs")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: runIds.length,
|
||||
deletedRunIds: runIds,
|
||||
storageErrors,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of incomplete runs (for UI display)
|
||||
*/
|
||||
export async function getIncompleteRunsCount(): Promise<number> {
|
||||
const incompleteRuns = await _getIncompleteRuns()
|
||||
return incompleteRuns.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all runs older than 30 days.
|
||||
* Removes both database records and storage folders.
|
||||
*/
|
||||
export async function deleteOldRuns(): Promise<DeleteIncompleteRunsResult> {
|
||||
const storageErrors: string[] = []
|
||||
|
||||
// Get all runs older than 30 days
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
const { getRuns } = await import("@roo-code/evals")
|
||||
const allRuns = await getRuns()
|
||||
const oldRuns = allRuns.filter((run) => run.createdAt < thirtyDaysAgo)
|
||||
const runIds = oldRuns.map((run) => run.id)
|
||||
|
||||
if (runIds.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: 0,
|
||||
deletedRunIds: [],
|
||||
storageErrors: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Delete storage folders for each run
|
||||
for (const runId of runIds) {
|
||||
const storagePath = path.join(EVALS_STORAGE_PATH, String(runId))
|
||||
try {
|
||||
if (fs.existsSync(storagePath)) {
|
||||
fs.rmSync(storagePath, { recursive: true, force: true })
|
||||
console.log(`Deleted storage folder: ${storagePath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete storage folder ${storagePath}:`, error)
|
||||
storageErrors.push(`Failed to delete storage for run ${runId}`)
|
||||
}
|
||||
|
||||
// Also try to clear Redis state
|
||||
try {
|
||||
const redis = await redisClient()
|
||||
await redis.del(`heartbeat:${runId}`)
|
||||
await redis.del(`runners:${runId}`)
|
||||
} catch (error) {
|
||||
// Non-critical error, just log it
|
||||
console.error(`Failed to clear Redis state for run ${runId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await _deleteRunsByIds(runIds)
|
||||
|
||||
revalidatePath("/runs")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: runIds.length,
|
||||
deletedRunIds: runIds,
|
||||
storageErrors,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the description of a run.
|
||||
*/
|
||||
export async function updateRunDescription(runId: number, description: string | null): Promise<{ success: boolean }> {
|
||||
try {
|
||||
await _updateRun(runId, { description })
|
||||
revalidatePath("/runs")
|
||||
revalidatePath(`/runs/${runId}`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error("Failed to update run description:", error)
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
129
apps/web-evals/src/app/api/runs/[id]/logs/failed/route.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import archiver from "archiver"
|
||||
|
||||
import { findRun, getTasks } from "@roo-code/evals"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
const LOG_BASE_PATH = "/tmp/evals/runs"
|
||||
|
||||
// Sanitize path components to prevent path traversal attacks
|
||||
function sanitizePathComponent(component: string): string {
|
||||
// Remove any path separators, null bytes, and other dangerous characters
|
||||
return component.replace(/[/\\:\0*?"<>|]/g, "_")
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const runId = Number(id)
|
||||
|
||||
if (isNaN(runId)) {
|
||||
return NextResponse.json({ error: "Invalid run ID" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify the run exists
|
||||
await findRun(runId)
|
||||
|
||||
// Get all tasks for this run
|
||||
const tasks = await getTasks(runId)
|
||||
|
||||
// Filter for failed tasks only
|
||||
const failedTasks = tasks.filter((task) => task.passed === false)
|
||||
|
||||
if (failedTasks.length === 0) {
|
||||
return NextResponse.json({ error: "No failed tasks to export" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create a zip archive
|
||||
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||
|
||||
// Collect chunks to build the response
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
archive.on("data", (chunk: Buffer) => {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
|
||||
// Track archive errors
|
||||
let archiveError: Error | null = null
|
||||
archive.on("error", (err: Error) => {
|
||||
archiveError = err
|
||||
})
|
||||
|
||||
// Set up the end promise before finalizing (proper event listener ordering)
|
||||
const archiveEndPromise = new Promise<void>((resolve, reject) => {
|
||||
archive.on("end", resolve)
|
||||
archive.on("error", reject)
|
||||
})
|
||||
|
||||
// Add each failed task's log file to the archive
|
||||
const logDir = path.join(LOG_BASE_PATH, String(runId))
|
||||
let filesAdded = 0
|
||||
|
||||
for (const task of failedTasks) {
|
||||
// Sanitize language and exercise to prevent path traversal
|
||||
const safeLanguage = sanitizePathComponent(task.language)
|
||||
const safeExercise = sanitizePathComponent(task.exercise)
|
||||
const logFileName = `${safeLanguage}-${safeExercise}.log`
|
||||
const logFilePath = path.join(logDir, logFileName)
|
||||
|
||||
// Verify the resolved path is within the expected directory (defense in depth)
|
||||
const resolvedPath = path.resolve(logFilePath)
|
||||
const expectedBase = path.resolve(LOG_BASE_PATH)
|
||||
if (!resolvedPath.startsWith(expectedBase)) {
|
||||
continue // Skip files with suspicious paths
|
||||
}
|
||||
|
||||
if (fs.existsSync(logFilePath)) {
|
||||
archive.file(logFilePath, { name: logFileName })
|
||||
filesAdded++
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any files were actually added
|
||||
if (filesAdded === 0) {
|
||||
archive.abort()
|
||||
return NextResponse.json(
|
||||
{ error: "No log files found - they may have been cleared from disk" },
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
// Finalize the archive
|
||||
await archive.finalize()
|
||||
|
||||
// Wait for all data to be collected
|
||||
await archiveEndPromise
|
||||
|
||||
// Check for archive errors
|
||||
if (archiveError) {
|
||||
throw archiveError
|
||||
}
|
||||
|
||||
// Combine all chunks into a single buffer
|
||||
const zipBuffer = Buffer.concat(chunks)
|
||||
|
||||
// Return the zip file
|
||||
return new NextResponse(zipBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="run-${runId}-failed-logs.zip"`,
|
||||
"Content-Length": String(zipBuffer.length),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error exporting failed logs:", error)
|
||||
|
||||
if (error instanceof Error && error.name === "RecordNotFoundError") {
|
||||
return NextResponse.json({ error: "Run not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Failed to export logs" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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,56 +1,642 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { LoaderCircle } from "lucide-react"
|
||||
import { useMemo, useState, useCallback, useEffect } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { LoaderCircle, FileText, Copy, Check, StopCircle } from "lucide-react"
|
||||
|
||||
import type { Run, TaskMetrics as _TaskMetrics } from "@roo-code/evals"
|
||||
import type { Run, TaskMetrics as _TaskMetrics, Task } from "@roo-code/evals"
|
||||
import type { ToolName } from "@roo-code/types"
|
||||
|
||||
import { formatCurrency, formatDuration, formatTokens } from "@/lib/formatters"
|
||||
import { formatCurrency, formatDuration, formatTokens, formatToolUsageSuccessRate } from "@/lib/formatters"
|
||||
import { useRunStatus } from "@/hooks/use-run-status"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
|
||||
import { killRun } from "@/actions/runs"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
ScrollArea,
|
||||
Button,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui"
|
||||
|
||||
import { TaskStatus } from "./task-status"
|
||||
import { RunStatus } from "./run-status"
|
||||
|
||||
type TaskMetrics = Pick<_TaskMetrics, "tokensIn" | "tokensOut" | "tokensContext" | "duration" | "cost">
|
||||
|
||||
type ToolUsageEntry = { attempts: number; failures: number }
|
||||
type ToolUsage = Record<string, ToolUsageEntry>
|
||||
|
||||
// Generate abbreviation from tool name (e.g., "read_file" -> "RF", "list_code_definition_names" -> "LCDN")
|
||||
function getToolAbbreviation(toolName: string): string {
|
||||
return toolName
|
||||
.split("_")
|
||||
.map((word) => word[0]?.toUpperCase() ?? "")
|
||||
.join("")
|
||||
}
|
||||
|
||||
// Pattern definitions for syntax highlighting
|
||||
type HighlightPattern = {
|
||||
pattern: RegExp
|
||||
className: string
|
||||
// If true, wraps the entire match; if a number, wraps that capture group
|
||||
wrapGroup?: number
|
||||
}
|
||||
|
||||
const HIGHLIGHT_PATTERNS: HighlightPattern[] = [
|
||||
// Log levels - styled as badges
|
||||
{ pattern: /\|\s*(INFO)\s*\|/g, className: "text-green-400", wrapGroup: 1 },
|
||||
{ pattern: /\|\s*(WARN|WARNING)\s*\|/g, className: "text-yellow-400", wrapGroup: 1 },
|
||||
{ pattern: /\|\s*(ERROR)\s*\|/g, className: "text-red-400 font-semibold", wrapGroup: 1 },
|
||||
{ pattern: /\|\s*(DEBUG)\s*\|/g, className: "text-gray-400", wrapGroup: 1 },
|
||||
// Task identifiers - important events
|
||||
{
|
||||
pattern: /(taskCreated|taskFocused|taskStarted|taskCompleted|taskAborted|taskResumable)/g,
|
||||
className: "text-purple-400 font-medium",
|
||||
},
|
||||
// Tool failures - highlight in red
|
||||
{ pattern: /(taskToolFailed)/g, className: "text-red-400 font-bold" },
|
||||
{ pattern: /(Tool execution failed|tool.*failed|failed.*tool)/gi, className: "text-red-400" },
|
||||
{ pattern: /(EvalPass)/g, className: "text-green-400 font-bold" },
|
||||
{ pattern: /(EvalFail)/g, className: "text-red-400 font-bold" },
|
||||
// Message arrows
|
||||
{ pattern: /→/g, className: "text-cyan-400" },
|
||||
// Tool names in quotes
|
||||
{ pattern: /"(tool)":\s*"([^"]+)"/g, className: "text-orange-400" },
|
||||
// JSON keys
|
||||
{ pattern: /"([^"]+)":/g, className: "text-sky-300" },
|
||||
// Boolean values
|
||||
{ pattern: /:\s*(true|false)/g, className: "text-amber-400", wrapGroup: 1 },
|
||||
// Numbers
|
||||
{ pattern: /:\s*(-?\d+\.?\d*)/g, className: "text-emerald-400", wrapGroup: 1 },
|
||||
]
|
||||
|
||||
// Extract timestamp from a log line and return elapsed time from baseline
|
||||
function formatElapsedTime(timestamp: string, baselineMs: number): string {
|
||||
const currentMs = new Date(timestamp).getTime()
|
||||
const elapsedMs = currentMs - baselineMs
|
||||
const totalSeconds = Math.floor(elapsedMs / 1000)
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
// Extract the first timestamp from the log to use as baseline
|
||||
function extractFirstTimestamp(log: string): number | null {
|
||||
// Match timestamp at start of line: [2025-11-28T09:35:23.187Z | ... or [2025-11-28T09:35:23.187Z]
|
||||
const match = log.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)[\s|\]]/)
|
||||
const isoString = match?.[1]
|
||||
if (!isoString) return null
|
||||
return new Date(isoString).getTime()
|
||||
}
|
||||
|
||||
// Simplify log line by removing redundant metadata
|
||||
function simplifyLogLine(line: string, baselineMs: number | null): { timestamp: string; simplified: string } {
|
||||
// Extract timestamp - matches [2025-11-28T09:35:23.187Z | ... format
|
||||
const timestampMatch = line.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)[\s|\]]/)
|
||||
const isoTimestamp = timestampMatch?.[1]
|
||||
if (!isoTimestamp) {
|
||||
return { timestamp: "", simplified: line }
|
||||
}
|
||||
|
||||
const timestamp = baselineMs !== null ? formatElapsedTime(isoTimestamp, baselineMs) : isoTimestamp.slice(11, 19)
|
||||
|
||||
// Remove the timestamp from the line (handles both [timestamp] and [timestamp | formats)
|
||||
let simplified = line.replace(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\s*\|?\s*/, "")
|
||||
|
||||
// Remove redundant metadata: pid, run, task IDs (they're same for entire log)
|
||||
simplified = simplified.replace(/\|\s*pid:\d+\s*/g, "")
|
||||
simplified = simplified.replace(/\|\s*run:\d+\s*/g, "")
|
||||
simplified = simplified.replace(/\|\s*task:\d+\s*/g, "")
|
||||
simplified = simplified.replace(/runTask\s*\|\s*/g, "")
|
||||
|
||||
// Clean up extra pipes, spaces, and trailing brackets
|
||||
simplified = simplified.replace(/\|\s*\|/g, "|")
|
||||
simplified = simplified.replace(/^\s*\|\s*/, "")
|
||||
simplified = simplified.replace(/\]\s*$/, "") // Remove trailing bracket if present
|
||||
|
||||
return { timestamp, simplified }
|
||||
}
|
||||
|
||||
// Format a single line with syntax highlighting using React elements (XSS-safe)
|
||||
function formatLine(line: string): React.ReactNode[] {
|
||||
// Find all matches with their positions
|
||||
type Match = { start: number; end: number; text: string; className: string }
|
||||
const matches: Match[] = []
|
||||
|
||||
for (const { pattern, className, wrapGroup } of HIGHLIGHT_PATTERNS) {
|
||||
// Reset regex state
|
||||
pattern.lastIndex = 0
|
||||
let regexMatch
|
||||
while ((regexMatch = pattern.exec(line)) !== null) {
|
||||
const capturedText = wrapGroup !== undefined ? regexMatch[wrapGroup] : regexMatch[0]
|
||||
// Skip if capture group didn't match
|
||||
if (!capturedText) continue
|
||||
const start =
|
||||
wrapGroup !== undefined ? regexMatch.index + regexMatch[0].indexOf(capturedText) : regexMatch.index
|
||||
matches.push({
|
||||
start,
|
||||
end: start + capturedText.length,
|
||||
text: capturedText,
|
||||
className,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort matches by position and filter overlapping ones
|
||||
matches.sort((a, b) => a.start - b.start)
|
||||
const filteredMatches: Match[] = []
|
||||
for (const m of matches) {
|
||||
const lastMatch = filteredMatches[filteredMatches.length - 1]
|
||||
if (!lastMatch || m.start >= lastMatch.end) {
|
||||
filteredMatches.push(m)
|
||||
}
|
||||
}
|
||||
|
||||
// Build result with highlighted spans
|
||||
const result: React.ReactNode[] = []
|
||||
let currentPos = 0
|
||||
|
||||
for (const [i, m] of filteredMatches.entries()) {
|
||||
// Add text before this match
|
||||
if (m.start > currentPos) {
|
||||
result.push(line.slice(currentPos, m.start))
|
||||
}
|
||||
// Add highlighted match
|
||||
result.push(
|
||||
<span key={`${i}-${m.start}`} className={m.className}>
|
||||
{m.text}
|
||||
</span>,
|
||||
)
|
||||
currentPos = m.end
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (currentPos < line.length) {
|
||||
result.push(line.slice(currentPos))
|
||||
}
|
||||
|
||||
return result.length > 0 ? result : [line]
|
||||
}
|
||||
|
||||
// Determine the visual style for a log line based on its content
|
||||
function getLineStyle(line: string): string {
|
||||
if (line.includes("ERROR")) return "bg-red-950/30 border-l-2 border-red-500"
|
||||
if (line.includes("WARN") || line.includes("WARNING")) return "bg-yellow-950/20 border-l-2 border-yellow-500"
|
||||
if (line.includes("taskToolFailed")) return "bg-red-950/30 border-l-2 border-red-500"
|
||||
if (line.includes("taskStarted") || line.includes("taskCreated")) return "bg-purple-950/20"
|
||||
if (line.includes("EvalPass")) return "bg-green-950/30 border-l-2 border-green-500"
|
||||
if (line.includes("EvalFail")) return "bg-red-950/30 border-l-2 border-red-500"
|
||||
if (line.includes("taskCompleted") || line.includes("taskAborted")) return "bg-blue-950/20"
|
||||
return ""
|
||||
}
|
||||
|
||||
// Format log content with basic highlighting (XSS-safe - no dangerouslySetInnerHTML)
|
||||
function formatLogContent(log: string): React.ReactNode[] {
|
||||
const lines = log.split("\n")
|
||||
const baselineMs = extractFirstTimestamp(log)
|
||||
|
||||
return lines.map((line, index) => {
|
||||
if (!line.trim()) {
|
||||
return (
|
||||
<div key={index} className="h-2">
|
||||
{" "}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const parsed = simplifyLogLine(line, baselineMs)
|
||||
const lineStyle = getLineStyle(line)
|
||||
|
||||
return (
|
||||
<div key={index} className={`flex hover:bg-white/10 py-0.5 rounded-sm transition-colors ${lineStyle}`}>
|
||||
{/* Elapsed time */}
|
||||
<span className="text-blue-400 font-mono w-12 flex-shrink-0 tabular-nums text-right pr-2">
|
||||
{parsed.timestamp}
|
||||
</span>
|
||||
{/* Log content - pl-12 ensures wrapped lines are indented under the timestamp */}
|
||||
<span className="flex-1 break-words" style={{ textIndent: "-0.5rem", paddingLeft: "0.5rem" }}>
|
||||
{formatLine(parsed.simplified)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function Run({ run }: { run: Run }) {
|
||||
const runStatus = useRunStatus(run)
|
||||
const { tasks, tokenUsage, usageUpdatedAt } = runStatus
|
||||
const { tasks, tokenUsage, toolUsage, usageUpdatedAt, heartbeat, runners } = runStatus
|
||||
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
|
||||
const [taskLog, setTaskLog] = useState<string | null>(null)
|
||||
const [isLoadingLog, setIsLoadingLog] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showKillDialog, setShowKillDialog] = useState(false)
|
||||
const [isKilling, setIsKilling] = useState(false)
|
||||
|
||||
// Determine if run is still active (has heartbeat or runners)
|
||||
const isRunActive = !run.taskMetricsId && (!!heartbeat || (runners && runners.length > 0))
|
||||
|
||||
const onKillRun = useCallback(async () => {
|
||||
setIsKilling(true)
|
||||
try {
|
||||
const result = await killRun(run.id)
|
||||
if (result.killedContainers.length > 0) {
|
||||
toast.success(`Killed ${result.killedContainers.length} container(s)`)
|
||||
} else if (result.errors.length === 0) {
|
||||
toast.info("No running containers found")
|
||||
} else {
|
||||
toast.error(result.errors.join(", "))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to kill run:", error)
|
||||
toast.error("Failed to kill run")
|
||||
} finally {
|
||||
setIsKilling(false)
|
||||
setShowKillDialog(false)
|
||||
}
|
||||
}, [run.id])
|
||||
|
||||
const onCopyLog = useCallback(async () => {
|
||||
if (!taskLog) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(taskLog)
|
||||
setCopied(true)
|
||||
toast.success("Log copied to clipboard")
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (error) {
|
||||
console.error("Failed to copy log:", error)
|
||||
toast.error("Failed to copy log")
|
||||
}
|
||||
}, [taskLog])
|
||||
|
||||
// Handle ESC key to close the dialog
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && selectedTask) {
|
||||
setSelectedTask(null)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
return () => document.removeEventListener("keydown", handleKeyDown)
|
||||
}, [selectedTask])
|
||||
|
||||
const onViewTaskLog = useCallback(
|
||||
async (task: Task) => {
|
||||
// Only allow viewing logs for tasks that have started
|
||||
if (!task.startedAt && !tokenUsage.get(task.id)) {
|
||||
toast.error("Task has not started yet")
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedTask(task)
|
||||
setIsLoadingLog(true)
|
||||
setTaskLog(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/runs/${run.id}/logs/${task.id}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
toast.error(error.error || "Failed to load log")
|
||||
setSelectedTask(null)
|
||||
return
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setTaskLog(data.logContent)
|
||||
} catch (error) {
|
||||
console.error("Error loading task log:", error)
|
||||
toast.error("Failed to load log")
|
||||
setSelectedTask(null)
|
||||
} finally {
|
||||
setIsLoadingLog(false)
|
||||
}
|
||||
},
|
||||
[run.id, tokenUsage],
|
||||
)
|
||||
|
||||
const taskMetrics: Record<number, TaskMetrics> = useMemo(() => {
|
||||
// Reference usageUpdatedAt to trigger recomputation when Map contents change
|
||||
void usageUpdatedAt
|
||||
const metrics: Record<number, TaskMetrics> = {}
|
||||
|
||||
tasks?.forEach((task) => {
|
||||
const usage = tokenUsage.get(task.id)
|
||||
const streamingUsage = tokenUsage.get(task.id)
|
||||
const dbMetrics = task.taskMetrics
|
||||
|
||||
if (task.finishedAt && task.taskMetrics) {
|
||||
metrics[task.id] = task.taskMetrics
|
||||
} else if (usage) {
|
||||
// For finished tasks, prefer DB values but fall back to streaming values
|
||||
// This handles race conditions during timeout where DB might not have latest data
|
||||
if (task.finishedAt) {
|
||||
// Check if DB metrics have meaningful values (not just default/empty)
|
||||
const dbHasData = dbMetrics && (dbMetrics.tokensIn > 0 || dbMetrics.tokensOut > 0 || dbMetrics.cost > 0)
|
||||
if (dbHasData) {
|
||||
metrics[task.id] = dbMetrics
|
||||
} else if (streamingUsage) {
|
||||
// Fall back to streaming values if DB is empty/stale
|
||||
metrics[task.id] = {
|
||||
tokensIn: streamingUsage.totalTokensIn,
|
||||
tokensOut: streamingUsage.totalTokensOut,
|
||||
tokensContext: streamingUsage.contextTokens,
|
||||
duration: streamingUsage.duration ?? 0,
|
||||
cost: streamingUsage.totalCost,
|
||||
}
|
||||
}
|
||||
} else if (streamingUsage) {
|
||||
// For running tasks, use streaming values
|
||||
metrics[task.id] = {
|
||||
tokensIn: usage.totalTokensIn,
|
||||
tokensOut: usage.totalTokensOut,
|
||||
tokensContext: usage.contextTokens,
|
||||
duration: usage.duration ?? 0,
|
||||
cost: usage.totalCost,
|
||||
tokensIn: streamingUsage.totalTokensIn,
|
||||
tokensOut: streamingUsage.totalTokensOut,
|
||||
tokensContext: streamingUsage.contextTokens,
|
||||
duration: streamingUsage.duration ?? 0,
|
||||
cost: streamingUsage.totalCost,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return metrics
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tasks, tokenUsage, usageUpdatedAt])
|
||||
|
||||
// Collect all unique tool names from all tasks and sort by total attempts
|
||||
const toolColumns = useMemo<ToolName[]>(() => {
|
||||
// Reference usageUpdatedAt to trigger recomputation when Map contents change
|
||||
void usageUpdatedAt
|
||||
if (!tasks) return []
|
||||
|
||||
const toolTotals = new Map<ToolName, number>()
|
||||
|
||||
for (const task of tasks) {
|
||||
// Get both DB and streaming values
|
||||
const dbToolUsage = task.taskMetrics?.toolUsage
|
||||
const streamingToolUsage = toolUsage.get(task.id)
|
||||
|
||||
// For finished tasks, prefer DB values but fall back to streaming values
|
||||
// For running tasks, use streaming values
|
||||
// This handles race conditions during timeout where DB might not have latest data
|
||||
const taskToolUsage = task.finishedAt
|
||||
? dbToolUsage && Object.keys(dbToolUsage).length > 0
|
||||
? dbToolUsage
|
||||
: streamingToolUsage
|
||||
: streamingToolUsage
|
||||
|
||||
if (taskToolUsage) {
|
||||
for (const [toolName, usage] of Object.entries(taskToolUsage)) {
|
||||
const tool = toolName as ToolName
|
||||
const current = toolTotals.get(tool) ?? 0
|
||||
toolTotals.set(tool, current + usage.attempts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by total attempts descending
|
||||
return Array.from(toolTotals.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([name]): ToolName => name)
|
||||
// toolUsage ref is stable; usageUpdatedAt triggers recomputation when Map contents change
|
||||
}, [tasks, toolUsage, usageUpdatedAt])
|
||||
|
||||
// Compute aggregate stats
|
||||
const stats = useMemo(() => {
|
||||
// Reference usageUpdatedAt to trigger recomputation when Map contents change
|
||||
void usageUpdatedAt
|
||||
if (!tasks) return null
|
||||
|
||||
const passed = tasks.filter((t) => t.passed === true).length
|
||||
const failed = tasks.filter((t) => t.passed === false).length
|
||||
const completed = passed + failed
|
||||
|
||||
let totalTokensIn = 0
|
||||
let totalTokensOut = 0
|
||||
let totalCost = 0
|
||||
let totalDuration = 0
|
||||
|
||||
// Aggregate tool usage from all tasks (both finished and running)
|
||||
const toolUsageAggregate: ToolUsage = {}
|
||||
|
||||
for (const task of tasks) {
|
||||
const metrics = taskMetrics[task.id]
|
||||
if (metrics) {
|
||||
totalTokensIn += metrics.tokensIn
|
||||
totalTokensOut += metrics.tokensOut
|
||||
totalCost += metrics.cost
|
||||
totalDuration += metrics.duration
|
||||
}
|
||||
|
||||
// Aggregate tool usage: prefer DB values for finished tasks, fall back to streaming values
|
||||
// This handles race conditions during timeout where DB might not have latest data
|
||||
const dbToolUsage = task.taskMetrics?.toolUsage
|
||||
const streamingToolUsage = toolUsage.get(task.id)
|
||||
const taskToolUsage = task.finishedAt
|
||||
? dbToolUsage && Object.keys(dbToolUsage).length > 0
|
||||
? dbToolUsage
|
||||
: streamingToolUsage
|
||||
: streamingToolUsage
|
||||
|
||||
if (taskToolUsage) {
|
||||
for (const [key, usage] of Object.entries(taskToolUsage)) {
|
||||
const tool = key as keyof ToolUsage
|
||||
if (!toolUsageAggregate[tool]) {
|
||||
toolUsageAggregate[tool] = { attempts: 0, failures: 0 }
|
||||
}
|
||||
toolUsageAggregate[tool].attempts += usage.attempts
|
||||
toolUsageAggregate[tool].failures += usage.failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passed,
|
||||
failed,
|
||||
completed,
|
||||
passRate: completed > 0 ? ((passed / completed) * 100).toFixed(1) : null,
|
||||
totalTokensIn,
|
||||
totalTokensOut,
|
||||
totalCost,
|
||||
totalDuration,
|
||||
toolUsage: toolUsageAggregate,
|
||||
}
|
||||
// Map refs are stable; usageUpdatedAt triggers recomputation when Map contents change
|
||||
}, [tasks, taskMetrics, toolUsage, usageUpdatedAt])
|
||||
|
||||
// Calculate elapsed time (wall-clock time from run creation to completion or now)
|
||||
const elapsedTime = useMemo(() => {
|
||||
// Reference usageUpdatedAt to trigger recomputation for live elapsed time updates
|
||||
void usageUpdatedAt
|
||||
if (!tasks || tasks.length === 0) return null
|
||||
|
||||
const startTime = new Date(run.createdAt).getTime()
|
||||
|
||||
// If run is complete, find the latest finishedAt from tasks
|
||||
if (run.taskMetricsId) {
|
||||
const latestFinish = tasks.reduce((latest, task) => {
|
||||
if (task.finishedAt) {
|
||||
const finishTime = new Date(task.finishedAt).getTime()
|
||||
return finishTime > latest ? finishTime : latest
|
||||
}
|
||||
return latest
|
||||
}, startTime)
|
||||
return latestFinish - startTime
|
||||
}
|
||||
|
||||
// If still running, use current time
|
||||
return Date.now() - startTime
|
||||
}, [tasks, run.createdAt, run.taskMetricsId, usageUpdatedAt])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<div>
|
||||
<div className="font-mono">{run.model}</div>
|
||||
{run.description && <div className="text-sm text-muted-foreground">{run.description}</div>}
|
||||
{stats && (
|
||||
<div className="mb-4 p-4 border rounded-lg bg-muted sticky top-0 z-10">
|
||||
{/* Provider, Model title and status */}
|
||||
<div className="flex items-center justify-center gap-3 mb-3 relative">
|
||||
{run.settings?.apiProvider && (
|
||||
<span className="text-sm text-muted-foreground">{run.settings.apiProvider}</span>
|
||||
)}
|
||||
<div className="font-mono">{run.model}</div>
|
||||
<RunStatus runStatus={runStatus} isComplete={!!run.taskMetricsId} />
|
||||
{run.description && (
|
||||
<span className="text-sm text-muted-foreground">- {run.description}</span>
|
||||
)}
|
||||
{isRunActive && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowKillDialog(true)}
|
||||
disabled={isKilling}
|
||||
className="absolute right-0 flex items-center gap-1 text-muted-foreground hover:text-destructive">
|
||||
{isKilling ? (
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
) : (
|
||||
<StopCircle className="size-4" />
|
||||
)}
|
||||
Kill
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop all containers for this run</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{/* Main Stats Row */}
|
||||
<div className="flex items-start justify-center gap-x-8 gap-y-3">
|
||||
{/* Passed/Failed */}
|
||||
<div className="text-center min-w-[80px]">
|
||||
<div className="text-2xl font-bold whitespace-nowrap">
|
||||
<span className="text-green-600">{stats.passed}</span>
|
||||
<span className="text-muted-foreground mx-1">/</span>
|
||||
<span className="text-red-600">{stats.failed}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Passed / Failed</div>
|
||||
</div>
|
||||
|
||||
{/* Pass Rate */}
|
||||
<div className="text-center min-w-[80px]">
|
||||
<div
|
||||
className={`text-2xl font-bold ${
|
||||
stats.passRate === null
|
||||
? ""
|
||||
: parseFloat(stats.passRate) === 100
|
||||
? ""
|
||||
: parseFloat(stats.passRate) >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
}`}>
|
||||
{stats.passRate ? `${stats.passRate}%` : "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Pass Rate</div>
|
||||
</div>
|
||||
|
||||
{/* Tokens */}
|
||||
<div className="text-center min-w-[140px]">
|
||||
<div className="text-xl font-bold font-mono whitespace-nowrap">
|
||||
{formatTokens(stats.totalTokensIn)}
|
||||
<span className="text-muted-foreground mx-1">/</span>
|
||||
{formatTokens(stats.totalTokensOut)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Tokens In / Out</div>
|
||||
</div>
|
||||
|
||||
{/* Cost */}
|
||||
<div className="text-center min-w-[70px]">
|
||||
<div className="text-2xl font-bold font-mono">{formatCurrency(stats.totalCost)}</div>
|
||||
<div className="text-xs text-muted-foreground">Cost</div>
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className="text-center min-w-[90px]">
|
||||
<div className="text-2xl font-bold font-mono whitespace-nowrap">
|
||||
{stats.totalDuration > 0 ? formatDuration(stats.totalDuration) : "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Duration</div>
|
||||
</div>
|
||||
|
||||
{/* Elapsed Time */}
|
||||
<div className="text-center min-w-[90px]">
|
||||
<div className="text-2xl font-bold font-mono whitespace-nowrap">
|
||||
{elapsedTime !== null ? formatDuration(elapsedTime) : "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Elapsed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tool Usage Row */}
|
||||
{Object.keys(stats.toolUsage).length > 0 && (
|
||||
<div className="flex items-center justify-center gap-2 flex-wrap mt-3">
|
||||
{Object.entries(stats.toolUsage)
|
||||
.sort(([, a], [, b]) => b.attempts - a.attempts)
|
||||
.map(([toolName, usage]) => {
|
||||
const abbr = getToolAbbreviation(toolName)
|
||||
const successRate =
|
||||
usage.attempts > 0
|
||||
? ((usage.attempts - usage.failures) / usage.attempts) * 100
|
||||
: 100
|
||||
const rateColor =
|
||||
successRate === 100
|
||||
? "text-green-500"
|
||||
: successRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
return (
|
||||
<Tooltip key={toolName}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded bg-background/50 border border-border/50 hover:border-border transition-colors cursor-default text-xs">
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{abbr}
|
||||
</span>
|
||||
<span className="font-bold tabular-nums">{usage.attempts}</span>
|
||||
<span className={`${rateColor}`}>
|
||||
{formatToolUsageSuccessRate(usage)}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{toolName}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!run.taskMetricsId && <RunStatus runStatus={runStatus} />}
|
||||
</div>
|
||||
)}
|
||||
{!tasks ? (
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
) : (
|
||||
|
|
@ -60,53 +646,213 @@ export function Run({ run }: { run: Run }) {
|
|||
<TableHead>Exercise</TableHead>
|
||||
<TableHead className="text-center">Tokens In / Out</TableHead>
|
||||
<TableHead>Context</TableHead>
|
||||
{toolColumns.map((toolName) => (
|
||||
<TableHead key={toolName} className="text-xs text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{getToolAbbreviation(toolName)}</TooltipTrigger>
|
||||
<TooltipContent>{toolName}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableHead>
|
||||
))}
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead>Cost</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.map((task) => (
|
||||
<TableRow key={task.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<TaskStatus
|
||||
task={task}
|
||||
running={!!task.startedAt || !!tokenUsage.get(task.id)}
|
||||
/>
|
||||
<div>
|
||||
{task.language}/{task.exercise}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
{taskMetrics[task.id] ? (
|
||||
<>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<div className="flex items-center justify-evenly">
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensIn)}</div>/
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensOut)}</div>
|
||||
{tasks.map((task) => {
|
||||
const hasStarted = !!task.startedAt || !!tokenUsage.get(task.id)
|
||||
return (
|
||||
<TableRow
|
||||
key={task.id}
|
||||
className={`${hasStarted ? "cursor-pointer hover:bg-muted/50" : ""} ${task.passed === false ? "bg-red-950/30 border-l-2 border-l-red-500" : ""}`}
|
||||
onClick={() => hasStarted && onViewTaskLog(task)}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<TaskStatus task={task} running={hasStarted} />
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
{task.language}/{task.exercise}
|
||||
{task.iteration > 1 && (
|
||||
<span className="text-muted-foreground ml-1">
|
||||
(#{task.iteration})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{hasStarted && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FileText className="size-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Click to view log</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatTokens(taskMetrics[task.id]!.tokensContext)}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{taskMetrics[task.id]!.duration
|
||||
? formatDuration(taskMetrics[task.id]!.duration)
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatCurrency(taskMetrics[task.id]!.cost)}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<TableCell colSpan={4} />
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
{taskMetrics[task.id] ? (
|
||||
<>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<div className="flex items-center justify-evenly">
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensIn)}</div>/
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensOut)}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatTokens(taskMetrics[task.id]!.tokensContext)}
|
||||
</TableCell>
|
||||
{toolColumns.map((toolName) => {
|
||||
// Use DB values for finished tasks, but fall back to streaming values
|
||||
// if DB values are missing (handles race condition during timeout)
|
||||
const dbUsage = task.taskMetrics?.toolUsage?.[toolName]
|
||||
const streamingUsage = toolUsage.get(task.id)?.[toolName]
|
||||
const usage = task.finishedAt
|
||||
? (dbUsage ?? streamingUsage)
|
||||
: streamingUsage
|
||||
|
||||
const successRate =
|
||||
usage && usage.attempts > 0
|
||||
? ((usage.attempts - usage.failures) / usage.attempts) * 100
|
||||
: 100
|
||||
const rateColor =
|
||||
successRate === 100
|
||||
? "text-muted-foreground"
|
||||
: successRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
return (
|
||||
<TableCell key={toolName} className="text-xs text-center">
|
||||
{usage ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">
|
||||
{usage.attempts}
|
||||
</span>
|
||||
<span className={rateColor}>
|
||||
{formatToolUsageSuccessRate(usage)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell className="font-mono text-xs">
|
||||
{taskMetrics[task.id]!.duration
|
||||
? formatDuration(taskMetrics[task.id]!.duration)
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatCurrency(taskMetrics[task.id]!.cost)}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<TableCell colSpan={4 + toolColumns.length} />
|
||||
)}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task Log Dialog - Full Screen */}
|
||||
<Dialog open={!!selectedTask} onOpenChange={() => setSelectedTask(null)}>
|
||||
<DialogContent className="w-[95vw] !max-w-[95vw] h-[90vh] flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="size-4" />
|
||||
{selectedTask?.language}/{selectedTask?.exercise}
|
||||
{selectedTask?.iteration && selectedTask.iteration > 1 && (
|
||||
<span className="text-muted-foreground">(#{selectedTask.iteration})</span>
|
||||
)}
|
||||
<span
|
||||
className={`ml-2 text-sm ${
|
||||
selectedTask?.passed === true
|
||||
? "text-green-600"
|
||||
: selectedTask?.passed === false
|
||||
? "text-red-600"
|
||||
: "text-yellow-500"
|
||||
}`}>
|
||||
(
|
||||
{selectedTask?.passed === true
|
||||
? "Passed"
|
||||
: selectedTask?.passed === false
|
||||
? "Failed"
|
||||
: "Running"}
|
||||
)
|
||||
</span>
|
||||
</DialogTitle>
|
||||
{taskLog && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCopyLog}
|
||||
className="flex items-center gap-1">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="size-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="size-4" />
|
||||
Copy Log
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
{isLoadingLog ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<LoaderCircle className="size-6 animate-spin" />
|
||||
</div>
|
||||
) : taskLog ? (
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="text-xs font-mono bg-muted p-4 rounded-md overflow-x-auto">
|
||||
{formatLogContent(taskLog)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
Log file not available (may have been cleared)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Kill Run Confirmation Dialog */}
|
||||
<AlertDialog open={showKillDialog} onOpenChange={setShowKillDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Kill Run?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will stop the controller and all task runner containers for this run. Any running tasks
|
||||
will be terminated immediately. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isKilling}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onKillRun}
|
||||
disabled={isKilling}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
{isKilling ? (
|
||||
<>
|
||||
<LoaderCircle className="size-4 animate-spin mr-2" />
|
||||
Killing...
|
||||
</>
|
||||
) : (
|
||||
"Kill Run"
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,89 @@ import {
|
|||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
ScrollArea,
|
||||
} from "@/components/ui"
|
||||
|
||||
type RunProps = {
|
||||
run: EvalsRun
|
||||
taskMetrics: EvalsTaskMetrics | null
|
||||
toolColumns: ToolName[]
|
||||
consolidatedToolColumns: string[]
|
||||
}
|
||||
|
||||
export function Run({ run, taskMetrics }: RunProps) {
|
||||
export function Run({ run, taskMetrics, toolColumns, consolidatedToolColumns }: RunProps) {
|
||||
const router = useRouter()
|
||||
const [deleteRunId, setDeleteRunId] = useState<number>()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [isExportingLogs, setIsExportingLogs] = useState(false)
|
||||
const [showNotesDialog, setShowNotesDialog] = useState(false)
|
||||
const [editingDescription, setEditingDescription] = useState(run.description ?? "")
|
||||
const [isSavingNotes, setIsSavingNotes] = useState(false)
|
||||
const continueRef = useRef<HTMLButtonElement>(null)
|
||||
const { isPending, copyRun, copied } = useCopyRun(run.id)
|
||||
|
||||
const hasDescription = Boolean(run.description && run.description.trim().length > 0)
|
||||
|
||||
const handleSaveDescription = useCallback(async () => {
|
||||
setIsSavingNotes(true)
|
||||
try {
|
||||
const result = await updateRunDescription(run.id, editingDescription.trim() || null)
|
||||
if (result.success) {
|
||||
toast.success("Description saved")
|
||||
setShowNotesDialog(false)
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error("Failed to save description")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving description:", error)
|
||||
toast.error("Failed to save description")
|
||||
} finally {
|
||||
setIsSavingNotes(false)
|
||||
}
|
||||
}, [run.id, editingDescription, router])
|
||||
|
||||
const onExportFailedLogs = useCallback(async () => {
|
||||
if (run.failed === 0) {
|
||||
toast.error("No failed tasks to export")
|
||||
return
|
||||
}
|
||||
|
||||
setIsExportingLogs(true)
|
||||
try {
|
||||
const response = await fetch(`/api/runs/${run.id}/logs/failed`)
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
toast.error(error.error || "Failed to export logs")
|
||||
return
|
||||
}
|
||||
|
||||
// Download the zip file
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `run-${run.id}-failed-logs.zip`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
toast.success("Failed logs exported successfully")
|
||||
} catch (error) {
|
||||
console.error("Error exporting logs:", error)
|
||||
toast.error("Failed to export logs")
|
||||
} finally {
|
||||
setIsExportingLogs(false)
|
||||
}
|
||||
}, [run.id, run.failed])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
if (!deleteRunId) {
|
||||
return
|
||||
|
|
@ -48,86 +132,233 @@ export function Run({ run, taskMetrics }: RunProps) {
|
|||
}
|
||||
}, [deleteRunId])
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// Don't navigate if clicking on the dropdown menu
|
||||
if ((e.target as HTMLElement).closest("[data-dropdown-trigger]")) {
|
||||
return
|
||||
}
|
||||
router.push(`/runs/${run.id}`)
|
||||
},
|
||||
[router, run.id],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell>{run.model}</TableCell>
|
||||
<TableRow className="cursor-pointer hover:bg-muted/50" onClick={handleRowClick}>
|
||||
<TableCell className="max-w-[200px] truncate">{run.model}</TableCell>
|
||||
<TableCell>{run.settings?.apiProvider ?? "-"}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{formatDateTime(run.createdAt)}
|
||||
</TableCell>
|
||||
<TableCell>{run.passed}</TableCell>
|
||||
<TableCell>{run.failed}</TableCell>
|
||||
<TableCell>
|
||||
{run.passed + run.failed > 0 && (
|
||||
<span>{((run.passed / (run.passed + run.failed)) * 100).toFixed(1)}%</span>
|
||||
)}
|
||||
{run.passed + run.failed > 0 &&
|
||||
(() => {
|
||||
const percent = (run.passed / (run.passed + run.failed)) * 100
|
||||
const colorClass =
|
||||
percent === 100 ? "text-green-500" : percent >= 80 ? "text-yellow-500" : "text-red-500"
|
||||
return <span className={colorClass}>{percent.toFixed(1)}%</span>
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{taskMetrics && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div>{formatTokens(taskMetrics.tokensIn)}</div>/
|
||||
<div>{formatTokens(taskMetrics.tokensOut)}</div>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{taskMetrics?.toolUsage?.apply_diff && (
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<div>{taskMetrics.toolUsage.apply_diff.attempts}</div>
|
||||
<div>/</div>
|
||||
<div>{formatToolUsageSuccessRate(taskMetrics.toolUsage.apply_diff)}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{formatTokens(taskMetrics.tokensIn)}</span>/
|
||||
<span>{formatTokens(taskMetrics.tokensOut)}</span>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
{consolidatedToolColumns.length > 0 && (
|
||||
<TableCell className="text-xs text-center">
|
||||
{taskMetrics?.toolUsage ? (
|
||||
(() => {
|
||||
// Calculate aggregated stats for consolidated tools
|
||||
let totalAttempts = 0
|
||||
let totalFailures = 0
|
||||
const breakdown: Array<{ tool: string; attempts: number; rate: string }> = []
|
||||
|
||||
for (const toolName of consolidatedToolColumns) {
|
||||
const usage = taskMetrics.toolUsage[toolName as ToolName]
|
||||
if (usage) {
|
||||
totalAttempts += usage.attempts
|
||||
totalFailures += usage.failures
|
||||
const rate =
|
||||
usage.attempts > 0
|
||||
? `${Math.round(((usage.attempts - usage.failures) / usage.attempts) * 100)}%`
|
||||
: "0%"
|
||||
breakdown.push({ tool: toolName, attempts: usage.attempts, rate })
|
||||
}
|
||||
}
|
||||
|
||||
const consolidatedRate =
|
||||
totalAttempts > 0 ? ((totalAttempts - totalFailures) / totalAttempts) * 100 : 100
|
||||
const rateColor =
|
||||
consolidatedRate === 100
|
||||
? "text-muted-foreground"
|
||||
: consolidatedRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
|
||||
return totalAttempts > 0 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">{totalAttempts}</span>
|
||||
<span className={rateColor}>{Math.round(consolidatedRate)}%</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-xs">
|
||||
<div className="font-semibold mb-1">Consolidated Tools:</div>
|
||||
{breakdown.map(({ tool, attempts, rate }) => (
|
||||
<div key={tool} className="flex justify-between gap-4">
|
||||
<span>{tool}:</span>
|
||||
<span>
|
||||
{attempts} ({rate})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)
|
||||
})()
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
{toolColumns.map((toolName) => {
|
||||
const usage = taskMetrics?.toolUsage?.[toolName]
|
||||
const successRate =
|
||||
usage && usage.attempts > 0 ? ((usage.attempts - usage.failures) / usage.attempts) * 100 : 100
|
||||
const rateColor =
|
||||
successRate === 100
|
||||
? "text-muted-foreground"
|
||||
: successRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
return (
|
||||
<TableCell key={toolName} className="text-xs text-center">
|
||||
{usage ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">{usage.attempts}</span>
|
||||
<span className={rateColor}>{formatToolUsageSuccessRate(usage)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell>{taskMetrics && formatCurrency(taskMetrics.cost)}</TableCell>
|
||||
<TableCell>{taskMetrics && formatDuration(taskMetrics.duration)}</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<DropdownMenuTrigger>
|
||||
<Ellipsis />
|
||||
</DropdownMenuTrigger>
|
||||
</Button>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/runs/${run.id}`}>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Note Icon */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={hasDescription ? "" : "opacity-30 hover:opacity-60"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setEditingDescription(run.description ?? "")
|
||||
setShowNotesDialog(true)
|
||||
}}>
|
||||
<StickyNote className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]">
|
||||
{hasDescription ? (
|
||||
<div className="whitespace-pre-wrap">{run.description}</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">No description. Click to add one.</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* More Actions Menu */}
|
||||
<DropdownMenu>
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<DropdownMenuTrigger data-dropdown-trigger>
|
||||
<Ellipsis />
|
||||
</DropdownMenuTrigger>
|
||||
</Button>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/runs/${run.id}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
<ClipboardList />
|
||||
<div>View Tasks</div>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{run.settings && (
|
||||
<DropdownMenuItem onClick={() => setShowSettings(true)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Settings />
|
||||
<div>View Settings</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{run.taskMetricsId && (
|
||||
<DropdownMenuItem onClick={() => copyRun()} disabled={isPending || copied}>
|
||||
<div className="flex items-center gap-1">
|
||||
{isPending ? (
|
||||
<>
|
||||
<LoaderCircle className="animate-spin" />
|
||||
Copying...
|
||||
</>
|
||||
) : copied ? (
|
||||
<>
|
||||
<Check />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy />
|
||||
Copy to Production
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{run.failed > 0 && (
|
||||
<DropdownMenuItem onClick={onExportFailedLogs} disabled={isExportingLogs}>
|
||||
<div className="flex items-center gap-1">
|
||||
{isExportingLogs ? (
|
||||
<>
|
||||
<LoaderCircle className="animate-spin" />
|
||||
Exporting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileDown />
|
||||
Export Failed Logs
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDeleteRunId(run.id)
|
||||
setTimeout(() => continueRef.current?.focus(), 0)
|
||||
}}>
|
||||
<div className="flex items-center gap-1">
|
||||
<ClipboardList />
|
||||
<div>View Tasks</div>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{run.taskMetricsId && (
|
||||
<DropdownMenuItem onClick={() => copyRun()} disabled={isPending || copied}>
|
||||
<div className="flex items-center gap-1">
|
||||
{isPending ? (
|
||||
<>
|
||||
<LoaderCircle className="animate-spin" />
|
||||
Copying...
|
||||
</>
|
||||
) : copied ? (
|
||||
<>
|
||||
<Check />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy />
|
||||
Copy to Production
|
||||
</>
|
||||
)}
|
||||
<Trash />
|
||||
<div>Delete</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDeleteRunId(run.id)
|
||||
setTimeout(() => continueRef.current?.focus(), 0)
|
||||
}}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Trash />
|
||||
<div>Delete</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<AlertDialog open={!!deleteRunId} onOpenChange={() => setDeleteRunId(undefined)}>
|
||||
|
|
@ -144,6 +375,51 @@ export function Run({ run, taskMetrics }: RunProps) {
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Run Settings</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[60vh]">
|
||||
<pre className="text-xs font-mono bg-muted p-4 rounded-md overflow-auto">
|
||||
{JSON.stringify(run.settings, null, 2)}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Notes/Description Dialog */}
|
||||
<Dialog open={showNotesDialog} onOpenChange={setShowNotesDialog}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Run Description</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<Textarea
|
||||
placeholder="Add a description or notes for this run..."
|
||||
value={editingDescription}
|
||||
onChange={(e) => setEditingDescription(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowNotesDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveDescription} disabled={isSavingNotes}>
|
||||
{isSavingNotes ? (
|
||||
<>
|
||||
<LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
"Save"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,656 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Rocket } from "lucide-react"
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ArrowUpDown,
|
||||
Combine,
|
||||
Ellipsis,
|
||||
LoaderCircle,
|
||||
Rocket,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import type { Run, TaskMetrics } from "@roo-code/evals"
|
||||
import type { ToolName } from "@roo-code/types"
|
||||
|
||||
import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
|
||||
import { deleteIncompleteRuns, deleteOldRuns } from "@/actions/runs"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
MultiSelect,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui"
|
||||
import { Run as Row } from "@/components/home/run"
|
||||
|
||||
type RunWithTaskMetrics = Run & { taskMetrics: TaskMetrics | null }
|
||||
|
||||
type SortColumn = "model" | "provider" | "passed" | "failed" | "percent" | "cost" | "duration" | "createdAt"
|
||||
type SortDirection = "asc" | "desc"
|
||||
|
||||
type TimeframeOption = "all" | "24h" | "7d" | "30d" | "90d"
|
||||
|
||||
const TIMEFRAME_OPTIONS: { value: TimeframeOption; label: string }[] = [
|
||||
{ value: "all", label: "All time" },
|
||||
{ value: "24h", label: "Last 24 hours" },
|
||||
{ value: "7d", label: "Last 7 days" },
|
||||
{ value: "30d", label: "Last 30 days" },
|
||||
{ value: "90d", label: "Last 90 days" },
|
||||
]
|
||||
|
||||
// LocalStorage keys
|
||||
const STORAGE_KEYS = {
|
||||
TIMEFRAME: "evals-runs-timeframe",
|
||||
MODEL_FILTER: "evals-runs-model-filter",
|
||||
PROVIDER_FILTER: "evals-runs-provider-filter",
|
||||
CONSOLIDATED_TOOLS: "evals-runs-consolidated-tools",
|
||||
}
|
||||
|
||||
function getTimeframeStartDate(timeframe: TimeframeOption): Date | null {
|
||||
if (timeframe === "all") return null
|
||||
const now = new Date()
|
||||
switch (timeframe) {
|
||||
case "24h":
|
||||
return new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
case "7d":
|
||||
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
case "30d":
|
||||
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||||
case "90d":
|
||||
return new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Generate abbreviation from tool name (e.g., "read_file" -> "RF", "list_code_definition_names" -> "LCDN")
|
||||
function getToolAbbreviation(toolName: string): string {
|
||||
return toolName
|
||||
.split("_")
|
||||
.map((word) => word[0]?.toUpperCase() ?? "")
|
||||
.join("")
|
||||
}
|
||||
|
||||
function SortIcon({
|
||||
column,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
}: {
|
||||
column: SortColumn
|
||||
sortColumn: SortColumn | null
|
||||
sortDirection: SortDirection
|
||||
}) {
|
||||
if (sortColumn !== column) {
|
||||
return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
}
|
||||
return sortDirection === "asc" ? <ArrowUp className="ml-1 h-3 w-3" /> : <ArrowDown className="ml-1 h-3 w-3" />
|
||||
}
|
||||
|
||||
export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
||||
const router = useRouter()
|
||||
const [sortColumn, setSortColumn] = useState<SortColumn | null>("createdAt")
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>("desc")
|
||||
|
||||
// Filter state - initialize from localStorage
|
||||
const [timeframeFilter, setTimeframeFilter] = useState<TimeframeOption>(() => {
|
||||
if (typeof window === "undefined") return "all"
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.TIMEFRAME)
|
||||
return (stored as TimeframeOption) || "all"
|
||||
})
|
||||
const [modelFilter, setModelFilter] = useState<string[]>(() => {
|
||||
if (typeof window === "undefined") return []
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.MODEL_FILTER)
|
||||
return stored ? JSON.parse(stored) : []
|
||||
})
|
||||
const [providerFilter, setProviderFilter] = useState<string[]>(() => {
|
||||
if (typeof window === "undefined") return []
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.PROVIDER_FILTER)
|
||||
return stored ? JSON.parse(stored) : []
|
||||
})
|
||||
|
||||
// Tool column consolidation state - initialize from localStorage
|
||||
const [consolidatedToolColumns, setConsolidatedToolColumns] = useState<string[]>(() => {
|
||||
if (typeof window === "undefined") return []
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.CONSOLIDATED_TOOLS)
|
||||
return stored ? JSON.parse(stored) : []
|
||||
})
|
||||
|
||||
// Delete runs state
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [showDeleteOldConfirm, setShowDeleteOldConfirm] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
// Persist filters to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEYS.TIMEFRAME, timeframeFilter)
|
||||
}, [timeframeFilter])
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEYS.MODEL_FILTER, JSON.stringify(modelFilter))
|
||||
}, [modelFilter])
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEYS.PROVIDER_FILTER, JSON.stringify(providerFilter))
|
||||
}, [providerFilter])
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEYS.CONSOLIDATED_TOOLS, JSON.stringify(consolidatedToolColumns))
|
||||
}, [consolidatedToolColumns])
|
||||
|
||||
// Count incomplete runs (runs without taskMetricsId)
|
||||
const incompleteRunsCount = useMemo(() => {
|
||||
return runs.filter((run) => run.taskMetrics === null).length
|
||||
}, [runs])
|
||||
|
||||
// Count runs older than 30 days
|
||||
const oldRunsCount = useMemo(() => {
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
return runs.filter((run) => run.createdAt < thirtyDaysAgo).length
|
||||
}, [runs])
|
||||
|
||||
const handleDeleteIncompleteRuns = useCallback(async () => {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
const result = await deleteIncompleteRuns()
|
||||
if (result.success) {
|
||||
toast.success(`Deleted ${result.deletedCount} incomplete run${result.deletedCount !== 1 ? "s" : ""}`)
|
||||
if (result.storageErrors.length > 0) {
|
||||
toast.warning(`Some storage folders could not be deleted: ${result.storageErrors.length} errors`)
|
||||
}
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error("Failed to delete incomplete runs")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting incomplete runs:", error)
|
||||
toast.error("Failed to delete incomplete runs")
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
setShowDeleteConfirm(false)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
const handleDeleteOldRuns = useCallback(async () => {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
const result = await deleteOldRuns()
|
||||
if (result.success) {
|
||||
toast.success(
|
||||
`Deleted ${result.deletedCount} run${result.deletedCount !== 1 ? "s" : ""} older than 30 days`,
|
||||
)
|
||||
if (result.storageErrors.length > 0) {
|
||||
toast.warning(`Some storage folders could not be deleted: ${result.storageErrors.length} errors`)
|
||||
}
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error("Failed to delete old runs")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting old runs:", error)
|
||||
toast.error("Failed to delete old runs")
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
setShowDeleteOldConfirm(false)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
const handleSort = (column: SortColumn) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === "asc" ? "desc" : "asc")
|
||||
} else {
|
||||
setSortColumn(column)
|
||||
setSortDirection("desc")
|
||||
}
|
||||
}
|
||||
|
||||
// Derive unique models and providers from runs
|
||||
const modelOptions = useMemo(() => {
|
||||
const models = new Set<string>()
|
||||
for (const run of runs) {
|
||||
if (run.model) models.add(run.model)
|
||||
}
|
||||
return Array.from(models)
|
||||
.sort()
|
||||
.map((model) => ({ label: model, value: model }))
|
||||
}, [runs])
|
||||
|
||||
const providerOptions = useMemo(() => {
|
||||
const providers = new Set<string>()
|
||||
for (const run of runs) {
|
||||
const provider = run.settings?.apiProvider
|
||||
if (provider) providers.add(provider)
|
||||
}
|
||||
return Array.from(providers)
|
||||
.sort()
|
||||
.map((provider) => ({ label: provider, value: provider }))
|
||||
}, [runs])
|
||||
|
||||
// Filter runs based on filter state
|
||||
const filteredRuns = useMemo(() => {
|
||||
return runs.filter((run) => {
|
||||
// Timeframe filter
|
||||
const timeframeStart = getTimeframeStartDate(timeframeFilter)
|
||||
if (timeframeStart && run.createdAt < timeframeStart) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Model filter
|
||||
if (modelFilter.length > 0 && !modelFilter.includes(run.model)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Provider filter
|
||||
if (providerFilter.length > 0) {
|
||||
const provider = run.settings?.apiProvider
|
||||
if (!provider || !providerFilter.includes(provider)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}, [runs, timeframeFilter, modelFilter, providerFilter])
|
||||
|
||||
// Collect all unique tool names from filtered runs and sort by total attempts
|
||||
const allToolColumns = useMemo<ToolName[]>(() => {
|
||||
const toolTotals = new Map<ToolName, number>()
|
||||
|
||||
for (const run of filteredRuns) {
|
||||
if (run.taskMetrics?.toolUsage) {
|
||||
for (const [toolName, usage] of Object.entries(run.taskMetrics.toolUsage)) {
|
||||
const tool = toolName as ToolName
|
||||
const current = toolTotals.get(tool) ?? 0
|
||||
toolTotals.set(tool, current + usage.attempts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by total attempts descending
|
||||
return Array.from(toolTotals.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([name]): ToolName => name)
|
||||
}, [filteredRuns])
|
||||
|
||||
// Tool column options for the consolidation dropdown
|
||||
const toolColumnOptions = useMemo(() => {
|
||||
return allToolColumns.map((tool) => ({
|
||||
label: tool,
|
||||
value: tool,
|
||||
}))
|
||||
}, [allToolColumns])
|
||||
|
||||
// Separate consolidated and individual tool columns
|
||||
const individualToolColumns = useMemo(() => {
|
||||
return allToolColumns.filter((tool) => !consolidatedToolColumns.includes(tool))
|
||||
}, [allToolColumns, consolidatedToolColumns])
|
||||
|
||||
// Create a "consolidated" column if any tools are selected for consolidation
|
||||
const hasConsolidatedColumn = consolidatedToolColumns.length > 0
|
||||
|
||||
// Use individualToolColumns for rendering
|
||||
const toolColumns = individualToolColumns
|
||||
|
||||
// Sort filtered runs based on current sort column and direction
|
||||
const sortedRuns = useMemo(() => {
|
||||
if (!sortColumn) return filteredRuns
|
||||
|
||||
return [...filteredRuns].sort((a, b) => {
|
||||
let aVal: string | number | Date | null = null
|
||||
let bVal: string | number | Date | null = null
|
||||
|
||||
switch (sortColumn) {
|
||||
case "model":
|
||||
aVal = a.model
|
||||
bVal = b.model
|
||||
break
|
||||
case "provider":
|
||||
aVal = a.settings?.apiProvider ?? ""
|
||||
bVal = b.settings?.apiProvider ?? ""
|
||||
break
|
||||
case "passed":
|
||||
aVal = a.passed
|
||||
bVal = b.passed
|
||||
break
|
||||
case "failed":
|
||||
aVal = a.failed
|
||||
bVal = b.failed
|
||||
break
|
||||
case "percent":
|
||||
aVal = a.passed + a.failed > 0 ? a.passed / (a.passed + a.failed) : 0
|
||||
bVal = b.passed + b.failed > 0 ? b.passed / (b.passed + b.failed) : 0
|
||||
break
|
||||
case "cost":
|
||||
aVal = a.taskMetrics?.cost ?? 0
|
||||
bVal = b.taskMetrics?.cost ?? 0
|
||||
break
|
||||
case "duration":
|
||||
aVal = a.taskMetrics?.duration ?? 0
|
||||
bVal = b.taskMetrics?.duration ?? 0
|
||||
break
|
||||
case "createdAt":
|
||||
aVal = a.createdAt
|
||||
bVal = b.createdAt
|
||||
break
|
||||
}
|
||||
|
||||
if (aVal === null || bVal === null) return 0
|
||||
|
||||
let comparison = 0
|
||||
if (typeof aVal === "string" && typeof bVal === "string") {
|
||||
comparison = aVal.localeCompare(bVal)
|
||||
} else if (aVal instanceof Date && bVal instanceof Date) {
|
||||
comparison = aVal.getTime() - bVal.getTime()
|
||||
} else {
|
||||
comparison = (aVal as number) - (bVal as number)
|
||||
}
|
||||
|
||||
return sortDirection === "asc" ? comparison : -comparison
|
||||
})
|
||||
}, [filteredRuns, sortColumn, sortDirection])
|
||||
|
||||
// Calculate colSpan for empty state (7 base columns + dynamic tools + consolidated column + 3 end columns)
|
||||
const totalColumns = 7 + toolColumns.length + (hasConsolidatedColumn ? 1 : 0) + 3
|
||||
|
||||
// Check if any filters or settings are active
|
||||
const hasActiveFilters = timeframeFilter !== "all" || modelFilter.length > 0 || providerFilter.length > 0
|
||||
const hasConsolidatedTools = consolidatedToolColumns.length > 0
|
||||
const hasAnyCustomization = hasActiveFilters || hasConsolidatedTools
|
||||
|
||||
const clearAllFilters = () => {
|
||||
setTimeframeFilter("all")
|
||||
setModelFilter([])
|
||||
setProviderFilter([])
|
||||
}
|
||||
|
||||
const resetAll = () => {
|
||||
setTimeframeFilter("all")
|
||||
setModelFilter([])
|
||||
setProviderFilter([])
|
||||
setConsolidatedToolColumns([])
|
||||
localStorage.removeItem(STORAGE_KEYS.TIMEFRAME)
|
||||
localStorage.removeItem(STORAGE_KEYS.MODEL_FILTER)
|
||||
localStorage.removeItem(STORAGE_KEYS.PROVIDER_FILTER)
|
||||
localStorage.removeItem(STORAGE_KEYS.CONSOLIDATED_TOOLS)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table className="border border-t-0">
|
||||
{/* Filter Controls */}
|
||||
<div className="flex items-center gap-4 p-4 border border-b-0 rounded-t-md bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">Timeframe:</span>
|
||||
<Select
|
||||
value={timeframeFilter}
|
||||
onValueChange={(value) => setTimeframeFilter(value as TimeframeOption)}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIMEFRAME_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">Model:</span>
|
||||
<MultiSelect
|
||||
options={modelOptions}
|
||||
value={modelFilter}
|
||||
onValueChange={setModelFilter}
|
||||
placeholder="All models"
|
||||
className="w-[200px]"
|
||||
maxCount={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">Provider:</span>
|
||||
<MultiSelect
|
||||
options={providerOptions}
|
||||
value={providerFilter}
|
||||
onValueChange={setProviderFilter}
|
||||
placeholder="All providers"
|
||||
className="w-[180px]"
|
||||
maxCount={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2">
|
||||
<Combine className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-muted-foreground">Consolidate:</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select tool columns to consolidate into a combined column</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="relative min-w-[100px] w-fit max-w-[140px]">
|
||||
<div className={consolidatedToolColumns.length > 0 ? "[&>div>div]:invisible" : ""}>
|
||||
<MultiSelect
|
||||
options={toolColumnOptions}
|
||||
value={consolidatedToolColumns}
|
||||
onValueChange={setConsolidatedToolColumns}
|
||||
placeholder="None"
|
||||
className="w-full min-w-[100px]"
|
||||
maxCount={0}
|
||||
popoverAutoWidth
|
||||
footer={
|
||||
hasAnyCustomization && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start text-muted-foreground hover:text-foreground"
|
||||
onClick={resetAll}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Reset all filters & consolidation
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{consolidatedToolColumns.length > 0 && (
|
||||
<div className="absolute inset-0 flex items-center px-3 pointer-events-none">
|
||||
<span className="text-sm font-medium whitespace-nowrap">
|
||||
{consolidatedToolColumns.length} tool
|
||||
{consolidatedToolColumns.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<Button variant="ghost" size="sm" onClick={clearAllFilters}>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Clear filters
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
{/* Bulk Actions Menu */}
|
||||
{(incompleteRunsCount > 0 || oldRunsCount > 0) && (
|
||||
<DropdownMenu>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<DropdownMenuTrigger disabled={isDeleting}>
|
||||
<Ellipsis className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
</Button>
|
||||
<DropdownMenuContent align="end">
|
||||
{incompleteRunsCount > 0 && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={isDeleting}
|
||||
className="text-destructive focus:text-destructive">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete {incompleteRunsCount} incomplete run
|
||||
{incompleteRunsCount !== 1 ? "s" : ""}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{oldRunsCount > 0 && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowDeleteOldConfirm(true)}
|
||||
disabled={isDeleting}
|
||||
className="text-destructive focus:text-destructive">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""} over 30d
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{filteredRuns.length} of {runs.length} runs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table className="border border-t-0 rounded-t-none">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Model</TableHead>
|
||||
<TableHead>Passed</TableHead>
|
||||
<TableHead>Failed</TableHead>
|
||||
<TableHead>% Correct</TableHead>
|
||||
<TableHead>Tokens In / Out</TableHead>
|
||||
<TableHead>Diff Edits</TableHead>
|
||||
<TableHead>Cost</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead />
|
||||
<TableHead
|
||||
className="max-w-[200px] cursor-pointer select-none"
|
||||
onClick={() => handleSort("model")}>
|
||||
<div className="flex items-center">
|
||||
Model
|
||||
<SortIcon column="model" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("provider")}>
|
||||
<div className="flex items-center">
|
||||
Provider
|
||||
<SortIcon column="provider" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("createdAt")}>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
<SortIcon column="createdAt" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("passed")}>
|
||||
<div className="flex items-center">
|
||||
Passed
|
||||
<SortIcon column="passed" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("failed")}>
|
||||
<div className="flex items-center">
|
||||
Failed
|
||||
<SortIcon column="failed" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("percent")}>
|
||||
<div className="flex items-center">
|
||||
%
|
||||
<SortIcon column="percent" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>Tokens</TableHead>
|
||||
{hasConsolidatedColumn && (
|
||||
<TableHead className="text-xs text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Combine className="h-3 w-3 inline" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-xs">
|
||||
<div className="font-semibold mb-1">Consolidated Tools:</div>
|
||||
{consolidatedToolColumns.map((tool) => (
|
||||
<div key={tool}>{tool}</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableHead>
|
||||
)}
|
||||
{toolColumns.map((toolName) => (
|
||||
<TableHead key={toolName} className="text-xs text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{getToolAbbreviation(toolName)}</TooltipTrigger>
|
||||
<TooltipContent>{toolName}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableHead>
|
||||
))}
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("cost")}>
|
||||
<div className="flex items-center">
|
||||
Cost
|
||||
<SortIcon column="cost" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("duration")}>
|
||||
<div className="flex items-center">
|
||||
Duration
|
||||
<SortIcon column="duration" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{runs.length ? (
|
||||
runs.map(({ taskMetrics, ...run }) => <Row key={run.id} run={run} taskMetrics={taskMetrics} />)
|
||||
{sortedRuns.length ? (
|
||||
sortedRuns.map(({ taskMetrics, ...run }) => (
|
||||
<Row
|
||||
key={run.id}
|
||||
run={run}
|
||||
taskMetrics={taskMetrics}
|
||||
toolColumns={toolColumns}
|
||||
consolidatedToolColumns={consolidatedToolColumns}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center">
|
||||
No eval runs yet.
|
||||
<Button variant="link" onClick={() => router.push("/runs/new")}>
|
||||
Launch
|
||||
</Button>
|
||||
one now.
|
||||
<TableCell colSpan={totalColumns} className="text-center py-8">
|
||||
{runs.length === 0 ? (
|
||||
<>
|
||||
No eval runs yet.
|
||||
<Button variant="link" onClick={() => router.push("/runs/new")}>
|
||||
Launch
|
||||
</Button>
|
||||
one now.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
No runs match the current filters.
|
||||
<Button variant="link" onClick={clearAllFilters}>
|
||||
Clear filters
|
||||
</Button>
|
||||
to see all runs.
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
|
@ -51,6 +662,70 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
onClick={() => router.push("/runs/new")}>
|
||||
<Rocket className="size-6" />
|
||||
</Button>
|
||||
|
||||
{/* Delete Incomplete Runs Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Incomplete Runs</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete {incompleteRunsCount} incomplete run
|
||||
{incompleteRunsCount !== 1 ? "s" : ""}? This will permanently remove all database records
|
||||
and storage folders for these runs. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteIncompleteRuns}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Delete {incompleteRunsCount} run{incompleteRunsCount !== 1 ? "s" : ""}
|
||||
</>
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Delete Old Runs Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteOldConfirm} onOpenChange={setShowDeleteOldConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Old Runs</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""} older than
|
||||
30 days? This will permanently remove all database records and storage folders for these
|
||||
runs. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteOldRuns}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""}
|
||||
</>
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,4 +45,14 @@ export const formatTokens = (tokens: number) => {
|
|||
}
|
||||
|
||||
export const formatToolUsageSuccessRate = (usage: { attempts: number; failures: number }) =>
|
||||
usage.attempts === 0 ? "0%" : `${(((usage.attempts - usage.failures) / usage.attempts) * 100).toFixed(1)}%`
|
||||
usage.attempts === 0 ? "0%" : `${Math.round(((usage.attempts - usage.failures) / usage.attempts) * 100)}%`
|
||||
|
||||
export const formatDateTime = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
}).format(date)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import { rooCodeSettingsSchema } from "@roo-code/types"
|
|||
* CreateRun
|
||||
*/
|
||||
|
||||
export const MODEL_DEFAULT = "anthropic/claude-sonnet-4"
|
||||
|
||||
export const CONCURRENCY_MIN = 1
|
||||
export const CONCURRENCY_MAX = 25
|
||||
export const CONCURRENCY_DEFAULT = 1
|
||||
|
|
@ -16,6 +14,10 @@ export const TIMEOUT_MIN = 5
|
|||
export const TIMEOUT_MAX = 10
|
||||
export const TIMEOUT_DEFAULT = 5
|
||||
|
||||
export const ITERATIONS_MIN = 1
|
||||
export const ITERATIONS_MAX = 10
|
||||
export const ITERATIONS_DEFAULT = 1
|
||||
|
||||
export const createRunSchema = z
|
||||
.object({
|
||||
model: z.string().min(1, { message: "Model is required." }),
|
||||
|
|
@ -25,7 +27,8 @@ export const createRunSchema = z
|
|||
settings: rooCodeSettingsSchema.optional(),
|
||||
concurrency: z.number().int().min(CONCURRENCY_MIN).max(CONCURRENCY_MAX),
|
||||
timeout: z.number().int().min(TIMEOUT_MIN).max(TIMEOUT_MAX),
|
||||
systemPrompt: z.string().optional(),
|
||||
iterations: z.number().int().min(ITERATIONS_MIN).max(ITERATIONS_MAX),
|
||||
jobToken: z.string().optional(),
|
||||
})
|
||||
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
|
||||
message: "Exercises are required when running a partial suite.",
|
||||
|
|
|
|||
|
|
@ -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.6",
|
||||
"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>
|
||||
|
|
|
|||
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,72 +60,67 @@ interface PricingTier {
|
|||
name: string
|
||||
icon: LucideIcon
|
||||
price: string
|
||||
priceSuffix: string
|
||||
period?: string
|
||||
creditPrice?: string
|
||||
trial?: string
|
||||
cancellation?: string
|
||||
description: string
|
||||
featuresIntro?: string
|
||||
features: string[]
|
||||
cta: {
|
||||
text: string
|
||||
href?: string
|
||||
isContactForm?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const pricingTiers: PricingTier[] = [
|
||||
{
|
||||
name: "VS Code Extension",
|
||||
icon: SquareTerminal,
|
||||
price: "Free",
|
||||
priceSuffix: "inference",
|
||||
description: "The best local coding agent",
|
||||
features: ["Unlimited local use", "Bring your own model", "Powerful, extensible modes", "Community support"],
|
||||
cta: {
|
||||
text: "Install Now",
|
||||
href: EXTERNAL_LINKS.MARKETPLACE,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cloud Free",
|
||||
icon: Cloud,
|
||||
price: "$0",
|
||||
cancellation: "Cancel anytime",
|
||||
description: "For folks just getting started",
|
||||
period: "/mo",
|
||||
priceSuffix: "credits",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
description: "For AI-forward engineers",
|
||||
featuresIntro: "Go beyond the extension with",
|
||||
features: [
|
||||
"Token usage analytics",
|
||||
"Access to Cloud Agents: fully autonomous development you can call from Slack, Github and the web",
|
||||
"Access to the Roo Code Cloud Provider",
|
||||
"Follow your tasks from anywhere",
|
||||
"Share tasks with friends and co-workers",
|
||||
"Early access to free AI Models",
|
||||
"Community support",
|
||||
"Token usage analytics",
|
||||
"Professional support",
|
||||
],
|
||||
cta: {
|
||||
text: "Get started",
|
||||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
icon: Star,
|
||||
price: "$20",
|
||||
period: "/mo",
|
||||
trial: "Free 14-day trial · ",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
cancellation: "Cancel anytime",
|
||||
description: "For pro Roo coders",
|
||||
featuresIntro: "Everything in Free +",
|
||||
features: [
|
||||
"Cloud Agents: PR Reviewer and more",
|
||||
"Roomote Control: Start, stop and control tasks from anywhere",
|
||||
"Paid support",
|
||||
],
|
||||
cta: {
|
||||
text: "Get started",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Team",
|
||||
name: "Cloud Team",
|
||||
icon: Users,
|
||||
price: "$99",
|
||||
priceSuffix: "credits",
|
||||
period: "/mo",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
trial: "Free 14-day trial · ",
|
||||
cancellation: "Cancel anytime",
|
||||
trial: "Free for 14 days, then",
|
||||
description: "For AI-forward teams",
|
||||
featuresIntro: "Everything in Pro +",
|
||||
featuresIntro: "Everything in Free +",
|
||||
features: ["Unlimited users (no per-seat cost)", "Shared configuration & policies", "Centralized billing"],
|
||||
cta: {
|
||||
text: "Get started",
|
||||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
|
||||
},
|
||||
},
|
||||
|
|
@ -138,52 +132,43 @@ export default function PricingPage() {
|
|||
<AnimatedBackground />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden pt-16 pb-12">
|
||||
<section className="relative overflow-hidden pt-12 pb-10">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-5xl font-bold tracking-tight">Roo Code Cloud Pricing</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
|
||||
Simple, transparent pricing that scales with your needs.
|
||||
<br />
|
||||
No inference markups. Free 14-day trials to kick the tires.
|
||||
<h1 className="text-5xl font-bold tracking-tight">Roo Code Pricing</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
For all of our products: the Roo Code VS Code Extension, Roo Code Cloud and the Roo Code
|
||||
Cloud inference Provider.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Free Extension Notice */}
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="rounded-xl p-4 mb-8 text-center bg-gradient-to-r from-blue-500/10 via-cyan-500/10 to-purple-500/10 border border-blue-500/20 dark:border-white/20">
|
||||
<p className="text-center">
|
||||
<strong className="font-semibold">The Roo Code extension is free! </strong>
|
||||
Roo Code Cloud is an optional service which takes it to the next level.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Tiers */}
|
||||
<section className="">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto grid max-w-6xl gap-4 lg:grid-cols-3">
|
||||
<div className="mx-auto grid max-w-6xl gap-4 md:grid-cols-3 md:px-4">
|
||||
{pricingTiers.map((tier) => {
|
||||
const Icon = tier.icon
|
||||
return (
|
||||
<div
|
||||
key={tier.name}
|
||||
className="relative p-6 flex flex-col justify-start bg-background border rounded-2xl transition-all hover:shadow-lg">
|
||||
className="relative group p-6 flex flex-col justify-start bg-background rounded-2xl outline outline-2 outline-border/50 hover:outline-8 transition-all shadow-xl hover:shadow-2xl hover:outline-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-2xl font-bold tracking-tight">{tier.name}</h3>
|
||||
<Icon className="size-6" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{tier.description}</p>
|
||||
<p className="text-sm font-medium">{tier.description}</p>
|
||||
</div>
|
||||
<div className="absolute -right-2 -top-4 rounded-full bg-card shadow-md p-4 outline outline-2 outline-border/50 group-hover:scale-105 group-hover:outline-8 transition-all">
|
||||
<Icon className="size-6" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="grow mb-8">
|
||||
<p className="text-sm text-muted-foreground font-light mb-2">
|
||||
{tier.featuresIntro}
|
||||
</p>
|
||||
<ul className="space-y-3 my-0 h-[148px]">
|
||||
<ul className="space-y-3 my-0 md:h-[192px]">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-4 w-4 text-muted-foreground shrink-0" />
|
||||
|
|
@ -193,52 +178,67 @@ export default function PricingPage() {
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="text-2xl mt-0 mb-1 tracking-tight">
|
||||
<strong>{tier.price}</strong>
|
||||
{tier.period}
|
||||
<p className="text-base font-light">{tier.trial}</p>
|
||||
|
||||
<p className="text-xl mb-1 tracking-tight font-light">
|
||||
<strong className="font-bold">{tier.price}</strong>
|
||||
{tier.period} + {tier.priceSuffix}
|
||||
<CornerRightDown className="inline size-4 ml-1 relative top-0.5" />
|
||||
</p>
|
||||
|
||||
{tier.creditPrice && (
|
||||
<p className="text-sm text-muted-foreground mb-1">
|
||||
+ {tier.creditPrice}/hour for Cloud tasks
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
{tier.trial}
|
||||
{tier.cancellation}
|
||||
<p className="text-sm text-muted-foreground mb-5">
|
||||
{tier.creditPrice && (
|
||||
<>
|
||||
Cloud Agents: {tier.creditPrice}/hour in credits
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
Inference:{" "}
|
||||
<Link href="/provider" className="underline hover:no-underline">
|
||||
Roo Provider pricing
|
||||
</Link>{" "}
|
||||
credits or{" "}
|
||||
<abbr title="Bring Your Own Model" className="cursor-help">
|
||||
BYOM
|
||||
</abbr>
|
||||
</p>
|
||||
|
||||
{tier.cta.isContactForm ? (
|
||||
<ContactForm
|
||||
formType="demo"
|
||||
buttonText={tier.cta.text}
|
||||
buttonClassName="w-full transition-all duration-300"
|
||||
/>
|
||||
) : (
|
||||
<Button size="lg" className="w-full transition-all duration-300" asChild>
|
||||
<Link href={tier.cta.href!} className="flex items-center justify-center">
|
||||
{tier.cta.text}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button size="lg" className="w-full transition-all duration-300" asChild>
|
||||
<Link href={tier.cta.href!} className="flex items-center justify-center">
|
||||
{tier.cta.text}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* <div className="bg-foreground/20 h-8 absolute -bottom-8 left-1/2 w-[1px]" /> */}
|
||||
<div className="h-[28px] absolute bottom-[-31px] left-1/2 w-[4px] transition-colors bg-gradient-to-b from-transparent to-violet-700/20 group-hover:from-violet-500/50 group-hover:to-violet-500/20" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto grid max-w-6xl gap-4 mt-4 relative">
|
||||
<p className="bg-background border rounded-2xl p-6 text-center text-sm text-muted-foreground">
|
||||
<Building2 className="inline size-4 mr-2 mb-0.5" />
|
||||
Need SAML, advanced security, custom integrations or terms? Enterprise is for you.
|
||||
<Link
|
||||
href="/enterprise#contact"
|
||||
className="font-medium ml-1 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Talk to Sales
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<div className="max-w-6xl mx-auto mt-8 p-7 flex flex-col md:flex-row gap-8 md:gap-4 bg-violet-200/20 outline-violet-700/20 outline outline-1 rounded-2xl transition-all shadow-none">
|
||||
<div className="md:border-r md:pr-4">
|
||||
<h3 className="text-lg font-medium mb-1">Roo Code Provider</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="">
|
||||
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
|
||||
Cloud provider – curated models to work with Roo with no markup, including the
|
||||
latest Gemini, GPT and Claude. Paid with credits.
|
||||
<Link href="/provider/pricing" className="underline hover:no-underline ml-1">
|
||||
See per model pricing.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<h3 className="text-lg font-medium mb-1">Credits</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Credits are pre-paid, in dollars, and are deducted with usage for inference and Cloud
|
||||
Agent runs. You're always in control of your spend, no surprises.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -249,7 +249,7 @@ export default function PricingPage() {
|
|||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Wait, is Roo Code free or not?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes! The Roo Code VS Code extension is open source and free forever. The extension acts
|
||||
|
|
@ -257,7 +257,7 @@ export default function PricingPage() {
|
|||
Code Cloud.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Is there a free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, all paid plans come with a 14-day free trial to try out functionality.
|
||||
|
|
@ -266,12 +266,25 @@ export default function PricingPage() {
|
|||
To use Cloud Agents, you can buy credits.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">How do Cloud Agent credits work?</h3>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">How do credits work?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Cloud Agents are a version of Roo running in the cloud without depending on your IDE.
|
||||
You can run as many as you want, and bring your own inference provider key.
|
||||
Roo Code Cloud credits can be used in two ways:
|
||||
</p>
|
||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
||||
<li>To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)</li>
|
||||
<li>
|
||||
To pay for AI model inference costs (
|
||||
<a
|
||||
href="https://app.roocode.com/provider/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline">
|
||||
varies by model
|
||||
</a>
|
||||
)
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
To cover our infrastructure costs, we charge ${PRICE_CREDITS}/hour while the agent is
|
||||
running (independent of inference costs).
|
||||
|
|
@ -280,25 +293,38 @@ export default function PricingPage() {
|
|||
There are no markups, no tiers, no dumbing-down of models to increase our profit.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Do I need a credit card for the free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, but you won'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>
|
||||
|
|
|
|||
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,117 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="grow ml-6 hidden text-sm font-medium md:flex md:items-center">
|
||||
<ScrollButton
|
||||
targetId="product"
|
||||
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground max-lg:hidden">
|
||||
Extension
|
||||
</ScrollButton>
|
||||
<Link
|
||||
href="/cloud"
|
||||
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
Cloud
|
||||
</Link>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DOCUMENTATION}
|
||||
target="_blank"
|
||||
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
Docs
|
||||
</a>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="text-muted-foreground px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
Pricing
|
||||
</Link>
|
||||
<nav className="grow ml-6 hidden text-sm md:flex md:items-center">
|
||||
{/* Product Dropdown */}
|
||||
<div className="relative group">
|
||||
<button className="flex items-center px-4 py-6 gap-1 transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
Product
|
||||
<ChevronDown className="size-3 ml-1 mt-0.5" />
|
||||
</button>
|
||||
<div className="absolute left-0 top-12 mt-2 w-[260px] rounded-md border border-border bg-background py-1 shadow-lg opacity-0 -translate-y-2 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-200">
|
||||
<Link
|
||||
href="/extension"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
<Puzzle className="size-3 inline mr-2 -mt-0.5" />
|
||||
Roo Code VS Code Extension
|
||||
</Link>
|
||||
<Link
|
||||
href="/cloud"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
<Cloud className="size-3 inline mr-2 -mt-0.5" />
|
||||
Roo Code Cloud
|
||||
</Link>
|
||||
<Link
|
||||
href="/provider"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
<Brain className="size-3 inline mr-2 -mt-0.5" />
|
||||
Roo Code Cloud Provider
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Resources Dropdown */}
|
||||
<div className="relative group">
|
||||
<button className="flex items-center px-4 py-6 gap-1 text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
<button className="flex items-center px-4 py-6 gap-1 transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
Resources
|
||||
<ChevronDown className="size-3" />
|
||||
<ChevronDown className="size-3 ml-1 mt-0.5" />
|
||||
</button>
|
||||
{/* Dropdown Menu */}
|
||||
<div className="absolute left-0 top-12 mt-2 w-40 rounded-md border border-border bg-background py-1 shadow-lg opacity-0 -translate-y-2 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-200">
|
||||
<ScrollButton
|
||||
targetId="faq"
|
||||
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground">
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
FAQ
|
||||
</ScrollButton>
|
||||
<Link
|
||||
href="/evals"
|
||||
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground">
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
Evals
|
||||
</Link>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground">
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SECURITY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Trust Center
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DOCUMENTATION}
|
||||
target="_blank"
|
||||
className="px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
Docs
|
||||
</a>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
|
||||
Pricing
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="hidden md:flex md:items-center md:space-x-4">
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="hidden md:flex md:items-center md:space-x-4 flex-shrink-0 font-medium">
|
||||
<div className="flex flex-row space-x-2 flex-shrink-0">
|
||||
<ThemeToggle />
|
||||
<Link
|
||||
href={EXTERNAL_LINKS.GITHUB}
|
||||
target="_blank"
|
||||
className="hidden items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground md:flex">
|
||||
className="hidden items-center gap-1.5 text-sm hover:text-foreground md:flex whitespace-nowrap">
|
||||
<RxGithubLogo className="h-4 w-4" />
|
||||
{stars !== null && <span>{stars}</span>}
|
||||
</Link>
|
||||
</div>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden items-center gap-1.5 rounded-md py-2 text-sm border border-primary-background px-4 text-primary-background transition-all duration-200 hover:shadow-lg hover:scale-105 lg:flex">
|
||||
Log in
|
||||
</a>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex">
|
||||
Sign Up
|
||||
</a>
|
||||
<Link
|
||||
href={EXTERNAL_LINKS.MARKETPLACE}
|
||||
target="_blank"
|
||||
className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex">
|
||||
className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex whitespace-nowrap">
|
||||
<VscVscode className="-mr-[2px] mt-[1px] h-4 w-4" />
|
||||
<span>
|
||||
Install <span className="font-black max-lg:text-xs">·</span>
|
||||
</span>
|
||||
{downloads !== null && <span>{downloads}</span>}
|
||||
</Link>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden items-center gap-1.5 rounded-md py-2 text-sm border border-primary-background px-4 font-medium text-primary-background transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex">
|
||||
<Cloud className="inline h-4 w-4" />
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
|
|
@ -141,18 +164,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
<nav className="flex flex-col justify-between h-full pb-16 overflow-y-auto bg-background pointer-events-auto">
|
||||
{/* Main navigation items */}
|
||||
<div className="grow-1 py-4 font-semibold text-lg">
|
||||
<ScrollButton
|
||||
targetId="product"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Extension
|
||||
</ScrollButton>
|
||||
<Link
|
||||
href="/cloud"
|
||||
className="block w-full p-5 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Cloud
|
||||
</Link>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DOCUMENTATION}
|
||||
target="_blank"
|
||||
|
|
@ -167,6 +178,31 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
Pricing
|
||||
</Link>
|
||||
|
||||
{/* Product Section */}
|
||||
<div className="mt-4 w-full">
|
||||
<div className="px-5 pb-2 pt-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Product
|
||||
</div>
|
||||
<Link
|
||||
href="/extension"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code VS Code Extension
|
||||
</Link>
|
||||
<Link
|
||||
href="/cloud"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code Cloud
|
||||
</Link>
|
||||
<Link
|
||||
href="/provider"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code Cloud Provider
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Resources Section */}
|
||||
<div className="mt-4 w-full">
|
||||
<div className="px-5 pb-2 pt-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
|
|
@ -209,7 +245,7 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
<Link
|
||||
href={EXTERNAL_LINKS.GITHUB}
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-2 rounded-md p-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
className="inline-flex items-center gap-2 rounded-md p-3 text-sm transition-colors hover:bg-accent hover:text-foreground"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
<RxGithubLogo className="h-6 w-6" />
|
||||
{stars !== null && <span>{stars}</span>}
|
||||
|
|
@ -220,21 +256,30 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
<Link
|
||||
href={EXTERNAL_LINKS.MARKETPLACE}
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-2 rounded-md p-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
className="inline-flex items-center gap-2 rounded-md p-3 text-sm transition-colors hover:bg-accent hover:text-foreground"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
<VscVscode className="h-6 w-6" />
|
||||
{downloads !== null && <span>{downloads}</span>}
|
||||
</Link>
|
||||
</div>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 rounded-lg border border-primary bg-background p-4 mx-4 mb-4 text-base font-semibold text-primary"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
<Cloud className="h-5 w-5" />
|
||||
Log in
|
||||
</a>
|
||||
<div className="flex gap-2 px-4 pb-4">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 rounded-full border border-primary bg-foreground p-4 w-full text-base font-semibold text-background"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Sign up
|
||||
</a>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 rounded-full border border-primary bg-background p-4 w-full text-base font-semibold text-primary"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
"use client"
|
||||
|
||||
import { motion } from "framer-motion"
|
||||
import { Shield, Users2, ReplaceAll, Keyboard, LucideIcon, CheckCheck, GitPullRequest } from "lucide-react"
|
||||
import {
|
||||
Shield,
|
||||
Users2,
|
||||
ReplaceAll,
|
||||
Keyboard,
|
||||
LucideIcon,
|
||||
CheckCheck,
|
||||
GitPullRequest,
|
||||
Boxes,
|
||||
TextSearch,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
|
||||
export interface Feature {
|
||||
|
|
@ -21,7 +31,8 @@ export const features: Feature[] = [
|
|||
{
|
||||
icon: ReplaceAll,
|
||||
title: "Model-agnostic",
|
||||
description: "Bring your own provider key or even run local inference — no markup, lock-in, no restrictions.",
|
||||
description:
|
||||
"Use the Roo Code cloud Provider, bring your own provider key or even run local inference — no markup, lock-in, no restrictions.",
|
||||
logos: ["Anthropic", "OpenAI", "Gemini", "Grok", "Qwen", "Kimi", "Mistral", "Ollama"],
|
||||
},
|
||||
{
|
||||
|
|
@ -31,16 +42,27 @@ export const features: Feature[] = [
|
|||
"Control each action and make Roo as autonomous as you want as you build confidence. Or go YOLO and let it rip.",
|
||||
},
|
||||
{
|
||||
icon: GitPullRequest,
|
||||
title: "Proudly open source",
|
||||
icon: Boxes,
|
||||
title: "Large task coordination",
|
||||
description:
|
||||
"Community-driven and fully auditable: no throttling or surprises about what's happening behind the scenes.",
|
||||
"Orchestrator mode handles large tasks by coordinating tasks for other agents, running for hours and delivering.",
|
||||
},
|
||||
{
|
||||
icon: TextSearch,
|
||||
title: "Performant with large codebases",
|
||||
description: "Configurable integrated semantic search for quicker retrieval in large codebases.",
|
||||
},
|
||||
{
|
||||
icon: Keyboard,
|
||||
title: "Highly customizable",
|
||||
description:
|
||||
"Fine-tune settings for Roo to work for you, like inference context, model properties, slash commands and more.",
|
||||
"Fine-tune settings for Roo to work for you, like inference context, model properties, slash commands and more. Most settings can be global or serialized in your repository.",
|
||||
},
|
||||
{
|
||||
icon: GitPullRequest,
|
||||
title: "Proudly open source",
|
||||
description:
|
||||
"Community-driven and fully auditable: no throttling or surprises about what's happening behind the scenes.",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
|
|
|
|||
|
|
@ -6,3 +6,9 @@ export * from "./features"
|
|||
export * from "./install-section"
|
||||
export * from "./testimonials"
|
||||
export * from "./whats-new-button"
|
||||
export * from "./option-overview-section"
|
||||
export * from "./pillars-section"
|
||||
export * from "./cloud-section"
|
||||
export * from "./ecosystem-section"
|
||||
export * from "./cta-section"
|
||||
export * from "./use-examples-section"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import { Laptop, Cloud, ArrowRight } from "lucide-react"
|
||||
import { Button } from "../ui"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
export function OptionOverviewSection() {
|
||||
return (
|
||||
<section className="py-24 bg-background">
|
||||
<div className="container px-4 mx-auto sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Different form factors for different ways of working.
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Roo's always there to help you get stuff done.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 max-w-5xl mx-auto relative z-1">
|
||||
<div className="absolute left-1/2 top-[140px] -translate-x-1/2 z-1">
|
||||
<div className="rounded-full size-[300px] border-[10px] border-dashed animate-[spin_10s_linear_infinite] blur-[5px]" />
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl bg-card outline outline-border/50 hover:outline-8 shadow-xl p-8 h-full group transition-all hover:shadow-2xl hover:shadow-blue-800/30 relative">
|
||||
<div className="size-15 p-3 rounded-full flex items-center justify-center shadow-lg absolute -top-4 -right-2 transition-all outline outline-foreground/5 bg-card group group-hover:outline-3 group-hover:scale-105">
|
||||
<Laptop className="size-8 text-blue-500" strokeWidth={1.5} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold tracking-tight mb-2">Roo Code VS Code Extension</h3>
|
||||
<p className="font-semibold text-blue-500 mb-4">For Individual Work</p>
|
||||
|
||||
<div className="text-muted-foreground mb-4">
|
||||
<p>
|
||||
Run Roo directly in VS Code (or any fork – even Cursor!), stay close to the code and
|
||||
control everything:
|
||||
</p>
|
||||
<ul className="list-inside my-4 space-y-1">
|
||||
<li className="list-disc">Approve every action (or set it to auto-approve)</li>
|
||||
<li className="list-disc">Manage the context window</li>
|
||||
<li className="list-disc">Configure every detail</li>
|
||||
<li className="list-disc">Preview changes live</li>
|
||||
<li className="list-disc">Stick to your customized editor</li>
|
||||
<li className="list-disc">Write code by hand (gasp!)</li>
|
||||
</ul>
|
||||
<p>
|
||||
Ideal for real-time debugging or quick iteration where you need full, immediate control.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button size="lg" variant="default" className="bg-blue-600 hover:bg-blue-600/80">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.MARKETPLACE}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Install now
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl bg-card outline outline-border/50 hover:outline-8 shadow-xl p-8 h-full group transition-all hover:shadow-2xl hover:shadow-blue-800/30 relative">
|
||||
<div className="size-15 p-3 rounded-full flex items-center justify-center shadow-lg absolute -top-4 -right-2 transition-all outline outline-foreground/5 bg-card group group-hover:outline-3 group-hover:scale-105">
|
||||
<Cloud className="size-8 text-violet-500" strokeWidth={1.5} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold tracking-tight mb-2">Roo Code Cloud</h3>
|
||||
<div className="font-semibold text-violet-500 mb-4">For Team Work with Agents</div>
|
||||
|
||||
<div className="text-muted-foreground mb-4">
|
||||
<p>
|
||||
Create your agent team in the Cloud, give them access to Github and start giving them
|
||||
tasks:
|
||||
</p>
|
||||
<ul className="list-inside my-4 space-y-1">
|
||||
<li className="list-disc">
|
||||
Use agents like the Planner, Coder, Explainer, Reviewer and Fixer
|
||||
</li>
|
||||
<li className="list-disc">Choose your provider and model</li>
|
||||
<li className="list-disc">
|
||||
Create tasks from the Web and Slack (more integrations soon)
|
||||
</li>
|
||||
<li className="list-disc">Get PR Reviews (and fixes) directly on Github</li>
|
||||
<li className="list-disc">Collaborate with co-workers</li>
|
||||
</ul>
|
||||
<p>
|
||||
Ideal for kicking projects off, parallelizing execution and looping in the rest of your
|
||||
team.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button size="lg" variant="default" className="bg-violet-600 hover:bg-violet-600/80">
|
||||
<a href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME} className="flex items-center justify-center">
|
||||
Try now for free
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
202
apps/web-roo-code/src/components/homepage/pillars-section.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { Brain, Keyboard, Shield, Users2, Map, Code, MessageCircleQuestion, Bug, TestTube } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { Link } from "../ui"
|
||||
|
||||
const MODEL_LOGOS = [
|
||||
"OpenRouter",
|
||||
"Anthropic",
|
||||
"OpenAI",
|
||||
"Gemini",
|
||||
"Grok",
|
||||
"Bedrock",
|
||||
"Moonshot",
|
||||
"Qwen",
|
||||
"Kimi",
|
||||
"Mistral",
|
||||
"Ollama",
|
||||
]
|
||||
const MODE_EXAMPLES = [
|
||||
{
|
||||
name: "Architect",
|
||||
description: "Plans complex changes without making changes.",
|
||||
icon: Map,
|
||||
},
|
||||
{
|
||||
name: "Code",
|
||||
description: "Implements, refactors and optimizes code.",
|
||||
icon: Code,
|
||||
},
|
||||
{
|
||||
name: "Ask",
|
||||
description: "Explains functionality and program behavior.",
|
||||
icon: MessageCircleQuestion,
|
||||
},
|
||||
{
|
||||
name: "Debug",
|
||||
description: "Diagnoses issues, traces failures, and proposes targeted, reliable fixes.",
|
||||
icon: Bug,
|
||||
},
|
||||
{
|
||||
name: "Test",
|
||||
description: "Creates and improves performant tests without changing the actual functionality.",
|
||||
icon: TestTube,
|
||||
},
|
||||
]
|
||||
|
||||
export function PillarsSection() {
|
||||
return (
|
||||
<section className="py-24 bg-muted/30 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="container px-4 mx-auto sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
To trust an agent, you have to do it on your own terms.
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-xl mx-auto">
|
||||
Roo is designed from the ground up to give you the confidence to do ever more with AI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:grid md:grid-cols-8 gap-8">
|
||||
<div className="relative md:col-span-3 h-full">
|
||||
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
|
||||
<div className="absolute -right-3 -top-5 bg-card border shadow-md rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
|
||||
<Brain className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-1">Model-agnostic by design</h3>
|
||||
<h4 className="font-semibold text-lg">Flexible and future-proof.</h4>
|
||||
<div className="text-muted-foreground my-4 space-y-1">
|
||||
<p>
|
||||
"The best model in the world" changes every other week. Providers
|
||||
throttle models with no warning. 1st-party coding agents only work with their
|
||||
own models.
|
||||
</p>
|
||||
<p>Roo doesn't care.</p>
|
||||
<p>
|
||||
It works great with 10s of models, from frontier to open weight. Choose from{" "}
|
||||
<Link href="/provider">the curated selection we offer at-cost</Link> or bring
|
||||
your own key.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Compatible with dozens of providers
|
||||
</span>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-4">
|
||||
{MODEL_LOGOS.map((logo, index) => (
|
||||
<Image
|
||||
key={logo}
|
||||
width={20}
|
||||
height={20}
|
||||
className="size-6 overflow-clip dark:invert"
|
||||
style={{ opacity: 1.1 - index / MODEL_LOGOS.length }}
|
||||
src={`/logos/${logo.toLowerCase()}.svg`}
|
||||
alt={`${logo} Logo`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative col-span-5 h-full">
|
||||
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
|
||||
<div className="absolute -right-3 -top-5 bg-card border shadow-lg rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
|
||||
<Users2 className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-1">Role-specific Modes</h3>
|
||||
<h4 className="font-semibold text-lg">On-task and under control.</h4>
|
||||
<div className="text-muted-foreground my-4 space-y-1">
|
||||
<p>
|
||||
As capable as they are, when let loose, LLMs hallucinate, cheat and can cause
|
||||
real damage.
|
||||
</p>
|
||||
<p>
|
||||
Roo's Modes keep models focused on a given task and limit their access to
|
||||
tools which are relevant to their role, keeping the context window clearer and
|
||||
avoiding surprises.
|
||||
</p>
|
||||
<p>
|
||||
Modes are even smart enough to ask to switch to another when stepping outside
|
||||
their responsibilities.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<span className="text-muted-foreground text-sm">Some examples</span>
|
||||
<ul className="flex gap-2 flex-wrap mt-2">
|
||||
{MODE_EXAMPLES.map((mode) => {
|
||||
const Icon = mode.icon
|
||||
return (
|
||||
<li
|
||||
key={mode.name}
|
||||
className="rounded-lg border bg-border/40 w-full md:w-[30%] min-w-[200px] text-sm px-3 py-2 flex gap-1">
|
||||
<Icon className="text-muted-foreground size-4 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-semibold">{mode.name}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{mode.description}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative col-span-4 h-full">
|
||||
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
|
||||
<div className="absolute -right-3 -top-5 bg-card border shadow-lg rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
|
||||
<Keyboard className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-1">Highly configurable</h3>
|
||||
<h4 className="font-semibold text-lg">Make it fit your workflow.</h4>
|
||||
<div className="text-muted-foreground my-4 space-y-1">
|
||||
<p>
|
||||
Developer tools need to fit like gloves. Highly tweakable,
|
||||
keyboard-shortcut-heavy gloves.
|
||||
</p>
|
||||
<p>We made Roo thoughtfully configurable to fit your workflow as best it can.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative col-span-4 h-full">
|
||||
<div className="rounded-2xl bg-card outline outline-border/50 hover:outline-8 shadow-lg p-8 h-full group transition-all hover:shadow-2xl">
|
||||
<div className="absolute -right-3 -top-5 bg-card border shadow-lg rounded-full p-3 transition-all group-hover:-top-4 group-hover:-right-2 group-hover:scale-110 group-hover:shadow-xl">
|
||||
<Shield className="size-8 text-violet-600 shrink-0 mt-1" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-1">Secure and transparent</h3>
|
||||
<h4 className="font-semibold text-lg">Open source from the get go.</h4>
|
||||
<div className="text-muted-foreground my-4 space-y-1">
|
||||
<p>
|
||||
The Roo Code Extension is{" "}
|
||||
<Link target="_blank" href="https://github.com/Roo-Code-Inc/Roo-Code">
|
||||
open source
|
||||
</Link>{" "}
|
||||
so you can see for yourself exactly what it's doing and we don't use
|
||||
your data for training.
|
||||
</p>
|
||||
<p>
|
||||
Plus we're fully SOC2 Type 2 compliant and follow industry-standard
|
||||
security practices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -193,11 +193,9 @@ export function Testimonials() {
|
|||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto mb-8 md:max-w-2xl text-center">
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
Developers <em>really</em> shipping with AI are using Roo Code
|
||||
More than 1 million people are shipping with Roo.
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-muted-foreground">
|
||||
Join more than 1M people revolutionizing their workflow worldwide
|
||||
</p>
|
||||
<p className="mt-6 text-lg text-muted-foreground">And they have some great things to say.</p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
|
|
@ -247,7 +245,11 @@ export function Testimonials() {
|
|||
{testimonial.name}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground transition-colors duration-300 dark:text-muted-foreground/80">
|
||||
{testimonial.role} at {testimonial.origin}
|
||||
{testimonial.role !== "Reviewer" && (
|
||||
<>
|
||||
{testimonial.role} at {testimonial.origin}
|
||||
</>
|
||||
)}
|
||||
{testimonial.stars && (
|
||||
<span className="flex items-center mt-1">
|
||||
{" "}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,438 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import {
|
||||
LucideIcon,
|
||||
Pointer,
|
||||
Slack,
|
||||
Github,
|
||||
Code,
|
||||
GitPullRequest,
|
||||
Wrench,
|
||||
Map,
|
||||
MessageCircleQuestionMark,
|
||||
CornerDownRight,
|
||||
ChevronDown,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { Button } from "../ui"
|
||||
|
||||
interface UseCase {
|
||||
role: string
|
||||
use: string
|
||||
agent: UseCaseAgent
|
||||
context: UseCaseSource
|
||||
}
|
||||
|
||||
interface UseCaseSource {
|
||||
name: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
interface UseCaseAgent {
|
||||
name: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
interface PositionedUseCase extends UseCase {
|
||||
layer: 1 | 2 | 3 | 4
|
||||
position: { x: number; y: number }
|
||||
scale: number
|
||||
zIndex: number
|
||||
avatar: string
|
||||
}
|
||||
|
||||
const SOURCES = {
|
||||
slack: {
|
||||
name: "Slack",
|
||||
icon: Slack,
|
||||
},
|
||||
web: {
|
||||
name: "Web",
|
||||
icon: Pointer,
|
||||
},
|
||||
github: {
|
||||
name: "Github",
|
||||
icon: Github,
|
||||
},
|
||||
extension: {
|
||||
name: "Extension",
|
||||
icon: Code,
|
||||
},
|
||||
}
|
||||
|
||||
const AGENTS = {
|
||||
explainer: {
|
||||
name: "Explainer",
|
||||
icon: MessageCircleQuestionMark,
|
||||
},
|
||||
planner: {
|
||||
name: "Planner",
|
||||
icon: Map,
|
||||
},
|
||||
coder: {
|
||||
name: "Coder",
|
||||
icon: Code,
|
||||
},
|
||||
reviewer: {
|
||||
name: "Reviewer",
|
||||
icon: GitPullRequest,
|
||||
},
|
||||
fixer: {
|
||||
name: "Fixer",
|
||||
icon: Wrench,
|
||||
},
|
||||
}
|
||||
|
||||
const USE_CASES: UseCase[] = [
|
||||
{
|
||||
role: "Frontend Developer",
|
||||
use: "Take Lisa's feedback above and incorporate it into the landing page.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.slack,
|
||||
},
|
||||
{
|
||||
role: "Customer Success",
|
||||
use: "What could be causing this bug as described by the customer?",
|
||||
agent: AGENTS.explainer,
|
||||
context: SOURCES.web,
|
||||
},
|
||||
{
|
||||
role: "Backend Engineer",
|
||||
use: "Create a migration denormalizing total_cost calculation and backfill the remainder.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.extension,
|
||||
},
|
||||
{
|
||||
role: "Security Engineer",
|
||||
use: "Do we use any of the libraries mentioned in the thread?",
|
||||
agent: AGENTS.explainer,
|
||||
context: SOURCES.slack,
|
||||
},
|
||||
{
|
||||
role: "Designer",
|
||||
use: "Refactor the button component to use CSS variables",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.slack,
|
||||
},
|
||||
{
|
||||
role: "Product Manager",
|
||||
use: "How big of a change would it be to turn this from a yes/no to have 4 options?",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.web,
|
||||
},
|
||||
{
|
||||
role: "QA Engineer",
|
||||
use: "Write a Playwright test for the login flow failure case, extract existing mocks into shared.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.github,
|
||||
},
|
||||
{
|
||||
role: "DevOps Engineer",
|
||||
use: "Update the Dockerfile to use Node 20 Alpine.",
|
||||
agent: AGENTS.fixer,
|
||||
context: SOURCES.slack,
|
||||
},
|
||||
{
|
||||
role: "Mobile Developer",
|
||||
use: "Copy what we did in PR #4253 and apply to this component.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.slack,
|
||||
},
|
||||
{
|
||||
role: "Technical Writer",
|
||||
use: "Generate JSDoc comments for the auth utility functions.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.github,
|
||||
},
|
||||
{
|
||||
role: "Junior Developer",
|
||||
use: "Review this pull request for potential performance improvements.",
|
||||
agent: AGENTS.reviewer,
|
||||
context: SOURCES.github,
|
||||
},
|
||||
{
|
||||
role: "Engineering Manager",
|
||||
use: "Break down this user profile feature into technical tasks, grouped by skill.",
|
||||
agent: AGENTS.planner,
|
||||
context: SOURCES.web,
|
||||
},
|
||||
{
|
||||
role: "Support Engineer",
|
||||
use: "What's causing this stack trace? The customer is on MacOS 26.1.",
|
||||
agent: AGENTS.explainer,
|
||||
context: SOURCES.web,
|
||||
},
|
||||
{
|
||||
role: "Frontend Developer",
|
||||
use: "Make the navigation menu responsive on mobile devices.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.web,
|
||||
},
|
||||
{
|
||||
role: "Backend Engineer",
|
||||
use: "Give me two architecture options for the notification system in this PRD.",
|
||||
agent: AGENTS.planner,
|
||||
context: SOURCES.web,
|
||||
},
|
||||
{
|
||||
role: "Designer",
|
||||
use: "Implement the loading spinner animation in CSS.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.web,
|
||||
},
|
||||
{
|
||||
role: "Customer Success",
|
||||
use: "Write a script to find patterns in these CPU load logs.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.slack,
|
||||
},
|
||||
{
|
||||
role: "Full Stack Dev",
|
||||
use: "Refactor user_preferences to use named columns instead of a single JSON blob",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.extension,
|
||||
},
|
||||
{
|
||||
role: "QA Engineer",
|
||||
use: "Automate the regression suite for the checkout process.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.extension,
|
||||
},
|
||||
{
|
||||
role: "DevOps Engineer",
|
||||
use: "Understand why this build error only happens in prod and fix it.",
|
||||
agent: AGENTS.coder,
|
||||
context: SOURCES.extension,
|
||||
},
|
||||
{
|
||||
role: "Product Marketer",
|
||||
use: "What were the 5 most significant PRs merged in the past week?",
|
||||
agent: AGENTS.explainer,
|
||||
context: SOURCES.slack,
|
||||
},
|
||||
{
|
||||
role: "Junior Developer",
|
||||
use: "Explain how useEffect dependency arrays work here.",
|
||||
agent: AGENTS.explainer,
|
||||
context: SOURCES.extension,
|
||||
},
|
||||
{
|
||||
role: "Senior Engineer",
|
||||
use: "Check if this implementation follows the Single Responsibility Principle.",
|
||||
agent: AGENTS.reviewer,
|
||||
context: SOURCES.github,
|
||||
},
|
||||
]
|
||||
|
||||
// Seeded random number generator for consistent layout
|
||||
function seededRandom(seed: number) {
|
||||
let value = seed
|
||||
return () => {
|
||||
value = (value * 9301 + 49297) % 233280
|
||||
return value / 233280
|
||||
}
|
||||
}
|
||||
|
||||
const LAYER_SCALES = {
|
||||
1: 0.7,
|
||||
2: 0.85,
|
||||
3: 1.0,
|
||||
4: 1.15,
|
||||
}
|
||||
|
||||
function distributeItems(items: UseCase[]): PositionedUseCase[] {
|
||||
const rng = seededRandom(Math.random() * 12345)
|
||||
const zones = { rows: 7, cols: 4 }
|
||||
const zoneWidth = 100 / zones.cols
|
||||
const zoneHeight = 100 / zones.rows
|
||||
|
||||
// Create array of zone indices [0...19] and shuffle them
|
||||
const zoneIndices = Array.from({ length: items.length }, (_, i) => i)
|
||||
for (let i = zoneIndices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rng() * (i + 1))
|
||||
const temp = zoneIndices[i]!
|
||||
zoneIndices[i] = zoneIndices[j]!
|
||||
zoneIndices[j] = temp
|
||||
}
|
||||
|
||||
return items.map((item, index) => {
|
||||
// Assign to a random unique zone
|
||||
const zoneIndex = zoneIndices[index]!
|
||||
const row = Math.floor(zoneIndex / zones.cols)
|
||||
const col = zoneIndex % zones.cols
|
||||
|
||||
// Distribute layers evenly
|
||||
const layer = ((index % 4) + 1) as 1 | 2 | 3 | 4
|
||||
|
||||
// Calculate base position (center of zone)
|
||||
const baseX = col * zoneWidth + zoneWidth / 2
|
||||
const baseY = row * zoneHeight + zoneHeight / 2
|
||||
|
||||
// Add jitter (±35% of zone size to keep somewhat contained but messy)
|
||||
const jitterX = (rng() - 0.5) * zoneWidth * 0.7
|
||||
const jitterY = (rng() - 0.5) * zoneHeight * 0.7
|
||||
|
||||
return {
|
||||
...item,
|
||||
avatar: `/illustrations/user-faces/${index + 1}.jpg`,
|
||||
layer,
|
||||
position: {
|
||||
x: baseX + jitterX,
|
||||
y: baseY + jitterY,
|
||||
},
|
||||
scale: LAYER_SCALES[layer],
|
||||
zIndex: layer,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function UseCaseCardContent({
|
||||
item,
|
||||
opacity = 1,
|
||||
className = "",
|
||||
}: {
|
||||
item: UseCase & { avatar: string }
|
||||
opacity?: number
|
||||
className?: string
|
||||
}) {
|
||||
const ContextIcon: LucideIcon = item.context.icon
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl outline outline-border/50 bg-card/80 backdrop-blur-sm p-3 md:p-4 shadow-xl transition-all hover:shadow-xl hover:outline-8 ${className}`}>
|
||||
<div
|
||||
className="text-sm flex items-center gap-2 font-medium text-violet-600 mb-1"
|
||||
style={{ opacity: opacity }}>
|
||||
<Image
|
||||
src={item.avatar}
|
||||
className="size-6 rounded-full outline-1 outline-border"
|
||||
alt=""
|
||||
width={18}
|
||||
height={18}
|
||||
unoptimized
|
||||
/>
|
||||
<span className="text-nowrap">{item.role}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-[0.7em] flex flex-wrap items-center gap-1 text-muted-foreground mb-1"
|
||||
style={{ opacity: opacity }}>
|
||||
<CornerDownRight className="size-4 shrink-0 ml-3 -mt-1" />
|
||||
<span className="text-nowrap font-mono">To {item.agent.name} Agent</span>
|
||||
</div>
|
||||
|
||||
<div className="text-base font-light leading-tight my-1 ml-8" style={{ opacity: opacity }}>
|
||||
{item.use}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-[0.7em] font-light text-muted-foreground leading-tight mt-2 ml-8"
|
||||
style={{ opacity: opacity }}>
|
||||
via <ContextIcon strokeWidth={1.5} className="size-3.5 inline ml-1" /> {item.context.name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopUseCaseCard({ item }: { item: PositionedUseCase }) {
|
||||
const opacity = Math.min(1, 0.5 + item.layer / 3)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="absolute w-[200px] cursor-default group"
|
||||
style={{
|
||||
left: `${item.position.x}%`,
|
||||
top: `${item.position.y}%`,
|
||||
zIndex: item.zIndex,
|
||||
width: Math.round(300 + Math.random() * 100),
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
whileInView={{
|
||||
opacity: 1,
|
||||
scale: item.scale,
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
delay: 0, // Stagger by layer
|
||||
},
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.3,
|
||||
zIndex: 30,
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
// Use standard CSS transform for the positioning to avoid conflicts with Framer Motion's scale
|
||||
transformTemplate={({ scale }) => `translate(-50%, -50%) scale(${scale})`}>
|
||||
<UseCaseCardContent
|
||||
item={item}
|
||||
opacity={opacity}
|
||||
className={item.layer === 4 ? "shadow-lg border-border" : ""}
|
||||
/>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export function UseExamplesSection({ agentTitle = false }: { agentTitle?: boolean }) {
|
||||
const positionedItems = useMemo(() => distributeItems(USE_CASES), [])
|
||||
const [showAllMobile, setShowAllMobile] = useState(false)
|
||||
|
||||
return (
|
||||
<section className="pt-24 bg-background overflow-hidden relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
|
||||
<div className="absolute left-1/2 top-1/2 h-[700px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground/10 blur-[140px]" />
|
||||
</div>
|
||||
<div className="container px-4 mx-auto sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold tracking-tight mb-4">
|
||||
{agentTitle ? (
|
||||
<>
|
||||
Part of the AI team to help your <em>entire</em> human team
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
The AI team to help your <em>entire</em> human team
|
||||
</>
|
||||
)}
|
||||
</h2>
|
||||
<p className="text-xl font-light text-muted-foreground max-w-2xl mx-auto">
|
||||
Developers, PMs, Designers, Customer Success: everyone moves faster and more independently with
|
||||
Roo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Vertical Staggered List */}
|
||||
<div className="md:hidden flex flex-col gap-2 px-2 pb-12 max-w-md mx-auto">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{positionedItems.slice(0, showAllMobile ? undefined : 8).map((item, index) => (
|
||||
<motion.div
|
||||
key={item.use} // Use a unique key for proper animation tracking
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: (index % 8) * 0.1, duration: 0.4 }}
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
className={`w-[90%] ${index % 2 === 0 ? "self-start" : "self-end"}`}>
|
||||
<UseCaseCardContent item={item} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{!showAllMobile && (
|
||||
<div className="text-center mt-8 z-10">
|
||||
<Button variant="outline" onClick={() => setShowAllMobile(true)}>
|
||||
More
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Positioned Items Container */}
|
||||
<div className="hidden md:block relative h-[800px] md:min-h-[800px] w-full max-w-6xl mx-auto">
|
||||
{positionedItems.map((item, index) => (
|
||||
<DesktopUseCaseCard key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -32,7 +32,6 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||
// Initialize PostHog immediately on the client side
|
||||
if (typeof window !== "undefined" && !posthog.__loaded) {
|
||||
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
|
||||
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST
|
||||
|
||||
// Check if environment variables are set
|
||||
if (!posthogKey) {
|
||||
|
|
@ -43,19 +42,13 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||
return
|
||||
}
|
||||
|
||||
if (!posthogHost) {
|
||||
console.warn(
|
||||
"PostHog host URL is missing. Using default host. " +
|
||||
"Please set NEXT_PUBLIC_POSTHOG_HOST in your .env file.",
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user has already consented to cookies
|
||||
const userHasConsented = hasConsent()
|
||||
|
||||
// Initialize PostHog with appropriate persistence based on consent
|
||||
posthog.init(posthogKey, {
|
||||
api_host: posthogHost || "https://us.i.posthog.com",
|
||||
api_host: "https://ph.roocode.com",
|
||||
ui_host: "https://us.posthog.com",
|
||||
capture_pageview: false, // We handle pageview tracking manually
|
||||
loaded: (posthogInstance) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
|
|
|
|||
|
|
@ -1,172 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import useEmblaCarousel from "embla-carousel-react"
|
||||
import AutoPlay from "embla-carousel-autoplay"
|
||||
import {
|
||||
Bug,
|
||||
FileText,
|
||||
Gauge,
|
||||
GitPullRequest,
|
||||
Languages,
|
||||
Microscope,
|
||||
PocketKnife,
|
||||
TestTube,
|
||||
Wrench,
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
// AI Agent types for the carousel
|
||||
interface AIAgent {
|
||||
icon: LucideIcon
|
||||
name: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
const aiAgents: AIAgent[] = [
|
||||
{ icon: GitPullRequest, name: "PR Reviewer", page: "/reviewer" },
|
||||
{ icon: Wrench, name: "PR Fixer", page: "/pr-fixer" },
|
||||
{ icon: PocketKnife, name: "Generalist" },
|
||||
{ icon: Bug, name: "Bug Fixer" },
|
||||
{ icon: TestTube, name: "Test Engineer" },
|
||||
{ icon: Microscope, name: "Security Auditor" },
|
||||
{ icon: Gauge, name: "Performance Optimizer" },
|
||||
{ icon: FileText, name: "Documentation Writer" },
|
||||
{ icon: Languages, name: "String Translator" },
|
||||
]
|
||||
|
||||
export function AgentCarousel({ currentAgent = "" }: { currentAgent?: string } = {}) {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel(
|
||||
{
|
||||
loop: true,
|
||||
align: "start",
|
||||
watchDrag: true,
|
||||
dragFree: false,
|
||||
containScroll: false,
|
||||
duration: 10000,
|
||||
},
|
||||
[
|
||||
AutoPlay({
|
||||
playOnInit: true,
|
||||
delay: 0,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: false,
|
||||
stopOnFocusIn: false,
|
||||
}),
|
||||
],
|
||||
)
|
||||
|
||||
// Continuous scrolling effect
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return
|
||||
|
||||
const autoPlay = emblaApi?.plugins()?.autoPlay as
|
||||
| {
|
||||
play?: () => void
|
||||
}
|
||||
| undefined
|
||||
|
||||
if (autoPlay?.play) {
|
||||
autoPlay.play()
|
||||
}
|
||||
|
||||
// Set up continuous scrolling
|
||||
const interval = setInterval(() => {
|
||||
if (emblaApi) {
|
||||
emblaApi.scrollNext()
|
||||
}
|
||||
}, 30) // Smooth continuous scroll
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [emblaApi])
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: [0.21, 0.45, 0.27, 0.9],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Duplicate the agents array for seamless infinite scroll
|
||||
const displayAgents = [...aiAgents, ...aiAgents]
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden border-t border-border py-32">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto mb-12 max-w-4xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
The first members of a whole new team
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-muted-foreground">
|
||||
Architecture, coding, reviewing, testing, debugging, documenting, designing – almost
|
||||
everything we do today is mostly through our agents. Now we're bringing them to you.
|
||||
</p>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Roo's {currentAgent} isn't yet another single-purpose tool to add to your already
|
||||
complicated stack. It's the first member of your AI-powered development team. More
|
||||
agents are shipping soon.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto md:max-w-[1200px]">
|
||||
<motion.div
|
||||
className="relative -mx-4 md:mx-auto max-w-[1400px]"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}>
|
||||
{/* Gradient Overlays */}
|
||||
<div className="absolute inset-y-0 left-0 z-10 w-[10%] bg-gradient-to-r from-background to-transparent pointer-events-none md:w-[15%]" />
|
||||
<div className="absolute inset-y-0 right-0 z-10 w-[10%] bg-gradient-to-l from-background to-transparent pointer-events-none md:w-[15%]" />
|
||||
|
||||
{/* Embla Carousel Container */}
|
||||
<div className="overflow-hidden" ref={emblaRef}>
|
||||
<div className="flex pb-4">
|
||||
{displayAgents.map((agent, index) => {
|
||||
const Icon = agent.icon
|
||||
return (
|
||||
<div
|
||||
key={`${agent.name}-${index}`}
|
||||
className="relative min-w-0 flex-[0_0_45%] px-2 md:flex-[0_0_30%] md:px-4 lg:flex-[0_0_15%]">
|
||||
<div className="group relative py-6 cursor-default">
|
||||
<div
|
||||
className="relative flex flex-col items-center justify-center rounded-full w-[150px] h-[150px] border border-border bg-background p-6 transition-all duration-500 ease-out shadow-xl
|
||||
hover:scale-110 hover:-translate-y-2
|
||||
hover:shadow-[0_20px_50px_rgba(39,110,226,0.25)] dark:hover:shadow-[0_20px_50px_rgba(59,130,246,0.25)]">
|
||||
<Icon
|
||||
strokeWidth={1}
|
||||
className="size-9 mb-2 text-foreground transition-colors duration-300"
|
||||
/>
|
||||
<h3 className="text-center leading-tight tracking-tight transition-colors duration-300 dark:text-foreground">
|
||||
{agent.page ? (
|
||||
<a
|
||||
href={agent.page}
|
||||
className="text-foreground/90 font-semibold">
|
||||
{agent.name}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-foreground/60 font-medium">
|
||||
{agent.name}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,12 +5,12 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
|
||||
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
|
|
@ -18,8 +18,9 @@ const buttonVariants = cva(
|
|||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
sm: "h-8 px-3 text-xs",
|
||||
lg: "h-10 px-8",
|
||||
xl: "h-14 px-8 text-lg",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export * from "./chart"
|
|||
export * from "./modal"
|
||||
export * from "./scroll-button"
|
||||
export * from "./table"
|
||||
export * from "./link"
|
||||
|
|
|
|||
38
apps/web-roo-code/src/components/ui/link.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import * as React from "react"
|
||||
import NextLink from "next/link"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type BaseLinkProps = React.ComponentPropsWithoutRef<typeof NextLink>
|
||||
|
||||
type LinkProps = BaseLinkProps & {
|
||||
newWindow?: boolean
|
||||
}
|
||||
|
||||
const Link = React.forwardRef<React.ElementRef<typeof NextLink>, LinkProps>(
|
||||
({ className, newWindow = false, target, rel, ...props }, ref) => {
|
||||
const computedTarget = newWindow ? "_blank" : target
|
||||
const computedRel = newWindow
|
||||
? rel
|
||||
? rel.includes("noreferrer")
|
||||
? rel
|
||||
: `${rel} noreferrer`
|
||||
: "noreferrer"
|
||||
: rel
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
ref={ref}
|
||||
className={cn("underline hover:no-underline", className)}
|
||||
target={computedTarget}
|
||||
rel={computedRel}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Link.displayName = "Link"
|
||||
|
||||
export { Link }
|
||||
export type { LinkProps }
|
||||
|
|
@ -26,7 +26,8 @@ export const EXTERNAL_LINKS = {
|
|||
TESTIMONIALS: "https://roocode.com/#testimonials",
|
||||
CLOUD_APP_LOGIN: "https://app.roocode.com/sign-in",
|
||||
CLOUD_APP_SIGNUP: "https://app.roocode.com/sign-up",
|
||||
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/welcome",
|
||||
CLOUD_APP_SIGNUP_HOME: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
}
|
||||
|
||||
export const INTERNAL_LINKS = {
|
||||
|
|
|
|||
22
apps/web-roo-code/src/lib/formatters.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
|
||||
export const formatCurrency = (amount: number) => formatter.format(amount)
|
||||
|
||||
export const formatTokens = (tokens: number) => {
|
||||
if (tokens < 1000) {
|
||||
return tokens.toString()
|
||||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}K`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
|
|
@ -104,13 +104,19 @@ export async function getVSCodeDownloads() {
|
|||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
// divide by 1000 to convert to "thousands" format,
|
||||
// multiply by 10, floor the result, then divide by 10 to keep one decimal place.
|
||||
// if number is 1 million or more, format as millions
|
||||
if (num >= 1000000) {
|
||||
const truncated = Math.floor((num / 1000000) * 100) / 100
|
||||
return truncated.toFixed(2) + "M"
|
||||
}
|
||||
|
||||
// otherwise, format as thousands
|
||||
const truncated = Math.floor((num / 1000) * 10) / 10
|
||||
// ensure one decimal is always shown and append "k"
|
||||
return truncated.toFixed(1) + "k"
|
||||
|
||||
// examples:
|
||||
// console.log(formatNumber(1033400)) -> "1.03M"
|
||||
// console.log(formatNumber(2500000)) -> "2.50M"
|
||||
// console.log(formatNumber(337231)) -> "337.2k"
|
||||
// console.log(formatNumber(23233)) -> "23.2k"
|
||||
// console.log(formatNumber(2322)) -> "2.3k"
|
||||
|
|
|
|||
31
apps/web-roo-code/src/lib/types/models.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
export interface ModelPricing {
|
||||
input: string
|
||||
output: string
|
||||
input_cache_read: string
|
||||
input_cache_write: string
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
object: string
|
||||
created: number
|
||||
owned_by: string
|
||||
name: string
|
||||
description: string
|
||||
context_window: number
|
||||
max_tokens: number
|
||||
type: string
|
||||
tags: string[]
|
||||
pricing: ModelPricing
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export interface ModelsResponse {
|
||||
data: Model[]
|
||||
}
|
||||
|
||||
export interface ModelWithTotalPrice extends Model {
|
||||
totalPrice: number
|
||||
}
|
||||
|
||||
export type SortOption = "alphabetical" | "price-asc" | "price-desc" | "context-window-asc" | "context-window-desc"
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"project": ["**/*.ts"]
|
||||
},
|
||||
"webview-ui": {
|
||||
"entry": ["src/index.tsx"],
|
||||
"entry": ["src/index.tsx", "src/browser-panel.tsx"],
|
||||
"project": ["src/**/*.{ts,tsx}", "../src/shared/*.ts"]
|
||||
},
|
||||
"packages/{build,cloud,evals,ipc,telemetry,types}": {
|
||||
|
|
|
|||
12
locales/ca/README.md
generated
|
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
|
||||
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
|
||||
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
|
|
@ -167,16 +167,6 @@ Ens encanten les contribucions de la comunitat! Comença llegint el nostre [CONT
|
|||
|
||||
---
|
||||
|
||||
## Col·laboradors
|
||||
|
||||
Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Llicència
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/de/README.md
generated
|
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
|
||||
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
|
||||
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
|
|
@ -167,16 +167,6 @@ Wir lieben Community-Beiträge! Lies unsere [CONTRIBUTING.md](CONTRIBUTING.md),
|
|||
|
||||
---
|
||||
|
||||
## Mitwirkende
|
||||
|
||||
Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code besser zu machen!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Lizenz
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/es/README.md
generated
|
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
|
||||
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
|
||||
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
|
|
@ -167,16 +167,6 @@ Usamos [changesets](https://github.com/changesets/changesets) para el versionado
|
|||
|
||||
---
|
||||
|
||||
## Colaboradores
|
||||
|
||||
¡Gracias a todos nuestros colaboradores que han ayudado a mejorar Roo Code!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Licencia
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||