mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Merge remote-tracking branch 'origin/main' into bb/christmas
This commit is contained in:
commit
b0a8700305
366 changed files with 26451 additions and 2768 deletions
|
|
@ -7,5 +7,5 @@
|
|||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
"ignore": ["@roo-code/cli"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,14 +76,18 @@ src/node_modules
|
|||
!pnpm-workspace.yaml
|
||||
!scripts/bootstrap.mjs
|
||||
!apps/web-evals/
|
||||
!apps/cli/
|
||||
!src/
|
||||
!webview-ui/
|
||||
!packages/evals/.docker/entrypoints/runner.sh
|
||||
!packages/build/
|
||||
!packages/config-eslint/
|
||||
!packages/config-typescript/
|
||||
!packages/core/
|
||||
!packages/evals/
|
||||
!packages/ipc/
|
||||
!packages/telemetry/
|
||||
!packages/types/
|
||||
!packages/vscode-shim/
|
||||
!packages/cloud/
|
||||
!locales/
|
||||
|
|
|
|||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -84,7 +84,6 @@ body:
|
|||
- Google Gemini
|
||||
- Google Vertex AI
|
||||
- Groq
|
||||
- Human Relay Provider
|
||||
- LiteLLM
|
||||
- LM Studio
|
||||
- Mistral AI
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
description: "Create a new release of the Roo Code extension"
|
||||
argument-hint: patch | minor | major
|
||||
mode: code
|
||||
---
|
||||
|
||||
1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt`
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
| Auto-approve | 自动批准 | 始终批准 | 权限相关术语 |
|
||||
| Checkpoint | 存档点 | 检查点/快照 | 技术概念统一 |
|
||||
| MCP Server | MCP 服务 | MCP 服务器 | 技术组件 |
|
||||
| Human Relay | 人工辅助模式 | 人工中继 | 功能描述清晰 |
|
||||
| Network Timeout | 请求超时 | 网络超时 | 更准确描述 |
|
||||
| Terminal | 终端 | 命令行 | 技术术语统一 |
|
||||
| diff | 差异更新 | 差分/补丁 | 代码变更 |
|
||||
|
|
|
|||
188
.roo/skills/evals-context/SKILL.md
Normal file
188
.roo/skills/evals-context/SKILL.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
---
|
||||
name: evals-context
|
||||
description: Provides context about the Roo Code evals system structure in this monorepo. Use when tasks mention "evals", "evaluation", "eval runs", "eval exercises", or working with the evals infrastructure. Helps distinguish between the evals execution system (packages/evals, apps/web-evals) and the public website evals display page (apps/web-roo-code/src/app/evals).
|
||||
---
|
||||
|
||||
# Evals Codebase Context
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when the task involves:
|
||||
|
||||
- Modifying or debugging the evals execution infrastructure
|
||||
- Adding new eval exercises or languages
|
||||
- Working with the evals web interface (apps/web-evals)
|
||||
- Modifying the public evals display page on roocode.com
|
||||
- Understanding where evals code lives in this monorepo
|
||||
|
||||
## When NOT to Use This Skill
|
||||
|
||||
Do NOT use this skill when:
|
||||
|
||||
- Working on unrelated parts of the codebase (extension, webview-ui, etc.)
|
||||
- The task is purely about the VS Code extension's core functionality
|
||||
- Working on the main website pages that don't involve evals
|
||||
|
||||
## Key Disambiguation: Two "Evals" Locations
|
||||
|
||||
This monorepo has **two distinct evals-related locations** that can cause confusion:
|
||||
|
||||
| Component | Path | Purpose |
|
||||
| --------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| **Evals Execution System** | `packages/evals/` | Core eval infrastructure: CLI, DB schema, Docker configs |
|
||||
| **Evals Management UI** | `apps/web-evals/` | Next.js app for creating/monitoring eval runs (localhost:3446) |
|
||||
| **Website Evals Page** | `apps/web-roo-code/src/app/evals/` | Public roocode.com page displaying eval results |
|
||||
| **External Exercises Repo** | [Roo-Code-Evals](https://github.com/RooCodeInc/Roo-Code-Evals) | Actual coding exercises (NOT in this monorepo) |
|
||||
|
||||
## Directory Structure Reference
|
||||
|
||||
### `packages/evals/` - Core Evals Package
|
||||
|
||||
```
|
||||
packages/evals/
|
||||
├── ARCHITECTURE.md # Detailed architecture documentation
|
||||
├── ADDING-EVALS.md # Guide for adding new exercises/languages
|
||||
├── README.md # Setup and running instructions
|
||||
├── docker-compose.yml # Container orchestration
|
||||
├── Dockerfile.runner # Runner container definition
|
||||
├── Dockerfile.web # Web app container
|
||||
├── drizzle.config.ts # Database ORM config
|
||||
├── src/
|
||||
│ ├── index.ts # Package exports
|
||||
│ ├── cli/ # CLI commands for running evals
|
||||
│ │ ├── runEvals.ts # Orchestrates complete eval runs
|
||||
│ │ ├── runTask.ts # Executes individual tasks in containers
|
||||
│ │ ├── runUnitTest.ts # Validates task completion via tests
|
||||
│ │ └── redis.ts # Redis pub/sub integration
|
||||
│ ├── db/
|
||||
│ │ ├── schema.ts # Database schema (runs, tasks)
|
||||
│ │ ├── queries/ # Database query functions
|
||||
│ │ └── migrations/ # SQL migrations
|
||||
│ └── exercises/
|
||||
│ └── index.ts # Exercise loading utilities
|
||||
└── scripts/
|
||||
└── setup.sh # Local macOS setup script
|
||||
```
|
||||
|
||||
### `apps/web-evals/` - Evals Management Web App
|
||||
|
||||
```
|
||||
apps/web-evals/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── page.tsx # Home page (runs list)
|
||||
│ │ ├── runs/
|
||||
│ │ │ ├── new/ # Create new eval run
|
||||
│ │ │ └── [id]/ # View specific run status
|
||||
│ │ └── api/runs/ # SSE streaming endpoint
|
||||
│ ├── actions/ # Server actions
|
||||
│ │ ├── runs.ts # Run CRUD operations
|
||||
│ │ ├── tasks.ts # Task queries
|
||||
│ │ ├── exercises.ts # Exercise listing
|
||||
│ │ └── heartbeat.ts # Controller health checks
|
||||
│ ├── hooks/ # React hooks (SSE, models, etc.)
|
||||
│ └── lib/ # Utilities and schemas
|
||||
```
|
||||
|
||||
### `apps/web-roo-code/src/app/evals/` - Public Website Evals Page
|
||||
|
||||
```
|
||||
apps/web-roo-code/src/app/evals/
|
||||
├── page.tsx # Fetches and displays public eval results
|
||||
├── evals.tsx # Main evals display component
|
||||
├── plot.tsx # Visualization component
|
||||
└── types.ts # EvalRun type (extends packages/evals types)
|
||||
```
|
||||
|
||||
This page **displays** eval results on the public roocode.com website. It imports types from `@roo-code/evals` but does NOT run evals.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The evals system is a distributed evaluation platform that runs AI coding tasks in isolated VS Code environments:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Web App (apps/web-evals) ──────────────────────────────── │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ PostgreSQL ◄────► Controller Container │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ Redis ◄───► Runner Containers (1-25 parallel) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key components:**
|
||||
|
||||
- **Controller**: Orchestrates eval runs, spawns runners, manages task queue (p-queue)
|
||||
- **Runner**: Isolated Docker container with VS Code + Roo Code extension + language runtimes
|
||||
- **Redis**: Pub/sub for real-time events (NOT task queuing)
|
||||
- **PostgreSQL**: Stores runs, tasks, metrics
|
||||
|
||||
## Common Tasks Quick Reference
|
||||
|
||||
### Adding a New Eval Exercise
|
||||
|
||||
1. Add exercise to [Roo-Code-Evals](https://github.com/RooCodeInc/Roo-Code-Evals) repo (external)
|
||||
2. See [`packages/evals/ADDING-EVALS.md`](packages/evals/ADDING-EVALS.md) for structure
|
||||
|
||||
### Modifying Eval CLI Behavior
|
||||
|
||||
Edit files in [`packages/evals/src/cli/`](packages/evals/src/cli/):
|
||||
|
||||
- [`runEvals.ts`](packages/evals/src/cli/runEvals.ts) - Run orchestration
|
||||
- [`runTask.ts`](packages/evals/src/cli/runTask.ts) - Task execution
|
||||
- [`runUnitTest.ts`](packages/evals/src/cli/runUnitTest.ts) - Test validation
|
||||
|
||||
### Modifying the Evals Web Interface
|
||||
|
||||
Edit files in [`apps/web-evals/src/`](apps/web-evals/src/):
|
||||
|
||||
- [`app/runs/new/new-run.tsx`](apps/web-evals/src/app/runs/new/new-run.tsx) - New run form
|
||||
- [`actions/runs.ts`](apps/web-evals/src/actions/runs.ts) - Run server actions
|
||||
|
||||
### Modifying the Public Evals Display Page
|
||||
|
||||
Edit files in [`apps/web-roo-code/src/app/evals/`](apps/web-roo-code/src/app/evals/):
|
||||
|
||||
- [`evals.tsx`](apps/web-roo-code/src/app/evals/evals.tsx) - Display component
|
||||
- [`plot.tsx`](apps/web-roo-code/src/app/evals/plot.tsx) - Charts
|
||||
|
||||
### Database Schema Changes
|
||||
|
||||
1. Edit [`packages/evals/src/db/schema.ts`](packages/evals/src/db/schema.ts)
|
||||
2. Generate migration: `cd packages/evals && pnpm drizzle-kit generate`
|
||||
3. Apply migration: `pnpm drizzle-kit migrate`
|
||||
|
||||
## Running Evals Locally
|
||||
|
||||
```bash
|
||||
# From repo root
|
||||
pnpm evals
|
||||
|
||||
# Opens web UI at http://localhost:3446
|
||||
```
|
||||
|
||||
**Ports (defaults):**
|
||||
|
||||
- PostgreSQL: 5433
|
||||
- Redis: 6380
|
||||
- Web: 3446
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# packages/evals tests
|
||||
cd packages/evals && npx vitest run
|
||||
|
||||
# apps/web-evals tests
|
||||
cd apps/web-evals && npx vitest run
|
||||
```
|
||||
|
||||
## Key Types/Exports from `@roo-code/evals`
|
||||
|
||||
The package exports are defined in [`packages/evals/src/index.ts`](packages/evals/src/index.ts):
|
||||
|
||||
- Database queries: `getRuns`, `getTasks`, `getTaskMetrics`, etc.
|
||||
- Schema types: `Run`, `Task`, `TaskMetrics`
|
||||
- Used by both `apps/web-evals` and `apps/web-roo-code`
|
||||
83
CHANGELOG.md
83
CHANGELOG.md
|
|
@ -1,5 +1,88 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.39.0] - 2026-01-08
|
||||
|
||||

|
||||
|
||||
- Implement sticky provider profile for task-level API config persistence (#8010 by @hannesrudolph, PR #10018 by @hannesrudolph)
|
||||
- Add support for image file @mentions (PR #10189 by @hannesrudolph)
|
||||
- Rename YOLO to BRRR (#8574 by @mojomast, PR #10507 by @roomote)
|
||||
- Add debug-mode proxy routing for debugging API calls (#7042 by @SleeperSmith, PR #10467 by @hannesrudolph)
|
||||
- Add Kimi K2 thinking model to Fireworks AI provider (#9201 by @kavehsfv, PR #9202 by @roomote)
|
||||
- Add xhigh reasoning effort to OpenAI compatible endpoints (#10060 by @Soorma718, PR #10061 by @roomote)
|
||||
- Filter @ mention file search results using .rooignore (#10169 by @jerrill-johnson-bitwerx, PR #10174 by @roomote)
|
||||
- Add image support documentation to read_file native tool description (#10440 by @nabilfreeman, PR #10442 by @roomote)
|
||||
- Add zai-glm-4.7 to Cerebras models (PR #10500 by @sebastiand-cerebras)
|
||||
- VSCode shim and basic CLI for running Roo Code headlessly (PR #10452 by @cte)
|
||||
- Add CLI installer for headless Roo Code (PR #10474 by @cte)
|
||||
- Add option to use CLI for evals (PR #10456 by @cte)
|
||||
- Remember last Roo model selection in web-evals and add evals skill (PR #10470 by @hannesrudolph)
|
||||
- Tweak the style of follow up suggestion modes (PR #9260 by @mrubens)
|
||||
- Fix: Handle PowerShell ENOENT error in os-name on Windows (#9859 by @Yang-strive, PR #9897 by @roomote)
|
||||
- Fix: Make command chaining examples shell-aware for Windows compatibility (#10352 by @AlexNek, PR #10434 by @roomote)
|
||||
- Fix: Preserve tool_use blocks for all tool_results in kept messages during condensation (PR #10471 by @daniel-lxs)
|
||||
- Fix: Add additionalProperties: false to MCP tool schemas for OpenAI Responses API (PR #10472 by @daniel-lxs)
|
||||
- Fix: Prevent duplicate tool_result blocks causing API errors (PR #10497 by @daniel-lxs)
|
||||
- Fix: Add explicit deduplication for duplicate tool_result blocks (#10465 by @nabilfreeman, PR #10466 by @roomote)
|
||||
- Fix: Use task stored API config as fallback for rate limit (PR #10266 by @roomote)
|
||||
- Fix: Remove legacy Claude 2 series models from Bedrock provider (#9220 by @KevinZhao, PR #10501 by @roomote)
|
||||
- Fix: Add missing description fields for debugProxy configuration (PR #10505 by @roomote)
|
||||
- Fix: Glitchy kangaroo bounce animation on welcome screen (PR #10035 by @objectiveSee)
|
||||
|
||||
## [3.38.3] - 2026-01-03
|
||||
|
||||
- Feat: Add option in Context settings to recursively load `.roo/rules` and `AGENTS.md` from subdirectories (PR #10446 by @mrubens)
|
||||
- Fix: Stop frequent Claude Code sign-ins by hardening OAuth refresh token handling (PR #10410 by @hannesrudolph)
|
||||
- Fix: Add `maxConcurrentFileReads` limit to native `read_file` tool schema (PR #10449 by @app/roomote)
|
||||
- Fix: Add type check for `lastMessage.text` in TTS useEffect to prevent runtime errors (PR #10431 by @app/roomote)
|
||||
|
||||
## [3.38.2] - 2025-12-31
|
||||
|
||||

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

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

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

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

|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](locales/zh-CN/README.md)
|
||||
- [繁體中文](locales/zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ Learn more: [Using Modes](https://docs.roocode.com/basic-usage/using-modes) •
|
|||
|
||||
<div align="center">
|
||||
|
||||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installing Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configuring Profiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase Indexing</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Custom Modes</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Todo Lists</b> |
|
||||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installing Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configuring Profiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase Indexing</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Custom Modes</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Context Management</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
231
apps/cli/README.md
Normal file
231
apps/cli/README.md
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
# @roo-code/cli
|
||||
|
||||
Command Line Interface for Roo Code - Run the Roo Code agent from the terminal without VSCode.
|
||||
|
||||
## Overview
|
||||
|
||||
This CLI uses the `@roo-code/vscode-shim` package to provide a VSCode API compatibility layer, allowing the main Roo Code extension to run in a Node.js environment.
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Install (Recommended)
|
||||
|
||||
Install the Roo Code CLI with a single command:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Node.js 20 or higher
|
||||
- macOS (Intel or Apple Silicon) or Linux (x64 or ARM64)
|
||||
|
||||
**Custom installation directory:**
|
||||
|
||||
```bash
|
||||
ROO_INSTALL_DIR=/opt/roo-code ROO_BIN_DIR=/usr/local/bin curl -fsSL ... | sh
|
||||
```
|
||||
|
||||
**Install a specific version:**
|
||||
|
||||
```bash
|
||||
ROO_VERSION=0.1.0 curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
Re-run the install script to update to the latest version:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
```
|
||||
|
||||
### Uninstalling
|
||||
|
||||
```bash
|
||||
rm -rf ~/.roo/cli ~/.local/bin/roo
|
||||
```
|
||||
|
||||
### Development Installation
|
||||
|
||||
For contributing or development:
|
||||
|
||||
```bash
|
||||
# From the monorepo root.
|
||||
pnpm install
|
||||
|
||||
# Build the main extension first.
|
||||
pnpm --filter roo-cline bundle
|
||||
|
||||
# Build the cli.
|
||||
pnpm --filter @roo-code/cli build
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode (Default)
|
||||
|
||||
By default, the CLI prompts for approval before executing actions:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
||||
roo "What is this project?" --workspace ~/Documents/my-project
|
||||
```
|
||||
|
||||
In interactive mode:
|
||||
|
||||
- Tool executions prompt for yes/no approval
|
||||
- Commands prompt for yes/no approval
|
||||
- Followup questions show suggestions and wait for user input
|
||||
- Browser and MCP actions prompt for approval
|
||||
|
||||
### Non-Interactive Mode (`-y`)
|
||||
|
||||
For automation and scripts, use `-y` to auto-approve all actions:
|
||||
|
||||
```bash
|
||||
roo -y "Refactor the utils.ts file" --workspace ~/Documents/my-project
|
||||
```
|
||||
|
||||
In non-interactive mode:
|
||||
|
||||
- Tool, command, browser, and MCP actions are auto-approved
|
||||
- Followup questions show a 10-second timeout, then auto-select the first suggestion
|
||||
- Typing any key cancels the timeout and allows manual input
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------ | ----------------- |
|
||||
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
|
||||
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
|
||||
| `-v, --verbose` | Enable verbose output (show VSCode and extension logs) | `false` |
|
||||
| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
|
||||
| `-x, --exit-on-complete` | Exit the process when task completes (useful for testing) | `false` |
|
||||
| `-y, --yes` | Non-interactive mode: auto-approve all actions | `false` |
|
||||
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
|
||||
| `-p, --provider <provider>` | API provider (anthropic, openai, openrouter, etc.) | `openrouter` |
|
||||
| `-m, --model <model>` | Model to use | Provider default |
|
||||
| `-M, --mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
|
||||
| `-r, --reasoning-effort <effort>` | Reasoning effort level (none, minimal, low, medium, high, xhigh) | `medium` |
|
||||
|
||||
By default, the CLI runs in quiet mode (suppressing VSCode/extension logs) and only shows assistant output. Use `-v` to see all logs, or `-d` for detailed debug information.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The CLI will look for API keys in environment variables if not provided via `--api-key`:
|
||||
|
||||
| Provider | Environment Variable |
|
||||
| ------------- | -------------------- |
|
||||
| anthropic | `ANTHROPIC_API_KEY` |
|
||||
| openai | `OPENAI_API_KEY` |
|
||||
| openrouter | `OPENROUTER_API_KEY` |
|
||||
| google/gemini | `GOOGLE_API_KEY` |
|
||||
| mistral | `MISTRAL_API_KEY` |
|
||||
| deepseek | `DEEPSEEK_API_KEY` |
|
||||
| bedrock | `AWS_ACCESS_KEY_ID` |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ CLI Entry │
|
||||
│ (index.ts) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ ExtensionHost │
|
||||
│ (extension- │
|
||||
│ host.ts) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────┐ ┌──────────┐
|
||||
│vscode │ │Extension │
|
||||
│-shim │ │ Bundle │
|
||||
└───────┘ └──────────┘
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **CLI Entry Point** (`index.ts`): Parses command line arguments and initializes the ExtensionHost
|
||||
|
||||
2. **ExtensionHost** (`extension-host.ts`):
|
||||
|
||||
- Creates a VSCode API mock using `@roo-code/vscode-shim`
|
||||
- Intercepts `require('vscode')` to return the mock
|
||||
- Loads and activates the extension bundle
|
||||
- Manages bidirectional message flow
|
||||
|
||||
3. **Message Flow**:
|
||||
- CLI → Extension: `emit("webviewMessage", {...})`
|
||||
- Extension → CLI: `emit("extensionWebviewMessage", {...})`
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- **No TUI**: Output is plain text (no React/Ink UI yet)
|
||||
- **No configuration file**: Settings are passed via command line flags
|
||||
- **No persistence**: Each run is a fresh session
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Watch mode for development
|
||||
pnpm dev
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Type checking
|
||||
pnpm check-types
|
||||
|
||||
# Linting
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
To create a new release, run the release script from the monorepo root:
|
||||
|
||||
```bash
|
||||
# Release using version from package.json
|
||||
./apps/cli/scripts/release.sh
|
||||
|
||||
# Release with a specific version
|
||||
./apps/cli/scripts/release.sh 0.1.0
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
1. Build the extension and CLI
|
||||
2. Create a platform-specific tarball (for your current OS/architecture)
|
||||
3. Create a GitHub release with the tarball attached
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- GitHub CLI (`gh`) installed and authenticated (`gh auth login`)
|
||||
- pnpm installed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Extension bundle not found
|
||||
|
||||
Make sure you've built the main extension first:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
pnpm bundle
|
||||
```
|
||||
|
||||
### Module resolution errors
|
||||
|
||||
The CLI expects the extension to be a CommonJS bundle. Make sure the extension's esbuild config outputs CommonJS.
|
||||
|
||||
### "vscode" module not found
|
||||
|
||||
The CLI intercepts `require('vscode')` calls. If you see this error, the module resolution interception may have failed.
|
||||
4
apps/cli/eslint.config.mjs
Normal file
4
apps/cli/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
287
apps/cli/install.sh
Executable file
287
apps/cli/install.sh
Executable file
|
|
@ -0,0 +1,287 @@
|
|||
#!/bin/sh
|
||||
# Roo Code CLI Installer
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
#
|
||||
# Environment variables:
|
||||
# ROO_INSTALL_DIR - Installation directory (default: ~/.roo/cli)
|
||||
# ROO_BIN_DIR - Binary symlink directory (default: ~/.local/bin)
|
||||
# ROO_VERSION - Specific version to install (default: latest)
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
INSTALL_DIR="${ROO_INSTALL_DIR:-$HOME/.roo/cli}"
|
||||
BIN_DIR="${ROO_BIN_DIR:-$HOME/.local/bin}"
|
||||
REPO="RooCodeInc/Roo-Code"
|
||||
MIN_NODE_VERSION=20
|
||||
|
||||
# Color output (only if terminal supports it)
|
||||
if [ -t 1 ]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
BOLD=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
info() { printf "${GREEN}==>${NC} %s\n" "$1"; }
|
||||
warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1"; }
|
||||
error() { printf "${RED}Error:${NC} %s\n" "$1" >&2; exit 1; }
|
||||
|
||||
# Check Node.js version
|
||||
check_node() {
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
error "Node.js is not installed. Please install Node.js $MIN_NODE_VERSION or higher.
|
||||
|
||||
Install Node.js:
|
||||
- macOS: brew install node
|
||||
- Linux: https://nodejs.org/en/download/package-manager
|
||||
- Or use a version manager like fnm, nvm, or mise"
|
||||
fi
|
||||
|
||||
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$NODE_VERSION" -lt "$MIN_NODE_VERSION" ]; then
|
||||
error "Node.js $MIN_NODE_VERSION+ required. Found: $(node -v)
|
||||
|
||||
Please upgrade Node.js to version $MIN_NODE_VERSION or higher."
|
||||
fi
|
||||
|
||||
info "Found Node.js $(node -v)"
|
||||
}
|
||||
|
||||
# Detect OS and architecture
|
||||
detect_platform() {
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS" in
|
||||
darwin) OS="darwin" ;;
|
||||
linux) OS="linux" ;;
|
||||
mingw*|msys*|cygwin*)
|
||||
error "Windows is not supported by this installer. Please use WSL or install manually."
|
||||
;;
|
||||
*) error "Unsupported OS: $OS" ;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH="x64" ;;
|
||||
arm64|aarch64) ARCH="arm64" ;;
|
||||
*) error "Unsupported architecture: $ARCH" ;;
|
||||
esac
|
||||
|
||||
PLATFORM="${OS}-${ARCH}"
|
||||
info "Detected platform: $PLATFORM"
|
||||
}
|
||||
|
||||
# Get latest release version or use specified version
|
||||
get_version() {
|
||||
if [ -n "$ROO_VERSION" ]; then
|
||||
VERSION="$ROO_VERSION"
|
||||
info "Using specified version: $VERSION"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Fetching latest version..."
|
||||
|
||||
# Try to get the latest cli release
|
||||
RELEASES_JSON=$(curl -fsSL "https://api.github.com/repos/$REPO/releases" 2>/dev/null) || {
|
||||
error "Failed to fetch releases from GitHub. Check your internet connection."
|
||||
}
|
||||
|
||||
# Extract the latest cli-v* tag
|
||||
VERSION=$(echo "$RELEASES_JSON" |
|
||||
grep -o '"tag_name": "cli-v[^"]*"' |
|
||||
head -1 |
|
||||
sed 's/"tag_name": "cli-v//' |
|
||||
sed 's/"//')
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
error "Could not find any CLI releases. The CLI may not have been released yet."
|
||||
fi
|
||||
|
||||
info "Latest version: $VERSION"
|
||||
}
|
||||
|
||||
# Download and extract
|
||||
download_and_install() {
|
||||
TARBALL="roo-cli-${PLATFORM}.tar.gz"
|
||||
URL="https://github.com/$REPO/releases/download/cli-v${VERSION}/${TARBALL}"
|
||||
|
||||
info "Downloading from $URL..."
|
||||
|
||||
# Create temp directory
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TMP_DIR" EXIT
|
||||
|
||||
# Download with progress indicator
|
||||
HTTP_CODE=$(curl -fsSL -w "%{http_code}" "$URL" -o "$TMP_DIR/$TARBALL" 2>/dev/null) || {
|
||||
if [ "$HTTP_CODE" = "404" ]; then
|
||||
error "Release not found for platform $PLATFORM version $VERSION.
|
||||
|
||||
Available at: https://github.com/$REPO/releases"
|
||||
fi
|
||||
error "Download failed. HTTP code: $HTTP_CODE"
|
||||
}
|
||||
|
||||
# Verify we got something
|
||||
if [ ! -s "$TMP_DIR/$TARBALL" ]; then
|
||||
error "Downloaded file is empty. Please try again."
|
||||
fi
|
||||
|
||||
# Remove old installation if exists
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
info "Removing previous installation..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Extract
|
||||
info "Extracting to $INSTALL_DIR..."
|
||||
tar -xzf "$TMP_DIR/$TARBALL" -C "$INSTALL_DIR" --strip-components=1 || {
|
||||
error "Failed to extract tarball. The download may be corrupted."
|
||||
}
|
||||
|
||||
# Save ripgrep binary before npm install (npm install will overwrite node_modules)
|
||||
RIPGREP_BIN=""
|
||||
if [ -f "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg" ]; then
|
||||
RIPGREP_BIN="$TMP_DIR/rg"
|
||||
cp "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg" "$RIPGREP_BIN"
|
||||
fi
|
||||
|
||||
# Install npm dependencies
|
||||
info "Installing dependencies..."
|
||||
cd "$INSTALL_DIR"
|
||||
npm install --production --silent 2>/dev/null || {
|
||||
warn "npm install failed, trying with --legacy-peer-deps..."
|
||||
npm install --production --legacy-peer-deps --silent 2>/dev/null || {
|
||||
error "Failed to install dependencies. Make sure npm is available."
|
||||
}
|
||||
}
|
||||
cd - > /dev/null
|
||||
|
||||
# Restore ripgrep binary after npm install
|
||||
if [ -n "$RIPGREP_BIN" ] && [ -f "$RIPGREP_BIN" ]; then
|
||||
mkdir -p "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin"
|
||||
cp "$RIPGREP_BIN" "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg"
|
||||
chmod +x "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg"
|
||||
fi
|
||||
|
||||
# Make executable
|
||||
chmod +x "$INSTALL_DIR/bin/roo"
|
||||
|
||||
# Also make ripgrep executable if it exists
|
||||
if [ -f "$INSTALL_DIR/bin/rg" ]; then
|
||||
chmod +x "$INSTALL_DIR/bin/rg"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create symlink in bin directory
|
||||
setup_bin() {
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
# Remove old symlink if exists
|
||||
if [ -L "$BIN_DIR/roo" ] || [ -f "$BIN_DIR/roo" ]; then
|
||||
rm -f "$BIN_DIR/roo"
|
||||
fi
|
||||
|
||||
ln -sf "$INSTALL_DIR/bin/roo" "$BIN_DIR/roo"
|
||||
info "Created symlink: $BIN_DIR/roo"
|
||||
}
|
||||
|
||||
# Check if bin dir is in PATH and provide instructions
|
||||
check_path() {
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*)
|
||||
# Already in PATH
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
warn "$BIN_DIR is not in your PATH"
|
||||
echo ""
|
||||
echo "Add this line to your shell profile:"
|
||||
echo ""
|
||||
|
||||
# Detect shell and provide specific instructions
|
||||
SHELL_NAME=$(basename "$SHELL")
|
||||
case "$SHELL_NAME" in
|
||||
zsh)
|
||||
echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.zshrc"
|
||||
echo " source ~/.zshrc"
|
||||
;;
|
||||
bash)
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.bashrc"
|
||||
echo " source ~/.bashrc"
|
||||
else
|
||||
echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.bash_profile"
|
||||
echo " source ~/.bash_profile"
|
||||
fi
|
||||
;;
|
||||
fish)
|
||||
echo " set -Ux fish_user_paths $BIN_DIR \$fish_user_paths"
|
||||
;;
|
||||
*)
|
||||
echo " export PATH=\"$BIN_DIR:\$PATH\""
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
verify_install() {
|
||||
if [ -x "$BIN_DIR/roo" ]; then
|
||||
info "Verifying installation..."
|
||||
# Just check if it runs without error
|
||||
"$BIN_DIR/roo" --version >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Print success message
|
||||
print_success() {
|
||||
echo ""
|
||||
printf "${GREEN}${BOLD}✓ Roo Code CLI installed successfully!${NC}\n"
|
||||
echo ""
|
||||
echo " Installation: $INSTALL_DIR"
|
||||
echo " Binary: $BIN_DIR/roo"
|
||||
echo " Version: $VERSION"
|
||||
echo ""
|
||||
echo " ${BOLD}Get started:${NC}"
|
||||
echo " roo --help"
|
||||
echo ""
|
||||
echo " ${BOLD}Example:${NC}"
|
||||
echo " export OPENROUTER_API_KEY=sk-or-v1-..."
|
||||
echo " roo \"What is this project?\" --workspace ~/my-project"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
echo ""
|
||||
printf "${BLUE}${BOLD}"
|
||||
echo " ╭─────────────────────────────────╮"
|
||||
echo " │ Roo Code CLI Installer │"
|
||||
echo " ╰─────────────────────────────────╯"
|
||||
printf "${NC}"
|
||||
echo ""
|
||||
|
||||
check_node
|
||||
detect_platform
|
||||
get_version
|
||||
download_and_install
|
||||
setup_bin
|
||||
check_path
|
||||
verify_install
|
||||
print_success
|
||||
}
|
||||
|
||||
main "$@"
|
||||
35
apps/cli/package.json
Normal file
35
apps/cli/package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@roo-code/cli",
|
||||
"version": "0.1.0",
|
||||
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"roo": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write 'src/**/*.ts'",
|
||||
"lint": "eslint src --ext .ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"build": "tsup",
|
||||
"start": "node dist/index.js",
|
||||
"clean": "rimraf dist .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@roo-code/types": "workspace:^",
|
||||
"@roo-code/vscode-shim": "workspace:^",
|
||||
"@vscode/ripgrep": "^1.15.9",
|
||||
"commander": "^12.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@types/node": "^24.1.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.8.3",
|
||||
"vitest": "^3.2.3"
|
||||
}
|
||||
}
|
||||
363
apps/cli/scripts/release.sh
Executable file
363
apps/cli/scripts/release.sh
Executable file
|
|
@ -0,0 +1,363 @@
|
|||
#!/bin/bash
|
||||
# Roo Code CLI Release Script
|
||||
#
|
||||
# Usage:
|
||||
# ./apps/cli/scripts/release.sh [version]
|
||||
#
|
||||
# Examples:
|
||||
# ./apps/cli/scripts/release.sh # Use version from package.json
|
||||
# ./apps/cli/scripts/release.sh 0.1.0 # Specify version
|
||||
#
|
||||
# This script:
|
||||
# 1. Builds the extension and CLI
|
||||
# 2. Creates a tarball for the current platform
|
||||
# 3. Creates a GitHub release and uploads the tarball
|
||||
#
|
||||
# Prerequisites:
|
||||
# - GitHub CLI (gh) installed and authenticated
|
||||
# - pnpm installed
|
||||
# - Run from the monorepo root directory
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { printf "${GREEN}==>${NC} %s\n" "$1"; }
|
||||
warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1"; }
|
||||
error() { printf "${RED}Error:${NC} %s\n" "$1" >&2; exit 1; }
|
||||
step() { printf "${BLUE}${BOLD}[%s]${NC} %s\n" "$1" "$2"; }
|
||||
|
||||
# Get script directory and repo root
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
CLI_DIR="$REPO_ROOT/apps/cli"
|
||||
|
||||
# Detect current platform
|
||||
detect_platform() {
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS" in
|
||||
darwin) OS="darwin" ;;
|
||||
linux) OS="linux" ;;
|
||||
*) error "Unsupported OS: $OS" ;;
|
||||
esac
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH="x64" ;;
|
||||
arm64|aarch64) ARCH="arm64" ;;
|
||||
*) error "Unsupported architecture: $ARCH" ;;
|
||||
esac
|
||||
|
||||
PLATFORM="${OS}-${ARCH}"
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
step "1/7" "Checking prerequisites..."
|
||||
|
||||
if ! command -v gh &> /dev/null; then
|
||||
error "GitHub CLI (gh) is not installed. Install it with: brew install gh"
|
||||
fi
|
||||
|
||||
if ! gh auth status &> /dev/null; then
|
||||
error "GitHub CLI is not authenticated. Run: gh auth login"
|
||||
fi
|
||||
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
error "pnpm is not installed."
|
||||
fi
|
||||
|
||||
if ! command -v node &> /dev/null; then
|
||||
error "Node.js is not installed."
|
||||
fi
|
||||
|
||||
info "Prerequisites OK"
|
||||
}
|
||||
|
||||
# Get version
|
||||
get_version() {
|
||||
if [ -n "$1" ]; then
|
||||
VERSION="$1"
|
||||
else
|
||||
VERSION=$(node -p "require('$CLI_DIR/package.json').version")
|
||||
fi
|
||||
|
||||
# Validate semver format
|
||||
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
|
||||
error "Invalid version format: $VERSION (expected semver like 0.1.0)"
|
||||
fi
|
||||
|
||||
TAG="cli-v$VERSION"
|
||||
info "Version: $VERSION (tag: $TAG)"
|
||||
}
|
||||
|
||||
# Build everything
|
||||
build() {
|
||||
step "2/7" "Building extension bundle..."
|
||||
cd "$REPO_ROOT"
|
||||
pnpm bundle
|
||||
|
||||
step "3/7" "Building CLI..."
|
||||
pnpm --filter @roo-code/cli build
|
||||
|
||||
info "Build complete"
|
||||
}
|
||||
|
||||
# Create release tarball
|
||||
create_tarball() {
|
||||
step "4/7" "Creating release tarball for $PLATFORM..."
|
||||
|
||||
RELEASE_DIR="$REPO_ROOT/roo-cli-${PLATFORM}"
|
||||
TARBALL="roo-cli-${PLATFORM}.tar.gz"
|
||||
|
||||
# Clean up any previous build
|
||||
rm -rf "$RELEASE_DIR"
|
||||
rm -f "$REPO_ROOT/$TARBALL"
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$RELEASE_DIR/bin"
|
||||
mkdir -p "$RELEASE_DIR/lib"
|
||||
mkdir -p "$RELEASE_DIR/extension"
|
||||
|
||||
# Copy CLI dist files
|
||||
info "Copying CLI files..."
|
||||
cp -r "$CLI_DIR/dist/"* "$RELEASE_DIR/lib/"
|
||||
|
||||
# Create package.json for npm install (only runtime dependencies)
|
||||
info "Creating package.json..."
|
||||
node -e "
|
||||
const pkg = require('$CLI_DIR/package.json');
|
||||
const newPkg = {
|
||||
name: '@roo-code/cli',
|
||||
version: pkg.version,
|
||||
type: 'module',
|
||||
dependencies: {
|
||||
commander: pkg.dependencies.commander
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(newPkg, null, 2));
|
||||
" > "$RELEASE_DIR/package.json"
|
||||
|
||||
# Copy extension bundle
|
||||
info "Copying extension bundle..."
|
||||
cp -r "$REPO_ROOT/src/dist/"* "$RELEASE_DIR/extension/"
|
||||
|
||||
# Add package.json to extension directory to mark it as CommonJS
|
||||
# This is necessary because the main package.json has "type": "module"
|
||||
# but the extension bundle is CommonJS
|
||||
echo '{"type": "commonjs"}' > "$RELEASE_DIR/extension/package.json"
|
||||
|
||||
# Find and copy ripgrep binary
|
||||
# The extension looks for ripgrep at: appRoot/node_modules/@vscode/ripgrep/bin/rg
|
||||
# The CLI sets appRoot to the CLI package root, so we need to put ripgrep there
|
||||
info "Looking for ripgrep binary..."
|
||||
RIPGREP_PATH=$(find "$REPO_ROOT/node_modules" -path "*/@vscode/ripgrep/bin/rg" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$RIPGREP_PATH" ] && [ -f "$RIPGREP_PATH" ]; then
|
||||
info "Found ripgrep at: $RIPGREP_PATH"
|
||||
# Create the expected directory structure for the extension to find ripgrep
|
||||
mkdir -p "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin"
|
||||
cp "$RIPGREP_PATH" "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/"
|
||||
chmod +x "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/rg"
|
||||
# Also keep a copy in bin/ for direct access
|
||||
mkdir -p "$RELEASE_DIR/bin"
|
||||
cp "$RIPGREP_PATH" "$RELEASE_DIR/bin/"
|
||||
chmod +x "$RELEASE_DIR/bin/rg"
|
||||
else
|
||||
warn "ripgrep binary not found - users will need ripgrep installed"
|
||||
fi
|
||||
|
||||
# Create the wrapper script
|
||||
info "Creating wrapper script..."
|
||||
cat > "$RELEASE_DIR/bin/roo" << 'WRAPPER_EOF'
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Set environment variables for the CLI
|
||||
process.env.ROO_EXTENSION_PATH = join(__dirname, '..', 'extension');
|
||||
process.env.ROO_RIPGREP_PATH = join(__dirname, 'rg');
|
||||
|
||||
// Import and run the actual CLI
|
||||
await import(join(__dirname, '..', 'lib', 'index.js'));
|
||||
WRAPPER_EOF
|
||||
|
||||
chmod +x "$RELEASE_DIR/bin/roo"
|
||||
|
||||
# Create version file
|
||||
echo "$VERSION" > "$RELEASE_DIR/VERSION"
|
||||
|
||||
# Create tarball
|
||||
info "Creating tarball..."
|
||||
cd "$REPO_ROOT"
|
||||
tar -czvf "$TARBALL" "$(basename "$RELEASE_DIR")"
|
||||
|
||||
# Clean up release directory
|
||||
rm -rf "$RELEASE_DIR"
|
||||
|
||||
# Show size
|
||||
TARBALL_PATH="$REPO_ROOT/$TARBALL"
|
||||
TARBALL_SIZE=$(ls -lh "$TARBALL_PATH" | awk '{print $5}')
|
||||
info "Created: $TARBALL ($TARBALL_SIZE)"
|
||||
}
|
||||
|
||||
# Create checksum
|
||||
create_checksum() {
|
||||
step "5/7" "Creating checksum..."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if command -v sha256sum &> /dev/null; then
|
||||
sha256sum "$TARBALL" > "${TARBALL}.sha256"
|
||||
elif command -v shasum &> /dev/null; then
|
||||
shasum -a 256 "$TARBALL" > "${TARBALL}.sha256"
|
||||
else
|
||||
warn "No sha256sum or shasum found, skipping checksum"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Checksum: $(cat "${TARBALL}.sha256")"
|
||||
}
|
||||
|
||||
# Check if release already exists
|
||||
check_existing_release() {
|
||||
step "6/7" "Checking for existing release..."
|
||||
|
||||
if gh release view "$TAG" &> /dev/null; then
|
||||
warn "Release $TAG already exists"
|
||||
read -p "Do you want to delete it and create a new one? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
info "Deleting existing release..."
|
||||
gh release delete "$TAG" --yes
|
||||
# Also delete the tag if it exists
|
||||
git tag -d "$TAG" 2>/dev/null || true
|
||||
git push origin ":refs/tags/$TAG" 2>/dev/null || true
|
||||
else
|
||||
error "Aborted. Use a different version or delete the existing release manually."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Create GitHub release
|
||||
create_release() {
|
||||
step "7/7" "Creating GitHub release..."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
RELEASE_NOTES=$(cat << EOF
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
\`\`\`
|
||||
|
||||
Or install a specific version:
|
||||
\`\`\`bash
|
||||
ROO_VERSION=$VERSION curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
|
||||
\`\`\`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20 or higher
|
||||
- macOS (Intel or Apple Silicon) or Linux (x64 or ARM64)
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
# Set your API key
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
||||
# Run a task
|
||||
roo "What is this project?" --workspace ~/my-project
|
||||
|
||||
# See all options
|
||||
roo --help
|
||||
\`\`\`
|
||||
|
||||
## Platform Support
|
||||
|
||||
This release includes:
|
||||
- \`roo-cli-${PLATFORM}.tar.gz\` - Built on $(uname -s) $(uname -m)
|
||||
|
||||
> **Note:** Additional platforms will be added as needed. If you need a different platform, please open an issue.
|
||||
|
||||
## Checksum
|
||||
|
||||
\`\`\`
|
||||
$(cat "${TARBALL}.sha256" 2>/dev/null || echo "N/A")
|
||||
\`\`\`
|
||||
EOF
|
||||
)
|
||||
|
||||
# Get the current commit SHA for the release target
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
info "Creating release at commit: ${COMMIT_SHA:0:8}"
|
||||
|
||||
# Create release (gh will create the tag automatically)
|
||||
info "Creating release..."
|
||||
RELEASE_FILES="$TARBALL"
|
||||
if [ -f "${TARBALL}.sha256" ]; then
|
||||
RELEASE_FILES="$RELEASE_FILES ${TARBALL}.sha256"
|
||||
fi
|
||||
|
||||
gh release create "$TAG" \
|
||||
--title "Roo Code CLI v$VERSION" \
|
||||
--notes "$RELEASE_NOTES" \
|
||||
--prerelease \
|
||||
--target "$COMMIT_SHA" \
|
||||
$RELEASE_FILES
|
||||
|
||||
info "Release created!"
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
cleanup() {
|
||||
info "Cleaning up..."
|
||||
cd "$REPO_ROOT"
|
||||
rm -f "$TARBALL" "${TARBALL}.sha256"
|
||||
}
|
||||
|
||||
# Print summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
printf "${GREEN}${BOLD}✓ Release v$VERSION created successfully!${NC}\n"
|
||||
echo ""
|
||||
echo " Release URL: https://github.com/RooCodeInc/Roo-Code/releases/tag/$TAG"
|
||||
echo ""
|
||||
echo " Install with:"
|
||||
echo " curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
echo ""
|
||||
printf "${BLUE}${BOLD}"
|
||||
echo " ╭─────────────────────────────────╮"
|
||||
echo " │ Roo Code CLI Release Script │"
|
||||
echo " ╰─────────────────────────────────╯"
|
||||
printf "${NC}"
|
||||
echo ""
|
||||
|
||||
detect_platform
|
||||
check_prerequisites
|
||||
get_version "$1"
|
||||
build
|
||||
create_tarball
|
||||
create_checksum
|
||||
check_existing_release
|
||||
create_release
|
||||
cleanup
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
1164
apps/cli/src/__tests__/extension-host.test.ts
Normal file
1164
apps/cli/src/__tests__/extension-host.test.ts
Normal file
File diff suppressed because it is too large
Load diff
144
apps/cli/src/__tests__/integration.test.ts
Normal file
144
apps/cli/src/__tests__/integration.test.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Integration tests for CLI
|
||||
*
|
||||
* These tests require a valid OPENROUTER_API_KEY environment variable.
|
||||
* They will be skipped if the API key is not available.
|
||||
*
|
||||
* Run with: OPENROUTER_API_KEY=sk-or-v1-... pnpm test
|
||||
*/
|
||||
|
||||
import { ExtensionHost } from "../extension-host.js"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import os from "os"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY
|
||||
const hasApiKey = !!OPENROUTER_API_KEY
|
||||
|
||||
// Find the extension path - we need a built extension for integration tests
|
||||
function findExtensionPath(): string | null {
|
||||
// From apps/cli/src/__tests__, go up to monorepo root then to src/dist
|
||||
const monorepoPath = path.resolve(__dirname, "../../../../src/dist")
|
||||
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
|
||||
return monorepoPath
|
||||
}
|
||||
// Also try from the apps/cli level
|
||||
const altPath = path.resolve(__dirname, "../../../src/dist")
|
||||
if (fs.existsSync(path.join(altPath, "extension.js"))) {
|
||||
return altPath
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const extensionPath = findExtensionPath()
|
||||
const hasExtension = !!extensionPath
|
||||
|
||||
// Create a temporary workspace directory for tests
|
||||
function createTempWorkspace(): string {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "roo-cli-test-"))
|
||||
return tempDir
|
||||
}
|
||||
|
||||
// Clean up temporary workspace
|
||||
function cleanupWorkspace(workspacePath: string): void {
|
||||
try {
|
||||
fs.rmSync(workspacePath, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(!hasApiKey || !hasExtension)(
|
||||
"CLI Integration Tests (requires OPENROUTER_API_KEY and built extension)",
|
||||
() => {
|
||||
let workspacePath: string
|
||||
let host: ExtensionHost
|
||||
|
||||
beforeAll(() => {
|
||||
console.log("Integration tests running with:")
|
||||
console.log(` - API Key: ${OPENROUTER_API_KEY?.substring(0, 12)}...`)
|
||||
console.log(` - Extension Path: ${extensionPath}`)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
workspacePath = createTempWorkspace()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (host) {
|
||||
await host.dispose()
|
||||
}
|
||||
cleanupWorkspace(workspacePath)
|
||||
})
|
||||
|
||||
/**
|
||||
* Main integration test - tests the complete end-to-end flow
|
||||
*
|
||||
* NOTE: Due to the extension using singletons (TelemetryService, etc.),
|
||||
* only one integration test can run per process. This single test covers
|
||||
* the main functionality: activation, task execution, completion, and disposal.
|
||||
*/
|
||||
it("should complete end-to-end task execution with proper lifecycle", async () => {
|
||||
host = new ExtensionHost({
|
||||
mode: "code",
|
||||
apiProvider: "openrouter",
|
||||
apiKey: OPENROUTER_API_KEY!,
|
||||
model: "anthropic/claude-haiku-4.5", // Use fast, cheap model for tests.
|
||||
workspacePath,
|
||||
extensionPath: extensionPath!,
|
||||
quiet: true,
|
||||
})
|
||||
|
||||
// Test activation
|
||||
await host.activate()
|
||||
|
||||
// Track state messages
|
||||
const stateMessages: unknown[] = []
|
||||
host.on("extensionWebviewMessage", (msg: Record<string, unknown>) => {
|
||||
if (msg.type === "state") {
|
||||
stateMessages.push(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// Test task execution with completion
|
||||
// Note: runTask internally waits for webview to be ready before sending messages
|
||||
await expect(host.runTask("Say hello in exactly 5 words")).resolves.toBeUndefined()
|
||||
|
||||
// After task completes, webview should have been ready
|
||||
expect(host.isInInitialSetup()).toBe(false)
|
||||
|
||||
// Verify we received state updates
|
||||
expect(stateMessages.length).toBeGreaterThan(0)
|
||||
|
||||
// Test disposal
|
||||
await host.dispose()
|
||||
expect((global as Record<string, unknown>).vscode).toBeUndefined()
|
||||
expect((global as Record<string, unknown>).__extensionHost).toBeUndefined()
|
||||
}, 120000) // 2 minute timeout
|
||||
},
|
||||
)
|
||||
|
||||
// Additional test to verify skip behavior
|
||||
describe("Integration test skip behavior", () => {
|
||||
it("should have OPENROUTER_API_KEY check", () => {
|
||||
if (hasApiKey) {
|
||||
console.log("OPENROUTER_API_KEY is set, integration tests will run")
|
||||
} else {
|
||||
console.log("OPENROUTER_API_KEY is not set, integration tests will be skipped")
|
||||
}
|
||||
expect(true).toBe(true) // Always passes
|
||||
})
|
||||
|
||||
it("should have extension check", () => {
|
||||
if (hasExtension) {
|
||||
console.log(`Extension found at: ${extensionPath}`)
|
||||
} else {
|
||||
console.log("Extension not found, integration tests will be skipped")
|
||||
}
|
||||
expect(true).toBe(true) // Always passes
|
||||
})
|
||||
})
|
||||
119
apps/cli/src/__tests__/utils.test.ts
Normal file
119
apps/cli/src/__tests__/utils.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Unit tests for CLI utility functions
|
||||
*/
|
||||
|
||||
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "../utils.js"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
// Mock fs module
|
||||
vi.mock("fs")
|
||||
|
||||
describe("getEnvVarName", () => {
|
||||
it.each([
|
||||
["anthropic", "ANTHROPIC_API_KEY"],
|
||||
["openai", "OPENAI_API_KEY"],
|
||||
["openrouter", "OPENROUTER_API_KEY"],
|
||||
["google", "GOOGLE_API_KEY"],
|
||||
["gemini", "GOOGLE_API_KEY"],
|
||||
["bedrock", "AWS_ACCESS_KEY_ID"],
|
||||
["ollama", "OLLAMA_API_KEY"],
|
||||
["mistral", "MISTRAL_API_KEY"],
|
||||
["deepseek", "DEEPSEEK_API_KEY"],
|
||||
])("should return %s for %s provider", (provider, expectedEnvVar) => {
|
||||
expect(getEnvVarName(provider)).toBe(expectedEnvVar)
|
||||
})
|
||||
|
||||
it("should handle case-insensitive provider names", () => {
|
||||
expect(getEnvVarName("ANTHROPIC")).toBe("ANTHROPIC_API_KEY")
|
||||
expect(getEnvVarName("Anthropic")).toBe("ANTHROPIC_API_KEY")
|
||||
expect(getEnvVarName("OpenRouter")).toBe("OPENROUTER_API_KEY")
|
||||
})
|
||||
|
||||
it("should return uppercase provider name with _API_KEY suffix for unknown providers", () => {
|
||||
expect(getEnvVarName("custom")).toBe("CUSTOM_API_KEY")
|
||||
expect(getEnvVarName("myProvider")).toBe("MYPROVIDER_API_KEY")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getApiKeyFromEnv", () => {
|
||||
const originalEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset process.env before each test
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for anthropic", () => {
|
||||
process.env.ANTHROPIC_API_KEY = "test-anthropic-key"
|
||||
expect(getApiKeyFromEnv("anthropic")).toBe("test-anthropic-key")
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for openrouter", () => {
|
||||
process.env.OPENROUTER_API_KEY = "test-openrouter-key"
|
||||
expect(getApiKeyFromEnv("openrouter")).toBe("test-openrouter-key")
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for openai", () => {
|
||||
process.env.OPENAI_API_KEY = "test-openai-key"
|
||||
expect(getApiKeyFromEnv("openai")).toBe("test-openai-key")
|
||||
})
|
||||
|
||||
it("should return undefined when API key is not set", () => {
|
||||
delete process.env.ANTHROPIC_API_KEY
|
||||
expect(getApiKeyFromEnv("anthropic")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle custom provider names", () => {
|
||||
process.env.CUSTOM_API_KEY = "test-custom-key"
|
||||
expect(getApiKeyFromEnv("custom")).toBe("test-custom-key")
|
||||
})
|
||||
|
||||
it("should handle case-insensitive provider lookup", () => {
|
||||
process.env.ANTHROPIC_API_KEY = "test-key"
|
||||
expect(getApiKeyFromEnv("ANTHROPIC")).toBe("test-key")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDefaultExtensionPath", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("should return monorepo path when extension.js exists there", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
expect(result).toBe(expectedMonorepoPath)
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
|
||||
})
|
||||
|
||||
it("should return package path when extension.js does not exist in monorepo path", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedPackagePath = path.resolve(mockDirname, "../extension")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
expect(result).toBe(expectedPackagePath)
|
||||
})
|
||||
|
||||
it("should check monorepo path first", () => {
|
||||
const mockDirname = "/some/path"
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
|
||||
getDefaultExtensionPath(mockDirname)
|
||||
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
|
||||
})
|
||||
})
|
||||
1663
apps/cli/src/extension-host.ts
Normal file
1663
apps/cli/src/extension-host.ts
Normal file
File diff suppressed because it is too large
Load diff
163
apps/cli/src/index.ts
Normal file
163
apps/cli/src/index.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* @roo-code/cli - Command Line Interface for Roo Code
|
||||
*/
|
||||
|
||||
import { Command } from "commander"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import {
|
||||
type ProviderName,
|
||||
type ReasoningEffortExtended,
|
||||
isProviderName,
|
||||
reasoningEffortsExtended,
|
||||
} from "@roo-code/types"
|
||||
import { setLogger } from "@roo-code/vscode-shim"
|
||||
|
||||
import { ExtensionHost } from "./extension-host.js"
|
||||
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "./utils.js"
|
||||
|
||||
const DEFAULTS = {
|
||||
mode: "code",
|
||||
reasoningEffort: "medium" as const,
|
||||
model: "anthropic/claude-sonnet-4.5",
|
||||
}
|
||||
|
||||
const REASONING_EFFORTS = [...reasoningEffortsExtended, "unspecified", "disabled"]
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program.name("roo").description("Roo Code CLI - Run the Roo Code agent from the command line").version("0.1.0")
|
||||
|
||||
program
|
||||
.argument("<prompt>", "The prompt/task to execute")
|
||||
.option("-w, --workspace <path>", "Workspace path to operate in", process.cwd())
|
||||
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
||||
.option("-v, --verbose", "Enable verbose output (show VSCode and extension logs)", false)
|
||||
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
|
||||
.option("-x, --exit-on-complete", "Exit the process when the task completes (useful for testing)", false)
|
||||
.option("-y, --yes", "Auto-approve all prompts (non-interactive mode)", false)
|
||||
.option("-k, --api-key <key>", "API key for the LLM provider (defaults to ANTHROPIC_API_KEY env var)")
|
||||
.option("-p, --provider <provider>", "API provider (anthropic, openai, openrouter, etc.)", "openrouter")
|
||||
.option("-m, --model <model>", "Model to use", DEFAULTS.model)
|
||||
.option("-M, --mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULTS.mode)
|
||||
.option(
|
||||
"-r, --reasoning-effort <effort>",
|
||||
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
||||
DEFAULTS.reasoningEffort,
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
prompt: string,
|
||||
options: {
|
||||
workspace: string
|
||||
extension?: string
|
||||
verbose: boolean
|
||||
debug: boolean
|
||||
exitOnComplete: boolean
|
||||
yes: boolean
|
||||
apiKey?: string
|
||||
provider: ProviderName
|
||||
model?: string
|
||||
mode?: string
|
||||
reasoningEffort?: ReasoningEffortExtended | "unspecified" | "disabled"
|
||||
},
|
||||
) => {
|
||||
// Default is quiet mode - suppress VSCode shim logs unless verbose
|
||||
// or debug is specified.
|
||||
if (!options.verbose && !options.debug) {
|
||||
setLogger({
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
const extensionPath = options.extension || getDefaultExtensionPath(__dirname)
|
||||
const apiKey = options.apiKey || getApiKeyFromEnv(options.provider)
|
||||
const workspacePath = path.resolve(options.workspace)
|
||||
|
||||
if (!apiKey) {
|
||||
console.error(
|
||||
`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`,
|
||||
)
|
||||
console.error(`[CLI] For ${options.provider}, set ${getEnvVarName(options.provider)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
console.error(`[CLI] Error: Workspace path does not exist: ${workspacePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!isProviderName(options.provider)) {
|
||||
console.error(`[CLI] Error: Invalid provider: ${options.provider}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (options.reasoningEffort && !REASONING_EFFORTS.includes(options.reasoningEffort)) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid reasoning effort: ${options.reasoningEffort}, must be one of: ${REASONING_EFFORTS.join(", ")}`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`[CLI] Mode: ${options.mode || "default"}`)
|
||||
console.log(`[CLI] Reasoning Effort: ${options.reasoningEffort || "default"}`)
|
||||
console.log(`[CLI] Provider: ${options.provider}`)
|
||||
console.log(`[CLI] Model: ${options.model || "default"}`)
|
||||
console.log(`[CLI] Workspace: ${workspacePath}`)
|
||||
|
||||
const host = new ExtensionHost({
|
||||
mode: options.mode || DEFAULTS.mode,
|
||||
reasoningEffort: options.reasoningEffort === "unspecified" ? undefined : options.reasoningEffort,
|
||||
apiProvider: options.provider,
|
||||
apiKey,
|
||||
model: options.model || DEFAULTS.model,
|
||||
workspacePath,
|
||||
extensionPath: path.resolve(extensionPath),
|
||||
verbose: options.debug,
|
||||
quiet: !options.verbose && !options.debug,
|
||||
nonInteractive: options.yes,
|
||||
})
|
||||
|
||||
// Handle SIGINT (Ctrl+C)
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\n[CLI] Received SIGINT, shutting down...")
|
||||
await host.dispose()
|
||||
process.exit(130)
|
||||
})
|
||||
|
||||
// Handle SIGTERM
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("\n[CLI] Received SIGTERM, shutting down...")
|
||||
await host.dispose()
|
||||
process.exit(143)
|
||||
})
|
||||
|
||||
try {
|
||||
await host.activate()
|
||||
await host.runTask(prompt)
|
||||
await host.dispose()
|
||||
|
||||
if (options.exitOnComplete) {
|
||||
process.exit(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[CLI] Error:", error instanceof Error ? error.message : String(error))
|
||||
|
||||
if (options.debug && error instanceof Error) {
|
||||
console.error(error.stack)
|
||||
}
|
||||
|
||||
await host.dispose()
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
program.parse()
|
||||
62
apps/cli/src/utils.ts
Normal file
62
apps/cli/src/utils.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Utility functions for the Roo Code CLI
|
||||
*/
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
/**
|
||||
* Get the environment variable name for a provider's API key
|
||||
*/
|
||||
export function getEnvVarName(provider: string): string {
|
||||
const envVarMap: Record<string, string> = {
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
openai: "OPENAI_API_KEY",
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
google: "GOOGLE_API_KEY",
|
||||
gemini: "GOOGLE_API_KEY",
|
||||
bedrock: "AWS_ACCESS_KEY_ID",
|
||||
ollama: "OLLAMA_API_KEY",
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
}
|
||||
return envVarMap[provider.toLowerCase()] || `${provider.toUpperCase()}_API_KEY`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from environment variable based on provider
|
||||
*/
|
||||
export function getApiKeyFromEnv(provider: string): string | undefined {
|
||||
const envVar = getEnvVarName(provider)
|
||||
return process.env[envVar]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default path to the extension bundle.
|
||||
* This assumes the CLI is installed alongside the built extension.
|
||||
*
|
||||
* @param dirname - The __dirname equivalent for the calling module
|
||||
*/
|
||||
export function getDefaultExtensionPath(dirname: string): string {
|
||||
// Check for environment variable first (set by install script)
|
||||
if (process.env.ROO_EXTENSION_PATH) {
|
||||
const envPath = process.env.ROO_EXTENSION_PATH
|
||||
if (fs.existsSync(path.join(envPath, "extension.js"))) {
|
||||
return envPath
|
||||
}
|
||||
}
|
||||
|
||||
// __dirname is apps/cli/dist when bundled
|
||||
// The extension is at src/dist (relative to monorepo root)
|
||||
// So from apps/cli/dist, we need to go ../../../src/dist
|
||||
const monorepoPath = path.resolve(dirname, "../../../src/dist")
|
||||
|
||||
// Try monorepo path first (for development)
|
||||
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
|
||||
return monorepoPath
|
||||
}
|
||||
|
||||
// Fallback: when installed via curl script, extension is at ../extension
|
||||
const packagePath = path.resolve(dirname, "../extension")
|
||||
return packagePath
|
||||
}
|
||||
9
apps/cli/tsconfig.json
Normal file
9
apps/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "*.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
24
apps/cli/tsup.config.ts
Normal file
24
apps/cli/tsup.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: "node20",
|
||||
platform: "node",
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
// Bundle workspace packages that export TypeScript
|
||||
noExternal: ["@roo-code/types", "@roo-code/vscode-shim"],
|
||||
external: [
|
||||
// Keep native modules external
|
||||
"@anthropic-ai/sdk",
|
||||
"@anthropic-ai/bedrock-sdk",
|
||||
"@anthropic-ai/vertex-sdk",
|
||||
// Keep @vscode/ripgrep external - we bundle the binary separately
|
||||
"@vscode/ripgrep",
|
||||
],
|
||||
})
|
||||
11
apps/cli/vitest.config.ts
Normal file
11
apps/cli/vitest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
watch: false,
|
||||
testTimeout: 120_000, // 2m for integration tests.
|
||||
include: ["src/**/*.test.ts"],
|
||||
},
|
||||
})
|
||||
|
|
@ -19,10 +19,6 @@ suite("Roo Code Extension", function () {
|
|||
"openInNewTab",
|
||||
"settingsButtonClicked",
|
||||
"historyButtonClicked",
|
||||
"showHumanRelayDialog",
|
||||
"registerHumanRelayCallback",
|
||||
"unregisterHumanRelayCallback",
|
||||
"handleHumanRelayResponse",
|
||||
"newTask",
|
||||
"setCustomStoragePath",
|
||||
"focusInput",
|
||||
|
|
|
|||
|
|
@ -28,10 +28,18 @@ const EVALS_STORAGE_PATH = "/tmp/evals/runs"
|
|||
|
||||
const EVALS_REPO_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../../evals")
|
||||
|
||||
export async function createRun({ suite, exercises = [], timeout, iterations = 1, ...values }: CreateRun) {
|
||||
export async function createRun({
|
||||
suite,
|
||||
exercises = [],
|
||||
timeout,
|
||||
iterations = 1,
|
||||
executionMethod = "vscode",
|
||||
...values
|
||||
}: CreateRun) {
|
||||
const run = await _createRun({
|
||||
...values,
|
||||
timeout,
|
||||
executionMethod,
|
||||
socketPath: "", // TODO: Get rid of this.
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,32 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, 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 { toast } from "sonner"
|
||||
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, Info, Plus, Minus } from "lucide-react"
|
||||
import {
|
||||
X,
|
||||
Rocket,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
SlidersHorizontal,
|
||||
Info,
|
||||
Plus,
|
||||
Minus,
|
||||
Terminal,
|
||||
MonitorPlay,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
globalSettingsSchema,
|
||||
providerSettingsSchema,
|
||||
EVALS_SETTINGS,
|
||||
getModelId,
|
||||
type ProviderSettings,
|
||||
type GlobalSettings,
|
||||
globalSettingsSchema,
|
||||
providerSettingsSchema,
|
||||
getModelId,
|
||||
EVALS_SETTINGS,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { createRun } from "@/actions/runs"
|
||||
|
|
@ -23,6 +34,7 @@ import { getExercises } from "@/actions/exercises"
|
|||
|
||||
import {
|
||||
type CreateRun,
|
||||
type ExecutionMethod,
|
||||
createRunSchema,
|
||||
CONCURRENCY_MIN,
|
||||
CONCURRENCY_MAX,
|
||||
|
|
@ -36,6 +48,9 @@ import {
|
|||
} from "@/lib/schemas"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import { loadRooLastModelSelection, saveRooLastModelSelection } from "@/lib/roo-last-model-selection"
|
||||
import { normalizeCreateRunForSubmit } from "@/lib/normalize-create-run"
|
||||
|
||||
import { useOpenRouterModels } from "@/hooks/use-open-router-models"
|
||||
import { useRooCodeCloudModels } from "@/hooks/use-roo-code-cloud-models"
|
||||
|
||||
|
|
@ -77,14 +92,12 @@ type ImportedSettings = {
|
|||
currentApiConfigName: string
|
||||
}
|
||||
|
||||
// Type for a model selection entry
|
||||
type ModelSelection = {
|
||||
id: string
|
||||
model: string
|
||||
popoverOpen: boolean
|
||||
}
|
||||
|
||||
// Type for a config selection entry (for import mode)
|
||||
type ConfigSelection = {
|
||||
id: string
|
||||
configName: string
|
||||
|
|
@ -93,18 +106,19 @@ type ConfigSelection = {
|
|||
|
||||
export function NewRun() {
|
||||
const router = useRouter()
|
||||
const modelSelectionsByProviderRef = useRef<Record<string, ModelSelection[]>>({})
|
||||
const modelValueByProviderRef = useRef<Record<string, string>>({})
|
||||
|
||||
const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("other")
|
||||
const [executionMethod, setExecutionMethod] = useState<ExecutionMethod>("vscode")
|
||||
const [useNativeToolProtocol, setUseNativeToolProtocol] = useState(true)
|
||||
const [commandExecutionTimeout, setCommandExecutionTimeout] = useState(20)
|
||||
const [terminalShellIntegrationTimeout, setTerminalShellIntegrationTimeout] = useState(30) // seconds
|
||||
|
||||
// State for multiple model selections
|
||||
const [modelSelections, setModelSelections] = useState<ModelSelection[]>([
|
||||
{ id: crypto.randomUUID(), model: "", popoverOpen: false },
|
||||
])
|
||||
|
||||
// State for imported settings with multiple config selections
|
||||
const [importedSettings, setImportedSettings] = useState<ImportedSettings | null>(null)
|
||||
const [configSelections, setConfigSelections] = useState<ConfigSelection[]>([
|
||||
{ id: crypto.randomUUID(), configName: "", popoverOpen: false },
|
||||
|
|
@ -119,7 +133,6 @@ export function NewRun() {
|
|||
|
||||
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>({
|
||||
|
|
@ -134,50 +147,91 @@ export function NewRun() {
|
|||
timeout: TIMEOUT_DEFAULT,
|
||||
iterations: ITERATIONS_DEFAULT,
|
||||
jobToken: "",
|
||||
executionMethod: "vscode",
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
clearErrors,
|
||||
watch,
|
||||
getValues,
|
||||
formState: { isSubmitting },
|
||||
} = form
|
||||
|
||||
const [suite, settings] = watch(["suite", "settings", "concurrency"])
|
||||
|
||||
const selectedModelIds = useMemo(
|
||||
() => modelSelections.map((s) => s.model).filter((m) => m.length > 0),
|
||||
[modelSelections],
|
||||
)
|
||||
|
||||
const applyModelIds = useCallback(
|
||||
(modelIds: string[]) => {
|
||||
const unique = Array.from(new Set(modelIds.map((m) => m.trim()).filter((m) => m.length > 0)))
|
||||
|
||||
if (unique.length === 0) {
|
||||
setModelSelections([{ id: crypto.randomUUID(), model: "", popoverOpen: false }])
|
||||
setValue("model", "")
|
||||
return
|
||||
}
|
||||
|
||||
setModelSelections(unique.map((model) => ({ id: crypto.randomUUID(), model, popoverOpen: false })))
|
||||
setValue("model", unique[0] ?? "")
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
|
||||
// Ensure the `exercises` field is registered so RHF always includes it in submit values.
|
||||
useEffect(() => {
|
||||
register("exercises")
|
||||
}, [register])
|
||||
|
||||
// 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")
|
||||
|
|
@ -189,48 +243,102 @@ export function NewRun() {
|
|||
setValue("exercises", parsed)
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
// Invalid JSON, ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [setValue])
|
||||
|
||||
// Track previous provider to detect switches
|
||||
const [prevProvider, setPrevProvider] = useState(provider)
|
||||
|
||||
// Preserve selections per provider; avoids cross-contamination while keeping UX stable.
|
||||
useEffect(() => {
|
||||
if (provider === prevProvider) return
|
||||
|
||||
modelSelectionsByProviderRef.current[prevProvider] = modelSelections
|
||||
modelValueByProviderRef.current[prevProvider] = getValues("model")
|
||||
|
||||
const nextModelSelections =
|
||||
modelSelectionsByProviderRef.current[provider] ??
|
||||
([{ id: crypto.randomUUID(), model: "", popoverOpen: false }] satisfies ModelSelection[])
|
||||
|
||||
setModelSelections(nextModelSelections)
|
||||
|
||||
const nextModelValue =
|
||||
modelValueByProviderRef.current[provider] ??
|
||||
nextModelSelections.find((s) => s.model.trim().length > 0)?.model ??
|
||||
(provider === "other" && importedSettings && configSelections[0]?.configName
|
||||
? (getModelId(importedSettings.apiConfigs[configSelections[0].configName] ?? {}) ?? "")
|
||||
: "")
|
||||
|
||||
setValue("model", nextModelValue)
|
||||
setPrevProvider(provider)
|
||||
}, [provider, prevProvider, modelSelections, setValue, getValues, importedSettings, configSelections])
|
||||
|
||||
// When switching to Roo provider, restore last-used selection if current selection is empty
|
||||
useEffect(() => {
|
||||
if (provider !== "roo") return
|
||||
if (selectedModelIds.length > 0) return
|
||||
|
||||
const last = loadRooLastModelSelection()
|
||||
if (last.length > 0) {
|
||||
applyModelIds(last)
|
||||
}
|
||||
}, [applyModelIds, provider, selectedModelIds.length])
|
||||
|
||||
// Persist last-used Roo provider model selection
|
||||
useEffect(() => {
|
||||
if (provider !== "roo") return
|
||||
saveRooLastModelSelection(selectedModelIds)
|
||||
}, [provider, selectedModelIds])
|
||||
|
||||
// Extract unique languages from exercises
|
||||
const languages = useMemo(() => {
|
||||
if (!exercises.data) return []
|
||||
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)
|
||||
|
||||
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 []
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +349,6 @@ export function NewRun() {
|
|||
[getExercisesForLanguage, selectedExercises, setValue],
|
||||
)
|
||||
|
||||
// Check if all exercises for a language are selected
|
||||
const isLanguageSelected = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
|
|
@ -250,7 +357,6 @@ export function NewRun() {
|
|||
[getExercisesForLanguage, selectedExercises],
|
||||
)
|
||||
|
||||
// Check if some (but not all) exercises for a language are selected
|
||||
const isLanguagePartiallySelected = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
|
|
@ -260,46 +366,40 @@ export function NewRun() {
|
|||
[getExercisesForLanguage, selectedExercises],
|
||||
)
|
||||
|
||||
// Add a new model selection
|
||||
const addModelSelection = useCallback(() => {
|
||||
setModelSelections((prev) => [...prev, { id: crypto.randomUUID(), model: "", popoverOpen: false }])
|
||||
}, [])
|
||||
|
||||
// Remove a model selection
|
||||
const removeModelSelection = useCallback((id: string) => {
|
||||
setModelSelections((prev) => prev.filter((s) => s.id !== id))
|
||||
}, [])
|
||||
|
||||
// Update a model selection
|
||||
const updateModelSelection = useCallback(
|
||||
(id: string, model: string) => {
|
||||
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, model, popoverOpen: false } : s)))
|
||||
// Also set the form model field for validation (use first non-empty model)
|
||||
// Also set the form model field for validation (use first non-empty model).
|
||||
setValue("model", model)
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
|
||||
// Toggle popover for a model selection
|
||||
const toggleModelPopover = useCallback((id: string, open: boolean) => {
|
||||
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
|
||||
}, [])
|
||||
|
||||
// Add a new config selection
|
||||
const addConfigSelection = useCallback(() => {
|
||||
setConfigSelections((prev) => [...prev, { id: crypto.randomUUID(), configName: "", popoverOpen: false }])
|
||||
}, [])
|
||||
|
||||
// Remove a config selection
|
||||
const removeConfigSelection = useCallback((id: string) => {
|
||||
setConfigSelections((prev) => prev.filter((s) => s.id !== id))
|
||||
}, [])
|
||||
|
||||
// Update a config selection
|
||||
const updateConfigSelection = useCallback(
|
||||
(id: string, configName: string) => {
|
||||
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, configName, popoverOpen: false } : s)))
|
||||
// Also update the form settings for the first config (for validation)
|
||||
|
||||
// Also update the form settings for the first config (for validation).
|
||||
if (importedSettings) {
|
||||
const providerSettings = importedSettings.apiConfigs[configName] ?? {}
|
||||
setValue("model", getModelId(providerSettings) ?? "")
|
||||
|
|
@ -309,7 +409,6 @@ export function NewRun() {
|
|||
[importedSettings, setValue],
|
||||
)
|
||||
|
||||
// Toggle popover for a config selection
|
||||
const toggleConfigPopover = useCallback((id: string, open: boolean) => {
|
||||
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
|
||||
}, [])
|
||||
|
|
@ -317,24 +416,23 @@ export function NewRun() {
|
|||
const onSubmit = useCallback(
|
||||
async (values: CreateRun) => {
|
||||
try {
|
||||
const baseValues = normalizeCreateRunForSubmit(values, selectedExercises, suite)
|
||||
|
||||
// Validate jobToken for Roo Code Cloud provider
|
||||
if (provider === "roo" && !values.jobToken?.trim()) {
|
||||
if (provider === "roo" && !baseValues.jobToken?.trim()) {
|
||||
toast.error("Roo Code Cloud Token is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which selections to use based on provider
|
||||
const selectionsToLaunch: { model: string; configName?: string }[] = []
|
||||
|
||||
if (provider === "other") {
|
||||
// For import mode, use config selections
|
||||
for (const config of configSelections) {
|
||||
if (config.configName) {
|
||||
selectionsToLaunch.push({ model: "", configName: config.configName })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For openrouter/roo, use model selections
|
||||
for (const selection of modelSelections) {
|
||||
if (selection.model) {
|
||||
selectionsToLaunch.push({ model: selection.model })
|
||||
|
|
@ -347,20 +445,18 @@ export function NewRun() {
|
|||
return
|
||||
}
|
||||
|
||||
// Show launching toast
|
||||
const totalRuns = selectionsToLaunch.length
|
||||
toast.info(totalRuns > 1 ? `Launching ${totalRuns} runs (every 20 seconds)...` : "Launching run...")
|
||||
|
||||
// Launch runs with 20-second delay between each
|
||||
for (let i = 0; i < selectionsToLaunch.length; i++) {
|
||||
const selection = selectionsToLaunch[i]!
|
||||
|
||||
// Wait 20 seconds between runs (except for the first one)
|
||||
// Wait 20 seconds between runs (except for the first one).
|
||||
if (i > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20000))
|
||||
await new Promise((resolve) => setTimeout(resolve, 20_000))
|
||||
}
|
||||
|
||||
const runValues = { ...values }
|
||||
const runValues = { ...baseValues }
|
||||
|
||||
if (provider === "openrouter") {
|
||||
runValues.model = selection.model
|
||||
|
|
@ -403,13 +499,14 @@ export function NewRun() {
|
|||
}
|
||||
}
|
||||
|
||||
// Navigate back to main evals UI
|
||||
router.push("/")
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "An unknown error occurred.")
|
||||
}
|
||||
},
|
||||
[
|
||||
suite,
|
||||
selectedExercises,
|
||||
provider,
|
||||
modelSelections,
|
||||
configSelections,
|
||||
|
|
@ -442,18 +539,15 @@ export function NewRun() {
|
|||
})
|
||||
.parse(JSON.parse(await file.text()))
|
||||
|
||||
// Store all imported configs for user selection
|
||||
setImportedSettings({
|
||||
apiConfigs: providerProfiles.apiConfigs,
|
||||
globalSettings,
|
||||
currentApiConfigName: providerProfiles.currentApiConfigName,
|
||||
})
|
||||
|
||||
// Default to the current config for the first selection
|
||||
const defaultConfigName = providerProfiles.currentApiConfigName
|
||||
setConfigSelections([{ id: crypto.randomUUID(), configName: defaultConfigName, popoverOpen: false }])
|
||||
|
||||
// Apply the default config
|
||||
const providerSettings = providerProfiles.apiConfigs[defaultConfigName] ?? {}
|
||||
setValue("model", getModelId(providerSettings) ?? "")
|
||||
setValue("settings", { ...EVALS_SETTINGS, ...providerSettings, ...globalSettings })
|
||||
|
|
@ -971,6 +1065,36 @@ export function NewRun() {
|
|||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Execution Method */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="executionMethod"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Execution Method</FormLabel>
|
||||
<Tabs
|
||||
value={executionMethod}
|
||||
onValueChange={(value) => {
|
||||
const newExecutionMethod = value as ExecutionMethod
|
||||
setExecutionMethod(newExecutionMethod)
|
||||
setValue("executionMethod", newExecutionMethod)
|
||||
}}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="vscode" className="flex items-center gap-2">
|
||||
<MonitorPlay className="size-4" />
|
||||
VSCode
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cli" className="flex items-center gap-2">
|
||||
<Terminal className="size-4" />
|
||||
CLI
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
import { normalizeCreateRunForSubmit } from "../normalize-create-run"
|
||||
|
||||
describe("normalizeCreateRunForSubmit", () => {
|
||||
it("uses selectedExercises for partial suite", () => {
|
||||
const result = normalizeCreateRunForSubmit(
|
||||
{
|
||||
model: "roo/model-a",
|
||||
description: "",
|
||||
suite: "partial",
|
||||
exercises: [],
|
||||
settings: undefined,
|
||||
concurrency: 1,
|
||||
timeout: 5,
|
||||
iterations: 1,
|
||||
jobToken: "",
|
||||
executionMethod: "vscode",
|
||||
},
|
||||
["js/foo", "py/bar"],
|
||||
)
|
||||
|
||||
expect(result.suite).toBe("partial")
|
||||
expect(result.exercises).toEqual(["js/foo", "py/bar"])
|
||||
})
|
||||
|
||||
it("dedupes selectedExercises for partial suite", () => {
|
||||
const result = normalizeCreateRunForSubmit(
|
||||
{
|
||||
model: "roo/model-a",
|
||||
description: "",
|
||||
suite: "partial",
|
||||
exercises: [],
|
||||
settings: undefined,
|
||||
concurrency: 1,
|
||||
timeout: 5,
|
||||
iterations: 1,
|
||||
jobToken: "",
|
||||
executionMethod: "vscode",
|
||||
},
|
||||
["js/foo", "js/foo", "py/bar"],
|
||||
)
|
||||
|
||||
expect(result.exercises).toEqual(["js/foo", "py/bar"])
|
||||
})
|
||||
|
||||
it("clears exercises for full suite", () => {
|
||||
const result = normalizeCreateRunForSubmit(
|
||||
{
|
||||
model: "roo/model-a",
|
||||
description: "",
|
||||
suite: "full",
|
||||
exercises: ["js/foo"],
|
||||
settings: undefined,
|
||||
concurrency: 1,
|
||||
timeout: 5,
|
||||
iterations: 1,
|
||||
jobToken: "",
|
||||
executionMethod: "vscode",
|
||||
},
|
||||
["js/foo"],
|
||||
)
|
||||
|
||||
expect(result.suite).toBe("full")
|
||||
expect(result.exercises).toEqual([])
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
loadRooLastModelSelection,
|
||||
ROO_LAST_MODEL_SELECTION_KEY,
|
||||
saveRooLastModelSelection,
|
||||
} from "../roo-last-model-selection"
|
||||
|
||||
class LocalStorageMock implements Storage {
|
||||
private store = new Map<string, string>()
|
||||
|
||||
get length(): number {
|
||||
return this.store.size
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store.clear()
|
||||
}
|
||||
|
||||
getItem(key: string): string | null {
|
||||
return this.store.get(key) ?? null
|
||||
}
|
||||
|
||||
key(index: number): string | null {
|
||||
return Array.from(this.store.keys())[index] ?? null
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
this.store.delete(key)
|
||||
}
|
||||
|
||||
setItem(key: string, value: string): void {
|
||||
this.store.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
value: new LocalStorageMock(),
|
||||
configurable: true,
|
||||
})
|
||||
})
|
||||
|
||||
describe("roo-last-model-selection", () => {
|
||||
it("saves and loads (deduped + trimmed)", () => {
|
||||
saveRooLastModelSelection([" roo/model-a ", "roo/model-a", "roo/model-b"])
|
||||
expect(loadRooLastModelSelection()).toEqual(["roo/model-a", "roo/model-b"])
|
||||
})
|
||||
|
||||
it("ignores invalid JSON", () => {
|
||||
localStorage.setItem(ROO_LAST_MODEL_SELECTION_KEY, "{this is not json")
|
||||
expect(loadRooLastModelSelection()).toEqual([])
|
||||
})
|
||||
|
||||
it("clears when empty", () => {
|
||||
localStorage.setItem(ROO_LAST_MODEL_SELECTION_KEY, JSON.stringify(["roo/model-a"]))
|
||||
saveRooLastModelSelection([])
|
||||
expect(localStorage.getItem(ROO_LAST_MODEL_SELECTION_KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it("does not throw if localStorage access fails", () => {
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
value: {
|
||||
getItem: () => {
|
||||
throw new Error("blocked")
|
||||
},
|
||||
setItem: () => {
|
||||
throw new Error("blocked")
|
||||
},
|
||||
removeItem: () => {
|
||||
throw new Error("blocked")
|
||||
},
|
||||
},
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
expect(() => loadRooLastModelSelection()).not.toThrow()
|
||||
expect(() => saveRooLastModelSelection(["roo/model-a"])).not.toThrow()
|
||||
})
|
||||
})
|
||||
20
apps/web-evals/src/lib/normalize-create-run.ts
Normal file
20
apps/web-evals/src/lib/normalize-create-run.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { CreateRun } from "./schemas"
|
||||
|
||||
/**
|
||||
* The New Run UI keeps exercise selection in component state.
|
||||
* This normalizer ensures we submit the *visible/selected* exercises when suite is partial.
|
||||
*/
|
||||
export function normalizeCreateRunForSubmit(
|
||||
values: CreateRun,
|
||||
selectedExercises: string[],
|
||||
suiteOverride?: CreateRun["suite"],
|
||||
): CreateRun {
|
||||
const suite = suiteOverride ?? values.suite
|
||||
const normalizedSelectedExercises = Array.from(new Set(selectedExercises))
|
||||
|
||||
return {
|
||||
...values,
|
||||
suite,
|
||||
exercises: suite === "partial" ? normalizedSelectedExercises : [],
|
||||
}
|
||||
}
|
||||
76
apps/web-evals/src/lib/roo-last-model-selection.ts
Normal file
76
apps/web-evals/src/lib/roo-last-model-selection.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { z } from "zod"
|
||||
|
||||
export const ROO_LAST_MODEL_SELECTION_KEY = "evals-roo-last-model-selection"
|
||||
|
||||
const modelIdListSchema = z.array(z.string())
|
||||
|
||||
function hasLocalStorage(): boolean {
|
||||
try {
|
||||
return typeof localStorage !== "undefined"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function safeGetItem(key: string): string | null {
|
||||
try {
|
||||
return localStorage.getItem(key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function safeSetItem(key: string, value: string): void {
|
||||
try {
|
||||
localStorage.setItem(key, value)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function safeRemoveItem(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseJson(raw: string | null): unknown {
|
||||
if (raw === null) return undefined
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeModelIds(modelIds: string[]): string[] {
|
||||
const unique = new Set<string>()
|
||||
for (const id of modelIds) {
|
||||
const trimmed = id.trim()
|
||||
if (trimmed) unique.add(trimmed)
|
||||
}
|
||||
return Array.from(unique)
|
||||
}
|
||||
|
||||
export function loadRooLastModelSelection(): string[] {
|
||||
if (!hasLocalStorage()) return []
|
||||
|
||||
const parsed = modelIdListSchema.safeParse(tryParseJson(safeGetItem(ROO_LAST_MODEL_SELECTION_KEY)))
|
||||
if (!parsed.success) return []
|
||||
|
||||
return normalizeModelIds(parsed.data)
|
||||
}
|
||||
|
||||
export function saveRooLastModelSelection(modelIds: string[]): void {
|
||||
if (!hasLocalStorage()) return
|
||||
|
||||
const normalized = normalizeModelIds(modelIds)
|
||||
if (normalized.length === 0) {
|
||||
safeRemoveItem(ROO_LAST_MODEL_SELECTION_KEY)
|
||||
return
|
||||
}
|
||||
|
||||
safeSetItem(ROO_LAST_MODEL_SELECTION_KEY, JSON.stringify(normalized))
|
||||
}
|
||||
|
|
@ -2,6 +2,13 @@ import { z } from "zod"
|
|||
|
||||
import { rooCodeSettingsSchema } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* ExecutionMethod
|
||||
*/
|
||||
|
||||
export const executionMethodSchema = z.enum(["vscode", "cli"])
|
||||
export type ExecutionMethod = z.infer<typeof executionMethodSchema>
|
||||
|
||||
/**
|
||||
* CreateRun
|
||||
*/
|
||||
|
|
@ -29,6 +36,7 @@ export const createRunSchema = z
|
|||
timeout: z.number().int().min(TIMEOUT_MIN).max(TIMEOUT_MAX),
|
||||
iterations: z.number().int().min(ITERATIONS_MIN).max(ITERATIONS_MAX),
|
||||
jobToken: z.string().optional(),
|
||||
executionMethod: executionMethodSchema,
|
||||
})
|
||||
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
|
||||
message: "Exercises are required when running a partial suite.",
|
||||
|
|
|
|||
|
|
@ -283,7 +283,8 @@ export default function Privacy() {
|
|||
</li>
|
||||
<li>
|
||||
<strong>Delete your Cloud account</strong> at any time from{" "}
|
||||
<strong>Security Settings</strong> inside Roo Code Cloud.
|
||||
<strong>Security Settings</strong> inside Roo Code Cloud (User Menu → My Settings
|
||||
→ Open Profile).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Marketing communications:</strong> You can unsubscribe from marketing and
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const features: Feature[] = [
|
|||
icon: CheckCheck,
|
||||
title: "Granular auto-approval",
|
||||
description:
|
||||
"Control each action and make Roo as autonomous as you want as you build confidence. Or go YOLO and let it rip.",
|
||||
"Control each action and make Roo as autonomous as you want as you build confidence. Or go BRRR and let it rip.",
|
||||
},
|
||||
{
|
||||
icon: Boxes,
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ export function PillarsSection() {
|
|||
<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">
|
||||
<Link target="_blank" href="https://github.com/RooCodeInc/Roo-Code">
|
||||
open source
|
||||
</Link>{" "}
|
||||
so you can see for yourself exactly what it's doing and we don't use
|
||||
|
|
|
|||
4
locales/ca/README.md
generated
4
locales/ca/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Més informació: [Ús de Modes](https://docs.roocode.com/basic-usage/using-mode
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instal·lant Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurant perfils</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexació de la base de codi</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personalitzats</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punts de control</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Llistes de tasques</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personalitzats</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punts de control</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestió de Context</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/de/README.md
generated
4
locales/de/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Mehr erfahren: [Modi verwenden](https://docs.roocode.com/basic-usage/using-modes
|
|||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code installieren</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Profile konfigurieren</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebasis-Indizierung</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Benutzerdefinierte Modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Todo-Listen</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Benutzerdefinierte Modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Kontextverwaltung</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/es/README.md
generated
4
locales/es/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Más info: [Usar Modos](https://docs.roocode.com/basic-usage/using-modes) • [M
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instalando Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurando perfiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexación de la base de código</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listas de Tareas</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestión de Contexto</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/fr/README.md
generated
4
locales/fr/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ En savoir plus : [Utiliser les Modes](https://docs.roocode.com/basic-usage/using
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installer Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurer les profils</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexation de la base de code</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personnalisés</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listes de tâches</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personnalisés</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestion du Contexte</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/hi/README.md
generated
4
locales/hi/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>रू कोड इंस्टॉल करना</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>प्रोफाइल कॉन्फ़िगर करना</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>कोडबेस इंडेक्सिंग</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>कस्टम मोड</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>चेकपॉइंट्स</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>टू-डू लिस्ट</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>कस्टम मोड</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>चेकपॉइंट्स</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>संदर्भ प्रबंधन</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/id/README.md
generated
4
locales/id/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Pelajari lebih lanjut: [Menggunakan Mode](https://docs.roocode.com/basic-usage/u
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Menginstal Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Mengonfigurasi Profil</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Pengindeksan Basis Kode</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Mode Kustom</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Pos Pemeriksaan</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Daftar Tugas</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Mode Kustom</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Pos Pemeriksaan</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Manajemen Konteks</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/it/README.md
generated
4
locales/it/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Scopri di più: [Usare le Modalità](https://docs.roocode.com/basic-usage/using-
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installazione di Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurazione dei profili</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indicizzazione della codebase</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modalità personalizzate</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoint</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Elenchi di cose da fare</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modalità personalizzate</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoint</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestione del Contesto</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/ja/README.md
generated
4
locales/ja/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Roo Codeは、あなたの働き方に合わせるように適応します。
|
|||
| | | |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Codeのインストール</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>プロファイルの設定</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>コードベースのインデックス作成</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>カスタムモード</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>チェックポイント</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>ToDoリスト</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>カスタムモード</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>チェックポイント</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>コンテキスト管理</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/ko/README.md
generated
4
locales/ko/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Roo Code는 당신의 작업 방식에 맞춰 적응합니다.
|
|||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code 설치하기</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>프로필 구성하기</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>코드베이스 인덱싱</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>사용자 지정 모드</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>체크포인트</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>할 일 목록</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>사용자 지정 모드</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>체크포인트</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>컨텍스트 관리</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/nl/README.md
generated
4
locales/nl/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Meer info: [Modi gebruiken](https://docs.roocode.com/basic-usage/using-modes)
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code installeren</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Profielen configureren</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase indexeren</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Aangepaste modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>To-Do Lijsten</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Aangepaste modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Contextbeheer</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/pl/README.md
generated
4
locales/pl/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Więcej: [Korzystanie z trybów](https://docs.roocode.com/basic-usage/using-mode
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instalacja Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Konfiguracja profili</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indeksowanie bazy kodu</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Tryby niestandardowe</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punkty kontrolne</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listy zadań</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Tryby niestandardowe</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punkty kontrolne</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Zarządzanie Kontekstem</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/pt-BR/README.md
generated
4
locales/pt-BR/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Saiba mais: [Usar Modos](https://docs.roocode.com/basic-usage/using-modes) • [
|
|||
| | | |
|
||||
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instalando o Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurando perfis</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexação da base de código</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listas de tarefas</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gerenciamento de Contexto</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/ru/README.md
generated
4
locales/ru/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Roo Code адаптируется к вашему стилю работы, а н
|
|||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Установка Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Настройка профилей</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Индексация кодовой базы</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Пользовательские режимы</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Контрольные точки</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Списки дел</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Пользовательские режимы</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Контрольные точки</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Управление Контекстом</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
10
locales/tr/README.md
generated
10
locales/tr/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ Daha fazla: [Modları kullanma](https://docs.roocode.com/basic-usage/using-modes
|
|||
|
||||
<div align="center">
|
||||
|
||||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code Kurulumu</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Profilleri Yapılandırma</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Kod Tabanı İndeksleme</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Özel Modlar</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Kontrol Noktaları</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Yapılacaklar Listeleri</b> |
|
||||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code Kurulumu</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Profilleri Yapılandırma</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Kod Tabanı İndeksleme</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Özel Modlar</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Kontrol Noktaları</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Bağlam Yönetimi</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
10
locales/vi/README.md
generated
10
locales/vi/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ Xem thêm: [Sử dụng Chế độ](https://docs.roocode.com/basic-usage/using-
|
|||
|
||||
<div align="center">
|
||||
|
||||
| | | |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Cài đặt Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Định cấu hình Hồ sơ</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Lập chỉ mục cơ sở mã</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Chế độ tùy chỉnh</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Điểm kiểm tra</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Danh sách việc cần làm</b> |
|
||||
| | | |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Cài đặt Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Định cấu hình Hồ sơ</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Lập chỉ mục cơ sở mã</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Chế độ tùy chỉnh</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Điểm kiểm tra</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Quản lý Ngữ cảnh</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
10
locales/zh-CN/README.md
generated
10
locales/zh-CN/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ Roo Code 适应您的工作方式,而不是相反:
|
|||
|
||||
<div align="center">
|
||||
|
||||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>安装 Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>配置个人资料</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>代码库索引</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自定义模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>检查点</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>待办事项列表</b> |
|
||||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>安装 Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>配置个人资料</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>代码库索引</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自定义模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>检查点</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>上下文管理</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
4
locales/zh-TW/README.md
generated
4
locales/zh-TW/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Roo Code 適應您的工作方式,而不是相反:
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>安裝 Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>設定設定檔</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>程式碼庫索引</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自訂模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>檢查點</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>待辦事項清單</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自訂模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>檢查點</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>上下文管理</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@vscode/ripgrep"
|
||||
],
|
||||
"overrides": {
|
||||
"tar-fs": ">=3.1.1",
|
||||
"esbuild": ">=0.25.0",
|
||||
|
|
|
|||
|
|
@ -331,10 +331,15 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
|
|||
|
||||
await this.storeCredentials(credentials)
|
||||
|
||||
// Store the provider model if provided
|
||||
// Store the provider model if provided, or flag that no model was selected
|
||||
if (providerModel) {
|
||||
await this.context.globalState.update("roo-provider-model", providerModel)
|
||||
await this.context.globalState.update("roo-auth-skip-model", undefined)
|
||||
this.log(`[auth] Stored provider model: ${providerModel}`)
|
||||
} else {
|
||||
// No model was selected during signup - flag this for the webview
|
||||
await this.context.globalState.update("roo-auth-skip-model", true)
|
||||
this.log(`[auth] No provider model selected during signup`)
|
||||
}
|
||||
|
||||
const vscode = await importVscode()
|
||||
|
|
|
|||
|
|
@ -395,9 +395,38 @@ describe("WebAuthService", () => {
|
|||
await authService.handleCallback("auth-code", storedState, null, "xai/grok-code-fast-1")
|
||||
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("roo-provider-model", "xai/grok-code-fast-1")
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("roo-auth-skip-model", undefined)
|
||||
expect(mockLog).toHaveBeenCalledWith("[auth] Stored provider model: xai/grok-code-fast-1")
|
||||
})
|
||||
|
||||
it("should set skip model flag when provider model is NOT provided in callback", async () => {
|
||||
const storedState = "valid-state"
|
||||
mockContext.globalState.get.mockReturnValue(storedState)
|
||||
|
||||
// Mock successful Clerk sign-in response
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
response: { created_session_id: "session-123" },
|
||||
}),
|
||||
headers: {
|
||||
get: (header: string) => (header === "authorization" ? "Bearer token-123" : null),
|
||||
},
|
||||
}
|
||||
mockFetch.mockResolvedValue(mockResponse)
|
||||
|
||||
const vscode = await import("vscode")
|
||||
const mockShowInfo = vi.fn()
|
||||
vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
|
||||
|
||||
// Call without provider model
|
||||
await authService.handleCallback("auth-code", storedState, null)
|
||||
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("roo-auth-skip-model", true)
|
||||
expect(mockLog).toHaveBeenCalledWith("[auth] No provider model selected during signup")
|
||||
})
|
||||
|
||||
it("should handle Clerk API errors", async () => {
|
||||
const storedState = "valid-state"
|
||||
mockContext.globalState.get.mockReturnValue(storedState)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import fs from "fs"
|
|||
import os from "os"
|
||||
import path from "path"
|
||||
|
||||
import { getEsbuildScriptPath, runEsbuild } from "../esbuild-runner.js"
|
||||
import { getEsbuildScriptPath, runEsbuild, NODE_BUILTIN_MODULES, COMMONJS_REQUIRE_BANNER } from "../esbuild-runner.js"
|
||||
|
||||
describe("getEsbuildScriptPath", () => {
|
||||
it("should find esbuild-wasm script in node_modules in development", () => {
|
||||
|
|
@ -153,4 +153,101 @@ describe("runEsbuild", () => {
|
|||
// File should be created successfully.
|
||||
expect(fs.existsSync(outputFile)).toBe(true)
|
||||
}, 30000)
|
||||
|
||||
it("should keep external modules as imports instead of bundling", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
// Write code that imports fs (a Node.js built-in).
|
||||
fs.writeFileSync(
|
||||
inputFile,
|
||||
`
|
||||
import fs from "fs"
|
||||
export function fileExists(p: string): boolean {
|
||||
return fs.existsSync(p)
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
bundle: true,
|
||||
external: ["fs"],
|
||||
})
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// fs should remain as an import, not bundled.
|
||||
expect(outputContent).toMatch(/import.*from\s*["']fs["']/)
|
||||
}, 30000)
|
||||
|
||||
it("should add banner code when specified", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const greeting = "Hello"`)
|
||||
|
||||
const customBanner = "// This is a custom banner comment"
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
banner: customBanner,
|
||||
})
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// Banner should be at the start of the file.
|
||||
expect(outputContent.startsWith(customBanner)).toBe(true)
|
||||
}, 30000)
|
||||
|
||||
it("should add CommonJS require shim banner for ESM bundles", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const value = 42`)
|
||||
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
banner: COMMONJS_REQUIRE_BANNER,
|
||||
})
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// Should contain the createRequire shim.
|
||||
expect(outputContent).toContain("createRequire")
|
||||
expect(outputContent).toContain("import.meta.url")
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe("NODE_BUILTIN_MODULES", () => {
|
||||
it("should include common Node.js built-in modules", () => {
|
||||
expect(NODE_BUILTIN_MODULES).toContain("fs")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("path")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("crypto")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("http")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("https")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("os")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("child_process")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("stream")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("util")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("events")
|
||||
})
|
||||
|
||||
it("should be an array of strings", () => {
|
||||
expect(Array.isArray(NODE_BUILTIN_MODULES)).toBe(true)
|
||||
expect(NODE_BUILTIN_MODULES.every((m) => typeof m === "string")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("COMMONJS_REQUIRE_BANNER", () => {
|
||||
it("should provide createRequire shim", () => {
|
||||
expect(COMMONJS_REQUIRE_BANNER).toContain("createRequire")
|
||||
expect(COMMONJS_REQUIRE_BANNER).toContain("import.meta.url")
|
||||
})
|
||||
|
||||
it("should define require variable", () => {
|
||||
expect(COMMONJS_REQUIRE_BANNER).toMatch(/var require\s*=/)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type { CustomToolDefinition, SerializedCustomToolDefinition, CustomToolPa
|
|||
|
||||
import type { StoredCustomTool, LoadResult } from "./types.js"
|
||||
import { serializeCustomTool } from "./serialize.js"
|
||||
import { runEsbuild } from "./esbuild-runner.js"
|
||||
import { runEsbuild, NODE_BUILTIN_MODULES, COMMONJS_REQUIRE_BANNER } from "./esbuild-runner.js"
|
||||
|
||||
export interface RegistryOptions {
|
||||
/** Directory for caching compiled TypeScript files. */
|
||||
|
|
@ -236,16 +236,22 @@ export class CustomToolRegistry {
|
|||
|
||||
/**
|
||||
* Clear the TypeScript compilation cache (both in-memory and on disk).
|
||||
* This removes all tool-specific subdirectories and their contents.
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.tsCache.clear()
|
||||
|
||||
if (fs.existsSync(this.cacheDir)) {
|
||||
try {
|
||||
const files = fs.readdirSync(this.cacheDir)
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".mjs")) {
|
||||
fs.unlinkSync(path.join(this.cacheDir, file))
|
||||
const entries = fs.readdirSync(this.cacheDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(this.cacheDir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
// Remove tool-specific subdirectory and all its contents.
|
||||
fs.rmSync(entryPath, { recursive: true, force: true })
|
||||
} else if (entry.name.endsWith(".mjs")) {
|
||||
// Also clean up any legacy flat .mjs files from older cache format.
|
||||
fs.unlinkSync(entryPath)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -259,6 +265,11 @@ export class CustomToolRegistry {
|
|||
/**
|
||||
* Dynamically import a TypeScript or JavaScript file.
|
||||
* TypeScript files are transpiled on-the-fly using esbuild.
|
||||
*
|
||||
* For TypeScript files, esbuild bundles the code with these considerations:
|
||||
* - Node.js built-in modules (fs, path, etc.) are kept external
|
||||
* - npm packages are bundled with a CommonJS shim for require() compatibility
|
||||
* - The tool's local node_modules is included in the resolution path
|
||||
*/
|
||||
private async import(filePath: string): Promise<Record<string, CustomToolDefinition>> {
|
||||
const absolutePath = path.resolve(filePath)
|
||||
|
|
@ -277,11 +288,13 @@ export class CustomToolRegistry {
|
|||
return import(`file://${cachedPath}`)
|
||||
}
|
||||
|
||||
// Ensure cache directory exists.
|
||||
fs.mkdirSync(this.cacheDir, { recursive: true })
|
||||
|
||||
const hash = createHash("sha256").update(cacheKey).digest("hex").slice(0, 16)
|
||||
const tempFile = path.join(this.cacheDir, `${hash}.mjs`)
|
||||
|
||||
// Use a tool-specific subdirectory to avoid .env file conflicts between tools.
|
||||
const toolCacheDir = path.join(this.cacheDir, hash)
|
||||
fs.mkdirSync(toolCacheDir, { recursive: true })
|
||||
|
||||
const tempFile = path.join(toolCacheDir, "bundle.mjs")
|
||||
|
||||
// Check if we have a cached version on disk (from a previous run/instance).
|
||||
if (fs.existsSync(tempFile)) {
|
||||
|
|
@ -289,7 +302,17 @@ export class CustomToolRegistry {
|
|||
return import(`file://${tempFile}`)
|
||||
}
|
||||
|
||||
// Get the tool's directory to include its node_modules in resolution path.
|
||||
const toolDir = path.dirname(absolutePath)
|
||||
const toolNodeModules = path.join(toolDir, "node_modules")
|
||||
|
||||
// Combine default nodePaths with tool-specific node_modules.
|
||||
// Tool's node_modules takes priority (listed first).
|
||||
const nodePaths = fs.existsSync(toolNodeModules) ? [toolNodeModules, ...this.nodePaths] : this.nodePaths
|
||||
|
||||
// Bundle the TypeScript file with dependencies using esbuild CLI.
|
||||
// - Node.js built-ins are external (they can't be bundled and are always available)
|
||||
// - npm packages are bundled with CommonJS require() shim for compatibility
|
||||
await runEsbuild(
|
||||
{
|
||||
entryPoint: absolutePath,
|
||||
|
|
@ -300,15 +323,54 @@ export class CustomToolRegistry {
|
|||
bundle: true,
|
||||
sourcemap: "inline",
|
||||
packages: "bundle",
|
||||
nodePaths: this.nodePaths,
|
||||
nodePaths,
|
||||
external: NODE_BUILTIN_MODULES,
|
||||
banner: COMMONJS_REQUIRE_BANNER,
|
||||
},
|
||||
this.extensionPath,
|
||||
)
|
||||
|
||||
// Copy .env files from the tool's source directory to the tool-specific cache directory.
|
||||
// This allows tools that use dotenv with __dirname to find their .env files,
|
||||
// while ensuring different tools' .env files don't overwrite each other.
|
||||
this.copyEnvFiles(toolDir, toolCacheDir)
|
||||
|
||||
this.tsCache.set(cacheKey, tempFile)
|
||||
return import(`file://${tempFile}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy .env files from the tool's source directory to the tool-specific cache directory.
|
||||
* This allows tools that use dotenv with __dirname to find their .env files,
|
||||
* while ensuring different tools' .env files don't overwrite each other.
|
||||
*
|
||||
* @param toolDir - The directory containing the tool source files
|
||||
* @param destDir - The tool-specific cache directory to copy .env files to
|
||||
*/
|
||||
private copyEnvFiles(toolDir: string, destDir: string): void {
|
||||
try {
|
||||
const files = fs.readdirSync(toolDir)
|
||||
const envFiles = files.filter((f) => f === ".env" || f.startsWith(".env."))
|
||||
|
||||
for (const envFile of envFiles) {
|
||||
const srcPath = path.join(toolDir, envFile)
|
||||
const destPath = path.join(destDir, envFile)
|
||||
|
||||
// Only copy if source is a file (not a directory).
|
||||
const stat = fs.statSync(srcPath)
|
||||
if (stat.isFile()) {
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
console.log(`[CustomToolRegistry] copied ${envFile} to tool cache directory`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Non-fatal: log but don't fail if we can't copy env files.
|
||||
console.warn(
|
||||
`[CustomToolRegistry] failed to copy .env files: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a Zod schema by looking for the _def property
|
||||
* which is present on all Zod types.
|
||||
|
|
|
|||
|
|
@ -11,9 +11,27 @@
|
|||
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import { builtinModules } from "module"
|
||||
import { fileURLToPath } from "url"
|
||||
import { execa } from "execa"
|
||||
|
||||
/**
|
||||
* Node.js built-in modules that should never be bundled.
|
||||
* These are always available in Node.js runtime and bundling them causes issues.
|
||||
*
|
||||
* Uses Node.js's authoritative list from `module.builtinModules` and adds
|
||||
* the `node:` prefixed versions for comprehensive coverage.
|
||||
*/
|
||||
export const NODE_BUILTIN_MODULES: readonly string[] = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)]
|
||||
|
||||
/**
|
||||
* Banner code to add to bundled output.
|
||||
* This provides a CommonJS-compatible `require` function for ESM bundles,
|
||||
* which is needed when bundled npm packages use `require()` internally.
|
||||
*/
|
||||
export const COMMONJS_REQUIRE_BANNER = `import { createRequire as __roo_createRequire } from 'module';
|
||||
var require = __roo_createRequire(import.meta.url);`
|
||||
|
||||
// Get the directory where this module is located.
|
||||
function getModuleDir(): string | undefined {
|
||||
try {
|
||||
|
|
@ -50,6 +68,10 @@ export interface EsbuildOptions {
|
|||
packages?: "bundle" | "external"
|
||||
/** Additional paths for module resolution */
|
||||
nodePaths?: string[]
|
||||
/** Modules to exclude from bundling (resolved at runtime) */
|
||||
external?: readonly string[]
|
||||
/** JavaScript code to prepend to the output bundle */
|
||||
banner?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -158,6 +180,18 @@ export async function runEsbuild(options: EsbuildOptions, extensionPath?: string
|
|||
args.push(`--packages=${options.packages}`)
|
||||
}
|
||||
|
||||
// Add external modules - these won't be bundled and will be resolved at runtime.
|
||||
if (options.external && options.external.length > 0) {
|
||||
for (const ext of options.external) {
|
||||
args.push(`--external:${ext}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add banner code (e.g., for CommonJS require shim in ESM bundles).
|
||||
if (options.banner) {
|
||||
args.push(`--banner:js=${options.banner}`)
|
||||
}
|
||||
|
||||
// Build environment with NODE_PATH for module resolution.
|
||||
const env: NodeJS.ProcessEnv = { ...process.env }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
FROM node:20-slim AS base
|
||||
# Build with:
|
||||
# docker compose -f packages/evals/docker-compose.yml build runner
|
||||
|
||||
# Install pnpm
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN npm install -g npm@latest npm-run-all
|
||||
# Test with:
|
||||
# docker compose -f packages/evals/docker-compose.yml run --rm runner bash
|
||||
|
||||
# Install system packages
|
||||
RUN apt update && \
|
||||
apt install -y \
|
||||
FROM debian:bookworm-slim AS base
|
||||
|
||||
# Install system packages (excluding language runtimes - those come from mise)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
vim \
|
||||
|
|
@ -22,18 +22,13 @@ RUN apt update && \
|
|||
gpg \
|
||||
xvfb \
|
||||
cmake \
|
||||
golang-go \
|
||||
default-jre \
|
||||
python3 \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker cli
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||
&& apt update && apt install -y docker-ce-cli \
|
||||
&& apt-get update && apt-get install -y docker-ce-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install VS Code
|
||||
|
|
@ -41,15 +36,43 @@ RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor
|
|||
&& install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg \
|
||||
&& echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | tee /etc/apt/sources.list.d/vscode.list > /dev/null \
|
||||
&& rm -f packages.microsoft.gpg \
|
||||
&& apt update && apt install -y code \
|
||||
&& apt-get update && apt-get install -y code \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /roo
|
||||
|
||||
# Install rust
|
||||
ARG RUST_VERSION=1.87.0
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${RUST_VERSION} \
|
||||
&& echo 'source $HOME/.cargo/env' >> $HOME/.bashrc
|
||||
# Install mise (https://mise.jdx.dev) for language runtime management
|
||||
RUN curl https://mise.run | sh \
|
||||
&& /root/.local/bin/mise --version
|
||||
|
||||
# Set up mise environment
|
||||
ENV MISE_DATA_DIR="/root/.local/share/mise"
|
||||
ENV PATH="/root/.local/share/mise/shims:/root/.local/bin:$PATH"
|
||||
|
||||
# Define language runtime versions (matching setup.sh)
|
||||
ARG NODE_VERSION=20.19.2
|
||||
ARG PYTHON_VERSION=3.13.2
|
||||
ARG GO_VERSION=1.24.2
|
||||
ARG RUST_VERSION=1.85.1
|
||||
ARG JAVA_VERSION=openjdk-17
|
||||
ARG UV_VERSION=0.7.11
|
||||
|
||||
# Install language runtimes via mise
|
||||
RUN mise use --global node@${NODE_VERSION} \
|
||||
&& mise use --global python@${PYTHON_VERSION} \
|
||||
&& mise use --global go@${GO_VERSION} \
|
||||
&& mise use --global rust@${RUST_VERSION} \
|
||||
&& mise use --global java@${JAVA_VERSION} \
|
||||
&& mise use --global uv@${UV_VERSION} \
|
||||
&& mise reshim
|
||||
|
||||
# Verify installations
|
||||
RUN node --version && python --version && go version && rustc --version && java --version && uv --version
|
||||
|
||||
# Install pnpm (after node is available from mise)
|
||||
ENV PNPM_HOME="/root/.local/share/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN npm install -g pnpm npm-run-all
|
||||
|
||||
# Install VS Code extensions
|
||||
ARG GOLANG_EXT_VERSION=0.46.1
|
||||
|
|
@ -72,17 +95,20 @@ RUN git clone ${EVALS_REPO_URL} evals \
|
|||
&& cd evals \
|
||||
&& git checkout ${EVALS_COMMIT}
|
||||
|
||||
# Install uv and sync python dependencies
|
||||
ARG UV_VERSION=0.7.11
|
||||
# Pre-warm Gradle wrapper cache (./gradlew downloads its own Gradle regardless of system install).
|
||||
# Find a Java project with gradlew and run it to cache the distribution.
|
||||
RUN find /roo/evals -name "gradlew" -type f | head -1 | xargs -I {} sh -c 'cd $(dirname {}) && ./gradlew --version'
|
||||
|
||||
# Sync python dependencies for evals
|
||||
WORKDIR /roo/evals/python
|
||||
RUN curl -LsSf https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh | sh \
|
||||
&& /root/.local/bin/uv sync
|
||||
RUN uv sync
|
||||
|
||||
WORKDIR /roo/repo
|
||||
|
||||
# Install npm packages
|
||||
RUN mkdir -p \
|
||||
scripts \
|
||||
apps/cli \
|
||||
packages/build \
|
||||
packages/config-eslint \
|
||||
packages/config-typescript \
|
||||
|
|
@ -92,6 +118,7 @@ RUN mkdir -p \
|
|||
packages/telemetry \
|
||||
packages/types \
|
||||
packages/cloud \
|
||||
packages/vscode-shim \
|
||||
src \
|
||||
webview-ui
|
||||
|
||||
|
|
@ -99,6 +126,7 @@ COPY ./package.json ./
|
|||
COPY ./pnpm-lock.yaml ./
|
||||
COPY ./pnpm-workspace.yaml ./
|
||||
COPY ./scripts/bootstrap.mjs ./scripts/
|
||||
COPY ./apps/cli/package.json ./apps/cli/
|
||||
COPY ./packages/build/package.json ./packages/build/
|
||||
COPY ./packages/config-eslint/package.json ./packages/config-eslint/
|
||||
COPY ./packages/config-typescript/package.json ./packages/config-typescript/
|
||||
|
|
@ -108,6 +136,7 @@ COPY ./packages/ipc/package.json ./packages/ipc/
|
|||
COPY ./packages/telemetry/package.json ./packages/telemetry/
|
||||
COPY ./packages/types/package.json ./packages/types/
|
||||
COPY ./packages/cloud/package.json ./packages/cloud/
|
||||
COPY ./packages/vscode-shim/package.json ./packages/vscode-shim/
|
||||
COPY ./src/package.json ./src/
|
||||
COPY ./webview-ui/package.json ./webview-ui/
|
||||
|
||||
|
|
@ -128,10 +157,15 @@ COPY packages/evals/.env.local ./packages/evals/
|
|||
# Copy the pre-installed VS Code extensions
|
||||
RUN cp -r /roo/.vscode-template /roo/.vscode
|
||||
|
||||
# Build the Roo Code extension
|
||||
# Build the Roo Code extension (for VSCode execution method)
|
||||
RUN pnpm vsix -- --out ../bin/roo-code.vsix \
|
||||
&& yes | code --no-sandbox --user-data-dir /roo/.vscode --install-extension bin/roo-code.vsix
|
||||
|
||||
# Build the extension bundle and CLI (for CLI execution method)
|
||||
# The CLI requires the extension bundle (src/dist/extension.js) and the CLI build (apps/cli/dist/index.js)
|
||||
RUN pnpm --filter roo-cline bundle \
|
||||
&& pnpm --filter @roo-code/cli build
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY packages/evals/.docker/entrypoints/runner.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { MessageLogDeduper } from "./messageLogDeduper.js"
|
||||
import { MessageLogDeduper } from "../messageLogDeduper.js"
|
||||
|
||||
describe("MessageLogDeduper", () => {
|
||||
it("dedupes identical messages for same action+ts", () => {
|
||||
|
|
@ -6,7 +6,7 @@ import { EVALS_REPO_PATH } from "../exercises/index.js"
|
|||
|
||||
import { runCi } from "./runCi.js"
|
||||
import { runEvals } from "./runEvals.js"
|
||||
import { processTask } from "./runTask.js"
|
||||
import { processTask } from "./processTask.js"
|
||||
|
||||
const main = async () => {
|
||||
await run(
|
||||
|
|
|
|||
150
packages/evals/src/cli/processTask.ts
Normal file
150
packages/evals/src/cli/processTask.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { execa } from "execa"
|
||||
|
||||
import { type TaskEvent, RooCodeEventName } from "@roo-code/types"
|
||||
|
||||
import { findRun, findTask, updateTask } from "../db/index.js"
|
||||
|
||||
import { Logger, getTag, isDockerContainer } from "./utils.js"
|
||||
import { redisClient, getPubSubKey, registerRunner, deregisterRunner } from "./redis.js"
|
||||
import { runUnitTest } from "./runUnitTest.js"
|
||||
import { runTaskWithCli } from "./runTaskInCli.js"
|
||||
import { runTaskInVscode } from "./runTaskInVscode.js"
|
||||
|
||||
export const processTask = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger?: Logger
|
||||
}) => {
|
||||
const task = await findTask(taskId)
|
||||
const { language, exercise } = task
|
||||
const run = await findRun(task.runId)
|
||||
await registerRunner({ runId: run.id, taskId, timeoutSeconds: (run.timeout || 5) * 60 })
|
||||
|
||||
const containerized = isDockerContainer()
|
||||
|
||||
logger =
|
||||
logger ||
|
||||
new Logger({
|
||||
logDir: containerized ? `/var/log/evals/runs/${run.id}` : `/tmp/evals/runs/${run.id}`,
|
||||
filename: `${language}-${exercise}.log`,
|
||||
tag: getTag("runTask", { run, task }),
|
||||
})
|
||||
|
||||
try {
|
||||
const publish = async (e: TaskEvent) => {
|
||||
const redis = await redisClient()
|
||||
await redis.publish(getPubSubKey(run.id), JSON.stringify(e))
|
||||
}
|
||||
|
||||
const executionMethod = run.executionMethod || "vscode"
|
||||
logger.info(`running task ${task.id} (${language}/${exercise}) via ${executionMethod}...`)
|
||||
|
||||
if (executionMethod === "cli") {
|
||||
await runTaskWithCli({ run, task, jobToken, publish, logger })
|
||||
} else {
|
||||
await runTaskInVscode({ run, task, jobToken, publish, logger })
|
||||
}
|
||||
|
||||
logger.info(`testing task ${task.id} (${language}/${exercise})...`)
|
||||
const passed = await runUnitTest({ task, logger })
|
||||
|
||||
logger.info(`task ${task.id} (${language}/${exercise}) -> ${passed}`)
|
||||
await updateTask(task.id, { passed })
|
||||
|
||||
await publish({
|
||||
eventName: passed ? RooCodeEventName.EvalPass : RooCodeEventName.EvalFail,
|
||||
taskId: task.id,
|
||||
})
|
||||
} finally {
|
||||
await deregisterRunner({ runId: run.id, taskId })
|
||||
}
|
||||
}
|
||||
|
||||
export const processTaskInContainer = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
maxRetries = 10,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger: Logger
|
||||
maxRetries?: number
|
||||
}) => {
|
||||
const baseArgs = [
|
||||
"--rm",
|
||||
"--network evals_default",
|
||||
"-v /var/run/docker.sock:/var/run/docker.sock",
|
||||
"-v /tmp/evals:/var/log/evals",
|
||||
"-e HOST_EXECUTION_METHOD=docker",
|
||||
]
|
||||
|
||||
if (jobToken) {
|
||||
baseArgs.push(`-e ROO_CODE_CLOUD_TOKEN=${jobToken}`)
|
||||
}
|
||||
|
||||
// Pass API keys to the container so the CLI can authenticate
|
||||
const apiKeyEnvVars = [
|
||||
"OPENROUTER_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"GOOGLE_API_KEY",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"MISTRAL_API_KEY",
|
||||
]
|
||||
|
||||
for (const envVar of apiKeyEnvVars) {
|
||||
if (process.env[envVar]) {
|
||||
baseArgs.push(`-e ${envVar}=${process.env[envVar]}`)
|
||||
}
|
||||
}
|
||||
|
||||
const command = `pnpm --filter @roo-code/evals cli --taskId ${taskId}`
|
||||
logger.info(command)
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const containerName = `evals-task-${taskId}.${attempt}`
|
||||
const args = [`--name ${containerName}`, `-e EVALS_ATTEMPT=${attempt}`, ...baseArgs]
|
||||
const isRetry = attempt > 0
|
||||
|
||||
if (isRetry) {
|
||||
const delayMs = Math.pow(2, attempt - 1) * 1000 * (0.5 + Math.random())
|
||||
logger.info(`retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`${isRetry ? "retrying" : "executing"} container command (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
|
||||
const subprocess = execa(`docker run ${args.join(" ")} evals-runner sh -c "${command}"`, { shell: true })
|
||||
// subprocess.stdout?.on("data", (data) => console.log(data.toString()))
|
||||
// subprocess.stderr?.on("data", (data) => console.error(data.toString()))
|
||||
|
||||
try {
|
||||
const result = await subprocess
|
||||
logger.info(`container process completed with exit code: ${result.exitCode}`)
|
||||
return
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "exitCode" in error) {
|
||||
logger.error(
|
||||
`container process failed with exit code: ${error.exitCode} (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
} else {
|
||||
logger.error(`container process failed with error: ${error} (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
}
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`all ${maxRetries + 1} attempts failed, giving up`)
|
||||
|
||||
// TODO: Mark task as failed.
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { EVALS_REPO_PATH } from "../exercises/index.js"
|
|||
|
||||
import { Logger, getTag, isDockerContainer, resetEvalsRepo, commitEvalsRepoChanges } from "./utils.js"
|
||||
import { startHeartbeat, stopHeartbeat } from "./redis.js"
|
||||
import { processTask, processTaskInContainer } from "./runTask.js"
|
||||
import { processTask, processTaskInContainer } from "./processTask.js"
|
||||
|
||||
export const runEvals = async (runId: number) => {
|
||||
const run = await findRun(runId)
|
||||
|
|
@ -53,13 +53,18 @@ export const runEvals = async (runId: number) => {
|
|||
}
|
||||
|
||||
try {
|
||||
// Add tasks with staggered start times when concurrency > 1
|
||||
// Add tasks with staggered start times when concurrency > 1.
|
||||
for (let i = 0; i < filteredTasks.length; i++) {
|
||||
const task = filteredTasks[i]
|
||||
if (!task) continue
|
||||
|
||||
if (!task) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (run.concurrency > 1 && i > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, STAGGER_DELAY_MS))
|
||||
}
|
||||
|
||||
queue.add(createTaskRunner(task))
|
||||
}
|
||||
|
||||
|
|
|
|||
313
packages/evals/src/cli/runTaskInCli.ts
Normal file
313
packages/evals/src/cli/runTaskInCli.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "node:os"
|
||||
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { execa } from "execa"
|
||||
|
||||
import { type ToolUsage, TaskCommandName, RooCodeEventName, IpcMessageType } from "@roo-code/types"
|
||||
import { IpcClient } from "@roo-code/ipc"
|
||||
|
||||
import { updateTask, createTaskMetrics, updateTaskMetrics, createToolError } from "../db/index.js"
|
||||
import { EVALS_REPO_PATH } from "../exercises/index.js"
|
||||
|
||||
import { type RunTaskOptions } from "./types.js"
|
||||
import { mergeToolUsage, waitForSubprocessWithTimeout } from "./utils.js"
|
||||
|
||||
/**
|
||||
* Run a task using the Roo Code CLI (headless mode).
|
||||
* Uses the same IPC protocol as VSCode since the CLI loads the same extension bundle.
|
||||
*/
|
||||
export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
||||
const { language, exercise } = task
|
||||
const prompt = fs.readFileSync(path.resolve(EVALS_REPO_PATH, `prompts/${language}.md`), "utf-8")
|
||||
const workspacePath = path.resolve(EVALS_REPO_PATH, language, exercise)
|
||||
const ipcSocketPath = path.resolve(os.tmpdir(), `evals-cli-${run.id}-${task.id}.sock`)
|
||||
|
||||
const env: Record<string, string> = {
|
||||
...(process.env as Record<string, string>),
|
||||
ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath,
|
||||
}
|
||||
|
||||
if (jobToken) {
|
||||
env.ROO_CODE_CLOUD_TOKEN = jobToken
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const cancelSignal = controller.signal
|
||||
|
||||
const cliArgs = [
|
||||
"--filter",
|
||||
"@roo-code/cli",
|
||||
"start",
|
||||
"--yes",
|
||||
"--exit-on-complete",
|
||||
"--reasoning-effort",
|
||||
"disabled",
|
||||
"--workspace",
|
||||
workspacePath,
|
||||
]
|
||||
|
||||
if (run.settings?.mode) {
|
||||
cliArgs.push("-M", run.settings.mode)
|
||||
}
|
||||
|
||||
if (run.settings?.apiProvider) {
|
||||
cliArgs.push("-p", run.settings.apiProvider)
|
||||
}
|
||||
|
||||
const modelId = run.settings?.apiModelId || run.settings?.openRouterModelId
|
||||
|
||||
if (modelId) {
|
||||
cliArgs.push("-m", modelId)
|
||||
}
|
||||
|
||||
cliArgs.push(prompt)
|
||||
|
||||
logger.info(`CLI command: pnpm ${cliArgs.join(" ")}`)
|
||||
|
||||
const subprocess = execa("pnpm", cliArgs, { env, cancelSignal, cwd: process.cwd() })
|
||||
|
||||
// Buffer for accumulating streaming output until we have complete lines.
|
||||
let stdoutBuffer = ""
|
||||
let stderrBuffer = ""
|
||||
|
||||
// Track subprocess exit code - with -x flag the CLI exits immediately after task completion.
|
||||
let subprocessExitCode: number | null = null
|
||||
|
||||
// Pipe CLI stdout/stderr to the logger for easier debugging.
|
||||
// Buffer output and only log complete lines to avoid fragmented token-by-token logging.
|
||||
// Use logger.raw() to output without the verbose prefix (timestamp, tag, etc).
|
||||
subprocess.stdout?.on("data", (data: Buffer) => {
|
||||
stdoutBuffer += data.toString()
|
||||
const lines = stdoutBuffer.split("\n")
|
||||
|
||||
// Keep the last incomplete line in the buffer.
|
||||
stdoutBuffer = lines.pop() || ""
|
||||
|
||||
// Log all complete lines without the verbose prefix.
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
logger.raw(line)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
subprocess.stderr?.on("data", (data: Buffer) => {
|
||||
stderrBuffer += data.toString()
|
||||
const lines = stderrBuffer.split("\n")
|
||||
|
||||
// Keep the last incomplete line in the buffer.
|
||||
stderrBuffer = lines.pop() || ""
|
||||
|
||||
// Log all complete lines without the verbose prefix.
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
logger.raw(line)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Log any remaining buffered output when the subprocess exits.
|
||||
subprocess.on("exit", (code) => {
|
||||
subprocessExitCode = code
|
||||
|
||||
if (stdoutBuffer.trim()) {
|
||||
logger.raw(stdoutBuffer)
|
||||
}
|
||||
|
||||
if (stderrBuffer.trim()) {
|
||||
logger.raw(stderrBuffer)
|
||||
}
|
||||
})
|
||||
|
||||
// Give CLI some time to start and create IPC server.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
||||
|
||||
let client: IpcClient | undefined = undefined
|
||||
let attempts = 10 // More attempts for CLI startup.
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
client = new IpcClient(ipcSocketPath)
|
||||
await pWaitFor(() => client!.isReady, { interval: 500, timeout: 2_000 })
|
||||
break
|
||||
} catch (_error) {
|
||||
client?.disconnect()
|
||||
attempts--
|
||||
|
||||
if (attempts <= 0) {
|
||||
logger.error(`unable to connect to IPC socket -> ${ipcSocketPath}`)
|
||||
throw new Error("Unable to connect to CLI IPC socket.")
|
||||
}
|
||||
|
||||
// Wait a bit before retrying.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||
}
|
||||
}
|
||||
|
||||
// For CLI mode, we need to create taskMetrics immediately because the CLI starts
|
||||
// the task right away (from command line args). By the time we connect to IPC,
|
||||
// the TaskStarted event may have already been sent and missed.
|
||||
// This is different from VSCode mode where we send StartNewTask via IPC and can
|
||||
// reliably receive TaskStarted.
|
||||
const taskMetrics = await createTaskMetrics({
|
||||
cost: 0,
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
tokensContext: 0,
|
||||
duration: 0,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
})
|
||||
|
||||
await updateTask(task.id, { taskMetricsId: taskMetrics.id, startedAt: new Date() })
|
||||
logger.info(`created taskMetrics with id ${taskMetrics.id}`)
|
||||
|
||||
// The rest of the logic handles IPC events for metrics updates.
|
||||
let taskStartedAt = Date.now()
|
||||
let taskFinishedAt: number | undefined
|
||||
let taskAbortedAt: number | undefined
|
||||
let taskTimedOut: boolean = false
|
||||
const taskMetricsId = taskMetrics.id // Already set, no need to wait for TaskStarted.
|
||||
let rooTaskId: string | undefined
|
||||
let isClientDisconnected = false
|
||||
const accumulatedToolUsage: ToolUsage = {}
|
||||
|
||||
// For CLI mode, we don't need verbose IPC message logging since we're logging stdout instead.
|
||||
// We only track what's needed for metrics and task state management.
|
||||
const ignoreEventsForBroadcast = [RooCodeEventName.Message]
|
||||
let isApiUnstable = false
|
||||
|
||||
client.on(IpcMessageType.TaskEvent, async (taskEvent) => {
|
||||
const { eventName, payload } = taskEvent
|
||||
|
||||
// Track API instability for retry logic.
|
||||
if (
|
||||
eventName === RooCodeEventName.Message &&
|
||||
payload[0].message.say &&
|
||||
["api_req_retry_delayed", "api_req_retried"].includes(payload[0].message.say)
|
||||
) {
|
||||
isApiUnstable = true
|
||||
}
|
||||
|
||||
// Publish events to Redis (except Message events) for the web UI.
|
||||
if (!ignoreEventsForBroadcast.includes(eventName)) {
|
||||
await publish({ ...taskEvent, taskId: task.id })
|
||||
}
|
||||
|
||||
// Handle task lifecycle events.
|
||||
// For CLI mode, we already created taskMetrics before connecting to IPC,
|
||||
// but we still want to capture the rooTaskId from TaskStarted if we receive it.
|
||||
if (eventName === RooCodeEventName.TaskStarted) {
|
||||
taskStartedAt = Date.now()
|
||||
rooTaskId = payload[0]
|
||||
logger.info(`received TaskStarted event, rooTaskId: ${rooTaskId}`)
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskToolFailed) {
|
||||
const [_taskId, toolName, error] = payload
|
||||
await createToolError({ taskId: task.id, toolName, error })
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskTokenUsageUpdated || eventName === RooCodeEventName.TaskCompleted) {
|
||||
// In CLI mode, taskMetricsId is always set before we register event handlers.
|
||||
const duration = Date.now() - taskStartedAt
|
||||
|
||||
const { totalCost, totalTokensIn, totalTokensOut, contextTokens, totalCacheWrites, totalCacheReads } =
|
||||
payload[1]
|
||||
|
||||
const incomingToolUsage: ToolUsage = payload[2] ?? {}
|
||||
mergeToolUsage(accumulatedToolUsage, incomingToolUsage)
|
||||
|
||||
await updateTaskMetrics(taskMetricsId, {
|
||||
cost: totalCost,
|
||||
tokensIn: totalTokensIn,
|
||||
tokensOut: totalTokensOut,
|
||||
tokensContext: contextTokens,
|
||||
duration,
|
||||
cacheWrites: totalCacheWrites ?? 0,
|
||||
cacheReads: totalCacheReads ?? 0,
|
||||
toolUsage: accumulatedToolUsage,
|
||||
})
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskAborted) {
|
||||
taskAbortedAt = Date.now()
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskCompleted) {
|
||||
taskFinishedAt = Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
client.on(IpcMessageType.Disconnect, async () => {
|
||||
logger.info(`disconnected from IPC socket -> ${ipcSocketPath}`)
|
||||
isClientDisconnected = true
|
||||
// Note: In CLI mode, we don't need to resolve taskMetricsReady since
|
||||
// taskMetrics is created synchronously before event handlers are registered.
|
||||
})
|
||||
|
||||
// Note: We do NOT send StartNewTask via IPC here because the CLI already
|
||||
// starts the task from its command line arguments. The IPC connection is
|
||||
// only used to receive events (TaskStarted, TaskCompleted, etc.) and metrics.
|
||||
// Sending StartNewTask here would start a SECOND task.
|
||||
|
||||
try {
|
||||
const timeoutMs = (run.timeout || 5) * 60 * 1_000
|
||||
|
||||
await pWaitFor(() => !!taskFinishedAt || !!taskAbortedAt || isClientDisconnected, {
|
||||
interval: 1_000,
|
||||
timeout: timeoutMs,
|
||||
})
|
||||
} catch (_error) {
|
||||
taskTimedOut = true
|
||||
logger.error("time limit reached")
|
||||
|
||||
if (rooTaskId && !isClientDisconnected) {
|
||||
logger.info("cancelling task")
|
||||
client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId })
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
||||
}
|
||||
|
||||
taskFinishedAt = Date.now()
|
||||
}
|
||||
|
||||
if (!taskFinishedAt && !taskTimedOut) {
|
||||
// With -x flag, CLI exits immediately after task completion, which can cause
|
||||
// IPC disconnection before we receive the TaskCompleted event.
|
||||
// If subprocess exited cleanly (code 0), treat as successful completion.
|
||||
if (subprocessExitCode === 0) {
|
||||
taskFinishedAt = Date.now()
|
||||
logger.info("subprocess exited cleanly (code 0), treating as task completion")
|
||||
} else {
|
||||
logger.error(`client disconnected before task finished (subprocess exit code: ${subprocessExitCode})`)
|
||||
throw new Error("Client disconnected before task completion.")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("setting task finished at")
|
||||
await updateTask(task.id, { finishedAt: new Date() })
|
||||
|
||||
if (rooTaskId && !isClientDisconnected) {
|
||||
logger.info("closing task")
|
||||
client.sendCommand({ commandName: TaskCommandName.CloseTask, data: rooTaskId })
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
||||
}
|
||||
|
||||
if (!isClientDisconnected) {
|
||||
logger.info("disconnecting client")
|
||||
client.disconnect()
|
||||
}
|
||||
|
||||
logger.info("waiting for subprocess to finish")
|
||||
controller.abort()
|
||||
|
||||
await waitForSubprocessWithTimeout({ subprocess, logger })
|
||||
|
||||
logger.close()
|
||||
|
||||
if (isApiUnstable && !taskFinishedAt) {
|
||||
throw new Error("API is unstable, throwing to trigger a retry.")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import * as fs from "fs"
|
||||
import * as fsp from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as os from "node:os"
|
||||
|
||||
|
|
@ -7,218 +6,23 @@ import pWaitFor from "p-wait-for"
|
|||
import { execa } from "execa"
|
||||
|
||||
import {
|
||||
type TaskEvent,
|
||||
type ClineSay,
|
||||
type ToolUsage,
|
||||
TaskCommandName,
|
||||
RooCodeEventName,
|
||||
IpcMessageType,
|
||||
EVALS_SETTINGS,
|
||||
type ToolUsage,
|
||||
} from "@roo-code/types"
|
||||
import { IpcClient } from "@roo-code/ipc"
|
||||
|
||||
import {
|
||||
type Run,
|
||||
type Task,
|
||||
findRun,
|
||||
findTask,
|
||||
updateTask,
|
||||
createTaskMetrics,
|
||||
updateTaskMetrics,
|
||||
createToolError,
|
||||
} from "../db/index.js"
|
||||
import { updateTask, createTaskMetrics, updateTaskMetrics, createToolError } from "../db/index.js"
|
||||
import { EVALS_REPO_PATH } from "../exercises/index.js"
|
||||
|
||||
import { Logger, getTag, isDockerContainer } from "./utils.js"
|
||||
import { redisClient, getPubSubKey, registerRunner, deregisterRunner } from "./redis.js"
|
||||
import { runUnitTest } from "./runUnitTest.js"
|
||||
import { type RunTaskOptions } from "./types.js"
|
||||
import { isDockerContainer, copyConversationHistory, mergeToolUsage, waitForSubprocessWithTimeout } from "./utils.js"
|
||||
import { MessageLogDeduper } from "./messageLogDeduper.js"
|
||||
|
||||
class SubprocessTimeoutError extends Error {
|
||||
constructor(timeout: number) {
|
||||
super(`Subprocess timeout after ${timeout}ms`)
|
||||
this.name = "SubprocessTimeoutError"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy conversation history files from VS Code extension storage to the log directory.
|
||||
* This allows us to preserve the api_conversation_history.json and ui_messages.json
|
||||
* files for post-mortem analysis alongside the log files.
|
||||
*/
|
||||
async function copyConversationHistory({
|
||||
rooTaskId,
|
||||
logDir,
|
||||
language,
|
||||
exercise,
|
||||
iteration,
|
||||
logger,
|
||||
}: {
|
||||
rooTaskId: string
|
||||
logDir: string
|
||||
language: string
|
||||
exercise: string
|
||||
iteration: number
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
// VS Code extension global storage path within the container
|
||||
const extensionStoragePath = "/roo/.vscode/User/globalStorage/rooveterinaryinc.roo-cline"
|
||||
const taskStoragePath = path.join(extensionStoragePath, "tasks", rooTaskId)
|
||||
|
||||
const filesToCopy = ["api_conversation_history.json", "ui_messages.json"]
|
||||
|
||||
for (const filename of filesToCopy) {
|
||||
const sourcePath = path.join(taskStoragePath, filename)
|
||||
// Use sanitized exercise name (replace slashes with dashes) for the destination filename
|
||||
// Include iteration number to handle multiple attempts at the same exercise
|
||||
const sanitizedExercise = exercise.replace(/\//g, "-")
|
||||
const destFilename = `${language}-${sanitizedExercise}.${iteration}_${filename}`
|
||||
const destPath = path.join(logDir, destFilename)
|
||||
|
||||
try {
|
||||
// Check if source file exists
|
||||
await fsp.access(sourcePath)
|
||||
|
||||
// Copy the file
|
||||
await fsp.copyFile(sourcePath, destPath)
|
||||
logger.info(`copied ${filename} to ${destPath}`)
|
||||
} catch (error) {
|
||||
// File may not exist if task didn't complete properly - this is not fatal
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
logger.info(`${filename} not found at ${sourcePath} - skipping`)
|
||||
} else {
|
||||
logger.error(`failed to copy ${filename}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const processTask = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger?: Logger
|
||||
}) => {
|
||||
const task = await findTask(taskId)
|
||||
const { language, exercise } = task
|
||||
const run = await findRun(task.runId)
|
||||
await registerRunner({ runId: run.id, taskId, timeoutSeconds: (run.timeout || 5) * 60 })
|
||||
|
||||
const containerized = isDockerContainer()
|
||||
|
||||
logger =
|
||||
logger ||
|
||||
new Logger({
|
||||
logDir: containerized ? `/var/log/evals/runs/${run.id}` : `/tmp/evals/runs/${run.id}`,
|
||||
filename: `${language}-${exercise}.log`,
|
||||
tag: getTag("runTask", { run, task }),
|
||||
})
|
||||
|
||||
try {
|
||||
const publish = async (e: TaskEvent) => {
|
||||
const redis = await redisClient()
|
||||
await redis.publish(getPubSubKey(run.id), JSON.stringify(e))
|
||||
}
|
||||
|
||||
logger.info(`running task ${task.id} (${language}/${exercise})...`)
|
||||
await runTask({ run, task, jobToken, publish, logger })
|
||||
|
||||
logger.info(`testing task ${task.id} (${language}/${exercise})...`)
|
||||
const passed = await runUnitTest({ task, logger })
|
||||
|
||||
logger.info(`task ${task.id} (${language}/${exercise}) -> ${passed}`)
|
||||
await updateTask(task.id, { passed })
|
||||
|
||||
await publish({
|
||||
eventName: passed ? RooCodeEventName.EvalPass : RooCodeEventName.EvalFail,
|
||||
taskId: task.id,
|
||||
})
|
||||
} finally {
|
||||
await deregisterRunner({ runId: run.id, taskId })
|
||||
}
|
||||
}
|
||||
|
||||
export const processTaskInContainer = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
maxRetries = 10,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger: Logger
|
||||
maxRetries?: number
|
||||
}) => {
|
||||
const baseArgs = [
|
||||
"--rm",
|
||||
"--network evals_default",
|
||||
"-v /var/run/docker.sock:/var/run/docker.sock",
|
||||
"-v /tmp/evals:/var/log/evals",
|
||||
"-e HOST_EXECUTION_METHOD=docker",
|
||||
]
|
||||
|
||||
if (jobToken) {
|
||||
baseArgs.push(`-e ROO_CODE_CLOUD_TOKEN=${jobToken}`)
|
||||
}
|
||||
|
||||
const command = `pnpm --filter @roo-code/evals cli --taskId ${taskId}`
|
||||
logger.info(command)
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const containerName = `evals-task-${taskId}.${attempt}`
|
||||
const args = [`--name ${containerName}`, `-e EVALS_ATTEMPT=${attempt}`, ...baseArgs]
|
||||
const isRetry = attempt > 0
|
||||
|
||||
if (isRetry) {
|
||||
const delayMs = Math.pow(2, attempt - 1) * 1000 * (0.5 + Math.random())
|
||||
logger.info(`retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`${isRetry ? "retrying" : "executing"} container command (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
|
||||
const subprocess = execa(`docker run ${args.join(" ")} evals-runner sh -c "${command}"`, { shell: true })
|
||||
// subprocess.stdout?.on("data", (data) => console.log(data.toString()))
|
||||
// subprocess.stderr?.on("data", (data) => console.error(data.toString()))
|
||||
|
||||
try {
|
||||
const result = await subprocess
|
||||
logger.info(`container process completed with exit code: ${result.exitCode}`)
|
||||
return
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "exitCode" in error) {
|
||||
logger.error(
|
||||
`container process failed with exit code: ${error.exitCode} (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
} else {
|
||||
logger.error(`container process failed with error: ${error} (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
}
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`all ${maxRetries + 1} attempts failed, giving up`)
|
||||
|
||||
// TODO: Mark task as failed.
|
||||
}
|
||||
|
||||
type RunTaskOptions = {
|
||||
run: Run
|
||||
task: Task
|
||||
jobToken: string | null
|
||||
publish: (taskEvent: TaskEvent) => Promise<void>
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
||||
export const runTaskInVscode = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
||||
const { language, exercise } = task
|
||||
const prompt = fs.readFileSync(path.resolve(EVALS_REPO_PATH, `prompts/${language}.md`), "utf-8")
|
||||
const workspacePath = path.resolve(EVALS_REPO_PATH, language, exercise)
|
||||
|
|
@ -301,6 +105,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
"diff_error",
|
||||
"condense_context",
|
||||
"condense_context_error",
|
||||
"api_req_rate_limit_wait",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_retried",
|
||||
]
|
||||
|
|
@ -409,24 +214,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
|
||||
// For both TaskTokenUsageUpdated and TaskCompleted: toolUsage is payload[2]
|
||||
const incomingToolUsage: ToolUsage = payload[2] ?? {}
|
||||
|
||||
// Merge incoming tool usage with accumulated data using MAX strategy.
|
||||
// This handles the case where a task is rehydrated after abort:
|
||||
// - Empty rehydrated data won't overwrite existing: max(5, 0) = 5
|
||||
// - Legitimate restart with additional work is captured: max(5, 8) = 8
|
||||
// Each task instance tracks its own cumulative values, so we take the max
|
||||
// to preserve the highest values seen across all instances.
|
||||
for (const [toolName, usage] of Object.entries(incomingToolUsage)) {
|
||||
const existing = accumulatedToolUsage[toolName as keyof ToolUsage]
|
||||
if (existing) {
|
||||
accumulatedToolUsage[toolName as keyof ToolUsage] = {
|
||||
attempts: Math.max(existing.attempts, usage.attempts),
|
||||
failures: Math.max(existing.failures, usage.failures),
|
||||
}
|
||||
} else {
|
||||
accumulatedToolUsage[toolName as keyof ToolUsage] = { ...usage }
|
||||
}
|
||||
}
|
||||
mergeToolUsage(accumulatedToolUsage, incomingToolUsage)
|
||||
|
||||
await updateTaskMetrics(taskMetricsId, {
|
||||
cost: totalCost,
|
||||
|
|
@ -513,35 +301,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
logger.info("waiting for subprocess to finish")
|
||||
controller.abort()
|
||||
|
||||
// Wait for subprocess to finish gracefully, with a timeout.
|
||||
const SUBPROCESS_TIMEOUT = 10_000
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
subprocess,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new SubprocessTimeoutError(SUBPROCESS_TIMEOUT)), SUBPROCESS_TIMEOUT),
|
||||
),
|
||||
])
|
||||
|
||||
logger.info("subprocess finished gracefully")
|
||||
} catch (error) {
|
||||
if (error instanceof SubprocessTimeoutError) {
|
||||
logger.error("subprocess did not finish within timeout, force killing")
|
||||
|
||||
try {
|
||||
if (subprocess.kill("SIGKILL")) {
|
||||
logger.info("SIGKILL sent to subprocess")
|
||||
} else {
|
||||
logger.error("failed to send SIGKILL to subprocess")
|
||||
}
|
||||
} catch (killError) {
|
||||
logger.error("subprocess.kill(SIGKILL) failed:", killError)
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
await waitForSubprocessWithTimeout({ subprocess, logger })
|
||||
|
||||
// Copy conversation history files from VS Code extension storage to the log directory
|
||||
// for post-mortem analysis. Only do this in containerized mode where we have a known path.
|
||||
19
packages/evals/src/cli/types.ts
Normal file
19
packages/evals/src/cli/types.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { type TaskEvent } from "@roo-code/types"
|
||||
|
||||
import type { Run, Task } from "../db/index.js"
|
||||
import { Logger } from "./utils.js"
|
||||
|
||||
export class SubprocessTimeoutError extends Error {
|
||||
constructor(timeout: number) {
|
||||
super(`Subprocess timeout after ${timeout}ms`)
|
||||
this.name = "SubprocessTimeoutError"
|
||||
}
|
||||
}
|
||||
|
||||
export type RunTaskOptions = {
|
||||
run: Run
|
||||
task: Task
|
||||
jobToken: string | null
|
||||
publish: (taskEvent: TaskEvent) => Promise<void>
|
||||
logger: Logger
|
||||
}
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
import * as fs from "fs"
|
||||
import * as fsp from "fs/promises"
|
||||
import * as path from "path"
|
||||
|
||||
import { execa } from "execa"
|
||||
import { execa, type ResultPromise } from "execa"
|
||||
|
||||
import type { ToolUsage } from "@roo-code/types"
|
||||
|
||||
import type { Run, Task } from "../db/index.js"
|
||||
|
||||
import { SubprocessTimeoutError } from "./types.js"
|
||||
|
||||
export const getTag = (caller: string, { run, task }: { run: Run; task?: Task }) =>
|
||||
task
|
||||
? `${caller} | pid:${process.pid} | run:${run.id} | task:${task.id} | ${task.language}/${task.exercise}`
|
||||
|
|
@ -107,6 +112,22 @@ export class Logger {
|
|||
this.info(message, ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write raw output without any prefix (timestamp, level, tag).
|
||||
* Useful for streaming CLI output where the prefix would be noise.
|
||||
*/
|
||||
public raw(message: string): void {
|
||||
try {
|
||||
console.log(message)
|
||||
|
||||
if (this.logStream) {
|
||||
this.logStream.write(message + "\n")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to write to log file ${this.logFilePath}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
if (this.logStream) {
|
||||
this.logStream.end()
|
||||
|
|
@ -114,3 +135,117 @@ export class Logger {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy conversation history files from VS Code extension storage to the log directory.
|
||||
* This allows us to preserve the api_conversation_history.json and ui_messages.json
|
||||
* files for post-mortem analysis alongside the log files.
|
||||
*/
|
||||
export async function copyConversationHistory({
|
||||
rooTaskId,
|
||||
logDir,
|
||||
language,
|
||||
exercise,
|
||||
iteration,
|
||||
logger,
|
||||
}: {
|
||||
rooTaskId: string
|
||||
logDir: string
|
||||
language: string
|
||||
exercise: string
|
||||
iteration: number
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
// VS Code extension global storage path within the container
|
||||
const extensionStoragePath = "/roo/.vscode/User/globalStorage/rooveterinaryinc.roo-cline"
|
||||
const taskStoragePath = path.join(extensionStoragePath, "tasks", rooTaskId)
|
||||
|
||||
const filesToCopy = ["api_conversation_history.json", "ui_messages.json"]
|
||||
|
||||
for (const filename of filesToCopy) {
|
||||
const sourcePath = path.join(taskStoragePath, filename)
|
||||
// Use sanitized exercise name (replace slashes with dashes) for the destination filename
|
||||
// Include iteration number to handle multiple attempts at the same exercise
|
||||
const sanitizedExercise = exercise.replace(/\//g, "-")
|
||||
const destFilename = `${language}-${sanitizedExercise}.${iteration}_${filename}`
|
||||
const destPath = path.join(logDir, destFilename)
|
||||
|
||||
try {
|
||||
// Check if source file exists
|
||||
await fsp.access(sourcePath)
|
||||
|
||||
// Copy the file
|
||||
await fsp.copyFile(sourcePath, destPath)
|
||||
logger.info(`copied ${filename} to ${destPath}`)
|
||||
} catch (error) {
|
||||
// File may not exist if task didn't complete properly - this is not fatal
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
logger.info(`${filename} not found at ${sourcePath} - skipping`)
|
||||
} else {
|
||||
logger.error(`failed to copy ${filename}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge incoming tool usage with accumulated data using MAX strategy.
|
||||
* This handles the case where a task is rehydrated after abort:
|
||||
* - Empty rehydrated data won't overwrite existing: max(5, 0) = 5
|
||||
* - Legitimate restart with additional work is captured: max(5, 8) = 8
|
||||
* Each task instance tracks its own cumulative values, so we take the max
|
||||
* to preserve the highest values seen across all instances.
|
||||
*/
|
||||
export function mergeToolUsage(accumulated: ToolUsage, incoming: ToolUsage): void {
|
||||
for (const [toolName, usage] of Object.entries(incoming)) {
|
||||
const existing = accumulated[toolName as keyof ToolUsage]
|
||||
|
||||
if (existing) {
|
||||
accumulated[toolName as keyof ToolUsage] = {
|
||||
attempts: Math.max(existing.attempts, usage.attempts),
|
||||
failures: Math.max(existing.failures, usage.failures),
|
||||
}
|
||||
} else {
|
||||
accumulated[toolName as keyof ToolUsage] = { ...usage }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a subprocess to finish gracefully, with a timeout.
|
||||
* If the subprocess doesn't finish within the timeout, force kill it with SIGKILL.
|
||||
*/
|
||||
export async function waitForSubprocessWithTimeout({
|
||||
subprocess,
|
||||
timeoutMs = 10_000,
|
||||
logger,
|
||||
}: {
|
||||
subprocess: ResultPromise
|
||||
timeoutMs?: number
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await Promise.race([
|
||||
subprocess,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new SubprocessTimeoutError(timeoutMs)), timeoutMs)),
|
||||
])
|
||||
|
||||
logger.info("subprocess finished gracefully")
|
||||
} catch (error) {
|
||||
if (error instanceof SubprocessTimeoutError) {
|
||||
logger.error("subprocess did not finish within timeout, force killing")
|
||||
|
||||
try {
|
||||
if (subprocess.kill("SIGKILL")) {
|
||||
logger.info("SIGKILL sent to subprocess")
|
||||
} else {
|
||||
logger.error("failed to send SIGKILL to subprocess")
|
||||
}
|
||||
} catch (killError) {
|
||||
logger.error("subprocess.kill(SIGKILL) failed:", killError)
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "runs" ADD COLUMN "execution_method" text DEFAULT 'vscode' NOT NULL;
|
||||
479
packages/evals/src/db/migrations/meta/0006_snapshot.json
Normal file
479
packages/evals/src/db/migrations/meta/0006_snapshot.json
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
{
|
||||
"id": "ae1ebc36-8f5b-43e1-8e47-5a63d72ed05f",
|
||||
"prevId": "71b54967-86df-42ec-a200-bfd8dad85069",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.runs": {
|
||||
"name": "runs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "runs_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"contextWindow": {
|
||||
"name": "contextWindow",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"inputPrice": {
|
||||
"name": "inputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"outputPrice": {
|
||||
"name": "outputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheWritesPrice": {
|
||||
"name": "cacheWritesPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheReadsPrice": {
|
||||
"name": "cacheReadsPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"jobToken": {
|
||||
"name": "jobToken",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"pid": {
|
||||
"name": "pid",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"socket_path": {
|
||||
"name": "socket_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"execution_method": {
|
||||
"name": "execution_method",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'vscode'"
|
||||
},
|
||||
"concurrency": {
|
||||
"name": "concurrency",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 2
|
||||
},
|
||||
"timeout": {
|
||||
"name": "timeout",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 5
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"failed": {
|
||||
"name": "failed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"runs_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "runs_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "runs",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.taskMetrics": {
|
||||
"name": "taskMetrics",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "taskMetrics_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"tokens_in": {
|
||||
"name": "tokens_in",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_out": {
|
||||
"name": "tokens_out",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_context": {
|
||||
"name": "tokens_context",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_writes": {
|
||||
"name": "cache_writes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_reads": {
|
||||
"name": "cache_reads",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tool_usage": {
|
||||
"name": "tool_usage",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tasks": {
|
||||
"name": "tasks",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "tasks_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"exercise": {
|
||||
"name": "exercise",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"finished_at": {
|
||||
"name": "finished_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tasks_language_exercise_iteration_idx": {
|
||||
"name": "tasks_language_exercise_iteration_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "run_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "language",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "exercise",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "iteration",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"tasks_run_id_runs_id_fk": {
|
||||
"name": "tasks_run_id_runs_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"tasks_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "tasks_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.toolErrors": {
|
||||
"name": "toolErrors",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "toolErrors_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_name": {
|
||||
"name": "tool_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"toolErrors_run_id_runs_id_fk": {
|
||||
"name": "toolErrors_run_id_runs_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"toolErrors_task_id_tasks_id_fk": {
|
||||
"name": "toolErrors_task_id_tasks_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "tasks",
|
||||
"columnsFrom": ["task_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,13 @@
|
|||
"when": 1765167049182,
|
||||
"tag": "0005_strong_skrulls",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1767550126096,
|
||||
"tag": "0006_worried_spectrum",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ import type { RooCodeSettings, ToolName, ToolUsage } from "@roo-code/types"
|
|||
|
||||
import type { ExerciseLanguage } from "../exercises/index.js"
|
||||
|
||||
/**
|
||||
* ExecutionMethod
|
||||
*/
|
||||
|
||||
export type ExecutionMethod = "vscode" | "cli"
|
||||
|
||||
/**
|
||||
* runs
|
||||
*/
|
||||
|
|
@ -24,6 +30,7 @@ export const runs = pgTable("runs", {
|
|||
jobToken: text(),
|
||||
pid: integer(),
|
||||
socketPath: text("socket_path").notNull(),
|
||||
executionMethod: text("execution_method").default("vscode").notNull().$type<ExecutionMethod>(),
|
||||
concurrency: integer().default(2).notNull(),
|
||||
timeout: integer().default(5).notNull(),
|
||||
passed: integer().default(0).notNull(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@roo-code/types",
|
||||
"version": "1.94.0",
|
||||
"version": "1.99.0",
|
||||
"description": "TypeScript type definitions for Roo Code.",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ export const globalSettingsSchema = z.object({
|
|||
maxOpenTabsContext: z.number().optional(),
|
||||
maxWorkspaceFiles: z.number().optional(),
|
||||
showRooIgnoredFiles: z.boolean().optional(),
|
||||
enableSubfolderRules: z.boolean().optional(),
|
||||
maxReadFileLine: z.number().optional(),
|
||||
maxImageFileSize: z.number().optional(),
|
||||
maxTotalImageSize: z.number().optional(),
|
||||
|
|
@ -291,7 +292,6 @@ export const isGlobalStateKey = (key: string): key is Keys<GlobalState> =>
|
|||
// Default settings when running evals (unless overridden).
|
||||
export const EVALS_SETTINGS: RooCodeSettings = {
|
||||
apiProvider: "openrouter",
|
||||
openRouterUseMiddleOutTransform: false,
|
||||
|
||||
lastShownAnnouncementId: "jul-09-2025-3-23-0",
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const historyItemSchema = z.object({
|
|||
* This ensures task resumption works correctly even when NTC settings change.
|
||||
*/
|
||||
toolProtocol: z.enum(["xml", "native"]).optional(),
|
||||
apiConfigName: z.string().optional(), // Provider profile name for sticky profile feature
|
||||
status: z.enum(["active", "completed", "delegated"]).optional(),
|
||||
delegatedToId: z.string().optional(), // Last child this parent delegated to
|
||||
childIds: z.array(z.string()).optional(), // All children spawned by this task
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export * from "./message.js"
|
|||
export * from "./mode.js"
|
||||
export * from "./model.js"
|
||||
export * from "./provider-settings.js"
|
||||
export * from "./single-file-read-models.js"
|
||||
export * from "./task.js"
|
||||
export * from "./todo.js"
|
||||
export * from "./telemetry.js"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
|
|||
* - `api_req_finished`: Indicates an API request has completed successfully
|
||||
* - `api_req_retried`: Indicates an API request is being retried after a failure
|
||||
* - `api_req_retry_delayed`: Indicates an API request retry has been delayed
|
||||
* - `api_req_rate_limit_wait`: Indicates a configured rate-limit wait (not an error)
|
||||
* - `api_req_deleted`: Indicates an API request has been deleted/cancelled
|
||||
* - `text`: General text message or assistant response
|
||||
* - `reasoning`: Assistant's reasoning or thought process (often hidden from user)
|
||||
|
|
@ -155,6 +156,7 @@ export const clineSays = [
|
|||
"api_req_finished",
|
||||
"api_req_retried",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_rate_limit_wait",
|
||||
"api_req_deleted",
|
||||
"text",
|
||||
"image",
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export const isCustomProvider = (key: string): key is CustomProvider => customPr
|
|||
* model lists.
|
||||
*/
|
||||
|
||||
export const fauxProviders = ["fake-ai", "human-relay"] as const
|
||||
export const fauxProviders = ["fake-ai"] as const
|
||||
|
||||
export type FauxProvider = (typeof fauxProviders)[number]
|
||||
|
||||
|
|
@ -207,7 +207,6 @@ const openRouterSchema = baseProviderSettingsSchema.extend({
|
|||
openRouterModelId: z.string().optional(),
|
||||
openRouterBaseUrl: z.string().optional(),
|
||||
openRouterSpecificProvider: z.string().optional(),
|
||||
openRouterUseMiddleOutTransform: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const bedrockSchema = apiModelIdProviderModelSchema.extend({
|
||||
|
|
@ -345,8 +344,6 @@ const requestySchema = baseProviderSettingsSchema.extend({
|
|||
requestyModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const humanRelaySchema = baseProviderSettingsSchema
|
||||
|
||||
const fakeAiSchema = baseProviderSettingsSchema.extend({
|
||||
fakeAi: z.unknown().optional(),
|
||||
})
|
||||
|
|
@ -448,7 +445,6 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
|
|||
minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })),
|
||||
unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })),
|
||||
requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })),
|
||||
humanRelaySchema.merge(z.object({ apiProvider: z.literal("human-relay") })),
|
||||
fakeAiSchema.merge(z.object({ apiProvider: z.literal("fake-ai") })),
|
||||
xaiSchema.merge(z.object({ apiProvider: z.literal("xai") })),
|
||||
groqSchema.merge(z.object({ apiProvider: z.literal("groq") })),
|
||||
|
|
@ -490,7 +486,6 @@ export const providerSettingsSchema = z.object({
|
|||
...minimaxSchema.shape,
|
||||
...unboundSchema.shape,
|
||||
...requestySchema.shape,
|
||||
...humanRelaySchema.shape,
|
||||
...fakeAiSchema.shape,
|
||||
...xaiSchema.shape,
|
||||
...groqSchema.shape,
|
||||
|
|
@ -628,7 +623,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
|
|||
*/
|
||||
|
||||
export const MODELS_BY_PROVIDER: Record<
|
||||
Exclude<ProviderName, "fake-ai" | "human-relay" | "gemini-cli" | "openai">,
|
||||
Exclude<ProviderName, "fake-ai" | "gemini-cli" | "openai">,
|
||||
{ id: ProviderName; label: string; models: string[] }
|
||||
> = {
|
||||
anthropic: {
|
||||
|
|
|
|||
|
|
@ -264,39 +264,6 @@ export const bedrockModels = {
|
|||
inputPrice: 0.25,
|
||||
outputPrice: 1.25,
|
||||
},
|
||||
"anthropic.claude-2-1-v1:0": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 100_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 8.0,
|
||||
outputPrice: 24.0,
|
||||
description: "Claude 2.1",
|
||||
},
|
||||
"anthropic.claude-2-0-v1:0": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 100_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 8.0,
|
||||
outputPrice: 24.0,
|
||||
description: "Claude 2.0",
|
||||
},
|
||||
"anthropic.claude-instant-v1:0": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 100_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2.4,
|
||||
description: "Claude Instant",
|
||||
},
|
||||
"deepseek.r1-v1:0": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 128_000,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const cerebrasDefaultModelId: CerebrasModelId = "gpt-oss-120b"
|
|||
|
||||
export const cerebrasModels = {
|
||||
"zai-glm-4.6": {
|
||||
maxTokens: 8192, // Conservative default to avoid premature rate limiting (Cerebras reserves quota upfront)
|
||||
maxTokens: 16384, // Conservative default to avoid premature rate limiting (Cerebras reserves quota upfront)
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
|
|
@ -15,10 +15,22 @@ export const cerebrasModels = {
|
|||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Highly intelligent general purpose model with up to 1,000 tokens/s",
|
||||
description: "Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.",
|
||||
},
|
||||
"zai-glm-4.7": {
|
||||
maxTokens: 16384, // Conservative default to avoid premature rate limiting (Cerebras reserves quota upfront)
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
"Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.",
|
||||
},
|
||||
"qwen-3-235b-a22b-instruct-2507": {
|
||||
maxTokens: 8192, // Conservative default to avoid premature rate limiting
|
||||
maxTokens: 16384, // Conservative default to avoid premature rate limiting
|
||||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
|
|
@ -29,7 +41,7 @@ export const cerebrasModels = {
|
|||
description: "Intelligent model with ~1400 tokens/s",
|
||||
},
|
||||
"llama-3.3-70b": {
|
||||
maxTokens: 8192, // Conservative default to avoid premature rate limiting
|
||||
maxTokens: 16384, // Conservative default to avoid premature rate limiting
|
||||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
|
|
@ -40,7 +52,7 @@ export const cerebrasModels = {
|
|||
description: "Powerful model with ~2600 tokens/s",
|
||||
},
|
||||
"qwen-3-32b": {
|
||||
maxTokens: 8192, // Conservative default to avoid premature rate limiting
|
||||
maxTokens: 16384, // Conservative default to avoid premature rate limiting
|
||||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
|
|
@ -51,7 +63,7 @@ export const cerebrasModels = {
|
|||
description: "SOTA coding performance with ~2500 tokens/s",
|
||||
},
|
||||
"gpt-oss-120b": {
|
||||
maxTokens: 8192, // Conservative default to avoid premature rate limiting
|
||||
maxTokens: 16384, // Conservative default to avoid premature rate limiting
|
||||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ModelInfo } from "../model.js"
|
|||
export type FireworksModelId =
|
||||
| "accounts/fireworks/models/kimi-k2-instruct"
|
||||
| "accounts/fireworks/models/kimi-k2-instruct-0905"
|
||||
| "accounts/fireworks/models/kimi-k2-thinking"
|
||||
| "accounts/fireworks/models/minimax-m2"
|
||||
| "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
|
||||
| "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct"
|
||||
|
|
@ -43,6 +44,21 @@ export const fireworksModels = {
|
|||
description:
|
||||
"Kimi K2 is a state-of-the-art mixture-of-experts (MoE) language model with 32 billion activated parameters and 1 trillion total parameters. Trained with the Muon optimizer, Kimi K2 achieves exceptional performance across frontier knowledge, reasoning, and coding tasks while being meticulously optimized for agentic capabilities.",
|
||||
},
|
||||
"accounts/fireworks/models/kimi-k2-thinking": {
|
||||
maxTokens: 16000,
|
||||
contextWindow: 256000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
supportsTemperature: true,
|
||||
preserveReasoning: true,
|
||||
defaultTemperature: 1.0,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
cacheReadsPrice: 0.15,
|
||||
description:
|
||||
"The kimi-k2-thinking model is a general-purpose agentic reasoning model developed by Moonshot AI. Thanks to its strength in deep reasoning and multi-turn tool use, it can solve even the hardest problems.",
|
||||
},
|
||||
"accounts/fireworks/models/minimax-m2": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 204800,
|
||||
|
|
|
|||
|
|
@ -143,7 +143,6 @@ export function getProviderDefaultModelId(
|
|||
return vercelAiGatewayDefaultModelId
|
||||
case "anthropic":
|
||||
case "gemini-cli":
|
||||
case "human-relay":
|
||||
case "fake-ai":
|
||||
default:
|
||||
return anthropicDefaultModelId
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* Configuration for models that should use simplified single-file read_file tool
|
||||
* These models will use the simpler <read_file><path>...</path></read_file> format
|
||||
* instead of the more complex multi-file args format
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if a model should use single file read format
|
||||
* @param modelId The model ID to check
|
||||
* @returns true if the model should use single file reads
|
||||
*/
|
||||
export function shouldUseSingleFileRead(modelId: string): boolean {
|
||||
return modelId.includes("grok-code-fast-1") || modelId.includes("code-supernova")
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ export const toolNames = [
|
|||
"update_todo_list",
|
||||
"run_slash_command",
|
||||
"generate_image",
|
||||
"custom_tool",
|
||||
] as const
|
||||
|
||||
export const toolNamesSchema = z.enum(toolNames)
|
||||
|
|
|
|||
|
|
@ -38,11 +38,6 @@ export const commandIds = [
|
|||
|
||||
"openInNewTab",
|
||||
|
||||
"showHumanRelayDialog",
|
||||
"registerHumanRelayCallback",
|
||||
"unregisterHumanRelayCallback",
|
||||
"handleHumanRelayResponse",
|
||||
|
||||
"newTask",
|
||||
|
||||
"setCustomStoragePath",
|
||||
|
|
|
|||
4
packages/vscode-shim/eslint.config.mjs
Normal file
4
packages/vscode-shim/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
20
packages/vscode-shim/package.json
Normal file
20
packages/vscode-shim/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@roo-code/vscode-shim",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": "./src/index.ts",
|
||||
"scripts": {
|
||||
"format": "prettier --write 'src/**/*.ts'",
|
||||
"lint": "eslint src --ext .ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@types/node": "^24.1.0",
|
||||
"vitest": "^3.2.3"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
378
packages/vscode-shim/src/__tests__/Additional.test.ts
Normal file
378
packages/vscode-shim/src/__tests__/Additional.test.ts
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
import {
|
||||
Location,
|
||||
DiagnosticRelatedInformation,
|
||||
Diagnostic,
|
||||
ThemeColor,
|
||||
ThemeIcon,
|
||||
CodeActionKind,
|
||||
CodeLens,
|
||||
LanguageModelTextPart,
|
||||
LanguageModelToolCallPart,
|
||||
LanguageModelToolResultPart,
|
||||
FileSystemError,
|
||||
} from "../classes/Additional.js"
|
||||
import { Uri } from "../classes/Uri.js"
|
||||
import { Range } from "../classes/Range.js"
|
||||
import { Position } from "../classes/Position.js"
|
||||
|
||||
describe("Location", () => {
|
||||
it("should create location with URI and Range", () => {
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const range = new Range(0, 0, 5, 10)
|
||||
const location = new Location(uri, range)
|
||||
|
||||
expect(location.uri).toBe(uri)
|
||||
expect(location.range).toBe(range)
|
||||
})
|
||||
|
||||
it("should create location with URI and Position", () => {
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const position = new Position(5, 10)
|
||||
const location = new Location(uri, position)
|
||||
|
||||
expect(location.uri).toBe(uri)
|
||||
expect(location.range).toBe(position)
|
||||
})
|
||||
})
|
||||
|
||||
describe("DiagnosticRelatedInformation", () => {
|
||||
it("should create diagnostic related information", () => {
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const range = new Range(0, 0, 1, 0)
|
||||
const location = new Location(uri, range)
|
||||
const message = "Related issue here"
|
||||
|
||||
const info = new DiagnosticRelatedInformation(location, message)
|
||||
|
||||
expect(info.location).toBe(location)
|
||||
expect(info.message).toBe(message)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Diagnostic", () => {
|
||||
it("should create diagnostic with default severity (Error)", () => {
|
||||
const range = new Range(0, 0, 0, 10)
|
||||
const message = "Error message"
|
||||
|
||||
const diagnostic = new Diagnostic(range, message)
|
||||
|
||||
expect(diagnostic.range.isEqual(range)).toBe(true)
|
||||
expect(diagnostic.message).toBe(message)
|
||||
expect(diagnostic.severity).toBe(0) // Error
|
||||
})
|
||||
|
||||
it("should create diagnostic with custom severity", () => {
|
||||
const range = new Range(0, 0, 0, 10)
|
||||
const message = "Warning message"
|
||||
|
||||
const diagnostic = new Diagnostic(range, message, 1) // Warning
|
||||
|
||||
expect(diagnostic.severity).toBe(1)
|
||||
})
|
||||
|
||||
it("should allow setting optional properties", () => {
|
||||
const range = new Range(0, 0, 0, 10)
|
||||
const diagnostic = new Diagnostic(range, "Test")
|
||||
|
||||
diagnostic.source = "eslint"
|
||||
diagnostic.code = "no-unused-vars"
|
||||
diagnostic.tags = [1] // Unnecessary
|
||||
|
||||
expect(diagnostic.source).toBe("eslint")
|
||||
expect(diagnostic.code).toBe("no-unused-vars")
|
||||
expect(diagnostic.tags).toEqual([1])
|
||||
})
|
||||
|
||||
it("should allow setting related information", () => {
|
||||
const range = new Range(0, 0, 0, 10)
|
||||
const diagnostic = new Diagnostic(range, "Test")
|
||||
|
||||
const relatedUri = Uri.file("/related.txt")
|
||||
const relatedLocation = new Location(relatedUri, new Range(1, 0, 1, 5))
|
||||
const relatedInfo = new DiagnosticRelatedInformation(relatedLocation, "Related issue")
|
||||
|
||||
diagnostic.relatedInformation = [relatedInfo]
|
||||
|
||||
expect(diagnostic.relatedInformation).toHaveLength(1)
|
||||
expect(diagnostic.relatedInformation[0]?.message).toBe("Related issue")
|
||||
})
|
||||
})
|
||||
|
||||
describe("ThemeColor", () => {
|
||||
it("should create theme color with ID", () => {
|
||||
const color = new ThemeColor("editor.foreground")
|
||||
|
||||
expect(color.id).toBe("editor.foreground")
|
||||
})
|
||||
|
||||
it("should handle custom color IDs", () => {
|
||||
const color = new ThemeColor("myExtension.customColor")
|
||||
|
||||
expect(color.id).toBe("myExtension.customColor")
|
||||
})
|
||||
})
|
||||
|
||||
describe("ThemeIcon", () => {
|
||||
it("should create theme icon with ID", () => {
|
||||
const icon = new ThemeIcon("file")
|
||||
|
||||
expect(icon.id).toBe("file")
|
||||
expect(icon.color).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should create theme icon with ID and color", () => {
|
||||
const color = new ThemeColor("errorForeground")
|
||||
const icon = new ThemeIcon("error", color)
|
||||
|
||||
expect(icon.id).toBe("error")
|
||||
expect(icon.color).toBe(color)
|
||||
expect(icon.color?.id).toBe("errorForeground")
|
||||
})
|
||||
})
|
||||
|
||||
describe("CodeActionKind", () => {
|
||||
describe("static properties", () => {
|
||||
it("should have Empty kind", () => {
|
||||
expect(CodeActionKind.Empty.value).toBe("")
|
||||
})
|
||||
|
||||
it("should have QuickFix kind", () => {
|
||||
expect(CodeActionKind.QuickFix.value).toBe("quickfix")
|
||||
})
|
||||
|
||||
it("should have Refactor kind", () => {
|
||||
expect(CodeActionKind.Refactor.value).toBe("refactor")
|
||||
})
|
||||
|
||||
it("should have RefactorExtract kind", () => {
|
||||
expect(CodeActionKind.RefactorExtract.value).toBe("refactor.extract")
|
||||
})
|
||||
|
||||
it("should have RefactorInline kind", () => {
|
||||
expect(CodeActionKind.RefactorInline.value).toBe("refactor.inline")
|
||||
})
|
||||
|
||||
it("should have RefactorRewrite kind", () => {
|
||||
expect(CodeActionKind.RefactorRewrite.value).toBe("refactor.rewrite")
|
||||
})
|
||||
|
||||
it("should have Source kind", () => {
|
||||
expect(CodeActionKind.Source.value).toBe("source")
|
||||
})
|
||||
|
||||
it("should have SourceOrganizeImports kind", () => {
|
||||
expect(CodeActionKind.SourceOrganizeImports.value).toBe("source.organizeImports")
|
||||
})
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create custom kind", () => {
|
||||
const kind = new CodeActionKind("custom.action")
|
||||
expect(kind.value).toBe("custom.action")
|
||||
})
|
||||
})
|
||||
|
||||
describe("append()", () => {
|
||||
it("should append to existing kind", () => {
|
||||
const kind = new CodeActionKind("refactor")
|
||||
const appended = kind.append("extract")
|
||||
|
||||
expect(appended.value).toBe("refactor.extract")
|
||||
})
|
||||
|
||||
it("should handle empty kind", () => {
|
||||
const kind = new CodeActionKind("")
|
||||
const appended = kind.append("quickfix")
|
||||
|
||||
expect(appended.value).toBe("quickfix")
|
||||
})
|
||||
})
|
||||
|
||||
describe("contains()", () => {
|
||||
it("should return true when kind contains another", () => {
|
||||
const parent = CodeActionKind.Refactor
|
||||
const child = CodeActionKind.RefactorExtract
|
||||
|
||||
expect(parent.contains(child)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when kinds are different hierarchies", () => {
|
||||
const quickfix = CodeActionKind.QuickFix
|
||||
const refactor = CodeActionKind.Refactor
|
||||
|
||||
expect(quickfix.contains(refactor)).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true for equal kinds", () => {
|
||||
const kind = new CodeActionKind("quickfix")
|
||||
expect(kind.contains(CodeActionKind.QuickFix)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("intersects()", () => {
|
||||
it("should return true when one contains the other", () => {
|
||||
const parent = CodeActionKind.Refactor
|
||||
const child = CodeActionKind.RefactorExtract
|
||||
|
||||
expect(parent.intersects(child)).toBe(true)
|
||||
expect(child.intersects(parent)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-intersecting kinds", () => {
|
||||
const quickfix = CodeActionKind.QuickFix
|
||||
const source = CodeActionKind.Source
|
||||
|
||||
expect(quickfix.intersects(source)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("CodeLens", () => {
|
||||
it("should create CodeLens with range only", () => {
|
||||
const range = new Range(0, 0, 0, 10)
|
||||
const lens = new CodeLens(range)
|
||||
|
||||
expect(lens.range.isEqual(range)).toBe(true)
|
||||
expect(lens.command).toBeUndefined()
|
||||
expect(lens.isResolved).toBe(false)
|
||||
})
|
||||
|
||||
it("should create CodeLens with range and command", () => {
|
||||
const range = new Range(5, 0, 5, 20)
|
||||
const command = {
|
||||
command: "myExtension.doSomething",
|
||||
title: "Click me",
|
||||
arguments: [1, 2, 3],
|
||||
}
|
||||
const lens = new CodeLens(range, command)
|
||||
|
||||
expect(lens.range).toBeDefined()
|
||||
expect(lens.command?.command).toBe("myExtension.doSomething")
|
||||
expect(lens.command?.title).toBe("Click me")
|
||||
expect(lens.command?.arguments).toEqual([1, 2, 3])
|
||||
})
|
||||
})
|
||||
|
||||
describe("LanguageModelTextPart", () => {
|
||||
it("should create text part with value", () => {
|
||||
const part = new LanguageModelTextPart("Hello, world!")
|
||||
|
||||
expect(part.value).toBe("Hello, world!")
|
||||
})
|
||||
})
|
||||
|
||||
describe("LanguageModelToolCallPart", () => {
|
||||
it("should create tool call part", () => {
|
||||
const part = new LanguageModelToolCallPart("call-123", "searchFiles", { query: "test" })
|
||||
|
||||
expect(part.callId).toBe("call-123")
|
||||
expect(part.name).toBe("searchFiles")
|
||||
expect(part.input).toEqual({ query: "test" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("LanguageModelToolResultPart", () => {
|
||||
it("should create tool result part", () => {
|
||||
const part = new LanguageModelToolResultPart("call-123", [{ type: "text", text: "result" }])
|
||||
|
||||
expect(part.callId).toBe("call-123")
|
||||
expect(part.content).toHaveLength(1)
|
||||
expect(part.content[0]).toEqual({ type: "text", text: "result" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("FileSystemError", () => {
|
||||
describe("constructor", () => {
|
||||
it("should create error with message", () => {
|
||||
const error = new FileSystemError("Something went wrong")
|
||||
|
||||
expect(error.message).toBe("Something went wrong")
|
||||
expect(error.code).toBe("Unknown")
|
||||
expect(error.name).toBe("FileSystemError")
|
||||
})
|
||||
|
||||
it("should create error with message and code", () => {
|
||||
const error = new FileSystemError("Custom error", "CustomCode")
|
||||
|
||||
expect(error.message).toBe("Custom error")
|
||||
expect(error.code).toBe("CustomCode")
|
||||
})
|
||||
})
|
||||
|
||||
describe("FileNotFound()", () => {
|
||||
it("should create FileNotFound error from string", () => {
|
||||
const error = FileSystemError.FileNotFound("File not found: /path/to/file")
|
||||
|
||||
expect(error.message).toBe("File not found: /path/to/file")
|
||||
expect(error.code).toBe("FileNotFound")
|
||||
})
|
||||
|
||||
it("should create FileNotFound error from URI", () => {
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const error = FileSystemError.FileNotFound(uri)
|
||||
|
||||
expect(error.message).toContain("/path/to/file.txt")
|
||||
expect(error.code).toBe("FileNotFound")
|
||||
})
|
||||
|
||||
it("should handle undefined input", () => {
|
||||
const error = FileSystemError.FileNotFound()
|
||||
|
||||
expect(error.message).toContain("unknown")
|
||||
expect(error.code).toBe("FileNotFound")
|
||||
})
|
||||
})
|
||||
|
||||
describe("FileExists()", () => {
|
||||
it("should create FileExists error", () => {
|
||||
const error = FileSystemError.FileExists("File already exists")
|
||||
|
||||
expect(error.message).toBe("File already exists")
|
||||
expect(error.code).toBe("FileExists")
|
||||
})
|
||||
|
||||
it("should create FileExists error from URI", () => {
|
||||
const uri = Uri.file("/existing/file.txt")
|
||||
const error = FileSystemError.FileExists(uri)
|
||||
|
||||
expect(error.message).toContain("/existing/file.txt")
|
||||
expect(error.code).toBe("FileExists")
|
||||
})
|
||||
})
|
||||
|
||||
describe("FileNotADirectory()", () => {
|
||||
it("should create FileNotADirectory error", () => {
|
||||
const error = FileSystemError.FileNotADirectory("Not a directory")
|
||||
|
||||
expect(error.message).toBe("Not a directory")
|
||||
expect(error.code).toBe("FileNotADirectory")
|
||||
})
|
||||
})
|
||||
|
||||
describe("FileIsADirectory()", () => {
|
||||
it("should create FileIsADirectory error", () => {
|
||||
const error = FileSystemError.FileIsADirectory("Is a directory")
|
||||
|
||||
expect(error.message).toBe("Is a directory")
|
||||
expect(error.code).toBe("FileIsADirectory")
|
||||
})
|
||||
})
|
||||
|
||||
describe("NoPermissions()", () => {
|
||||
it("should create NoPermissions error", () => {
|
||||
const error = FileSystemError.NoPermissions("Access denied")
|
||||
|
||||
expect(error.message).toBe("Access denied")
|
||||
expect(error.code).toBe("NoPermissions")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Unavailable()", () => {
|
||||
it("should create Unavailable error", () => {
|
||||
const error = FileSystemError.Unavailable("Resource unavailable")
|
||||
|
||||
expect(error.message).toBe("Resource unavailable")
|
||||
expect(error.code).toBe("Unavailable")
|
||||
})
|
||||
})
|
||||
})
|
||||
156
packages/vscode-shim/src/__tests__/CancellationToken.test.ts
Normal file
156
packages/vscode-shim/src/__tests__/CancellationToken.test.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import { CancellationTokenSource } from "../classes/CancellationToken.js"
|
||||
|
||||
describe("CancellationToken", () => {
|
||||
describe("initial state", () => {
|
||||
it("should not be cancelled initially", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const token = source.token
|
||||
|
||||
expect(token.isCancellationRequested).toBe(false)
|
||||
})
|
||||
|
||||
it("should have onCancellationRequested function", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const token = source.token
|
||||
|
||||
expect(typeof token.onCancellationRequested).toBe("function")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("CancellationTokenSource", () => {
|
||||
describe("token property", () => {
|
||||
it("should return a CancellationToken", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const token = source.token
|
||||
|
||||
expect(token).toBeDefined()
|
||||
expect(typeof token.isCancellationRequested).toBe("boolean")
|
||||
expect(typeof token.onCancellationRequested).toBe("function")
|
||||
})
|
||||
|
||||
it("should return the same token instance on multiple accesses", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
|
||||
expect(source.token).toBe(source.token)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancel()", () => {
|
||||
it("should set isCancellationRequested to true", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
|
||||
source.cancel()
|
||||
|
||||
expect(source.token.isCancellationRequested).toBe(true)
|
||||
})
|
||||
|
||||
it("should fire onCancellationRequested event", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const listener = vi.fn()
|
||||
|
||||
source.token.onCancellationRequested(listener)
|
||||
source.cancel()
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should only fire event once on multiple cancel calls", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const listener = vi.fn()
|
||||
|
||||
source.token.onCancellationRequested(listener)
|
||||
source.cancel()
|
||||
source.cancel()
|
||||
source.cancel()
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should be idempotent", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
|
||||
source.cancel()
|
||||
source.cancel()
|
||||
|
||||
expect(source.token.isCancellationRequested).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose()", () => {
|
||||
it("should cancel the token", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
|
||||
source.dispose()
|
||||
|
||||
expect(source.token.isCancellationRequested).toBe(true)
|
||||
})
|
||||
|
||||
it("should fire onCancellationRequested event", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const listener = vi.fn()
|
||||
|
||||
source.token.onCancellationRequested(listener)
|
||||
source.dispose()
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
|
||||
expect(() => {
|
||||
source.dispose()
|
||||
source.dispose()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("onCancellationRequested", () => {
|
||||
it("should return a disposable", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const listener = vi.fn()
|
||||
|
||||
const disposable = source.token.onCancellationRequested(listener)
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("should stop listening after disposing", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const listener = vi.fn()
|
||||
|
||||
const disposable = source.token.onCancellationRequested(listener)
|
||||
disposable.dispose()
|
||||
source.cancel()
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call listener immediately if already cancelled", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
source.cancel()
|
||||
|
||||
const listener = vi.fn()
|
||||
source.token.onCancellationRequested(listener)
|
||||
|
||||
// Event was already fired, listener added after won't be called
|
||||
// This matches VSCode behavior
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should support multiple listeners", () => {
|
||||
const source = new CancellationTokenSource()
|
||||
const listener1 = vi.fn()
|
||||
const listener2 = vi.fn()
|
||||
|
||||
source.token.onCancellationRequested(listener1)
|
||||
source.token.onCancellationRequested(listener2)
|
||||
source.cancel()
|
||||
|
||||
expect(listener1).toHaveBeenCalledTimes(1)
|
||||
expect(listener2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
157
packages/vscode-shim/src/__tests__/CommandsAPI.test.ts
Normal file
157
packages/vscode-shim/src/__tests__/CommandsAPI.test.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { CommandsAPI } from "../api/CommandsAPI.js"
|
||||
|
||||
describe("CommandsAPI", () => {
|
||||
let commands: CommandsAPI
|
||||
|
||||
beforeEach(() => {
|
||||
commands = new CommandsAPI()
|
||||
})
|
||||
|
||||
describe("registerCommand()", () => {
|
||||
it("should register a command", () => {
|
||||
const callback = vi.fn()
|
||||
|
||||
commands.registerCommand("test.command", callback)
|
||||
commands.executeCommand("test.command")
|
||||
|
||||
expect(callback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return a disposable", () => {
|
||||
const callback = vi.fn()
|
||||
|
||||
const disposable = commands.registerCommand("test.command", callback)
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("should unregister command on dispose", async () => {
|
||||
const callback = vi.fn()
|
||||
|
||||
const disposable = commands.registerCommand("test.command", callback)
|
||||
disposable.dispose()
|
||||
await commands.executeCommand("test.command")
|
||||
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should allow registering multiple commands", () => {
|
||||
const callback1 = vi.fn()
|
||||
const callback2 = vi.fn()
|
||||
|
||||
commands.registerCommand("test.command1", callback1)
|
||||
commands.registerCommand("test.command2", callback2)
|
||||
|
||||
commands.executeCommand("test.command1")
|
||||
commands.executeCommand("test.command2")
|
||||
|
||||
expect(callback1).toHaveBeenCalled()
|
||||
expect(callback2).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("executeCommand()", () => {
|
||||
it("should execute registered command", async () => {
|
||||
const callback = vi.fn().mockReturnValue("result")
|
||||
|
||||
commands.registerCommand("test.command", callback)
|
||||
const result = await commands.executeCommand("test.command")
|
||||
|
||||
expect(result).toBe("result")
|
||||
})
|
||||
|
||||
it("should pass arguments to command handler", async () => {
|
||||
const callback = vi.fn()
|
||||
|
||||
commands.registerCommand("test.command", callback)
|
||||
await commands.executeCommand("test.command", "arg1", "arg2", 123)
|
||||
|
||||
expect(callback).toHaveBeenCalledWith("arg1", "arg2", 123)
|
||||
})
|
||||
|
||||
it("should return promise for unknown command", () => {
|
||||
const result = commands.executeCommand("unknown.command")
|
||||
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
|
||||
it("should resolve to undefined for unknown command", async () => {
|
||||
const result = await commands.executeCommand("unknown.command")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should reject if handler throws", async () => {
|
||||
commands.registerCommand("test.error", () => {
|
||||
throw new Error("Test error")
|
||||
})
|
||||
|
||||
await expect(commands.executeCommand("test.error")).rejects.toThrow("Test error")
|
||||
})
|
||||
|
||||
it("should handle async command handlers", async () => {
|
||||
commands.registerCommand("test.async", async () => {
|
||||
return "async result"
|
||||
})
|
||||
|
||||
const result = await commands.executeCommand("test.async")
|
||||
|
||||
expect(result).toBe("async result")
|
||||
})
|
||||
})
|
||||
|
||||
describe("built-in commands", () => {
|
||||
it("should handle workbench.action.files.saveFiles", async () => {
|
||||
const result = await commands.executeCommand("workbench.action.files.saveFiles")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle workbench.action.closeWindow", async () => {
|
||||
const result = await commands.executeCommand("workbench.action.closeWindow")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle workbench.action.reloadWindow", async () => {
|
||||
const result = await commands.executeCommand("workbench.action.reloadWindow")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("generic type support", () => {
|
||||
it("should support typed return values", async () => {
|
||||
commands.registerCommand("test.typed", () => 42)
|
||||
|
||||
const result = await commands.executeCommand<number>("test.typed")
|
||||
|
||||
expect(result).toBe(42)
|
||||
})
|
||||
|
||||
it("should support complex return types", async () => {
|
||||
const expected = { name: "test", value: 123 }
|
||||
commands.registerCommand("test.object", () => expected)
|
||||
|
||||
const result = await commands.executeCommand<{ name: string; value: number }>("test.object")
|
||||
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("command overwriting", () => {
|
||||
it("should allow registering same command multiple times", () => {
|
||||
const callback1 = vi.fn().mockReturnValue(1)
|
||||
const callback2 = vi.fn().mockReturnValue(2)
|
||||
|
||||
commands.registerCommand("test.command", callback1)
|
||||
commands.registerCommand("test.command", callback2)
|
||||
|
||||
// Last registration wins
|
||||
const result = commands.executeCommand("test.command")
|
||||
|
||||
expect(result).resolves.toBe(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
133
packages/vscode-shim/src/__tests__/EventEmitter.test.ts
Normal file
133
packages/vscode-shim/src/__tests__/EventEmitter.test.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { EventEmitter } from "../classes/EventEmitter.js"
|
||||
|
||||
describe("EventEmitter", () => {
|
||||
describe("event subscription", () => {
|
||||
it("should subscribe and receive events", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
const listener = vi.fn()
|
||||
|
||||
emitter.event(listener)
|
||||
emitter.fire("test")
|
||||
|
||||
expect(listener).toHaveBeenCalledWith("test")
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should support multiple listeners", () => {
|
||||
const emitter = new EventEmitter<number>()
|
||||
const listener1 = vi.fn()
|
||||
const listener2 = vi.fn()
|
||||
|
||||
emitter.event(listener1)
|
||||
emitter.event(listener2)
|
||||
emitter.fire(42)
|
||||
|
||||
expect(listener1).toHaveBeenCalledWith(42)
|
||||
expect(listener2).toHaveBeenCalledWith(42)
|
||||
})
|
||||
|
||||
it("should bind thisArgs when provided", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
const context = { name: "test", capturedThis: null as unknown }
|
||||
|
||||
emitter.event(function (this: typeof context) {
|
||||
this.capturedThis = this
|
||||
}, context)
|
||||
|
||||
emitter.fire("event")
|
||||
expect(context.capturedThis).toBe(context)
|
||||
})
|
||||
|
||||
it("should add disposable to array when provided", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
const disposables: { dispose: () => void }[] = []
|
||||
|
||||
emitter.event(() => {}, undefined, disposables)
|
||||
|
||||
expect(disposables).toHaveLength(1)
|
||||
expect(typeof disposables[0]?.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose subscription", () => {
|
||||
it("should stop receiving events after dispose", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
const listener = vi.fn()
|
||||
|
||||
const disposable = emitter.event(listener)
|
||||
emitter.fire("before")
|
||||
|
||||
disposable.dispose()
|
||||
emitter.fire("after")
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
expect(listener).toHaveBeenCalledWith("before")
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose emitter", () => {
|
||||
it("should remove all listeners on dispose", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
const listener1 = vi.fn()
|
||||
const listener2 = vi.fn()
|
||||
|
||||
emitter.event(listener1)
|
||||
emitter.event(listener2)
|
||||
|
||||
emitter.dispose()
|
||||
emitter.fire("test")
|
||||
|
||||
expect(listener1).not.toHaveBeenCalled()
|
||||
expect(listener2).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should have zero listeners after dispose", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
emitter.event(() => {})
|
||||
emitter.event(() => {})
|
||||
|
||||
expect(emitter.listenerCount).toBe(2)
|
||||
|
||||
emitter.dispose()
|
||||
expect(emitter.listenerCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should not fail if a listener throws", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
const goodListener = vi.fn()
|
||||
|
||||
emitter.event(() => {
|
||||
throw new Error("Listener error")
|
||||
})
|
||||
emitter.event(goodListener)
|
||||
|
||||
// Should not throw
|
||||
expect(() => emitter.fire("test")).not.toThrow()
|
||||
|
||||
// Good listener should still be called
|
||||
expect(goodListener).toHaveBeenCalledWith("test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("listenerCount", () => {
|
||||
it("should track number of listeners", () => {
|
||||
const emitter = new EventEmitter<string>()
|
||||
|
||||
expect(emitter.listenerCount).toBe(0)
|
||||
|
||||
const d1 = emitter.event(() => {})
|
||||
expect(emitter.listenerCount).toBe(1)
|
||||
|
||||
const d2 = emitter.event(() => {})
|
||||
expect(emitter.listenerCount).toBe(2)
|
||||
|
||||
d1.dispose()
|
||||
expect(emitter.listenerCount).toBe(1)
|
||||
|
||||
d2.dispose()
|
||||
expect(emitter.listenerCount).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
343
packages/vscode-shim/src/__tests__/ExtensionContext.test.ts
Normal file
343
packages/vscode-shim/src/__tests__/ExtensionContext.test.ts
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
import { ExtensionContextImpl } from "../context/ExtensionContext.js"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { tmpdir } from "os"
|
||||
|
||||
describe("ExtensionContextImpl", () => {
|
||||
let tempDir: string
|
||||
let extensionPath: string
|
||||
let workspacePath: string
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(tmpdir(), "ext-context-test-"))
|
||||
extensionPath = path.join(tempDir, "extension")
|
||||
workspacePath = path.join(tempDir, "workspace")
|
||||
fs.mkdirSync(extensionPath, { recursive: true })
|
||||
fs.mkdirSync(workspacePath, { recursive: true })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create context with extension path", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
expect(context.extensionPath).toBe(extensionPath)
|
||||
expect(context.extensionUri.fsPath).toBe(extensionPath)
|
||||
})
|
||||
|
||||
it("should use default extension mode (Production)", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
expect(context.extensionMode).toBe(1) // Production
|
||||
})
|
||||
|
||||
it("should allow custom extension mode", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
extensionMode: 2, // Development
|
||||
})
|
||||
|
||||
expect(context.extensionMode).toBe(2)
|
||||
})
|
||||
|
||||
it("should initialize empty subscriptions array", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
expect(context.subscriptions).toEqual([])
|
||||
})
|
||||
|
||||
it("should initialize environmentVariableCollection", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
expect(context.environmentVariableCollection).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("storage paths", () => {
|
||||
it("should set up global storage path", () => {
|
||||
const customStorageDir = path.join(tempDir, "custom-storage")
|
||||
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: customStorageDir,
|
||||
})
|
||||
|
||||
expect(context.globalStoragePath).toContain("global-storage")
|
||||
expect(context.globalStorageUri.fsPath).toBe(context.globalStoragePath)
|
||||
})
|
||||
|
||||
it("should set up workspace storage path with hash", () => {
|
||||
const customStorageDir = path.join(tempDir, "custom-storage")
|
||||
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: customStorageDir,
|
||||
})
|
||||
|
||||
expect(context.storagePath).toContain("workspace-storage")
|
||||
expect(context.storageUri?.fsPath).toBe(context.storagePath)
|
||||
})
|
||||
|
||||
it("should set up log path", () => {
|
||||
const customStorageDir = path.join(tempDir, "custom-storage")
|
||||
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: customStorageDir,
|
||||
})
|
||||
|
||||
expect(context.logPath).toContain("logs")
|
||||
expect(context.logUri.fsPath).toBe(context.logPath)
|
||||
})
|
||||
|
||||
it("should create storage directories", () => {
|
||||
const customStorageDir = path.join(tempDir, "custom-storage")
|
||||
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: customStorageDir,
|
||||
})
|
||||
|
||||
expect(fs.existsSync(context.globalStoragePath)).toBe(true)
|
||||
expect(fs.existsSync(context.storagePath!)).toBe(true)
|
||||
expect(fs.existsSync(context.logPath)).toBe(true)
|
||||
})
|
||||
|
||||
it("should generate different workspace hashes for different paths", () => {
|
||||
const workspace1 = path.join(tempDir, "workspace1")
|
||||
const workspace2 = path.join(tempDir, "workspace2")
|
||||
fs.mkdirSync(workspace1, { recursive: true })
|
||||
fs.mkdirSync(workspace2, { recursive: true })
|
||||
|
||||
const context1 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath: workspace1,
|
||||
storageDir: path.join(tempDir, "storage1"),
|
||||
})
|
||||
|
||||
const context2 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath: workspace2,
|
||||
storageDir: path.join(tempDir, "storage2"),
|
||||
})
|
||||
|
||||
// The hashes should be different
|
||||
const hash1 = path.basename(context1.storagePath!)
|
||||
const hash2 = path.basename(context2.storagePath!)
|
||||
expect(hash1).not.toBe(hash2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("workspaceState", () => {
|
||||
it("should provide workspaceState memento", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: path.join(tempDir, "storage"),
|
||||
})
|
||||
|
||||
expect(context.workspaceState).toBeDefined()
|
||||
expect(typeof context.workspaceState.get).toBe("function")
|
||||
expect(typeof context.workspaceState.update).toBe("function")
|
||||
expect(typeof context.workspaceState.keys).toBe("function")
|
||||
})
|
||||
|
||||
it("should persist workspace state", async () => {
|
||||
const storageDir = path.join(tempDir, "storage")
|
||||
|
||||
const context1 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir,
|
||||
})
|
||||
|
||||
await context1.workspaceState.update("testKey", "testValue")
|
||||
|
||||
// Create new context with same storage
|
||||
const context2 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir,
|
||||
})
|
||||
|
||||
expect(context2.workspaceState.get("testKey")).toBe("testValue")
|
||||
})
|
||||
})
|
||||
|
||||
describe("globalState", () => {
|
||||
it("should provide globalState memento", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: path.join(tempDir, "storage"),
|
||||
})
|
||||
|
||||
expect(context.globalState).toBeDefined()
|
||||
expect(typeof context.globalState.get).toBe("function")
|
||||
expect(typeof context.globalState.update).toBe("function")
|
||||
expect(typeof context.globalState.keys).toBe("function")
|
||||
})
|
||||
|
||||
it("should have setKeysForSync method", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: path.join(tempDir, "storage"),
|
||||
})
|
||||
|
||||
expect(typeof context.globalState.setKeysForSync).toBe("function")
|
||||
// Should not throw
|
||||
expect(() => context.globalState.setKeysForSync(["key1", "key2"])).not.toThrow()
|
||||
})
|
||||
|
||||
it("should persist global state", async () => {
|
||||
const storageDir = path.join(tempDir, "storage")
|
||||
|
||||
const context1 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir,
|
||||
})
|
||||
|
||||
await context1.globalState.update("globalKey", "globalValue")
|
||||
|
||||
// Create new context with same storage
|
||||
const context2 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir,
|
||||
})
|
||||
|
||||
expect(context2.globalState.get("globalKey")).toBe("globalValue")
|
||||
})
|
||||
})
|
||||
|
||||
describe("secrets", () => {
|
||||
it("should provide secrets storage", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir: path.join(tempDir, "storage"),
|
||||
})
|
||||
|
||||
expect(context.secrets).toBeDefined()
|
||||
expect(typeof context.secrets.get).toBe("function")
|
||||
expect(typeof context.secrets.store).toBe("function")
|
||||
expect(typeof context.secrets.delete).toBe("function")
|
||||
})
|
||||
|
||||
it("should persist secrets", async () => {
|
||||
const storageDir = path.join(tempDir, "storage")
|
||||
|
||||
const context1 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir,
|
||||
})
|
||||
|
||||
await context1.secrets.store("apiKey", "secret123")
|
||||
|
||||
// Create new context with same storage
|
||||
const context2 = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
storageDir,
|
||||
})
|
||||
|
||||
const secret = await context2.secrets.get("apiKey")
|
||||
expect(secret).toBe("secret123")
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose()", () => {
|
||||
it("should dispose all subscriptions", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
const disposable1 = { dispose: vi.fn() }
|
||||
const disposable2 = { dispose: vi.fn() }
|
||||
|
||||
context.subscriptions.push(disposable1)
|
||||
context.subscriptions.push(disposable2)
|
||||
|
||||
context.dispose()
|
||||
|
||||
expect(disposable1.dispose).toHaveBeenCalledTimes(1)
|
||||
expect(disposable2.dispose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should clear subscriptions array after dispose", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
context.subscriptions.push({ dispose: () => {} })
|
||||
context.subscriptions.push({ dispose: () => {} })
|
||||
|
||||
context.dispose()
|
||||
|
||||
expect(context.subscriptions).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle disposal errors gracefully", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
// Add a disposable that throws
|
||||
context.subscriptions.push({
|
||||
dispose: () => {
|
||||
throw new Error("Disposal error")
|
||||
},
|
||||
})
|
||||
|
||||
// Add a normal disposable
|
||||
const normalDisposable = { dispose: vi.fn() }
|
||||
context.subscriptions.push(normalDisposable)
|
||||
|
||||
// Should not throw
|
||||
expect(() => context.dispose()).not.toThrow()
|
||||
|
||||
// Normal disposable should still be called
|
||||
expect(normalDisposable.dispose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("default storage directory", () => {
|
||||
it("should use home directory based default when no storageDir provided", () => {
|
||||
const context = new ExtensionContextImpl({
|
||||
extensionPath,
|
||||
workspacePath,
|
||||
})
|
||||
|
||||
// Should contain .vscode-mock in the path
|
||||
expect(context.globalStoragePath).toContain(".vscode-mock")
|
||||
})
|
||||
})
|
||||
})
|
||||
129
packages/vscode-shim/src/__tests__/FileSystemAPI.test.ts
Normal file
129
packages/vscode-shim/src/__tests__/FileSystemAPI.test.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { tmpdir } from "os"
|
||||
|
||||
import { FileSystemAPI } from "../api/FileSystemAPI.js"
|
||||
import { Uri } from "../classes/Uri.js"
|
||||
|
||||
describe("FileSystemAPI", () => {
|
||||
let tempDir: string
|
||||
let fsAPI: FileSystemAPI
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(tmpdir(), "fs-api-test-"))
|
||||
fsAPI = new FileSystemAPI()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe("stat()", () => {
|
||||
it("should stat a file", async () => {
|
||||
const filePath = path.join(tempDir, "test.txt")
|
||||
fs.writeFileSync(filePath, "test content")
|
||||
|
||||
const uri = Uri.file(filePath)
|
||||
const stat = await fsAPI.stat(uri)
|
||||
|
||||
expect(stat.type).toBe(1) // File
|
||||
expect(stat.size).toBeGreaterThan(0)
|
||||
expect(stat.mtime).toBeGreaterThan(0)
|
||||
expect(stat.ctime).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should stat a directory", async () => {
|
||||
const uri = Uri.file(tempDir)
|
||||
const stat = await fsAPI.stat(uri)
|
||||
|
||||
expect(stat.type).toBe(2) // Directory
|
||||
})
|
||||
|
||||
it("should return default stat for non-existent file", async () => {
|
||||
const uri = Uri.file(path.join(tempDir, "nonexistent.txt"))
|
||||
const stat = await fsAPI.stat(uri)
|
||||
|
||||
expect(stat.type).toBe(1) // File (default)
|
||||
expect(stat.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readFile()", () => {
|
||||
it("should read file content", async () => {
|
||||
const filePath = path.join(tempDir, "test.txt")
|
||||
fs.writeFileSync(filePath, "Hello, world!")
|
||||
|
||||
const uri = Uri.file(filePath)
|
||||
const content = await fsAPI.readFile(uri)
|
||||
|
||||
expect(Buffer.from(content).toString()).toBe("Hello, world!")
|
||||
})
|
||||
|
||||
it("should throw FileSystemError for non-existent file", async () => {
|
||||
const uri = Uri.file(path.join(tempDir, "nonexistent.txt"))
|
||||
|
||||
await expect(fsAPI.readFile(uri)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("writeFile()", () => {
|
||||
it("should write file content", async () => {
|
||||
const filePath = path.join(tempDir, "output.txt")
|
||||
const uri = Uri.file(filePath)
|
||||
|
||||
await fsAPI.writeFile(uri, new TextEncoder().encode("Written content"))
|
||||
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toBe("Written content")
|
||||
})
|
||||
|
||||
it("should create parent directories if they don't exist", async () => {
|
||||
const filePath = path.join(tempDir, "subdir", "nested", "file.txt")
|
||||
const uri = Uri.file(filePath)
|
||||
|
||||
await fsAPI.writeFile(uri, new TextEncoder().encode("Nested content"))
|
||||
|
||||
expect(fs.readFileSync(filePath, "utf-8")).toBe("Nested content")
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete()", () => {
|
||||
it("should delete a file", async () => {
|
||||
const filePath = path.join(tempDir, "to-delete.txt")
|
||||
fs.writeFileSync(filePath, "delete me")
|
||||
|
||||
const uri = Uri.file(filePath)
|
||||
await fsAPI.delete(uri)
|
||||
|
||||
expect(fs.existsSync(filePath)).toBe(false)
|
||||
})
|
||||
|
||||
it("should throw error for non-existent file", async () => {
|
||||
const uri = Uri.file(path.join(tempDir, "nonexistent.txt"))
|
||||
|
||||
await expect(fsAPI.delete(uri)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createDirectory()", () => {
|
||||
it("should create a directory", async () => {
|
||||
const dirPath = path.join(tempDir, "new-dir")
|
||||
const uri = Uri.file(dirPath)
|
||||
|
||||
await fsAPI.createDirectory(uri)
|
||||
|
||||
expect(fs.existsSync(dirPath)).toBe(true)
|
||||
expect(fs.statSync(dirPath).isDirectory()).toBe(true)
|
||||
})
|
||||
|
||||
it("should create nested directories", async () => {
|
||||
const dirPath = path.join(tempDir, "a", "b", "c")
|
||||
const uri = Uri.file(dirPath)
|
||||
|
||||
await fsAPI.createDirectory(uri)
|
||||
|
||||
expect(fs.existsSync(dirPath)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
117
packages/vscode-shim/src/__tests__/OutputChannel.test.ts
Normal file
117
packages/vscode-shim/src/__tests__/OutputChannel.test.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { OutputChannel } from "../classes/OutputChannel.js"
|
||||
import { setLogger } from "../utils/logger.js"
|
||||
|
||||
describe("OutputChannel", () => {
|
||||
let mockLogger: {
|
||||
debug: ReturnType<typeof vi.fn>
|
||||
info: ReturnType<typeof vi.fn>
|
||||
warn: ReturnType<typeof vi.fn>
|
||||
error: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}
|
||||
setLogger(mockLogger)
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create an output channel with the given name", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
expect(channel.name).toBe("TestChannel")
|
||||
})
|
||||
})
|
||||
|
||||
describe("name property", () => {
|
||||
it("should return the channel name", () => {
|
||||
const channel = new OutputChannel("MyChannel")
|
||||
|
||||
expect(channel.name).toBe("MyChannel")
|
||||
})
|
||||
})
|
||||
|
||||
describe("append()", () => {
|
||||
it("should log the value with channel name prefix", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
channel.append("test message")
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"[TestChannel] test message",
|
||||
"VSCode.OutputChannel",
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle empty strings", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
channel.append("")
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith("[TestChannel] ", "VSCode.OutputChannel", undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe("appendLine()", () => {
|
||||
it("should log the value with channel name prefix", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
channel.appendLine("line message")
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"[TestChannel] line message",
|
||||
"VSCode.OutputChannel",
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle multi-line strings", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
channel.appendLine("line1\nline2")
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
"[TestChannel] line1\nline2",
|
||||
"VSCode.OutputChannel",
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("clear()", () => {
|
||||
it("should not throw when called", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
expect(() => channel.clear()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("show()", () => {
|
||||
it("should not throw when called without arguments", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
expect(() => channel.show()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("hide()", () => {
|
||||
it("should not throw when called", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
expect(() => channel.hide()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose()", () => {
|
||||
it("should not throw when called", () => {
|
||||
const channel = new OutputChannel("TestChannel")
|
||||
|
||||
expect(() => channel.dispose()).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
139
packages/vscode-shim/src/__tests__/Position.test.ts
Normal file
139
packages/vscode-shim/src/__tests__/Position.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { Position } from "../classes/Position.js"
|
||||
|
||||
describe("Position", () => {
|
||||
describe("constructor", () => {
|
||||
it("should create a position with line and character", () => {
|
||||
const pos = new Position(5, 10)
|
||||
expect(pos.line).toBe(5)
|
||||
expect(pos.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should reject negative line numbers", () => {
|
||||
expect(() => new Position(-1, 0)).toThrow("Line number must be non-negative")
|
||||
})
|
||||
|
||||
it("should reject negative character offsets", () => {
|
||||
expect(() => new Position(0, -1)).toThrow("Character offset must be non-negative")
|
||||
})
|
||||
})
|
||||
|
||||
describe("isEqual()", () => {
|
||||
it("should return true for equal positions", () => {
|
||||
const pos1 = new Position(5, 10)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.isEqual(pos2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for different positions", () => {
|
||||
const pos1 = new Position(5, 10)
|
||||
const pos2 = new Position(5, 11)
|
||||
expect(pos1.isEqual(pos2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isBefore()", () => {
|
||||
it("should return true when line is before", () => {
|
||||
const pos1 = new Position(3, 10)
|
||||
const pos2 = new Position(5, 5)
|
||||
expect(pos1.isBefore(pos2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true when same line but character before", () => {
|
||||
const pos1 = new Position(5, 8)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.isBefore(pos2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when equal", () => {
|
||||
const pos1 = new Position(5, 10)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.isBefore(pos2)).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false when after", () => {
|
||||
const pos1 = new Position(6, 0)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.isBefore(pos2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isAfter()", () => {
|
||||
it("should return true when line is after", () => {
|
||||
const pos1 = new Position(5, 10)
|
||||
const pos2 = new Position(3, 10)
|
||||
expect(pos1.isAfter(pos2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when equal", () => {
|
||||
const pos1 = new Position(5, 10)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.isAfter(pos2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("compareTo()", () => {
|
||||
it("should return -1 when before", () => {
|
||||
const pos1 = new Position(3, 10)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.compareTo(pos2)).toBe(-1)
|
||||
})
|
||||
|
||||
it("should return 0 when equal", () => {
|
||||
const pos1 = new Position(5, 10)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.compareTo(pos2)).toBe(0)
|
||||
})
|
||||
|
||||
it("should return 1 when after", () => {
|
||||
const pos1 = new Position(7, 10)
|
||||
const pos2 = new Position(5, 10)
|
||||
expect(pos1.compareTo(pos2)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("translate()", () => {
|
||||
it("should translate by delta values", () => {
|
||||
const pos = new Position(5, 10)
|
||||
const translated = pos.translate(2, 3)
|
||||
expect(translated.line).toBe(7)
|
||||
expect(translated.character).toBe(13)
|
||||
})
|
||||
|
||||
it("should translate by change object", () => {
|
||||
const pos = new Position(5, 10)
|
||||
const translated = pos.translate({ lineDelta: 1, characterDelta: -2 })
|
||||
expect(translated.line).toBe(6)
|
||||
expect(translated.character).toBe(8)
|
||||
})
|
||||
|
||||
it("should handle omitted deltas as zero", () => {
|
||||
const pos = new Position(5, 10)
|
||||
const translated = pos.translate()
|
||||
expect(translated.line).toBe(5)
|
||||
expect(translated.character).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe("with()", () => {
|
||||
it("should create new position with changed line", () => {
|
||||
const pos = new Position(5, 10)
|
||||
const modified = pos.with(8)
|
||||
expect(modified.line).toBe(8)
|
||||
expect(modified.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should create new position with change object", () => {
|
||||
const pos = new Position(5, 10)
|
||||
const modified = pos.with({ line: 8, character: 15 })
|
||||
expect(modified.line).toBe(8)
|
||||
expect(modified.character).toBe(15)
|
||||
})
|
||||
|
||||
it("should preserve unchanged properties", () => {
|
||||
const pos = new Position(5, 10)
|
||||
const modified = pos.with({ line: 8 })
|
||||
expect(modified.line).toBe(8)
|
||||
expect(modified.character).toBe(10)
|
||||
})
|
||||
})
|
||||
})
|
||||
153
packages/vscode-shim/src/__tests__/Range.test.ts
Normal file
153
packages/vscode-shim/src/__tests__/Range.test.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { Range } from "../classes/Range.js"
|
||||
import { Position } from "../classes/Position.js"
|
||||
|
||||
describe("Range", () => {
|
||||
describe("constructor", () => {
|
||||
it("should create range from Position objects", () => {
|
||||
const start = new Position(0, 0)
|
||||
const end = new Position(5, 10)
|
||||
const range = new Range(start, end)
|
||||
|
||||
expect(range.start.line).toBe(0)
|
||||
expect(range.start.character).toBe(0)
|
||||
expect(range.end.line).toBe(5)
|
||||
expect(range.end.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should create range from numbers", () => {
|
||||
const range = new Range(0, 0, 5, 10)
|
||||
|
||||
expect(range.start.line).toBe(0)
|
||||
expect(range.start.character).toBe(0)
|
||||
expect(range.end.line).toBe(5)
|
||||
expect(range.end.character).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isEmpty", () => {
|
||||
it("should return true for empty range", () => {
|
||||
const range = new Range(5, 10, 5, 10)
|
||||
expect(range.isEmpty).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-empty range", () => {
|
||||
const range = new Range(5, 10, 5, 15)
|
||||
expect(range.isEmpty).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isSingleLine", () => {
|
||||
it("should return true for single line range", () => {
|
||||
const range = new Range(5, 0, 5, 10)
|
||||
expect(range.isSingleLine).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for multi-line range", () => {
|
||||
const range = new Range(5, 0, 6, 10)
|
||||
expect(range.isSingleLine).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("contains()", () => {
|
||||
it("should return true when range contains position", () => {
|
||||
const range = new Range(0, 0, 10, 10)
|
||||
const pos = new Position(5, 5)
|
||||
expect(range.contains(pos)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when position is outside range", () => {
|
||||
const range = new Range(0, 0, 10, 10)
|
||||
const pos = new Position(15, 5)
|
||||
expect(range.contains(pos)).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true when range contains another range", () => {
|
||||
const outer = new Range(0, 0, 10, 10)
|
||||
const inner = new Range(2, 2, 8, 8)
|
||||
expect(outer.contains(inner)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when range does not contain another range", () => {
|
||||
const range1 = new Range(0, 0, 5, 10)
|
||||
const range2 = new Range(6, 0, 10, 10)
|
||||
expect(range1.contains(range2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isEqual()", () => {
|
||||
it("should return true for equal ranges", () => {
|
||||
const range1 = new Range(0, 0, 5, 10)
|
||||
const range2 = new Range(0, 0, 5, 10)
|
||||
expect(range1.isEqual(range2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for different ranges", () => {
|
||||
const range1 = new Range(0, 0, 5, 10)
|
||||
const range2 = new Range(0, 0, 5, 11)
|
||||
expect(range1.isEqual(range2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("intersection()", () => {
|
||||
it("should return intersection of overlapping ranges", () => {
|
||||
const range1 = new Range(0, 0, 10, 10)
|
||||
const range2 = new Range(5, 5, 15, 15)
|
||||
const intersection = range1.intersection(range2)
|
||||
|
||||
expect(intersection).toBeDefined()
|
||||
expect(intersection!.start.line).toBe(5)
|
||||
expect(intersection!.start.character).toBe(5)
|
||||
expect(intersection!.end.line).toBe(10)
|
||||
expect(intersection!.end.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should return undefined for non-overlapping ranges", () => {
|
||||
const range1 = new Range(0, 0, 5, 10)
|
||||
const range2 = new Range(10, 0, 15, 10)
|
||||
const intersection = range1.intersection(range2)
|
||||
|
||||
expect(intersection).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("union()", () => {
|
||||
it("should return union of two ranges", () => {
|
||||
const range1 = new Range(0, 0, 5, 10)
|
||||
const range2 = new Range(3, 5, 8, 15)
|
||||
const union = range1.union(range2)
|
||||
|
||||
expect(union.start.line).toBe(0)
|
||||
expect(union.start.character).toBe(0)
|
||||
expect(union.end.line).toBe(8)
|
||||
expect(union.end.character).toBe(15)
|
||||
})
|
||||
|
||||
it("should handle non-overlapping ranges", () => {
|
||||
const range1 = new Range(0, 0, 2, 10)
|
||||
const range2 = new Range(5, 0, 8, 10)
|
||||
const union = range1.union(range2)
|
||||
|
||||
expect(union.start.line).toBe(0)
|
||||
expect(union.end.line).toBe(8)
|
||||
})
|
||||
})
|
||||
|
||||
describe("with()", () => {
|
||||
it("should create new range with modified start", () => {
|
||||
const range = new Range(0, 0, 5, 10)
|
||||
const modified = range.with(new Position(1, 0))
|
||||
|
||||
expect(modified.start.line).toBe(1)
|
||||
expect(modified.end.line).toBe(5)
|
||||
})
|
||||
|
||||
it("should create new range with change object", () => {
|
||||
const range = new Range(0, 0, 5, 10)
|
||||
const modified = range.with({ end: new Position(8, 15) })
|
||||
|
||||
expect(modified.start.line).toBe(0)
|
||||
expect(modified.end.line).toBe(8)
|
||||
expect(modified.end.character).toBe(15)
|
||||
})
|
||||
})
|
||||
})
|
||||
123
packages/vscode-shim/src/__tests__/Selection.test.ts
Normal file
123
packages/vscode-shim/src/__tests__/Selection.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { Selection } from "../classes/Selection.js"
|
||||
import { Position } from "../classes/Position.js"
|
||||
|
||||
describe("Selection", () => {
|
||||
describe("constructor with Position objects", () => {
|
||||
it("should create selection from Position objects", () => {
|
||||
const anchor = new Position(0, 0)
|
||||
const active = new Position(5, 10)
|
||||
const selection = new Selection(anchor, active)
|
||||
|
||||
expect(selection.anchor.line).toBe(0)
|
||||
expect(selection.anchor.character).toBe(0)
|
||||
expect(selection.active.line).toBe(5)
|
||||
expect(selection.active.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should set start and end correctly for non-reversed selection", () => {
|
||||
const anchor = new Position(0, 0)
|
||||
const active = new Position(5, 10)
|
||||
const selection = new Selection(anchor, active)
|
||||
|
||||
expect(selection.start.line).toBe(0)
|
||||
expect(selection.start.character).toBe(0)
|
||||
expect(selection.end.line).toBe(5)
|
||||
expect(selection.end.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should set start and end correctly for reversed selection", () => {
|
||||
const anchor = new Position(5, 10)
|
||||
const active = new Position(0, 0)
|
||||
const selection = new Selection(anchor, active)
|
||||
|
||||
// Start/end are inherited from Range, which normalizes
|
||||
expect(selection.anchor.line).toBe(5)
|
||||
expect(selection.anchor.character).toBe(10)
|
||||
expect(selection.active.line).toBe(0)
|
||||
expect(selection.active.character).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("constructor with line/character numbers", () => {
|
||||
it("should create selection from line and character numbers", () => {
|
||||
const selection = new Selection(0, 0, 5, 10)
|
||||
|
||||
expect(selection.anchor.line).toBe(0)
|
||||
expect(selection.anchor.character).toBe(0)
|
||||
expect(selection.active.line).toBe(5)
|
||||
expect(selection.active.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should handle reversed selection with numbers", () => {
|
||||
const selection = new Selection(5, 10, 0, 0)
|
||||
|
||||
expect(selection.anchor.line).toBe(5)
|
||||
expect(selection.anchor.character).toBe(10)
|
||||
expect(selection.active.line).toBe(0)
|
||||
expect(selection.active.character).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isReversed", () => {
|
||||
it("should return false when active is after anchor", () => {
|
||||
const selection = new Selection(0, 0, 5, 10)
|
||||
expect(selection.isReversed).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true when active is before anchor", () => {
|
||||
const selection = new Selection(5, 10, 0, 0)
|
||||
expect(selection.isReversed).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when anchor equals active", () => {
|
||||
const selection = new Selection(5, 10, 5, 10)
|
||||
expect(selection.isReversed).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true when same line but active character is before anchor", () => {
|
||||
const selection = new Selection(5, 10, 5, 5)
|
||||
expect(selection.isReversed).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when same line and active character is after anchor", () => {
|
||||
const selection = new Selection(5, 5, 5, 10)
|
||||
expect(selection.isReversed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("inherited Range properties", () => {
|
||||
it("should have isEmpty property", () => {
|
||||
const emptySelection = new Selection(5, 10, 5, 10)
|
||||
expect(emptySelection.isEmpty).toBe(true)
|
||||
|
||||
const nonEmptySelection = new Selection(0, 0, 5, 10)
|
||||
expect(nonEmptySelection.isEmpty).toBe(false)
|
||||
})
|
||||
|
||||
it("should have isSingleLine property", () => {
|
||||
const singleLineSelection = new Selection(5, 0, 5, 10)
|
||||
expect(singleLineSelection.isSingleLine).toBe(true)
|
||||
|
||||
const multiLineSelection = new Selection(0, 0, 5, 10)
|
||||
expect(multiLineSelection.isSingleLine).toBe(false)
|
||||
})
|
||||
|
||||
it("should support contains method", () => {
|
||||
const selection = new Selection(0, 0, 10, 10)
|
||||
const pos = new Position(5, 5)
|
||||
expect(selection.contains(pos)).toBe(true)
|
||||
|
||||
const outsidePos = new Position(15, 5)
|
||||
expect(selection.contains(outsidePos)).toBe(false)
|
||||
})
|
||||
|
||||
it("should support isEqual method", () => {
|
||||
const selection1 = new Selection(0, 0, 5, 10)
|
||||
const selection2 = new Selection(0, 0, 5, 10)
|
||||
const selection3 = new Selection(0, 0, 5, 11)
|
||||
|
||||
expect(selection1.isEqual(selection2)).toBe(true)
|
||||
expect(selection1.isEqual(selection3)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
214
packages/vscode-shim/src/__tests__/StatusBarItem.test.ts
Normal file
214
packages/vscode-shim/src/__tests__/StatusBarItem.test.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { StatusBarItem } from "../classes/StatusBarItem.js"
|
||||
import { StatusBarAlignment } from "../types.js"
|
||||
|
||||
describe("StatusBarItem", () => {
|
||||
describe("constructor", () => {
|
||||
it("should create with alignment", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.alignment).toBe(StatusBarAlignment.Left)
|
||||
})
|
||||
|
||||
it("should create with alignment and priority", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Right, 100)
|
||||
|
||||
expect(item.alignment).toBe(StatusBarAlignment.Right)
|
||||
expect(item.priority).toBe(100)
|
||||
})
|
||||
|
||||
it("should have undefined priority when not provided", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.priority).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("text property", () => {
|
||||
it("should have empty text initially", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.text).toBe("")
|
||||
})
|
||||
|
||||
it("should allow setting text", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.text = "Hello"
|
||||
|
||||
expect(item.text).toBe("Hello")
|
||||
})
|
||||
})
|
||||
|
||||
describe("tooltip property", () => {
|
||||
it("should be undefined initially", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.tooltip).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should allow setting tooltip", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.tooltip = "My tooltip"
|
||||
|
||||
expect(item.tooltip).toBe("My tooltip")
|
||||
})
|
||||
|
||||
it("should allow setting to undefined", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
item.tooltip = "tooltip"
|
||||
|
||||
item.tooltip = undefined
|
||||
|
||||
expect(item.tooltip).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("command property", () => {
|
||||
it("should be undefined initially", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.command).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should allow setting command", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.command = "myExtension.doSomething"
|
||||
|
||||
expect(item.command).toBe("myExtension.doSomething")
|
||||
})
|
||||
})
|
||||
|
||||
describe("color property", () => {
|
||||
it("should be undefined initially", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.color).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should allow setting color", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.color = "#ff0000"
|
||||
|
||||
expect(item.color).toBe("#ff0000")
|
||||
})
|
||||
})
|
||||
|
||||
describe("backgroundColor property", () => {
|
||||
it("should be undefined initially", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.backgroundColor).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should allow setting backgroundColor", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.backgroundColor = "#00ff00"
|
||||
|
||||
expect(item.backgroundColor).toBe("#00ff00")
|
||||
})
|
||||
})
|
||||
|
||||
describe("isVisible property", () => {
|
||||
it("should be false initially", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(item.isVisible).toBe(false)
|
||||
})
|
||||
|
||||
it("should be true after show()", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.show()
|
||||
|
||||
expect(item.isVisible).toBe(true)
|
||||
})
|
||||
|
||||
it("should be false after hide()", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
item.show()
|
||||
|
||||
item.hide()
|
||||
|
||||
expect(item.isVisible).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("show()", () => {
|
||||
it("should make item visible", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.show()
|
||||
|
||||
expect(item.isVisible).toBe(true)
|
||||
})
|
||||
|
||||
it("should be idempotent", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
item.show()
|
||||
item.show()
|
||||
|
||||
expect(item.isVisible).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hide()", () => {
|
||||
it("should make item invisible", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
item.show()
|
||||
|
||||
item.hide()
|
||||
|
||||
expect(item.isVisible).toBe(false)
|
||||
})
|
||||
|
||||
it("should be safe to call when already hidden", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(() => item.hide()).not.toThrow()
|
||||
expect(item.isVisible).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose()", () => {
|
||||
it("should make item invisible", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
item.show()
|
||||
|
||||
item.dispose()
|
||||
|
||||
expect(item.isVisible).toBe(false)
|
||||
})
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
expect(() => {
|
||||
item.dispose()
|
||||
item.dispose()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("alignment property", () => {
|
||||
it("should be readonly", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left)
|
||||
|
||||
// TypeScript prevents reassignment at compile time
|
||||
// Just verify the value is what we expect
|
||||
expect(item.alignment).toBe(StatusBarAlignment.Left)
|
||||
})
|
||||
})
|
||||
|
||||
describe("priority property", () => {
|
||||
it("should be readonly", () => {
|
||||
const item = new StatusBarItem(StatusBarAlignment.Left, 50)
|
||||
|
||||
expect(item.priority).toBe(50)
|
||||
})
|
||||
})
|
||||
})
|
||||
163
packages/vscode-shim/src/__tests__/TabGroupsAPI.test.ts
Normal file
163
packages/vscode-shim/src/__tests__/TabGroupsAPI.test.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { TabGroupsAPI, type Tab, type TabGroup } from "../api/TabGroupsAPI.js"
|
||||
import { Uri } from "../classes/Uri.js"
|
||||
|
||||
describe("TabGroupsAPI", () => {
|
||||
let tabGroups: TabGroupsAPI
|
||||
|
||||
beforeEach(() => {
|
||||
tabGroups = new TabGroupsAPI()
|
||||
})
|
||||
|
||||
describe("all property", () => {
|
||||
it("should return empty array initially", () => {
|
||||
expect(tabGroups.all).toEqual([])
|
||||
})
|
||||
|
||||
it("should return array of TabGroup", () => {
|
||||
expect(Array.isArray(tabGroups.all)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("onDidChangeTabs()", () => {
|
||||
it("should return a disposable", () => {
|
||||
const disposable = tabGroups.onDidChangeTabs(() => {})
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("should call listener when _simulateTabChange is called", () => {
|
||||
const listener = vi.fn()
|
||||
tabGroups.onDidChangeTabs(listener)
|
||||
|
||||
tabGroups._simulateTabChange()
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should not call listener after dispose", () => {
|
||||
const listener = vi.fn()
|
||||
const disposable = tabGroups.onDidChangeTabs(listener)
|
||||
|
||||
disposable.dispose()
|
||||
tabGroups._simulateTabChange()
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should support multiple listeners", () => {
|
||||
const listener1 = vi.fn()
|
||||
const listener2 = vi.fn()
|
||||
|
||||
tabGroups.onDidChangeTabs(listener1)
|
||||
tabGroups.onDidChangeTabs(listener2)
|
||||
tabGroups._simulateTabChange()
|
||||
|
||||
expect(listener1).toHaveBeenCalledTimes(1)
|
||||
expect(listener2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("close()", () => {
|
||||
it("should return false when tab is not found", async () => {
|
||||
const mockTab: Tab = {
|
||||
input: { uri: Uri.file("/test/file.txt") },
|
||||
label: "file.txt",
|
||||
isActive: true,
|
||||
isDirty: false,
|
||||
}
|
||||
|
||||
const result = await tabGroups.close(mockTab)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("should return a promise", () => {
|
||||
const mockTab: Tab = {
|
||||
input: { uri: Uri.file("/test/file.txt") },
|
||||
label: "file.txt",
|
||||
isActive: true,
|
||||
isDirty: false,
|
||||
}
|
||||
|
||||
const result = tabGroups.close(mockTab)
|
||||
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
})
|
||||
|
||||
describe("_simulateTabChange()", () => {
|
||||
it("should fire the onDidChangeTabs event", () => {
|
||||
const listener = vi.fn()
|
||||
tabGroups.onDidChangeTabs(listener)
|
||||
|
||||
tabGroups._simulateTabChange()
|
||||
|
||||
expect(listener).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose()", () => {
|
||||
it("should not throw when called", () => {
|
||||
expect(() => tabGroups.dispose()).not.toThrow()
|
||||
})
|
||||
|
||||
it("should stop firing events after dispose", () => {
|
||||
const listener = vi.fn()
|
||||
tabGroups.onDidChangeTabs(listener)
|
||||
|
||||
tabGroups.dispose()
|
||||
// After dispose, internal emitter is disposed so new events shouldn't fire
|
||||
// But existing listeners may still be registered
|
||||
})
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
expect(() => {
|
||||
tabGroups.dispose()
|
||||
tabGroups.dispose()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Tab interface", () => {
|
||||
it("should have required properties", () => {
|
||||
const tab: Tab = {
|
||||
input: { uri: Uri.file("/test/file.txt") },
|
||||
label: "file.txt",
|
||||
isActive: true,
|
||||
isDirty: false,
|
||||
}
|
||||
|
||||
expect(tab.input).toBeDefined()
|
||||
expect(tab.label).toBe("file.txt")
|
||||
expect(tab.isActive).toBe(true)
|
||||
expect(tab.isDirty).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("TabGroup interface", () => {
|
||||
it("should have tabs array", () => {
|
||||
const tabGroup: TabGroup = {
|
||||
tabs: [],
|
||||
}
|
||||
|
||||
expect(Array.isArray(tabGroup.tabs)).toBe(true)
|
||||
})
|
||||
|
||||
it("should contain Tab objects", () => {
|
||||
const tab: Tab = {
|
||||
input: { uri: Uri.file("/test/file.txt") },
|
||||
label: "file.txt",
|
||||
isActive: true,
|
||||
isDirty: false,
|
||||
}
|
||||
|
||||
const tabGroup: TabGroup = {
|
||||
tabs: [tab],
|
||||
}
|
||||
|
||||
expect(tabGroup.tabs).toHaveLength(1)
|
||||
expect(tabGroup.tabs[0]).toBe(tab)
|
||||
})
|
||||
})
|
||||
263
packages/vscode-shim/src/__tests__/TextEdit.test.ts
Normal file
263
packages/vscode-shim/src/__tests__/TextEdit.test.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import { TextEdit, WorkspaceEdit } from "../classes/TextEdit.js"
|
||||
import { Position } from "../classes/Position.js"
|
||||
import { Range } from "../classes/Range.js"
|
||||
import { Uri } from "../classes/Uri.js"
|
||||
|
||||
describe("TextEdit", () => {
|
||||
describe("constructor", () => {
|
||||
it("should create a TextEdit with range and newText", () => {
|
||||
const range = new Range(0, 0, 0, 5)
|
||||
const edit = new TextEdit(range, "hello")
|
||||
|
||||
expect(edit.range.start.line).toBe(0)
|
||||
expect(edit.range.start.character).toBe(0)
|
||||
expect(edit.range.end.line).toBe(0)
|
||||
expect(edit.range.end.character).toBe(5)
|
||||
expect(edit.newText).toBe("hello")
|
||||
})
|
||||
})
|
||||
|
||||
describe("replace()", () => {
|
||||
it("should create a replace edit", () => {
|
||||
const range = new Range(1, 0, 1, 10)
|
||||
const edit = TextEdit.replace(range, "replacement")
|
||||
|
||||
expect(edit.range.isEqual(range)).toBe(true)
|
||||
expect(edit.newText).toBe("replacement")
|
||||
})
|
||||
|
||||
it("should handle multi-line ranges", () => {
|
||||
const range = new Range(0, 0, 5, 10)
|
||||
const edit = TextEdit.replace(range, "new content")
|
||||
|
||||
expect(edit.range.start.line).toBe(0)
|
||||
expect(edit.range.end.line).toBe(5)
|
||||
expect(edit.newText).toBe("new content")
|
||||
})
|
||||
})
|
||||
|
||||
describe("insert()", () => {
|
||||
it("should create an insert edit at position", () => {
|
||||
const position = new Position(5, 10)
|
||||
const edit = TextEdit.insert(position, "inserted text")
|
||||
|
||||
expect(edit.range.start.line).toBe(5)
|
||||
expect(edit.range.start.character).toBe(10)
|
||||
expect(edit.range.end.line).toBe(5)
|
||||
expect(edit.range.end.character).toBe(10)
|
||||
expect(edit.range.isEmpty).toBe(true)
|
||||
expect(edit.newText).toBe("inserted text")
|
||||
})
|
||||
|
||||
it("should handle insert at beginning of file", () => {
|
||||
const position = new Position(0, 0)
|
||||
const edit = TextEdit.insert(position, "prefix")
|
||||
|
||||
expect(edit.range.start.isEqual(position)).toBe(true)
|
||||
expect(edit.newText).toBe("prefix")
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete()", () => {
|
||||
it("should create a delete edit", () => {
|
||||
const range = new Range(0, 5, 0, 10)
|
||||
const edit = TextEdit.delete(range)
|
||||
|
||||
expect(edit.range.isEqual(range)).toBe(true)
|
||||
expect(edit.newText).toBe("")
|
||||
})
|
||||
|
||||
it("should handle multi-line deletion", () => {
|
||||
const range = new Range(0, 0, 5, 0)
|
||||
const edit = TextEdit.delete(range)
|
||||
|
||||
expect(edit.range.start.line).toBe(0)
|
||||
expect(edit.range.end.line).toBe(5)
|
||||
expect(edit.newText).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("setEndOfLine()", () => {
|
||||
it("should create a setEndOfLine edit", () => {
|
||||
const edit = TextEdit.setEndOfLine()
|
||||
|
||||
expect(edit.range.start.line).toBe(0)
|
||||
expect(edit.range.start.character).toBe(0)
|
||||
expect(edit.newText).toBe("")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("WorkspaceEdit", () => {
|
||||
describe("set() and get()", () => {
|
||||
it("should set and get edits for a URI", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const edits = [
|
||||
TextEdit.replace(new Range(0, 0, 0, 5), "hello"),
|
||||
TextEdit.insert(new Position(1, 0), "world"),
|
||||
]
|
||||
|
||||
workspaceEdit.set(uri, edits)
|
||||
const retrieved = workspaceEdit.get(uri)
|
||||
|
||||
expect(retrieved).toHaveLength(2)
|
||||
expect(retrieved[0]?.newText).toBe("hello")
|
||||
expect(retrieved[1]?.newText).toBe("world")
|
||||
})
|
||||
|
||||
it("should return empty array for unknown URI", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/nonexistent.txt")
|
||||
|
||||
expect(workspaceEdit.get(uri)).toEqual([])
|
||||
})
|
||||
|
||||
it("should overwrite edits when setting same URI", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
|
||||
workspaceEdit.set(uri, [TextEdit.insert(new Position(0, 0), "first")])
|
||||
workspaceEdit.set(uri, [TextEdit.insert(new Position(0, 0), "second")])
|
||||
|
||||
const edits = workspaceEdit.get(uri)
|
||||
expect(edits).toHaveLength(1)
|
||||
expect(edits[0]?.newText).toBe("second")
|
||||
})
|
||||
})
|
||||
|
||||
describe("has()", () => {
|
||||
it("should return true when URI has edits", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
|
||||
workspaceEdit.set(uri, [TextEdit.insert(new Position(0, 0), "text")])
|
||||
|
||||
expect(workspaceEdit.has(uri)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when URI has no edits", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
|
||||
expect(workspaceEdit.has(uri)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete()", () => {
|
||||
it("should add a delete edit for URI", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const range = new Range(0, 5, 0, 10)
|
||||
|
||||
workspaceEdit.delete(uri, range)
|
||||
|
||||
const edits = workspaceEdit.get(uri)
|
||||
expect(edits).toHaveLength(1)
|
||||
expect(edits[0]?.newText).toBe("")
|
||||
expect(edits[0]?.range.start.character).toBe(5)
|
||||
expect(edits[0]?.range.end.character).toBe(10)
|
||||
})
|
||||
|
||||
it("should append to existing edits", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
|
||||
workspaceEdit.insert(uri, new Position(0, 0), "text")
|
||||
workspaceEdit.delete(uri, new Range(1, 0, 1, 5))
|
||||
|
||||
const edits = workspaceEdit.get(uri)
|
||||
expect(edits).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("insert()", () => {
|
||||
it("should add an insert edit for URI", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const position = new Position(5, 10)
|
||||
|
||||
workspaceEdit.insert(uri, position, "inserted")
|
||||
|
||||
const edits = workspaceEdit.get(uri)
|
||||
expect(edits).toHaveLength(1)
|
||||
expect(edits[0]?.newText).toBe("inserted")
|
||||
expect(edits[0]?.range.start.line).toBe(5)
|
||||
expect(edits[0]?.range.start.character).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe("replace()", () => {
|
||||
it("should add a replace edit for URI", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const range = new Range(0, 0, 0, 10)
|
||||
|
||||
workspaceEdit.replace(uri, range, "replacement")
|
||||
|
||||
const edits = workspaceEdit.get(uri)
|
||||
expect(edits).toHaveLength(1)
|
||||
expect(edits[0]?.newText).toBe("replacement")
|
||||
expect(edits[0]?.range.start.line).toBe(0)
|
||||
expect(edits[0]?.range.end.character).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe("size", () => {
|
||||
it("should return 0 for empty WorkspaceEdit", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
expect(workspaceEdit.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should return number of documents with edits", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri1 = Uri.file("/path/to/file1.txt")
|
||||
const uri2 = Uri.file("/path/to/file2.txt")
|
||||
const uri3 = Uri.file("/path/to/file3.txt")
|
||||
|
||||
workspaceEdit.insert(uri1, new Position(0, 0), "text1")
|
||||
workspaceEdit.insert(uri2, new Position(0, 0), "text2")
|
||||
workspaceEdit.insert(uri3, new Position(0, 0), "text3")
|
||||
|
||||
expect(workspaceEdit.size).toBe(3)
|
||||
})
|
||||
|
||||
it("should count same URI only once", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
|
||||
workspaceEdit.insert(uri, new Position(0, 0), "text1")
|
||||
workspaceEdit.insert(uri, new Position(1, 0), "text2")
|
||||
workspaceEdit.insert(uri, new Position(2, 0), "text3")
|
||||
|
||||
expect(workspaceEdit.size).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("entries()", () => {
|
||||
it("should return empty array for empty WorkspaceEdit", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
expect(workspaceEdit.entries()).toEqual([])
|
||||
})
|
||||
|
||||
it("should return all URI/edits pairs", () => {
|
||||
const workspaceEdit = new WorkspaceEdit()
|
||||
const uri1 = Uri.file("/path/to/file1.txt")
|
||||
const uri2 = Uri.file("/path/to/file2.txt")
|
||||
|
||||
workspaceEdit.insert(uri1, new Position(0, 0), "text1")
|
||||
workspaceEdit.replace(uri2, new Range(0, 0, 0, 5), "text2")
|
||||
|
||||
const entries = workspaceEdit.entries()
|
||||
expect(entries).toHaveLength(2)
|
||||
|
||||
// Entries should have URI-like objects with toString and fsPath
|
||||
expect(typeof entries[0]?.[0]?.toString).toBe("function")
|
||||
expect(typeof entries[0]?.[0]?.fsPath).toBe("string")
|
||||
|
||||
// Should contain the edits
|
||||
expect(entries.some((e) => e[1][0]?.newText === "text1")).toBe(true)
|
||||
expect(entries.some((e) => e[1][0]?.newText === "text2")).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { TextEditorDecorationType } from "../classes/TextEditorDecorationType.js"
|
||||
|
||||
describe("TextEditorDecorationType", () => {
|
||||
describe("constructor", () => {
|
||||
it("should create with a key", () => {
|
||||
const decoration = new TextEditorDecorationType("my-decoration")
|
||||
|
||||
expect(decoration.key).toBe("my-decoration")
|
||||
})
|
||||
|
||||
it("should allow any string key", () => {
|
||||
const decoration = new TextEditorDecorationType("decoration-12345")
|
||||
|
||||
expect(decoration.key).toBe("decoration-12345")
|
||||
})
|
||||
})
|
||||
|
||||
describe("key property", () => {
|
||||
it("should be accessible", () => {
|
||||
const decoration = new TextEditorDecorationType("test-key")
|
||||
|
||||
expect(decoration.key).toBe("test-key")
|
||||
})
|
||||
|
||||
it("should be mutable", () => {
|
||||
const decoration = new TextEditorDecorationType("original")
|
||||
|
||||
decoration.key = "modified"
|
||||
|
||||
expect(decoration.key).toBe("modified")
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose()", () => {
|
||||
it("should not throw when called", () => {
|
||||
const decoration = new TextEditorDecorationType("test")
|
||||
|
||||
expect(() => decoration.dispose()).not.toThrow()
|
||||
})
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
const decoration = new TextEditorDecorationType("test")
|
||||
|
||||
expect(() => {
|
||||
decoration.dispose()
|
||||
decoration.dispose()
|
||||
decoration.dispose()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Disposable interface", () => {
|
||||
it("should implement Disposable interface", () => {
|
||||
const decoration = new TextEditorDecorationType("test")
|
||||
|
||||
expect(typeof decoration.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
})
|
||||
102
packages/vscode-shim/src/__tests__/Uri.test.ts
Normal file
102
packages/vscode-shim/src/__tests__/Uri.test.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { Uri } from "../classes/Uri.js"
|
||||
|
||||
describe("Uri", () => {
|
||||
describe("file()", () => {
|
||||
it("should create a file URI", () => {
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
expect(uri.scheme).toBe("file")
|
||||
expect(uri.path).toBe("/path/to/file.txt")
|
||||
expect(uri.fsPath).toBe("/path/to/file.txt")
|
||||
})
|
||||
|
||||
it("should handle Windows paths", () => {
|
||||
const uri = Uri.file("C:\\Users\\test\\file.txt")
|
||||
expect(uri.scheme).toBe("file")
|
||||
expect(uri.fsPath).toBe("C:\\Users\\test\\file.txt")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parse()", () => {
|
||||
it("should parse HTTP URLs", () => {
|
||||
const uri = Uri.parse("https://example.com/path?query=1#fragment")
|
||||
expect(uri.scheme).toBe("https")
|
||||
expect(uri.authority).toBe("example.com")
|
||||
expect(uri.path).toBe("/path")
|
||||
expect(uri.query).toBe("query=1")
|
||||
expect(uri.fragment).toBe("fragment")
|
||||
})
|
||||
|
||||
it("should parse file URLs", () => {
|
||||
const uri = Uri.parse("file:///path/to/file.txt")
|
||||
expect(uri.scheme).toBe("file")
|
||||
expect(uri.path).toBe("/path/to/file.txt")
|
||||
})
|
||||
|
||||
it("should handle invalid URLs by treating as file paths", () => {
|
||||
const uri = Uri.parse("/just/a/path")
|
||||
expect(uri.scheme).toBe("file")
|
||||
expect(uri.fsPath).toBe("/just/a/path")
|
||||
})
|
||||
})
|
||||
|
||||
describe("joinPath()", () => {
|
||||
it("should join path segments", () => {
|
||||
const base = Uri.file("/base/path")
|
||||
const joined = Uri.joinPath(base, "sub", "file.txt")
|
||||
expect(joined.fsPath).toContain("sub")
|
||||
expect(joined.fsPath).toContain("file.txt")
|
||||
})
|
||||
})
|
||||
|
||||
describe("with()", () => {
|
||||
it("should create new URI with modified scheme", () => {
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const modified = uri.with({ scheme: "vscode" })
|
||||
expect(modified.scheme).toBe("vscode")
|
||||
expect(modified.path).toBe("/path/to/file.txt")
|
||||
})
|
||||
|
||||
it("should create new URI with modified path", () => {
|
||||
const uri = Uri.parse("https://example.com/old/path")
|
||||
const modified = uri.with({ path: "/new/path" })
|
||||
expect(modified.path).toBe("/new/path")
|
||||
expect(modified.scheme).toBe("https")
|
||||
})
|
||||
|
||||
it("should preserve unchanged properties", () => {
|
||||
const uri = Uri.parse("https://example.com/path?query=1#fragment")
|
||||
const modified = uri.with({ path: "/newpath" })
|
||||
expect(modified.scheme).toBe("https")
|
||||
expect(modified.query).toBe("query=1")
|
||||
expect(modified.fragment).toBe("fragment")
|
||||
})
|
||||
})
|
||||
|
||||
describe("toString()", () => {
|
||||
it("should convert to URI string", () => {
|
||||
const uri = Uri.parse("https://example.com/path?query=1#fragment")
|
||||
const str = uri.toString()
|
||||
expect(str).toBe("https://example.com/path?query=1#fragment")
|
||||
})
|
||||
|
||||
it("should handle file URIs", () => {
|
||||
const uri = Uri.file("/path/to/file.txt")
|
||||
const str = uri.toString()
|
||||
expect(str).toBe("file:///path/to/file.txt")
|
||||
})
|
||||
})
|
||||
|
||||
describe("toJSON()", () => {
|
||||
it("should convert to JSON object", () => {
|
||||
const uri = Uri.parse("https://example.com/path?query=1#fragment")
|
||||
const json = uri.toJSON()
|
||||
expect(json).toEqual({
|
||||
scheme: "https",
|
||||
authority: "example.com",
|
||||
path: "/path",
|
||||
query: "query=1",
|
||||
fragment: "fragment",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
305
packages/vscode-shim/src/__tests__/WindowAPI.test.ts
Normal file
305
packages/vscode-shim/src/__tests__/WindowAPI.test.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import { WindowAPI } from "../api/WindowAPI.js"
|
||||
import { Uri } from "../classes/Uri.js"
|
||||
import { StatusBarAlignment } from "../types.js"
|
||||
|
||||
describe("WindowAPI", () => {
|
||||
let windowAPI: WindowAPI
|
||||
|
||||
beforeEach(() => {
|
||||
windowAPI = new WindowAPI()
|
||||
})
|
||||
|
||||
describe("tabGroups property", () => {
|
||||
it("should have tabGroups", () => {
|
||||
expect(windowAPI.tabGroups).toBeDefined()
|
||||
})
|
||||
|
||||
it("should return TabGroupsAPI instance", () => {
|
||||
expect(typeof windowAPI.tabGroups.onDidChangeTabs).toBe("function")
|
||||
expect(Array.isArray(windowAPI.tabGroups.all)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("visibleTextEditors property", () => {
|
||||
it("should be an empty array initially", () => {
|
||||
expect(windowAPI.visibleTextEditors).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("createOutputChannel()", () => {
|
||||
it("should create an output channel with the given name", () => {
|
||||
const channel = windowAPI.createOutputChannel("TestChannel")
|
||||
|
||||
expect(channel.name).toBe("TestChannel")
|
||||
})
|
||||
|
||||
it("should return an OutputChannel instance", () => {
|
||||
const channel = windowAPI.createOutputChannel("Test")
|
||||
|
||||
expect(typeof channel.append).toBe("function")
|
||||
expect(typeof channel.appendLine).toBe("function")
|
||||
expect(typeof channel.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createStatusBarItem()", () => {
|
||||
it("should create with default alignment", () => {
|
||||
const item = windowAPI.createStatusBarItem()
|
||||
|
||||
expect(item.alignment).toBe(StatusBarAlignment.Left)
|
||||
})
|
||||
|
||||
it("should create with specified alignment", () => {
|
||||
const item = windowAPI.createStatusBarItem(StatusBarAlignment.Right)
|
||||
|
||||
expect(item.alignment).toBe(StatusBarAlignment.Right)
|
||||
})
|
||||
|
||||
it("should create with alignment and priority", () => {
|
||||
const item = windowAPI.createStatusBarItem(StatusBarAlignment.Left, 100)
|
||||
|
||||
expect(item.alignment).toBe(StatusBarAlignment.Left)
|
||||
expect(item.priority).toBe(100)
|
||||
})
|
||||
|
||||
it("should handle overloaded signature with id", () => {
|
||||
const item = windowAPI.createStatusBarItem("myId", StatusBarAlignment.Right, 50)
|
||||
|
||||
expect(item.alignment).toBe(StatusBarAlignment.Right)
|
||||
expect(item.priority).toBe(50)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createTextEditorDecorationType()", () => {
|
||||
it("should create a decoration type", () => {
|
||||
const decoration = windowAPI.createTextEditorDecorationType({})
|
||||
|
||||
expect(decoration).toBeDefined()
|
||||
expect(decoration.key).toContain("decoration-")
|
||||
})
|
||||
|
||||
it("should return unique keys", () => {
|
||||
const decoration1 = windowAPI.createTextEditorDecorationType({})
|
||||
const decoration2 = windowAPI.createTextEditorDecorationType({})
|
||||
|
||||
expect(decoration1.key).not.toBe(decoration2.key)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createTerminal()", () => {
|
||||
it("should create a terminal with default name", () => {
|
||||
const terminal = windowAPI.createTerminal()
|
||||
|
||||
expect(terminal.name).toBe("Terminal")
|
||||
})
|
||||
|
||||
it("should create a terminal with specified name", () => {
|
||||
const terminal = windowAPI.createTerminal({ name: "MyTerminal" })
|
||||
|
||||
expect(terminal.name).toBe("MyTerminal")
|
||||
})
|
||||
|
||||
it("should return terminal with expected methods", () => {
|
||||
const terminal = windowAPI.createTerminal()
|
||||
|
||||
expect(typeof terminal.sendText).toBe("function")
|
||||
expect(typeof terminal.show).toBe("function")
|
||||
expect(typeof terminal.hide).toBe("function")
|
||||
expect(typeof terminal.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("should have processId promise", async () => {
|
||||
const terminal = windowAPI.createTerminal()
|
||||
|
||||
const processId = await terminal.processId
|
||||
|
||||
expect(processId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("showInformationMessage()", () => {
|
||||
it("should return a promise", () => {
|
||||
const result = windowAPI.showInformationMessage("Test message")
|
||||
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
|
||||
it("should resolve to undefined", async () => {
|
||||
const result = await windowAPI.showInformationMessage("Test message")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("showWarningMessage()", () => {
|
||||
it("should return a promise", () => {
|
||||
const result = windowAPI.showWarningMessage("Warning message")
|
||||
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
|
||||
it("should resolve to undefined", async () => {
|
||||
const result = await windowAPI.showWarningMessage("Warning message")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("showErrorMessage()", () => {
|
||||
it("should return a promise", () => {
|
||||
const result = windowAPI.showErrorMessage("Error message")
|
||||
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
|
||||
it("should resolve to undefined", async () => {
|
||||
const result = await windowAPI.showErrorMessage("Error message")
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("showQuickPick()", () => {
|
||||
it("should return first item", async () => {
|
||||
const result = await windowAPI.showQuickPick(["item1", "item2", "item3"])
|
||||
|
||||
expect(result).toBe("item1")
|
||||
})
|
||||
|
||||
it("should return undefined for empty array", async () => {
|
||||
const result = await windowAPI.showQuickPick([])
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("showInputBox()", () => {
|
||||
it("should return empty string", async () => {
|
||||
const result = await windowAPI.showInputBox()
|
||||
|
||||
expect(result).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("showOpenDialog()", () => {
|
||||
it("should return empty array", async () => {
|
||||
const result = await windowAPI.showOpenDialog()
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("showTextDocument()", () => {
|
||||
it("should return an editor", async () => {
|
||||
const uri = Uri.file("/test/file.txt")
|
||||
const editor = await windowAPI.showTextDocument(uri)
|
||||
|
||||
expect(editor).toBeDefined()
|
||||
expect(editor.document).toBeDefined()
|
||||
})
|
||||
|
||||
it("should add editor to visibleTextEditors", async () => {
|
||||
const uri = Uri.file("/test/file.txt")
|
||||
await windowAPI.showTextDocument(uri)
|
||||
|
||||
expect(windowAPI.visibleTextEditors.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerWebviewViewProvider()", () => {
|
||||
it("should return a disposable", () => {
|
||||
const mockProvider = {
|
||||
resolveWebviewView: vi.fn(),
|
||||
}
|
||||
|
||||
const disposable = windowAPI.registerWebviewViewProvider("myView", mockProvider)
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerUriHandler()", () => {
|
||||
it("should return a disposable", () => {
|
||||
const mockHandler = {
|
||||
handleUri: vi.fn(),
|
||||
}
|
||||
|
||||
const disposable = windowAPI.registerUriHandler(mockHandler)
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("onDidChangeTextEditorSelection()", () => {
|
||||
it("should return a disposable", () => {
|
||||
const disposable = windowAPI.onDidChangeTextEditorSelection(() => {})
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("onDidChangeActiveTextEditor()", () => {
|
||||
it("should return a disposable", () => {
|
||||
const disposable = windowAPI.onDidChangeActiveTextEditor(() => {})
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("onDidChangeVisibleTextEditors()", () => {
|
||||
it("should return a disposable", () => {
|
||||
const disposable = windowAPI.onDidChangeVisibleTextEditors(() => {})
|
||||
|
||||
expect(disposable).toBeDefined()
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("terminal events", () => {
|
||||
it("onDidCloseTerminal should return disposable", () => {
|
||||
const disposable = windowAPI.onDidCloseTerminal(() => {})
|
||||
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("onDidOpenTerminal should return disposable", () => {
|
||||
const disposable = windowAPI.onDidOpenTerminal(() => {})
|
||||
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("onDidChangeActiveTerminal should return disposable", () => {
|
||||
const disposable = windowAPI.onDidChangeActiveTerminal(() => {})
|
||||
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("onDidChangeTerminalDimensions should return disposable", () => {
|
||||
const disposable = windowAPI.onDidChangeTerminalDimensions(() => {})
|
||||
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
|
||||
it("onDidWriteTerminalData should return disposable", () => {
|
||||
const disposable = windowAPI.onDidWriteTerminalData(() => {})
|
||||
|
||||
expect(typeof disposable.dispose).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("activeTerminal property", () => {
|
||||
it("should return undefined", () => {
|
||||
expect(windowAPI.activeTerminal).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("terminals property", () => {
|
||||
it("should return empty array", () => {
|
||||
expect(windowAPI.terminals).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue