Merge remote-tracking branch 'origin/main' into bb/open-pr-button
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -76,7 +76,7 @@ body:
|
|||
label: API Provider (optional)
|
||||
options:
|
||||
- Anthropic
|
||||
- AWS Bedrock
|
||||
- Amazon Bedrock
|
||||
- Chutes AI
|
||||
- DeepSeek
|
||||
- Featherless AI
|
||||
|
|
|
|||
|
|
@ -18,6 +18,19 @@ fi
|
|||
|
||||
$pnpm_cmd run check-types
|
||||
|
||||
# Use dotenvx to securely load .env.local and run commands that depend on it
|
||||
if [ -f ".env.local" ]; then
|
||||
# Check if RUN_TESTS_ON_PUSH is set to true and run tests with dotenvx
|
||||
if npx dotenvx get RUN_TESTS_ON_PUSH -f .env.local 2>/dev/null | grep -q "^true$"; then
|
||||
npx dotenvx run -f .env.local -- $pnpm_cmd run test
|
||||
fi
|
||||
else
|
||||
# Fallback: run tests if RUN_TESTS_ON_PUSH is set in regular environment
|
||||
if [ "$RUN_TESTS_ON_PUSH" = "true" ]; then
|
||||
$pnpm_cmd run test
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for new changesets.
|
||||
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
|
||||
echo "Changeset files: $NEW_CHANGESETS"
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ argument-hint: patch | minor | major
|
|||
[list of changes]
|
||||
```
|
||||
|
||||
- Always include contributor attribution using format: (thanks @username!)
|
||||
- For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)"
|
||||
- For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)"
|
||||
- Always include contributor attribution and the PR number: use "(PR #<prNumber> by @username)".
|
||||
- For PRs that close issues, include both the issue number and the PR number and authors: "- Fix: Description (#123 by @reporter, PR #456 by @contributor)"
|
||||
- For PRs without linked issues, include the PR number and author: "- Add support for feature (PR #456 by @contributor)"
|
||||
- Provide brief descriptions of each item to explain the change
|
||||
- Order the list from most important to least important
|
||||
- Example formats:
|
||||
- With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)"
|
||||
- Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)"
|
||||
- With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR #789 by @prAuthor)"
|
||||
- Without issue: "- Add support for Gemini 2.5 Pro caching (PR #789 by @contributor)"
|
||||
- CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed.
|
||||
|
||||
6. If the generate_image tool is available, create a release image at `releases/[version]-release.png`
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
Then retrieve the issue:
|
||||
|
||||
<execute_command>
|
||||
<command>gh issue view [issue-number] --repo [owner]/[repo] --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author</command>
|
||||
<command>gh api repos/[owner]/[repo]/issues/[issue-number] --jq '{number,title,body,state,labels,assignees,milestone,createdAt:.created_at,updatedAt:.updated_at,closedAt:.closed_at,author:.user.login}'</command>
|
||||
</execute_command>
|
||||
|
||||
If the command fails with an authentication error (e.g., "gh: Not authenticated" or "HTTP 401"), ask the user to authenticate:
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
- Any decisions or changes to requirements
|
||||
|
||||
<execute_command>
|
||||
<command>gh issue view [issue number] --repo [owner]/[repo] --comments</command>
|
||||
<command>gh api repos/[owner]/[repo]/issues/[issue-number]/comments --paginate --jq '.[].body'</command>
|
||||
</execute_command>
|
||||
|
||||
Also check for:
|
||||
|
|
|
|||
|
|
@ -29,23 +29,23 @@
|
|||
|
||||
<primary_commands>
|
||||
<command name="gh_issue_view">
|
||||
<purpose>Retrieve the issue details at the start</purpose>
|
||||
<purpose>Retrieve the issue details at the start using the REST Issues API.</purpose>
|
||||
<when>Always use first to get the full issue content</when>
|
||||
<syntax>gh issue view [issue-number] --repo [owner]/[repo] --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author</syntax>
|
||||
<syntax>gh api repos/[owner]/[repo]/issues/[issue-number] --jq '{number,title,body,state,labels,assignees,milestone,createdAt:.created_at,updatedAt:.updated_at,closedAt:.closed_at,author:.user.login}'</syntax>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue view 123 --repo octocat/hello-world --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author</command>
|
||||
<command>gh api repos/octocat/hello-world/issues/123 --jq '{number,title,body,state,labels,assignees,milestone,createdAt:.created_at,updatedAt:.updated_at,closedAt:.closed_at,author:.user.login}'</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh_issue_comments">
|
||||
<purpose>Get additional context and requirements from issue comments</purpose>
|
||||
<purpose>Get additional context and requirements from issue comments.</purpose>
|
||||
<when>Always use after viewing issue to see full discussion</when>
|
||||
<syntax>gh issue view [issue-number] --repo [owner]/[repo] --comments</syntax>
|
||||
<syntax>gh api repos/[owner]/[repo]/issues/[issue-number]/comments --paginate --jq '.[].body'</syntax>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue view 123 --repo octocat/hello-world --comments</command>
|
||||
<command>gh api repos/octocat/hello-world/issues/123/comments --paginate --jq '.[].body'</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
|
@ -109,6 +109,30 @@
|
|||
</command>
|
||||
</optional_commands>
|
||||
|
||||
<projects_v2_commands>
|
||||
<command name="gh_projects_v2_for_issue">
|
||||
<purpose>Inspect associations with GitHub Projects (new Projects experience) for a given issue</purpose>
|
||||
<when>Use when project context is relevant to understanding priority, ownership, or workflow</when>
|
||||
<syntax>gh api graphql -f query='
|
||||
query($owner:String!, $repo:String!, $number:Int!) {
|
||||
repository(owner:$owner, name:$repo) {
|
||||
issue(number:$number) {
|
||||
projectsV2(first:20) {
|
||||
nodes {
|
||||
title
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' -F owner=[owner] -F repo=[repo] -F number=[issue-number]</syntax>
|
||||
<note>
|
||||
This uses the projectsV2 field from the new GitHub Projects experience for issue-level project context.
|
||||
</note>
|
||||
</command>
|
||||
</projects_v2_commands>
|
||||
|
||||
<pull_request_commands>
|
||||
<command name="gh_pr_create">
|
||||
<purpose>Create a pull request</purpose>
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@
|
|||
|
||||
- 保留英文品牌名
|
||||
- 技术术语保持一致性
|
||||
- 保留英文专有名词:如"AWS Bedrock ARN"
|
||||
- 保留英文专有名词:如"Amazon Bedrock ARN"
|
||||
|
||||
4. **用户操作**
|
||||
- 操作动词统一:
|
||||
|
|
|
|||
351
CHANGELOG.md
|
|
@ -1,5 +1,336 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.36.0] - 2025-12-04
|
||||
|
||||

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

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

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

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

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

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

|
||||
|
||||
- Experimental feature to enable multiple native tool calls per turn (PR #9273 by @daniel-lxs)
|
||||
- Add Bedrock Opus 4.5 to global inference model list (PR #9595 by @roomote)
|
||||
- Fix: Update API handler when toolProtocol changes (PR #9599 by @mrubens)
|
||||
- Set native tools as default for minimax-m2 and claude-haiku-4.5 (PR #9586 by @daniel-lxs)
|
||||
- Make single file read only apply to XML tools (PR #9600 by @mrubens)
|
||||
- Enhance web-evals dashboard with dynamic tool columns and UX improvements (PR #9592 by @hannesrudolph)
|
||||
- Revert "Add support for Roo Code Cloud as an embeddings provider" while we fix some issues (PR #9602 by @mrubens)
|
||||
|
||||
## [3.34.4] - 2025-11-25
|
||||
|
||||

|
||||
|
||||
- Add new Black Forest Labs image generation models, free on Roo Code Cloud and also available on OpenRouter (PR #9587 and #9589 by @mrubens)
|
||||
- Fix: Preserve dynamic MCP tool names in native mode API history to prevent tool name mismatches (PR #9559 by @daniel-lxs)
|
||||
- Fix: Preserve tool_use blocks in summary message during condensing with native tools to maintain conversation context (PR #9582 by @daniel-lxs)
|
||||
|
||||
## [3.34.3] - 2025-11-25
|
||||
|
||||

|
||||
|
||||
- Implement streaming for native tool calls, providing real-time feedback during tool execution (PR #9542 by @daniel-lxs)
|
||||
- Add Claude Opus 4.5 model to Claude Code provider (PR #9560 by @mrubens)
|
||||
- Add Claude Opus 4.5 model to Bedrock provider (#9571 by @pisicode, PR #9572 by @roomote)
|
||||
- Enable caching for Opus 4.5 model to improve performance (#9567 by @iainRedro, PR #9568 by @roomote)
|
||||
- Add support for Roo Code Cloud as an embeddings provider (PR #9543 by @mrubens)
|
||||
- Fix ask_followup_question streaming issue and add missing tool cases (PR #9561 by @daniel-lxs)
|
||||
- Add contact links to About Roo Code settings page (PR #9570 by @roomote)
|
||||
- Switch from asdf to mise-en-place in bare-metal evals setup script (PR #9548 by @cte)
|
||||
|
||||
## [3.34.2] - 2025-11-24
|
||||
|
||||

|
||||
|
||||
- Add support for Claude Opus 4.5 in Anthropic and Vertex providers (PR #9541 by @daniel-lxs)
|
||||
- Add support for Claude Opus 4.5 in OpenRouter with prompt caching and reasoning budget (PR #9540 by @daniel-lxs)
|
||||
- Add Roo Code Cloud as an image generation provider (PR #9528 by @mrubens)
|
||||
- Fix: Gracefully skip unsupported content blocks in Gemini transformer (PR #9537 by @daniel-lxs)
|
||||
- Fix: Flush LiteLLM cache when credentials change on refresh (PR #9536 by @daniel-lxs)
|
||||
- Fix: Ensure XML parser state matches tool protocol on config update (PR #9535 by @daniel-lxs)
|
||||
- Update Cerebras models (PR #9527 by @sebastiand-cerebras)
|
||||
- Fix: Support reasoning_details format for Gemini 3 models (PR #9506 by @daniel-lxs)
|
||||
|
||||
## [3.34.1] - 2025-11-23
|
||||
|
||||
- Show the prompt for image generation in the UI (PR #9505 by @mrubens)
|
||||
- Fix double todo list display issue (PR #9517 by @mrubens)
|
||||
- Add tracking for cloud synced messages (PR #9518 by @mrubens)
|
||||
- Enable the Roo Code Cloud provider in evals (PR #9492 by @cte)
|
||||
|
||||
## [3.34.0] - 2025-11-21
|
||||
|
||||

|
||||
|
||||
- Add Browser Use 2.0 with enhanced browser interaction capabilities (PR #8941 by @hannesrudolph)
|
||||
- Add support for Baseten as a new AI provider (PR #9461 by @AlexKer)
|
||||
- Improve base OpenAI compatible provider with better error handling and configuration (PR #9462 by @mrubens)
|
||||
- Add provider-oriented welcome screen to improve onboarding experience (PR #9484 by @mrubens)
|
||||
- Pin Roo provider to the top of the provider list for better discoverability (PR #9485 by @mrubens)
|
||||
- Enhance native tool descriptions with examples and clarifications for better AI understanding (PR #9486 by @daniel-lxs)
|
||||
- Fix: Make cancel button immediately responsive during streaming (#9435 by @jwadow, PR #9448 by @daniel-lxs)
|
||||
- Fix: Resolve apply_diff performance regression from earlier changes (PR #9474 by @daniel-lxs)
|
||||
- Fix: Implement model cache refresh to prevent stale disk cache issues (PR #9478 by @daniel-lxs)
|
||||
- Fix: Copy model-level capabilities to OpenRouter endpoint models correctly (PR #9483 by @daniel-lxs)
|
||||
- Fix: Add fallback to yield tool calls regardless of finish_reason (PR #9476 by @daniel-lxs)
|
||||
|
||||
## [3.33.3] - 2025-11-20
|
||||
|
||||

|
||||
|
||||
- Add Google Gemini 3 Pro Image Preview to image generation models (PR #9440 by @app/roomote)
|
||||
- Add support for Minimax as Anthropic-compatible provider (PR #9455 by @daniel-lxs)
|
||||
- Store reasoning in conversation history for all providers (PR #9451 by @daniel-lxs)
|
||||
- Fix: Improve preserveReasoning flag to control API reasoning inclusion (PR #9453 by @daniel-lxs)
|
||||
- Fix: Prevent OpenAI Native parallel tool calls for native tool calling (PR #9433 by @hannesrudolph)
|
||||
- Fix: Improve search and replace symbol parsing (PR #9456 by @daniel-lxs)
|
||||
- Fix: Send tool_result blocks for skipped tools in native protocol (PR #9457 by @daniel-lxs)
|
||||
- Fix: Improve markdown formatting and add reasoning support (PR #9458 by @daniel-lxs)
|
||||
- Fix: Prevent duplicate environment_details when resuming cancelled tasks (PR #9442 by @daniel-lxs)
|
||||
- Improve read_file tool description with examples (PR #9422 by @daniel-lxs)
|
||||
- Update glob dependency to ^11.1.0 (PR #9449 by @jr)
|
||||
- Update tar-fs to 3.1.1 via pnpm override (PR #9450 by @app/roomote)
|
||||
|
||||
## [3.33.2] - 2025-11-19
|
||||
|
||||
- Enable native tool calling for Gemini provider (PR #9343 by @hannesrudolph)
|
||||
- Add RCC credit balance display (PR #9386 by @jr)
|
||||
- Fix: Preserve user images in native tool call results (PR #9401 by @daniel-lxs)
|
||||
- Perf: Reduce excessive getModel() calls and implement disk cache fallback (PR #9410 by @daniel-lxs)
|
||||
- Show zero price for free models (PR #9419 by @mrubens)
|
||||
|
||||
## [3.33.1] - 2025-11-18
|
||||
|
||||

|
||||
|
||||
- Add native tool calling support to OpenAI-compatible (PR #9369 by @mrubens)
|
||||
- Fix: Resolve native tool protocol race condition causing 400 errors (PR #9363 by @daniel-lxs)
|
||||
- Fix: Update tools to return structured JSON for native protocol (PR #9373 by @daniel-lxs)
|
||||
- Fix: Include nativeArgs in tool repetition detection (PR #9377 by @daniel-lxs)
|
||||
- Fix: Ensure no XML parsing when protocol is native (PR #9371 by @daniel-lxs)
|
||||
- Fix: Gemini maxOutputTokens and reasoning config (PR #9375 by @hannesrudolph)
|
||||
- Fix: Gemini thought signature validation and token counting errors (PR #9380 by @hannesrudolph)
|
||||
- Fix: Exclude XML tool examples from MODES section when native protocol enabled (PR #9367 by @daniel-lxs)
|
||||
- Retry eval tasks if API instability detected (PR #9365 by @cte)
|
||||
- Add toolProtocol property to PostHog tool usage telemetry (PR #9374 by @app/roomote)
|
||||
|
||||
## [3.33.0] - 2025-11-18
|
||||
|
||||

|
||||
|
||||
- Add Gemini 3 Pro Preview model (PR #9357 by @hannesrudolph)
|
||||
- Improve Google Gemini defaults with better temperature and cost reporting (PR #9327 by @hannesrudolph)
|
||||
- Enable native tool calling for openai-native provider (PR #9348 by @hannesrudolph)
|
||||
- Add git status information to environment details (PR #9310 by @daniel-lxs)
|
||||
- Add tool protocol selector to advanced settings (PR #9324 by @daniel-lxs)
|
||||
- Implement dynamic tool protocol resolution with proper precedence hierarchy (PR #9286 by @daniel-lxs)
|
||||
- Move Import/Export functionality to Modes view toolbar and cleanup Mode Edit view (PR #9077 by @hannesrudolph)
|
||||
- Update cloud agent CTA to point to setup page (PR #9338 by @app/roomote)
|
||||
- Fix: Prevent duplicate tool_result blocks in native tool protocol (PR #9248 by @daniel-lxs)
|
||||
- Fix: Format tool responses properly for native protocol (PR #9270 by @daniel-lxs)
|
||||
- Fix: Centralize toolProtocol configuration checks (PR #9279 by @daniel-lxs)
|
||||
- Fix: Preserve tool blocks for native protocol in conversation history (PR #9319 by @daniel-lxs)
|
||||
- Fix: Prevent infinite loop when task_done succeeds (PR #9325 by @daniel-lxs)
|
||||
- Fix: Sync parser state with profile/model changes (PR #9355 by @daniel-lxs)
|
||||
- Fix: Pass tool protocol parameter to lineCountTruncationError (PR #9358 by @daniel-lxs)
|
||||
- Use VSCode theme color for outline button borders (PR #9336 by @app/roomote)
|
||||
- Replace broken badgen.net badges with shields.io (PR #9318 by @app/roomote)
|
||||
- Add max git status files setting to evals (PR #9322 by @mrubens)
|
||||
- Roo Code Cloud Provider pricing page and changes elsewhere (PR #9195 by @brunobergher)
|
||||
|
||||
## [3.32.1] - 2025-11-14
|
||||
|
||||

|
||||
|
||||
- Fix: Add abort controller for request cancellation in OpenAI native protocol (PR #9276 by @daniel-lxs)
|
||||
- Fix: Resolve duplicate tool blocks causing 'tool has already been used' error in native protocol mode (PR #9275 by @daniel-lxs)
|
||||
- Fix: Prevent duplicate tool_result blocks in native protocol mode for read_file (PR #9272 by @daniel-lxs)
|
||||
- Fix: Correct OpenAI Native handling of encrypted reasoning blocks to prevent errors during condensing (PR #9263 by @hannesrudolph)
|
||||
- Fix: Disable XML parser for native tool protocol to prevent parsing conflicts (PR #9277 by @daniel-lxs)
|
||||
|
||||
## [3.32.0] - 2025-11-14
|
||||
|
||||

|
||||
|
||||
- Feature: Add GPT-5.1 models to OpenAI provider (PR #9252 by @hannesrudolph)
|
||||
- Feature: Support for OpenAI Responses 24 hour prompt caching (PR #9259 by @hannesrudolph)
|
||||
- Fix: Repair the share button in the UI (PR #9253 by @hannesrudolph)
|
||||
- Docs: Include PR numbers in the release guide to improve traceability (PR #9236 by @hannesrudolph)
|
||||
|
||||
## [3.31.3] - 2025-11-13
|
||||
|
||||

|
||||
|
||||
- Fix: OpenAI Native encrypted_content handling and remove gpt-5-chat-latest verbosity flag (#9225 by @politsin, PR by @hannesrudolph)
|
||||
- Fix: Roo Code Cloud provider Anthropic input token normalization to avoid double-counting (thanks @hannesrudolph!)
|
||||
- Refactor: Rename sliding-window to context-management and truncateConversationIfNeeded to manageContext (thanks @hannesrudolph!)
|
||||
|
||||
## [3.31.2] - 2025-11-12
|
||||
|
||||
- Fix: Apply updated API profile settings when provider/model unchanged (#9208 by @hannesrudolph, PR by @hannesrudolph)
|
||||
- Migrate conversation continuity to plugin-side encrypted reasoning items using Responses API for improved reliability (thanks @hannesrudolph!)
|
||||
- Fix: Include mcpServers in getState() for auto-approval (#9190 by @bozoweed, PR by @daniel-lxs)
|
||||
- Batch settings updates from the webview to the extension host for improved performance (thanks @cte!)
|
||||
- Fix: Replace rate-limited badges with badgen.net to improve README reliability (thanks @daniel-lxs!)
|
||||
|
||||
## [3.31.1] - 2025-11-11
|
||||
|
||||

|
||||
|
||||
- Fix: Prevent command_output ask from blocking in cloud/headless environments (thanks @daniel-lxs!)
|
||||
- Add IPC command for sending messages to the current task (thanks @mrubens!)
|
||||
- Fix: Model switch re-applies selected profile, ensuring task configuration stays in sync (#9179 by @hannesrudolph, PR by @hannesrudolph)
|
||||
- Move auto-approval logic from `ChatView` to `Task` for better architecture (thanks @cte!)
|
||||
- Add custom Button component with variant system (thanks @brunobergher!)
|
||||
|
||||
## [3.31.0] - 2025-11-07
|
||||
|
||||

|
||||
|
||||
- Improvements to to-do lists and task headers (thanks @brunobergher!)
|
||||
- Fix: Prevent crash when streaming chunks have null choices array (thanks @daniel-lxs!)
|
||||
- Fix: Prevent context condensing on settings save when provider/model unchanged (#4430 by @hannesrudolph, PR by @daniel-lxs)
|
||||
- Fix: Respect custom OpenRouter URL for all API operations (#8947 by @sstraus, PR by @roomote)
|
||||
- Add comprehensive error logging to Roo Cloud provider (thanks @daniel-lxs!)
|
||||
- UX: Less caffeinated kangaroo (thanks @brunobergher!)
|
||||
|
||||
## [3.30.3] - 2025-11-06
|
||||
|
||||

|
||||
|
||||
- Feat: Add kimi-k2-thinking model to Moonshot provider (thanks @daniel-lxs!)
|
||||
- Fix: Auto-retry on empty assistant response to prevent task failures (#9076 by @Akillatech, PR by @daniel-lxs)
|
||||
- Fix: Use system role for OpenAI Compatible provider when streaming is disabled (#8215 by @whitfin, PR by @roomote)
|
||||
- Fix: Prevent notification sound on attempt_completion with queued messages (#8537 by @hannesrudolph, PR by @roomote)
|
||||
- Feat: Auto-switch to imported mode with architect fallback for better mode detection (#8239 by @hannesrudolph, PR by @daniel-lxs)
|
||||
- Feat: Add MiniMax-M2-Stable model and enable prompt caching (#9070 by @nokaka, PR by @roomote)
|
||||
- Feat: Improve diff appearance in main chat view (thanks @hannesrudolph!)
|
||||
- UX: Home screen visuals (thanks @brunobergher!)
|
||||
- Docs: Clarify that setting 0 disables Error & Repetition Limit (thanks @roomote!)
|
||||
- Chore: Update dependency @changesets/cli to v2.29.7 (thanks @renovate!)
|
||||
|
||||
## [3.30.2] - 2025-11-05
|
||||
|
||||

|
||||
|
||||
- Fix: eliminate UI flicker during task cancellation (thanks @daniel-lxs!)
|
||||
- Add Global Inference support for Bedrock models (#8750 by @ronyblum, PR by @hannesrudolph)
|
||||
- Add Qwen3 embedding models (0.6B and 4B) to OpenRouter support (#9058 by @dmarkey, PR by @app/roomote)
|
||||
- Fix: resolve incorrect commit location when GIT_DIR set in Dev Containers (#4567 by @nonsleepr, PR by @heyseth)
|
||||
- Fix: keep pinned models fixed at top of scrollable list (#8812 by @XiaoYingYo, PR by @app/roomote)
|
||||
- Fix: update Opus 4.1 max tokens from 8K to 32K (#9045 by @kaveh-deriv, PR by @app/roomote)
|
||||
- Set Claude Sonnet 4.5 as default for key providers (thanks @hannesrudolph!)
|
||||
- Fix: dynamic provider model validation to prevent cross-contamination (#9047 by @NotADev137, PR by @daniel-lxs)
|
||||
- Fix: Bedrock user agent to report full SDK details (#9031 by @ajjuaire, PR by @ajjuaire)
|
||||
- Add file path tooltips with centralized PathTooltip component (#8278 by @da2ce7, PR by @daniel-lxs)
|
||||
- Add conditional test running to pre-push hook (thanks @daniel-lxs!)
|
||||
- Update Cerebras integration (thanks @sebastiand-cerebras!)
|
||||
|
||||
## [3.30.1] - 2025-11-04
|
||||
|
||||
- Fix: Correct OpenRouter Mistral model embedding dimension from 3072 to 1536 (thanks @daniel-lxs!)
|
||||
- Revert: Previous UI flicker fix that caused issues with task resumption (thanks @mrubens!)
|
||||
|
||||
## [3.30.0] - 2025-11-03
|
||||
|
||||

|
||||
|
|
@ -148,7 +479,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
|
||||
|
|
@ -480,7 +811,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
|
||||
|
|
@ -845,7 +1176,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!)
|
||||
|
|
@ -859,7 +1190,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
|
||||
|
||||
|
|
@ -1483,7 +1814,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
|
||||
|
|
@ -1732,12 +2063,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!)
|
||||
|
|
@ -1875,7 +2206,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
|
||||
|
||||
|
|
@ -1892,7 +2223,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
|
||||
|
|
@ -2076,7 +2407,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]
|
||||
|
||||
|
|
|
|||
14
README.md
|
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/badge/VS_Code_Marketplace-007ACC?style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code Marketplace"></a>
|
||||
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
|
||||
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
|
||||
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](locales/zh-CN/README.md)
|
||||
- [繁體中文](locales/zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ We love community contributions! Get started by reading our [CONTRIBUTING.md](CO
|
|||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all our contributors who have helped make Roo Code better!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](./LICENSE)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"@types/vscode": "^1.95.0",
|
||||
"@vscode/test-cli": "^0.0.11",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"glob": "^11.0.1",
|
||||
"glob": "^11.1.0",
|
||||
"mocha": "^11.1.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "5.8.3"
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ suite("Roo Code Extension", function () {
|
|||
"SidebarProvider.removeView",
|
||||
"activationCompleted",
|
||||
"plusButtonClicked",
|
||||
"mcpButtonClicked",
|
||||
"promptsButtonClicked",
|
||||
"popoutButtonClicked",
|
||||
"openInNewTab",
|
||||
"settingsButtonClicked",
|
||||
|
|
|
|||
|
|
@ -1,628 +0,0 @@
|
|||
import * as assert from "assert"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
||||
|
||||
import { waitFor, sleep } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite.skip("Roo Code insert_content Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let workspaceDir: string
|
||||
|
||||
// Pre-created test files that will be used across tests
|
||||
const testFiles = {
|
||||
simpleText: {
|
||||
name: `test-insert-simple-${Date.now()}.txt`,
|
||||
content: "Line 1\nLine 2\nLine 3",
|
||||
path: "",
|
||||
},
|
||||
jsFile: {
|
||||
name: `test-insert-js-${Date.now()}.js`,
|
||||
content: `function hello() {
|
||||
console.log("Hello World")
|
||||
}
|
||||
|
||||
function goodbye() {
|
||||
console.log("Goodbye World")
|
||||
}`,
|
||||
path: "",
|
||||
},
|
||||
emptyFile: {
|
||||
name: `test-insert-empty-${Date.now()}.txt`,
|
||||
content: "",
|
||||
path: "",
|
||||
},
|
||||
pythonFile: {
|
||||
name: `test-insert-python-${Date.now()}.py`,
|
||||
content: `def main():
|
||||
print("Start")
|
||||
print("End")`,
|
||||
path: "",
|
||||
},
|
||||
}
|
||||
|
||||
// Get the actual workspace directory that VSCode is using and create all test files
|
||||
suiteSetup(async function () {
|
||||
// Get the workspace folder from VSCode
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error("No workspace folder found")
|
||||
}
|
||||
workspaceDir = workspaceFolders[0]!.uri.fsPath
|
||||
console.log("Using workspace directory:", workspaceDir)
|
||||
|
||||
// Create all test files before any tests run
|
||||
console.log("Creating test files in workspace...")
|
||||
for (const [key, file] of Object.entries(testFiles)) {
|
||||
file.path = path.join(workspaceDir, file.name)
|
||||
await fs.writeFile(file.path, file.content)
|
||||
console.log(`Created ${key} test file at:`, file.path)
|
||||
}
|
||||
|
||||
// Verify all files exist
|
||||
for (const [key, file] of Object.entries(testFiles)) {
|
||||
const exists = await fs
|
||||
.access(file.path)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (!exists) {
|
||||
throw new Error(`Failed to create ${key} test file at ${file.path}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up after all tests
|
||||
suiteTeardown(async () => {
|
||||
// Cancel any running tasks before cleanup
|
||||
test("Should insert content at the beginning of a file (line 1)", async function () {
|
||||
const api = globalThis.api
|
||||
// Clean up before each test
|
||||
setup(async () => {
|
||||
// Cancel any previous task
|
||||
try {
|
||||
await globalThis.api.cancelCurrentTask()
|
||||
} catch {
|
||||
// Task might not be running
|
||||
}
|
||||
|
||||
// Small delay to ensure clean state
|
||||
await sleep(100)
|
||||
})
|
||||
|
||||
// Clean up after each test
|
||||
teardown(async () => {
|
||||
// Cancel the current task
|
||||
try {
|
||||
await globalThis.api.cancelCurrentTask()
|
||||
} catch {
|
||||
// Task might not be running
|
||||
}
|
||||
|
||||
// Small delay to ensure clean state
|
||||
await sleep(100)
|
||||
})
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.simpleText
|
||||
const insertContent = "New first line"
|
||||
const expectedContent = `${insertContent}
|
||||
${testFile.content}`
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let errorOccurred: string | null = null
|
||||
let insertContentExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("insert_content")) {
|
||||
insertContentExecuted = true
|
||||
console.log("insert_content tool executed!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start the task
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use insert_content to add "${insertContent}" at line 1 (beginning) of the file ${testFile.name}. The file already exists with this content:
|
||||
${testFile.content}
|
||||
|
||||
Assume the file exists and you can modify it directly.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Check for early errors
|
||||
if (errorOccurred) {
|
||||
console.error("Early error detected:", errorOccurred)
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 45_000 })
|
||||
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Check if the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after insertion:", actualContent)
|
||||
|
||||
// Verify tool was executed
|
||||
assert.strictEqual(insertContentExecuted, true, "insert_content tool should have been executed")
|
||||
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"Content should be inserted at the beginning of the file",
|
||||
)
|
||||
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(
|
||||
errorOccurred,
|
||||
null,
|
||||
`Task should complete without errors, but got: ${errorOccurred}`,
|
||||
)
|
||||
|
||||
console.log("Test passed! insert_content tool executed and content inserted at beginning successfully")
|
||||
} finally {
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
try {
|
||||
await globalThis.api.cancelCurrentTask()
|
||||
} catch {
|
||||
// Task might not be running
|
||||
}
|
||||
|
||||
// Clean up all test files
|
||||
console.log("Cleaning up test files...")
|
||||
for (const [key, file] of Object.entries(testFiles)) {
|
||||
try {
|
||||
await fs.unlink(file.path)
|
||||
console.log(`Cleaned up ${key} test file`)
|
||||
} catch (error) {
|
||||
console.log(`Failed to clean up ${key} test file:`, error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("Should insert content at the end of a file (line 0)", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.simpleText
|
||||
const insertContent = "New last line"
|
||||
const expectedContent = `${testFile.content}
|
||||
${insertContent}`
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let errorOccurred: string | null = null
|
||||
let insertContentExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("insert_content")) {
|
||||
insertContentExecuted = true
|
||||
console.log("insert_content tool executed!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start the task
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use insert_content to add "${insertContent}" at line 0 (end of file) of the file ${testFile.name}. The file already exists with this content:
|
||||
${testFile.content}
|
||||
|
||||
Assume the file exists and you can modify it directly.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Check for early errors
|
||||
if (errorOccurred) {
|
||||
console.error("Early error detected:", errorOccurred)
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 45_000 })
|
||||
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Check if the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after insertion:", actualContent)
|
||||
|
||||
// Verify tool was executed
|
||||
test("Should insert multiline content into a JavaScript file", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.jsFile
|
||||
const insertContent = `// New import statements
|
||||
import { utils } from './utils'
|
||||
import { helpers } from './helpers'`
|
||||
const expectedContent = `${insertContent}
|
||||
${testFile.content}`
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let errorOccurred: string | null = null
|
||||
let insertContentExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("insert_content")) {
|
||||
insertContentExecuted = true
|
||||
console.log("insert_content tool executed!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start the task
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use insert_content to add import statements at the beginning (line 1) of the JavaScript file ${testFile.name}. Add these lines:
|
||||
${insertContent}
|
||||
|
||||
The file already exists with this content:
|
||||
${testFile.content}
|
||||
|
||||
Assume the file exists and you can modify it directly.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Check for early errors
|
||||
if (errorOccurred) {
|
||||
console.error("Early error detected:", errorOccurred)
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 45_000 })
|
||||
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
test("Should insert content into an empty file", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.emptyFile
|
||||
const insertContent = `# My New File
|
||||
This is the first line of content
|
||||
And this is the second line`
|
||||
const expectedContent = insertContent
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let errorOccurred: string | null = null
|
||||
let insertContentExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (
|
||||
message.type === "say" &&
|
||||
(message.say === "completion_result" || message.say === "text")
|
||||
) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("insert_content")) {
|
||||
insertContentExecuted = true
|
||||
console.log("insert_content tool executed!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start the task
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use insert_content to add content to the empty file ${testFile.name}. Add this content at line 0 (end of file):
|
||||
${insertContent}
|
||||
|
||||
The file is currently empty. Assume the file exists and you can modify it directly.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Check for early errors
|
||||
if (errorOccurred) {
|
||||
console.error("Early error detected:", errorOccurred)
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 45_000 })
|
||||
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Check if the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after insertion:", actualContent)
|
||||
|
||||
// Verify tool was executed
|
||||
assert.strictEqual(
|
||||
insertContentExecuted,
|
||||
true,
|
||||
"insert_content tool should have been executed",
|
||||
)
|
||||
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"Content should be inserted into the empty file",
|
||||
)
|
||||
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(
|
||||
errorOccurred,
|
||||
null,
|
||||
`Task should complete without errors, but got: ${errorOccurred}`,
|
||||
)
|
||||
|
||||
console.log(
|
||||
"Test passed! insert_content tool executed and content inserted into empty file successfully",
|
||||
)
|
||||
} finally {
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
// Check if the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after insertion:", actualContent)
|
||||
|
||||
// Verify tool was executed
|
||||
assert.strictEqual(insertContentExecuted, true, "insert_content tool should have been executed")
|
||||
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"Multiline content should be inserted at the beginning of the JavaScript file",
|
||||
)
|
||||
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(
|
||||
errorOccurred,
|
||||
null,
|
||||
`Task should complete without errors, but got: ${errorOccurred}`,
|
||||
)
|
||||
|
||||
console.log("Test passed! insert_content tool executed and multiline content inserted successfully")
|
||||
} finally {
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
assert.strictEqual(insertContentExecuted, true, "insert_content tool should have been executed")
|
||||
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"Content should be inserted at the end of the file",
|
||||
)
|
||||
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, `Task should complete without errors, but got: ${errorOccurred}`)
|
||||
|
||||
console.log("Test passed! insert_content tool executed and content inserted at end successfully")
|
||||
} finally {
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
// Tests will be added here one by one
|
||||
})
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-checkbox": "^1.1.5",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
|
|
@ -28,12 +29,13 @@
|
|||
"@roo-code/evals": "workspace:^",
|
||||
"@roo-code/types": "workspace:^",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"archiver": "^7.0.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.0",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"lucide-react": "^0.518.0",
|
||||
"next": "^15.2.5",
|
||||
"next": "~15.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"p-map": "^7.0.3",
|
||||
"react": "^18.3.1",
|
||||
|
|
@ -51,6 +53,7 @@
|
|||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/ps-tree": "^1.1.6",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
|
|
|
|||
207
apps/web-evals/src/actions/__tests__/killRun.spec.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// npx vitest run src/actions/__tests__/killRun.spec.ts
|
||||
|
||||
import { execFileSync } from "child_process"
|
||||
|
||||
// Mock child_process
|
||||
vi.mock("child_process", () => ({
|
||||
execFileSync: vi.fn(),
|
||||
spawn: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock next/cache
|
||||
vi.mock("next/cache", () => ({
|
||||
revalidatePath: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock redis client
|
||||
vi.mock("@/lib/server/redis", () => ({
|
||||
redisClient: vi.fn().mockResolvedValue({
|
||||
del: vi.fn().mockResolvedValue(1),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock @roo-code/evals
|
||||
vi.mock("@roo-code/evals", () => ({
|
||||
createRun: vi.fn(),
|
||||
deleteRun: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
exerciseLanguages: [],
|
||||
getExercisesForLanguage: vi.fn().mockResolvedValue([]),
|
||||
}))
|
||||
|
||||
// Mock timers to speed up tests
|
||||
vi.useFakeTimers()
|
||||
|
||||
// Import after mocks
|
||||
import { killRun } from "../runs"
|
||||
|
||||
const mockExecFileSync = execFileSync as ReturnType<typeof vi.fn>
|
||||
|
||||
describe("killRun", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers()
|
||||
})
|
||||
|
||||
it("should kill controller first, wait, then kill task containers", async () => {
|
||||
const runId = 123
|
||||
|
||||
// execFileSync is used for all docker commands
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce("") // docker kill controller
|
||||
.mockReturnValueOnce("evals-task-123-456.0\nevals-task-123-789.1\n") // docker ps
|
||||
.mockReturnValueOnce("") // docker kill evals-task-123-456.0
|
||||
.mockReturnValueOnce("") // docker kill evals-task-123-789.1
|
||||
|
||||
const resultPromise = killRun(runId)
|
||||
|
||||
// Fast-forward past the 10 second sleep
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.killedContainers).toContain("evals-controller-123")
|
||||
expect(result.killedContainers).toContain("evals-task-123-456.0")
|
||||
expect(result.killedContainers).toContain("evals-task-123-789.1")
|
||||
expect(result.errors).toHaveLength(0)
|
||||
|
||||
// Verify execFileSync was called for docker kill
|
||||
expect(mockExecFileSync).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"docker",
|
||||
["kill", "evals-controller-123"],
|
||||
expect.any(Object),
|
||||
)
|
||||
// Verify execFileSync was called for docker ps with run-specific filter
|
||||
expect(mockExecFileSync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"docker",
|
||||
["ps", "--format", "{{.Names}}", "--filter", "name=evals-task-123-"],
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it("should continue killing runners even if controller is not running", async () => {
|
||||
const runId = 456
|
||||
|
||||
mockExecFileSync
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("No such container")
|
||||
}) // controller kill fails
|
||||
.mockReturnValueOnce("evals-task-456-100.0\n") // docker ps
|
||||
.mockReturnValueOnce("") // docker kill task
|
||||
|
||||
const resultPromise = killRun(runId)
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.killedContainers).toContain("evals-task-456-100.0")
|
||||
// Controller not in list since it failed
|
||||
expect(result.killedContainers).not.toContain("evals-controller-456")
|
||||
})
|
||||
|
||||
it("should clear Redis state after killing containers", async () => {
|
||||
const runId = 789
|
||||
|
||||
const mockDel = vi.fn().mockResolvedValue(1)
|
||||
const { redisClient } = await import("@/lib/server/redis")
|
||||
vi.mocked(redisClient).mockResolvedValue({ del: mockDel } as never)
|
||||
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce("") // controller kill
|
||||
.mockReturnValueOnce("") // docker ps (no tasks)
|
||||
|
||||
const resultPromise = killRun(runId)
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
await resultPromise
|
||||
|
||||
expect(mockDel).toHaveBeenCalledWith("heartbeat:789")
|
||||
expect(mockDel).toHaveBeenCalledWith("runners:789")
|
||||
})
|
||||
|
||||
it("should handle docker ps failure gracefully", async () => {
|
||||
const runId = 111
|
||||
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce("") // controller kill succeeds
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("Docker error")
|
||||
}) // docker ps fails
|
||||
|
||||
const resultPromise = killRun(runId)
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
const result = await resultPromise
|
||||
|
||||
// Should still be successful because controller was killed
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.killedContainers).toContain("evals-controller-111")
|
||||
expect(result.errors).toContain("Failed to list Docker task containers")
|
||||
})
|
||||
|
||||
it("should handle individual task kill failures", async () => {
|
||||
const runId = 222
|
||||
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce("") // controller kill
|
||||
.mockReturnValueOnce("evals-task-222-300.0\nevals-task-222-400.0\n") // docker ps
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("Kill failed")
|
||||
}) // first task kill fails
|
||||
.mockReturnValueOnce("") // second task kill succeeds
|
||||
|
||||
const resultPromise = killRun(runId)
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.killedContainers).toContain("evals-controller-222")
|
||||
expect(result.killedContainers).toContain("evals-task-222-400.0")
|
||||
expect(result.errors.length).toBe(1)
|
||||
expect(result.errors[0]).toContain("evals-task-222-300.0")
|
||||
})
|
||||
|
||||
it("should return success with no containers when nothing is running", async () => {
|
||||
const runId = 333
|
||||
|
||||
mockExecFileSync
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("No such container")
|
||||
}) // controller not running
|
||||
.mockReturnValueOnce("") // no task containers
|
||||
|
||||
const resultPromise = killRun(runId)
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.killedContainers).toHaveLength(0)
|
||||
expect(result.errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should only kill containers belonging to the specific run", async () => {
|
||||
const runId = 555
|
||||
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce("") // controller kill
|
||||
.mockReturnValueOnce("evals-task-555-100.0\n") // docker ps
|
||||
.mockReturnValueOnce("") // docker kill task
|
||||
|
||||
const resultPromise = killRun(runId)
|
||||
await vi.advanceTimersByTimeAsync(10000)
|
||||
const result = await resultPromise
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
// Verify execFileSync was called for docker ps with run-specific filter
|
||||
expect(mockExecFileSync).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"docker",
|
||||
["ps", "--format", "{{.Names}}", "--filter", "name=evals-task-555-"],
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import * as path from "path"
|
||||
import fs from "fs"
|
||||
import { fileURLToPath } from "url"
|
||||
import { spawn } from "child_process"
|
||||
import { spawn, execFileSync } from "child_process"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import pMap from "p-map"
|
||||
|
|
@ -18,11 +18,11 @@ import {
|
|||
} from "@roo-code/evals"
|
||||
|
||||
import { CreateRun } from "@/lib/schemas"
|
||||
import { redisClient } from "@/lib/server/redis"
|
||||
|
||||
const EVALS_REPO_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../../evals")
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export async function createRun({ suite, exercises = [], systemPrompt, timeout, ...values }: CreateRun) {
|
||||
export async function createRun({ suite, exercises = [], timeout, iterations = 1, ...values }: CreateRun) {
|
||||
const run = await _createRun({
|
||||
...values,
|
||||
timeout,
|
||||
|
|
@ -37,15 +37,34 @@ export async function createRun({ suite, exercises = [], systemPrompt, timeout,
|
|||
throw new Error("Invalid exercise path: " + path)
|
||||
}
|
||||
|
||||
await createTask({ ...values, runId: run.id, language: language as ExerciseLanguage, exercise })
|
||||
// Create multiple tasks for each iteration
|
||||
for (let iteration = 1; iteration <= iterations; iteration++) {
|
||||
await createTask({
|
||||
...values,
|
||||
runId: run.id,
|
||||
language: language as ExerciseLanguage,
|
||||
exercise,
|
||||
iteration,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const language of exerciseLanguages) {
|
||||
const exercises = await getExercisesForLanguage(EVALS_REPO_PATH, language)
|
||||
const languageExercises = await getExercisesForLanguage(EVALS_REPO_PATH, language)
|
||||
|
||||
await pMap(exercises, (exercise) => createTask({ runId: run.id, language, exercise }), {
|
||||
concurrency: 10,
|
||||
})
|
||||
// Create tasks for all iterations of each exercise
|
||||
const tasksToCreate: Array<{ language: ExerciseLanguage; exercise: string; iteration: number }> = []
|
||||
for (const exercise of languageExercises) {
|
||||
for (let iteration = 1; iteration <= iterations; iteration++) {
|
||||
tasksToCreate.push({ language, exercise, iteration })
|
||||
}
|
||||
}
|
||||
|
||||
await pMap(
|
||||
tasksToCreate,
|
||||
({ language, exercise, iteration }) => createTask({ runId: run.id, language, exercise, iteration }),
|
||||
{ concurrency: 10 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,3 +117,100 @@ 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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import { NextResponse } from "next/server"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "healthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || "production",
|
||||
},
|
||||
{ status: 200 },
|
||||
)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "unhealthy",
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
{ status: 503 },
|
||||
)
|
||||
}
|
||||
}
|
||||
74
apps/web-evals/src/app/api/runs/[id]/logs/[taskId]/route.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as path from "node:path"
|
||||
|
||||
import { findTask, findRun } from "@roo-code/evals"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
const LOG_BASE_PATH = "/tmp/evals/runs"
|
||||
|
||||
// Sanitize path components to prevent path traversal attacks
|
||||
function sanitizePathComponent(component: string): string {
|
||||
// Remove any path separators, null bytes, and other dangerous characters
|
||||
return component.replace(/[/\\:\0*?"<>|]/g, "_")
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string; taskId: string }> }) {
|
||||
const { id, taskId } = await params
|
||||
|
||||
try {
|
||||
const runId = Number(id)
|
||||
const taskIdNum = Number(taskId)
|
||||
|
||||
if (isNaN(runId) || isNaN(taskIdNum)) {
|
||||
return NextResponse.json({ error: "Invalid run ID or task ID" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify the run exists
|
||||
await findRun(runId)
|
||||
|
||||
// Get the task to find its language and exercise
|
||||
const task = await findTask(taskIdNum)
|
||||
|
||||
// Verify the task belongs to this run
|
||||
if (task.runId !== runId) {
|
||||
return NextResponse.json({ error: "Task does not belong to this run" }, { status: 404 })
|
||||
}
|
||||
|
||||
// Sanitize language and exercise to prevent path traversal
|
||||
const safeLanguage = sanitizePathComponent(task.language)
|
||||
const safeExercise = sanitizePathComponent(task.exercise)
|
||||
|
||||
// Construct the log file path
|
||||
const logFileName = `${safeLanguage}-${safeExercise}.log`
|
||||
const logFilePath = path.join(LOG_BASE_PATH, String(runId), logFileName)
|
||||
|
||||
// Verify the resolved path is within the expected directory (defense in depth)
|
||||
const resolvedPath = path.resolve(logFilePath)
|
||||
const expectedBase = path.resolve(LOG_BASE_PATH)
|
||||
if (!resolvedPath.startsWith(expectedBase)) {
|
||||
return NextResponse.json({ error: "Invalid log path" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if the log file exists and read it (async)
|
||||
try {
|
||||
const logContent = await fs.readFile(logFilePath, "utf-8")
|
||||
return NextResponse.json({ logContent })
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return NextResponse.json({ error: "Log file not found", logContent: null }, { status: 200 })
|
||||
}
|
||||
throw err
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reading task log:", error)
|
||||
|
||||
if (error instanceof Error && error.name === "RecordNotFoundError") {
|
||||
return NextResponse.json({ error: "Task or run not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Failed to read log file" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
129
apps/web-evals/src/app/api/runs/[id]/logs/failed/route.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import archiver from "archiver"
|
||||
|
||||
import { findRun, getTasks } from "@roo-code/evals"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
const LOG_BASE_PATH = "/tmp/evals/runs"
|
||||
|
||||
// Sanitize path components to prevent path traversal attacks
|
||||
function sanitizePathComponent(component: string): string {
|
||||
// Remove any path separators, null bytes, and other dangerous characters
|
||||
return component.replace(/[/\\:\0*?"<>|]/g, "_")
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const runId = Number(id)
|
||||
|
||||
if (isNaN(runId)) {
|
||||
return NextResponse.json({ error: "Invalid run ID" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Verify the run exists
|
||||
await findRun(runId)
|
||||
|
||||
// Get all tasks for this run
|
||||
const tasks = await getTasks(runId)
|
||||
|
||||
// Filter for failed tasks only
|
||||
const failedTasks = tasks.filter((task) => task.passed === false)
|
||||
|
||||
if (failedTasks.length === 0) {
|
||||
return NextResponse.json({ error: "No failed tasks to export" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create a zip archive
|
||||
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||
|
||||
// Collect chunks to build the response
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
archive.on("data", (chunk: Buffer) => {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
|
||||
// Track archive errors
|
||||
let archiveError: Error | null = null
|
||||
archive.on("error", (err: Error) => {
|
||||
archiveError = err
|
||||
})
|
||||
|
||||
// Set up the end promise before finalizing (proper event listener ordering)
|
||||
const archiveEndPromise = new Promise<void>((resolve, reject) => {
|
||||
archive.on("end", resolve)
|
||||
archive.on("error", reject)
|
||||
})
|
||||
|
||||
// Add each failed task's log file to the archive
|
||||
const logDir = path.join(LOG_BASE_PATH, String(runId))
|
||||
let filesAdded = 0
|
||||
|
||||
for (const task of failedTasks) {
|
||||
// Sanitize language and exercise to prevent path traversal
|
||||
const safeLanguage = sanitizePathComponent(task.language)
|
||||
const safeExercise = sanitizePathComponent(task.exercise)
|
||||
const logFileName = `${safeLanguage}-${safeExercise}.log`
|
||||
const logFilePath = path.join(logDir, logFileName)
|
||||
|
||||
// Verify the resolved path is within the expected directory (defense in depth)
|
||||
const resolvedPath = path.resolve(logFilePath)
|
||||
const expectedBase = path.resolve(LOG_BASE_PATH)
|
||||
if (!resolvedPath.startsWith(expectedBase)) {
|
||||
continue // Skip files with suspicious paths
|
||||
}
|
||||
|
||||
if (fs.existsSync(logFilePath)) {
|
||||
archive.file(logFilePath, { name: logFileName })
|
||||
filesAdded++
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any files were actually added
|
||||
if (filesAdded === 0) {
|
||||
archive.abort()
|
||||
return NextResponse.json(
|
||||
{ error: "No log files found - they may have been cleared from disk" },
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
// Finalize the archive
|
||||
await archive.finalize()
|
||||
|
||||
// Wait for all data to be collected
|
||||
await archiveEndPromise
|
||||
|
||||
// Check for archive errors
|
||||
if (archiveError) {
|
||||
throw archiveError
|
||||
}
|
||||
|
||||
// Combine all chunks into a single buffer
|
||||
const zipBuffer = Buffer.concat(chunks)
|
||||
|
||||
// Return the zip file
|
||||
return new NextResponse(zipBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="run-${runId}-failed-logs.zip"`,
|
||||
"Content-Length": String(zipBuffer.length),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error exporting failed logs:", error)
|
||||
|
||||
if (error instanceof Error && error.name === "RecordNotFoundError") {
|
||||
return NextResponse.json({ error: "Run not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Failed to export logs" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||
const run = await findRun(Number(id))
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-12 p-12">
|
||||
<div className="w-full px-6 py-12">
|
||||
<Run run={run} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,55 +1,79 @@
|
|||
"use client"
|
||||
|
||||
import { Link2, Link2Off, CheckCircle2 } from "lucide-react"
|
||||
import type { RunStatus as _RunStatus } from "@/hooks/use-run-status"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui"
|
||||
|
||||
export const RunStatus = ({ runStatus: { sseStatus, heartbeat, runners = [] } }: { runStatus: _RunStatus }) => (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>Task Stream:</div>
|
||||
<div className="font-mono text-sm text-muted-foreground">{sseStatus}</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
|
||||
"bg-green-500": sseStatus === "connected",
|
||||
"bg-amber-500": sseStatus === "waiting",
|
||||
"bg-rose-500": sseStatus === "error",
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={cn("size-2.5 rounded-full", {
|
||||
"bg-green-500": sseStatus === "connected",
|
||||
"bg-amber-500": sseStatus === "waiting",
|
||||
"bg-rose-500": sseStatus === "error",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>Task Controller:</div>
|
||||
<div className="font-mono text-sm text-muted-foreground">{heartbeat ?? "dead"}</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
|
||||
"bg-green-500": !!heartbeat,
|
||||
"bg-rose-500": !heartbeat,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={cn("size-2.5 rounded-full", {
|
||||
"bg-green-500": !!heartbeat,
|
||||
"bg-rose-500": !heartbeat,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div>Task Runners:</div>
|
||||
{runners.length > 0 && <div className="font-mono text-sm text-muted-foreground">{runners?.join(", ")}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
function StreamIcon({ status }: { status: "connected" | "waiting" | "error" }) {
|
||||
if (status === "connected") {
|
||||
return <Link2 className="size-4 text-green-500" />
|
||||
}
|
||||
return <Link2Off className={cn("size-4", status === "waiting" ? "text-amber-500" : "text-rose-500")} />
|
||||
}
|
||||
|
||||
export const RunStatus = ({
|
||||
runStatus: { sseStatus, heartbeat, runners = [] },
|
||||
isComplete = false,
|
||||
}: {
|
||||
runStatus: _RunStatus
|
||||
isComplete?: boolean
|
||||
}) => {
|
||||
// For completed runs, show a simple "Complete" badge
|
||||
if (isComplete) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1 cursor-default text-muted-foreground">
|
||||
<CheckCircle2 className="size-4" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="font-mono text-xs">
|
||||
Run complete
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2 cursor-default text-xs font-mono">
|
||||
{/* Task Stream status icon */}
|
||||
<StreamIcon status={sseStatus} />
|
||||
|
||||
{/* Task Controller ID */}
|
||||
<span className={heartbeat ? "text-green-500" : "text-rose-500"}>{heartbeat ?? "-"}</span>
|
||||
|
||||
{/* Task Runners count */}
|
||||
<span className={runners.length > 0 ? "text-green-500" : "text-rose-500"}>
|
||||
{runners.length > 0 ? `${runners.length}r` : "0r"}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="font-mono text-xs max-w-md">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<StreamIcon status={sseStatus} />
|
||||
<span>Task Stream: {sseStatus}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={heartbeat ? "text-green-500" : "text-rose-500"}>●</span>
|
||||
<span>Task Controller: {heartbeat ?? "dead"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={runners.length > 0 ? "text-green-500" : "text-rose-500"}>●</span>
|
||||
<span>Task Runners: {runners.length > 0 ? runners.length : "none"}</span>
|
||||
</div>
|
||||
{runners.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-border text-muted-foreground space-y-0.5">
|
||||
{runners.map((runner) => (
|
||||
<div key={runner}>{runner}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,339 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { LoaderCircle } from "lucide-react"
|
||||
import { useMemo, useState, useCallback, useEffect } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { LoaderCircle, FileText, Copy, Check, StopCircle } from "lucide-react"
|
||||
|
||||
import type { Run, TaskMetrics as _TaskMetrics } from "@roo-code/evals"
|
||||
import type { Run, TaskMetrics as _TaskMetrics, Task } from "@roo-code/evals"
|
||||
import type { ToolName } from "@roo-code/types"
|
||||
|
||||
import { formatCurrency, formatDuration, formatTokens } from "@/lib/formatters"
|
||||
import { formatCurrency, formatDuration, formatTokens, formatToolUsageSuccessRate } from "@/lib/formatters"
|
||||
import { useRunStatus } from "@/hooks/use-run-status"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
|
||||
import { killRun } from "@/actions/runs"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
ScrollArea,
|
||||
Button,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui"
|
||||
|
||||
import { TaskStatus } from "./task-status"
|
||||
import { RunStatus } from "./run-status"
|
||||
|
||||
type TaskMetrics = Pick<_TaskMetrics, "tokensIn" | "tokensOut" | "tokensContext" | "duration" | "cost">
|
||||
|
||||
type ToolUsageEntry = { attempts: number; failures: number }
|
||||
type ToolUsage = Record<string, ToolUsageEntry>
|
||||
|
||||
// Generate abbreviation from tool name (e.g., "read_file" -> "RF", "list_code_definition_names" -> "LCDN")
|
||||
function getToolAbbreviation(toolName: string): string {
|
||||
return toolName
|
||||
.split("_")
|
||||
.map((word) => word[0]?.toUpperCase() ?? "")
|
||||
.join("")
|
||||
}
|
||||
|
||||
// Pattern definitions for syntax highlighting
|
||||
type HighlightPattern = {
|
||||
pattern: RegExp
|
||||
className: string
|
||||
// If true, wraps the entire match; if a number, wraps that capture group
|
||||
wrapGroup?: number
|
||||
}
|
||||
|
||||
const HIGHLIGHT_PATTERNS: HighlightPattern[] = [
|
||||
// Log levels - styled as badges
|
||||
{ pattern: /\|\s*(INFO)\s*\|/g, className: "text-green-400", wrapGroup: 1 },
|
||||
{ pattern: /\|\s*(WARN|WARNING)\s*\|/g, className: "text-yellow-400", wrapGroup: 1 },
|
||||
{ pattern: /\|\s*(ERROR)\s*\|/g, className: "text-red-400 font-semibold", wrapGroup: 1 },
|
||||
{ pattern: /\|\s*(DEBUG)\s*\|/g, className: "text-gray-400", wrapGroup: 1 },
|
||||
// Task identifiers - important events
|
||||
{
|
||||
pattern: /(taskCreated|taskFocused|taskStarted|taskCompleted|taskAborted|taskResumable)/g,
|
||||
className: "text-purple-400 font-medium",
|
||||
},
|
||||
// Tool failures - highlight in red
|
||||
{ pattern: /(taskToolFailed)/g, className: "text-red-400 font-bold" },
|
||||
{ pattern: /(Tool execution failed|tool.*failed|failed.*tool)/gi, className: "text-red-400" },
|
||||
{ pattern: /(EvalPass)/g, className: "text-green-400 font-bold" },
|
||||
{ pattern: /(EvalFail)/g, className: "text-red-400 font-bold" },
|
||||
// Message arrows
|
||||
{ pattern: /→/g, className: "text-cyan-400" },
|
||||
// Tool names in quotes
|
||||
{ pattern: /"(tool)":\s*"([^"]+)"/g, className: "text-orange-400" },
|
||||
// JSON keys
|
||||
{ pattern: /"([^"]+)":/g, className: "text-sky-300" },
|
||||
// Boolean values
|
||||
{ pattern: /:\s*(true|false)/g, className: "text-amber-400", wrapGroup: 1 },
|
||||
// Numbers
|
||||
{ pattern: /:\s*(-?\d+\.?\d*)/g, className: "text-emerald-400", wrapGroup: 1 },
|
||||
]
|
||||
|
||||
// Extract timestamp from a log line and return elapsed time from baseline
|
||||
function formatElapsedTime(timestamp: string, baselineMs: number): string {
|
||||
const currentMs = new Date(timestamp).getTime()
|
||||
const elapsedMs = currentMs - baselineMs
|
||||
const totalSeconds = Math.floor(elapsedMs / 1000)
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
// Extract the first timestamp from the log to use as baseline
|
||||
function extractFirstTimestamp(log: string): number | null {
|
||||
// Match timestamp at start of line: [2025-11-28T09:35:23.187Z | ... or [2025-11-28T09:35:23.187Z]
|
||||
const match = log.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)[\s|\]]/)
|
||||
const isoString = match?.[1]
|
||||
if (!isoString) return null
|
||||
return new Date(isoString).getTime()
|
||||
}
|
||||
|
||||
// Simplify log line by removing redundant metadata
|
||||
function simplifyLogLine(line: string, baselineMs: number | null): { timestamp: string; simplified: string } {
|
||||
// Extract timestamp - matches [2025-11-28T09:35:23.187Z | ... format
|
||||
const timestampMatch = line.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)[\s|\]]/)
|
||||
const isoTimestamp = timestampMatch?.[1]
|
||||
if (!isoTimestamp) {
|
||||
return { timestamp: "", simplified: line }
|
||||
}
|
||||
|
||||
const timestamp = baselineMs !== null ? formatElapsedTime(isoTimestamp, baselineMs) : isoTimestamp.slice(11, 19)
|
||||
|
||||
// Remove the timestamp from the line (handles both [timestamp] and [timestamp | formats)
|
||||
let simplified = line.replace(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\s*\|?\s*/, "")
|
||||
|
||||
// Remove redundant metadata: pid, run, task IDs (they're same for entire log)
|
||||
simplified = simplified.replace(/\|\s*pid:\d+\s*/g, "")
|
||||
simplified = simplified.replace(/\|\s*run:\d+\s*/g, "")
|
||||
simplified = simplified.replace(/\|\s*task:\d+\s*/g, "")
|
||||
simplified = simplified.replace(/runTask\s*\|\s*/g, "")
|
||||
|
||||
// Clean up extra pipes, spaces, and trailing brackets
|
||||
simplified = simplified.replace(/\|\s*\|/g, "|")
|
||||
simplified = simplified.replace(/^\s*\|\s*/, "")
|
||||
simplified = simplified.replace(/\]\s*$/, "") // Remove trailing bracket if present
|
||||
|
||||
return { timestamp, simplified }
|
||||
}
|
||||
|
||||
// Format a single line with syntax highlighting using React elements (XSS-safe)
|
||||
function formatLine(line: string): React.ReactNode[] {
|
||||
// Find all matches with their positions
|
||||
type Match = { start: number; end: number; text: string; className: string }
|
||||
const matches: Match[] = []
|
||||
|
||||
for (const { pattern, className, wrapGroup } of HIGHLIGHT_PATTERNS) {
|
||||
// Reset regex state
|
||||
pattern.lastIndex = 0
|
||||
let regexMatch
|
||||
while ((regexMatch = pattern.exec(line)) !== null) {
|
||||
const capturedText = wrapGroup !== undefined ? regexMatch[wrapGroup] : regexMatch[0]
|
||||
// Skip if capture group didn't match
|
||||
if (!capturedText) continue
|
||||
const start =
|
||||
wrapGroup !== undefined ? regexMatch.index + regexMatch[0].indexOf(capturedText) : regexMatch.index
|
||||
matches.push({
|
||||
start,
|
||||
end: start + capturedText.length,
|
||||
text: capturedText,
|
||||
className,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort matches by position and filter overlapping ones
|
||||
matches.sort((a, b) => a.start - b.start)
|
||||
const filteredMatches: Match[] = []
|
||||
for (const m of matches) {
|
||||
const lastMatch = filteredMatches[filteredMatches.length - 1]
|
||||
if (!lastMatch || m.start >= lastMatch.end) {
|
||||
filteredMatches.push(m)
|
||||
}
|
||||
}
|
||||
|
||||
// Build result with highlighted spans
|
||||
const result: React.ReactNode[] = []
|
||||
let currentPos = 0
|
||||
|
||||
for (const [i, m] of filteredMatches.entries()) {
|
||||
// Add text before this match
|
||||
if (m.start > currentPos) {
|
||||
result.push(line.slice(currentPos, m.start))
|
||||
}
|
||||
// Add highlighted match
|
||||
result.push(
|
||||
<span key={`${i}-${m.start}`} className={m.className}>
|
||||
{m.text}
|
||||
</span>,
|
||||
)
|
||||
currentPos = m.end
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (currentPos < line.length) {
|
||||
result.push(line.slice(currentPos))
|
||||
}
|
||||
|
||||
return result.length > 0 ? result : [line]
|
||||
}
|
||||
|
||||
// Determine the visual style for a log line based on its content
|
||||
function getLineStyle(line: string): string {
|
||||
if (line.includes("ERROR")) return "bg-red-950/30 border-l-2 border-red-500"
|
||||
if (line.includes("WARN") || line.includes("WARNING")) return "bg-yellow-950/20 border-l-2 border-yellow-500"
|
||||
if (line.includes("taskToolFailed")) return "bg-red-950/30 border-l-2 border-red-500"
|
||||
if (line.includes("taskStarted") || line.includes("taskCreated")) return "bg-purple-950/20"
|
||||
if (line.includes("EvalPass")) return "bg-green-950/30 border-l-2 border-green-500"
|
||||
if (line.includes("EvalFail")) return "bg-red-950/30 border-l-2 border-red-500"
|
||||
if (line.includes("taskCompleted") || line.includes("taskAborted")) return "bg-blue-950/20"
|
||||
return ""
|
||||
}
|
||||
|
||||
// Format log content with basic highlighting (XSS-safe - no dangerouslySetInnerHTML)
|
||||
function formatLogContent(log: string): React.ReactNode[] {
|
||||
const lines = log.split("\n")
|
||||
const baselineMs = extractFirstTimestamp(log)
|
||||
|
||||
return lines.map((line, index) => {
|
||||
if (!line.trim()) {
|
||||
return (
|
||||
<div key={index} className="h-2">
|
||||
{" "}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const parsed = simplifyLogLine(line, baselineMs)
|
||||
const lineStyle = getLineStyle(line)
|
||||
|
||||
return (
|
||||
<div key={index} className={`flex hover:bg-white/10 py-0.5 rounded-sm transition-colors ${lineStyle}`}>
|
||||
{/* Elapsed time */}
|
||||
<span className="text-blue-400 font-mono w-12 flex-shrink-0 tabular-nums text-right pr-2">
|
||||
{parsed.timestamp}
|
||||
</span>
|
||||
{/* Log content - pl-12 ensures wrapped lines are indented under the timestamp */}
|
||||
<span className="flex-1 break-words" style={{ textIndent: "-0.5rem", paddingLeft: "0.5rem" }}>
|
||||
{formatLine(parsed.simplified)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function Run({ run }: { run: Run }) {
|
||||
const runStatus = useRunStatus(run)
|
||||
const { tasks, tokenUsage, usageUpdatedAt } = runStatus
|
||||
const { tasks, tokenUsage, usageUpdatedAt, heartbeat, runners } = runStatus
|
||||
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
|
||||
const [taskLog, setTaskLog] = useState<string | null>(null)
|
||||
const [isLoadingLog, setIsLoadingLog] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showKillDialog, setShowKillDialog] = useState(false)
|
||||
const [isKilling, setIsKilling] = useState(false)
|
||||
|
||||
// Determine if run is still active (has heartbeat or runners)
|
||||
const isRunActive = !run.taskMetricsId && (!!heartbeat || (runners && runners.length > 0))
|
||||
|
||||
const onKillRun = useCallback(async () => {
|
||||
setIsKilling(true)
|
||||
try {
|
||||
const result = await killRun(run.id)
|
||||
if (result.killedContainers.length > 0) {
|
||||
toast.success(`Killed ${result.killedContainers.length} container(s)`)
|
||||
} else if (result.errors.length === 0) {
|
||||
toast.info("No running containers found")
|
||||
} else {
|
||||
toast.error(result.errors.join(", "))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to kill run:", error)
|
||||
toast.error("Failed to kill run")
|
||||
} finally {
|
||||
setIsKilling(false)
|
||||
setShowKillDialog(false)
|
||||
}
|
||||
}, [run.id])
|
||||
|
||||
const onCopyLog = useCallback(async () => {
|
||||
if (!taskLog) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(taskLog)
|
||||
setCopied(true)
|
||||
toast.success("Log copied to clipboard")
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (error) {
|
||||
console.error("Failed to copy log:", error)
|
||||
toast.error("Failed to copy log")
|
||||
}
|
||||
}, [taskLog])
|
||||
|
||||
// Handle ESC key to close the dialog
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && selectedTask) {
|
||||
setSelectedTask(null)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
return () => document.removeEventListener("keydown", handleKeyDown)
|
||||
}, [selectedTask])
|
||||
|
||||
const onViewTaskLog = useCallback(
|
||||
async (task: Task) => {
|
||||
// Only allow viewing logs for tasks that have started
|
||||
if (!task.startedAt && !tokenUsage.get(task.id)) {
|
||||
toast.error("Task has not started yet")
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedTask(task)
|
||||
setIsLoadingLog(true)
|
||||
setTaskLog(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/runs/${run.id}/logs/${task.id}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
toast.error(error.error || "Failed to load log")
|
||||
setSelectedTask(null)
|
||||
return
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setTaskLog(data.logContent)
|
||||
} catch (error) {
|
||||
console.error("Error loading task log:", error)
|
||||
toast.error("Failed to load log")
|
||||
setSelectedTask(null)
|
||||
} finally {
|
||||
setIsLoadingLog(false)
|
||||
}
|
||||
},
|
||||
[run.id, tokenUsage],
|
||||
)
|
||||
|
||||
const taskMetrics: Record<number, TaskMetrics> = useMemo(() => {
|
||||
const metrics: Record<number, TaskMetrics> = {}
|
||||
|
|
@ -41,16 +358,239 @@ export function Run({ run }: { run: Run }) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tasks, tokenUsage, usageUpdatedAt])
|
||||
|
||||
// Collect all unique tool names from all tasks and sort by total attempts
|
||||
const toolColumns = useMemo<ToolName[]>(() => {
|
||||
if (!tasks) return []
|
||||
|
||||
const toolTotals = new Map<ToolName, number>()
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.taskMetrics?.toolUsage) {
|
||||
for (const [toolName, usage] of Object.entries(task.taskMetrics.toolUsage)) {
|
||||
const tool = toolName as ToolName
|
||||
const current = toolTotals.get(tool) ?? 0
|
||||
toolTotals.set(tool, current + usage.attempts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by total attempts descending
|
||||
return Array.from(toolTotals.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([name]): ToolName => name)
|
||||
}, [tasks])
|
||||
|
||||
// Compute aggregate stats
|
||||
const stats = useMemo(() => {
|
||||
if (!tasks) return null
|
||||
|
||||
const passed = tasks.filter((t) => t.passed === true).length
|
||||
const failed = tasks.filter((t) => t.passed === false).length
|
||||
const completed = passed + failed
|
||||
|
||||
let totalTokensIn = 0
|
||||
let totalTokensOut = 0
|
||||
let totalCost = 0
|
||||
let totalDuration = 0
|
||||
|
||||
// Aggregate tool usage from completed tasks
|
||||
const toolUsage: ToolUsage = {}
|
||||
|
||||
for (const task of tasks) {
|
||||
const metrics = taskMetrics[task.id]
|
||||
if (metrics) {
|
||||
totalTokensIn += metrics.tokensIn
|
||||
totalTokensOut += metrics.tokensOut
|
||||
totalCost += metrics.cost
|
||||
totalDuration += metrics.duration
|
||||
}
|
||||
|
||||
// Aggregate tool usage from finished tasks with taskMetrics
|
||||
if (task.finishedAt && task.taskMetrics?.toolUsage) {
|
||||
for (const [key, usage] of Object.entries(task.taskMetrics.toolUsage)) {
|
||||
const tool = key as keyof ToolUsage
|
||||
if (!toolUsage[tool]) {
|
||||
toolUsage[tool] = { attempts: 0, failures: 0 }
|
||||
}
|
||||
toolUsage[tool].attempts += usage.attempts
|
||||
toolUsage[tool].failures += usage.failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passed,
|
||||
failed,
|
||||
completed,
|
||||
passRate: completed > 0 ? ((passed / completed) * 100).toFixed(1) : null,
|
||||
totalTokensIn,
|
||||
totalTokensOut,
|
||||
totalCost,
|
||||
totalDuration,
|
||||
toolUsage,
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tasks, taskMetrics, tokenUsage, usageUpdatedAt])
|
||||
|
||||
// Calculate elapsed time (wall-clock time from run creation to completion or now)
|
||||
const elapsedTime = useMemo(() => {
|
||||
if (!tasks || tasks.length === 0) return null
|
||||
|
||||
const startTime = new Date(run.createdAt).getTime()
|
||||
|
||||
// If run is complete, find the latest finishedAt from tasks
|
||||
if (run.taskMetricsId) {
|
||||
const latestFinish = tasks.reduce((latest, task) => {
|
||||
if (task.finishedAt) {
|
||||
const finishTime = new Date(task.finishedAt).getTime()
|
||||
return finishTime > latest ? finishTime : latest
|
||||
}
|
||||
return latest
|
||||
}, startTime)
|
||||
return latestFinish - startTime
|
||||
}
|
||||
|
||||
// If still running, use current time
|
||||
return Date.now() - startTime
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tasks, run.createdAt, run.taskMetricsId, usageUpdatedAt])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<div>
|
||||
<div className="font-mono">{run.model}</div>
|
||||
{run.description && <div className="text-sm text-muted-foreground">{run.description}</div>}
|
||||
{stats && (
|
||||
<div className="mb-4 p-4 border rounded-lg bg-muted sticky top-0 z-10">
|
||||
{/* Provider, Model title and status */}
|
||||
<div className="flex items-center justify-center gap-3 mb-3 relative">
|
||||
{run.settings?.apiProvider && (
|
||||
<span className="text-sm text-muted-foreground">{run.settings.apiProvider}</span>
|
||||
)}
|
||||
<div className="font-mono">{run.model}</div>
|
||||
<RunStatus runStatus={runStatus} isComplete={!!run.taskMetricsId} />
|
||||
{run.description && (
|
||||
<span className="text-sm text-muted-foreground">- {run.description}</span>
|
||||
)}
|
||||
{isRunActive && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowKillDialog(true)}
|
||||
disabled={isKilling}
|
||||
className="absolute right-0 flex items-center gap-1 text-muted-foreground hover:text-destructive">
|
||||
{isKilling ? (
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
) : (
|
||||
<StopCircle className="size-4" />
|
||||
)}
|
||||
Kill
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop all containers for this run</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{/* Main Stats Row */}
|
||||
<div className="flex items-start justify-center gap-x-8 gap-y-3">
|
||||
{/* Passed/Failed */}
|
||||
<div className="text-center min-w-[80px]">
|
||||
<div className="text-2xl font-bold whitespace-nowrap">
|
||||
<span className="text-green-600">{stats.passed}</span>
|
||||
<span className="text-muted-foreground mx-1">/</span>
|
||||
<span className="text-red-600">{stats.failed}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Passed / Failed</div>
|
||||
</div>
|
||||
|
||||
{/* Pass Rate */}
|
||||
<div className="text-center min-w-[80px]">
|
||||
<div
|
||||
className={`text-2xl font-bold ${
|
||||
stats.passRate === null
|
||||
? ""
|
||||
: parseFloat(stats.passRate) === 100
|
||||
? ""
|
||||
: parseFloat(stats.passRate) >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
}`}>
|
||||
{stats.passRate ? `${stats.passRate}%` : "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Pass Rate</div>
|
||||
</div>
|
||||
|
||||
{/* Tokens */}
|
||||
<div className="text-center min-w-[140px]">
|
||||
<div className="text-xl font-bold font-mono whitespace-nowrap">
|
||||
{formatTokens(stats.totalTokensIn)}
|
||||
<span className="text-muted-foreground mx-1">/</span>
|
||||
{formatTokens(stats.totalTokensOut)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Tokens In / Out</div>
|
||||
</div>
|
||||
|
||||
{/* Cost */}
|
||||
<div className="text-center min-w-[70px]">
|
||||
<div className="text-2xl font-bold font-mono">{formatCurrency(stats.totalCost)}</div>
|
||||
<div className="text-xs text-muted-foreground">Cost</div>
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className="text-center min-w-[90px]">
|
||||
<div className="text-2xl font-bold font-mono whitespace-nowrap">
|
||||
{stats.totalDuration > 0 ? formatDuration(stats.totalDuration) : "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Duration</div>
|
||||
</div>
|
||||
|
||||
{/* Elapsed Time */}
|
||||
<div className="text-center min-w-[90px]">
|
||||
<div className="text-2xl font-bold font-mono whitespace-nowrap">
|
||||
{elapsedTime !== null ? formatDuration(elapsedTime) : "-"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Elapsed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tool Usage Row */}
|
||||
{Object.keys(stats.toolUsage).length > 0 && (
|
||||
<div className="flex items-center justify-center gap-2 flex-wrap mt-3">
|
||||
{Object.entries(stats.toolUsage)
|
||||
.sort(([, a], [, b]) => b.attempts - a.attempts)
|
||||
.map(([toolName, usage]) => {
|
||||
const abbr = getToolAbbreviation(toolName)
|
||||
const successRate =
|
||||
usage.attempts > 0
|
||||
? ((usage.attempts - usage.failures) / usage.attempts) * 100
|
||||
: 100
|
||||
const rateColor =
|
||||
successRate === 100
|
||||
? "text-green-500"
|
||||
: successRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
return (
|
||||
<Tooltip key={toolName}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded bg-background/50 border border-border/50 hover:border-border transition-colors cursor-default text-xs">
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{abbr}
|
||||
</span>
|
||||
<span className="font-bold tabular-nums">{usage.attempts}</span>
|
||||
<span className={`${rateColor}`}>
|
||||
{formatToolUsageSuccessRate(usage)}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{toolName}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!run.taskMetricsId && <RunStatus runStatus={runStatus} />}
|
||||
</div>
|
||||
)}
|
||||
{!tasks ? (
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
) : (
|
||||
|
|
@ -60,53 +600,206 @@ export function Run({ run }: { run: Run }) {
|
|||
<TableHead>Exercise</TableHead>
|
||||
<TableHead className="text-center">Tokens In / Out</TableHead>
|
||||
<TableHead>Context</TableHead>
|
||||
{toolColumns.map((toolName) => (
|
||||
<TableHead key={toolName} className="text-xs text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{getToolAbbreviation(toolName)}</TooltipTrigger>
|
||||
<TooltipContent>{toolName}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableHead>
|
||||
))}
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead>Cost</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.map((task) => (
|
||||
<TableRow key={task.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<TaskStatus
|
||||
task={task}
|
||||
running={!!task.startedAt || !!tokenUsage.get(task.id)}
|
||||
/>
|
||||
<div>
|
||||
{task.language}/{task.exercise}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
{taskMetrics[task.id] ? (
|
||||
<>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<div className="flex items-center justify-evenly">
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensIn)}</div>/
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensOut)}</div>
|
||||
{tasks.map((task) => {
|
||||
const hasStarted = !!task.startedAt || !!tokenUsage.get(task.id)
|
||||
return (
|
||||
<TableRow
|
||||
key={task.id}
|
||||
className={`${hasStarted ? "cursor-pointer hover:bg-muted/50" : ""} ${task.passed === false ? "bg-red-950/30 border-l-2 border-l-red-500" : ""}`}
|
||||
onClick={() => hasStarted && onViewTaskLog(task)}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<TaskStatus task={task} running={hasStarted} />
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
{task.language}/{task.exercise}
|
||||
{task.iteration > 1 && (
|
||||
<span className="text-muted-foreground ml-1">
|
||||
(#{task.iteration})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{hasStarted && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FileText className="size-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Click to view log</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatTokens(taskMetrics[task.id]!.tokensContext)}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{taskMetrics[task.id]!.duration
|
||||
? formatDuration(taskMetrics[task.id]!.duration)
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatCurrency(taskMetrics[task.id]!.cost)}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<TableCell colSpan={4} />
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
{taskMetrics[task.id] ? (
|
||||
<>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<div className="flex items-center justify-evenly">
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensIn)}</div>/
|
||||
<div>{formatTokens(taskMetrics[task.id]!.tokensOut)}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatTokens(taskMetrics[task.id]!.tokensContext)}
|
||||
</TableCell>
|
||||
{toolColumns.map((toolName) => {
|
||||
const usage = task.taskMetrics?.toolUsage?.[toolName]
|
||||
const successRate =
|
||||
usage && usage.attempts > 0
|
||||
? ((usage.attempts - usage.failures) / usage.attempts) * 100
|
||||
: 100
|
||||
const rateColor =
|
||||
successRate === 100
|
||||
? "text-muted-foreground"
|
||||
: successRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
return (
|
||||
<TableCell key={toolName} className="text-xs text-center">
|
||||
{usage ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">
|
||||
{usage.attempts}
|
||||
</span>
|
||||
<span className={rateColor}>
|
||||
{formatToolUsageSuccessRate(usage)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell className="font-mono text-xs">
|
||||
{taskMetrics[task.id]!.duration
|
||||
? formatDuration(taskMetrics[task.id]!.duration)
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{formatCurrency(taskMetrics[task.id]!.cost)}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<TableCell colSpan={4 + toolColumns.length} />
|
||||
)}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task Log Dialog - Full Screen */}
|
||||
<Dialog open={!!selectedTask} onOpenChange={() => setSelectedTask(null)}>
|
||||
<DialogContent className="w-[95vw] !max-w-[95vw] h-[90vh] flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="size-4" />
|
||||
{selectedTask?.language}/{selectedTask?.exercise}
|
||||
{selectedTask?.iteration && selectedTask.iteration > 1 && (
|
||||
<span className="text-muted-foreground">(#{selectedTask.iteration})</span>
|
||||
)}
|
||||
<span
|
||||
className={`ml-2 text-sm ${
|
||||
selectedTask?.passed === true
|
||||
? "text-green-600"
|
||||
: selectedTask?.passed === false
|
||||
? "text-red-600"
|
||||
: "text-yellow-500"
|
||||
}`}>
|
||||
(
|
||||
{selectedTask?.passed === true
|
||||
? "Passed"
|
||||
: selectedTask?.passed === false
|
||||
? "Failed"
|
||||
: "Running"}
|
||||
)
|
||||
</span>
|
||||
</DialogTitle>
|
||||
{taskLog && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCopyLog}
|
||||
className="flex items-center gap-1">
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="size-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="size-4" />
|
||||
Copy Log
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
{isLoadingLog ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<LoaderCircle className="size-6 animate-spin" />
|
||||
</div>
|
||||
) : taskLog ? (
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="text-xs font-mono bg-muted p-4 rounded-md overflow-x-auto">
|
||||
{formatLogContent(taskLog)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
Log file not available (may have been cleared)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Kill Run Confirmation Dialog */}
|
||||
<AlertDialog open={showKillDialog} onOpenChange={setShowKillDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Kill Run?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will stop the controller and all task runner containers for this run. Any running tasks
|
||||
will be terminated immediately. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isKilling}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onKillRun}
|
||||
disabled={isKilling}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
{isKilling ? (
|
||||
<>
|
||||
<LoaderCircle className="size-4 animate-spin mr-2" />
|
||||
Killing...
|
||||
</>
|
||||
) : (
|
||||
"Kill Run"
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,55 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { z } from "zod"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useForm, FormProvider } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { toast } from "sonner"
|
||||
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, CircleCheck } from "lucide-react"
|
||||
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, Info } from "lucide-react"
|
||||
|
||||
import { globalSettingsSchema, providerSettingsSchema, EVALS_SETTINGS, getModelId } from "@roo-code/types"
|
||||
import {
|
||||
globalSettingsSchema,
|
||||
providerSettingsSchema,
|
||||
EVALS_SETTINGS,
|
||||
getModelId,
|
||||
type ProviderSettings,
|
||||
type GlobalSettings,
|
||||
type ReasoningEffort,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { createRun } from "@/actions/runs"
|
||||
import { getExercises } from "@/actions/exercises"
|
||||
|
||||
import {
|
||||
createRunSchema,
|
||||
type CreateRun,
|
||||
MODEL_DEFAULT,
|
||||
createRunSchema,
|
||||
CONCURRENCY_MIN,
|
||||
CONCURRENCY_MAX,
|
||||
CONCURRENCY_DEFAULT,
|
||||
TIMEOUT_MIN,
|
||||
TIMEOUT_MAX,
|
||||
TIMEOUT_DEFAULT,
|
||||
ITERATIONS_MIN,
|
||||
ITERATIONS_MAX,
|
||||
ITERATIONS_DEFAULT,
|
||||
} from "@/lib/schemas"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import { useOpenRouterModels } from "@/hooks/use-open-router-models"
|
||||
import { useRooCodeCloudModels } from "@/hooks/use-roo-code-cloud-models"
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
Tabs,
|
||||
TabsList,
|
||||
|
|
@ -48,36 +64,66 @@ import {
|
|||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
ScrollArea,
|
||||
ScrollBar,
|
||||
Slider,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui"
|
||||
|
||||
import { SettingsDiff } from "./settings-diff"
|
||||
|
||||
type ImportedSettings = {
|
||||
apiConfigs: Record<string, ProviderSettings>
|
||||
globalSettings: GlobalSettings
|
||||
currentApiConfigName: string
|
||||
}
|
||||
|
||||
export function NewRun() {
|
||||
const router = useRouter()
|
||||
|
||||
const [mode, setMode] = useState<"openrouter" | "settings">("openrouter")
|
||||
const [modelSearchValue, setModelSearchValue] = useState("")
|
||||
const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("other")
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
|
||||
const [useNativeToolProtocol, setUseNativeToolProtocol] = useState(true)
|
||||
const [useMultipleNativeToolCalls, setUseMultipleNativeToolCalls] = useState(false)
|
||||
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort | "">("")
|
||||
const [commandExecutionTimeout, setCommandExecutionTimeout] = useState(20)
|
||||
const [terminalShellIntegrationTimeout, setTerminalShellIntegrationTimeout] = useState(30) // seconds
|
||||
|
||||
const modelSearchResultsRef = useRef<Map<string, number>>(new Map())
|
||||
const modelSearchValueRef = useRef("")
|
||||
// State for imported settings with config selection
|
||||
const [importedSettings, setImportedSettings] = useState<ImportedSettings | null>(null)
|
||||
const [selectedConfigName, setSelectedConfigName] = useState<string>("")
|
||||
const [configPopoverOpen, setConfigPopoverOpen] = useState(false)
|
||||
|
||||
const openRouter = useOpenRouterModels()
|
||||
const rooCodeCloud = useRooCodeCloudModels()
|
||||
const models = provider === "openrouter" ? openRouter.data : rooCodeCloud.data
|
||||
const searchValue = provider === "openrouter" ? openRouter.searchValue : rooCodeCloud.searchValue
|
||||
const setSearchValue = provider === "openrouter" ? openRouter.setSearchValue : rooCodeCloud.setSearchValue
|
||||
const onFilter = provider === "openrouter" ? openRouter.onFilter : rooCodeCloud.onFilter
|
||||
|
||||
const models = useOpenRouterModels()
|
||||
const exercises = useQuery({ queryKey: ["getExercises"], queryFn: () => getExercises() })
|
||||
|
||||
// State for selected exercises (needed for language toggle buttons)
|
||||
const [selectedExercises, setSelectedExercises] = useState<string[]>([])
|
||||
|
||||
const form = useForm<CreateRun>({
|
||||
resolver: zodResolver(createRunSchema),
|
||||
defaultValues: {
|
||||
model: MODEL_DEFAULT,
|
||||
model: "",
|
||||
description: "",
|
||||
suite: "full",
|
||||
exercises: [],
|
||||
settings: undefined,
|
||||
concurrency: CONCURRENCY_DEFAULT,
|
||||
timeout: TIMEOUT_DEFAULT,
|
||||
iterations: ITERATIONS_DEFAULT,
|
||||
jobToken: "",
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -90,11 +136,169 @@ export function NewRun() {
|
|||
|
||||
const [model, suite, settings] = watch(["model", "suite", "settings", "concurrency"])
|
||||
|
||||
// Load settings from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedConcurrency = localStorage.getItem("evals-concurrency")
|
||||
if (savedConcurrency) {
|
||||
const parsed = parseInt(savedConcurrency, 10)
|
||||
if (!isNaN(parsed) && parsed >= CONCURRENCY_MIN && parsed <= CONCURRENCY_MAX) {
|
||||
setValue("concurrency", parsed)
|
||||
}
|
||||
}
|
||||
const savedTimeout = localStorage.getItem("evals-timeout")
|
||||
if (savedTimeout) {
|
||||
const parsed = parseInt(savedTimeout, 10)
|
||||
if (!isNaN(parsed) && parsed >= TIMEOUT_MIN && parsed <= TIMEOUT_MAX) {
|
||||
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
|
||||
const languages = useMemo(() => {
|
||||
if (!exercises.data) return []
|
||||
const langs = new Set<string>()
|
||||
for (const path of exercises.data) {
|
||||
const lang = path.split("/")[0]
|
||||
if (lang) langs.add(lang)
|
||||
}
|
||||
return Array.from(langs).sort()
|
||||
}, [exercises.data])
|
||||
|
||||
// Get exercises for a specific language
|
||||
const getExercisesForLanguage = useCallback(
|
||||
(lang: string) => {
|
||||
if (!exercises.data) return []
|
||||
return exercises.data.filter((path) => path.startsWith(`${lang}/`))
|
||||
},
|
||||
[exercises.data],
|
||||
)
|
||||
|
||||
// Toggle all exercises for a language
|
||||
const toggleLanguage = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
const allSelected = langExercises.every((ex) => selectedExercises.includes(ex))
|
||||
|
||||
let newSelected: string[]
|
||||
if (allSelected) {
|
||||
// Remove all exercises for this language
|
||||
newSelected = selectedExercises.filter((ex) => !ex.startsWith(`${lang}/`))
|
||||
} else {
|
||||
// Add all exercises for this language (avoiding duplicates)
|
||||
const existing = new Set(selectedExercises)
|
||||
for (const ex of langExercises) {
|
||||
existing.add(ex)
|
||||
}
|
||||
newSelected = Array.from(existing)
|
||||
}
|
||||
|
||||
setSelectedExercises(newSelected)
|
||||
setValue("exercises", newSelected)
|
||||
localStorage.setItem("evals-exercises", JSON.stringify(newSelected))
|
||||
},
|
||||
[getExercisesForLanguage, selectedExercises, setValue],
|
||||
)
|
||||
|
||||
// Check if all exercises for a language are selected
|
||||
const isLanguageSelected = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
return langExercises.length > 0 && langExercises.every((ex) => selectedExercises.includes(ex))
|
||||
},
|
||||
[getExercisesForLanguage, selectedExercises],
|
||||
)
|
||||
|
||||
// Check if some (but not all) exercises for a language are selected
|
||||
const isLanguagePartiallySelected = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
const selectedCount = langExercises.filter((ex) => selectedExercises.includes(ex)).length
|
||||
return selectedCount > 0 && selectedCount < langExercises.length
|
||||
},
|
||||
[getExercisesForLanguage, selectedExercises],
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: CreateRun) => {
|
||||
try {
|
||||
if (mode === "openrouter") {
|
||||
values.settings = { ...(values.settings || {}), openRouterModelId: model }
|
||||
// Validate jobToken for Roo Code Cloud provider
|
||||
if (provider === "roo" && !values.jobToken?.trim()) {
|
||||
toast.error("Roo Code Cloud Token is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Build experiments settings
|
||||
const experimentsSettings = useMultipleNativeToolCalls
|
||||
? { experiments: { multipleNativeToolCalls: true } }
|
||||
: {}
|
||||
|
||||
if (provider === "openrouter") {
|
||||
values.settings = {
|
||||
...(values.settings || {}),
|
||||
apiProvider: "openrouter",
|
||||
openRouterModelId: model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, // Convert to ms
|
||||
...experimentsSettings,
|
||||
}
|
||||
} else if (provider === "roo") {
|
||||
values.settings = {
|
||||
...(values.settings || {}),
|
||||
apiProvider: "roo",
|
||||
apiModelId: model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, // Convert to ms
|
||||
...experimentsSettings,
|
||||
...(reasoningEffort
|
||||
? {
|
||||
enableReasoningEffort: true,
|
||||
reasoningEffort: reasoningEffort as ReasoningEffort,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
} else if (provider === "other" && values.settings) {
|
||||
// For imported settings, merge in experiments and tool protocol
|
||||
values.settings = {
|
||||
...values.settings,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, // Convert to ms
|
||||
...experimentsSettings,
|
||||
}
|
||||
}
|
||||
|
||||
const { id } = await createRun(values)
|
||||
|
|
@ -103,28 +307,16 @@ export function NewRun() {
|
|||
toast.error(e instanceof Error ? e.message : "An unknown error occurred.")
|
||||
}
|
||||
},
|
||||
[mode, model, router],
|
||||
)
|
||||
|
||||
const onFilterModels = useCallback(
|
||||
(value: string, search: string) => {
|
||||
if (modelSearchValueRef.current !== search) {
|
||||
modelSearchValueRef.current = search
|
||||
modelSearchResultsRef.current.clear()
|
||||
|
||||
for (const {
|
||||
obj: { id },
|
||||
score,
|
||||
} of fuzzysort.go(search, models.data || [], {
|
||||
key: "name",
|
||||
})) {
|
||||
modelSearchResultsRef.current.set(id, score)
|
||||
}
|
||||
}
|
||||
|
||||
return modelSearchResultsRef.current.get(value) ?? 0
|
||||
},
|
||||
[models.data],
|
||||
[
|
||||
provider,
|
||||
model,
|
||||
router,
|
||||
useNativeToolProtocol,
|
||||
useMultipleNativeToolCalls,
|
||||
reasoningEffort,
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout,
|
||||
],
|
||||
)
|
||||
|
||||
const onSelectModel = useCallback(
|
||||
|
|
@ -132,7 +324,7 @@ export function NewRun() {
|
|||
setValue("model", model)
|
||||
setModelPopoverOpen(false)
|
||||
},
|
||||
[setValue],
|
||||
[setValue, setModelPopoverOpen],
|
||||
)
|
||||
|
||||
const onImportSettings = useCallback(
|
||||
|
|
@ -156,11 +348,21 @@ export function NewRun() {
|
|||
})
|
||||
.parse(JSON.parse(await file.text()))
|
||||
|
||||
const providerSettings = providerProfiles.apiConfigs[providerProfiles.currentApiConfigName] ?? {}
|
||||
// Store all imported configs for user selection
|
||||
setImportedSettings({
|
||||
apiConfigs: providerProfiles.apiConfigs,
|
||||
globalSettings,
|
||||
currentApiConfigName: providerProfiles.currentApiConfigName,
|
||||
})
|
||||
|
||||
// Default to the current config
|
||||
const defaultConfigName = providerProfiles.currentApiConfigName
|
||||
setSelectedConfigName(defaultConfigName)
|
||||
|
||||
// Apply the default config
|
||||
const providerSettings = providerProfiles.apiConfigs[defaultConfigName] ?? {}
|
||||
setValue("model", getModelId(providerSettings) ?? "")
|
||||
setValue("settings", { ...EVALS_SETTINGS, ...providerSettings, ...globalSettings })
|
||||
setMode("settings")
|
||||
|
||||
event.target.value = ""
|
||||
} catch (e) {
|
||||
|
|
@ -171,19 +373,155 @@ 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}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col justify-center divide-y divide-primary *:py-5">
|
||||
<div className="flex flex-row justify-between gap-4">
|
||||
{mode === "openrouter" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model"
|
||||
render={() => (
|
||||
<FormItem className="flex-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Tabs
|
||||
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>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{provider === "other" ? (
|
||||
<div className="space-y-2 overflow-auto">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => document.getElementById("json-upload")?.click()}
|
||||
className="w-full">
|
||||
<SlidersHorizontal />
|
||||
Import Settings
|
||||
</Button>
|
||||
<input
|
||||
id="json-upload"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
className="hidden"
|
||||
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",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</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>
|
||||
<label
|
||||
htmlFor="multipleNativeToolCalls-other"
|
||||
className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
id="multipleNativeToolCalls-other"
|
||||
checked={useMultipleNativeToolCalls}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setUseMultipleNativeToolCalls(checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">Use Multiple Native Tool Calls</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settings && (
|
||||
<SettingsDiff defaultSettings={EVALS_SETTINGS} customSettings={settings} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Popover open={modelPopoverOpen} onOpenChange={setModelPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -192,25 +530,23 @@ export function NewRun() {
|
|||
aria-expanded={modelPopoverOpen}
|
||||
className="flex items-center justify-between">
|
||||
<div>
|
||||
{models.data?.find(({ id }) => id === model)?.name ||
|
||||
model ||
|
||||
"Select OpenRouter Model"}
|
||||
{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={onFilterModels}>
|
||||
<Command filter={onFilter}>
|
||||
<CommandInput
|
||||
placeholder="Search"
|
||||
value={modelSearchValue}
|
||||
onValueChange={setModelSearchValue}
|
||||
value={searchValue}
|
||||
onValueChange={setSearchValue}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{models.data?.map(({ id, name }) => (
|
||||
{models?.map(({ id, name }) => (
|
||||
<CommandItem
|
||||
key={id}
|
||||
value={id}
|
||||
|
|
@ -229,45 +565,108 @@ export function NewRun() {
|
|||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormItem className="flex-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => document.getElementById("json-upload")?.click()}>
|
||||
<SlidersHorizontal />
|
||||
Import Settings
|
||||
</Button>
|
||||
<input
|
||||
id="json-upload"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
className="hidden"
|
||||
onChange={onImportSettings}
|
||||
/>
|
||||
{settings && (
|
||||
<ScrollArea className="max-h-64 border rounded-sm">
|
||||
<>
|
||||
<div className="flex items-center gap-1 p-2 border-b">
|
||||
<CircleCheck className="size-4 text-ring" />
|
||||
<div className="text-sm">
|
||||
Imported valid Roo Code settings. Showing differences from default
|
||||
settings.
|
||||
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-4">
|
||||
<div className="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>
|
||||
<label
|
||||
htmlFor="multipleNativeToolCalls"
|
||||
className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
id="multipleNativeToolCalls"
|
||||
checked={useMultipleNativeToolCalls}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setUseMultipleNativeToolCalls(checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">Use Multiple Native Tool Calls</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{provider === "roo" && (
|
||||
<div className="space-y-2 pt-2 border-t border-border">
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Reasoning Effort
|
||||
</Label>
|
||||
<Select
|
||||
value={reasoningEffort || "none"}
|
||||
onValueChange={(value) =>
|
||||
setReasoningEffort(
|
||||
value === "none" ? "" : (value as ReasoningEffort),
|
||||
)
|
||||
}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="None (default)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None (default)</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground pl-1">
|
||||
When set, enableReasoningEffort will be automatically enabled
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SettingsDiff defaultSettings={EVALS_SETTINGS} customSettings={settings} />
|
||||
</>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{provider === "roo" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="jobToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<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" placeholder="Required" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</div>
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
|
@ -275,18 +674,54 @@ export function NewRun() {
|
|||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Exercises</FormLabel>
|
||||
<Tabs
|
||||
defaultValue="full"
|
||||
onValueChange={(value) => setValue("suite", value as "full" | "partial")}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="full">All</TabsTrigger>
|
||||
<TabsTrigger value="partial">Some</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Tabs
|
||||
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>
|
||||
<TabsTrigger value="full">All</TabsTrigger>
|
||||
<TabsTrigger value="partial">Some</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{suite === "partial" && languages.length > 0 && (
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{languages.map((lang) => (
|
||||
<Button
|
||||
key={lang}
|
||||
type="button"
|
||||
variant={
|
||||
isLanguageSelected(lang)
|
||||
? "default"
|
||||
: isLanguagePartiallySelected(lang)
|
||||
? "secondary"
|
||||
: "outline"
|
||||
}
|
||||
size="sm"
|
||||
onClick={() => toggleLanguage(lang)}
|
||||
className="text-xs capitalize">
|
||||
{lang}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{suite === "partial" && (
|
||||
<MultiSelect
|
||||
options={exercises.data?.map((path) => ({ value: path, label: path })) || []}
|
||||
onValueChange={(value) => setValue("exercises", value)}
|
||||
value={selectedExercises}
|
||||
onValueChange={(value) => {
|
||||
setSelectedExercises(value)
|
||||
setValue("exercises", value)
|
||||
localStorage.setItem("evals-exercises", JSON.stringify(value))
|
||||
}}
|
||||
placeholder="Select"
|
||||
variant="inverted"
|
||||
maxCount={4}
|
||||
|
|
@ -306,11 +741,14 @@ export function NewRun() {
|
|||
<FormControl>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
defaultValue={[field.value]}
|
||||
value={[field.value]}
|
||||
min={CONCURRENCY_MIN}
|
||||
max={CONCURRENCY_MAX}
|
||||
step={1}
|
||||
onValueChange={(value) => field.onChange(value[0])}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0])
|
||||
localStorage.setItem("evals-concurrency", String(value[0]))
|
||||
}}
|
||||
/>
|
||||
<div>{field.value}</div>
|
||||
</div>
|
||||
|
|
@ -329,11 +767,14 @@ export function NewRun() {
|
|||
<FormControl>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Slider
|
||||
defaultValue={[field.value]}
|
||||
value={[field.value]}
|
||||
min={TIMEOUT_MIN}
|
||||
max={TIMEOUT_MAX}
|
||||
step={1}
|
||||
onValueChange={(value) => field.onChange(value[0])}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value[0])
|
||||
localStorage.setItem("evals-timeout", String(value[0]))
|
||||
}}
|
||||
/>
|
||||
<div>{field.value}</div>
|
||||
</div>
|
||||
|
|
@ -343,6 +784,96 @@ export function NewRun() {
|
|||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="iterations"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Iterations per Exercise</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>{field.value}</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>Run each exercise multiple times to compare results</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormItem className="py-5">
|
||||
<div className="flex items-center gap-1">
|
||||
<Label>Terminal 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 className="py-5">
|
||||
<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>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Fragment, HTMLAttributes } from "react"
|
||||
|
||||
import { type Keys, type RooCodeSettings, GLOBAL_SETTINGS_KEYS, PROVIDER_SETTINGS_KEYS } from "@roo-code/types"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
|
||||
|
||||
export const ROO_CODE_SETTINGS_KEYS = [...GLOBAL_SETTINGS_KEYS, ...PROVIDER_SETTINGS_KEYS] as Keys<RooCodeSettings>[]
|
||||
export const ROO_CODE_SETTINGS_KEYS = [
|
||||
...new Set([...GLOBAL_SETTINGS_KEYS, ...PROVIDER_SETTINGS_KEYS]),
|
||||
] as Keys<RooCodeSettings>[]
|
||||
|
||||
type SettingsDiffProps = HTMLAttributes<HTMLDivElement> & {
|
||||
type SettingsDiffProps = {
|
||||
defaultSettings: RooCodeSettings
|
||||
customSettings: RooCodeSettings
|
||||
}
|
||||
|
|
@ -14,53 +14,45 @@ type SettingsDiffProps = HTMLAttributes<HTMLDivElement> & {
|
|||
export function SettingsDiff({
|
||||
customSettings: { experiments: customExperiments, ...customSettings },
|
||||
defaultSettings: { experiments: defaultExperiments, ...defaultSettings },
|
||||
className,
|
||||
...props
|
||||
}: SettingsDiffProps) {
|
||||
const defaults = { ...defaultSettings, ...defaultExperiments }
|
||||
const custom = { ...customSettings, ...customExperiments }
|
||||
|
||||
return (
|
||||
<div className={cn("grid grid-cols-3 gap-2 text-sm p-2", className)} {...props}>
|
||||
<div className="font-medium text-muted-foreground">Setting</div>
|
||||
<div className="font-medium text-muted-foreground">Default</div>
|
||||
<div className="font-medium text-muted-foreground">Custom</div>
|
||||
{ROO_CODE_SETTINGS_KEYS.map((key) => {
|
||||
const defaultValue = defaults[key as keyof typeof defaults]
|
||||
const customValue = custom[key as keyof typeof custom]
|
||||
const isDefault = JSON.stringify(defaultValue) === JSON.stringify(customValue)
|
||||
<div className="border rounded-sm">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="font-medium text-muted-foreground">
|
||||
<TableHead>Setting</TableHead>
|
||||
<TableHead>Default</TableHead>
|
||||
<TableHead>Custom</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ROO_CODE_SETTINGS_KEYS.map((key) => {
|
||||
const defaultValue = JSON.stringify(defaults[key as keyof typeof defaults], null, 2)
|
||||
const customValue = JSON.stringify(custom[key as keyof typeof custom], null, 2)
|
||||
|
||||
return isDefault ? null : (
|
||||
<SettingDiff
|
||||
key={key}
|
||||
name={key}
|
||||
defaultValue={JSON.stringify(defaultValue, null, 2)}
|
||||
customValue={JSON.stringify(customValue, null, 2)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
return defaultValue === customValue ||
|
||||
(isEmpty(defaultValue) && isEmpty(customValue)) ? null : (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="font-mono" title={key}>
|
||||
{key}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-rose-500 line-through" title={defaultValue}>
|
||||
{defaultValue}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-teal-500" title={customValue}>
|
||||
{customValue}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type SettingDiffProps = HTMLAttributes<HTMLDivElement> & {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
customValue?: string
|
||||
}
|
||||
|
||||
export function SettingDiff({ name, defaultValue, customValue, ...props }: SettingDiffProps) {
|
||||
return (
|
||||
<Fragment {...props}>
|
||||
<div className="font-mono" title={name}>
|
||||
{name}
|
||||
</div>
|
||||
<pre className="inline text-rose-500 line-through" title={defaultValue}>
|
||||
{defaultValue}
|
||||
</pre>
|
||||
<pre className="inline text-teal-500" title={customValue}>
|
||||
{customValue}
|
||||
</pre>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
const isEmpty = (value: string | undefined) =>
|
||||
value === undefined || value === "" || value === "null" || value === '""' || value === "[]" || value === "{}"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
import { useCallback, useState, useRef } from "react"
|
||||
import Link from "next/link"
|
||||
import { Ellipsis, ClipboardList, Copy, Check, LoaderCircle, Trash } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
import { Ellipsis, ClipboardList, Copy, Check, LoaderCircle, Trash, Settings, FileDown } from "lucide-react"
|
||||
|
||||
import type { Run as EvalsRun, TaskMetrics as EvalsTaskMetrics } from "@roo-code/evals"
|
||||
import type { ToolName } from "@roo-code/types"
|
||||
|
||||
import { deleteRun } from "@/actions/runs"
|
||||
import { formatCurrency, formatDuration, formatTokens, formatToolUsageSuccessRate } from "@/lib/formatters"
|
||||
import {
|
||||
formatCurrency,
|
||||
formatDateTime,
|
||||
formatDuration,
|
||||
formatTokens,
|
||||
formatToolUsageSuccessRate,
|
||||
} from "@/lib/formatters"
|
||||
import { useCopyRun } from "@/hooks/use-copy-run"
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -23,18 +32,63 @@ import {
|
|||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
ScrollArea,
|
||||
} from "@/components/ui"
|
||||
|
||||
type RunProps = {
|
||||
run: EvalsRun
|
||||
taskMetrics: EvalsTaskMetrics | null
|
||||
toolColumns: ToolName[]
|
||||
}
|
||||
|
||||
export function Run({ run, taskMetrics }: RunProps) {
|
||||
export function Run({ run, taskMetrics, toolColumns }: RunProps) {
|
||||
const router = useRouter()
|
||||
const [deleteRunId, setDeleteRunId] = useState<number>()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [isExportingLogs, setIsExportingLogs] = useState(false)
|
||||
const continueRef = useRef<HTMLButtonElement>(null)
|
||||
const { isPending, copyRun, copied } = useCopyRun(run.id)
|
||||
|
||||
const onExportFailedLogs = useCallback(async () => {
|
||||
if (run.failed === 0) {
|
||||
toast.error("No failed tasks to export")
|
||||
return
|
||||
}
|
||||
|
||||
setIsExportingLogs(true)
|
||||
try {
|
||||
const response = await fetch(`/api/runs/${run.id}/logs/failed`)
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
toast.error(error.error || "Failed to export logs")
|
||||
return
|
||||
}
|
||||
|
||||
// Download the zip file
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `run-${run.id}-failed-logs.zip`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
toast.success("Failed logs exported successfully")
|
||||
} catch (error) {
|
||||
console.error("Error exporting logs:", error)
|
||||
toast.error("Failed to export logs")
|
||||
} finally {
|
||||
setIsExportingLogs(false)
|
||||
}
|
||||
}, [run.id, run.failed])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
if (!deleteRunId) {
|
||||
return
|
||||
|
|
@ -48,40 +102,73 @@ export function Run({ run, taskMetrics }: RunProps) {
|
|||
}
|
||||
}, [deleteRunId])
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// Don't navigate if clicking on the dropdown menu
|
||||
if ((e.target as HTMLElement).closest("[data-dropdown-trigger]")) {
|
||||
return
|
||||
}
|
||||
router.push(`/runs/${run.id}`)
|
||||
},
|
||||
[router, run.id],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell>{run.model}</TableCell>
|
||||
<TableRow className="cursor-pointer hover:bg-muted/50" onClick={handleRowClick}>
|
||||
<TableCell className="max-w-[200px] truncate">{run.model}</TableCell>
|
||||
<TableCell>{run.settings?.apiProvider ?? "-"}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{formatDateTime(run.createdAt)}
|
||||
</TableCell>
|
||||
<TableCell>{run.passed}</TableCell>
|
||||
<TableCell>{run.failed}</TableCell>
|
||||
<TableCell>
|
||||
{run.passed + run.failed > 0 && (
|
||||
<span>{((run.passed / (run.passed + run.failed)) * 100).toFixed(1)}%</span>
|
||||
)}
|
||||
{run.passed + run.failed > 0 &&
|
||||
(() => {
|
||||
const percent = (run.passed / (run.passed + run.failed)) * 100
|
||||
const colorClass =
|
||||
percent === 100 ? "text-green-500" : percent >= 80 ? "text-yellow-500" : "text-red-500"
|
||||
return <span className={colorClass}>{percent.toFixed(1)}%</span>
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{taskMetrics && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div>{formatTokens(taskMetrics.tokensIn)}</div>/
|
||||
<div>{formatTokens(taskMetrics.tokensOut)}</div>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{taskMetrics?.toolUsage?.apply_diff && (
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<div>{taskMetrics.toolUsage.apply_diff.attempts}</div>
|
||||
<div>/</div>
|
||||
<div>{formatToolUsageSuccessRate(taskMetrics.toolUsage.apply_diff)}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{formatTokens(taskMetrics.tokensIn)}</span>/
|
||||
<span>{formatTokens(taskMetrics.tokensOut)}</span>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
{toolColumns.map((toolName) => {
|
||||
const usage = taskMetrics?.toolUsage?.[toolName]
|
||||
const successRate =
|
||||
usage && usage.attempts > 0 ? ((usage.attempts - usage.failures) / usage.attempts) * 100 : 100
|
||||
const rateColor =
|
||||
successRate === 100
|
||||
? "text-muted-foreground"
|
||||
: successRate >= 80
|
||||
? "text-yellow-500"
|
||||
: "text-red-500"
|
||||
return (
|
||||
<TableCell key={toolName} className="text-xs text-center">
|
||||
{usage ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="font-medium">{usage.attempts}</span>
|
||||
<span className={rateColor}>{formatToolUsageSuccessRate(usage)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell>{taskMetrics && formatCurrency(taskMetrics.cost)}</TableCell>
|
||||
<TableCell>{taskMetrics && formatDuration(taskMetrics.duration)}</TableCell>
|
||||
<TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenu>
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger data-dropdown-trigger>
|
||||
<Ellipsis />
|
||||
</DropdownMenuTrigger>
|
||||
</Button>
|
||||
|
|
@ -94,6 +181,14 @@ export function Run({ run, taskMetrics }: RunProps) {
|
|||
</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">
|
||||
|
|
@ -116,6 +211,23 @@ export function Run({ run, taskMetrics }: RunProps) {
|
|||
</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)
|
||||
|
|
@ -144,6 +256,18 @@ export function Run({ run, taskMetrics }: RunProps) {
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Run Settings</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[60vh]">
|
||||
<pre className="text-xs font-mono bg-muted p-4 rounded-md overflow-auto">
|
||||
{JSON.stringify(run.settings, null, 2)}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,224 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Rocket } from "lucide-react"
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Rocket } from "lucide-react"
|
||||
|
||||
import type { Run, TaskMetrics } from "@roo-code/evals"
|
||||
import type { ToolName } from "@roo-code/types"
|
||||
|
||||
import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui"
|
||||
import { Run as Row } from "@/components/home/run"
|
||||
|
||||
type RunWithTaskMetrics = Run & { taskMetrics: TaskMetrics | null }
|
||||
|
||||
type SortColumn = "model" | "provider" | "passed" | "failed" | "percent" | "cost" | "duration" | "createdAt"
|
||||
type SortDirection = "asc" | "desc"
|
||||
|
||||
// Generate abbreviation from tool name (e.g., "read_file" -> "RF", "list_code_definition_names" -> "LCDN")
|
||||
function getToolAbbreviation(toolName: string): string {
|
||||
return toolName
|
||||
.split("_")
|
||||
.map((word) => word[0]?.toUpperCase() ?? "")
|
||||
.join("")
|
||||
}
|
||||
|
||||
function SortIcon({
|
||||
column,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
}: {
|
||||
column: SortColumn
|
||||
sortColumn: SortColumn | null
|
||||
sortDirection: SortDirection
|
||||
}) {
|
||||
if (sortColumn !== column) {
|
||||
return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
}
|
||||
return sortDirection === "asc" ? <ArrowUp className="ml-1 h-3 w-3" /> : <ArrowDown className="ml-1 h-3 w-3" />
|
||||
}
|
||||
|
||||
export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
||||
const router = useRouter()
|
||||
const [sortColumn, setSortColumn] = useState<SortColumn | null>("createdAt")
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>("desc")
|
||||
|
||||
const handleSort = (column: SortColumn) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === "asc" ? "desc" : "asc")
|
||||
} else {
|
||||
setSortColumn(column)
|
||||
setSortDirection("desc")
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all unique tool names from all runs and sort by total attempts
|
||||
const toolColumns = useMemo<ToolName[]>(() => {
|
||||
const toolTotals = new Map<ToolName, number>()
|
||||
|
||||
for (const run of runs) {
|
||||
if (run.taskMetrics?.toolUsage) {
|
||||
for (const [toolName, usage] of Object.entries(run.taskMetrics.toolUsage)) {
|
||||
const tool = toolName as ToolName
|
||||
const current = toolTotals.get(tool) ?? 0
|
||||
toolTotals.set(tool, current + usage.attempts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by total attempts descending
|
||||
return Array.from(toolTotals.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([name]): ToolName => name)
|
||||
}, [runs])
|
||||
|
||||
// Sort runs based on current sort column and direction
|
||||
const sortedRuns = useMemo(() => {
|
||||
if (!sortColumn) return runs
|
||||
|
||||
return [...runs].sort((a, b) => {
|
||||
let aVal: string | number | Date | null = null
|
||||
let bVal: string | number | Date | null = null
|
||||
|
||||
switch (sortColumn) {
|
||||
case "model":
|
||||
aVal = a.model
|
||||
bVal = b.model
|
||||
break
|
||||
case "provider":
|
||||
aVal = a.settings?.apiProvider ?? ""
|
||||
bVal = b.settings?.apiProvider ?? ""
|
||||
break
|
||||
case "passed":
|
||||
aVal = a.passed
|
||||
bVal = b.passed
|
||||
break
|
||||
case "failed":
|
||||
aVal = a.failed
|
||||
bVal = b.failed
|
||||
break
|
||||
case "percent":
|
||||
aVal = a.passed + a.failed > 0 ? a.passed / (a.passed + a.failed) : 0
|
||||
bVal = b.passed + b.failed > 0 ? b.passed / (b.passed + b.failed) : 0
|
||||
break
|
||||
case "cost":
|
||||
aVal = a.taskMetrics?.cost ?? 0
|
||||
bVal = b.taskMetrics?.cost ?? 0
|
||||
break
|
||||
case "duration":
|
||||
aVal = a.taskMetrics?.duration ?? 0
|
||||
bVal = b.taskMetrics?.duration ?? 0
|
||||
break
|
||||
case "createdAt":
|
||||
aVal = a.createdAt
|
||||
bVal = b.createdAt
|
||||
break
|
||||
}
|
||||
|
||||
if (aVal === null || bVal === null) return 0
|
||||
|
||||
let comparison = 0
|
||||
if (typeof aVal === "string" && typeof bVal === "string") {
|
||||
comparison = aVal.localeCompare(bVal)
|
||||
} else if (aVal instanceof Date && bVal instanceof Date) {
|
||||
comparison = aVal.getTime() - bVal.getTime()
|
||||
} else {
|
||||
comparison = (aVal as number) - (bVal as number)
|
||||
}
|
||||
|
||||
return sortDirection === "asc" ? comparison : -comparison
|
||||
})
|
||||
}, [runs, sortColumn, sortDirection])
|
||||
|
||||
// Calculate colSpan for empty state (7 base columns + dynamic tools + 3 end columns)
|
||||
const totalColumns = 7 + toolColumns.length + 3
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table className="border border-t-0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Model</TableHead>
|
||||
<TableHead>Passed</TableHead>
|
||||
<TableHead>Failed</TableHead>
|
||||
<TableHead>% Correct</TableHead>
|
||||
<TableHead>Tokens In / Out</TableHead>
|
||||
<TableHead>Diff Edits</TableHead>
|
||||
<TableHead>Cost</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead />
|
||||
<TableHead
|
||||
className="max-w-[200px] cursor-pointer select-none"
|
||||
onClick={() => handleSort("model")}>
|
||||
<div className="flex items-center">
|
||||
Model
|
||||
<SortIcon column="model" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("provider")}>
|
||||
<div className="flex items-center">
|
||||
Provider
|
||||
<SortIcon column="provider" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("createdAt")}>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
<SortIcon column="createdAt" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("passed")}>
|
||||
<div className="flex items-center">
|
||||
Passed
|
||||
<SortIcon column="passed" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("failed")}>
|
||||
<div className="flex items-center">
|
||||
Failed
|
||||
<SortIcon column="failed" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("percent")}>
|
||||
<div className="flex items-center">
|
||||
%
|
||||
<SortIcon column="percent" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>Tokens</TableHead>
|
||||
{toolColumns.map((toolName) => (
|
||||
<TableHead key={toolName} className="text-xs text-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{getToolAbbreviation(toolName)}</TooltipTrigger>
|
||||
<TooltipContent>{toolName}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TableHead>
|
||||
))}
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("cost")}>
|
||||
<div className="flex items-center">
|
||||
Cost
|
||||
<SortIcon column="cost" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer select-none" onClick={() => handleSort("duration")}>
|
||||
<div className="flex items-center">
|
||||
Duration
|
||||
<SortIcon column="duration" sortColumn={sortColumn} sortDirection={sortDirection} />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{runs.length ? (
|
||||
runs.map(({ taskMetrics, ...run }) => <Row key={run.id} run={run} taskMetrics={taskMetrics} />)
|
||||
{sortedRuns.length ? (
|
||||
sortedRuns.map(({ taskMetrics, ...run }) => (
|
||||
<Row key={run.id} run={run} taskMetrics={taskMetrics} toolColumns={toolColumns} />
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center">
|
||||
<TableCell colSpan={totalColumns} className="text-center">
|
||||
No eval runs yet.
|
||||
<Button variant="link" onClick={() => router.push("/runs/new")}>
|
||||
Launch
|
||||
|
|
|
|||
27
apps/web-evals/src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none">
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
export * from "./alert-dialog"
|
||||
export * from "./badge"
|
||||
export * from "./button"
|
||||
export * from "./checkbox"
|
||||
export * from "./command"
|
||||
export * from "./dialog"
|
||||
export * from "./drawer"
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ interface MultiSelectProps extends React.HTMLAttributes<HTMLDivElement>, Variant
|
|||
*/
|
||||
onValueChange: (value: string[]) => void
|
||||
|
||||
/** The default selected values when the component mounts. */
|
||||
/** The controlled selected values. When provided, the component becomes controlled. */
|
||||
value?: string[]
|
||||
|
||||
/** The default selected values when the component mounts (uncontrolled mode). */
|
||||
defaultValue?: string[]
|
||||
|
||||
/**
|
||||
|
|
@ -89,6 +92,7 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|||
options,
|
||||
onValueChange,
|
||||
variant,
|
||||
value,
|
||||
defaultValue = [],
|
||||
placeholder = "Select options",
|
||||
maxCount = 3,
|
||||
|
|
@ -98,17 +102,30 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|||
},
|
||||
ref,
|
||||
) => {
|
||||
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
|
||||
const [internalSelectedValues, setInternalSelectedValues] = React.useState<string[]>(defaultValue)
|
||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false)
|
||||
|
||||
// Use controlled value if provided, otherwise use internal state
|
||||
const isControlled = value !== undefined
|
||||
const selectedValues = isControlled ? value : internalSelectedValues
|
||||
|
||||
const setSelectedValues = React.useCallback(
|
||||
(newValues: string[]) => {
|
||||
if (!isControlled) {
|
||||
setInternalSelectedValues(newValues)
|
||||
}
|
||||
onValueChange(newValues)
|
||||
},
|
||||
[isControlled, onValueChange],
|
||||
)
|
||||
|
||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
setIsPopoverOpen(true)
|
||||
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
||||
const newSelectedValues = [...selectedValues]
|
||||
newSelectedValues.pop()
|
||||
if (!selectedValues.length) return
|
||||
const newSelectedValues = selectedValues.slice(0, -1)
|
||||
setSelectedValues(newSelectedValues)
|
||||
onValueChange(newSelectedValues)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +134,6 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|||
? selectedValues.filter((value) => value !== option)
|
||||
: [...selectedValues, option]
|
||||
setSelectedValues(newSelectedValues)
|
||||
onValueChange(newSelectedValues)
|
||||
}
|
||||
|
||||
const handleTogglePopover = () => {
|
||||
|
|
@ -127,7 +143,6 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|||
const clearExtraOptions = () => {
|
||||
const newSelectedValues = selectedValues.slice(0, maxCount)
|
||||
setSelectedValues(newSelectedValues)
|
||||
onValueChange(newSelectedValues)
|
||||
}
|
||||
|
||||
const searchResultsRef = React.useRef<Map<string, number>>(new Map())
|
||||
|
|
@ -141,12 +156,10 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
|
|||
selectedValues.sort().join(",") === values.sort().join(",")
|
||||
) {
|
||||
setSelectedValues([])
|
||||
onValueChange([])
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedValues(values)
|
||||
onValueChange(values)
|
||||
}
|
||||
|
||||
const onFilter = React.useCallback(
|
||||
|
|
|
|||
37
apps/web-evals/src/hooks/use-fuzzy-model-search.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useCallback, useRef, useState } from "react"
|
||||
import fuzzysort from "fuzzysort"
|
||||
|
||||
interface ModelWithId {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const useFuzzyModelSearch = <T extends ModelWithId>(data: T[] | undefined) => {
|
||||
const [searchValue, setSearchValue] = useState("")
|
||||
|
||||
const searchResultsRef = useRef<Map<string, number>>(new Map())
|
||||
const searchValueRef = useRef("")
|
||||
|
||||
const onFilter = useCallback(
|
||||
(value: string, search: string) => {
|
||||
if (searchValueRef.current !== search) {
|
||||
searchValueRef.current = search
|
||||
searchResultsRef.current.clear()
|
||||
|
||||
for (const {
|
||||
obj: { id },
|
||||
score,
|
||||
} of fuzzysort.go(search, data || [], {
|
||||
key: "name",
|
||||
})) {
|
||||
searchResultsRef.current.set(id, score)
|
||||
}
|
||||
}
|
||||
|
||||
return searchResultsRef.current.get(value) ?? 0
|
||||
},
|
||||
[data],
|
||||
)
|
||||
|
||||
return { searchValue, setSearchValue, onFilter }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { z } from "zod"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useFuzzyModelSearch } from "./use-fuzzy-model-search"
|
||||
|
||||
export const openRouterModelSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -25,8 +26,13 @@ export const getOpenRouterModels = async (): Promise<OpenRouterModel[]> => {
|
|||
return result.data.data.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
export const useOpenRouterModels = () =>
|
||||
useQuery({
|
||||
export const useOpenRouterModels = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ["getOpenRouterModels"],
|
||||
queryFn: getOpenRouterModels,
|
||||
})
|
||||
|
||||
const { searchValue, setSearchValue, onFilter } = useFuzzyModelSearch(query.data)
|
||||
|
||||
return { ...query, searchValue, setSearchValue, onFilter }
|
||||
}
|
||||
|
|
|
|||
66
apps/web-evals/src/hooks/use-roo-code-cloud-models.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { z } from "zod"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useFuzzyModelSearch } from "./use-fuzzy-model-search"
|
||||
|
||||
export const rooCodeCloudModelSchema = z.object({
|
||||
object: z.literal("model"),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
context_window: z.number(),
|
||||
max_tokens: z.number(),
|
||||
supports_images: z.boolean().optional(),
|
||||
supports_prompt_cache: z.boolean().optional(),
|
||||
type: z.literal("language"),
|
||||
tags: z.array(z.string()).optional(),
|
||||
deprecationMessage: z.string().optional(),
|
||||
owned_by: z.string(),
|
||||
pricing: z.object({
|
||||
input: z.string(),
|
||||
output: z.string(),
|
||||
input_cache_read: z.string().optional(),
|
||||
input_cache_write: z.string().optional(),
|
||||
}),
|
||||
evals: z
|
||||
.object({
|
||||
score: z.number().min(0).max(100),
|
||||
})
|
||||
.optional(),
|
||||
created: z.number(),
|
||||
deprecated: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type RooCodeCloudModel = z.infer<typeof rooCodeCloudModelSchema>
|
||||
|
||||
export const getRooCodeCloudModels = async (): Promise<RooCodeCloudModel[]> => {
|
||||
const response = await fetch("https://api.roocode.com/proxy/v1/models")
|
||||
|
||||
if (!response.ok) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result = z
|
||||
.object({
|
||||
object: z.literal("list"),
|
||||
data: z.array(rooCodeCloudModelSchema),
|
||||
})
|
||||
.safeParse(await response.json())
|
||||
|
||||
if (!result.success) {
|
||||
console.error(result.error)
|
||||
return []
|
||||
}
|
||||
|
||||
return result.data.data.filter((model) => !model.deprecated).sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
export const useRooCodeCloudModels = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ["getRooCodeCloudModels"],
|
||||
queryFn: getRooCodeCloudModels,
|
||||
})
|
||||
|
||||
const { searchValue, setSearchValue, onFilter } = useFuzzyModelSearch(query.data)
|
||||
|
||||
return { ...query, searchValue, setSearchValue, onFilter }
|
||||
}
|
||||
|
|
@ -46,3 +46,13 @@ export const formatTokens = (tokens: number) => {
|
|||
|
||||
export const formatToolUsageSuccessRate = (usage: { attempts: number; failures: number }) =>
|
||||
usage.attempts === 0 ? "0%" : `${(((usage.attempts - usage.failures) / usage.attempts) * 100).toFixed(1)}%`
|
||||
|
||||
export const formatDateTime = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
}).format(date)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import { rooCodeSettingsSchema } from "@roo-code/types"
|
|||
* CreateRun
|
||||
*/
|
||||
|
||||
export const MODEL_DEFAULT = "anthropic/claude-sonnet-4"
|
||||
|
||||
export const CONCURRENCY_MIN = 1
|
||||
export const CONCURRENCY_MAX = 25
|
||||
export const CONCURRENCY_DEFAULT = 1
|
||||
|
|
@ -16,6 +14,10 @@ export const TIMEOUT_MIN = 5
|
|||
export const TIMEOUT_MAX = 10
|
||||
export const TIMEOUT_DEFAULT = 5
|
||||
|
||||
export const ITERATIONS_MIN = 1
|
||||
export const ITERATIONS_MAX = 10
|
||||
export const ITERATIONS_DEFAULT = 1
|
||||
|
||||
export const createRunSchema = z
|
||||
.object({
|
||||
model: z.string().min(1, { message: "Model is required." }),
|
||||
|
|
@ -25,7 +27,8 @@ export const createRunSchema = z
|
|||
settings: rooCodeSettingsSchema.optional(),
|
||||
concurrency: z.number().int().min(CONCURRENCY_MIN).max(CONCURRENCY_MAX),
|
||||
timeout: z.number().int().min(TIMEOUT_MIN).max(TIMEOUT_MAX),
|
||||
systemPrompt: z.string().optional(),
|
||||
iterations: z.number().int().min(ITERATIONS_MIN).max(ITERATIONS_MAX),
|
||||
jobToken: z.string().optional(),
|
||||
})
|
||||
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
|
||||
message: "Exercises are required when running a partial suite.",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# PostHog Analytics Configuration
|
||||
# Replace these values with your actual PostHog API key and host
|
||||
NEXT_PUBLIC_POSTHOG_KEY=your_posthog_api_key_here
|
||||
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
|
||||
# Basin Form Endpoint for Static Form Submissions
|
||||
# Replace this with your actual Basin form endpoint (e.g., https://usebasin.com/f/your-form-id)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "12.15.0",
|
||||
"lucide-react": "^0.518.0",
|
||||
"next": "^15.2.5",
|
||||
"next": "~15.2.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"posthog-js": "^1.248.1",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
|||
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 |
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { ArrowRight, GitPullRequest, History, Key, MessageSquareCode, Wrench, type LucideIcon } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { trackGoogleAdsConversion } from "@/lib/analytics/google-ads"
|
||||
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import hero from "/public/heroes/agent-pr-fixer.png"
|
||||
|
||||
interface Feature {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string | React.ReactNode
|
||||
logos?: string[]
|
||||
}
|
||||
|
||||
const workflowSteps: Feature[] = [
|
||||
{
|
||||
icon: GitPullRequest,
|
||||
title: "1. Connect your GitHub repositories",
|
||||
description: "Pick which repos the PR Fixer can work on by pushing to ongoing branches.",
|
||||
},
|
||||
{
|
||||
icon: MessageSquareCode,
|
||||
title: "2. Invoke from a comment",
|
||||
description:
|
||||
'Ask the agent to fix issues directly from GitHub PR comments (e.g. "@roomote: fix these review comments"). It’s fully aware of the entire comment history and latest diffs and focuses on fixing them – not random changes to your code.',
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
title: "3. Get clean scoped commits",
|
||||
description: (
|
||||
<>
|
||||
The agent proposes targeted changes and pushes concise commits or patch suggestions you (or{" "}
|
||||
<Link href="/pr-reviewer">PR Reviewer</Link>) can review and merge quickly.
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const howItWorks: Feature[] = [
|
||||
{
|
||||
icon: History,
|
||||
title: "Comment-history aware",
|
||||
description:
|
||||
"Understands the entire conversation on the PR – previous reviews, your replies, follow-ups – and uses that context to produce accurate fixes.",
|
||||
},
|
||||
{
|
||||
icon: Key,
|
||||
title: "Bring your own key",
|
||||
description:
|
||||
"Use your preferred models at full strength. We optimize prompts and execution without capping your model to protect our margins.",
|
||||
},
|
||||
{
|
||||
icon: GitPullRequest,
|
||||
title: "Repository- and diff-aware",
|
||||
description:
|
||||
"Analyzes the full repo context and the latest diff to ensure fixes align with project conventions and pass checks.",
|
||||
},
|
||||
]
|
||||
|
||||
export function PrFixerContent() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative flex md:h-[calc(70vh-theme(spacing.12))] 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-20 lg:grid-cols-2">
|
||||
<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mt-8 md:text-left md:text-4xl lg:text-5xl lg:mt-0">
|
||||
<Wrench className="size-12 mb-4" />
|
||||
State-of-the-art fixes for the comments on your PRs.
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
|
||||
<p>
|
||||
Roo Code{"'"}s PR Fixer applies high-quality changes to your PRs, right from
|
||||
GitHub. Invoke via a PR comment and it will read the entire comment history to
|
||||
understand context, agreements, and tradeoffs — then implement the right fix.
|
||||
</p>
|
||||
<p>
|
||||
As always, you bring the model key; we orchestrate smart, efficient workflows.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cross-agent link */}
|
||||
<div className="mt-6 flex flex-col md:flex-row md:items-center gap-2">
|
||||
Works great with
|
||||
<Link
|
||||
href="/reviewer"
|
||||
className="flex p-4 items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-3 py-1 text-sm text-blue-600 backdrop-blur-sm transition-colors hover:bg-blue-500/20 dark:text-blue-400"
|
||||
aria-label="Works great with PR Reviewer">
|
||||
<GitPullRequest className="size-4 mr-2" />
|
||||
PR Reviewer Agent
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0 md:items-center">
|
||||
<Button
|
||||
size="lg"
|
||||
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}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={trackGoogleAdsConversion}
|
||||
className="flex w-full items-center justify-center">
|
||||
Start 14-day Free Trial
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
<span className="text-sm text-center md:text-left text-muted-foreground md:ml-2">
|
||||
(cancel anytime)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
|
||||
<div className="md:w-[670px] md:h-[600px] relative overflow-clip">
|
||||
<div className="block">
|
||||
<Image
|
||||
src={hero}
|
||||
alt="Example of a PR Fixer applying changes from review comments"
|
||||
className="max-w-full h-auto"
|
||||
width={800}
|
||||
height={711}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">How It Works</h2>
|
||||
</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-3 lg:gap-8">
|
||||
{workflowSteps.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 hover:shadow-lg">
|
||||
<Icon className="size-6 text-foreground/80" />
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
|
||||
{step.title}
|
||||
</h3>
|
||||
<div className="leading-relaxed font-light text-muted-foreground">
|
||||
{step.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
Why Roo Code{"'"}s PR Fixer is different
|
||||
</h2>
|
||||
</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">
|
||||
{howItWorks.map((feature, index) => {
|
||||
const Icon = feature.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" />
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<div className="leading-relaxed font-light text-muted-foreground space-y-2">
|
||||
{feature.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20">
|
||||
<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">
|
||||
Ship fixes, not follow-ups.
|
||||
</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">
|
||||
Let Roo Code{"'"}s PR Fixer turn your review feedback into clean, ready-to-merge commits.
|
||||
</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"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={trackGoogleAdsConversion}
|
||||
className="flex items-center justify-center">
|
||||
Start 14-day Free Trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
95
apps/web-roo-code/src/app/pr-fixer/content-a.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { type AgentPageContent } from "@/app/shared/agent-page-content"
|
||||
import Link from "next/link"
|
||||
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import hero from "/public/heroes/agent-pr-fixer.png"
|
||||
|
||||
// Re-export for convenience
|
||||
export type { AgentPageContent }
|
||||
|
||||
export const content: AgentPageContent = {
|
||||
agentName: "PR Fixer",
|
||||
hero: {
|
||||
icon: "Wrench",
|
||||
heading: "State-of-the-art fixes for the comments on your PRs.",
|
||||
paragraphs: [
|
||||
"Roo Code's PR Fixer applies high-quality changes to your PRs, right from GitHub. Invoke via a PR comment and it will read the entire comment history to understand context, agreements, and tradeoffs — then implement the right fix.",
|
||||
"As always, you bring the model key; we orchestrate smart, efficient workflows.",
|
||||
],
|
||||
image: {
|
||||
url: hero.src,
|
||||
width: 800,
|
||||
height: 711,
|
||||
alt: "Example of a PR Fixer applying changes from review comments",
|
||||
},
|
||||
crossAgentLink: {
|
||||
text: "Works great with",
|
||||
links: [
|
||||
{
|
||||
text: "PR Reviewer Agent",
|
||||
href: "/reviewer",
|
||||
icon: "GitPullRequest",
|
||||
},
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
buttonText: "Try now for free",
|
||||
disclaimer: "",
|
||||
tracking: "&agent=pr-fixer",
|
||||
},
|
||||
},
|
||||
howItWorks: {
|
||||
heading: "How It Works",
|
||||
steps: [
|
||||
{
|
||||
title: "1. Connect your GitHub repositories",
|
||||
description: "Pick which repos the PR Fixer can work on by pushing to ongoing branches.",
|
||||
icon: "GitPullRequest",
|
||||
},
|
||||
{
|
||||
title: "2. Invoke from a comment",
|
||||
description:
|
||||
'Ask the agent to fix issues directly from GitHub PR comments (e.g. "@roomote: fix these review comments"). It\'s fully aware of the entire comment history and latest diffs and focuses on fixing them – not random changes to your code.',
|
||||
icon: "MessageSquareCode",
|
||||
},
|
||||
{
|
||||
title: "3. Get clean scoped commits",
|
||||
description: (
|
||||
<>
|
||||
The agent proposes targeted changes and pushes concise commits or patch suggestions you (or{" "}
|
||||
<Link href="/reviewer">PR Reviewer</Link>) can review and merge quickly.
|
||||
</>
|
||||
),
|
||||
icon: "Wrench",
|
||||
},
|
||||
],
|
||||
},
|
||||
whyBetter: {
|
||||
heading: "Why Roo Code's PR Fixer is different",
|
||||
features: [
|
||||
{
|
||||
title: "Comment-history aware",
|
||||
description:
|
||||
"Understands the entire conversation on the PR – previous reviews, your replies, follow-ups – and uses that context to produce accurate fixes.",
|
||||
icon: "History",
|
||||
},
|
||||
{
|
||||
title: "Bring your own key",
|
||||
description:
|
||||
"Use your preferred models at full strength. We optimize prompts and execution without capping your model to protect our margins.",
|
||||
icon: "Key",
|
||||
},
|
||||
{
|
||||
title: "Repository- and diff-aware",
|
||||
description:
|
||||
"Analyzes the full repo context and the latest diff to ensure fixes align with project conventions and pass checks.",
|
||||
icon: "GitPullRequest",
|
||||
},
|
||||
],
|
||||
},
|
||||
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: "Try now for free",
|
||||
},
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ import type { Metadata } from "next"
|
|||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { PrFixerContent } from "./PrFixerContent"
|
||||
import { AgentLandingContent } from "@/app/shared/AgentLandingContent"
|
||||
import { getContentVariant } from "@/app/shared/getContentVariant"
|
||||
import { content as contentA } from "./content-a"
|
||||
|
||||
const TITLE = "PR Fixer"
|
||||
const DESCRIPTION =
|
||||
|
|
@ -55,6 +57,11 @@ export const metadata: Metadata = {
|
|||
],
|
||||
}
|
||||
|
||||
export default function AgentPrFixerPage() {
|
||||
return <PrFixerContent />
|
||||
export default async function AgentPrFixerPage({ searchParams }: { searchParams: Promise<{ v?: string }> }) {
|
||||
const params = await searchParams
|
||||
const content = getContentVariant(params, {
|
||||
A: contentA,
|
||||
})
|
||||
|
||||
return <AgentLandingContent content={content} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { Users, Building2, ArrowRight, Star, LucideIcon, Check, Cloud } from "lucide-react"
|
||||
import { Users, ArrowRight, LucideIcon, Check, SquareTerminal, CornerRightDown, Cloud } from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { ContactForm } from "@/components/enterprise/contact-form"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
const TITLE = "Roo Code Cloud Pricing"
|
||||
const TITLE = "Roo Code Pricing"
|
||||
const DESCRIPTION =
|
||||
"Simple, transparent pricing for Roo Code Cloud. The VS Code extension is free forever. Choose the cloud plan that fits your needs."
|
||||
"Simple, transparent pricing for all Roo Code products. The VS Code extension is free forever. Choose the cloud plan that fits your needs."
|
||||
const OG_DESCRIPTION = ""
|
||||
const PATH = "/pricing"
|
||||
|
||||
|
|
@ -61,72 +60,67 @@ interface PricingTier {
|
|||
name: string
|
||||
icon: LucideIcon
|
||||
price: string
|
||||
priceSuffix: string
|
||||
period?: string
|
||||
creditPrice?: string
|
||||
trial?: string
|
||||
cancellation?: string
|
||||
description: string
|
||||
featuresIntro?: string
|
||||
features: string[]
|
||||
cta: {
|
||||
text: string
|
||||
href?: string
|
||||
isContactForm?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const pricingTiers: PricingTier[] = [
|
||||
{
|
||||
name: "VS Code Extension",
|
||||
icon: SquareTerminal,
|
||||
price: "Free",
|
||||
priceSuffix: "inference",
|
||||
description: "The best local coding agent",
|
||||
features: ["Unlimited local use", "Bring your own model", "Powerful, extensible modes", "Community support"],
|
||||
cta: {
|
||||
text: "Install Now",
|
||||
href: EXTERNAL_LINKS.MARKETPLACE,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cloud Free",
|
||||
icon: Cloud,
|
||||
price: "$0",
|
||||
cancellation: "Cancel anytime",
|
||||
description: "For folks just getting started",
|
||||
period: "/mo",
|
||||
priceSuffix: "credits",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
description: "For AI-forward engineers",
|
||||
featuresIntro: "Go beyond the extension with",
|
||||
features: [
|
||||
"Token usage analytics",
|
||||
"Access to Cloud Agents: fully autonomous development you can call from Slack, Github and the web",
|
||||
"Access to the Roo Code Cloud Provider",
|
||||
"Follow your tasks from anywhere",
|
||||
"Share tasks with friends and co-workers",
|
||||
"Early access to free AI Models",
|
||||
"Community support",
|
||||
"Token usage analytics",
|
||||
"Professional support",
|
||||
],
|
||||
cta: {
|
||||
text: "Get started",
|
||||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
icon: Star,
|
||||
price: "$20",
|
||||
period: "/mo",
|
||||
trial: "Free 14-day trial · ",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
cancellation: "Cancel anytime",
|
||||
description: "For pro Roo coders",
|
||||
featuresIntro: "Everything in Free +",
|
||||
features: [
|
||||
"Cloud Agents: PR Reviewer and more",
|
||||
"Roomote Control: Start, stop and control tasks from anywhere",
|
||||
"Paid support",
|
||||
],
|
||||
cta: {
|
||||
text: "Get started",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Team",
|
||||
name: "Cloud Team",
|
||||
icon: Users,
|
||||
price: "$99",
|
||||
priceSuffix: "credits",
|
||||
period: "/mo",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
trial: "Free 14-day trial · ",
|
||||
cancellation: "Cancel anytime",
|
||||
trial: "Free for 14 days, then",
|
||||
description: "For AI-forward teams",
|
||||
featuresIntro: "Everything in Pro +",
|
||||
featuresIntro: "Everything in Free +",
|
||||
features: ["Unlimited users (no per-seat cost)", "Shared configuration & policies", "Centralized billing"],
|
||||
cta: {
|
||||
text: "Get started",
|
||||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
|
||||
},
|
||||
},
|
||||
|
|
@ -138,52 +132,43 @@ export default function PricingPage() {
|
|||
<AnimatedBackground />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden pt-16 pb-12">
|
||||
<section className="relative overflow-hidden pt-12 pb-10">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-5xl font-bold tracking-tight">Roo Code Cloud Pricing</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
|
||||
Simple, transparent pricing that scales with your needs.
|
||||
<br />
|
||||
No inference markups. Free 14-day trials to kick the tires.
|
||||
<h1 className="text-5xl font-bold tracking-tight">Roo Code Pricing</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
For all of our products: the Roo Code VS Code Extension, Roo Code Cloud and the Roo Code
|
||||
Cloud inference Provider.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Free Extension Notice */}
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="rounded-xl p-4 mb-8 text-center bg-gradient-to-r from-blue-500/10 via-cyan-500/10 to-purple-500/10 border border-blue-500/20 dark:border-white/20">
|
||||
<p className="text-center">
|
||||
<strong className="font-semibold">The Roo Code extension is free! </strong>
|
||||
Roo Code Cloud is an optional service which takes it to the next level.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Tiers */}
|
||||
<section className="">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto grid max-w-6xl gap-4 lg:grid-cols-3">
|
||||
<div className="mx-auto grid max-w-6xl gap-4 md:grid-cols-3 md:px-4">
|
||||
{pricingTiers.map((tier) => {
|
||||
const Icon = tier.icon
|
||||
return (
|
||||
<div
|
||||
key={tier.name}
|
||||
className="relative p-6 flex flex-col justify-start bg-background border rounded-2xl transition-all hover:shadow-lg">
|
||||
className="relative group p-6 flex flex-col justify-start bg-background rounded-2xl outline outline-2 outline-border/50 hover:outline-8 transition-all shadow-xl hover:shadow-2xl hover:outline-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-2xl font-bold tracking-tight">{tier.name}</h3>
|
||||
<Icon className="size-6" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{tier.description}</p>
|
||||
<p className="text-sm font-medium">{tier.description}</p>
|
||||
</div>
|
||||
<div className="absolute -right-2 -top-4 rounded-full bg-card shadow-md p-4 outline outline-2 outline-border/50 group-hover:scale-105 group-hover:outline-8 transition-all">
|
||||
<Icon className="size-6" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="grow mb-8">
|
||||
<p className="text-sm text-muted-foreground font-light mb-2">
|
||||
{tier.featuresIntro}
|
||||
</p>
|
||||
<ul className="space-y-3 my-0 h-[148px]">
|
||||
<ul className="space-y-3 my-0 md:h-[192px]">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-4 w-4 text-muted-foreground shrink-0" />
|
||||
|
|
@ -193,52 +178,67 @@ export default function PricingPage() {
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="text-2xl mt-0 mb-1 tracking-tight">
|
||||
<strong>{tier.price}</strong>
|
||||
{tier.period}
|
||||
<p className="text-base font-light">{tier.trial}</p>
|
||||
|
||||
<p className="text-xl mb-1 tracking-tight font-light">
|
||||
<strong className="font-bold">{tier.price}</strong>
|
||||
{tier.period} + {tier.priceSuffix}
|
||||
<CornerRightDown className="inline size-4 ml-1 relative top-0.5" />
|
||||
</p>
|
||||
|
||||
{tier.creditPrice && (
|
||||
<p className="text-sm text-muted-foreground mb-1">
|
||||
+ {tier.creditPrice}/hour for Cloud tasks
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
{tier.trial}
|
||||
{tier.cancellation}
|
||||
<p className="text-sm text-muted-foreground mb-5">
|
||||
{tier.creditPrice && (
|
||||
<>
|
||||
Cloud Agents: {tier.creditPrice}/hour in credits
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
Inference:{" "}
|
||||
<Link href="/provider/pricing" className="underline hover:no-underline">
|
||||
Roo Provider
|
||||
</Link>{" "}
|
||||
credits or{" "}
|
||||
<abbr title="Bring Your Own Model" className="cursor-help">
|
||||
BYOM
|
||||
</abbr>
|
||||
</p>
|
||||
|
||||
{tier.cta.isContactForm ? (
|
||||
<ContactForm
|
||||
formType="demo"
|
||||
buttonText={tier.cta.text}
|
||||
buttonClassName="w-full transition-all duration-300"
|
||||
/>
|
||||
) : (
|
||||
<Button size="lg" className="w-full transition-all duration-300" asChild>
|
||||
<Link href={tier.cta.href!} className="flex items-center justify-center">
|
||||
{tier.cta.text}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button size="lg" className="w-full transition-all duration-300" asChild>
|
||||
<Link href={tier.cta.href!} className="flex items-center justify-center">
|
||||
{tier.cta.text}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* <div className="bg-foreground/20 h-8 absolute -bottom-8 left-1/2 w-[1px]" /> */}
|
||||
<div className="h-[28px] absolute bottom-[-31px] left-1/2 w-[4px] transition-colors bg-gradient-to-b from-transparent to-violet-700/20 group-hover:from-violet-500/50 group-hover:to-violet-500/20" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto grid max-w-6xl gap-4 mt-4 relative">
|
||||
<p className="bg-background border rounded-2xl p-6 text-center text-sm text-muted-foreground">
|
||||
<Building2 className="inline size-4 mr-2 mb-0.5" />
|
||||
Need SAML, advanced security, custom integrations or terms? Enterprise is for you.
|
||||
<Link
|
||||
href="/enterprise#contact"
|
||||
className="font-medium ml-1 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Talk to Sales
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<div className="max-w-6xl mx-auto mt-8 p-7 flex flex-col md:flex-row gap-8 md:gap-4 bg-violet-200/20 outline-violet-700/20 outline outline-1 rounded-2xl transition-all shadow-none">
|
||||
<div className="md:border-r md:pr-4">
|
||||
<h3 className="text-lg font-medium mb-1">Roo Code Provider</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="">
|
||||
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
|
||||
Cloud provider – curated models to work with Roo with no markup, including the
|
||||
latest Gemini, GPT and Claude. Paid with credits.
|
||||
<Link href="/provider/pricing" className="underline hover:no-underline ml-1">
|
||||
See per model pricing.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<h3 className="text-lg font-medium mb-1">Credits</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Credits are pre-paid, in dollars, and are deducted with usage for inference and Cloud
|
||||
Agent runs. You're always in control of your spend, no surprises.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -249,7 +249,7 @@ export default function PricingPage() {
|
|||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Wait, is Roo Code free or not?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes! The Roo Code VS Code extension is open source and free forever. The extension acts
|
||||
|
|
@ -257,7 +257,7 @@ export default function PricingPage() {
|
|||
Code Cloud.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Is there a free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, all paid plans come with a 14-day free trial to try out functionality.
|
||||
|
|
@ -266,12 +266,25 @@ export default function PricingPage() {
|
|||
To use Cloud Agents, you can buy credits.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">How do Cloud Agent credits work?</h3>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">How do credits work?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Cloud Agents are a version of Roo running in the cloud without depending on your IDE.
|
||||
You can run as many as you want, and bring your own inference provider key.
|
||||
Roo Code Cloud credits can be used in two ways:
|
||||
</p>
|
||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
||||
<li>To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)</li>
|
||||
<li>
|
||||
To pay for AI model inference costs (
|
||||
<a
|
||||
href="https://app.roocode.com/provider/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline">
|
||||
varies by model
|
||||
</a>
|
||||
)
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
To cover our infrastructure costs, we charge ${PRICE_CREDITS}/hour while the agent is
|
||||
running (independent of inference costs).
|
||||
|
|
@ -280,25 +293,38 @@ export default function PricingPage() {
|
|||
There are no markups, no tiers, no dumbing-down of models to increase our profit.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Do I need a credit card for the free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, but you won't be charged until your trial ends, except for credit purchases.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">You can cancel anytime with one click.</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">What payment methods do you accept?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
We accept all major credit cards, debit cards, and can arrange invoice billing for
|
||||
Enterprise customers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Can I change plans anytime?</h3>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Can I cancel or change plans?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, you can upgrade or downgrade your plan at any time. Changes will be reflected in
|
||||
your next billing cycle.
|
||||
Yes, you can upgrade, downgrade or cancel your plan at any time. Changes will be
|
||||
reflected in your next billing cycle.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6 md:col-span-2">
|
||||
<h3 className="font-semibold">
|
||||
What if I have enterprise-level needs like SAML/SCIM, large-scale deployments, specific
|
||||
integrations and custom terms?
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
We have an Enterprise plan which can be a fit. Please{" "}
|
||||
<Link href="/enterprise#contact" className="underline hover:no-underline">
|
||||
reach out to our sales team
|
||||
</Link>{" "}
|
||||
to discuss it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
import { ModelWithTotalPrice } from "@/lib/types/models"
|
||||
import { formatCurrency, formatTokens } from "@/lib/formatters"
|
||||
import {
|
||||
ArrowLeftToLine,
|
||||
ArrowRightToLine,
|
||||
Building2,
|
||||
Check,
|
||||
Expand,
|
||||
Gift,
|
||||
HardDriveDownload,
|
||||
HardDriveUpload,
|
||||
RulerDimensionLine,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
interface ModelCardProps {
|
||||
model: ModelWithTotalPrice
|
||||
}
|
||||
|
||||
export function ModelCard({ model }: ModelCardProps) {
|
||||
// Prices are per token, multiply by 1M to get price per million tokens
|
||||
const inputPrice = parseFloat(model.pricing.input) * 1_000_000
|
||||
const outputPrice = parseFloat(model.pricing.output) * 1_000_000
|
||||
const cacheReadPrice = parseFloat(model.pricing.input_cache_read || "0") * 1_000_000
|
||||
const cacheWritePrice = parseFloat(model.pricing.input_cache_write || "0") * 1_000_000
|
||||
|
||||
const free = model.tags.includes("free")
|
||||
// Filter tags to only show vision and reasoning
|
||||
const displayTags = model.tags.filter((tag) => tag === "vision" || tag === "reasoning")
|
||||
|
||||
// Mobile collapsed/expanded state
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"relative cursor-default px-8 pt-7 pb-5 flex flex-col justify-start bg-background border rounded-3xl transition-all hover:shadow-xl",
|
||||
// On mobile, visually hint at expandability
|
||||
"sm:cursor-default",
|
||||
].join(" ")}>
|
||||
{/* Header: always visible */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-semibold tracking-tight mb-2 flex items-center gap-2 justify-between">
|
||||
{model.name}
|
||||
{free && (
|
||||
<span className="inline-flex items-center text-sm font-medium text-green-500">
|
||||
<Gift className="size-4 mr-1" />
|
||||
Free!
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p
|
||||
className={[
|
||||
"text-sm text-muted-foreground",
|
||||
// On mobile + collapsed: clamp description
|
||||
"sm:line-clamp-none",
|
||||
!expanded ? "line-clamp-2" : "",
|
||||
]
|
||||
.join(" ")
|
||||
.trim()}>
|
||||
{model.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content - pinned to bottom */}
|
||||
<div className="overflow-x-auto mt-auto">
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
{/* Provider: always visible if present */}
|
||||
{model.owned_by && (
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<Building2 className="size-4 inline-block mr-1.5" />
|
||||
Provider
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{model.owned_by}</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Context Window: always visible */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<RulerDimensionLine className="size-4 inline-block mr-1.5" />
|
||||
Context Window
|
||||
</td>
|
||||
<td className="py-1.5 text-right font-mono">{formatTokens(model.context_window)}</td>
|
||||
</tr>
|
||||
|
||||
{/* Max Output Tokens: always visible on >=sm, expandable on mobile */}
|
||||
<tr
|
||||
className={["border-b border-border", expanded ? "table-row" : "hidden sm:table-row"].join(
|
||||
" ",
|
||||
)}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<Expand className="size-4 inline-block mr-1.5" />
|
||||
Max Output Tokens
|
||||
</td>
|
||||
<td className="py-1.5 text-right font-mono">{formatTokens(model.max_tokens)}</td>
|
||||
</tr>
|
||||
|
||||
{/* Input Price: always visible */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<ArrowRightToLine className="size-4 inline-block mr-1.5" />
|
||||
Input Price
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{inputPrice === 0 ? "Free" : `${formatCurrency(inputPrice)}/1M tokens`}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Output Price: always visible */}
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
// Add subtle separation from toggle on mobile
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<ArrowLeftToLine className="size-4 inline-block mr-1.5" />
|
||||
Output Price
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{outputPrice === 0 ? "Free" : `${formatCurrency(outputPrice)}/1M tokens`}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Cache pricing: only visible on mobile when expanded, always visible on >=sm */}
|
||||
{cacheReadPrice > 0 && (
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
expanded ? "table-row" : "hidden sm:table-row",
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<HardDriveUpload className="size-4 inline-block mr-1.5" />
|
||||
Cache Read
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{formatCurrency(cacheReadPrice)}/1M tokens</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{cacheWritePrice > 0 && (
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
expanded ? "table-row" : "hidden sm:table-row",
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<HardDriveDownload className="size-4 inline-block mr-1.5" />
|
||||
Cache Write
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{formatCurrency(cacheWritePrice)}/1M tokens</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Tags row: only show if there are vision or reasoning tags */}
|
||||
{displayTags.length > 0 && (
|
||||
<tr className={[expanded ? "table-row" : "hidden sm:table-row"].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground align-top">Features</td>
|
||||
<td className="py-1.5">
|
||||
{displayTags.map((tag) => (
|
||||
<span key={tag} className="flex justify-end items-center text-xs capitalize">
|
||||
<Check className="size-3 m-1" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Mobile-only toggle row */}
|
||||
<tr className="sm:hidden">
|
||||
<td colSpan={2} className="pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="w-full inline-flex items-center justify-center gap-1.5 text-xs font-medium text-primary">
|
||||
{expanded ? "Less" : "More"}
|
||||
{expanded ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
253
apps/web-roo-code/src/app/provider/pricing/page.tsx
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { ModelCard } from "./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"
|
||||
|
||||
const API_URL = "https://api.roocode.com/proxy/v1/models?include_paid=true"
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What are AI model providers?",
|
||||
answer: "AI model providers offer various language models with different capabilities and pricing.",
|
||||
},
|
||||
{
|
||||
question: "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>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "But how much does the Roo Code Cloud service cost?",
|
||||
answer: (
|
||||
<>
|
||||
Our{" "}
|
||||
<Link href="/pricing" className="underline hover:no-underline">
|
||||
service pricing is here.
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function calculateTotalPrice(model: Model): number {
|
||||
return parseFloat(model.pricing.input) + parseFloat(model.pricing.output)
|
||||
}
|
||||
|
||||
function enrichModelWithTotalPrice(model: Model): ModelWithTotalPrice {
|
||||
return {
|
||||
...model,
|
||||
totalPrice: calculateTotalPrice(model),
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProviderPricingPage() {
|
||||
const [models, setModels] = useState<ModelWithTotalPrice[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortOption, setSortOption] = useState<SortOption>("alphabetical")
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchModels() {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const response = await fetch(API_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.statusText}`)
|
||||
}
|
||||
const data: ModelsResponse = await response.json()
|
||||
const enrichedModels = data.data.map(enrichModelWithTotalPrice)
|
||||
setModels(enrichedModels)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "An error occurred while fetching models")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchModels()
|
||||
}, [])
|
||||
|
||||
const filteredAndSortedModels = useMemo(() => {
|
||||
// Filter out deprecated models
|
||||
let filtered = models.filter((model) => !model.deprecated)
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
filtered = filtered.filter((model) => {
|
||||
return (
|
||||
model.name.toLowerCase().includes(query) ||
|
||||
model.owned_by?.toLowerCase().includes(query) ||
|
||||
model.description.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Sort filtered results
|
||||
const sorted = [...filtered]
|
||||
switch (sortOption) {
|
||||
case "alphabetical":
|
||||
sorted.sort((a, b) => a.name.localeCompare(b.name))
|
||||
break
|
||||
case "price-asc":
|
||||
sorted.sort((a, b) => a.totalPrice - b.totalPrice)
|
||||
break
|
||||
case "price-desc":
|
||||
sorted.sort((a, b) => b.totalPrice - a.totalPrice)
|
||||
break
|
||||
case "context-window-asc":
|
||||
sorted.sort((a, b) => a.context_window - b.context_window)
|
||||
break
|
||||
case "context-window-desc":
|
||||
sorted.sort((a, b) => b.context_window - a.context_window)
|
||||
break
|
||||
}
|
||||
|
||||
return sorted
|
||||
}, [models, searchQuery, sortOption])
|
||||
|
||||
// Count non-deprecated models for the display
|
||||
const nonDeprecatedCount = useMemo(() => models.filter((model) => !model.deprecated).length, [models])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative overflow-hidden py-16">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">
|
||||
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 (
|
||||
<Link href="#faq" className="underline hover:no-underline">
|
||||
FAQ
|
||||
</Link>
|
||||
).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-10 relative border-t border-b">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/0 via-violet-500/10 to-violet-500/0 dark:from-blue-500/10 dark:via-cyan-500/10 dark:to-purple-500/10" />
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search models..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-full border border-input bg-background px-10 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
|
||||
<div className="text-sm cursor-default text-muted-foreground absolute bg-background right-0 top-0 m-0.5 px-3 py-2 rounded-full">
|
||||
{filteredAndSortedModels.length} of {nonDeprecatedCount} models
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<select
|
||||
id="sort"
|
||||
value={sortOption}
|
||||
onChange={(e) => setSortOption(e.target.value as SortOption)}
|
||||
className="rounded-full cursor-pointer border border-input bg-background hover:bg-muted pl-4 w-full md:w-auto pr-9 py-2.5 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative appearance-none">
|
||||
<option value="alphabetical">Alphabetical</option>
|
||||
<option value="price-asc">Price: Low to High</option>
|
||||
<option value="price-desc">Price: High to Low</option>
|
||||
<option value="context-window-asc">Context Window: Small to Large</option>
|
||||
<option value="context-window-desc">Context Window: Large to Small</option>
|
||||
</select>
|
||||
<ChevronDown className="size-4 absolute right-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 ">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{loading && (
|
||||
<div className="text-center pt-12 space-y-2 mb-4">
|
||||
<LoaderCircle className="size-8 text-muted-foreground mx-auto animate-spin" />
|
||||
<p className="text-lg">Loading model list...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-center pt-12 space-y-2">
|
||||
<CircleX className="size-8 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-lg">Oops, couldn't load the model list.</p>
|
||||
<p className="text-muted-foreground">Try again in a bit please.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && filteredAndSortedModels.length === 0 && (
|
||||
<div className="text-center pt-12 space-y-2">
|
||||
<Loader className="size-8 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-lg">No models match your search.</p>
|
||||
<p className="text-muted-foreground">
|
||||
Keep in mind we don't have every model under the sun – only the ones we think
|
||||
are worth using.
|
||||
<br />
|
||||
You can always use a third-party provider to access a wider selection.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && filteredAndSortedModels.length > 0 && (
|
||||
<div className="grid gap-4 pt-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredAndSortedModels.map((model) => (
|
||||
<ModelCard key={model.id} model={model} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="bg-background my-16 relative z-50">
|
||||
<a id="faq" />
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
|
||||
{faqs.map((faq, index) => (
|
||||
<div key={index} className="rounded-lg border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">{faq.question}</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{faq.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
ArrowRight,
|
||||
Blocks,
|
||||
BookMarked,
|
||||
ListChecks,
|
||||
LucideIcon,
|
||||
GitPullRequest,
|
||||
Key,
|
||||
MessageSquareCode,
|
||||
Wrench,
|
||||
} from "lucide-react"
|
||||
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 { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { trackGoogleAdsConversion } from "@/lib/analytics/google-ads"
|
||||
|
||||
interface Feature {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string | React.ReactNode
|
||||
logos?: string[]
|
||||
}
|
||||
|
||||
const workflowSteps: Feature[] = [
|
||||
{
|
||||
icon: GitPullRequest,
|
||||
title: "1. Connect Your Repository",
|
||||
description: "Link your GitHub repository and configure which branches and pull requests should be reviewed.",
|
||||
},
|
||||
{
|
||||
icon: Key,
|
||||
title: "2. Add Your API Key",
|
||||
description:
|
||||
"Provide your AI provider API key and set your review preferences, custom rules, and quality standards.",
|
||||
},
|
||||
{
|
||||
icon: MessageSquareCode,
|
||||
title: "3. Get Review Comments",
|
||||
description:
|
||||
"Every pull request gets detailed GitHub comments in minutes from a Roo Code agent highlighting issues and suggesting improvements.",
|
||||
},
|
||||
]
|
||||
|
||||
const howItWorks: Feature[] = [
|
||||
{
|
||||
icon: Blocks,
|
||||
title: "Our agents, your provider keys",
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
We orchestrate the review, optimize the hell out of the prompts, integrate with GitHub, keep you
|
||||
properly posted.
|
||||
</p>
|
||||
<p>We're thoughtful about token usage, but not incentivized to skimp to grow our margins.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: ListChecks,
|
||||
title: "Advanced reasoning and workflows",
|
||||
description:
|
||||
"We optimize for state-of-the-art reasoning models and leverage powerful workflows (Diff analysis → Context Gathering → Impact Mapping → Contract checks) to produce crisp, actionable comments at the right level.",
|
||||
},
|
||||
{
|
||||
icon: BookMarked,
|
||||
title: "Fully repository-aware",
|
||||
description:
|
||||
"Reviews traverse code ownership, dependency graphs, and historical patterns to surface risk and deviations, not noise.",
|
||||
},
|
||||
]
|
||||
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import hero from "/public/heroes/agent-reviewer.png"
|
||||
|
||||
export function ReviewerContent() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative flex md:h-[calc(70vh-theme(spacing.12))] 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-20 lg:grid-cols-2">
|
||||
<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mt-8 md:text-left md:text-4xl lg:text-5xl lg:mt-0">
|
||||
<GitPullRequest className="size-12 mb-4" />
|
||||
Get comprehensive code reviews that save you time, not tokens.
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
|
||||
<p>
|
||||
Regular AI code review tools cap model usage to protect their margins from fixed
|
||||
monthly prices. That leads to shallow prompts, limited context, and missed
|
||||
issues.
|
||||
</p>
|
||||
<p>
|
||||
Roo Code's PR Reviewer flips the script: you bring your own key and
|
||||
leverage it to the max – to find real issues, increase code quality and keep
|
||||
your pull request queue moving.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cross-agent link */}
|
||||
<div className="mt-6 flex flex-col md:flex-row md:items-center gap-2">
|
||||
Works great with
|
||||
<Link
|
||||
href="/pr-fixer"
|
||||
className="flex p-4 items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-3 py-1 text-sm text-blue-600 backdrop-blur-sm transition-colors hover:bg-blue-500/20 dark:text-blue-400"
|
||||
aria-label="Works great with PR Fixer">
|
||||
<Wrench className="size-4 mr-2" />
|
||||
PR Fixer Agent
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0 md:items-center">
|
||||
<Button
|
||||
size="lg"
|
||||
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}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={trackGoogleAdsConversion}
|
||||
className="flex w-full items-center justify-center">
|
||||
Start 14-day Free Trial
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
<span className="text-sm text-center md:text-left text-muted-foreground md:ml-2">
|
||||
(cancel anytime)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
|
||||
<div className="md:w-[800px] md:h-[474px] relative overflow-clip">
|
||||
<div className="block">
|
||||
<Image
|
||||
src={hero}
|
||||
alt="Example of a code review generated by Roo Code PR Reviewer"
|
||||
className="max-w-full h-auto"
|
||||
width={800}
|
||||
height={474}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">How It Works</h2>
|
||||
</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-3 lg:gap-8">
|
||||
{workflowSteps.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 hover:shadow-lg">
|
||||
<Icon className="size-6 text-foreground/80" />
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
|
||||
{step.title}
|
||||
</h3>
|
||||
<div className="leading-relaxed font-light text-muted-foreground">
|
||||
{step.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
Why Roo's PR Reviewer is so much better
|
||||
</h2>
|
||||
</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">
|
||||
{howItWorks.map((feature, index) => {
|
||||
const Icon = feature.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" />
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<div className="leading-relaxed font-light text-muted-foreground space-y-2">
|
||||
{feature.description}
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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 max-w-4xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
The first member of a whole new team
|
||||
</h2>
|
||||
|
||||
<p className="mt-6 text-lg text-muted-foreground">
|
||||
Architecture, coding, reviewing, testing, debugging, documenting, designing –{" "}
|
||||
<em>almost everything</em> 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 PR Reviewer isn't yet another single-purpose tool to add to your already
|
||||
complicated stack.
|
||||
<br />
|
||||
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]">
|
||||
<AgentCarousel />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20">
|
||||
<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">Stop wasting time.</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">
|
||||
Give Roo Code's PR Reviewer your model key and turn painful reviews into a tangible
|
||||
quality advantage.
|
||||
</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"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={trackGoogleAdsConversion}
|
||||
className="flex items-center justify-center">
|
||||
Start 14-day Free Trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
93
apps/web-roo-code/src/app/reviewer/content-b.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { type AgentPageContent } from "@/app/shared/agent-page-content"
|
||||
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import hero from "/public/heroes/agent-reviewer.png"
|
||||
|
||||
// Re-export for convenience
|
||||
export type { AgentPageContent }
|
||||
|
||||
export const content: AgentPageContent = {
|
||||
agentName: "PR Reviewer",
|
||||
hero: {
|
||||
icon: "GitPullRequest",
|
||||
heading: "Code reviews that catch what other AI tools (and most humans) miss.",
|
||||
paragraphs: [
|
||||
"Run-of-the-mill, token-saving AI code review tools will surely catch syntax errors and style issues, but they'll usually miss the bugs that actually matter: logic flaws, security vulnerabilities, and misunderstood requirements.",
|
||||
"Roo Code's PR Reviewer uses advanced reasoning models and full repository context to find the issues that slip through—before they reach production.",
|
||||
],
|
||||
image: {
|
||||
url: hero.src,
|
||||
width: 800,
|
||||
height: 474,
|
||||
alt: "Example of a code review generated by Roo Code PR Reviewer",
|
||||
},
|
||||
crossAgentLink: {
|
||||
text: "Works great with",
|
||||
links: [
|
||||
{
|
||||
text: "PR Fixer Agent",
|
||||
href: "/pr-fixer",
|
||||
icon: "Wrench",
|
||||
},
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
buttonText: "Try now for free",
|
||||
disclaimer: "",
|
||||
tracking: "&agent=reviewer",
|
||||
},
|
||||
},
|
||||
howItWorks: {
|
||||
heading: "How It Works",
|
||||
steps: [
|
||||
{
|
||||
title: "1. Connect Your Repository",
|
||||
description:
|
||||
"Link your GitHub repository and configure which branches and pull requests should be reviewed.",
|
||||
icon: "GitPullRequest",
|
||||
},
|
||||
{
|
||||
title: "2. Add Your API Key",
|
||||
description:
|
||||
"Provide your AI provider API key and set your review preferences, custom rules, and quality standards.",
|
||||
icon: "Key",
|
||||
},
|
||||
{
|
||||
title: "3. Get Review Comments",
|
||||
description:
|
||||
"Every pull request gets detailed GitHub comments in minutes from a Roo Code agent highlighting issues and suggesting improvements.",
|
||||
icon: "MessageSquareCode",
|
||||
},
|
||||
],
|
||||
},
|
||||
whyBetter: {
|
||||
heading: "Why Roo's PR Reviewer is different",
|
||||
features: [
|
||||
{
|
||||
title: "Bring your own key, get uncompromised reviews",
|
||||
paragraphs: [
|
||||
"Most AI review tools use fixed pricing, which means they skimp on tokens to protect their margins. That leads to shallow analysis and missed issues.",
|
||||
"With Roo, you bring your own API key. We optimize prompts for depth, not cost-cutting, so reviews focus on real problems like business logic, security vulnerabilities, and architectural issues.",
|
||||
],
|
||||
icon: "Blocks",
|
||||
},
|
||||
{
|
||||
title: "Advanced reasoning that understands what matters",
|
||||
description:
|
||||
"We leverage state-of-the-art reasoning models with sophisticated workflows: diff analysis, context gathering, impact mapping, and contract validation. This catches the subtle bugs that surface-level tools miss—misunderstood requirements, edge cases, and integration risks.",
|
||||
icon: "ListChecks",
|
||||
},
|
||||
{
|
||||
title: "Repository-aware, not snippet-aware",
|
||||
description:
|
||||
"Roo analyzes your entire codebase context—dependency graphs, code ownership, team conventions, and historical patterns. It understands how changes interact with existing systems, not just whether individual lines look correct.",
|
||||
icon: "BookMarked",
|
||||
},
|
||||
],
|
||||
},
|
||||
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: "Try now for free",
|
||||
},
|
||||
}
|
||||
93
apps/web-roo-code/src/app/reviewer/content.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { type AgentPageContent } from "@/app/shared/agent-page-content"
|
||||
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import hero from "/public/heroes/agent-reviewer.png"
|
||||
|
||||
// Re-export for convenience
|
||||
export type { AgentPageContent }
|
||||
|
||||
export const content: AgentPageContent = {
|
||||
agentName: "PR Reviewer",
|
||||
hero: {
|
||||
icon: "GitPullRequest",
|
||||
heading: "Code reviews that catch what other AI tools (and most humans) miss.",
|
||||
paragraphs: [
|
||||
"Run-of-the-mill, token-saving AI code review tools will surely catch syntax errors and style issues, but they'll usually miss the bugs that actually matter: logic flaws, security vulnerabilities, and misunderstood requirements.",
|
||||
"Roo Code's PR Reviewer uses advanced reasoning models and full repository context to find the issues that slip through—before they reach production.",
|
||||
],
|
||||
image: {
|
||||
url: hero.src,
|
||||
width: 800,
|
||||
height: 474,
|
||||
alt: "Example of a code review generated by Roo Code PR Reviewer",
|
||||
},
|
||||
crossAgentLink: {
|
||||
text: "Works great with",
|
||||
links: [
|
||||
{
|
||||
text: "PR Fixer Agent",
|
||||
href: "/pr-fixer",
|
||||
icon: "Wrench",
|
||||
},
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
buttonText: "Try now for free",
|
||||
disclaimer: "",
|
||||
tracking: "&agent=reviewer",
|
||||
},
|
||||
},
|
||||
howItWorks: {
|
||||
heading: "How It Works",
|
||||
steps: [
|
||||
{
|
||||
title: "1. Connect Your Repository",
|
||||
description:
|
||||
"Link your GitHub repository and configure which branches and pull requests should be reviewed.",
|
||||
icon: "GitPullRequest",
|
||||
},
|
||||
{
|
||||
title: "2. Add Your API Key",
|
||||
description:
|
||||
"Provide your AI provider API key and set your review preferences, custom rules, and quality standards.",
|
||||
icon: "Key",
|
||||
},
|
||||
{
|
||||
title: "3. Get Review Comments",
|
||||
description:
|
||||
"Every pull request gets detailed GitHub comments in minutes from a Roo Code agent highlighting issues and suggesting improvements.",
|
||||
icon: "MessageSquareCode",
|
||||
},
|
||||
],
|
||||
},
|
||||
whyBetter: {
|
||||
heading: "Why Roo's PR Reviewer is different",
|
||||
features: [
|
||||
{
|
||||
title: "Bring your own key, get uncompromised reviews",
|
||||
paragraphs: [
|
||||
"Most AI review tools use fixed pricing, which means they skimp on tokens to protect their margins. That leads to shallow analysis and missed issues.",
|
||||
"With Roo, you bring your own API key. We optimize prompts for depth, not cost-cutting, so reviews focus on real problems like business logic, security vulnerabilities, and architectural issues.",
|
||||
],
|
||||
icon: "Blocks",
|
||||
},
|
||||
{
|
||||
title: "Advanced reasoning that understands what matters",
|
||||
description:
|
||||
"We leverage state-of-the-art reasoning models with sophisticated workflows: diff analysis, context gathering, impact mapping, and contract validation. This catches the subtle bugs that surface-level tools miss—misunderstood requirements, edge cases, and integration risks.",
|
||||
icon: "ListChecks",
|
||||
},
|
||||
{
|
||||
title: "Repository-aware, not snippet-aware",
|
||||
description:
|
||||
"Roo analyzes your entire codebase context—dependency graphs, code ownership, team conventions, and historical patterns. It understands how changes interact with existing systems, not just whether individual lines look correct.",
|
||||
icon: "BookMarked",
|
||||
},
|
||||
],
|
||||
},
|
||||
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: "Try now for free",
|
||||
},
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@ import type { Metadata } from "next"
|
|||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { ReviewerContent } from "./ReviewerContent"
|
||||
import { AgentLandingContent } from "@/app/shared/AgentLandingContent"
|
||||
import { getContentVariant } from "@/app/shared/getContentVariant"
|
||||
import { content as contentA } from "./content"
|
||||
import { content as contentB } from "./content-b"
|
||||
|
||||
const TITLE = "PR Reviewer"
|
||||
const DESCRIPTION =
|
||||
|
|
@ -56,6 +59,12 @@ export const metadata: Metadata = {
|
|||
],
|
||||
}
|
||||
|
||||
export default function AgentReviewerPage() {
|
||||
return <ReviewerContent />
|
||||
export default async function AgentReviewerPage({ searchParams }: { searchParams: Promise<{ v?: string }> }) {
|
||||
const params = await searchParams
|
||||
const content = getContentVariant(params, {
|
||||
A: contentA,
|
||||
B: contentB,
|
||||
})
|
||||
|
||||
return <AgentLandingContent content={content} />
|
||||
}
|
||||
|
|
|
|||
235
apps/web-roo-code/src/app/shared/AgentLandingContent.tsx
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
ArrowRight,
|
||||
GitPullRequest,
|
||||
Wrench,
|
||||
Key,
|
||||
MessageSquareCode,
|
||||
Blocks,
|
||||
ListChecks,
|
||||
BookMarked,
|
||||
History,
|
||||
LucideIcon,
|
||||
} from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground, UseExamplesSection } from "@/components/homepage"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { type AgentPageContent, type IconName } from "./agent-page-content"
|
||||
|
||||
/**
|
||||
* Maps icon names to actual Lucide icon components
|
||||
*/
|
||||
const iconMap: Record<IconName, LucideIcon> = {
|
||||
GitPullRequest,
|
||||
Wrench,
|
||||
Key,
|
||||
MessageSquareCode,
|
||||
Blocks,
|
||||
ListChecks,
|
||||
BookMarked,
|
||||
History,
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an icon name string to a Lucide icon component
|
||||
*/
|
||||
function getIcon(iconName?: IconName): LucideIcon | undefined {
|
||||
return iconName ? iconMap[iconName] : undefined
|
||||
}
|
||||
|
||||
export function AgentLandingContent({ content }: { content: AgentPageContent }) {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex min-h-screen md:min-h-[calc(70vh-theme(spacing.12))] items-center overflow-hidden py-12 md:py-0">
|
||||
<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:gap-20 lg:grid-cols-2">
|
||||
<div className="flex flex-col justify-center space-y-6 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight md:text-left md:text-4xl lg:text-5xl">
|
||||
{content.hero.icon &&
|
||||
(() => {
|
||||
const Icon = getIcon(content.hero.icon)
|
||||
return Icon ? <Icon className="size-12 mb-4" /> : null
|
||||
})()}
|
||||
{content.hero.heading}
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 max-w-full lg:max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
|
||||
{content.hero.paragraphs.map((paragraph, index) => (
|
||||
<p key={index}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Cross-agent link */}
|
||||
<div className="mt-6 flex flex-col md:flex-row md:items-center gap-2">
|
||||
{content.hero.crossAgentLink.text}
|
||||
{content.hero.crossAgentLink.links.map((link, index) => {
|
||||
const Icon = getIcon(link.icon)
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={link.href}
|
||||
className="flex p-4 items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-3 py-1 text-sm text-blue-600 backdrop-blur-sm transition-colors hover:bg-blue-500/20 dark:text-blue-400"
|
||||
aria-label={`Works great with ${link.text}`}>
|
||||
{Icon && <Icon className="size-4 mr-2" />}
|
||||
{link.text}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0 md:items-center">
|
||||
<Button
|
||||
size="lg"
|
||||
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}${content.hero.cta.tracking}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex w-full items-center justify-center">
|
||||
{content.hero.cta.buttonText}
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
<span className="text-sm text-center md:text-left text-muted-foreground md:ml-2">
|
||||
{content.hero.cta.disclaimer}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{content.hero.image && (
|
||||
<div className="flex items-center justify-center lg:justify-end mx-auto h-full w-full">
|
||||
<div className="relative w-full max-w-full overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src={content.hero.image.url}
|
||||
alt={content.hero.image.alt || "Hero image"}
|
||||
className="w-full h-auto"
|
||||
width={content.hero.image.width}
|
||||
height={content.hero.image.height}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
{content.howItWorks.heading}
|
||||
</h2>
|
||||
</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-3 lg:gap-8">
|
||||
{content.howItWorks.steps.map((step, index) => {
|
||||
const Icon = getIcon(step.icon)
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
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">
|
||||
{step.title}
|
||||
</h3>
|
||||
<div className="leading-relaxed font-light text-muted-foreground">
|
||||
{step.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
{content.whyBetter.heading}
|
||||
</h2>
|
||||
</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">
|
||||
{content.whyBetter.features.map((feature, index) => {
|
||||
const Icon = getIcon(feature.icon)
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300">
|
||||
{Icon && <Icon className="size-6 text-foreground/80" />}
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<div className="leading-relaxed font-light text-muted-foreground space-y-2">
|
||||
{feature.description && <p>{feature.description}</p>}
|
||||
{feature.paragraphs &&
|
||||
feature.paragraphs.map((paragraph, pIndex) => (
|
||||
<p key={pIndex}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<UseExamplesSection agentTitle={true} />
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20">
|
||||
<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">{content.cta.heading}</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">
|
||||
{content.cta.description}
|
||||
</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"
|
||||
asChild>
|
||||
<a
|
||||
href={`${EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}${content.hero.cta.tracking}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
{content.hero.cta.buttonText}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
75
apps/web-roo-code/src/app/shared/agent-page-content.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Supported icon names that can be used in agent page content.
|
||||
* These strings are mapped to actual Lucide components in the client.
|
||||
*/
|
||||
export type IconName =
|
||||
| "GitPullRequest"
|
||||
| "Wrench"
|
||||
| "Key"
|
||||
| "MessageSquareCode"
|
||||
| "Blocks"
|
||||
| "ListChecks"
|
||||
| "BookMarked"
|
||||
| "History"
|
||||
|
||||
/**
|
||||
* Generic content structure for agent landing pages.
|
||||
* This interface can be reused across different agent pages (PR Reviewer, PR Fixer, etc.)
|
||||
* to maintain consistency and enable A/B testing capabilities.
|
||||
*
|
||||
* Note: Icons are referenced by string names (not components) to support
|
||||
* serialization from Server Components to Client Components.
|
||||
*/
|
||||
export interface AgentPageContent {
|
||||
agentName: string
|
||||
hero: {
|
||||
/** Optional icon name to display in the hero section */
|
||||
icon?: IconName
|
||||
heading: string
|
||||
paragraphs: string[]
|
||||
image?: {
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
alt?: string
|
||||
}
|
||||
crossAgentLink: {
|
||||
text: string
|
||||
links: Array<{
|
||||
text: string
|
||||
href: string
|
||||
icon?: IconName
|
||||
}>
|
||||
}
|
||||
cta: {
|
||||
buttonText: string
|
||||
disclaimer: string
|
||||
tracking: string
|
||||
}
|
||||
}
|
||||
howItWorks: {
|
||||
heading: string
|
||||
steps: Array<{
|
||||
title: string
|
||||
/** Supports rich text content including React components */
|
||||
description: string | React.ReactNode
|
||||
icon?: IconName
|
||||
}>
|
||||
}
|
||||
whyBetter: {
|
||||
heading: string
|
||||
features: Array<{
|
||||
title: string
|
||||
/** Supports rich text content including React components */
|
||||
description?: string | React.ReactNode
|
||||
/** Supports rich text content including React components */
|
||||
paragraphs?: Array<string | React.ReactNode>
|
||||
icon?: IconName
|
||||
}>
|
||||
}
|
||||
cta: {
|
||||
heading: string
|
||||
description: string
|
||||
buttonText: string
|
||||
}
|
||||
}
|
||||
36
apps/web-roo-code/src/app/shared/getContentVariant.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { AgentPageContent } from "./agent-page-content"
|
||||
|
||||
/**
|
||||
* Selects the appropriate content variant based on the query parameter.
|
||||
*
|
||||
* @param searchParams - The search parameters from the page props
|
||||
* @param variants - A record mapping variant letters to content objects
|
||||
* @returns The selected content variant, defaulting to variant 'A' if not found or invalid
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const content = getContentVariant(searchParams, {
|
||||
* A: contentA,
|
||||
* B: contentB,
|
||||
* C: contentC,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function getContentVariant(
|
||||
searchParams: { v?: string },
|
||||
variants: Record<string, AgentPageContent>,
|
||||
): AgentPageContent {
|
||||
const variant = searchParams.v?.toUpperCase()
|
||||
|
||||
// Return the specified variant if it exists, otherwise default to 'A'
|
||||
if (variant && variants[variant]) {
|
||||
return variants[variant]
|
||||
}
|
||||
|
||||
// Ensure 'A' variant always exists as fallback
|
||||
if (!variants.A) {
|
||||
throw new Error("Content variants must include variant 'A' as the default")
|
||||
}
|
||||
|
||||
return variants.A
|
||||
}
|
||||
|
|
@ -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 { ChevronDown, X } from "lucide-react"
|
||||
|
||||
interface NavBarProps {
|
||||
stars: string | null
|
||||
|
|
@ -93,35 +93,41 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
</div>
|
||||
</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">
|
||||
<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 font-medium text-muted-foreground 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 font-medium 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 font-medium 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 font-medium 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 */}
|
||||
|
|
@ -226,15 +232,24 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
{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 text-center 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-[22px] 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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/pricing">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,153 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Script from "next/script"
|
||||
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
|
||||
|
||||
// Google Tag Manager ID
|
||||
const GTM_ID = "AW-17391954825"
|
||||
|
||||
/**
|
||||
* Google Analytics Provider with Consent Mode v2
|
||||
* Implements cookieless pings and advanced consent management
|
||||
*/
|
||||
export function GoogleAnalyticsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [shouldLoad, setShouldLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize consent defaults BEFORE loading gtag.js (required for Consent Mode v2)
|
||||
initializeConsentDefaults()
|
||||
|
||||
// Check initial consent status
|
||||
if (hasConsent()) {
|
||||
setShouldLoad(true)
|
||||
updateConsentGranted()
|
||||
}
|
||||
|
||||
// Listen for consent changes
|
||||
const unsubscribe = onConsentChange((consented) => {
|
||||
if (consented) {
|
||||
if (!shouldLoad) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
updateConsentGranted()
|
||||
} else {
|
||||
updateConsentDenied()
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- shouldLoad intentionally omitted to prevent re-initialization loop
|
||||
}, [])
|
||||
|
||||
const initializeConsentDefaults = () => {
|
||||
// Set up consent defaults before gtag loads (Consent Mode v2 requirement)
|
||||
if (typeof window !== "undefined") {
|
||||
window.dataLayer = window.dataLayer || []
|
||||
window.gtag = function (...args: GtagArgs) {
|
||||
window.dataLayer.push(args)
|
||||
}
|
||||
|
||||
// Set default consent state to 'denied' with cookieless pings enabled
|
||||
window.gtag("consent", "default", {
|
||||
ad_storage: "denied",
|
||||
ad_user_data: "denied",
|
||||
ad_personalization: "denied",
|
||||
analytics_storage: "denied",
|
||||
functionality_storage: "denied",
|
||||
personalization_storage: "denied",
|
||||
security_storage: "granted", // Always granted for security
|
||||
wait_for_update: 500, // Wait 500ms for consent before sending data
|
||||
})
|
||||
|
||||
// Enable cookieless pings for Google Ads
|
||||
window.gtag("set", "url_passthrough", true)
|
||||
}
|
||||
}
|
||||
|
||||
const updateConsentGranted = () => {
|
||||
// User accepted cookies - update consent to granted
|
||||
if (typeof window !== "undefined" && window.gtag) {
|
||||
window.gtag("consent", "update", {
|
||||
ad_storage: "granted",
|
||||
ad_user_data: "granted",
|
||||
ad_personalization: "granted",
|
||||
analytics_storage: "granted",
|
||||
functionality_storage: "granted",
|
||||
personalization_storage: "granted",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateConsentDenied = () => {
|
||||
// User declined cookies - keep consent denied (cookieless pings still work)
|
||||
if (typeof window !== "undefined" && window.gtag) {
|
||||
window.gtag("consent", "update", {
|
||||
ad_storage: "denied",
|
||||
ad_user_data: "denied",
|
||||
ad_personalization: "denied",
|
||||
analytics_storage: "denied",
|
||||
functionality_storage: "denied",
|
||||
personalization_storage: "denied",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Always render scripts (Consent Mode v2 needs gtag loaded even without consent)
|
||||
// Cookieless pings will work with denied consent
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Google tag (gtag.js) - Loads immediately for Consent Mode v2 */}
|
||||
<Script
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${GTM_ID}`}
|
||||
strategy="afterInteractive"
|
||||
onLoad={() => {
|
||||
// Initialize gtag config after script loads
|
||||
if (typeof window !== "undefined" && window.gtag) {
|
||||
window.gtag("js", new Date())
|
||||
window.gtag("config", GTM_ID)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Type definitions for Google Analytics with Consent Mode v2
|
||||
type ConsentState = "granted" | "denied"
|
||||
|
||||
interface ConsentParams {
|
||||
ad_storage?: ConsentState
|
||||
ad_user_data?: ConsentState
|
||||
ad_personalization?: ConsentState
|
||||
analytics_storage?: ConsentState
|
||||
functionality_storage?: ConsentState
|
||||
personalization_storage?: ConsentState
|
||||
security_storage?: ConsentState
|
||||
wait_for_update?: number
|
||||
}
|
||||
|
||||
type GtagArgs =
|
||||
| ["js", Date]
|
||||
| ["config", string, GtagConfig?]
|
||||
| ["event", string, GtagEventParameters?]
|
||||
| ["consent", "default" | "update", ConsentParams]
|
||||
| ["set", string, unknown]
|
||||
|
||||
interface GtagConfig {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface GtagEventParameters {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Declare global types for TypeScript
|
||||
declare global {
|
||||
interface Window {
|
||||
dataLayer: GtagArgs[]
|
||||
gtag: (...args: GtagArgs) => void
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Script from "next/script"
|
||||
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
|
||||
|
||||
// Google Tag Manager Container ID
|
||||
const GTM_ID = "GTM-M2JZHV8N"
|
||||
|
||||
/**
|
||||
* Google Tag Manager Provider
|
||||
* Loads GTM only after user consent is given, following GDPR requirements
|
||||
*/
|
||||
export function GoogleTagManagerProvider({ children }: { children: React.ReactNode }) {
|
||||
const [shouldLoad, setShouldLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial consent status
|
||||
if (hasConsent()) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
|
||||
// Listen for consent changes
|
||||
const unsubscribe = onConsentChange((consented) => {
|
||||
if (consented) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldLoad && (
|
||||
<>
|
||||
{/* Google Tag Manager Script */}
|
||||
<Script
|
||||
id="google-tag-manager"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','${GTM_ID}');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* Google Tag Manager (noscript) */}
|
||||
<noscript>
|
||||
<iframe
|
||||
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
|
||||
height="0"
|
||||
width="0"
|
||||
style={{ display: "none", visibility: "hidden" }}
|
||||
/>
|
||||
</noscript>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -32,7 +32,6 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||
// Initialize PostHog immediately on the client side
|
||||
if (typeof window !== "undefined" && !posthog.__loaded) {
|
||||
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
|
||||
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST
|
||||
|
||||
// Check if environment variables are set
|
||||
if (!posthogKey) {
|
||||
|
|
@ -43,19 +42,13 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||
return
|
||||
}
|
||||
|
||||
if (!posthogHost) {
|
||||
console.warn(
|
||||
"PostHog host URL is missing. Using default host. " +
|
||||
"Please set NEXT_PUBLIC_POSTHOG_HOST in your .env file.",
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user has already consented to cookies
|
||||
const userHasConsented = hasConsent()
|
||||
|
||||
// Initialize PostHog with appropriate persistence based on consent
|
||||
posthog.init(posthogKey, {
|
||||
api_host: posthogHost || "https://us.i.posthog.com",
|
||||
api_host: "https://ph.roocode.com",
|
||||
ui_host: "https://us.posthog.com",
|
||||
capture_pageview: false, // We handle pageview tracking manually
|
||||
loaded: (posthogInstance) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { ThemeProvider } from "next-themes"
|
||||
|
||||
import { GoogleTagManagerProvider } from "./google-tag-manager-provider"
|
||||
import { PostHogProvider } from "./posthog-provider"
|
||||
import { GoogleAnalyticsProvider } from "./google-analytics-provider"
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GoogleAnalyticsProvider>
|
||||
<GoogleTagManagerProvider>
|
||||
<PostHogProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</PostHogProvider>
|
||||
</GoogleAnalyticsProvider>
|
||||
</GoogleTagManagerProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,125 +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, Languages, Microscope, PocketKnife, TestTube, type LucideIcon } from "lucide-react"
|
||||
|
||||
// AI Agent types for the carousel
|
||||
interface AIAgent {
|
||||
icon: LucideIcon
|
||||
name: string
|
||||
}
|
||||
|
||||
const aiAgents: AIAgent[] = [
|
||||
{ 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() {
|
||||
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 (
|
||||
<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 font-medium text-foreground/90 transition-colors duration-300 dark:text-foreground">
|
||||
{agent.name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
/**
|
||||
* Google Ads conversion tracking utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Track a Google Ads conversion event
|
||||
* This should only be called after user consent has been given
|
||||
*/
|
||||
export function trackGoogleAdsConversion() {
|
||||
if (typeof window !== "undefined" && window.gtag) {
|
||||
window.gtag("event", "conversion", {
|
||||
send_to: "AW-17391954825/VtOZCJe_77MbEInXkOVA",
|
||||
value: 10.0,
|
||||
currency: "USD",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,8 @@ export const EXTERNAL_LINKS = {
|
|||
TESTIMONIALS: "https://roocode.com/#testimonials",
|
||||
CLOUD_APP_LOGIN: "https://app.roocode.com/sign-in",
|
||||
CLOUD_APP_SIGNUP: "https://app.roocode.com/sign-up",
|
||||
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/welcome",
|
||||
CLOUD_APP_SIGNUP_HOME: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
}
|
||||
|
||||
export const INTERNAL_LINKS = {
|
||||
|
|
|
|||
22
apps/web-roo-code/src/lib/formatters.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
|
||||
export const formatCurrency = (amount: number) => formatter.format(amount)
|
||||
|
||||
export const formatTokens = (tokens: number) => {
|
||||
if (tokens < 1000) {
|
||||
return tokens.toString()
|
||||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}K`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
|
|
@ -104,13 +104,19 @@ export async function getVSCodeDownloads() {
|
|||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
// divide by 1000 to convert to "thousands" format,
|
||||
// multiply by 10, floor the result, then divide by 10 to keep one decimal place.
|
||||
// if number is 1 million or more, format as millions
|
||||
if (num >= 1000000) {
|
||||
const truncated = Math.floor((num / 1000000) * 100) / 100
|
||||
return truncated.toFixed(2) + "M"
|
||||
}
|
||||
|
||||
// otherwise, format as thousands
|
||||
const truncated = Math.floor((num / 1000) * 10) / 10
|
||||
// ensure one decimal is always shown and append "k"
|
||||
return truncated.toFixed(1) + "k"
|
||||
|
||||
// examples:
|
||||
// console.log(formatNumber(1033400)) -> "1.03M"
|
||||
// console.log(formatNumber(2500000)) -> "2.50M"
|
||||
// console.log(formatNumber(337231)) -> "337.2k"
|
||||
// console.log(formatNumber(23233)) -> "23.2k"
|
||||
// console.log(formatNumber(2322)) -> "2.3k"
|
||||
|
|
|
|||