Merge branch 'RooCodeInc:main' into add-dart-codebase-index
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -76,12 +76,11 @@ body:
|
|||
label: API Provider (optional)
|
||||
options:
|
||||
- Anthropic
|
||||
- AWS Bedrock
|
||||
- Amazon Bedrock
|
||||
- Chutes AI
|
||||
- DeepSeek
|
||||
- Featherless AI
|
||||
- Fireworks AI
|
||||
- Glama
|
||||
- Google Gemini
|
||||
- Google Vertex AI
|
||||
- Groq
|
||||
|
|
|
|||
3
.gitignore
vendored
|
|
@ -49,3 +49,6 @@ logs
|
|||
|
||||
# Qdrant
|
||||
qdrant_storage/
|
||||
|
||||
# Architect plans
|
||||
plans/
|
||||
|
|
@ -1,25 +1,6 @@
|
|||
version: "1.0"
|
||||
|
||||
commands:
|
||||
- name: Pull latest changes
|
||||
run: git pull
|
||||
timeout: 60
|
||||
execution_phase: task_run
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
timeout: 60
|
||||
execution_phase: task_run
|
||||
|
||||
github_events:
|
||||
- event: issues.opened
|
||||
action:
|
||||
name: github.issue.fix
|
||||
- event: issue_comment.created
|
||||
action:
|
||||
name: github.issue.comment.respond
|
||||
- event: pull_request.opened
|
||||
action:
|
||||
name: github.pr.review
|
||||
- event: pull_request_review_comment.created
|
||||
action:
|
||||
name: github.pr.comment.respond
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@
|
|||
|
||||
- 保留英文品牌名
|
||||
- 技术术语保持一致性
|
||||
- 保留英文专有名词:如"AWS Bedrock ARN"
|
||||
- 保留英文专有名词:如"Amazon Bedrock ARN"
|
||||
|
||||
4. **用户操作**
|
||||
- 操作动词统一:
|
||||
|
|
|
|||
338
CHANGELOG.md
|
|
@ -1,5 +1,321 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.36.16] - 2025-12-19
|
||||
|
||||
- Fix: Normalize tool schemas for VS Code LM API to resolve error 400 when using VS Code Language Model API providers (PR #10221 by @hannesrudolph)
|
||||
|
||||
## [3.36.15] - 2025-12-19
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
- Refactor: Unified context-management architecture with improved UX for better context control (PR #9795 by @hannesrudolph)
|
||||
- Add new `search_replace` native tool for single-replacement operations with improved editing precision (PR #9918 by @hannesrudolph)
|
||||
- Streaming tool stats and token usage throttling for better real-time feedback during generation (PR #9926 by @hannesrudolph)
|
||||
- Add versioned settings support with minPluginVersion gating for Roo provider (PR #9934 by @hannesrudolph)
|
||||
- Make Architect mode save plans to `/plans` directory and gitignore it (PR #9944 by @brunobergher)
|
||||
- Add announcement support CTA and social icons to UI (PR #9945 by @hannesrudolph)
|
||||
- Add ability to save screenshots from the browser tool (PR #9963 by @mrubens)
|
||||
- Refactor: Decouple tools from system prompt for cleaner architecture (PR #9784 by @daniel-lxs)
|
||||
- Update DeepSeek models to V3.2 with new pricing (PR #9962 by @hannesrudolph)
|
||||
- Add minimal and medium reasoning effort levels for Gemini models (PR #9973 by @hannesrudolph)
|
||||
- Update xAI models catalog with latest model options (PR #9872 by @hannesrudolph)
|
||||
- Add DeepSeek V3-2 support for Baseten provider (PR #9861 by @AlexKer)
|
||||
- Tweaks to Baseten model definitions for better defaults (PR #9866 by @mrubens)
|
||||
- Fix: Add xhigh reasoning effort support for gpt-5.1-codex-max (#9891 by @andrewginns, PR #9900 by @andrewginns)
|
||||
- Fix: Add Kimi, MiniMax, and Qwen model configurations for Bedrock (#9902 by @jbearak, PR #9905 by @app/roomote)
|
||||
- Configure tool preferences for xAI models (PR #9923 by @hannesrudolph)
|
||||
- Default to using native tools when supported on OpenRouter (PR #9878 by @mrubens)
|
||||
- Fix: Exclude apply_diff from native tools when diffEnabled is false (#9919 by @denis-kudelin, PR #9920 by @app/roomote)
|
||||
- Fix: Always show tool protocol selector for openai-compatible provider (#9965 by @bozoweed, PR #9966 by @hannesrudolph)
|
||||
- Fix: Respect explicit supportsReasoningEffort array values for proper model configuration (PR #9970 by @hannesrudolph)
|
||||
- Add timeout configuration to OpenAI Compatible Provider Client (PR #9898 by @dcbartlett)
|
||||
- Revert default tool protocol change from xml to native for stability (PR #9956 by @mrubens)
|
||||
- Remove defaultTemperature from Roo provider configuration (PR #9932 by @mrubens)
|
||||
- Improve OpenAI error messages to be more useful for debugging (PR #9639 by @mrubens)
|
||||
- Better error logs for parseToolCall exceptions (PR #9857 by @cte)
|
||||
- Improve cloud job error logging for RCC provider errors (PR #9924 by @cte)
|
||||
- Fix: Display actual API error message instead of generic text on retry (PR #9954 by @hannesrudolph)
|
||||
- Add API error telemetry to OpenRouter provider for better diagnostics (PR #9953 by @daniel-lxs)
|
||||
- Fix: Sanitize removed/invalid API providers to prevent infinite loop (PR #9869 by @hannesrudolph)
|
||||
- Fix: Use foreground color for context-management icons (PR #9912 by @hannesrudolph)
|
||||
- Fix: Suppress 'ask promise was ignored' error in handleError (PR #9914 by @daniel-lxs)
|
||||
- Fix: Process finish_reason to emit tool_call_end events properly (PR #9927 by @daniel-lxs)
|
||||
- Fix: Add finish_reason processing to xai.ts provider (PR #9929 by @daniel-lxs)
|
||||
- Fix: Validate and fix tool_result IDs before API requests (PR #9952 by @daniel-lxs)
|
||||
- Fix: Return undefined instead of 0 for disabled API timeout (PR #9960 by @hannesrudolph)
|
||||
- Stop making unnecessary count_tokens requests for better performance (PR #9884 by @mrubens)
|
||||
- Refactor: Consolidate ThinkingBudget components and fix disable handling (PR #9930 by @hannesrudolph)
|
||||
- Forbid time estimates in architect mode for more focused planning (PR #9931 by @app/roomote)
|
||||
- Web: Add product pages (PR #9865 by @brunobergher)
|
||||
- Make eval runs deleteable in the web UI (PR #9909 by @mrubens)
|
||||
- Feat: Change defaultToolProtocol default from xml to native (later reverted) (PR #9892 by @app/roomote)
|
||||
|
||||
## [3.36.2] - 2025-12-04
|
||||
|
||||

|
||||
|
||||
- Restrict GPT-5 tool set to apply_patch for improved compatibility (PR #9853 by @hannesrudolph)
|
||||
- Add dynamic settings support for Roo models from API, allowing model-specific configurations to be fetched dynamically (PR #9852 by @hannesrudolph)
|
||||
- Fix: Resolve Chutes provider model fetching issue (PR #9854 by @cte)
|
||||
|
||||
## [3.36.1] - 2025-12-04
|
||||
|
||||

|
||||
|
||||
- Add MessageManager layer for centralized history coordination, fixing message synchronization issues (PR #9842 by @hannesrudolph)
|
||||
- Fix: Prevent cascading truncation loop by only truncating visible messages (PR #9844 by @hannesrudolph)
|
||||
- Fix: Handle unknown/invalid native tool calls to prevent extension freeze (PR #9834 by @daniel-lxs)
|
||||
- Always enable reasoning for models that require it (PR #9836 by @cte)
|
||||
- ChatView: Smoother stick-to-bottom behavior during streaming (PR #8999 by @hannesrudolph)
|
||||
- UX: Improved error messages and documentation links (PR #9777 by @brunobergher)
|
||||
- Fix: Overly round follow-up question suggestions styling (PR #9829 by @brunobergher)
|
||||
- Add symlink support for slash commands in .roo/commands folder (PR #9838 by @mrubens)
|
||||
- Ignore input to the execa terminal process for safer command execution (PR #9827 by @mrubens)
|
||||
- Be safer about large file reads (PR #9843 by @jr)
|
||||
- Add gpt-5.1-codex-max model to OpenAI provider (PR #9848 by @hannesrudolph)
|
||||
- Evals UI: Add filtering, bulk delete, tool consolidation, and run notes (PR #9837 by @hannesrudolph)
|
||||
- Evals UI: Add multi-model launch and UI improvements (PR #9845 by @hannesrudolph)
|
||||
- Web: New pricing page (PR #9821 by @brunobergher)
|
||||
|
||||
## [3.36.0] - 2025-12-04
|
||||
|
||||

|
||||
|
||||
- Fix: Restore context when rewinding after condense (#8295 by @hannesrudolph, PR #9665 by @hannesrudolph)
|
||||
- Add reasoning_details support to Roo provider for enhanced model reasoning visibility (PR #9796 by @app/roomote)
|
||||
- Default to native tools for all models in the Roo provider for improved performance (PR #9811 by @mrubens)
|
||||
- Enable search_and_replace for Minimax models (PR #9780 by @mrubens)
|
||||
- Fix: Resolve Vercel AI Gateway model fetching issues (PR #9791 by @cte)
|
||||
- Fix: Apply conservative max tokens for Cerebras provider (PR #9804 by @sebastiand-cerebras)
|
||||
- Fix: Remove omission detection logic to eliminate false positives (#9785 by @Michaelzag, PR #9787 by @app/roomote)
|
||||
- Refactor: Remove deprecated insert_content tool (PR #9751 by @daniel-lxs)
|
||||
- Chore: Hide parallel tool calls experiment and disable feature (PR #9798 by @hannesrudolph)
|
||||
- Update next.js documentation site dependencies (PR #9799 by @jr)
|
||||
- Fix: Correct download count display on homepage (PR #9807 by @mrubens)
|
||||
|
||||
## [3.35.5] - 2025-12-03
|
||||
|
||||
- Feat: Add provider routing selection for OpenRouter embeddings (#9144 by @SannidhyaSah, PR #9693 by @SannidhyaSah)
|
||||
- Default Minimax M2 to native tool calling (PR #9778 by @mrubens)
|
||||
- Sanitize the native tool calls to fix a bug with Gemini (PR #9769 by @mrubens)
|
||||
- UX: Updates to CloudView (PR #9776 by @roomote)
|
||||
|
||||
## [3.35.4] - 2025-12-02
|
||||
|
||||
- Fix: Handle malformed native tool calls to prevent hanging (PR #9758 by @daniel-lxs)
|
||||
- Fix: Remove reasoning toggles for GLM-4.5 and GLM-4.6 on z.ai provider (PR #9752 by @roomote)
|
||||
- Refactor: Remove line_count parameter from write_to_file tool (PR #9667 by @hannesrudolph)
|
||||
|
||||
## [3.35.3] - 2025-12-02
|
||||
|
||||
- Switch to new welcome view for improved onboarding experience (PR #9741 by @mrubens)
|
||||
- Update homepage with latest changes (PR #9675 by @brunobergher)
|
||||
- Improve privacy for stealth models by adding vendor confidentiality section to system prompt (PR #9742 by @mrubens)
|
||||
|
||||
## [3.35.2] - 2025-12-01
|
||||
|
||||

|
||||
|
||||
- Allow models to contain default temperature settings for provider-specific optimal defaults (PR #9734 by @mrubens)
|
||||
- Add tag-based native tool calling detection for Roo provider models (PR #9735 by @mrubens)
|
||||
- Enable native tool support for all LiteLLM models by default (PR #9736 by @mrubens)
|
||||
- Pass app version to provider for improved request tracking (PR #9730 by @cte)
|
||||
|
||||
## [3.35.1] - 2025-12-01
|
||||
|
||||
- Fix: Flush pending tool results before task delegation (PR #9726 by @daniel-lxs)
|
||||
- Improve: Better IPC error logging for easier debugging (PR #9727 by @cte)
|
||||
|
||||
## [3.35.0] - 2025-12-01
|
||||
|
||||

|
||||
|
||||
- Metadata-driven subtasks with automatic parent resume and single-open safety for improved task orchestration (#8081 by @hannesrudolph, PR #9090 by @hannesrudolph)
|
||||
- Native tool calling support expanded across many providers: Bedrock (PR #9698 by @mrubens), Cerebras (PR #9692 by @mrubens), Chutes with auto-detection from API (PR #9715 by @daniel-lxs), DeepInfra (PR #9691 by @mrubens), DeepSeek and Doubao (PR #9671 by @daniel-lxs), Groq (PR #9673 by @daniel-lxs), LiteLLM (PR #9719 by @daniel-lxs), Ollama (PR #9696 by @mrubens), OpenAI-compatible providers (PR #9676 by @daniel-lxs), Requesty (PR #9672 by @daniel-lxs), Unbound (PR #9699 by @mrubens), Vercel AI Gateway (PR #9697 by @mrubens), Vertex Gemini (PR #9678 by @daniel-lxs), and xAI with new Grok 4 Fast and Grok 4.1 Fast models (PR #9690 by @mrubens)
|
||||
- Fix: Preserve tool_use blocks in summary for parallel tool calls (#9700 by @SilentFlower, PR #9714 by @SilentFlower)
|
||||
- Default Grok Code Fast to native tools for better performance (PR #9717 by @mrubens)
|
||||
- UX improvements to the Roo Code Cloud provider-centric onboarding flow (PR #9709 by @brunobergher)
|
||||
- UX toolbar cleanup and settings consolidation for a cleaner interface (PR #9710 by @brunobergher)
|
||||
- Add model-specific tool customization via `excludedTools` and `includedTools` configuration (PR #9641 by @daniel-lxs)
|
||||
- Add new `apply_patch` native tool for more efficient file editing operations (PR #9663 by @hannesrudolph)
|
||||
- Add new `search_and_replace` tool for batch text replacements across files (PR #9549 by @hannesrudolph)
|
||||
- Add debug buttons to view API and UI history for troubleshooting (PR #9684 by @hannesrudolph)
|
||||
- Include tool format in environment details for better context awareness (PR #9661 by @mrubens)
|
||||
- Fix: Display install count in millions instead of thousands (PR #9677 by @app/roomote)
|
||||
- Web-evals improvements: add task log viewing, export failed logs, and new run options (PR #9637 by @hannesrudolph)
|
||||
- Web-evals updates: add kill run functionality (PR #9681 by @hannesrudolph)
|
||||
- Fix: Prevent navigation buttons from wrapping on smaller screens (PR #9721 by @app/roomote)
|
||||
|
||||
## [3.34.8] - 2025-11-27
|
||||
|
||||

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

|
||||
|
||||
- Support native tools in the Anthropic provider for improved tool calling (PR #9644 by @mrubens)
|
||||
- Enable native tool calling for z.ai models (PR #9645 by @mrubens)
|
||||
- Enable native tool calling for Moonshot models (PR #9646 by @mrubens)
|
||||
- Fix: OpenRouter tool calls handling improvements (PR #9642 by @mrubens)
|
||||
- Fix: OpenRouter GPT-5 strict schema validation for read_file tool (PR #9633 by @daniel-lxs)
|
||||
- Fix: Create parent directories early in write_to_file to prevent ENOENT errors (#9634 by @ivanenev, PR #9640 by @daniel-lxs)
|
||||
- Fix: Disable native tools and temperature support for claude-code provider (PR #9643 by @hannesrudolph)
|
||||
- Add 'taking you to cloud' screen after provider welcome for improved onboarding (PR #9652 by @mrubens)
|
||||
|
||||
## [3.34.6] - 2025-11-26
|
||||
|
||||

|
||||
|
||||
- Add support for AWS Bedrock embeddings in code indexing (#8658 by @kyle-hobbs, PR #9475 by @ggoranov-smar)
|
||||
- Add native tool calling support for Mistral provider (PR #9625 by @hannesrudolph)
|
||||
- Wire MULTIPLE_NATIVE_TOOL_CALLS experiment to OpenAI parallel_tool_calls for parallel tool execution (PR #9621 by @hannesrudolph)
|
||||
- Add fine grained tool streaming for OpenRouter Anthropic (PR #9629 by @mrubens)
|
||||
- Allow global inference selection for Bedrock when cross-region is enabled (PR #9616 by @roomote)
|
||||
- Fix: Filter non-Anthropic content blocks before sending to Vertex API (#9583 by @cardil, PR #9618 by @hannesrudolph)
|
||||
- Fix: Restore content undefined check in WriteToFileTool.handlePartial() (#9611 by @Lissanro, PR #9614 by @daniel-lxs)
|
||||
- Fix: Prevent model cache from persisting empty API responses (#9597 by @zx2021210538, PR #9623 by @daniel-lxs)
|
||||
- Fix: Exclude access_mcp_resource tool when MCP has no resources (PR #9615 by @daniel-lxs)
|
||||
- Fix: Update default settings for inline terminal and codebase indexing (PR #9622 by @roomote)
|
||||
- Fix: Convert line_ranges strings to lineRanges objects in native tool calls (PR #9627 by @daniel-lxs)
|
||||
- Fix: Defer new_task tool_result until subtask completes for native protocol (PR #9628 by @daniel-lxs)
|
||||
|
||||
## [3.34.5] - 2025-11-25
|
||||
|
||||

|
||||
|
|
@ -111,7 +427,7 @@
|
|||
|
||||
## [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)
|
||||
|
|
@ -374,7 +690,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
|
||||
|
|
@ -706,7 +1022,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
|
||||
|
|
@ -1071,7 +1387,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!)
|
||||
|
|
@ -1085,7 +1401,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
|
||||
|
||||
|
|
@ -1709,7 +2025,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
|
||||
|
|
@ -1958,12 +2274,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!)
|
||||
|
|
@ -2101,7 +2417,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
|
||||
|
||||
|
|
@ -2118,7 +2434,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
|
||||
|
|
@ -2302,7 +2618,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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -29,12 +29,13 @@
|
|||
"@roo-code/evals": "workspace:^",
|
||||
"@roo-code/types": "workspace:^",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"archiver": "^7.0.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.0",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"lucide-react": "^0.518.0",
|
||||
"next": "^15.2.5",
|
||||
"next": "~15.2.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"p-map": "^7.0.3",
|
||||
"react": "^18.3.1",
|
||||
|
|
@ -52,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,15 +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")
|
||||
|
||||
export async function createRun({ suite, exercises = [], timeout, ...values }: CreateRun) {
|
||||
export async function createRun({ suite, exercises = [], timeout, iterations = 1, ...values }: CreateRun) {
|
||||
const run = await _createRun({
|
||||
...values,
|
||||
timeout,
|
||||
|
|
@ -36,15 +43,34 @@ export async function createRun({ suite, exercises = [], timeout, ...values }: C
|
|||
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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
74
apps/web-evals/src/app/api/runs/[id]/logs/[taskId]/route.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as path from "node:path"
|
||||
|
||||
import { findTask, findRun } from "@roo-code/evals"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
const LOG_BASE_PATH = "/tmp/evals/runs"
|
||||
|
||||
// Sanitize path components to prevent path traversal attacks
|
||||
function sanitizePathComponent(component: string): string {
|
||||
// Remove any path separators, null bytes, and other dangerous characters
|
||||
return component.replace(/[/\\:\0*?"<>|]/g, "_")
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string; taskId: string }> }) {
|
||||
const { id, taskId } = await params
|
||||
|
||||
try {
|
||||
const runId = Number(id)
|
||||
const taskIdNum = Number(taskId)
|
||||
|
||||
if (isNaN(runId) || isNaN(taskIdNum)) {
|
||||
return NextResponse.json({ error: "Invalid run ID or task ID" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify the run exists
|
||||
await findRun(runId)
|
||||
|
||||
// Get the task to find its language and exercise
|
||||
const task = await findTask(taskIdNum)
|
||||
|
||||
// Verify the task belongs to this run
|
||||
if (task.runId !== runId) {
|
||||
return NextResponse.json({ error: "Task does not belong to this run" }, { status: 404 })
|
||||
}
|
||||
|
||||
// Sanitize language and exercise to prevent path traversal
|
||||
const safeLanguage = sanitizePathComponent(task.language)
|
||||
const safeExercise = sanitizePathComponent(task.exercise)
|
||||
|
||||
// Construct the log file path
|
||||
const logFileName = `${safeLanguage}-${safeExercise}.log`
|
||||
const logFilePath = path.join(LOG_BASE_PATH, String(runId), logFileName)
|
||||
|
||||
// Verify the resolved path is within the expected directory (defense in depth)
|
||||
const resolvedPath = path.resolve(logFilePath)
|
||||
const expectedBase = path.resolve(LOG_BASE_PATH)
|
||||
if (!resolvedPath.startsWith(expectedBase)) {
|
||||
return NextResponse.json({ error: "Invalid log path" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if the log file exists and read it (async)
|
||||
try {
|
||||
const logContent = await fs.readFile(logFilePath, "utf-8")
|
||||
return NextResponse.json({ logContent })
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return NextResponse.json({ error: "Log file not found", logContent: null }, { status: 200 })
|
||||
}
|
||||
throw err
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reading task log:", error)
|
||||
|
||||
if (error instanceof Error && error.name === "RecordNotFoundError") {
|
||||
return NextResponse.json({ error: "Task or run not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Failed to read log file" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
147
apps/web-evals/src/app/api/runs/[id]/logs/failed/route.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import archiver from "archiver"
|
||||
|
||||
import { findRun, getTasks } from "@roo-code/evals"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
const LOG_BASE_PATH = "/tmp/evals/runs"
|
||||
|
||||
// Sanitize path components to prevent path traversal attacks
|
||||
function sanitizePathComponent(component: string): string {
|
||||
// Remove any path separators, null bytes, and other dangerous characters
|
||||
return component.replace(/[/\\:\0*?"<>|]/g, "_")
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const runId = Number(id)
|
||||
|
||||
if (isNaN(runId)) {
|
||||
return NextResponse.json({ error: "Invalid run ID" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify the run exists
|
||||
await findRun(runId)
|
||||
|
||||
// Get all tasks for this run
|
||||
const tasks = await getTasks(runId)
|
||||
|
||||
// Filter for failed tasks only
|
||||
const failedTasks = tasks.filter((task) => task.passed === false)
|
||||
|
||||
if (failedTasks.length === 0) {
|
||||
return NextResponse.json({ error: "No failed tasks to export" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create a zip archive
|
||||
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||
|
||||
// Collect chunks to build the response
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
archive.on("data", (chunk: Buffer) => {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
|
||||
// Track archive errors
|
||||
let archiveError: Error | null = null
|
||||
archive.on("error", (err: Error) => {
|
||||
archiveError = err
|
||||
})
|
||||
|
||||
// Set up the end promise before finalizing (proper event listener ordering)
|
||||
const archiveEndPromise = new Promise<void>((resolve, reject) => {
|
||||
archive.on("end", resolve)
|
||||
archive.on("error", reject)
|
||||
})
|
||||
|
||||
// Add each failed task's log file and history files to the archive
|
||||
const logDir = path.join(LOG_BASE_PATH, String(runId))
|
||||
let filesAdded = 0
|
||||
|
||||
for (const task of failedTasks) {
|
||||
// Sanitize language and exercise to prevent path traversal
|
||||
const safeLanguage = sanitizePathComponent(task.language)
|
||||
const safeExercise = sanitizePathComponent(task.exercise)
|
||||
const expectedBase = path.resolve(LOG_BASE_PATH)
|
||||
|
||||
// Add the log file
|
||||
const logFileName = `${safeLanguage}-${safeExercise}.log`
|
||||
const logFilePath = path.join(logDir, logFileName)
|
||||
|
||||
// Verify the resolved path is within the expected directory (defense in depth)
|
||||
const resolvedLogPath = path.resolve(logFilePath)
|
||||
if (resolvedLogPath.startsWith(expectedBase) && fs.existsSync(logFilePath)) {
|
||||
archive.file(logFilePath, { name: logFileName })
|
||||
filesAdded++
|
||||
}
|
||||
|
||||
// Add the API conversation history file
|
||||
// Format: {language}-{exercise}.{iteration}_api_conversation_history.json
|
||||
const apiHistoryFileName = `${safeLanguage}-${safeExercise}.${task.iteration}_api_conversation_history.json`
|
||||
const apiHistoryFilePath = path.join(logDir, apiHistoryFileName)
|
||||
const resolvedApiHistoryPath = path.resolve(apiHistoryFilePath)
|
||||
if (resolvedApiHistoryPath.startsWith(expectedBase) && fs.existsSync(apiHistoryFilePath)) {
|
||||
archive.file(apiHistoryFilePath, { name: apiHistoryFileName })
|
||||
filesAdded++
|
||||
}
|
||||
|
||||
// Add the UI messages file
|
||||
// Format: {language}-{exercise}.{iteration}_ui_messages.json
|
||||
const uiMessagesFileName = `${safeLanguage}-${safeExercise}.${task.iteration}_ui_messages.json`
|
||||
const uiMessagesFilePath = path.join(logDir, uiMessagesFileName)
|
||||
const resolvedUiMessagesPath = path.resolve(uiMessagesFilePath)
|
||||
if (resolvedUiMessagesPath.startsWith(expectedBase) && fs.existsSync(uiMessagesFilePath)) {
|
||||
archive.file(uiMessagesFilePath, { name: uiMessagesFileName })
|
||||
filesAdded++
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any files were actually added
|
||||
if (filesAdded === 0) {
|
||||
archive.abort()
|
||||
return NextResponse.json(
|
||||
{ error: "No log files found - they may have been cleared from disk" },
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
// Finalize the archive
|
||||
await archive.finalize()
|
||||
|
||||
// Wait for all data to be collected
|
||||
await archiveEndPromise
|
||||
|
||||
// Check for archive errors
|
||||
if (archiveError) {
|
||||
throw archiveError
|
||||
}
|
||||
|
||||
// Combine all chunks into a single buffer
|
||||
const zipBuffer = Buffer.concat(chunks)
|
||||
|
||||
// Return the zip file
|
||||
return new NextResponse(zipBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="run-${runId}-failed-logs.zip"`,
|
||||
"Content-Length": String(zipBuffer.length),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error exporting failed logs:", error)
|
||||
|
||||
if (error instanceof Error && error.name === "RecordNotFoundError") {
|
||||
return NextResponse.json({ error: "Run not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Failed to export logs" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||
const run = await findRun(Number(id))
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-12 p-12">
|
||||
<div className="w-full px-6 py-12">
|
||||
<Run run={run} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,55 +1,79 @@
|
|||
"use client"
|
||||
|
||||
import { Link2, Link2Off, CheckCircle2 } from "lucide-react"
|
||||
import type { RunStatus as _RunStatus } from "@/hooks/use-run-status"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui"
|
||||
|
||||
export const RunStatus = ({ runStatus: { sseStatus, heartbeat, runners = [] } }: { runStatus: _RunStatus }) => (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>Task Stream:</div>
|
||||
<div className="font-mono text-sm text-muted-foreground">{sseStatus}</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
|
||||
"bg-green-500": sseStatus === "connected",
|
||||
"bg-amber-500": sseStatus === "waiting",
|
||||
"bg-rose-500": sseStatus === "error",
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={cn("size-2.5 rounded-full", {
|
||||
"bg-green-500": sseStatus === "connected",
|
||||
"bg-amber-500": sseStatus === "waiting",
|
||||
"bg-rose-500": sseStatus === "error",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>Task Controller:</div>
|
||||
<div className="font-mono text-sm text-muted-foreground">{heartbeat ?? "dead"}</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
|
||||
"bg-green-500": !!heartbeat,
|
||||
"bg-rose-500": !heartbeat,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={cn("size-2.5 rounded-full", {
|
||||
"bg-green-500": !!heartbeat,
|
||||
"bg-rose-500": !heartbeat,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div>Task Runners:</div>
|
||||
{runners.length > 0 && <div className="font-mono text-sm text-muted-foreground">{runners?.join(", ")}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
function StreamIcon({ status }: { status: "connected" | "waiting" | "error" }) {
|
||||
if (status === "connected") {
|
||||
return <Link2 className="size-4 text-green-500" />
|
||||
}
|
||||
return <Link2Off className={cn("size-4", status === "waiting" ? "text-amber-500" : "text-rose-500")} />
|
||||
}
|
||||
|
||||
export const RunStatus = ({
|
||||
runStatus: { sseStatus, heartbeat, runners = [] },
|
||||
isComplete = false,
|
||||
}: {
|
||||
runStatus: _RunStatus
|
||||
isComplete?: boolean
|
||||
}) => {
|
||||
// For completed runs, show a simple "Complete" badge
|
||||
if (isComplete) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1 cursor-default text-muted-foreground">
|
||||
<CheckCircle2 className="size-4" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="font-mono text-xs">
|
||||
Run complete
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2 cursor-default text-xs font-mono">
|
||||
{/* Task Stream status icon */}
|
||||
<StreamIcon status={sseStatus} />
|
||||
|
||||
{/* Task Controller ID */}
|
||||
<span className={heartbeat ? "text-green-500" : "text-rose-500"}>{heartbeat ?? "-"}</span>
|
||||
|
||||
{/* Task Runners count */}
|
||||
<span className={runners.length > 0 ? "text-green-500" : "text-rose-500"}>
|
||||
{runners.length > 0 ? `${runners.length}r` : "0r"}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="font-mono text-xs max-w-md">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<StreamIcon status={sseStatus} />
|
||||
<span>Task Stream: {sseStatus}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={heartbeat ? "text-green-500" : "text-rose-500"}>●</span>
|
||||
<span>Task Controller: {heartbeat ?? "dead"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={runners.length > 0 ? "text-green-500" : "text-rose-500"}>●</span>
|
||||
<span>Task Runners: {runners.length > 0 ? runners.length : "none"}</span>
|
||||
</div>
|
||||
{runners.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-border text-muted-foreground space-y-0.5">
|
||||
{runners.map((runner) => (
|
||||
<div key={runner}>{runner}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useQuery } from "@tanstack/react-query"
|
|||
import { useForm, FormProvider } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { toast } from "sonner"
|
||||
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal } from "lucide-react"
|
||||
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, Info, Plus, Minus } from "lucide-react"
|
||||
|
||||
import {
|
||||
globalSettingsSchema,
|
||||
|
|
@ -30,6 +30,9 @@ import {
|
|||
TIMEOUT_MIN,
|
||||
TIMEOUT_MAX,
|
||||
TIMEOUT_DEFAULT,
|
||||
ITERATIONS_MIN,
|
||||
ITERATIONS_MAX,
|
||||
ITERATIONS_DEFAULT,
|
||||
} from "@/lib/schemas"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
|
@ -61,7 +64,9 @@ import {
|
|||
PopoverTrigger,
|
||||
Slider,
|
||||
Label,
|
||||
FormDescription,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui"
|
||||
|
||||
import { SettingsDiff } from "./settings-diff"
|
||||
|
|
@ -72,17 +77,38 @@ type ImportedSettings = {
|
|||
currentApiConfigName: string
|
||||
}
|
||||
|
||||
// Type for a model selection entry
|
||||
type ModelSelection = {
|
||||
id: string
|
||||
model: string
|
||||
popoverOpen: boolean
|
||||
}
|
||||
|
||||
// Type for a config selection entry (for import mode)
|
||||
type ConfigSelection = {
|
||||
id: string
|
||||
configName: string
|
||||
popoverOpen: boolean
|
||||
}
|
||||
|
||||
export function NewRun() {
|
||||
const router = useRouter()
|
||||
|
||||
const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("roo")
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("other")
|
||||
const [useNativeToolProtocol, setUseNativeToolProtocol] = useState(true)
|
||||
const [commandExecutionTimeout, setCommandExecutionTimeout] = useState(20)
|
||||
const [terminalShellIntegrationTimeout, setTerminalShellIntegrationTimeout] = useState(30) // seconds
|
||||
|
||||
// State for imported settings with config selection
|
||||
// State for multiple model selections
|
||||
const [modelSelections, setModelSelections] = useState<ModelSelection[]>([
|
||||
{ id: crypto.randomUUID(), model: "", popoverOpen: false },
|
||||
])
|
||||
|
||||
// State for imported settings with multiple config selections
|
||||
const [importedSettings, setImportedSettings] = useState<ImportedSettings | null>(null)
|
||||
const [selectedConfigName, setSelectedConfigName] = useState<string>("")
|
||||
const [configPopoverOpen, setConfigPopoverOpen] = useState(false)
|
||||
const [configSelections, setConfigSelections] = useState<ConfigSelection[]>([
|
||||
{ id: crypto.randomUUID(), configName: "", popoverOpen: false },
|
||||
])
|
||||
|
||||
const openRouter = useOpenRouterModels()
|
||||
const rooCodeCloud = useRooCodeCloudModels()
|
||||
|
|
@ -106,6 +132,7 @@ export function NewRun() {
|
|||
settings: undefined,
|
||||
concurrency: CONCURRENCY_DEFAULT,
|
||||
timeout: TIMEOUT_DEFAULT,
|
||||
iterations: ITERATIONS_DEFAULT,
|
||||
jobToken: "",
|
||||
},
|
||||
})
|
||||
|
|
@ -117,9 +144,9 @@ export function NewRun() {
|
|||
formState: { isSubmitting },
|
||||
} = form
|
||||
|
||||
const [model, suite, settings] = watch(["model", "suite", "settings", "concurrency"])
|
||||
const [suite, settings] = watch(["suite", "settings", "concurrency"])
|
||||
|
||||
// Load concurrency and timeout from localStorage on mount
|
||||
// Load settings from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedConcurrency = localStorage.getItem("evals-concurrency")
|
||||
if (savedConcurrency) {
|
||||
|
|
@ -135,6 +162,37 @@ export function NewRun() {
|
|||
setValue("timeout", parsed)
|
||||
}
|
||||
}
|
||||
const savedCommandTimeout = localStorage.getItem("evals-command-execution-timeout")
|
||||
if (savedCommandTimeout) {
|
||||
const parsed = parseInt(savedCommandTimeout, 10)
|
||||
if (!isNaN(parsed) && parsed >= 20 && parsed <= 60) {
|
||||
setCommandExecutionTimeout(parsed)
|
||||
}
|
||||
}
|
||||
const savedShellTimeout = localStorage.getItem("evals-shell-integration-timeout")
|
||||
if (savedShellTimeout) {
|
||||
const parsed = parseInt(savedShellTimeout, 10)
|
||||
if (!isNaN(parsed) && parsed >= 30 && parsed <= 60) {
|
||||
setTerminalShellIntegrationTimeout(parsed)
|
||||
}
|
||||
}
|
||||
// Load saved exercises selection
|
||||
const savedSuite = localStorage.getItem("evals-suite")
|
||||
if (savedSuite === "partial") {
|
||||
setValue("suite", "partial")
|
||||
const savedExercises = localStorage.getItem("evals-exercises")
|
||||
if (savedExercises) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedExercises) as string[]
|
||||
if (Array.isArray(parsed)) {
|
||||
setSelectedExercises(parsed)
|
||||
setValue("exercises", parsed)
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [setValue])
|
||||
|
||||
// Extract unique languages from exercises
|
||||
|
|
@ -178,6 +236,7 @@ export function NewRun() {
|
|||
|
||||
setSelectedExercises(newSelected)
|
||||
setValue("exercises", newSelected)
|
||||
localStorage.setItem("evals-exercises", JSON.stringify(newSelected))
|
||||
},
|
||||
[getExercisesForLanguage, selectedExercises, setValue],
|
||||
)
|
||||
|
|
@ -201,40 +260,165 @@ export function NewRun() {
|
|||
[getExercisesForLanguage, selectedExercises],
|
||||
)
|
||||
|
||||
// Add a new model selection
|
||||
const addModelSelection = useCallback(() => {
|
||||
setModelSelections((prev) => [...prev, { id: crypto.randomUUID(), model: "", popoverOpen: false }])
|
||||
}, [])
|
||||
|
||||
// Remove a model selection
|
||||
const removeModelSelection = useCallback((id: string) => {
|
||||
setModelSelections((prev) => prev.filter((s) => s.id !== id))
|
||||
}, [])
|
||||
|
||||
// Update a model selection
|
||||
const updateModelSelection = useCallback(
|
||||
(id: string, model: string) => {
|
||||
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, model, popoverOpen: false } : s)))
|
||||
// Also set the form model field for validation (use first non-empty model)
|
||||
setValue("model", model)
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
|
||||
// Toggle popover for a model selection
|
||||
const toggleModelPopover = useCallback((id: string, open: boolean) => {
|
||||
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
|
||||
}, [])
|
||||
|
||||
// Add a new config selection
|
||||
const addConfigSelection = useCallback(() => {
|
||||
setConfigSelections((prev) => [...prev, { id: crypto.randomUUID(), configName: "", popoverOpen: false }])
|
||||
}, [])
|
||||
|
||||
// Remove a config selection
|
||||
const removeConfigSelection = useCallback((id: string) => {
|
||||
setConfigSelections((prev) => prev.filter((s) => s.id !== id))
|
||||
}, [])
|
||||
|
||||
// Update a config selection
|
||||
const updateConfigSelection = useCallback(
|
||||
(id: string, configName: string) => {
|
||||
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, configName, popoverOpen: false } : s)))
|
||||
// Also update the form settings for the first config (for validation)
|
||||
if (importedSettings) {
|
||||
const providerSettings = importedSettings.apiConfigs[configName] ?? {}
|
||||
setValue("model", getModelId(providerSettings) ?? "")
|
||||
setValue("settings", { ...EVALS_SETTINGS, ...providerSettings, ...importedSettings.globalSettings })
|
||||
}
|
||||
},
|
||||
[importedSettings, setValue],
|
||||
)
|
||||
|
||||
// Toggle popover for a config selection
|
||||
const toggleConfigPopover = useCallback((id: string, open: boolean) => {
|
||||
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
|
||||
}, [])
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: CreateRun) => {
|
||||
try {
|
||||
if (provider === "openrouter") {
|
||||
values.settings = {
|
||||
...(values.settings || {}),
|
||||
apiProvider: "openrouter",
|
||||
openRouterModelId: model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
// Validate jobToken for Roo Code Cloud provider
|
||||
if (provider === "roo" && !values.jobToken?.trim()) {
|
||||
toast.error("Roo Code Cloud Token is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which selections to use based on provider
|
||||
const selectionsToLaunch: { model: string; configName?: string }[] = []
|
||||
|
||||
if (provider === "other") {
|
||||
// For import mode, use config selections
|
||||
for (const config of configSelections) {
|
||||
if (config.configName) {
|
||||
selectionsToLaunch.push({ model: "", configName: config.configName })
|
||||
}
|
||||
}
|
||||
} else if (provider === "roo") {
|
||||
values.settings = {
|
||||
...(values.settings || {}),
|
||||
apiProvider: "roo",
|
||||
apiModelId: model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
} else {
|
||||
// For openrouter/roo, use model selections
|
||||
for (const selection of modelSelections) {
|
||||
if (selection.model) {
|
||||
selectionsToLaunch.push({ model: selection.model })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { id } = await createRun(values)
|
||||
router.push(`/runs/${id}`)
|
||||
if (selectionsToLaunch.length === 0) {
|
||||
toast.error("Please select at least one model or config")
|
||||
return
|
||||
}
|
||||
|
||||
// Show launching toast
|
||||
const totalRuns = selectionsToLaunch.length
|
||||
toast.info(totalRuns > 1 ? `Launching ${totalRuns} runs (every 20 seconds)...` : "Launching run...")
|
||||
|
||||
// Launch runs with 20-second delay between each
|
||||
for (let i = 0; i < selectionsToLaunch.length; i++) {
|
||||
const selection = selectionsToLaunch[i]!
|
||||
|
||||
// Wait 20 seconds between runs (except for the first one)
|
||||
if (i > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20000))
|
||||
}
|
||||
|
||||
const runValues = { ...values }
|
||||
|
||||
if (provider === "openrouter") {
|
||||
runValues.model = selection.model
|
||||
runValues.settings = {
|
||||
...(runValues.settings || {}),
|
||||
apiProvider: "openrouter",
|
||||
openRouterModelId: selection.model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
|
||||
}
|
||||
} else if (provider === "roo") {
|
||||
runValues.model = selection.model
|
||||
runValues.settings = {
|
||||
...(runValues.settings || {}),
|
||||
apiProvider: "roo",
|
||||
apiModelId: selection.model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
|
||||
}
|
||||
} else if (provider === "other" && selection.configName && importedSettings) {
|
||||
const providerSettings = importedSettings.apiConfigs[selection.configName] ?? {}
|
||||
runValues.model = getModelId(providerSettings) ?? ""
|
||||
runValues.settings = {
|
||||
...EVALS_SETTINGS,
|
||||
...providerSettings,
|
||||
...importedSettings.globalSettings,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await createRun(runValues)
|
||||
toast.success(`Run ${i + 1}/${totalRuns} launched`)
|
||||
} catch (e) {
|
||||
toast.error(`Run ${i + 1} failed: ${e instanceof Error ? e.message : "Unknown error"}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate back to main evals UI
|
||||
router.push("/")
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "An unknown error occurred.")
|
||||
}
|
||||
},
|
||||
[provider, model, router, useNativeToolProtocol],
|
||||
)
|
||||
|
||||
const onSelectModel = useCallback(
|
||||
(model: string) => {
|
||||
setValue("model", model)
|
||||
setModelPopoverOpen(false)
|
||||
},
|
||||
[setValue, setModelPopoverOpen],
|
||||
[
|
||||
provider,
|
||||
modelSelections,
|
||||
configSelections,
|
||||
importedSettings,
|
||||
router,
|
||||
useNativeToolProtocol,
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout,
|
||||
],
|
||||
)
|
||||
|
||||
const onImportSettings = useCallback(
|
||||
|
|
@ -265,9 +449,9 @@ export function NewRun() {
|
|||
currentApiConfigName: providerProfiles.currentApiConfigName,
|
||||
})
|
||||
|
||||
// Default to the current config
|
||||
// Default to the current config for the first selection
|
||||
const defaultConfigName = providerProfiles.currentApiConfigName
|
||||
setSelectedConfigName(defaultConfigName)
|
||||
setConfigSelections([{ id: crypto.randomUUID(), configName: defaultConfigName, popoverOpen: false }])
|
||||
|
||||
// Apply the default config
|
||||
const providerSettings = providerProfiles.apiConfigs[defaultConfigName] ?? {}
|
||||
|
|
@ -283,22 +467,6 @@ export function NewRun() {
|
|||
[clearErrors, setValue],
|
||||
)
|
||||
|
||||
const onSelectConfig = useCallback(
|
||||
(configName: string) => {
|
||||
if (!importedSettings) {
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedConfigName(configName)
|
||||
setConfigPopoverOpen(false)
|
||||
|
||||
const providerSettings = importedSettings.apiConfigs[configName] ?? {}
|
||||
setValue("model", getModelId(providerSettings) ?? "")
|
||||
setValue("settings", { ...EVALS_SETTINGS, ...providerSettings, ...importedSettings.globalSettings })
|
||||
},
|
||||
[importedSettings, setValue],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...form}>
|
||||
|
|
@ -314,9 +482,9 @@ export function NewRun() {
|
|||
value={provider}
|
||||
onValueChange={(value) => setModelSource(value as "roo" | "openrouter" | "other")}>
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="other">Import</TabsTrigger>
|
||||
<TabsTrigger value="roo">Roo Code Cloud</TabsTrigger>
|
||||
<TabsTrigger value="openrouter">OpenRouter</TabsTrigger>
|
||||
<TabsTrigger value="other">Other</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
|
|
@ -338,121 +506,217 @@ export function NewRun() {
|
|||
onChange={onImportSettings}
|
||||
/>
|
||||
|
||||
{importedSettings && Object.keys(importedSettings.apiConfigs).length > 1 && (
|
||||
<div className="space-y-1">
|
||||
<Label>API Config</Label>
|
||||
<Popover open={configPopoverOpen} onOpenChange={setConfigPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="input"
|
||||
role="combobox"
|
||||
aria-expanded={configPopoverOpen}
|
||||
className="flex items-center justify-between w-full">
|
||||
<div>{selectedConfigName || "Select config"}</div>
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search configs..."
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No config found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{Object.keys(importedSettings.apiConfigs).map(
|
||||
(configName) => (
|
||||
<CommandItem
|
||||
key={configName}
|
||||
value={configName}
|
||||
onSelect={onSelectConfig}>
|
||||
{configName}
|
||||
{configName ===
|
||||
importedSettings.currentApiConfigName && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
(default)
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto size-4",
|
||||
configName ===
|
||||
selectedConfigName
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
{importedSettings && Object.keys(importedSettings.apiConfigs).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>API Configs</Label>
|
||||
{configSelections.map((selection, index) => (
|
||||
<div key={selection.id} className="flex items-center gap-2">
|
||||
<Popover
|
||||
open={selection.popoverOpen}
|
||||
onOpenChange={(open) =>
|
||||
toggleConfigPopover(selection.id, open)
|
||||
}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="input"
|
||||
role="combobox"
|
||||
aria-expanded={selection.popoverOpen}
|
||||
className="flex items-center justify-between flex-1">
|
||||
<div>{selection.configName || "Select config"}</div>
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search configs..."
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No config found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{Object.keys(
|
||||
importedSettings.apiConfigs,
|
||||
).map((configName) => (
|
||||
<CommandItem
|
||||
key={configName}
|
||||
value={configName}
|
||||
onSelect={() =>
|
||||
updateConfigSelection(
|
||||
selection.id,
|
||||
configName,
|
||||
)
|
||||
}>
|
||||
{configName}
|
||||
{configName ===
|
||||
importedSettings.currentApiConfigName && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
(default)
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto size-4",
|
||||
configName ===
|
||||
selection.configName
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{index === configSelections.length - 1 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={addConfigSelection}
|
||||
className="shrink-0">
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => removeConfigSelection(selection.id)}
|
||||
className="shrink-0">
|
||||
<Minus className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-3">
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Tool Protocol Options
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2.5 pl-1">
|
||||
<label
|
||||
htmlFor="native-other"
|
||||
className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
id="native-other"
|
||||
checked={useNativeToolProtocol}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setUseNativeToolProtocol(checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">Use Native Tool Calls</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settings && (
|
||||
<SettingsDiff defaultSettings={EVALS_SETTINGS} customSettings={settings} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Popover open={modelPopoverOpen} onOpenChange={setModelPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="input"
|
||||
role="combobox"
|
||||
aria-expanded={modelPopoverOpen}
|
||||
className="flex items-center justify-between">
|
||||
<div>
|
||||
{models?.find(({ id }) => id === model)?.name || `Select`}
|
||||
</div>
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
|
||||
<Command filter={onFilter}>
|
||||
<CommandInput
|
||||
placeholder="Search"
|
||||
value={searchValue}
|
||||
onValueChange={setSearchValue}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{models?.map(({ id, name }) => (
|
||||
<CommandItem
|
||||
key={id}
|
||||
value={id}
|
||||
onSelect={onSelectModel}>
|
||||
{name}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto text-accent group-data-[selected=true]:text-accent-foreground size-4",
|
||||
id === model ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="space-y-2">
|
||||
{modelSelections.map((selection, index) => (
|
||||
<div key={selection.id} className="flex items-center gap-2">
|
||||
<Popover
|
||||
open={selection.popoverOpen}
|
||||
onOpenChange={(open) => toggleModelPopover(selection.id, open)}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="input"
|
||||
role="combobox"
|
||||
aria-expanded={selection.popoverOpen}
|
||||
className="flex items-center justify-between flex-1">
|
||||
<div>
|
||||
{models?.find(({ id }) => id === selection.model)
|
||||
?.name || `Select`}
|
||||
</div>
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
|
||||
<Command filter={onFilter}>
|
||||
<CommandInput
|
||||
placeholder="Search"
|
||||
value={searchValue}
|
||||
onValueChange={setSearchValue}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{models?.map(({ id, name }) => (
|
||||
<CommandItem
|
||||
key={id}
|
||||
value={id}
|
||||
onSelect={() =>
|
||||
updateModelSelection(
|
||||
selection.id,
|
||||
id,
|
||||
)
|
||||
}>
|
||||
{name}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto text-accent group-data-[selected=true]:text-accent-foreground size-4",
|
||||
id === selection.model
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{index === modelSelections.length - 1 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={addModelSelection}
|
||||
className="shrink-0">
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => removeModelSelection(selection.id)}
|
||||
className="shrink-0">
|
||||
<Minus className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Checkbox
|
||||
id="native"
|
||||
checked={useNativeToolProtocol}
|
||||
onCheckedChange={(checked) =>
|
||||
setUseNativeToolProtocol(checked === true)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="native">Use Native Tool Calls</Label>
|
||||
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-3">
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Tool Protocol Options
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2.5 pl-1">
|
||||
<label
|
||||
htmlFor="native"
|
||||
className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
id="native"
|
||||
checked={useNativeToolProtocol}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setUseNativeToolProtocol(checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">Use Native Tool Calls</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -468,20 +732,28 @@ export function NewRun() {
|
|||
name="jobToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Roo Code Cloud Token</FormLabel>
|
||||
<div className="flex items-center gap-1">
|
||||
<FormLabel>Roo Code Cloud Token</FormLabel>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="size-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p>
|
||||
If you have access to the Roo Code Cloud repository and the
|
||||
decryption key for the .env.* files, generate a token with:
|
||||
</p>
|
||||
<code className="text-xs block mt-1">
|
||||
pnpm --filter @roo-code-cloud/auth production:create-auth-token
|
||||
[email] [org] [ttl]
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
<Input type="password" placeholder="Required" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
If you have access to the Roo Code Cloud repository then you can generate a
|
||||
token with:
|
||||
<br />
|
||||
<code className="text-xs">
|
||||
pnpm --filter @roo-code-cloud/auth production:create-job-token [org]
|
||||
[timeout]
|
||||
</code>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -495,12 +767,14 @@ export function NewRun() {
|
|||
<FormLabel>Exercises</FormLabel>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Tabs
|
||||
defaultValue="full"
|
||||
value={suite}
|
||||
onValueChange={(value) => {
|
||||
setValue("suite", value as "full" | "partial")
|
||||
localStorage.setItem("evals-suite", value)
|
||||
if (value === "full") {
|
||||
setSelectedExercises([])
|
||||
setValue("exercises", [])
|
||||
localStorage.removeItem("evals-exercises")
|
||||
}
|
||||
}}>
|
||||
<TabsList>
|
||||
|
|
@ -537,6 +811,7 @@ export function NewRun() {
|
|||
onValueChange={(value) => {
|
||||
setSelectedExercises(value)
|
||||
setValue("exercises", value)
|
||||
localStorage.setItem("evals-exercises", JSON.stringify(value))
|
||||
}}
|
||||
placeholder="Select"
|
||||
variant="inverted"
|
||||
|
|
@ -548,57 +823,153 @@ export function NewRun() {
|
|||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="concurrency"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Concurrency</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
value={[field.value]}
|
||||
min={CONCURRENCY_MIN}
|
||||
max={CONCURRENCY_MAX}
|
||||
step={1}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0])
|
||||
localStorage.setItem("evals-concurrency", String(value[0]))
|
||||
}}
|
||||
/>
|
||||
<div>{field.value}</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Concurrency, Timeout, and Iterations in a 3-column row */}
|
||||
<div className="grid grid-cols-3 gap-4 py-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="concurrency"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Concurrency</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
value={[field.value]}
|
||||
min={CONCURRENCY_MIN}
|
||||
max={CONCURRENCY_MAX}
|
||||
step={1}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0])
|
||||
localStorage.setItem("evals-concurrency", String(value[0]))
|
||||
}}
|
||||
/>
|
||||
<div className="w-6 text-right">{field.value}</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Timeout (Minutes)</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
value={[field.value]}
|
||||
min={TIMEOUT_MIN}
|
||||
max={TIMEOUT_MAX}
|
||||
step={1}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0])
|
||||
localStorage.setItem("evals-timeout", String(value[0]))
|
||||
}}
|
||||
/>
|
||||
<div>{field.value}</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Timeout (Minutes)</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
value={[field.value]}
|
||||
min={TIMEOUT_MIN}
|
||||
max={TIMEOUT_MAX}
|
||||
step={1}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0])
|
||||
localStorage.setItem("evals-timeout", String(value[0]))
|
||||
}}
|
||||
/>
|
||||
<div className="w-6 text-right">{field.value}</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="iterations"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Iterations</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
value={[field.value]}
|
||||
min={ITERATIONS_MIN}
|
||||
max={ITERATIONS_MAX}
|
||||
step={1}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0])
|
||||
}}
|
||||
/>
|
||||
<div className="w-6 text-right">{field.value}</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Terminal timeouts in a 2-column row */}
|
||||
<div className="grid grid-cols-2 gap-4 py-5">
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-1">
|
||||
<Label>Command Timeout (Seconds)</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="size-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p>
|
||||
Maximum time in seconds to wait for terminal command execution to complete
|
||||
before timing out. This applies to commands run via the execute_command
|
||||
tool.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
value={[commandExecutionTimeout]}
|
||||
min={20}
|
||||
max={60}
|
||||
step={1}
|
||||
onValueChange={([value]) => {
|
||||
if (value !== undefined) {
|
||||
setCommandExecutionTimeout(value)
|
||||
localStorage.setItem("evals-command-execution-timeout", String(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="w-8 text-right">{commandExecutionTimeout}</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-1">
|
||||
<Label>Shell Integration Timeout (Seconds)</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="size-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p>
|
||||
Maximum time in seconds to wait for shell integration to initialize when
|
||||
opening a new terminal.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
value={[terminalShellIntegrationTimeout]}
|
||||
min={30}
|
||||
max={60}
|
||||
step={1}
|
||||
onValueChange={([value]) => {
|
||||
if (value !== undefined) {
|
||||
setTerminalShellIntegrationTimeout(value)
|
||||
localStorage.setItem("evals-shell-integration-timeout", String(value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="w-8 text-right">{terminalShellIntegrationTimeout}</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import { type Keys, type RooCodeSettings, GLOBAL_SETTINGS_KEYS, PROVIDER_SETTING
|
|||
|
||||
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 = {
|
||||
defaultSettings: RooCodeSettings
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useCallback, useState, useRef } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Ellipsis, ClipboardList, Copy, Check, LoaderCircle, Trash, Settings } from "lucide-react"
|
||||
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 { deleteRun, updateRunDescription } from "@/actions/runs"
|
||||
import {
|
||||
formatCurrency,
|
||||
formatDateTime,
|
||||
|
|
@ -19,6 +20,10 @@ import {
|
|||
Button,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
|
|
@ -33,24 +38,95 @@ import {
|
|||
AlertDialogTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
ScrollArea,
|
||||
} from "@/components/ui"
|
||||
|
||||
// Tool group type (same as in runs.tsx)
|
||||
type ToolGroup = {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
tools: string[]
|
||||
}
|
||||
|
||||
type RunProps = {
|
||||
run: EvalsRun
|
||||
taskMetrics: EvalsTaskMetrics | null
|
||||
toolColumns: ToolName[]
|
||||
toolGroups: ToolGroup[]
|
||||
}
|
||||
|
||||
export function Run({ run, taskMetrics, toolColumns }: RunProps) {
|
||||
export function Run({ run, taskMetrics, toolColumns, toolGroups }: RunProps) {
|
||||
const router = useRouter()
|
||||
const [deleteRunId, setDeleteRunId] = useState<number>()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [isExportingLogs, setIsExportingLogs] = useState(false)
|
||||
const [showNotesDialog, setShowNotesDialog] = useState(false)
|
||||
const [editingDescription, setEditingDescription] = useState(run.description ?? "")
|
||||
const [isSavingNotes, setIsSavingNotes] = useState(false)
|
||||
const continueRef = useRef<HTMLButtonElement>(null)
|
||||
const { isPending, copyRun, copied } = useCopyRun(run.id)
|
||||
|
||||
const hasDescription = Boolean(run.description && run.description.trim().length > 0)
|
||||
|
||||
const handleSaveDescription = useCallback(async () => {
|
||||
setIsSavingNotes(true)
|
||||
try {
|
||||
const result = await updateRunDescription(run.id, editingDescription.trim() || null)
|
||||
if (result.success) {
|
||||
toast.success("Description saved")
|
||||
setShowNotesDialog(false)
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error("Failed to save description")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving description:", error)
|
||||
toast.error("Failed to save description")
|
||||
} finally {
|
||||
setIsSavingNotes(false)
|
||||
}
|
||||
}, [run.id, editingDescription, router])
|
||||
|
||||
const onExportFailedLogs = useCallback(async () => {
|
||||
if (run.failed === 0) {
|
||||
toast.error("No failed tasks to export")
|
||||
return
|
||||
}
|
||||
|
||||
setIsExportingLogs(true)
|
||||
try {
|
||||
const response = await fetch(`/api/runs/${run.id}/logs/failed`)
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
toast.error(error.error || "Failed to export logs")
|
||||
return
|
||||
}
|
||||
|
||||
// Download the zip file
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `run-${run.id}-failed-logs.zip`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
toast.success("Failed logs exported successfully")
|
||||
} catch (error) {
|
||||
console.error("Error exporting logs:", error)
|
||||
toast.error("Failed to export logs")
|
||||
} finally {
|
||||
setIsExportingLogs(false)
|
||||
}
|
||||
}, [run.id, run.failed])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
if (!deleteRunId) {
|
||||
return
|
||||
|
|
@ -75,6 +151,62 @@ export function Run({ run, taskMetrics, toolColumns }: RunProps) {
|
|||
[router, run.id],
|
||||
)
|
||||
|
||||
// Helper to render a tool group cell
|
||||
const renderToolGroupCell = (group: ToolGroup) => {
|
||||
if (!taskMetrics?.toolUsage) {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
let totalAttempts = 0
|
||||
let totalFailures = 0
|
||||
const breakdown: Array<{ tool: string; attempts: number; rate: string }> = []
|
||||
|
||||
for (const toolName of group.tools) {
|
||||
const usage = taskMetrics.toolUsage[toolName as ToolName]
|
||||
if (usage) {
|
||||
totalAttempts += usage.attempts
|
||||
totalFailures += usage.failures
|
||||
const rate =
|
||||
usage.attempts > 0
|
||||
? `${Math.round(((usage.attempts - usage.failures) / usage.attempts) * 100)}%`
|
||||
: "0%"
|
||||
breakdown.push({ tool: toolName, attempts: usage.attempts, rate })
|
||||
}
|
||||
}
|
||||
|
||||
if (totalAttempts === 0) {
|
||||
return <span className="text-muted-foreground">-</span>
|
||||
}
|
||||
|
||||
const successRate = ((totalAttempts - totalFailures) / totalAttempts) * 100
|
||||
const rateColor =
|
||||
successRate === 100 ? "text-muted-foreground" : successRate >= 80 ? "text-yellow-500" : "text-red-500"
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">{totalAttempts}</span>
|
||||
<span className={rateColor}>{Math.round(successRate)}%</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-xs">
|
||||
<div className="font-semibold mb-1">{group.name}</div>
|
||||
{breakdown.map(({ tool, attempts, rate }) => (
|
||||
<div key={tool} className="flex justify-between gap-4">
|
||||
<span>{tool}:</span>
|
||||
<span>
|
||||
{attempts} ({rate})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow className="cursor-pointer hover:bg-muted/50" onClick={handleRowClick}>
|
||||
|
|
@ -86,9 +218,13 @@ export function Run({ run, taskMetrics, toolColumns }: RunProps) {
|
|||
<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 && (
|
||||
|
|
@ -98,14 +234,28 @@ export function Run({ run, taskMetrics, toolColumns }: RunProps) {
|
|||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* Tool Group Columns */}
|
||||
{toolGroups.map((group) => (
|
||||
<TableCell key={group.id} className="text-xs text-center">
|
||||
{renderToolGroupCell(group)}
|
||||
</TableCell>
|
||||
))}
|
||||
{toolColumns.map((toolName) => {
|
||||
const usage = taskMetrics?.toolUsage?.[toolName]
|
||||
const successRate =
|
||||
usage && usage.attempts > 0 ? ((usage.attempts - usage.failures) / usage.attempts) * 100 : 100
|
||||
const rateColor =
|
||||
successRate === 100
|
||||
? "text-muted-foreground"
|
||||
: successRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
return (
|
||||
<TableCell key={toolName} className="text-xs text-center">
|
||||
{usage ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">{usage.attempts}</span>
|
||||
<span className="text-muted-foreground">{formatToolUsageSuccessRate(usage)}</span>
|
||||
<span className={rateColor}>{formatToolUsageSuccessRate(usage)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
|
|
@ -116,63 +266,107 @@ export function Run({ run, taskMetrics, toolColumns }: RunProps) {
|
|||
<TableCell>{taskMetrics && formatCurrency(taskMetrics.cost)}</TableCell>
|
||||
<TableCell>{taskMetrics && formatDuration(taskMetrics.duration)}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<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">
|
||||
{/* 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.settings && (
|
||||
<DropdownMenuItem onClick={() => setShowSettings(true)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Settings />
|
||||
<div>View Settings</div>
|
||||
<Trash />
|
||||
<div>Delete</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>
|
||||
)}
|
||||
<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)}>
|
||||
|
|
@ -201,6 +395,39 @@ export function Run({ run, taskMetrics, toolColumns }: RunProps) {
|
|||
</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,14 +1,82 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState, memo } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Rocket } from "lucide-react"
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ArrowUpDown,
|
||||
Box,
|
||||
Boxes,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CircleDot,
|
||||
ClipboardList,
|
||||
Cog,
|
||||
Combine,
|
||||
Ellipsis,
|
||||
File,
|
||||
FileText,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Hammer,
|
||||
Hexagon,
|
||||
Layers,
|
||||
List,
|
||||
ListChecks,
|
||||
ListTodo,
|
||||
LoaderCircle,
|
||||
Package,
|
||||
Pencil,
|
||||
PencilLine,
|
||||
Plus,
|
||||
Rocket,
|
||||
Search,
|
||||
Settings2,
|
||||
Shapes,
|
||||
Square,
|
||||
Star,
|
||||
Tag,
|
||||
Terminal,
|
||||
Trash2,
|
||||
Wrench,
|
||||
X,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import type { Run, TaskMetrics } from "@roo-code/evals"
|
||||
import type { ToolName } from "@roo-code/types"
|
||||
|
||||
import { deleteIncompleteRuns, deleteOldRuns } from "@/actions/runs"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Input,
|
||||
MultiSelect,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
|
|
@ -21,11 +89,206 @@ import {
|
|||
} from "@/components/ui"
|
||||
import { Run as Row } from "@/components/home/run"
|
||||
|
||||
// Available icons for tool groups
|
||||
const TOOL_GROUP_ICONS: { name: string; icon: LucideIcon }[] = [
|
||||
{ name: "combine", icon: Combine },
|
||||
{ name: "layers", icon: Layers },
|
||||
{ name: "box", icon: Box },
|
||||
{ name: "boxes", icon: Boxes },
|
||||
{ name: "package", icon: Package },
|
||||
{ name: "folder", icon: Folder },
|
||||
{ name: "folder-open", icon: FolderOpen },
|
||||
{ name: "file", icon: File },
|
||||
{ name: "file-text", icon: FileText },
|
||||
{ name: "list", icon: List },
|
||||
{ name: "list-todo", icon: ListTodo },
|
||||
{ name: "list-checks", icon: ListChecks },
|
||||
{ name: "clipboard-list", icon: ClipboardList },
|
||||
{ name: "check", icon: Check },
|
||||
{ name: "check-circle", icon: CheckCircle },
|
||||
{ name: "pencil", icon: PencilLine },
|
||||
{ name: "trash", icon: Trash2 },
|
||||
{ name: "x", icon: X },
|
||||
{ name: "search", icon: Search },
|
||||
{ name: "terminal", icon: Terminal },
|
||||
{ name: "shapes", icon: Shapes },
|
||||
{ name: "hexagon", icon: Hexagon },
|
||||
{ name: "square", icon: Square },
|
||||
{ name: "circle-dot", icon: CircleDot },
|
||||
{ name: "star", icon: Star },
|
||||
{ name: "zap", icon: Zap },
|
||||
{ name: "hammer", icon: Hammer },
|
||||
{ name: "wrench", icon: Wrench },
|
||||
{ name: "cog", icon: Cog },
|
||||
{ name: "settings", icon: Settings2 },
|
||||
{ name: "tag", icon: Tag },
|
||||
]
|
||||
|
||||
// Tool group type
|
||||
export type ToolGroup = {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
tools: string[]
|
||||
}
|
||||
|
||||
// Helper to get icon component by name
|
||||
function getIconByName(name: string): LucideIcon {
|
||||
return TOOL_GROUP_ICONS.find((i) => i.name === name)?.icon ?? Combine
|
||||
}
|
||||
|
||||
// Generate a unique ID for tool groups
|
||||
function generateGroupId(): string {
|
||||
return `group-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
||||
}
|
||||
|
||||
// Isolated dialog component to prevent parent re-renders on state changes
|
||||
const ToolGroupEditorDialog = memo(function ToolGroupEditorDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
editingGroup,
|
||||
availableTools,
|
||||
onSave,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
editingGroup: ToolGroup | null
|
||||
availableTools: { label: string; value: string }[]
|
||||
onSave: (group: ToolGroup) => void
|
||||
}) {
|
||||
const [groupName, setGroupName] = useState(editingGroup?.name ?? "")
|
||||
const [groupIcon, setGroupIcon] = useState(editingGroup?.icon ?? "combine")
|
||||
const [groupTools, setGroupTools] = useState<string[]>(editingGroup?.tools ?? [])
|
||||
|
||||
// Reset form when dialog opens or editingGroup changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setGroupName(editingGroup?.name ?? "")
|
||||
setGroupIcon(editingGroup?.icon ?? "combine")
|
||||
setGroupTools(editingGroup?.tools ?? [])
|
||||
}
|
||||
}, [open, editingGroup])
|
||||
|
||||
const canSaveGroup = groupName.trim().length > 0 && groupTools.length > 0
|
||||
|
||||
const handleSave = () => {
|
||||
if (!canSaveGroup) return
|
||||
const group: ToolGroup = {
|
||||
id: editingGroup?.id ?? generateGroupId(),
|
||||
name: groupName.trim(),
|
||||
icon: groupIcon,
|
||||
tools: groupTools,
|
||||
}
|
||||
onSave(group)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingGroup ? "Edit Tool Group" : "Create Tool Group"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Group Name <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g., File Operations"
|
||||
value={groupName}
|
||||
onChange={(e) => setGroupName(e.target.value)}
|
||||
className={!groupName.trim() ? "border-muted-foreground/30" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Icon</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{TOOL_GROUP_ICONS.map(({ name, icon: IconComponent }) => (
|
||||
<Button
|
||||
key={name}
|
||||
variant={groupIcon === name ? "default" : "outline"}
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setGroupIcon(name)}>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Tools <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<MultiSelect
|
||||
options={availableTools}
|
||||
value={groupTools}
|
||||
onValueChange={setGroupTools}
|
||||
placeholder="Select tools..."
|
||||
className="w-full"
|
||||
maxCount={3}
|
||||
modalPopover
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{groupTools.length > 0
|
||||
? `${groupTools.length} tool${groupTools.length !== 1 ? "s" : ""} selected`
|
||||
: "Select at least one tool"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!canSaveGroup}>
|
||||
{editingGroup ? "Save Changes" : "Create Group"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
||||
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",
|
||||
TOOL_GROUPS: "evals-runs-tool-groups",
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -54,6 +317,120 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
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 groups state - initialize from localStorage
|
||||
const [toolGroups, setToolGroups] = useState<ToolGroup[]>(() => {
|
||||
if (typeof window === "undefined") return []
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.TOOL_GROUPS)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// Tool group editor dialog state
|
||||
const [showGroupDialog, setShowGroupDialog] = useState(false)
|
||||
const [editingGroup, setEditingGroup] = useState<ToolGroup | null>(null)
|
||||
|
||||
// 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.TOOL_GROUPS, JSON.stringify(toolGroups))
|
||||
}, [toolGroups])
|
||||
|
||||
// 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")
|
||||
|
|
@ -63,11 +440,59 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
}
|
||||
}
|
||||
|
||||
// Collect all unique tool names from all runs and sort by total attempts
|
||||
const toolColumns = useMemo<ToolName[]>(() => {
|
||||
// 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 runs) {
|
||||
for (const run of filteredRuns) {
|
||||
if (run.taskMetrics?.toolUsage) {
|
||||
for (const [toolName, usage] of Object.entries(run.taskMetrics.toolUsage)) {
|
||||
const tool = toolName as ToolName
|
||||
|
|
@ -81,13 +506,40 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
return Array.from(toolTotals.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([name]): ToolName => name)
|
||||
}, [runs])
|
||||
}, [filteredRuns])
|
||||
|
||||
// Sort runs based on current sort column and direction
|
||||
// Tool column options for the group editor
|
||||
const toolColumnOptions = useMemo(() => {
|
||||
return allToolColumns.map((tool) => ({
|
||||
label: tool,
|
||||
value: tool,
|
||||
}))
|
||||
}, [allToolColumns])
|
||||
|
||||
// Get all tools that are in any group
|
||||
const groupedTools = useMemo(() => {
|
||||
const grouped = new Set<string>()
|
||||
for (const group of toolGroups) {
|
||||
for (const tool of group.tools) {
|
||||
grouped.add(tool)
|
||||
}
|
||||
}
|
||||
return grouped
|
||||
}, [toolGroups])
|
||||
|
||||
// Separate grouped and individual tool columns
|
||||
const individualToolColumns = useMemo(() => {
|
||||
return allToolColumns.filter((tool) => !groupedTools.has(tool))
|
||||
}, [allToolColumns, groupedTools])
|
||||
|
||||
// Use individualToolColumns for rendering
|
||||
const toolColumns = individualToolColumns
|
||||
|
||||
// Sort filtered runs based on current sort column and direction
|
||||
const sortedRuns = useMemo(() => {
|
||||
if (!sortColumn) return runs
|
||||
if (!sortColumn) return filteredRuns
|
||||
|
||||
return [...runs].sort((a, b) => {
|
||||
return [...filteredRuns].sort((a, b) => {
|
||||
let aVal: string | number | Date | null = null
|
||||
let bVal: string | number | Date | null = null
|
||||
|
||||
|
|
@ -139,14 +591,231 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
|
||||
return sortDirection === "asc" ? comparison : -comparison
|
||||
})
|
||||
}, [runs, sortColumn, sortDirection])
|
||||
}, [filteredRuns, sortColumn, sortDirection])
|
||||
|
||||
// Calculate colSpan for empty state (7 base columns + dynamic tools + 3 end columns)
|
||||
const totalColumns = 7 + toolColumns.length + 3
|
||||
// Calculate colSpan for empty state (7 base columns + tool groups + dynamic tools + 3 end columns)
|
||||
const totalColumns = 7 + toolGroups.length + toolColumns.length + 3
|
||||
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters = timeframeFilter !== "all" || modelFilter.length > 0 || providerFilter.length > 0
|
||||
|
||||
const clearAllFilters = () => {
|
||||
setTimeframeFilter("all")
|
||||
setModelFilter([])
|
||||
setProviderFilter([])
|
||||
}
|
||||
|
||||
// Tool group management handlers
|
||||
const openNewGroupDialog = useCallback(() => {
|
||||
setEditingGroup(null)
|
||||
setShowGroupDialog(true)
|
||||
}, [])
|
||||
|
||||
const openEditGroupDialog = useCallback((group: ToolGroup) => {
|
||||
setEditingGroup(group)
|
||||
setShowGroupDialog(true)
|
||||
}, [])
|
||||
|
||||
const handleSaveGroup = useCallback(
|
||||
(group: ToolGroup) => {
|
||||
setToolGroups((prev) => {
|
||||
const existingIndex = prev.findIndex((g) => g.id === group.id)
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing group
|
||||
const newGroups = [...prev]
|
||||
newGroups[existingIndex] = group
|
||||
return newGroups
|
||||
} else {
|
||||
// Add new group
|
||||
return [...prev, group]
|
||||
}
|
||||
})
|
||||
toast.success(editingGroup ? "Group updated" : "Group created")
|
||||
},
|
||||
[editingGroup],
|
||||
)
|
||||
|
||||
const handleDeleteGroup = useCallback((groupId: string) => {
|
||||
setToolGroups((prev) => prev.filter((g) => g.id !== groupId))
|
||||
toast.success("Group deleted")
|
||||
}, [])
|
||||
|
||||
// Get available tools for group editor (tools not in other groups)
|
||||
const availableToolsForEditor = useMemo(() => {
|
||||
const usedInOtherGroups = new Set<string>()
|
||||
for (const group of toolGroups) {
|
||||
if (editingGroup && group.id === editingGroup.id) continue
|
||||
for (const tool of group.tools) {
|
||||
usedInOtherGroups.add(tool)
|
||||
}
|
||||
}
|
||||
return toolColumnOptions.filter((opt) => !usedInOtherGroups.has(opt.value))
|
||||
}, [toolColumnOptions, toolGroups, editingGroup])
|
||||
|
||||
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>
|
||||
|
||||
{/* Tool Groups Dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4" />
|
||||
<span>Groups</span>
|
||||
{toolGroups.length > 0 && (
|
||||
<span className="bg-primary text-primary-foreground text-xs px-1.5 rounded-full">
|
||||
{toolGroups.length}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
{toolGroups.length > 0 ? (
|
||||
<>
|
||||
{toolGroups.map((group) => {
|
||||
const IconComponent = getIconByName(group.icon)
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={group.id}
|
||||
className="flex items-center justify-between"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openEditGroupDialog(group)
|
||||
}}>
|
||||
<div className="flex items-center gap-2">
|
||||
<IconComponent className="h-4 w-4" />
|
||||
<span>{group.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({group.tools.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
openEditGroupDialog(group)
|
||||
}}>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-destructive hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteGroup(group.id)
|
||||
}}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
) : (
|
||||
<div className="px-2 py-1.5 text-sm text-muted-foreground">No groups yet</div>
|
||||
)}
|
||||
<DropdownMenuItem onClick={openNewGroupDialog}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Group
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</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
|
||||
|
|
@ -188,6 +857,30 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
</div>
|
||||
</TableHead>
|
||||
<TableHead>Tokens</TableHead>
|
||||
{/* Tool Group Columns */}
|
||||
{toolGroups.map((group) => {
|
||||
const IconComponent = getIconByName(group.icon)
|
||||
return (
|
||||
<TableHead key={group.id} className="text-center">
|
||||
<div className="flex justify-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="text-xs">
|
||||
<div className="font-semibold mb-1">{group.name}</div>
|
||||
{group.tools.map((tool) => (
|
||||
<div key={tool}>{tool}</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
{/* Individual Tool Columns */}
|
||||
{toolColumns.map((toolName) => (
|
||||
<TableHead key={toolName} className="text-xs text-center">
|
||||
<Tooltip>
|
||||
|
|
@ -214,16 +907,34 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
<TableBody>
|
||||
{sortedRuns.length ? (
|
||||
sortedRuns.map(({ taskMetrics, ...run }) => (
|
||||
<Row key={run.id} run={run} taskMetrics={taskMetrics} toolColumns={toolColumns} />
|
||||
<Row
|
||||
key={run.id}
|
||||
run={run}
|
||||
taskMetrics={taskMetrics}
|
||||
toolColumns={toolColumns}
|
||||
toolGroups={toolGroups}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={totalColumns} 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>
|
||||
)}
|
||||
|
|
@ -235,6 +946,79 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|||
onClick={() => router.push("/runs/new")}>
|
||||
<Rocket className="size-6" />
|
||||
</Button>
|
||||
|
||||
{/* Tool Group Editor Dialog */}
|
||||
<ToolGroupEditorDialog
|
||||
open={showGroupDialog}
|
||||
onOpenChange={setShowGroupDialog}
|
||||
editingGroup={editingGroup}
|
||||
availableTools={availableToolsForEditor}
|
||||
onSave={handleSaveGroup}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,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>(
|
||||
|
|
@ -97,6 +109,8 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|||
placeholder = "Select options",
|
||||
maxCount = 3,
|
||||
modalPopover = false,
|
||||
popoverAutoWidth = false,
|
||||
footer,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
|
|
@ -243,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}>
|
||||
|
|
@ -276,6 +290,7 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
{footer && <div className="border-t p-2">{footer}</div>}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useCallback, useRef } from "react"
|
||||
import { useQuery, keepPreviousData } from "@tanstack/react-query"
|
||||
|
||||
import { type TokenUsage, RooCodeEventName, taskEventSchema } from "@roo-code/types"
|
||||
import { type TokenUsage, type ToolUsage, RooCodeEventName, taskEventSchema } from "@roo-code/types"
|
||||
import type { Run, Task, TaskMetrics } from "@roo-code/evals"
|
||||
|
||||
import { getHeartbeat } from "@/actions/heartbeat"
|
||||
|
|
@ -15,6 +15,7 @@ export type RunStatus = {
|
|||
runners: string[] | undefined
|
||||
tasks: (Task & { taskMetrics: TaskMetrics | null })[] | undefined
|
||||
tokenUsage: Map<number, TokenUsage & { duration?: number }>
|
||||
toolUsage: Map<number, ToolUsage>
|
||||
usageUpdatedAt: number | undefined
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ export const useRunStatus = (run: Run): RunStatus => {
|
|||
const [usageUpdatedAt, setUsageUpdatedAt] = useState<number>()
|
||||
|
||||
const tokenUsage = useRef<Map<number, TokenUsage & { duration?: number }>>(new Map())
|
||||
const toolUsage = useRef<Map<number, ToolUsage>>(new Map())
|
||||
const startTimes = useRef<Map<number, number>>(new Map())
|
||||
|
||||
const { data: heartbeat } = useQuery({
|
||||
|
|
@ -78,6 +80,12 @@ export const useRunStatus = (run: Run): RunStatus => {
|
|||
const startTime = startTimes.current.get(taskId)
|
||||
const duration = startTime ? Date.now() - startTime : undefined
|
||||
tokenUsage.current.set(taskId, { ...payload[1], duration })
|
||||
|
||||
// Track tool usage from streaming updates
|
||||
if (payload[2]) {
|
||||
toolUsage.current.set(taskId, payload[2])
|
||||
}
|
||||
|
||||
setUsageUpdatedAt(Date.now())
|
||||
break
|
||||
}
|
||||
|
|
@ -96,6 +104,7 @@ export const useRunStatus = (run: Run): RunStatus => {
|
|||
runners,
|
||||
tasks,
|
||||
tokenUsage: tokenUsage.current,
|
||||
toolUsage: toolUsage.current,
|
||||
usageUpdatedAt,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
apps/web-evals/src/lib/__tests__/formatters.spec.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { formatDuration, formatTokens } from "../formatters"
|
||||
|
||||
describe("formatDuration()", () => {
|
||||
it("formats as H:MM:SS", () => {
|
||||
expect(formatDuration(0)).toBe("0:00:00")
|
||||
expect(formatDuration(1_000)).toBe("0:00:01")
|
||||
expect(formatDuration(61_000)).toBe("0:01:01")
|
||||
expect(formatDuration(3_661_000)).toBe("1:01:01")
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatTokens()", () => {
|
||||
it("formats small numbers without suffix", () => {
|
||||
expect(formatTokens(0)).toBe("0")
|
||||
expect(formatTokens(999)).toBe("999")
|
||||
})
|
||||
|
||||
it("formats thousands without decimals and clamps to 1.0M at boundary", () => {
|
||||
expect(formatTokens(1_000)).toBe("1k")
|
||||
expect(formatTokens(72_500)).toBe("73k")
|
||||
expect(formatTokens(999_499)).toBe("999k")
|
||||
expect(formatTokens(999_500)).toBe("1.0M")
|
||||
})
|
||||
|
||||
it("formats millions with one decimal and clamps to 1.0B at boundary", () => {
|
||||
expect(formatTokens(1_000_000)).toBe("1.0M")
|
||||
expect(formatTokens(3_240_000)).toBe("3.2M")
|
||||
expect(formatTokens(999_950_000)).toBe("1.0B")
|
||||
})
|
||||
})
|
||||
|
|
@ -11,21 +11,10 @@ export const formatDuration = (durationMs: number) => {
|
|||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
|
||||
const parts = []
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours}h`)
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes}m`)
|
||||
}
|
||||
|
||||
if (remainingSeconds > 0 || parts.length === 0) {
|
||||
parts.push(`${remainingSeconds}s`)
|
||||
}
|
||||
|
||||
return parts.join(" ")
|
||||
// Format as H:MM:SS
|
||||
const mm = minutes.toString().padStart(2, "0")
|
||||
const ss = remainingSeconds.toString().padStart(2, "0")
|
||||
return `${hours}:${mm}:${ss}`
|
||||
}
|
||||
|
||||
export const formatTokens = (tokens: number) => {
|
||||
|
|
@ -34,18 +23,30 @@ export const formatTokens = (tokens: number) => {
|
|||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}k`
|
||||
// No decimal for thousands (e.g., 72k not 72.5k)
|
||||
const rounded = Math.round(tokens / 1000)
|
||||
// If rounding crosses the boundary to 1000k, show as 1.0M instead
|
||||
if (rounded >= 1000) {
|
||||
return "1.0M"
|
||||
}
|
||||
return `${rounded}k`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
// Keep decimal for millions (e.g., 3.2M)
|
||||
const rounded = Math.round(tokens / 100000) / 10 // Round to 1 decimal
|
||||
// If rounding crosses the boundary to 1000M, show as 1.0B instead
|
||||
if (rounded >= 1000) {
|
||||
return "1.0B"
|
||||
}
|
||||
return `${rounded.toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
|
||||
export const formatToolUsageSuccessRate = (usage: { attempts: number; failures: number }) =>
|
||||
usage.attempts === 0 ? "0%" : `${(((usage.attempts - usage.failures) / usage.attempts) * 100).toFixed(1)}%`
|
||||
usage.attempts === 0 ? "0%" : `${Math.round(((usage.attempts - usage.failures) / usage.attempts) * 100)}%`
|
||||
|
||||
export const formatDateTime = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
|
|
|
|||
|
|
@ -14,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." }),
|
||||
|
|
@ -23,6 +27,7 @@ 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),
|
||||
iterations: z.number().int().min(ITERATIONS_MIN).max(ITERATIONS_MAX),
|
||||
jobToken: z.string().optional(),
|
||||
})
|
||||
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ const nextConfig: NextConfig = {
|
|||
destination: "https://roo-code.notion.site/238fd1401b0a8087b858e1ad431507cf?pvs=105",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/provider/pricing",
|
||||
destination: "/provider",
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "12.15.0",
|
||||
"lucide-react": "^0.518.0",
|
||||
"next": "^15.2.5",
|
||||
"next": "~15.2.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"posthog-js": "^1.248.1",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
apps/web-roo-code/public/illustrations/form-factor-cloud.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
apps/web-roo-code/public/illustrations/form-factor-extension.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/1.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/10.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/11.jpg
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/12.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/13.jpg
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/14.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/15.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/16.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/17.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/18.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/19.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/2.jpg
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/20.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/21.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/22.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/23.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/24.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/3.jpg
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/4.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/5.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/6.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/7.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/8.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/web-roo-code/public/illustrations/user-faces/9.jpg
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
1
apps/web-roo-code/public/logos/bedrock.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bedrock</title><path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z"></path></svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
1
apps/web-roo-code/public/logos/moonshot.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MoonshotAI</title><path d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
apps/web-roo-code/public/logos/openrouter.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenRouter</title><path d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z"></path></svg>
|
||||
|
After Width: | Height: | Size: 906 B |
|
|
@ -1,29 +1,33 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Bot,
|
||||
Brain,
|
||||
ChartLine,
|
||||
Cloud,
|
||||
Lock,
|
||||
Github,
|
||||
History,
|
||||
LucideIcon,
|
||||
Megaphone,
|
||||
MessageCircleQuestionMark,
|
||||
ReplaceAll,
|
||||
Pencil,
|
||||
Router,
|
||||
Share2,
|
||||
Slack,
|
||||
Users,
|
||||
Users2,
|
||||
} from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { AnimatedBackground, UseExamplesSection } from "@/components/homepage"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import Image from "next/image"
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import screenshotDark from "/public/heroes/cloud-screen.png"
|
||||
|
||||
const TITLE = "Roo Code Cloud"
|
||||
const DESCRIPTION =
|
||||
"Roo Code Cloud gives you and your team the tools to take AI-coding to the next level with cloud agents, remote control, and more."
|
||||
const OG_DESCRIPTION = "Go way beyond the IDE"
|
||||
"Your AI Software Engineering Team in the Cloud. Delegate tasks to autonomous agents, review PRs, and collaborate with your team."
|
||||
const OG_DESCRIPTION = "Your AI Team in the Cloud"
|
||||
const PATH = "/cloud"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
@ -54,161 +58,171 @@ export const metadata: Metadata = {
|
|||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development"],
|
||||
keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development", "autonomous agents"],
|
||||
}
|
||||
|
||||
const howItWorks = [
|
||||
{
|
||||
title: "1. Connect your GitHub account",
|
||||
description:
|
||||
"Pick which repos the agents can work with in their isolated containers and choose what model you want to power each of them. You're in control.",
|
||||
icon: Github,
|
||||
},
|
||||
{
|
||||
title: "2. Set up your agent team",
|
||||
description:
|
||||
"Choose the roles you want filled, like Explainer, Planner, Coder, PR Reviewer and PR Fixer. They know how to act in each situation and stay on-task with no deviations.",
|
||||
icon: Users2,
|
||||
},
|
||||
{
|
||||
title: "3. Start giving them tasks",
|
||||
description:
|
||||
"Describe what you want them to do from the web UI, get the Reviewer automatically reviewing PRs, get the Coder building features from Slack threads and much more. They're now part of your team.",
|
||||
icon: Pencil,
|
||||
},
|
||||
]
|
||||
|
||||
interface Feature {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
logos?: string[]
|
||||
}
|
||||
|
||||
const cloudFeatures: Feature[] = [
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: Bot,
|
||||
title: "Autonomous Cloud Agents",
|
||||
description:
|
||||
"Delegate work to specialized agents like the Planner, Coder, Explainer, Reviewer, and Fixer that run 24/7.",
|
||||
},
|
||||
{
|
||||
icon: Brain,
|
||||
title: "Model Agnostic",
|
||||
description:
|
||||
"Bring your own keys or use the Roo Code Cloud Provider with access to all top models with no markup.",
|
||||
},
|
||||
{
|
||||
icon: Github,
|
||||
title: "GitHub PR Reviews",
|
||||
description:
|
||||
"Agents can automatically review Pull Requests, provide feedback, and even push fixes directly to your repository.",
|
||||
},
|
||||
{
|
||||
icon: Slack,
|
||||
title: "Slack Integration",
|
||||
description: "Start tasks, get updates, and collaborate with agents directly from your team's Slack channels.",
|
||||
},
|
||||
{
|
||||
icon: Router,
|
||||
title: "Roomote Control",
|
||||
description: "Control your IDE from anywhere and keep coding away from your computer.",
|
||||
},
|
||||
{
|
||||
icon: Cloud,
|
||||
title: "Cloud Agents",
|
||||
description:
|
||||
"Specialized agents running in the Cloud to get stuff done while you sleep, with a credit-based system that doesn't lock you in or dumb your models down.",
|
||||
"Connect to your local VS Code instance and control the extension remotely from the browser or Slack.",
|
||||
},
|
||||
{
|
||||
icon: ReplaceAll,
|
||||
title: "Still Model-agnostic",
|
||||
description: "Bring your own provider key — no markup, lock-in, no restrictions.",
|
||||
logos: ["Anthropic", "OpenAI", "Gemini", "Grok", "Qwen", "Kimi", "Mistral", "Ollama"],
|
||||
icon: Users,
|
||||
title: "Team Collaboration",
|
||||
description:
|
||||
"Manage your team and their access to tasks and resources, with centralized billing and configuration.",
|
||||
},
|
||||
{
|
||||
icon: ChartLine,
|
||||
title: "Usage Analytics",
|
||||
description: "Detailed token analytics to help you optimize your costs and usage.",
|
||||
description: "Detailed token analytics to help you optimize your costs and usage across your team.",
|
||||
},
|
||||
{
|
||||
icon: Megaphone,
|
||||
title: "Early Model Access",
|
||||
description: "Get early, free access to new, stealth coding models as they become available.",
|
||||
icon: History,
|
||||
title: "Task History",
|
||||
description: "Access from anywhere all of your tasks, from the cloud and the extension",
|
||||
},
|
||||
{
|
||||
icon: Share2,
|
||||
title: "Task Sharing",
|
||||
description: "Share tasks with friends and co-workers and let them follow your work.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Team Management",
|
||||
description:
|
||||
"Manage your team and their access to tasks and resources, with centralized billing, analytics and configuration.",
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
title: "Secure and Private",
|
||||
description:
|
||||
"Your data is never used for training, and we're SOC2 Type 2 and GDPR compliant, following state-of-the-art security practices, with deep respect for your IP.",
|
||||
},
|
||||
{
|
||||
icon: MessageCircleQuestionMark,
|
||||
title: "Priority support",
|
||||
description: "Get quick help from the people who know Roo best.",
|
||||
description: "Share tasks with friends and co-workers and let them follow your work in real-time.",
|
||||
},
|
||||
]
|
||||
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import screenshotDark from "/public/heroes/cloud-screen.png"
|
||||
|
||||
export default function CloudPage() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative flex md:h-[calc(80vh-theme(spacing.12))] items-center overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid h-full relative gap-4 md:gap-0 lg:grid-cols-2">
|
||||
<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mt-8 text-center md:text-left md:text-4xl lg:text-5xl lg:mt-0">
|
||||
Go <em>way</em> beyond the IDE
|
||||
</h1>
|
||||
<p className="mt-4 max-w-md text-lg text-muted-foreground text-center md:text-left sm:mt-6">
|
||||
Roo Code Cloud gives you (and your team) the tools to take AI-coding to the next
|
||||
level
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full sm:w-auto bg-white/20 dark:bg-white/10 backdrop-blur-sm border border-black/40 dark:border-white/30 hover:border-blue-400 hover:bg-white/30 dark:hover:bg-white/20 hover:shadow-[0_0_20px_rgba(59,130,246,0.5)] transition-all duration-300">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex w-full items-center justify-center">
|
||||
Start Free Trial
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
|
||||
<div className="md:w-[900px] md:h-[700px] relative rounded-md overflow-clip">
|
||||
<div className="block">
|
||||
<Image
|
||||
src={screenshotDark}
|
||||
alt="Screenshot of Roo Code Cloud"
|
||||
className="max-w-full h-auto"
|
||||
width={1390}
|
||||
height={1012}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center max-w-4xl mx-auto mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
|
||||
Your AI Team <span className="text-violet-500">in the Cloud</span>
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
|
||||
from Web and Slack.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Try Cloud for Free
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a href="/pricing" className="flex items-center justify-center">
|
||||
View Pricing
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Screenshot */}
|
||||
<div className="relative mx-auto mt-4 md:max-w-[1000px]">
|
||||
<Image
|
||||
src={screenshotDark}
|
||||
alt="Roo Code Cloud Interface"
|
||||
className="w-full h-auto"
|
||||
width={1390}
|
||||
height={1012}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<section className="relative overflow-hidden border-t border-border py-32">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto mb-12 md:mb-24 max-w-4xl text-center">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">Power and Flexibility</h2>
|
||||
<p className="mt-6 text-lg text-muted-foreground">
|
||||
Code in the cloud, access free models, get usage analytics and more
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-4">How it works</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
It only takes 2 minutes to expand your team by 10x.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto md:max-w-[1200px]">
|
||||
<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-2 lg:grid-cols-3 lg:gap-8">
|
||||
{cloudFeatures.map((feature, index) => {
|
||||
const Icon = feature.icon
|
||||
<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-3 lg:gap-8">
|
||||
{howItWorks.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300">
|
||||
<Icon className="size-6 text-foreground/80" />
|
||||
className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300 hover:shadow-lg">
|
||||
{Icon && <Icon className="size-6 text-foreground/80" />}
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
|
||||
{feature.title}
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="leading-relaxed font-light text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
{feature.logos && (
|
||||
<div className="mt-4 flex flex-wrap items-center gap-4">
|
||||
{feature.logos.map((logo) => (
|
||||
<Image
|
||||
key={logo}
|
||||
width={20}
|
||||
height={20}
|
||||
className="w-5 h-5 overflow-clip opacity-50 dark:invert"
|
||||
src={`/logos/${logo.toLowerCase()}.svg`}
|
||||
alt={`${logo} Logo`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="leading-relaxed font-light text-muted-foreground">
|
||||
{step.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
|
@ -217,25 +231,61 @@ export default function CloudPage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<div id="faq"></div>
|
||||
{/* Use Cases Section */}
|
||||
<UseExamplesSection />
|
||||
|
||||
{/* Features Grid */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Powering the next generation of software development
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Everything you need to scale your development capacity with AI.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto relative">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-background p-8 rounded-2xl border border-border hover:shadow-lg transition-all duration-300">
|
||||
<div className="bg-violet-100 dark:bg-violet-900/20 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
|
||||
<Icon className="size-6 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">{feature.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">{feature.description}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20">
|
||||
<section className="py-24">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-blue-500/5 via-cyan-500/5 to-purple-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/20 dark:bg-gradient-to-br dark:from-gray-800 dark:via-gray-900 dark:to-black sm:p-12">
|
||||
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">Try Roo Code Cloud now</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">Code from anywhere.</p>
|
||||
<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
|
||||
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Try a completely new way of working.
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">Start for free today.</p>
|
||||
<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-black text-white hover:bg-gray-800 hover:shadow-lg hover:shadow-black/20 dark:bg-white dark:text-black dark:hover:bg-gray-200 dark:hover:shadow-white/20 transition-all duration-300"
|
||||
className="bg-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Create a free Cloud account
|
||||
Sign up now
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
|
|
|
|||
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, PlugZap } 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,6 +60,7 @@ interface PricingTier {
|
|||
name: string
|
||||
icon: LucideIcon
|
||||
price: string
|
||||
priceSuffix: string
|
||||
period?: string
|
||||
creditPrice?: string
|
||||
trial?: string
|
||||
|
|
@ -70,60 +70,57 @@ interface PricingTier {
|
|||
cta: {
|
||||
text: string
|
||||
href?: string
|
||||
isContactForm?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const pricingTiers: PricingTier[] = [
|
||||
{
|
||||
name: "Cloud Free",
|
||||
icon: Cloud,
|
||||
price: "$0",
|
||||
description: "For folks just getting started",
|
||||
features: [
|
||||
"Token usage analytics",
|
||||
"Access to the Roo Code Cloud Provider, including early access to free stealth models",
|
||||
"Follow your tasks from anywhere",
|
||||
"Share tasks with friends and co-workers",
|
||||
"Community support",
|
||||
],
|
||||
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: "Get started",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP,
|
||||
text: "Install Now",
|
||||
href: EXTERNAL_LINKS.MARKETPLACE,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cloud Pro",
|
||||
icon: Star,
|
||||
price: "$20",
|
||||
name: "Cloud Free",
|
||||
icon: Cloud,
|
||||
price: "$0",
|
||||
period: "/mo",
|
||||
trial: "Free for 14 days, then",
|
||||
priceSuffix: "credits",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
description: "For pro Roo coders",
|
||||
featuresIntro: "Everything in Free +",
|
||||
description: "For AI-forward engineers",
|
||||
featuresIntro: "Go beyond the extension with",
|
||||
features: [
|
||||
"Cloud Agents: Coder, Explainer, Planner, Reviewer, Fixer and more",
|
||||
"Start tasks from Slack",
|
||||
"Roomote Control: Start, stop and control extension tasks from anywhere",
|
||||
"Paid support",
|
||||
"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",
|
||||
"Token usage analytics",
|
||||
"Professional support",
|
||||
],
|
||||
cta: {
|
||||
text: "Get started",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
|
||||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cloud Team",
|
||||
icon: Users,
|
||||
price: "$99",
|
||||
priceSuffix: "credits",
|
||||
period: "/mo",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
|
|
@ -135,70 +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>
|
||||
|
||||
<div className="mx-6 md: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 totally free! </strong>
|
||||
But Cloud takes you so much further.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-6 md:mx-auto max-w-6xl p-7 mb-4 relative flex flex-col justify-start bg-background border rounded-2xl transition-all shadow-none hover:shadow-lg">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2 justify-between">
|
||||
Roo Code Provider
|
||||
<PlugZap className="size-6" />
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-1 mt-2">
|
||||
<p className="">
|
||||
On any plan, you can bring your own provider key or use the built-in Roo Code Cloud provider.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
We offer a select mix of tested state of the art closed and open weight LLMs for you to choose,
|
||||
with no markup.
|
||||
<Link href="/provider/pricing" className="underline hover:no-underline ml-1">
|
||||
See detailed pricing
|
||||
</Link>
|
||||
</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-[168px]">
|
||||
<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" />
|
||||
|
|
@ -210,58 +180,65 @@ export default function PricingPage() {
|
|||
|
||||
<p className="text-base font-light">{tier.trial}</p>
|
||||
|
||||
<p className="text-xl my-1 tracking-tight font-light">
|
||||
<p className="text-xl mb-1 tracking-tight font-light">
|
||||
<strong className="font-bold">{tier.price}</strong>
|
||||
{tier.period} + prepaid credits
|
||||
{tier.period} + {tier.priceSuffix}
|
||||
<CornerRightDown className="inline size-4 ml-1 relative top-0.5" />
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
<p className="text-sm text-muted-foreground mb-5">
|
||||
{tier.creditPrice && (
|
||||
<>
|
||||
Cloud Agents: {tier.creditPrice}/hour if used
|
||||
Cloud Agents: {tier.creditPrice}/hour in credits
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
Inference:{" "}
|
||||
<Link href="/provider/pricing" className="underline hover:no-underline">
|
||||
<Link href="/provider" className="underline hover:no-underline">
|
||||
Roo Provider pricing
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<abbr title="Bring Your Own Key" className="cursor-help">
|
||||
BYOK
|
||||
credits or{" "}
|
||||
<abbr title="Bring Your Own Model" className="cursor-help">
|
||||
BYOM
|
||||
</abbr>
|
||||
</p>
|
||||
|
||||
{tier.cta.isContactForm ? (
|
||||
<ContactForm
|
||||
formType="demo"
|
||||
buttonText={tier.cta.text}
|
||||
buttonClassName="w-full transition-all duration-300"
|
||||
/>
|
||||
) : (
|
||||
<Button size="lg" className="w-full transition-all duration-300" asChild>
|
||||
<Link href={tier.cta.href!} className="flex items-center justify-center">
|
||||
{tier.cta.text}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button size="lg" className="w-full transition-all duration-300" asChild>
|
||||
<Link href={tier.cta.href!} className="flex items-center justify-center">
|
||||
{tier.cta.text}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* <div className="bg-foreground/20 h-8 absolute -bottom-8 left-1/2 w-[1px]" /> */}
|
||||
<div className="h-[28px] absolute bottom-[-31px] left-1/2 w-[4px] transition-colors bg-gradient-to-b from-transparent to-violet-700/20 group-hover:from-violet-500/50 group-hover:to-violet-500/20" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto grid max-w-6xl gap-4 mt-4 relative">
|
||||
<p className="bg-background border rounded-2xl p-6 text-center text-sm text-muted-foreground">
|
||||
<Building2 className="inline size-4 mr-2 mb-0.5" />
|
||||
Need SAML, advanced security, custom integrations or terms? Enterprise is for you.
|
||||
<Link
|
||||
href="/enterprise#contact"
|
||||
className="font-medium ml-1 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Talk to Sales
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<div className="max-w-6xl mx-auto mt-8 p-7 flex flex-col md:flex-row gap-8 md:gap-4 bg-violet-200/20 outline-violet-700/20 outline outline-1 rounded-2xl transition-all shadow-none">
|
||||
<div className="md:border-r md:pr-4">
|
||||
<h3 className="text-lg font-medium mb-1">Roo Code Provider</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="">
|
||||
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
|
||||
Cloud provider – curated models to work with Roo with no markup, including the
|
||||
latest Gemini, GPT and Claude. Paid with credits.
|
||||
<Link href="/provider" className="underline hover:no-underline ml-1">
|
||||
See per model pricing.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<h3 className="text-lg font-medium mb-1">Credits</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Credits are pre-paid, in dollars, and are deducted with usage for inference and Cloud
|
||||
Agent runs. You're always in control of your spend, no surprises.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -337,6 +314,19 @@ export default function PricingPage() {
|
|||
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>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { ModelCard } from "./components/model-card"
|
||||
import { ModelCard } from "./pricing/components/model-card"
|
||||
import { Model, ModelWithTotalPrice, ModelsResponse, SortOption } from "@/lib/types/models"
|
||||
import Link from "next/link"
|
||||
import { ChevronDown, CircleX, Loader, LoaderCircle, Search } from "lucide-react"
|
||||
import { ChevronDown, CircleX, Cloud, Loader, LoaderCircle, Puzzle, Search } from "lucide-react"
|
||||
|
||||
const API_URL = "https://api.roocode.com/proxy/v1/models?include_paid=true"
|
||||
|
||||
|
|
@ -13,28 +13,29 @@ const faqs = [
|
|||
question: "What are AI model providers?",
|
||||
answer: "AI model providers offer various language models with different capabilities and pricing.",
|
||||
},
|
||||
{
|
||||
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: "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>
|
||||
It offers a selection of state-of-the-art LLMs (both closed and open weight) we know work well with
|
||||
Roo for you to choose, with no markup.
|
||||
</p>
|
||||
<p>
|
||||
We also often feature 100% free models which labs share with us for the community to use and provide
|
||||
feedback.
|
||||
</p>
|
||||
<p>You don't have to use it to use Roo Code, but it's the easiest way to do it.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "But how much does the Roo Code Cloud service cost?",
|
||||
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{" "}
|
||||
|
|
@ -57,7 +58,7 @@ function enrichModelWithTotalPrice(model: Model): ModelWithTotalPrice {
|
|||
}
|
||||
}
|
||||
|
||||
export default function ProviderPricingPage() {
|
||||
export default function ProviderPage() {
|
||||
const [models, setModels] = useState<ModelWithTotalPrice[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
|
@ -133,13 +134,22 @@ export default function ProviderPricingPage() {
|
|||
<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">
|
||||
Roo Code Cloud Provider Pricing
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl md:text-lg text-muted-foreground">
|
||||
See pricing and features for all models we offer in our selection.
|
||||
<br />
|
||||
You can always bring your own key (
|
||||
<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>
|
||||
|
|
@ -240,7 +250,7 @@ export default function ProviderPricingPage() {
|
|||
</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-lg border border-border bg-card p-6">
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,6 +26,7 @@ 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_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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
"tar-fs": ">=3.1.1",
|
||||
"esbuild": ">=0.25.0",
|
||||
"undici": ">=5.29.0",
|
||||
"brace-expansion": ">=2.0.2",
|
||||
"brace-expansion": "^2.0.2",
|
||||
"form-data": ">=4.0.4",
|
||||
"bluebird": ">=3.7.2",
|
||||
"glob": ">=11.1.0"
|
||||
|
|
|
|||
|
|
@ -337,6 +337,11 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
|
|||
return this.shareService!.canShareTask()
|
||||
}
|
||||
|
||||
public async canSharePublicly(): Promise<boolean> {
|
||||
this.ensureInitialized()
|
||||
return this.shareService!.canSharePublicly()
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
|
||||
public dispose(): void {
|
||||
|
|
|
|||
|
|
@ -47,4 +47,15 @@ export class CloudShareService {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async canSharePublicly(): Promise<boolean> {
|
||||
try {
|
||||
const cloudSettings = this.settingsService.getSettings()?.cloudSettings
|
||||
// Public sharing requires both enableTaskSharing AND allowPublicTaskSharing to be true
|
||||
return !!cloudSettings?.enableTaskSharing && cloudSettings?.allowPublicTaskSharing !== false
|
||||
} catch (error) {
|
||||
this.log("[ShareService] Error checking if task can be shared publicly:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ abstract class BaseTelemetryClient implements TelemetryClient {
|
|||
|
||||
public abstract capture(event: TelemetryEvent): Promise<void>
|
||||
|
||||
public async captureException(_error: Error, _additionalProperties?: Record<string, unknown>): Promise<void> {}
|
||||
|
||||
public setProvider(provider: TelemetryPropertiesProvider): void {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,18 +188,46 @@ export class ExtensionChannel extends BaseChannel<
|
|||
{ from: RooCodeEventName.TaskPaused, to: ExtensionBridgeEventName.TaskPaused },
|
||||
{ from: RooCodeEventName.TaskUnpaused, to: ExtensionBridgeEventName.TaskUnpaused },
|
||||
{ from: RooCodeEventName.TaskSpawned, to: ExtensionBridgeEventName.TaskSpawned },
|
||||
{ from: RooCodeEventName.TaskDelegated, to: ExtensionBridgeEventName.TaskDelegated },
|
||||
{ from: RooCodeEventName.TaskDelegationCompleted, to: ExtensionBridgeEventName.TaskDelegationCompleted },
|
||||
{ from: RooCodeEventName.TaskDelegationResumed, to: ExtensionBridgeEventName.TaskDelegationResumed },
|
||||
{ from: RooCodeEventName.TaskUserMessage, to: ExtensionBridgeEventName.TaskUserMessage },
|
||||
{ from: RooCodeEventName.TaskTokenUsageUpdated, to: ExtensionBridgeEventName.TaskTokenUsageUpdated },
|
||||
] as const
|
||||
|
||||
eventMapping.forEach(({ from, to }) => {
|
||||
// Create and store the listener function for cleanup.
|
||||
const listener = async (..._args: unknown[]) => {
|
||||
this.publish(ExtensionSocketEvents.EVENT, {
|
||||
const listener = async (...args: unknown[]) => {
|
||||
const baseEvent: {
|
||||
type: ExtensionBridgeEventName
|
||||
instance: ExtensionInstance
|
||||
timestamp: number
|
||||
} = {
|
||||
type: to,
|
||||
instance: await this.updateInstance(),
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
let eventToPublish: ExtensionBridgeEvent
|
||||
|
||||
// Add payload for delegation events while avoiding `any`
|
||||
if (to === ExtensionBridgeEventName.TaskDelegationCompleted) {
|
||||
const [parentTaskId, childTaskId, summary] = args as [string, string, string]
|
||||
eventToPublish = {
|
||||
...(baseEvent as unknown as ExtensionBridgeEvent),
|
||||
payload: { parentTaskId, childTaskId, summary },
|
||||
} as unknown as ExtensionBridgeEvent
|
||||
} else if (to === ExtensionBridgeEventName.TaskDelegationResumed) {
|
||||
const [parentTaskId, childTaskId] = args as [string, string]
|
||||
eventToPublish = {
|
||||
...(baseEvent as unknown as ExtensionBridgeEvent),
|
||||
payload: { parentTaskId, childTaskId },
|
||||
} as unknown as ExtensionBridgeEvent
|
||||
} else {
|
||||
eventToPublish = baseEvent as unknown as ExtensionBridgeEvent
|
||||
}
|
||||
|
||||
this.publish(ExtensionSocketEvents.EVENT, eventToPublish)
|
||||
}
|
||||
|
||||
this.eventListeners.set(from, listener)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,10 @@ describe("ExtensionChannel", () => {
|
|||
RooCodeEventName.TaskPaused,
|
||||
RooCodeEventName.TaskUnpaused,
|
||||
RooCodeEventName.TaskSpawned,
|
||||
RooCodeEventName.TaskDelegated,
|
||||
RooCodeEventName.TaskDelegationCompleted,
|
||||
RooCodeEventName.TaskDelegationResumed,
|
||||
|
||||
RooCodeEventName.TaskUserMessage,
|
||||
RooCodeEventName.TaskTokenUsageUpdated,
|
||||
]
|
||||
|
|
@ -246,6 +250,116 @@ describe("ExtensionChannel", () => {
|
|||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it("should forward delegation events to socket", async () => {
|
||||
await extensionChannel.onConnect(mockSocket)
|
||||
;(mockSocket.emit as any).mockClear()
|
||||
|
||||
const delegatedListeners = eventListeners.get(RooCodeEventName.TaskDelegated)
|
||||
expect(delegatedListeners).toBeDefined()
|
||||
expect(delegatedListeners!.size).toBe(1)
|
||||
|
||||
const listener = Array.from(delegatedListeners!)[0]
|
||||
if (listener) {
|
||||
await (listener as any)("parent-id", "child-id")
|
||||
}
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith(
|
||||
ExtensionSocketEvents.EVENT,
|
||||
expect.objectContaining({
|
||||
type: ExtensionBridgeEventName.TaskDelegated,
|
||||
instance: expect.any(Object),
|
||||
timestamp: expect.any(Number),
|
||||
}),
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it("should forward TaskDelegationCompleted with correct payload", async () => {
|
||||
await extensionChannel.onConnect(mockSocket)
|
||||
;(mockSocket.emit as any).mockClear()
|
||||
|
||||
const completedListeners = eventListeners.get(RooCodeEventName.TaskDelegationCompleted)
|
||||
expect(completedListeners).toBeDefined()
|
||||
|
||||
const listener = Array.from(completedListeners!)[0]
|
||||
if (listener) {
|
||||
await (listener as any)("parent-1", "child-1", "Summary text")
|
||||
}
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith(
|
||||
ExtensionSocketEvents.EVENT,
|
||||
expect.objectContaining({
|
||||
type: ExtensionBridgeEventName.TaskDelegationCompleted,
|
||||
instance: expect.any(Object),
|
||||
timestamp: expect.any(Number),
|
||||
payload: expect.objectContaining({
|
||||
parentTaskId: "parent-1",
|
||||
childTaskId: "child-1",
|
||||
summary: "Summary text",
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it("should forward TaskDelegationResumed with correct payload", async () => {
|
||||
await extensionChannel.onConnect(mockSocket)
|
||||
;(mockSocket.emit as any).mockClear()
|
||||
|
||||
const resumedListeners = eventListeners.get(RooCodeEventName.TaskDelegationResumed)
|
||||
expect(resumedListeners).toBeDefined()
|
||||
|
||||
const listener = Array.from(resumedListeners!)[0]
|
||||
if (listener) {
|
||||
await (listener as any)("parent-2", "child-2")
|
||||
}
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith(
|
||||
ExtensionSocketEvents.EVENT,
|
||||
expect.objectContaining({
|
||||
type: ExtensionBridgeEventName.TaskDelegationResumed,
|
||||
instance: expect.any(Object),
|
||||
timestamp: expect.any(Number),
|
||||
payload: expect.objectContaining({
|
||||
parentTaskId: "parent-2",
|
||||
childTaskId: "child-2",
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it("should propagate all three delegation events in order", async () => {
|
||||
await extensionChannel.onConnect(mockSocket)
|
||||
;(mockSocket.emit as any).mockClear()
|
||||
|
||||
// Trigger TaskDelegated
|
||||
const delegatedListener = Array.from(eventListeners.get(RooCodeEventName.TaskDelegated)!)[0]
|
||||
await (delegatedListener as any)("p1", "c1")
|
||||
|
||||
// Trigger TaskDelegationCompleted
|
||||
const completedListener = Array.from(eventListeners.get(RooCodeEventName.TaskDelegationCompleted)!)[0]
|
||||
await (completedListener as any)("p1", "c1", "result")
|
||||
|
||||
// Trigger TaskDelegationResumed
|
||||
const resumedListener = Array.from(eventListeners.get(RooCodeEventName.TaskDelegationResumed)!)[0]
|
||||
await (resumedListener as any)("p1", "c1")
|
||||
|
||||
// Verify all three events were emitted
|
||||
const emittedEvents = (mockSocket.emit as any).mock.calls.map((call: any[]) => call[1]?.type)
|
||||
expect(emittedEvents).toContain(ExtensionBridgeEventName.TaskDelegated)
|
||||
expect(emittedEvents).toContain(ExtensionBridgeEventName.TaskDelegationCompleted)
|
||||
expect(emittedEvents).toContain(ExtensionBridgeEventName.TaskDelegationResumed)
|
||||
|
||||
// Verify correct order: Delegated → Completed → Resumed
|
||||
const delegatedIdx = emittedEvents.indexOf(ExtensionBridgeEventName.TaskDelegated)
|
||||
const completedIdx = emittedEvents.indexOf(ExtensionBridgeEventName.TaskDelegationCompleted)
|
||||
const resumedIdx = emittedEvents.indexOf(ExtensionBridgeEventName.TaskDelegationResumed)
|
||||
|
||||
expect(delegatedIdx).toBeLessThan(completedIdx)
|
||||
expect(completedIdx).toBeLessThan(resumedIdx)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Memory Leak Prevention", () => {
|
||||
|
|
@ -257,7 +371,7 @@ describe("ExtensionChannel", () => {
|
|||
}
|
||||
|
||||
// Listeners should still be the same count (not accumulated)
|
||||
expect(eventListeners.size).toBe(15)
|
||||
expect(eventListeners.size).toBe(18)
|
||||
|
||||
// Each event should have exactly 1 listener
|
||||
eventListeners.forEach((listeners) => {
|
||||
|
|
|
|||
45
packages/evals/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Development overrides - automatically loaded by docker compose
|
||||
# These settings only apply when running locally for development
|
||||
#
|
||||
# For production, use: docker compose -f docker-compose.yml up
|
||||
# (explicitly exclude override file)
|
||||
|
||||
services:
|
||||
web:
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
# Mount log files so web can access task logs
|
||||
- /tmp/evals:/tmp/evals:ro
|
||||
# Mount source code for hot reload in development
|
||||
- ../../apps/web-evals:/roo/repo/apps/web-evals:delegated
|
||||
- ../../packages/evals:/roo/repo/packages/evals:delegated
|
||||
- ../../packages/types:/roo/repo/packages/types:delegated
|
||||
- ../../packages/ipc:/roo/repo/packages/ipc:delegated
|
||||
- ../../packages/cloud:/roo/repo/packages/cloud:delegated
|
||||
# Exclude node_modules from being overwritten
|
||||
- /roo/repo/node_modules
|
||||
- /roo/repo/apps/web-evals/node_modules
|
||||
- /roo/repo/packages/evals/node_modules
|
||||
- /roo/repo/packages/types/node_modules
|
||||
- /roo/repo/packages/ipc/node_modules
|
||||
- /roo/repo/packages/cloud/node_modules
|
||||
entrypoint: []
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo '🚀 Starting evals web service in development mode...'
|
||||
wait_for_db() {
|
||||
echo '⏳ Waiting for database...'
|
||||
until pg_isready -h db -p 5432 -U postgres -d evals_development > /dev/null 2>&1; do
|
||||
echo '⏳ Database not ready yet, waiting 2 seconds...'
|
||||
sleep 2
|
||||
done
|
||||
echo '✅ Database is ready'
|
||||
}
|
||||
wait_for_db
|
||||
echo '🔄 Running database migrations...'
|
||||
pnpm --filter @roo-code/evals db:migrate
|
||||
echo '🌐 Starting Next.js dev server...'
|
||||
cd /roo/repo/apps/web-evals && npx next dev -p 3446
|
||||
|
|
@ -55,8 +55,11 @@ services:
|
|||
- "${EVALS_WEB_PORT:-3446}:3446"
|
||||
environment:
|
||||
- HOST_EXECUTION_METHOD=docker
|
||||
- PRODUCTION_DATABASE_URL
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Mount log files so web can access task logs
|
||||
- /tmp/evals:/tmp/evals:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -37,22 +37,33 @@ export const runEvals = async (runId: number) => {
|
|||
const heartbeat = await startHeartbeat(run.id)
|
||||
const queue = new PQueue({ concurrency: run.concurrency })
|
||||
|
||||
const STAGGER_DELAY_MS = 5000
|
||||
const filteredTasks = tasks.filter((task) => task.finishedAt === null)
|
||||
|
||||
const createTaskRunner = (task: (typeof filteredTasks)[number]) => async () => {
|
||||
try {
|
||||
if (containerized) {
|
||||
await processTaskInContainer({ taskId: task.id, jobToken: run.jobToken, logger })
|
||||
} else {
|
||||
await processTask({ taskId: task.id, jobToken: run.jobToken, logger })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("error processing task", error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await queue.addAll(
|
||||
tasks
|
||||
.filter((task) => task.finishedAt === null)
|
||||
.map((task) => async () => {
|
||||
try {
|
||||
if (containerized) {
|
||||
await processTaskInContainer({ taskId: task.id, jobToken: run.jobToken, logger })
|
||||
} else {
|
||||
await processTask({ taskId: task.id, jobToken: run.jobToken, logger })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("error processing task", error)
|
||||
}
|
||||
}),
|
||||
)
|
||||
// Add tasks with staggered start times when concurrency > 1
|
||||
for (let i = 0; i < filteredTasks.length; i++) {
|
||||
const task = filteredTasks[i]
|
||||
if (!task) continue
|
||||
if (run.concurrency > 1 && i > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, STAGGER_DELAY_MS))
|
||||
}
|
||||
queue.add(createTaskRunner(task))
|
||||
}
|
||||
|
||||
await queue.onIdle()
|
||||
|
||||
logger.info("finishRun")
|
||||
const result = await finishRun(run.id)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as fs from "fs"
|
||||
import * as fsp from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as os from "node:os"
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
RooCodeEventName,
|
||||
IpcMessageType,
|
||||
EVALS_SETTINGS,
|
||||
type ToolUsage,
|
||||
} from "@roo-code/types"
|
||||
import { IpcClient } from "@roo-code/ipc"
|
||||
|
||||
|
|
@ -38,6 +40,58 @@ class SubprocessTimeoutError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy conversation history files from VS Code extension storage to the log directory.
|
||||
* This allows us to preserve the api_conversation_history.json and ui_messages.json
|
||||
* files for post-mortem analysis alongside the log files.
|
||||
*/
|
||||
async function copyConversationHistory({
|
||||
rooTaskId,
|
||||
logDir,
|
||||
language,
|
||||
exercise,
|
||||
iteration,
|
||||
logger,
|
||||
}: {
|
||||
rooTaskId: string
|
||||
logDir: string
|
||||
language: string
|
||||
exercise: string
|
||||
iteration: number
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
// VS Code extension global storage path within the container
|
||||
const extensionStoragePath = "/roo/.vscode/User/globalStorage/rooveterinaryinc.roo-cline"
|
||||
const taskStoragePath = path.join(extensionStoragePath, "tasks", rooTaskId)
|
||||
|
||||
const filesToCopy = ["api_conversation_history.json", "ui_messages.json"]
|
||||
|
||||
for (const filename of filesToCopy) {
|
||||
const sourcePath = path.join(taskStoragePath, filename)
|
||||
// Use sanitized exercise name (replace slashes with dashes) for the destination filename
|
||||
// Include iteration number to handle multiple attempts at the same exercise
|
||||
const sanitizedExercise = exercise.replace(/\//g, "-")
|
||||
const destFilename = `${language}-${sanitizedExercise}.${iteration}_${filename}`
|
||||
const destPath = path.join(logDir, destFilename)
|
||||
|
||||
try {
|
||||
// Check if source file exists
|
||||
await fsp.access(sourcePath)
|
||||
|
||||
// Copy the file
|
||||
await fsp.copyFile(sourcePath, destPath)
|
||||
logger.info(`copied ${filename} to ${destPath}`)
|
||||
} catch (error) {
|
||||
// File may not exist if task didn't complete properly - this is not fatal
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
logger.info(`${filename} not found at ${sourcePath} - skipping`)
|
||||
} else {
|
||||
logger.error(`failed to copy ${filename}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const processTask = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
|
|
@ -114,7 +168,7 @@ export const processTaskInContainer = async ({
|
|||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const containerName = `evals-task-${taskId}.${attempt}`
|
||||
const args = [`--name ${containerName}`, ...baseArgs]
|
||||
const args = [`--name ${containerName}`, `-e EVALS_ATTEMPT=${attempt}`, ...baseArgs]
|
||||
const isRetry = attempt > 0
|
||||
|
||||
if (isRetry) {
|
||||
|
|
@ -172,6 +226,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
const controller = new AbortController()
|
||||
const cancelSignal = controller.signal
|
||||
const containerized = isDockerContainer()
|
||||
const logDir = containerized ? `/var/log/evals/runs/${run.id}` : `/tmp/evals/runs/${run.id}`
|
||||
|
||||
let codeCommand = containerized
|
||||
? `xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}`
|
||||
|
|
@ -223,6 +278,15 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
let taskMetricsId: number | undefined
|
||||
let rooTaskId: string | undefined
|
||||
let isClientDisconnected = false
|
||||
// Track accumulated tool usage across task instances (handles rehydration after abort)
|
||||
const accumulatedToolUsage: ToolUsage = {}
|
||||
|
||||
// Promise that resolves when taskMetricsId is set, preventing race conditions
|
||||
// where TaskTokenUsageUpdated arrives before TaskStarted handler completes
|
||||
let resolveTaskMetricsReady: () => void
|
||||
const taskMetricsReady = new Promise<void>((resolve) => {
|
||||
resolveTaskMetricsReady = resolve
|
||||
})
|
||||
|
||||
const ignoreEvents: Record<"broadcast" | "log", RooCodeEventName[]> = {
|
||||
broadcast: [RooCodeEventName.Message],
|
||||
|
|
@ -266,7 +330,23 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
(payload[0].message.say && loggableSays.includes(payload[0].message.say)) ||
|
||||
payload[0].message.partial !== true)
|
||||
) {
|
||||
logger.info(`${eventName} ->`, payload)
|
||||
// Extract tool name for tool-related messages for clearer logging
|
||||
let logEventName: string = eventName
|
||||
if (eventName === RooCodeEventName.Message && payload[0]?.message?.ask === "tool") {
|
||||
try {
|
||||
const textJson = JSON.parse(payload[0].message.text ?? "{}")
|
||||
if (textJson.tool) {
|
||||
logEventName = `${eventName} (tool: ${textJson.tool})`
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, use the default event name
|
||||
}
|
||||
} else if (eventName === RooCodeEventName.Message && payload[0]?.message?.ask === "command") {
|
||||
logEventName = `${eventName} (command)`
|
||||
} else if (eventName === RooCodeEventName.Message && payload[0]?.message?.ask === "completion_result") {
|
||||
logEventName = `${eventName} (completion_result)`
|
||||
}
|
||||
logger.info(`${logEventName} ->`, payload)
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskStarted) {
|
||||
|
|
@ -287,6 +367,9 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
taskStartedAt = Date.now()
|
||||
taskMetricsId = taskMetrics.id
|
||||
rooTaskId = payload[0]
|
||||
|
||||
// Signal that taskMetricsId is now ready for other handlers
|
||||
resolveTaskMetricsReady()
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskToolFailed) {
|
||||
|
|
@ -294,15 +377,46 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
await createToolError({ taskId: task.id, toolName, error })
|
||||
}
|
||||
|
||||
if (
|
||||
(eventName === RooCodeEventName.TaskTokenUsageUpdated || eventName === RooCodeEventName.TaskCompleted) &&
|
||||
taskMetricsId
|
||||
) {
|
||||
if (eventName === RooCodeEventName.TaskTokenUsageUpdated || eventName === RooCodeEventName.TaskCompleted) {
|
||||
// Wait for taskMetricsId to be set by the TaskStarted handler.
|
||||
// This prevents a race condition where these events arrive before
|
||||
// the TaskStarted handler finishes its async database operations.
|
||||
// Note: taskMetricsReady is also resolved on disconnect to prevent deadlock.
|
||||
await taskMetricsReady
|
||||
|
||||
// Guard: taskMetricsReady may have been resolved due to disconnect
|
||||
// without taskMetricsId being set. Skip metrics update in this case.
|
||||
if (!taskMetricsId) {
|
||||
logger.info(`skipping metrics update: taskMetricsId not set (event: ${eventName})`)
|
||||
return
|
||||
}
|
||||
|
||||
const duration = Date.now() - taskStartedAt
|
||||
|
||||
const { totalCost, totalTokensIn, totalTokensOut, contextTokens, totalCacheWrites, totalCacheReads } =
|
||||
payload[1]
|
||||
|
||||
// For both TaskTokenUsageUpdated and TaskCompleted: toolUsage is payload[2]
|
||||
const incomingToolUsage: ToolUsage = payload[2] ?? {}
|
||||
|
||||
// Merge incoming tool usage with accumulated data using MAX strategy.
|
||||
// This handles the case where a task is rehydrated after abort:
|
||||
// - Empty rehydrated data won't overwrite existing: max(5, 0) = 5
|
||||
// - Legitimate restart with additional work is captured: max(5, 8) = 8
|
||||
// Each task instance tracks its own cumulative values, so we take the max
|
||||
// to preserve the highest values seen across all instances.
|
||||
for (const [toolName, usage] of Object.entries(incomingToolUsage)) {
|
||||
const existing = accumulatedToolUsage[toolName as keyof ToolUsage]
|
||||
if (existing) {
|
||||
accumulatedToolUsage[toolName as keyof ToolUsage] = {
|
||||
attempts: Math.max(existing.attempts, usage.attempts),
|
||||
failures: Math.max(existing.failures, usage.failures),
|
||||
}
|
||||
} else {
|
||||
accumulatedToolUsage[toolName as keyof ToolUsage] = { ...usage }
|
||||
}
|
||||
}
|
||||
|
||||
await updateTaskMetrics(taskMetricsId, {
|
||||
cost: totalCost,
|
||||
tokensIn: totalTokensIn,
|
||||
|
|
@ -311,14 +425,10 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
duration,
|
||||
cacheWrites: totalCacheWrites ?? 0,
|
||||
cacheReads: totalCacheReads ?? 0,
|
||||
toolUsage: accumulatedToolUsage,
|
||||
})
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskCompleted && taskMetricsId) {
|
||||
const toolUsage = payload[2]
|
||||
await updateTaskMetrics(taskMetricsId, { toolUsage })
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskAborted) {
|
||||
taskAbortedAt = Date.now()
|
||||
}
|
||||
|
|
@ -331,6 +441,10 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
client.on(IpcMessageType.Disconnect, async () => {
|
||||
logger.info(`disconnected from IPC socket -> ${ipcSocketPath}`)
|
||||
isClientDisconnected = true
|
||||
// Resolve taskMetricsReady to unblock any handlers waiting on it.
|
||||
// This prevents deadlock if TaskStarted never fired or threw before resolving.
|
||||
// The handlers check for taskMetricsId being set before proceeding.
|
||||
resolveTaskMetricsReady()
|
||||
})
|
||||
|
||||
client.sendCommand({
|
||||
|
|
@ -418,9 +532,25 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
}
|
||||
}
|
||||
|
||||
// Copy conversation history files from VS Code extension storage to the log directory
|
||||
// for post-mortem analysis. Only do this in containerized mode where we have a known path.
|
||||
if (containerized && rooTaskId) {
|
||||
await copyConversationHistory({
|
||||
rooTaskId,
|
||||
logDir,
|
||||
language,
|
||||
exercise,
|
||||
iteration: task.iteration,
|
||||
logger,
|
||||
})
|
||||
}
|
||||
|
||||
logger.close()
|
||||
|
||||
if (isApiUnstable) {
|
||||
// Only throw for API instability if the task didn't complete successfully.
|
||||
// If taskFinishedAt is set via TaskCompleted event, the task succeeded despite
|
||||
// API retries, so re-running from scratch would waste resources.
|
||||
if (isApiUnstable && !taskFinishedAt) {
|
||||
throw new Error("API is unstable, throwing to trigger a retry.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
DROP INDEX "tasks_language_exercise_idx";--> statement-breakpoint
|
||||
ALTER TABLE "tasks" ADD COLUMN "iteration" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "tasks_language_exercise_iteration_idx" ON "tasks" USING btree ("run_id","language","exercise","iteration");
|
||||
12
packages/evals/src/db/migrations/0005_strong_skrulls.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
ALTER TABLE "tasks" DROP CONSTRAINT "tasks_run_id_runs_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "tasks" DROP CONSTRAINT "tasks_task_metrics_id_taskMetrics_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "toolErrors" DROP CONSTRAINT "toolErrors_run_id_runs_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "toolErrors" DROP CONSTRAINT "toolErrors_task_id_tasks_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_run_id_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_task_metrics_id_taskMetrics_id_fk" FOREIGN KEY ("task_metrics_id") REFERENCES "public"."taskMetrics"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "toolErrors" ADD CONSTRAINT "toolErrors_run_id_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "toolErrors" ADD CONSTRAINT "toolErrors_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;
|
||||
472
packages/evals/src/db/migrations/meta/0004_snapshot.json
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
{
|
||||
"id": "9caa4487-e146-4084-907d-fbf9cc3e03b9",
|
||||
"prevId": "853d308a-3946-4ea8-9039-236bfce3c6c0",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.runs": {
|
||||
"name": "runs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "runs_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"contextWindow": {
|
||||
"name": "contextWindow",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"inputPrice": {
|
||||
"name": "inputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"outputPrice": {
|
||||
"name": "outputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheWritesPrice": {
|
||||
"name": "cacheWritesPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheReadsPrice": {
|
||||
"name": "cacheReadsPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"jobToken": {
|
||||
"name": "jobToken",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"pid": {
|
||||
"name": "pid",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"socket_path": {
|
||||
"name": "socket_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"concurrency": {
|
||||
"name": "concurrency",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 2
|
||||
},
|
||||
"timeout": {
|
||||
"name": "timeout",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 5
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"failed": {
|
||||
"name": "failed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"runs_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "runs_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "runs",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.taskMetrics": {
|
||||
"name": "taskMetrics",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "taskMetrics_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"tokens_in": {
|
||||
"name": "tokens_in",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_out": {
|
||||
"name": "tokens_out",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_context": {
|
||||
"name": "tokens_context",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_writes": {
|
||||
"name": "cache_writes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_reads": {
|
||||
"name": "cache_reads",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tool_usage": {
|
||||
"name": "tool_usage",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tasks": {
|
||||
"name": "tasks",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "tasks_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"exercise": {
|
||||
"name": "exercise",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"finished_at": {
|
||||
"name": "finished_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tasks_language_exercise_iteration_idx": {
|
||||
"name": "tasks_language_exercise_iteration_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "run_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "language",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "exercise",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "iteration",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"tasks_run_id_runs_id_fk": {
|
||||
"name": "tasks_run_id_runs_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"tasks_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "tasks_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.toolErrors": {
|
||||
"name": "toolErrors",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "toolErrors_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_name": {
|
||||
"name": "tool_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"toolErrors_run_id_runs_id_fk": {
|
||||
"name": "toolErrors_run_id_runs_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"toolErrors_task_id_tasks_id_fk": {
|
||||
"name": "toolErrors_task_id_tasks_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "tasks",
|
||||
"columnsFrom": ["task_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
472
packages/evals/src/db/migrations/meta/0005_snapshot.json
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
{
|
||||
"id": "71b54967-86df-42ec-a200-bfd8dad85069",
|
||||
"prevId": "9caa4487-e146-4084-907d-fbf9cc3e03b9",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.runs": {
|
||||
"name": "runs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "runs_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"contextWindow": {
|
||||
"name": "contextWindow",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"inputPrice": {
|
||||
"name": "inputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"outputPrice": {
|
||||
"name": "outputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheWritesPrice": {
|
||||
"name": "cacheWritesPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheReadsPrice": {
|
||||
"name": "cacheReadsPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"jobToken": {
|
||||
"name": "jobToken",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"pid": {
|
||||
"name": "pid",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"socket_path": {
|
||||
"name": "socket_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"concurrency": {
|
||||
"name": "concurrency",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 2
|
||||
},
|
||||
"timeout": {
|
||||
"name": "timeout",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 5
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"failed": {
|
||||
"name": "failed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"runs_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "runs_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "runs",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.taskMetrics": {
|
||||
"name": "taskMetrics",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "taskMetrics_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"tokens_in": {
|
||||
"name": "tokens_in",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_out": {
|
||||
"name": "tokens_out",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_context": {
|
||||
"name": "tokens_context",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_writes": {
|
||||
"name": "cache_writes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_reads": {
|
||||
"name": "cache_reads",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tool_usage": {
|
||||
"name": "tool_usage",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tasks": {
|
||||
"name": "tasks",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "tasks_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"exercise": {
|
||||
"name": "exercise",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"finished_at": {
|
||||
"name": "finished_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tasks_language_exercise_iteration_idx": {
|
||||
"name": "tasks_language_exercise_iteration_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "run_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "language",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "exercise",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "iteration",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"tasks_run_id_runs_id_fk": {
|
||||
"name": "tasks_run_id_runs_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"tasks_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "tasks_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.toolErrors": {
|
||||
"name": "toolErrors",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "toolErrors_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_name": {
|
||||
"name": "tool_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"toolErrors_run_id_runs_id_fk": {
|
||||
"name": "toolErrors_run_id_runs_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"toolErrors_task_id_tasks_id_fk": {
|
||||
"name": "toolErrors_task_id_tasks_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "tasks",
|
||||
"columnsFrom": ["task_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,20 @@
|
|||
"when": 1763797232454,
|
||||
"tag": "0003_simple_retro_girl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1764201678953,
|
||||
"tag": "0004_sloppy_black_knight",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1765167049182,
|
||||
"tag": "0005_strong_skrulls",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,3 +134,67 @@ export const deleteRun = async (runId: number) => {
|
|||
|
||||
await db.delete(schema.taskMetrics).where(inArray(schema.taskMetrics.id, taskMetricsIds))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all runs without a taskMetricsId (incomplete runs)
|
||||
*/
|
||||
export const getIncompleteRuns = async () => {
|
||||
return db.query.runs.findMany({
|
||||
where: sql`${schema.runs.taskMetricsId} IS NULL`,
|
||||
columns: { id: true },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple runs by their IDs
|
||||
*/
|
||||
export const deleteRunsByIds = async (runIds: number[]) => {
|
||||
if (runIds.length === 0) return
|
||||
|
||||
// Get all tasks for these runs
|
||||
const tasks = await db.query.tasks.findMany({
|
||||
where: inArray(schema.tasks.runId, runIds),
|
||||
columns: { id: true, taskMetricsId: true },
|
||||
})
|
||||
|
||||
const taskIds = tasks.map(({ id }) => id)
|
||||
|
||||
// Get run taskMetricsIds
|
||||
const runs = await db.query.runs.findMany({
|
||||
where: inArray(schema.runs.id, runIds),
|
||||
columns: { taskMetricsId: true },
|
||||
})
|
||||
|
||||
// Delete tool errors for tasks
|
||||
if (taskIds.length > 0) {
|
||||
await db.delete(schema.toolErrors).where(inArray(schema.toolErrors.taskId, taskIds))
|
||||
}
|
||||
|
||||
// Delete tasks
|
||||
await db.delete(schema.tasks).where(inArray(schema.tasks.runId, runIds))
|
||||
|
||||
// Delete tool errors for runs
|
||||
await db.delete(schema.toolErrors).where(inArray(schema.toolErrors.runId, runIds))
|
||||
|
||||
// Delete from tables that exist in DB but not in drizzle schema
|
||||
// Using individual deletes since drizzle's sql template doesn't support custom table schemas
|
||||
for (const runId of runIds) {
|
||||
await db.execute(sql`DELETE FROM "cpuMetrics" WHERE run_id = ${runId}`)
|
||||
await db.execute(sql`DELETE FROM "notes" WHERE run_id = ${runId}`)
|
||||
}
|
||||
|
||||
// Delete runs
|
||||
await db.delete(schema.runs).where(inArray(schema.runs.id, runIds))
|
||||
|
||||
// Delete task metrics
|
||||
const taskMetricsIds = [
|
||||
...tasks
|
||||
.map(({ taskMetricsId }) => taskMetricsId)
|
||||
.filter((id): id is number => id !== null && id !== undefined),
|
||||
...runs.map(({ taskMetricsId }) => taskMetricsId).filter((id): id is number => id !== null && id !== undefined),
|
||||
]
|
||||
|
||||
if (taskMetricsIds.length > 0) {
|
||||
await db.delete(schema.taskMetrics).where(inArray(schema.taskMetrics.id, taskMetricsIds))
|
||||
}
|
||||
}
|
||||
|
|
|
|||