mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
chore: merge main to resolve conflicts
The getKeepMessagesWithToolBlocks function and related code were removed in main, making the ROO-520 fix obsolete. Accepting main changes. Also fixed type error in useMcpToolTool.spec.ts where invalid properties were passed to ToolCallbacks.
This commit is contained in:
commit
ed47d6c3ff
568 changed files with 21384 additions and 25936 deletions
25
.github/workflows/website-preview.yml
vendored
25
.github/workflows/website-preview.yml
vendored
|
|
@ -70,15 +70,20 @@ jobs:
|
|||
comment.body.includes(commentIdentifier)
|
||||
);
|
||||
|
||||
if (existingComment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = commentIdentifier + '\n🚀 **Preview deployed!**\n\nYour changes have been deployed to Vercel:\n\n**Preview URL:** ' + deploymentUrl + '\n\nThis preview will be updated automatically when you push new commits to this PR.';
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
if (existingComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
body: comment
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,16 +48,58 @@ mode: code
|
|||
- Include links to relevant source files where helpful
|
||||
- Describe changes from the user's perspective
|
||||
|
||||
5. Commit the version bump and changelog update:
|
||||
5. Create a release branch and commit the changes:
|
||||
|
||||
```bash
|
||||
# Ensure you're on main and up to date
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Create a new branch for the release
|
||||
git checkout -b cli-release-v<version>
|
||||
|
||||
# Commit the version bump and changelog update
|
||||
git add apps/cli/package.json apps/cli/CHANGELOG.md
|
||||
git commit -m "chore(cli): prepare release v<version>"
|
||||
|
||||
# Push the branch to origin
|
||||
git push -u origin cli-release-v<version>
|
||||
```
|
||||
|
||||
6. Run the release script from the monorepo root:
|
||||
6. Create a pull request for the release:
|
||||
|
||||
```bash
|
||||
gh pr create --title "chore(cli): prepare release v<version>" \
|
||||
--body "## CLI Release v<version>
|
||||
|
||||
This PR prepares the CLI release v<version>.
|
||||
|
||||
### Changes
|
||||
- Version bump in package.json
|
||||
- Changelog update
|
||||
|
||||
### Checklist
|
||||
- [ ] Version number is correct
|
||||
- [ ] Changelog entry is complete and accurate
|
||||
- [ ] All CI checks pass" \
|
||||
--base main
|
||||
```
|
||||
|
||||
7. Wait for PR approval and merge:
|
||||
|
||||
- Request review if required by your workflow
|
||||
- Ensure CI checks pass
|
||||
- Merge the PR using: `gh pr merge --squash --delete-branch`
|
||||
- Or merge via the GitHub UI
|
||||
|
||||
8. Run the release script from the monorepo root:
|
||||
|
||||
```bash
|
||||
# Ensure you're on the updated main branch after the PR merge
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Run the release script
|
||||
./apps/cli/scripts/release.sh
|
||||
```
|
||||
|
||||
|
|
@ -69,7 +111,7 @@ mode: code
|
|||
- Extract changelog content and include it in the GitHub release notes
|
||||
- Create the GitHub release with the tarball attached
|
||||
|
||||
7. After a successful release, verify:
|
||||
9. After a successful release, verify:
|
||||
- Check the release page: https://github.com/RooCodeInc/Roo-Code/releases
|
||||
- Verify the "What's New" section contains the changelog content
|
||||
- Test installation: `curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh`
|
||||
|
|
|
|||
5
AGENTS.md
Normal file
5
AGENTS.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# AGENTS.md
|
||||
|
||||
This file provides guidance to agents when working with code in this repository.
|
||||
|
||||
- Settings View Pattern: When working on `SettingsView`, inputs must bind to the local `cachedState`, NOT the live `useExtensionState()`. The `cachedState` acts as a buffer for user edits, isolating them from the `ContextProxy` source-of-truth until the user explicitly clicks "Save". Wiring inputs directly to the live state causes race conditions.
|
||||
76
CHANGELOG.md
76
CHANGELOG.md
|
|
@ -1,5 +1,81 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.43.0] - 2026-01-23
|
||||
|
||||

|
||||
|
||||
- Intelligent Context Condensation v2: New context condensation system that intelligently summarizes conversation history when approaching context limits, preserving important information while reducing token usage (PR #10873 by @hannesrudolph)
|
||||
- Improved context condensation with environment details, accurate token counts, and lazy evaluation for better performance (PR #10920 by @hannesrudolph)
|
||||
- Move condense prompt editor to Context Management tab for better discoverability and organization (PR #10909 by @hannesrudolph)
|
||||
- Update Z.AI models with new variants and pricing (#10859 by @ErdemGKSL, PR #10860 by @ErdemGKSL)
|
||||
- Add pnpm install:vsix:nightly command for easier nightly build installation (PR #10912 by @hannesrudolph)
|
||||
- Fix: Convert orphaned tool_results to text blocks after condensing to prevent API errors (PR #10927 by @daniel-lxs)
|
||||
- Fix: Auto-migrate v1 condensing prompt and handle invalid providers on import (PR #10931 by @hannesrudolph)
|
||||
- Fix: Use json-stream-stringify for pretty-printing MCP config files to prevent memory issues with large configs (#9862 by @Michaelzag, PR #9864 by @Michaelzag)
|
||||
- Fix: Correct Gemini 3 pricing for Flash and Pro models (#10432 by @rossdonald, PR #10487 by @roomote)
|
||||
- Fix: Skip thoughtSignature blocks during markdown export for cleaner output (#10199 by @rossdonald, PR #10932 by @rossdonald)
|
||||
- Fix: Duplicate model display for OpenAI Codex provider (PR #10930 by @roomote)
|
||||
- Remove diffEnabled and fuzzyMatchThreshold settings as they are no longer needed (#10648 by @hannesrudolph, PR #10298 by @hannesrudolph)
|
||||
- Remove MULTI_FILE_APPLY_DIFF experiment (PR #10925 by @hannesrudolph)
|
||||
- Remove POWER_STEERING experimental feature (PR #10926 by @hannesrudolph)
|
||||
- Remove legacy XML tool calling code (getToolDescription) for cleaner codebase (PR #10929 by @hannesrudolph)
|
||||
|
||||
## [3.42.0] - 2026-01-22
|
||||
|
||||

|
||||
|
||||
- Added UI to track your ChatGPT usage limits in the OpenAI Codex provider (PR #10813 by @hannesrudolph)
|
||||
- Removed deprecated Claude Code provider (PR #10883 by @daniel-lxs)
|
||||
- Streamlined codebase by removing legacy XML tool calling functionality (#10848 by @hannesrudolph, PR #10841 by @hannesrudolph)
|
||||
- Standardize model selectors across all providers: Improved consistency of model selection UI (#10650 by @hannesrudolph, PR #10294 by @hannesrudolph)
|
||||
- Enable prompt caching for Cerebras zai-glm-4.7 model (#10601 by @jahanson, PR #10670 by @app/roomote)
|
||||
- Add Kimi K2 thinking model to VertexAI provider (#9268 by @diwakar-s-maurya, PR #9269 by @app/roomote)
|
||||
- Warn users when too many MCP tools are enabled (PR #10772 by @app/roomote)
|
||||
- Migrate context condensing prompt to customSupportPrompts (PR #10881 by @hannesrudolph)
|
||||
- Unify export path logic and default to Downloads folder (PR #10882 by @hannesrudolph)
|
||||
- Performance improvements for webview state synchronization (PR #10842 by @hannesrudolph)
|
||||
- Fix: Handle mode selector empty state on workspace switch (#10660 by @hannesrudolph, PR #9674 by @app/roomote)
|
||||
- Fix: Resolve race condition in context condensing prompt input (PR #10876 by @hannesrudolph)
|
||||
- Fix: Prevent double emission of text/reasoning in OpenAI native and codex handlers (PR #10888 by @hannesrudolph)
|
||||
- Fix: Prevent task abortion when resuming via IPC/bridge (PR #10892 by @cte)
|
||||
- Fix: Enforce file restrictions for all editing tools (PR #10896 by @app/roomote)
|
||||
- Fix: Remove custom condensing model option (PR #10901 by @hannesrudolph)
|
||||
- Unify user content tags to <user_message> for consistent prompt formatting (#10658 by @hannesrudolph, PR #10723 by @app/roomote)
|
||||
- Clarify linked SKILL.md file handling in prompts (PR #10907 by @hannesrudolph)
|
||||
- Fix: Padding on Roo Code Cloud teaser (PR #10889 by @app/roomote)
|
||||
|
||||
## [3.41.3] - 2026-01-18
|
||||
|
||||
- Fix: Thinking block word-breaking to prevent horizontal scroll in the chat UI (PR #10806 by @roomote)
|
||||
- Add Claude-like CLI flags and authentication fixes for the Roo Code CLI (PR #10797 by @cte)
|
||||
- Improve CLI authentication by using a redirect instead of a fetch (PR #10799 by @cte)
|
||||
- Fix: Roo Code Router fixes for the CLI (PR #10789 by @cte)
|
||||
- Release CLI v0.0.48 with latest improvements (PR #10800 by @cte)
|
||||
- Release CLI v0.0.47 (PR #10798 by @cte)
|
||||
- Revert E2E tests enablement to address stability issues (PR #10794 by @cte)
|
||||
|
||||
## [3.41.2] - 2026-01-16
|
||||
|
||||
- Add button to open markdown in VSCode preview for easier reading of formatted content (PR #10773 by @brunobergher)
|
||||
- Fix: Reset invalid model selection when using OpenAI Codex provider (PR #10777 by @hannesrudolph)
|
||||
- Fix: Add openai-codex to providers that don't require an API key (PR #10786 by @roomote)
|
||||
- Fix: Detect Gemini models with space-separated names for proper thought signature injection in LiteLLM (PR #10787 by @daniel-lxs)
|
||||
|
||||
## [3.41.1] - 2026-01-16
|
||||
|
||||

|
||||
|
||||
- Feat: Aggregate subtask costs in parent task (#5376 by @hannesrudolph, PR #10757 by @taltas)
|
||||
- Fix: Prevent duplicate tool_use IDs causing API 400 errors (PR #10760 by @daniel-lxs)
|
||||
- Fix: Handle missing tool identity in OpenAI Native streams (PR #10719 by @hannesrudolph)
|
||||
- Fix: Truncate call_id to 64 chars for OpenAI Responses API (PR #10763 by @daniel-lxs)
|
||||
- Fix: Gemini thought signature validation errors (PR #10694 by @daniel-lxs)
|
||||
- Fix: Filter out empty text blocks from user messages for Gemini compatibility (PR #10728 by @daniel-lxs)
|
||||
- Fix: Flatten top-level anyOf/oneOf/allOf in MCP tool schemas (PR #10726 by @daniel-lxs)
|
||||
- Fix: Filter Ollama models without native tool support (PR #10735 by @daniel-lxs)
|
||||
- Feat: Add settings tab titles to search index (PR #10761 by @roomote)
|
||||
- Feat: Clarify Slack and Linear are Cloud Team only features (PR #10748 by @roomote)
|
||||
|
||||
## [3.41.0] - 2026-01-15
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -5,6 +5,41 @@ All notable changes to the `@roo-code/cli` package will be documented in this fi
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.0.49] - 2026-01-18
|
||||
|
||||
### Added
|
||||
|
||||
- **Output Format Options**: New `--output-format` flag to control CLI output format for scripting and automation:
|
||||
- `text` (default) - Human-readable interactive output
|
||||
- `json` - Single JSON object with all events and final result at task completion
|
||||
- `stream-json` - NDJSON (newline-delimited JSON) for real-time streaming of events
|
||||
- See [`json-events.ts`](src/types/json-events.ts) for the complete event schema
|
||||
- New [`JsonEventEmitter`](src/agent/json-event-emitter.ts) for structured output generation
|
||||
|
||||
## [0.0.48] - 2026-01-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Simplified authentication callback flow by using HTTP redirects instead of POST requests with CORS headers for improved browser compatibility
|
||||
|
||||
## [0.0.47] - 2026-01-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Workspace flag**: New `-w, --workspace <path>` option to specify a custom workspace directory instead of using the current working directory
|
||||
- **Oneshot mode**: New `--oneshot` flag to exit upon task completion, useful for scripting and automation (can also be saved in settings via [`CliSettings.oneshot`](src/types/types.ts))
|
||||
|
||||
### Changed
|
||||
|
||||
- Skip onboarding flow when a provider is explicitly specified via `--provider` flag or saved in settings
|
||||
- Unified permission flags: Combined `-y`, `--yes`, and `--dangerously-skip-permissions` into a single option for Claude Code-like CLI compatibility
|
||||
- Improved Roo Code Router authentication flow and error messaging
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed unnecessary timeout that could cause issues with long-running tasks
|
||||
- Fixed authentication token validation for Roo Code Router provider
|
||||
|
||||
## [0.0.45] - 2026-01-08
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ By default, the CLI prompts for approval before executing actions:
|
|||
```bash
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
||||
roo ~/Documents/my-project -P "What is this project?"
|
||||
roo "What is this project?" -w ~/Documents/my-project
|
||||
```
|
||||
|
||||
You can also run without a prompt and enter it interactively in TUI mode:
|
||||
|
|
@ -92,7 +92,7 @@ In interactive mode:
|
|||
For automation and scripts, use `-y` to auto-approve all actions:
|
||||
|
||||
```bash
|
||||
roo ~/Documents/my-project -y -P "Refactor the utils.ts file"
|
||||
roo "Refactor the utils.ts file" -y -w ~/Documents/my-project
|
||||
```
|
||||
|
||||
In non-interactive mode:
|
||||
|
|
@ -149,8 +149,8 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
|
|||
|
||||
| Option | Description | Default |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------- |
|
||||
| `[workspace]` | Workspace path to operate in (positional argument) | Current directory |
|
||||
| `-P, --prompt <prompt>` | The prompt/task to execute (optional in TUI mode) | None |
|
||||
| `[prompt]` | Your prompt (positional argument, optional) | None |
|
||||
| `-w, --workspace <path>` | Workspace path to operate in | Current directory |
|
||||
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
|
||||
| `-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` |
|
||||
|
|
@ -249,7 +249,7 @@ pnpm lint
|
|||
To create a new release, execute the /cli-release slash command:
|
||||
|
||||
```bash
|
||||
roo ~/Documents/Roo-Code -P "/cli-release" -y
|
||||
roo "/cli-release" -w ~/Documents/Roo-Code -y
|
||||
```
|
||||
|
||||
The workflow will:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@roo-code/cli",
|
||||
"version": "0.0.45",
|
||||
"version": "0.0.49",
|
||||
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
@ -14,8 +14,10 @@
|
|||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"build": "tsup",
|
||||
"build:extension": "pnpm --filter roo-cline bundle",
|
||||
"build:all": "pnpm --filter roo-cline bundle && tsup",
|
||||
"dev": "tsup --watch",
|
||||
"start": "ROO_SDK_BASE_URL=http://localhost:3001 ROO_AUTH_BASE_URL=http://localhost:3000 node dist/index.js",
|
||||
"start": "ROO_AUTH_BASE_URL=http://localhost:3000 ROO_SDK_BASE_URL=http://localhost:3001 ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy node dist/index.js",
|
||||
"start:production": "node dist/index.js",
|
||||
"release": "scripts/release.sh",
|
||||
"clean": "rimraf dist .turbo"
|
||||
|
|
@ -28,6 +30,8 @@
|
|||
"@trpc/client": "^11.8.1",
|
||||
"@vscode/ripgrep": "^1.15.9",
|
||||
"commander": "^12.1.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"execa": "^9.5.2",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"ink": "^6.6.0",
|
||||
"p-wait-for": "^5.0.2",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ create_tarball() {
|
|||
'commander': pkg.dependencies.commander,
|
||||
'fuzzysort': pkg.dependencies.fuzzysort,
|
||||
'ink': pkg.dependencies.ink,
|
||||
'p-wait-for': pkg.dependencies['p-wait-for'],
|
||||
'react': pkg.dependencies.react,
|
||||
'superjson': pkg.dependencies.superjson,
|
||||
'zustand': pkg.dependencies.zustand
|
||||
|
|
@ -420,8 +421,7 @@ verify_local_install() {
|
|||
mkdir -p "$VERIFY_WORKSPACE"
|
||||
|
||||
# Run the CLI with a simple prompt
|
||||
# Use timeout to prevent hanging if something goes wrong
|
||||
if timeout 60 "$VERIFY_BIN_DIR/roo" --yes --exit-on-complete --prompt "1+1=?" "$VERIFY_WORKSPACE" > "$VERIFY_DIR/test-output.log" 2>&1; then
|
||||
if timeout 60 "$VERIFY_BIN_DIR/roo" --yes --oneshot -w "$VERIFY_WORKSPACE" "1+1=?" > "$VERIFY_DIR/test-output.log" 2>&1; then
|
||||
info "End-to-end test passed"
|
||||
else
|
||||
EXIT_CODE=$?
|
||||
|
|
@ -536,11 +536,8 @@ ROO_VERSION=$VERSION curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo
|
|||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
# Set your API key
|
||||
export OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
||||
# Run a task
|
||||
roo "What is this project?" ~/my-project
|
||||
roo "What is this project?"
|
||||
|
||||
# See all options
|
||||
roo --help
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ function createTestHost({
|
|||
model,
|
||||
workspacePath: "/test/workspace",
|
||||
extensionPath: "/test/extension",
|
||||
ephemeral: false,
|
||||
debug: false,
|
||||
exitOnComplete: false,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
|
@ -94,16 +97,20 @@ describe("ExtensionHost", () => {
|
|||
apiKey: "test-key",
|
||||
provider: "openrouter",
|
||||
model: "test-model",
|
||||
ephemeral: false,
|
||||
debug: false,
|
||||
exitOnComplete: false,
|
||||
integrationTest: true, // Set explicitly for testing
|
||||
}
|
||||
|
||||
const host = new ExtensionHost(options)
|
||||
|
||||
// Options are stored but integrationTest is set to true
|
||||
// Options are stored as-is
|
||||
const storedOptions = getPrivate<ExtensionHostOptions>(host, "options")
|
||||
expect(storedOptions.mode).toBe(options.mode)
|
||||
expect(storedOptions.workspacePath).toBe(options.workspacePath)
|
||||
expect(storedOptions.extensionPath).toBe(options.extensionPath)
|
||||
expect(storedOptions.integrationTest).toBe(true) // Always set to true in constructor
|
||||
expect(storedOptions.integrationTest).toBe(true)
|
||||
})
|
||||
|
||||
it("should be an EventEmitter instance", () => {
|
||||
|
|
@ -292,16 +299,19 @@ describe("ExtensionHost", () => {
|
|||
})
|
||||
|
||||
it("should suppress console when integrationTest is false", () => {
|
||||
const host = createTestHost()
|
||||
// Capture the real console.log before any host is created
|
||||
const originalLog = console.log
|
||||
|
||||
// Override integrationTest to false
|
||||
// Create host with integrationTest: true to prevent constructor from suppressing
|
||||
const host = createTestHost({ integrationTest: true })
|
||||
|
||||
// Override integrationTest to false to test suppression
|
||||
const options = getPrivate<ExtensionHostOptions>(host, "options")
|
||||
options.integrationTest = false
|
||||
|
||||
callPrivate(host, "setupQuietMode")
|
||||
|
||||
// Console should be modified
|
||||
// Console should be modified (suppressed)
|
||||
expect(console.log).not.toBe(originalLog)
|
||||
|
||||
// Restore for other tests
|
||||
|
|
@ -326,9 +336,12 @@ describe("ExtensionHost", () => {
|
|||
|
||||
describe("restoreConsole", () => {
|
||||
it("should restore original console methods when suppressed", () => {
|
||||
const host = createTestHost()
|
||||
// Capture the real console.log before any host is created
|
||||
const originalLog = console.log
|
||||
|
||||
// Create host with integrationTest: true to prevent constructor from suppressing
|
||||
const host = createTestHost({ integrationTest: true })
|
||||
|
||||
// Override integrationTest to false to actually suppress
|
||||
const options = getPrivate<ExtensionHostOptions>(host, "options")
|
||||
options.integrationTest = false
|
||||
|
|
|
|||
|
|
@ -58,16 +58,17 @@ export interface ExtensionHostOptions {
|
|||
workspacePath: string
|
||||
extensionPath: string
|
||||
nonInteractive?: boolean
|
||||
debug?: boolean
|
||||
/**
|
||||
* When true, uses a temporary storage directory that is cleaned up on exit.
|
||||
*/
|
||||
ephemeral: boolean
|
||||
debug: boolean
|
||||
exitOnComplete: boolean
|
||||
/**
|
||||
* When true, completely disables all direct stdout/stderr output.
|
||||
* Use this when running in TUI mode where Ink controls the terminal.
|
||||
*/
|
||||
disableOutput?: boolean
|
||||
/**
|
||||
* When true, uses a temporary storage directory that is cleaned up on exit.
|
||||
*/
|
||||
ephemeral?: boolean
|
||||
/**
|
||||
* When true, don't suppress node warnings and console output since we're
|
||||
* running in an integration test and we want to see the output.
|
||||
|
|
@ -152,7 +153,10 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
super()
|
||||
|
||||
this.options = options
|
||||
this.options.integrationTest = true
|
||||
|
||||
// Set up quiet mode early, before any extension code runs.
|
||||
// This suppresses console output from the extension during load.
|
||||
this.setupQuietMode()
|
||||
|
||||
// Initialize client - single source of truth for agent state (including mode).
|
||||
this.client = new ExtensionClient({
|
||||
|
|
@ -161,9 +165,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
})
|
||||
|
||||
// Initialize output manager.
|
||||
this.outputManager = new OutputManager({
|
||||
disabled: options.disableOutput,
|
||||
})
|
||||
this.outputManager = new OutputManager({ disabled: options.disableOutput })
|
||||
|
||||
// Initialize prompt manager with console mode callbacks.
|
||||
this.promptManager = new PromptManager({
|
||||
|
|
@ -221,8 +223,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
this.initialSettings.reasoningEffort = this.options.reasoningEffort
|
||||
}
|
||||
}
|
||||
|
||||
this.setupQuietMode()
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
|
|
@ -266,7 +266,8 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
// ==========================================================================
|
||||
|
||||
private setupQuietMode(): void {
|
||||
if (this.options.integrationTest) {
|
||||
// Skip if already set up or if integrationTest mode
|
||||
if (this.originalConsole || this.options.integrationTest) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -291,18 +292,16 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
}
|
||||
|
||||
private restoreConsole(): void {
|
||||
if (this.options.integrationTest) {
|
||||
if (!this.originalConsole) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.originalConsole) {
|
||||
console.log = this.originalConsole.log
|
||||
console.warn = this.originalConsole.warn
|
||||
console.error = this.originalConsole.error
|
||||
console.debug = this.originalConsole.debug
|
||||
console.info = this.originalConsole.info
|
||||
this.originalConsole = null
|
||||
}
|
||||
console.log = this.originalConsole.log
|
||||
console.warn = this.originalConsole.warn
|
||||
console.error = this.originalConsole.error
|
||||
console.debug = this.originalConsole.debug
|
||||
console.info = this.originalConsole.info
|
||||
this.originalConsole = null
|
||||
|
||||
if (this.originalProcessEmitWarning) {
|
||||
process.emitWarning = this.originalProcessEmitWarning
|
||||
|
|
@ -436,9 +435,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
this.sendToExtension({ type: "newTask", text: prompt })
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | null = null
|
||||
const timeoutMs: number = 110_000
|
||||
|
||||
const completeHandler = () => {
|
||||
cleanup()
|
||||
resolve()
|
||||
|
|
@ -450,23 +446,10 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = null
|
||||
}
|
||||
|
||||
this.client.off("taskCompleted", completeHandler)
|
||||
this.client.off("error", errorHandler)
|
||||
}
|
||||
|
||||
// Set timeout to prevent indefinite hanging.
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(
|
||||
new Error(`Task completion timeout after ${timeoutMs}ms - no completion or error event received`),
|
||||
)
|
||||
}, timeoutMs)
|
||||
|
||||
this.client.once("taskCompleted", completeHandler)
|
||||
this.client.once("error", errorHandler)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "./extension-host.js"
|
||||
export * from "./json-event-emitter.js"
|
||||
|
|
|
|||
464
apps/cli/src/agent/json-event-emitter.ts
Normal file
464
apps/cli/src/agent/json-event-emitter.ts
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
/**
|
||||
* JsonEventEmitter - Handles structured JSON output for the CLI
|
||||
*
|
||||
* This class transforms internal CLI events (ClineMessage, state changes, etc.)
|
||||
* into structured JSON events and outputs them to stdout.
|
||||
*
|
||||
* Supports two output modes:
|
||||
* - "stream-json": NDJSON format (one JSON object per line) for real-time streaming
|
||||
* - "json": Single JSON object at the end with accumulated events
|
||||
*
|
||||
* Schema is optimized for efficiency with high message volume:
|
||||
* - Minimal fields per event
|
||||
* - No redundant wrappers
|
||||
* - `done` flag instead of partial:false
|
||||
*/
|
||||
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
|
||||
import type { JsonEvent, JsonEventCost, JsonFinalOutput } from "@/types/json-events.js"
|
||||
|
||||
import type { ExtensionClient } from "./extension-client.js"
|
||||
import type { TaskCompletedEvent } from "./events.js"
|
||||
|
||||
/**
|
||||
* Options for JsonEventEmitter.
|
||||
*/
|
||||
export interface JsonEventEmitterOptions {
|
||||
/** Output mode: "json" or "stream-json" */
|
||||
mode: "json" | "stream-json"
|
||||
/** Output stream (defaults to process.stdout) */
|
||||
stdout?: NodeJS.WriteStream
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tool information from a ClineMessage text field.
|
||||
* Tool messages are JSON with a `tool` field containing the tool name.
|
||||
*/
|
||||
function parseToolInfo(text: string | undefined): { name: string; input: Record<string, unknown> } | null {
|
||||
if (!text) return null
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
return parsed.tool ? { name: parsed.tool, input: parsed } : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse API request cost information from api_req_started message text.
|
||||
*/
|
||||
function parseApiReqCost(text: string | undefined): JsonEventCost | undefined {
|
||||
if (!text) return undefined
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
return parsed.cost !== undefined
|
||||
? {
|
||||
totalCost: parsed.cost,
|
||||
inputTokens: parsed.tokensIn,
|
||||
outputTokens: parsed.tokensOut,
|
||||
cacheWrites: parsed.cacheWrites,
|
||||
cacheReads: parsed.cacheReads,
|
||||
}
|
||||
: undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal events that should not be emitted */
|
||||
const SKIP_SAY_TYPES = new Set([
|
||||
"api_req_finished",
|
||||
"api_req_retried",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_rate_limit_wait",
|
||||
"api_req_deleted",
|
||||
"checkpoint_saved",
|
||||
"condense_context",
|
||||
"condense_context_error",
|
||||
"sliding_window_truncation",
|
||||
])
|
||||
|
||||
/** Key offset for reasoning content to avoid collision with text content delta tracking */
|
||||
const REASONING_KEY_OFFSET = 1_000_000_000
|
||||
|
||||
export class JsonEventEmitter {
|
||||
private mode: "json" | "stream-json"
|
||||
private stdout: NodeJS.WriteStream
|
||||
private events: JsonEvent[] = []
|
||||
private unsubscribers: (() => void)[] = []
|
||||
private lastCost: JsonEventCost | undefined
|
||||
private seenMessageIds = new Set<number>()
|
||||
// Track previous content for delta computation
|
||||
private previousContent = new Map<number, string>()
|
||||
// Track the completion result content
|
||||
private completionResultContent: string | undefined
|
||||
|
||||
constructor(options: JsonEventEmitterOptions) {
|
||||
this.mode = options.mode
|
||||
this.stdout = options.stdout ?? process.stdout
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach to an ExtensionClient and subscribe to its events.
|
||||
*/
|
||||
attachToClient(client: ExtensionClient): void {
|
||||
// Subscribe to message events
|
||||
const unsubMessage = client.on("message", (msg) => this.handleMessage(msg, false))
|
||||
const unsubMessageUpdated = client.on("messageUpdated", (msg) => this.handleMessage(msg, true))
|
||||
const unsubTaskCompleted = client.on("taskCompleted", (event) => this.handleTaskCompleted(event))
|
||||
const unsubError = client.on("error", (error) => this.handleError(error))
|
||||
|
||||
this.unsubscribers.push(unsubMessage, unsubMessageUpdated, unsubTaskCompleted, unsubError)
|
||||
|
||||
// Emit init event
|
||||
this.emitEvent({
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
content: "Task started",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach from the client and clean up subscriptions.
|
||||
*/
|
||||
detach(): void {
|
||||
for (const unsub of this.unsubscribers) {
|
||||
unsub()
|
||||
}
|
||||
this.unsubscribers = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the delta (new content) for a streaming message.
|
||||
* Returns null if there's no new content.
|
||||
*/
|
||||
private computeDelta(msgId: number, fullContent: string | undefined): string | null {
|
||||
if (!fullContent) return null
|
||||
|
||||
const previous = this.previousContent.get(msgId) || ""
|
||||
if (fullContent === previous) return null
|
||||
|
||||
this.previousContent.set(msgId, fullContent)
|
||||
// If content is appended, return only the new part
|
||||
return fullContent.startsWith(previous) ? fullContent.slice(previous.length) : fullContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a streaming partial message with no new content.
|
||||
*/
|
||||
private isEmptyStreamingDelta(content: string | null): boolean {
|
||||
return this.mode === "stream-json" && content === null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content to send for a message (delta for streaming, full for json mode).
|
||||
*/
|
||||
private getContentToSend(msgId: number, text: string | undefined, isPartial: boolean): string | null {
|
||||
if (this.mode === "stream-json" && isPartial) {
|
||||
return this.computeDelta(msgId, text)
|
||||
}
|
||||
return text ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a base event with optional done flag.
|
||||
*/
|
||||
private buildTextEvent(
|
||||
type: "assistant" | "thinking" | "user",
|
||||
id: number,
|
||||
content: string | null,
|
||||
isDone: boolean,
|
||||
subtype?: string,
|
||||
): JsonEvent {
|
||||
const event: JsonEvent = { type, id }
|
||||
if (content !== null) {
|
||||
event.content = content
|
||||
}
|
||||
if (subtype) {
|
||||
event.subtype = subtype
|
||||
}
|
||||
if (isDone) {
|
||||
event.done = true
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a ClineMessage and emit the appropriate JSON event.
|
||||
*/
|
||||
private handleMessage(msg: ClineMessage, _isUpdate: boolean): void {
|
||||
const isDone = !msg.partial
|
||||
|
||||
// In json mode, only emit complete (non-partial) messages
|
||||
if (this.mode === "json" && msg.partial) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip duplicate complete messages
|
||||
if (isDone && this.seenMessageIds.has(msg.ts)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isDone) {
|
||||
this.seenMessageIds.add(msg.ts)
|
||||
this.previousContent.delete(msg.ts)
|
||||
}
|
||||
|
||||
const contentToSend = this.getContentToSend(msg.ts, msg.text, msg.partial ?? false)
|
||||
|
||||
// Skip if no new content for streaming partial messages
|
||||
if (msg.partial && this.isEmptyStreamingDelta(contentToSend)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.type === "say" && msg.say) {
|
||||
this.handleSayMessage(msg, contentToSend, isDone)
|
||||
}
|
||||
|
||||
if (msg.type === "ask" && msg.ask) {
|
||||
this.handleAskMessage(msg, contentToSend, isDone)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "say" type messages.
|
||||
*/
|
||||
private handleSayMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void {
|
||||
switch (msg.say) {
|
||||
case "text":
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone))
|
||||
break
|
||||
|
||||
case "reasoning":
|
||||
this.handleReasoningMessage(msg, isDone)
|
||||
break
|
||||
|
||||
case "error":
|
||||
this.emitEvent({ type: "error", id: msg.ts, content: contentToSend ?? undefined })
|
||||
break
|
||||
|
||||
case "command_output":
|
||||
this.emitEvent({
|
||||
type: "tool_result",
|
||||
tool_result: { name: "execute_command", output: msg.text },
|
||||
})
|
||||
break
|
||||
|
||||
case "user_feedback":
|
||||
case "user_feedback_diff":
|
||||
this.emitEvent(this.buildTextEvent("user", msg.ts, contentToSend, isDone))
|
||||
break
|
||||
|
||||
case "api_req_started": {
|
||||
const cost = parseApiReqCost(msg.text)
|
||||
if (cost) {
|
||||
this.lastCost = cost
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "browser_action":
|
||||
case "browser_action_result":
|
||||
this.emitEvent({
|
||||
type: "tool_result",
|
||||
subtype: "browser",
|
||||
tool_result: { name: "browser_action", output: msg.text },
|
||||
})
|
||||
break
|
||||
|
||||
case "mcp_server_response":
|
||||
this.emitEvent({
|
||||
type: "tool_result",
|
||||
subtype: "mcp",
|
||||
tool_result: { name: "mcp_server", output: msg.text },
|
||||
})
|
||||
break
|
||||
|
||||
case "completion_result":
|
||||
if (msg.text && !msg.partial) {
|
||||
this.completionResultContent = msg.text
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
if (SKIP_SAY_TYPES.has(msg.say!)) {
|
||||
break
|
||||
}
|
||||
if (msg.text) {
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, msg.say))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle reasoning/thinking messages with separate delta tracking.
|
||||
*/
|
||||
private handleReasoningMessage(msg: ClineMessage, isDone: boolean): void {
|
||||
const reasoningContent = msg.reasoning || msg.text
|
||||
const reasoningKey = msg.ts + REASONING_KEY_OFFSET
|
||||
const reasoningDelta = this.getContentToSend(reasoningKey, reasoningContent, msg.partial ?? false)
|
||||
|
||||
if (msg.partial && this.isEmptyStreamingDelta(reasoningDelta)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!msg.partial) {
|
||||
this.previousContent.delete(reasoningKey)
|
||||
}
|
||||
|
||||
this.emitEvent(this.buildTextEvent("thinking", msg.ts, reasoningDelta, isDone))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "ask" type messages.
|
||||
*/
|
||||
private handleAskMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void {
|
||||
switch (msg.ask) {
|
||||
case "tool": {
|
||||
const toolInfo = parseToolInfo(msg.text)
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "tool",
|
||||
tool_use: toolInfo ?? { name: "unknown_tool", input: { raw: msg.text } },
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "command":
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "command",
|
||||
tool_use: { name: "execute_command", input: { command: msg.text } },
|
||||
})
|
||||
break
|
||||
|
||||
case "browser_action_launch":
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "browser",
|
||||
tool_use: { name: "browser_action", input: { raw: msg.text } },
|
||||
})
|
||||
break
|
||||
|
||||
case "use_mcp_server":
|
||||
this.emitEvent({
|
||||
type: "tool_use",
|
||||
id: msg.ts,
|
||||
subtype: "mcp",
|
||||
tool_use: { name: "mcp_server", input: { raw: msg.text } },
|
||||
})
|
||||
break
|
||||
|
||||
case "followup":
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, "followup"))
|
||||
break
|
||||
|
||||
case "command_output":
|
||||
// Handled in say type
|
||||
break
|
||||
|
||||
case "completion_result":
|
||||
if (msg.text && !msg.partial) {
|
||||
this.completionResultContent = msg.text
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
if (msg.text) {
|
||||
this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, msg.ask))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle task completion and emit result event.
|
||||
*/
|
||||
private handleTaskCompleted(event: TaskCompletedEvent): void {
|
||||
// Use tracked completion result content, falling back to event message
|
||||
const resultContent = this.completionResultContent || event.message?.text
|
||||
|
||||
this.emitEvent({
|
||||
type: "result",
|
||||
id: event.message?.ts ?? Date.now(),
|
||||
content: resultContent,
|
||||
done: true,
|
||||
success: event.success,
|
||||
cost: this.lastCost,
|
||||
})
|
||||
|
||||
// For "json" mode, output the final accumulated result
|
||||
if (this.mode === "json") {
|
||||
this.outputFinalResult(event.success, resultContent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors and emit error event.
|
||||
*/
|
||||
private handleError(error: Error): void {
|
||||
this.emitEvent({
|
||||
type: "error",
|
||||
id: Date.now(),
|
||||
content: error.message,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a JSON event.
|
||||
* For stream-json mode: immediately output to stdout
|
||||
* For json mode: accumulate for final output
|
||||
*/
|
||||
private emitEvent(event: JsonEvent): void {
|
||||
this.events.push(event)
|
||||
|
||||
if (this.mode === "stream-json") {
|
||||
this.outputLine(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a single JSON line (NDJSON format).
|
||||
*/
|
||||
private outputLine(data: unknown): void {
|
||||
this.stdout.write(JSON.stringify(data) + "\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the final accumulated result (for "json" mode).
|
||||
*/
|
||||
private outputFinalResult(success: boolean, content?: string): void {
|
||||
const output: JsonFinalOutput = {
|
||||
type: "result",
|
||||
success,
|
||||
content,
|
||||
cost: this.lastCost,
|
||||
events: this.events.filter((e) => e.type !== "result"), // Exclude the result event itself
|
||||
}
|
||||
|
||||
this.stdout.write(JSON.stringify(output, null, 2) + "\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accumulated events (for testing or external use).
|
||||
*/
|
||||
getEvents(): JsonEvent[] {
|
||||
return [...this.events]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear accumulated events and state.
|
||||
*/
|
||||
clear(): void {
|
||||
this.events = []
|
||||
this.lastCost = undefined
|
||||
this.seenMessageIds.clear()
|
||||
this.previousContent.clear()
|
||||
this.completionResultContent = undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,15 @@ export interface LoginOptions {
|
|||
verbose?: boolean
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
success: boolean
|
||||
error?: string
|
||||
userId?: string
|
||||
orgId?: string | null
|
||||
}
|
||||
export type LoginResult =
|
||||
| {
|
||||
success: true
|
||||
token: string
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: string
|
||||
}
|
||||
|
||||
const LOCALHOST = "127.0.0.1"
|
||||
|
||||
|
|
@ -43,11 +46,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
|
|||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=error-in-callback`)
|
||||
errorUrl.searchParams.set("message", error)
|
||||
res.writeHead(302, { Location: errorUrl.toString() })
|
||||
res.end()
|
||||
// Wait for response to be fully sent before closing server and rejecting.
|
||||
// The 'close' event fires when the underlying connection is terminated,
|
||||
// ensuring the browser has received the redirect before we shut down.
|
||||
res.on("close", () => {
|
||||
res.end(() => {
|
||||
server.close()
|
||||
reject(new Error(error))
|
||||
})
|
||||
|
|
@ -55,24 +54,21 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
|
|||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=missing-token`)
|
||||
errorUrl.searchParams.set("message", "Missing token in callback")
|
||||
res.writeHead(302, { Location: errorUrl.toString() })
|
||||
res.end()
|
||||
res.on("close", () => {
|
||||
res.end(() => {
|
||||
server.close()
|
||||
reject(new Error("Missing token in callback"))
|
||||
})
|
||||
} else if (receivedState !== state) {
|
||||
const errorUrl = new URL(`${AUTH_BASE_URL}/cli/sign-in?error=invalid-state-parameter`)
|
||||
errorUrl.searchParams.set("message", "Invalid state parameter (possible CSRF attack)")
|
||||
errorUrl.searchParams.set("message", "Invalid state parameter")
|
||||
res.writeHead(302, { Location: errorUrl.toString() })
|
||||
res.end()
|
||||
res.on("close", () => {
|
||||
res.end(() => {
|
||||
server.close()
|
||||
reject(new Error("Invalid state parameter"))
|
||||
})
|
||||
} else {
|
||||
res.writeHead(302, { Location: `${AUTH_BASE_URL}/cli/sign-in?success=true` })
|
||||
res.end()
|
||||
res.on("close", () => {
|
||||
res.end(() => {
|
||||
server.close()
|
||||
resolve({ token, state: receivedState })
|
||||
})
|
||||
|
|
@ -90,12 +86,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
|
|||
reject(new Error("Authentication timed out"))
|
||||
}, timeout)
|
||||
|
||||
server.on("listening", () => {
|
||||
console.log(`[Auth] Callback server listening on port ${port}`)
|
||||
})
|
||||
|
||||
server.on("close", () => {
|
||||
console.log("[Auth] Callback server closed")
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
})
|
||||
|
|
@ -121,7 +112,7 @@ export async function login({ timeout = 5 * 60 * 1000, verbose = false }: LoginO
|
|||
const { token } = await tokenPromise
|
||||
await saveToken(token)
|
||||
console.log("✓ Successfully authenticated!")
|
||||
return { success: true }
|
||||
return { success: true, token }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`✗ Authentication failed: ${message}`)
|
||||
|
|
|
|||
93
apps/cli/src/commands/cli/__tests__/run.test.ts
Normal file
93
apps/cli/src/commands/cli/__tests__/run.test.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
|
||||
describe("run command --prompt-file option", () => {
|
||||
let tempDir: string
|
||||
let promptFilePath: string
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cli-test-"))
|
||||
promptFilePath = path.join(tempDir, "prompt.md")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("should read prompt from file when --prompt-file is provided", () => {
|
||||
const promptContent = `This is a test prompt with special characters:
|
||||
- Quotes: "hello" and 'world'
|
||||
- Backticks: \`code\`
|
||||
- Newlines and tabs
|
||||
- Unicode: 你好 🎉`
|
||||
|
||||
fs.writeFileSync(promptFilePath, promptContent)
|
||||
|
||||
// Verify the file was written correctly
|
||||
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||
expect(readContent).toBe(promptContent)
|
||||
})
|
||||
|
||||
it("should handle multi-line prompts correctly", () => {
|
||||
const multiLinePrompt = `Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
|
||||
Empty line above
|
||||
\tTabbed line
|
||||
Indented line`
|
||||
|
||||
fs.writeFileSync(promptFilePath, multiLinePrompt)
|
||||
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||
|
||||
expect(readContent).toBe(multiLinePrompt)
|
||||
expect(readContent.split("\n")).toHaveLength(7)
|
||||
})
|
||||
|
||||
it("should handle very long prompts that would exceed ARG_MAX", () => {
|
||||
// ARG_MAX is typically 128KB-2MB, so let's test with a 500KB prompt
|
||||
const longPrompt = "x".repeat(500 * 1024)
|
||||
|
||||
fs.writeFileSync(promptFilePath, longPrompt)
|
||||
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||
|
||||
expect(readContent.length).toBe(500 * 1024)
|
||||
expect(readContent).toBe(longPrompt)
|
||||
})
|
||||
|
||||
it("should preserve shell-sensitive characters", () => {
|
||||
const shellSensitivePrompt = `
|
||||
$HOME
|
||||
$(echo dangerous)
|
||||
\`rm -rf /\`
|
||||
"quoted string"
|
||||
'single quoted'
|
||||
$((1+1))
|
||||
&&
|
||||
||
|
||||
;
|
||||
> /dev/null
|
||||
< input.txt
|
||||
| grep something
|
||||
*
|
||||
?
|
||||
[abc]
|
||||
{a,b}
|
||||
~
|
||||
!
|
||||
#comment
|
||||
%s
|
||||
\n\t\r
|
||||
`
|
||||
|
||||
fs.writeFileSync(promptFilePath, shellSensitivePrompt)
|
||||
const readContent = fs.readFileSync(promptFilePath, "utf-8")
|
||||
|
||||
// All shell-sensitive characters should be preserved exactly
|
||||
expect(readContent).toBe(shellSensitivePrompt)
|
||||
expect(readContent).toContain("$HOME")
|
||||
expect(readContent).toContain("$(echo dangerous)")
|
||||
expect(readContent).toContain("`rm -rf /`")
|
||||
})
|
||||
})
|
||||
|
|
@ -4,7 +4,6 @@ import { fileURLToPath } from "url"
|
|||
|
||||
import { createElement } from "react"
|
||||
|
||||
import { isProviderName } from "@roo-code/types"
|
||||
import { setLogger } from "@roo-code/vscode-shim"
|
||||
|
||||
import {
|
||||
|
|
@ -12,14 +11,16 @@ import {
|
|||
isSupportedProvider,
|
||||
OnboardingProviderChoice,
|
||||
supportedProviders,
|
||||
ASCII_ROO,
|
||||
DEFAULT_FLAGS,
|
||||
REASONING_EFFORTS,
|
||||
SDK_BASE_URL,
|
||||
OutputFormat,
|
||||
} from "@/types/index.js"
|
||||
import { isValidOutputFormat } from "@/types/json-events.js"
|
||||
import { JsonEventEmitter } from "@/agent/json-event-emitter.js"
|
||||
|
||||
import { type User, createClient } from "@/lib/sdk/index.js"
|
||||
import { loadToken, hasToken, loadSettings } from "@/lib/storage/index.js"
|
||||
import { createClient } from "@/lib/sdk/index.js"
|
||||
import { loadToken, loadSettings } from "@/lib/storage/index.js"
|
||||
import { getEnvVarName, getApiKeyFromEnv } from "@/lib/utils/provider.js"
|
||||
import { runOnboarding } from "@/lib/utils/onboarding.js"
|
||||
import { getDefaultExtensionPath } from "@/lib/utils/extension.js"
|
||||
|
|
@ -29,7 +30,7 @@ import { ExtensionHost, ExtensionHostOptions } from "@/agent/index.js"
|
|||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export async function run(workspaceArg: string, options: FlagOptions) {
|
||||
export async function run(promptArg: string | undefined, flagOptions: FlagOptions) {
|
||||
setLogger({
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
|
|
@ -37,56 +38,107 @@ export async function run(workspaceArg: string, options: FlagOptions) {
|
|||
debug: () => {},
|
||||
})
|
||||
|
||||
const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY
|
||||
const isTuiEnabled = options.tui && isTuiSupported
|
||||
const extensionPath = options.extension || getDefaultExtensionPath(__dirname)
|
||||
const workspacePath = path.resolve(workspaceArg)
|
||||
let prompt = promptArg
|
||||
|
||||
if (!isSupportedProvider(options.provider)) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid provider: ${options.provider}; must be one of: ${supportedProviders.join(", ")}`,
|
||||
)
|
||||
if (flagOptions.promptFile) {
|
||||
if (!fs.existsSync(flagOptions.promptFile)) {
|
||||
console.error(`[CLI] Error: Prompt file does not exist: ${flagOptions.promptFile}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
prompt = fs.readFileSync(flagOptions.promptFile, "utf-8")
|
||||
}
|
||||
|
||||
let apiKey = options.apiKey || getApiKeyFromEnv(options.provider)
|
||||
let provider = options.provider
|
||||
let user: User | null = null
|
||||
let useCloudProvider = false
|
||||
// Options
|
||||
|
||||
if (isTuiEnabled) {
|
||||
let { onboardingProviderChoice } = await loadSettings()
|
||||
let rooToken = await loadToken()
|
||||
const settings = await loadSettings()
|
||||
|
||||
const isTuiSupported = process.stdin.isTTY && process.stdout.isTTY
|
||||
const isTuiEnabled = !flagOptions.print && isTuiSupported
|
||||
const isOnboardingEnabled = isTuiEnabled && !rooToken && !flagOptions.provider && !settings.provider
|
||||
|
||||
// Determine effective values: CLI flags > settings file > DEFAULT_FLAGS.
|
||||
const effectiveMode = flagOptions.mode || settings.mode || DEFAULT_FLAGS.mode
|
||||
const effectiveModel = flagOptions.model || settings.model || DEFAULT_FLAGS.model
|
||||
const effectiveReasoningEffort =
|
||||
flagOptions.reasoningEffort || settings.reasoningEffort || DEFAULT_FLAGS.reasoningEffort
|
||||
const effectiveProvider = flagOptions.provider ?? settings.provider ?? (rooToken ? "roo" : "openrouter")
|
||||
const effectiveWorkspacePath = flagOptions.workspace ? path.resolve(flagOptions.workspace) : process.cwd()
|
||||
const effectiveDangerouslySkipPermissions =
|
||||
flagOptions.yes || flagOptions.dangerouslySkipPermissions || settings.dangerouslySkipPermissions || false
|
||||
const effectiveExitOnComplete = flagOptions.print || flagOptions.oneshot || settings.oneshot || false
|
||||
|
||||
const extensionHostOptions: ExtensionHostOptions = {
|
||||
mode: effectiveMode,
|
||||
reasoningEffort: effectiveReasoningEffort === "unspecified" ? undefined : effectiveReasoningEffort,
|
||||
user: null,
|
||||
provider: effectiveProvider,
|
||||
model: effectiveModel,
|
||||
workspacePath: effectiveWorkspacePath,
|
||||
extensionPath: path.resolve(flagOptions.extension || getDefaultExtensionPath(__dirname)),
|
||||
nonInteractive: effectiveDangerouslySkipPermissions,
|
||||
ephemeral: flagOptions.ephemeral,
|
||||
debug: flagOptions.debug,
|
||||
exitOnComplete: effectiveExitOnComplete,
|
||||
}
|
||||
|
||||
// Roo Code Cloud Authentication
|
||||
|
||||
if (isOnboardingEnabled) {
|
||||
let { onboardingProviderChoice } = settings
|
||||
|
||||
if (!onboardingProviderChoice) {
|
||||
const result = await runOnboarding()
|
||||
onboardingProviderChoice = result.choice
|
||||
const { choice, token } = await runOnboarding()
|
||||
onboardingProviderChoice = choice
|
||||
rooToken = token ?? null
|
||||
}
|
||||
|
||||
if (onboardingProviderChoice === OnboardingProviderChoice.Roo) {
|
||||
useCloudProvider = true
|
||||
const authenticated = await hasToken()
|
||||
|
||||
if (authenticated) {
|
||||
const token = await loadToken()
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const client = createClient({ url: SDK_BASE_URL, authToken: token })
|
||||
const me = await client.auth.me.query()
|
||||
provider = "roo"
|
||||
apiKey = token
|
||||
user = me?.type === "user" ? me.user : null
|
||||
} catch {
|
||||
// Token may be expired or invalid - user will need to re-authenticate.
|
||||
}
|
||||
}
|
||||
}
|
||||
extensionHostOptions.provider = "roo"
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
if (useCloudProvider) {
|
||||
if (extensionHostOptions.provider === "roo") {
|
||||
if (rooToken) {
|
||||
try {
|
||||
const client = createClient({ url: SDK_BASE_URL, authToken: rooToken })
|
||||
const me = await client.auth.me.query()
|
||||
|
||||
if (me?.type !== "user") {
|
||||
throw new Error("Invalid token")
|
||||
}
|
||||
|
||||
extensionHostOptions.apiKey = rooToken
|
||||
extensionHostOptions.user = me.user
|
||||
} catch {
|
||||
console.error("[CLI] Your Roo Code Router token is not valid.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
console.error("[CLI] Your Roo Code Router token is missing.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Validations
|
||||
// TODO: Validate the API key for the chosen provider.
|
||||
// TODO: Validate the model for the chosen provider.
|
||||
|
||||
if (!isSupportedProvider(extensionHostOptions.provider)) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid provider: ${extensionHostOptions.provider}; must be one of: ${supportedProviders.join(", ")}`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
extensionHostOptions.apiKey =
|
||||
extensionHostOptions.apiKey || flagOptions.apiKey || getApiKeyFromEnv(extensionHostOptions.provider)
|
||||
|
||||
if (!extensionHostOptions.apiKey) {
|
||||
if (extensionHostOptions.provider === "roo") {
|
||||
console.error("[CLI] Error: Authentication with Roo Code Cloud failed or was cancelled.")
|
||||
console.error("[CLI] Please run: roo auth login")
|
||||
console.error("[CLI] Or use --api-key to provide your own API key.")
|
||||
|
|
@ -94,40 +146,58 @@ export async function run(workspaceArg: string, options: FlagOptions) {
|
|||
console.error(
|
||||
`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`,
|
||||
)
|
||||
console.error(`[CLI] For ${provider}, set ${getEnvVarName(provider)}`)
|
||||
console.error(
|
||||
`[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`,
|
||||
)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
console.error(`[CLI] Error: Workspace path does not exist: ${workspacePath}`)
|
||||
if (!fs.existsSync(extensionHostOptions.workspacePath)) {
|
||||
console.error(`[CLI] Error: Workspace path does not exist: ${extensionHostOptions.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)) {
|
||||
if (extensionHostOptions.reasoningEffort && !REASONING_EFFORTS.includes(extensionHostOptions.reasoningEffort)) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid reasoning effort: ${options.reasoningEffort}, must be one of: ${REASONING_EFFORTS.join(", ")}`,
|
||||
`[CLI] Error: Invalid reasoning effort: ${extensionHostOptions.reasoningEffort}, must be one of: ${REASONING_EFFORTS.join(", ")}`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (options.tui && !isTuiSupported) {
|
||||
console.log("[CLI] TUI disabled (no TTY support), falling back to plain text mode")
|
||||
}
|
||||
// Validate output format
|
||||
const outputFormat: OutputFormat = (flagOptions.outputFormat as OutputFormat) || "text"
|
||||
|
||||
if (!isTuiEnabled && !options.prompt) {
|
||||
console.error("[CLI] Error: prompt is required in plain text mode")
|
||||
console.error("[CLI] Usage: roo [workspace] -P <prompt> [options]")
|
||||
console.error("[CLI] Use TUI mode (without --no-tui) for interactive input")
|
||||
if (!isValidOutputFormat(outputFormat)) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid output format: ${flagOptions.outputFormat}; must be one of: text, json, stream-json`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Output format only works with --print mode
|
||||
if (outputFormat !== "text" && !flagOptions.print && isTuiSupported) {
|
||||
console.error("[CLI] Error: --output-format requires --print mode")
|
||||
console.error("[CLI] Usage: roo <prompt> --print --output-format json")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!isTuiEnabled) {
|
||||
if (!prompt) {
|
||||
console.error("[CLI] Error: prompt is required in print mode")
|
||||
console.error("[CLI] Usage: roo <prompt> --print [options]")
|
||||
console.error("[CLI] Run without -p for interactive mode")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!flagOptions.print) {
|
||||
console.warn("[CLI] TUI disabled (no TTY support), falling back to print mode")
|
||||
}
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
if (isTuiEnabled) {
|
||||
try {
|
||||
const { render } = await import("ink")
|
||||
|
|
@ -135,21 +205,9 @@ export async function run(workspaceArg: string, options: FlagOptions) {
|
|||
|
||||
render(
|
||||
createElement(App, {
|
||||
initialPrompt: options.prompt || "",
|
||||
workspacePath: workspacePath,
|
||||
extensionPath: path.resolve(extensionPath),
|
||||
user,
|
||||
provider,
|
||||
apiKey,
|
||||
model: options.model || DEFAULT_FLAGS.model,
|
||||
mode: options.mode || DEFAULT_FLAGS.mode,
|
||||
nonInteractive: options.yes,
|
||||
debug: options.debug,
|
||||
exitOnComplete: options.exitOnComplete,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
ephemeral: options.ephemeral,
|
||||
...extensionHostOptions,
|
||||
initialPrompt: prompt,
|
||||
version: VERSION,
|
||||
// Create extension host factory for dependency injection.
|
||||
createExtensionHost: (opts: ExtensionHostOptions) => new ExtensionHost(opts),
|
||||
}),
|
||||
// Handle Ctrl+C in App component for double-press exit.
|
||||
|
|
@ -165,53 +223,53 @@ export async function run(workspaceArg: string, options: FlagOptions) {
|
|||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
console.log(ASCII_ROO)
|
||||
console.log()
|
||||
console.log(
|
||||
`[roo] Running ${options.model || "default"} (${options.reasoningEffort || "default"}) on ${provider} in ${options.mode || "default"} mode in ${workspacePath}`,
|
||||
)
|
||||
const useJsonOutput = outputFormat === "json" || outputFormat === "stream-json"
|
||||
|
||||
const host = new ExtensionHost({
|
||||
mode: options.mode || DEFAULT_FLAGS.mode,
|
||||
reasoningEffort: options.reasoningEffort === "unspecified" ? undefined : options.reasoningEffort,
|
||||
user,
|
||||
provider,
|
||||
apiKey,
|
||||
model: options.model || DEFAULT_FLAGS.model,
|
||||
workspacePath,
|
||||
extensionPath: path.resolve(extensionPath),
|
||||
nonInteractive: options.yes,
|
||||
ephemeral: options.ephemeral,
|
||||
debug: options.debug,
|
||||
})
|
||||
extensionHostOptions.disableOutput = useJsonOutput
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\n[CLI] Received SIGINT, shutting down...")
|
||||
const host = new ExtensionHost(extensionHostOptions)
|
||||
|
||||
const jsonEmitter = useJsonOutput
|
||||
? new JsonEventEmitter({ mode: outputFormat as "json" | "stream-json" })
|
||||
: null
|
||||
|
||||
async function shutdown(signal: string, exitCode: number): Promise<void> {
|
||||
if (!useJsonOutput) {
|
||||
console.log(`\n[CLI] Received ${signal}, shutting down...`)
|
||||
}
|
||||
jsonEmitter?.detach()
|
||||
await host.dispose()
|
||||
process.exit(130)
|
||||
})
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("\n[CLI] Received SIGTERM, shutting down...")
|
||||
await host.dispose()
|
||||
process.exit(143)
|
||||
})
|
||||
process.on("SIGINT", () => shutdown("SIGINT", 130))
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM", 143))
|
||||
|
||||
try {
|
||||
await host.activate()
|
||||
await host.runTask(options.prompt!)
|
||||
|
||||
if (jsonEmitter) {
|
||||
jsonEmitter.attachToClient(host.client)
|
||||
}
|
||||
|
||||
await host.runTask(prompt!)
|
||||
jsonEmitter?.detach()
|
||||
await host.dispose()
|
||||
|
||||
if (!options.waitOnComplete) {
|
||||
process.exit(0)
|
||||
}
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
console.error("[CLI] Error:", error instanceof Error ? error.message : String(error))
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error(error.stack)
|
||||
if (useJsonOutput) {
|
||||
const errorEvent = { type: "error", id: Date.now(), content: errorMessage }
|
||||
process.stdout.write(JSON.stringify(errorEvent) + "\n")
|
||||
} else {
|
||||
console.error("[CLI] Error:", errorMessage)
|
||||
if (error instanceof Error) {
|
||||
console.error(error.stack)
|
||||
}
|
||||
}
|
||||
|
||||
jsonEmitter?.detach()
|
||||
await host.dispose()
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,31 +6,35 @@ import { run, login, logout, status } from "@/commands/index.js"
|
|||
|
||||
const program = new Command()
|
||||
|
||||
program.name("roo").description("Roo Code CLI - Run the Roo Code agent from the command line").version(VERSION)
|
||||
program
|
||||
.name("roo")
|
||||
.description("Roo Code CLI - starts an interactive session by default, use -p/--print for non-interactive output")
|
||||
.version(VERSION)
|
||||
|
||||
program
|
||||
.argument("[workspace]", "Workspace path to operate in", process.cwd())
|
||||
.option("-P, --prompt <prompt>", "The prompt/task to execute (optional in TUI mode)")
|
||||
.argument("[prompt]", "Your prompt")
|
||||
.option("--prompt-file <path>", "Read prompt from a file instead of command line argument")
|
||||
.option("-w, --workspace <path>", "Workspace directory path (defaults to current working directory)")
|
||||
.option("-p, --print", "Print response and exit (non-interactive mode)", false)
|
||||
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
||||
.option("-d, --debug", "Enable debug output (includes detailed debug information)", 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 OPENROUTER_API_KEY env var)")
|
||||
.option("-p, --provider <provider>", "API provider (anthropic, openai, openrouter, etc.)", "openrouter")
|
||||
.option("-y, --yes, --dangerously-skip-permissions", "Auto-approve all prompts (use with caution)", false)
|
||||
.option("-k, --api-key <key>", "API key for the LLM provider")
|
||||
.option("--provider <provider>", "API provider (roo, anthropic, openai, openrouter, etc.)")
|
||||
.option("-m, --model <model>", "Model to use", DEFAULT_FLAGS.model)
|
||||
.option("-M, --mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode)
|
||||
.option("--mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode)
|
||||
.option(
|
||||
"-r, --reasoning-effort <effort>",
|
||||
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
||||
DEFAULT_FLAGS.reasoningEffort,
|
||||
)
|
||||
.option("-x, --exit-on-complete", "Exit the process when the task completes (applies to TUI mode only)", false)
|
||||
.option(
|
||||
"-w, --wait-on-complete",
|
||||
"Keep the process running when the task completes (applies to plain text mode only)",
|
||||
false,
|
||||
)
|
||||
.option("--ephemeral", "Run without persisting state (uses temporary storage)", false)
|
||||
.option("--no-tui", "Disable TUI, use plain text output")
|
||||
.option("--oneshot", "Exit upon task completion", false)
|
||||
.option(
|
||||
"--output-format <format>",
|
||||
'Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming)',
|
||||
"text",
|
||||
)
|
||||
.action(run)
|
||||
|
||||
const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud")
|
||||
|
|
|
|||
236
apps/cli/src/lib/storage/__tests__/settings.test.ts
Normal file
236
apps/cli/src/lib/storage/__tests__/settings.test.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
// Use vi.hoisted to make the test directory available to the mock
|
||||
// This must return the path synchronously since settings path is computed at import time
|
||||
const { getTestConfigDir } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const os = require("os")
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const path = require("path")
|
||||
const testRunId = Date.now().toString()
|
||||
const testConfigDir = path.join(os.tmpdir(), `roo-cli-settings-test-${testRunId}`)
|
||||
return { getTestConfigDir: () => testConfigDir }
|
||||
})
|
||||
|
||||
vi.mock("../config-dir.js", () => ({
|
||||
getConfigDir: getTestConfigDir,
|
||||
}))
|
||||
|
||||
// Import after mocking
|
||||
import { loadSettings, saveSettings, resetOnboarding, getSettingsPath } from "../settings.js"
|
||||
import { OnboardingProviderChoice } from "@/types/index.js"
|
||||
|
||||
// Re-derive the test config dir for use in tests (must match the hoisted one)
|
||||
const actualTestConfigDir = getTestConfigDir()
|
||||
|
||||
describe("Settings Storage", () => {
|
||||
const expectedSettingsFile = path.join(actualTestConfigDir, "cli-settings.json")
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear test directory before each test
|
||||
await fs.rm(actualTestConfigDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test directory
|
||||
await fs.rm(actualTestConfigDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("getSettingsPath", () => {
|
||||
it("should return the correct settings file path", () => {
|
||||
expect(getSettingsPath()).toBe(expectedSettingsFile)
|
||||
})
|
||||
})
|
||||
|
||||
describe("loadSettings", () => {
|
||||
it("should return empty object if no settings file exists", async () => {
|
||||
const settings = await loadSettings()
|
||||
expect(settings).toEqual({})
|
||||
})
|
||||
|
||||
it("should load saved settings", async () => {
|
||||
const settingsData = {
|
||||
onboardingProviderChoice: OnboardingProviderChoice.Roo,
|
||||
mode: "architect",
|
||||
provider: "anthropic" as const,
|
||||
model: "claude-sonnet-4-20250514",
|
||||
reasoningEffort: "high" as const,
|
||||
}
|
||||
|
||||
await fs.mkdir(actualTestConfigDir, { recursive: true })
|
||||
await fs.writeFile(expectedSettingsFile, JSON.stringify(settingsData), "utf-8")
|
||||
|
||||
const loaded = await loadSettings()
|
||||
expect(loaded).toEqual(settingsData)
|
||||
})
|
||||
|
||||
it("should load settings with only some fields set", async () => {
|
||||
const settingsData = {
|
||||
mode: "code",
|
||||
}
|
||||
|
||||
await fs.mkdir(actualTestConfigDir, { recursive: true })
|
||||
await fs.writeFile(expectedSettingsFile, JSON.stringify(settingsData), "utf-8")
|
||||
|
||||
const loaded = await loadSettings()
|
||||
expect(loaded).toEqual(settingsData)
|
||||
})
|
||||
})
|
||||
|
||||
describe("saveSettings", () => {
|
||||
it("should save settings to disk", async () => {
|
||||
await saveSettings({ mode: "debug" })
|
||||
|
||||
const savedData = await fs.readFile(expectedSettingsFile, "utf-8")
|
||||
const settings = JSON.parse(savedData)
|
||||
|
||||
expect(settings.mode).toBe("debug")
|
||||
})
|
||||
|
||||
it("should merge settings with existing ones", async () => {
|
||||
await saveSettings({ mode: "code" })
|
||||
await saveSettings({ provider: "openrouter" as const })
|
||||
|
||||
const savedData = await fs.readFile(expectedSettingsFile, "utf-8")
|
||||
const settings = JSON.parse(savedData)
|
||||
|
||||
expect(settings.mode).toBe("code")
|
||||
expect(settings.provider).toBe("openrouter")
|
||||
})
|
||||
|
||||
it("should save all default settings fields", async () => {
|
||||
await saveSettings({
|
||||
mode: "architect",
|
||||
provider: "anthropic" as const,
|
||||
model: "claude-opus-4.5",
|
||||
reasoningEffort: "medium" as const,
|
||||
})
|
||||
|
||||
const savedData = await fs.readFile(expectedSettingsFile, "utf-8")
|
||||
const settings = JSON.parse(savedData)
|
||||
|
||||
expect(settings.mode).toBe("architect")
|
||||
expect(settings.provider).toBe("anthropic")
|
||||
expect(settings.model).toBe("claude-opus-4.5")
|
||||
expect(settings.reasoningEffort).toBe("medium")
|
||||
})
|
||||
|
||||
it("should create config directory if it doesn't exist", async () => {
|
||||
await saveSettings({ mode: "ask" })
|
||||
|
||||
const dirStats = await fs.stat(actualTestConfigDir)
|
||||
expect(dirStats.isDirectory()).toBe(true)
|
||||
})
|
||||
|
||||
// Unix file permissions don't apply on Windows - skip this test
|
||||
it.skipIf(process.platform === "win32")("should set restrictive file permissions", async () => {
|
||||
await saveSettings({ mode: "code" })
|
||||
|
||||
const stats = await fs.stat(expectedSettingsFile)
|
||||
// Check that only owner has read/write (mode 0o600)
|
||||
const mode = stats.mode & 0o777
|
||||
expect(mode).toBe(0o600)
|
||||
})
|
||||
})
|
||||
|
||||
describe("resetOnboarding", () => {
|
||||
it("should reset onboarding provider choice", async () => {
|
||||
await saveSettings({ onboardingProviderChoice: OnboardingProviderChoice.Roo })
|
||||
|
||||
await resetOnboarding()
|
||||
|
||||
const settings = await loadSettings()
|
||||
expect(settings.onboardingProviderChoice).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should preserve other settings when resetting onboarding", async () => {
|
||||
await saveSettings({
|
||||
onboardingProviderChoice: OnboardingProviderChoice.Byok,
|
||||
mode: "architect",
|
||||
provider: "gemini" as const,
|
||||
})
|
||||
|
||||
await resetOnboarding()
|
||||
|
||||
const settings = await loadSettings()
|
||||
expect(settings.onboardingProviderChoice).toBeUndefined()
|
||||
expect(settings.mode).toBe("architect")
|
||||
expect(settings.provider).toBe("gemini")
|
||||
})
|
||||
})
|
||||
|
||||
describe("default settings priority", () => {
|
||||
it("should support all configurable default settings", async () => {
|
||||
// Test that all the settings that can be used as defaults are properly saved and loaded
|
||||
const defaultSettings = {
|
||||
mode: "debug",
|
||||
provider: "openai-native" as const,
|
||||
model: "gpt-4o",
|
||||
reasoningEffort: "low" as const,
|
||||
}
|
||||
|
||||
await saveSettings(defaultSettings)
|
||||
const loaded = await loadSettings()
|
||||
|
||||
expect(loaded.mode).toBe("debug")
|
||||
expect(loaded.provider).toBe("openai-native")
|
||||
expect(loaded.model).toBe("gpt-4o")
|
||||
expect(loaded.reasoningEffort).toBe("low")
|
||||
})
|
||||
|
||||
it("should support dangerouslySkipPermissions setting", async () => {
|
||||
await saveSettings({ dangerouslySkipPermissions: true })
|
||||
const loaded = await loadSettings()
|
||||
|
||||
expect(loaded.dangerouslySkipPermissions).toBe(true)
|
||||
})
|
||||
|
||||
it("should support all settings together including dangerouslySkipPermissions", async () => {
|
||||
const allSettings = {
|
||||
mode: "architect",
|
||||
provider: "anthropic" as const,
|
||||
model: "claude-sonnet-4-20250514",
|
||||
reasoningEffort: "high" as const,
|
||||
dangerouslySkipPermissions: true,
|
||||
}
|
||||
|
||||
await saveSettings(allSettings)
|
||||
const loaded = await loadSettings()
|
||||
|
||||
expect(loaded.mode).toBe("architect")
|
||||
expect(loaded.provider).toBe("anthropic")
|
||||
expect(loaded.model).toBe("claude-sonnet-4-20250514")
|
||||
expect(loaded.reasoningEffort).toBe("high")
|
||||
expect(loaded.dangerouslySkipPermissions).toBe(true)
|
||||
})
|
||||
|
||||
it("should support oneshot setting", async () => {
|
||||
await saveSettings({ oneshot: true })
|
||||
const loaded = await loadSettings()
|
||||
|
||||
expect(loaded.oneshot).toBe(true)
|
||||
})
|
||||
|
||||
it("should support all settings together including oneshot", async () => {
|
||||
const allSettings = {
|
||||
mode: "architect",
|
||||
provider: "anthropic" as const,
|
||||
model: "claude-sonnet-4-20250514",
|
||||
reasoningEffort: "high" as const,
|
||||
dangerouslySkipPermissions: true,
|
||||
oneshot: true,
|
||||
}
|
||||
|
||||
await saveSettings(allSettings)
|
||||
const loaded = await loadSettings()
|
||||
|
||||
expect(loaded.mode).toBe("architect")
|
||||
expect(loaded.provider).toBe("anthropic")
|
||||
expect(loaded.model).toBe("claude-sonnet-4-20250514")
|
||||
expect(loaded.reasoningEffort).toBe("high")
|
||||
expect(loaded.dangerouslySkipPermissions).toBe(true)
|
||||
expect(loaded.oneshot).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -17,9 +17,14 @@ export async function runOnboarding(): Promise<OnboardingResult> {
|
|||
console.log("")
|
||||
|
||||
if (choice === OnboardingProviderChoice.Roo) {
|
||||
const { success: authenticated } = await login()
|
||||
const result = await login()
|
||||
await saveSettings({ onboardingProviderChoice: choice })
|
||||
resolve({ choice: OnboardingProviderChoice.Roo, authenticated, skipped: false })
|
||||
|
||||
resolve({
|
||||
choice: OnboardingProviderChoice.Roo,
|
||||
token: result.success ? result.token : undefined,
|
||||
skipped: false,
|
||||
})
|
||||
} else {
|
||||
console.log("Using your own API key.")
|
||||
console.log("Set your API key via --api-key or environment variable.")
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./types.js"
|
||||
export * from "./constants.js"
|
||||
export * from "./json-events.js"
|
||||
|
|
|
|||
120
apps/cli/src/types/json-events.ts
Normal file
120
apps/cli/src/types/json-events.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* JSON Event Types for Structured CLI Output
|
||||
*
|
||||
* This module defines the types for structured JSON output from the CLI.
|
||||
* The output format is NDJSON (newline-delimited JSON) for stream-json mode,
|
||||
* or a single JSON object for json mode.
|
||||
*
|
||||
* Schema is optimized for efficiency with high message volume:
|
||||
* - Minimal fields per event
|
||||
* - No redundant wrappers
|
||||
* - `done` flag instead of partial:false
|
||||
*/
|
||||
|
||||
/**
|
||||
* Output format options for the CLI.
|
||||
*/
|
||||
export const OUTPUT_FORMATS = ["text", "json", "stream-json"] as const
|
||||
|
||||
export type OutputFormat = (typeof OUTPUT_FORMATS)[number]
|
||||
|
||||
export function isValidOutputFormat(format: string): format is OutputFormat {
|
||||
return (OUTPUT_FORMATS as readonly string[]).includes(format)
|
||||
}
|
||||
|
||||
/**
|
||||
* Event type discriminators for JSON output.
|
||||
*/
|
||||
export type JsonEventType =
|
||||
| "system" // System messages (init, ready, shutdown)
|
||||
| "assistant" // Assistant text messages
|
||||
| "user" // User messages (echoed input)
|
||||
| "tool_use" // Tool invocations (file ops, commands, browser, MCP)
|
||||
| "tool_result" // Results from tool execution
|
||||
| "thinking" // Reasoning/thinking content
|
||||
| "error" // Errors
|
||||
| "result" // Final task result
|
||||
|
||||
/**
|
||||
* Tool use information for tool_use events.
|
||||
*/
|
||||
export interface JsonEventToolUse {
|
||||
/** Tool name (e.g., "read_file", "write_to_file", "execute_command") */
|
||||
name: string
|
||||
/** Tool input parameters */
|
||||
input?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool result information for tool_result events.
|
||||
*/
|
||||
export interface JsonEventToolResult {
|
||||
/** Tool name that produced this result */
|
||||
name: string
|
||||
/** Tool output (for successful execution) */
|
||||
output?: string
|
||||
/** Error message (for failed execution) */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Cost and token usage information.
|
||||
*/
|
||||
export interface JsonEventCost {
|
||||
/** Total cost in USD */
|
||||
totalCost?: number
|
||||
/** Input tokens used */
|
||||
inputTokens?: number
|
||||
/** Output tokens generated */
|
||||
outputTokens?: number
|
||||
/** Cache write tokens */
|
||||
cacheWrites?: number
|
||||
/** Cache read tokens */
|
||||
cacheReads?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Base JSON event structure.
|
||||
* Optimized for minimal payload size.
|
||||
*
|
||||
* For streaming deltas:
|
||||
* - Each delta includes `id` for easy correlation
|
||||
* - Final message has `done: true`
|
||||
*/
|
||||
export interface JsonEvent {
|
||||
/** Event type discriminator */
|
||||
type: JsonEventType
|
||||
/** Message ID - included on first delta and final message */
|
||||
id?: number
|
||||
/** Content text (for text-based events) */
|
||||
content?: string
|
||||
/** True when this is the final message (stream complete) */
|
||||
done?: boolean
|
||||
/** Optional subtype for more specific categorization */
|
||||
subtype?: string
|
||||
/** Tool use information (for tool_use events) */
|
||||
tool_use?: JsonEventToolUse
|
||||
/** Tool result information (for tool_result events) */
|
||||
tool_result?: JsonEventToolResult
|
||||
/** Whether the task succeeded (for result events) */
|
||||
success?: boolean
|
||||
/** Cost and token usage (for result events) */
|
||||
cost?: JsonEventCost
|
||||
}
|
||||
|
||||
/**
|
||||
* Final JSON output for "json" mode (single object at end).
|
||||
* Contains the result and accumulated messages.
|
||||
*/
|
||||
export interface JsonFinalOutput {
|
||||
/** Final result type */
|
||||
type: "result"
|
||||
/** Whether the task succeeded */
|
||||
success: boolean
|
||||
/** Result content/message */
|
||||
content?: string
|
||||
/** Cost and token usage */
|
||||
cost?: JsonEventCost
|
||||
/** All events that occurred during the task */
|
||||
events: JsonEvent[]
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ProviderName, ReasoningEffortExtended } from "@roo-code/types"
|
||||
import type { OutputFormat } from "./json-events.js"
|
||||
|
||||
export const supportedProviders = [
|
||||
"anthropic",
|
||||
|
|
@ -18,19 +19,21 @@ export function isSupportedProvider(provider: string): provider is SupportedProv
|
|||
export type ReasoningEffortFlagOptions = ReasoningEffortExtended | "unspecified" | "disabled"
|
||||
|
||||
export type FlagOptions = {
|
||||
prompt?: string
|
||||
promptFile?: string
|
||||
workspace?: string
|
||||
print: boolean
|
||||
extension?: string
|
||||
debug: boolean
|
||||
yes: boolean
|
||||
dangerouslySkipPermissions: boolean
|
||||
apiKey?: string
|
||||
provider: SupportedProvider
|
||||
provider?: SupportedProvider
|
||||
model?: string
|
||||
mode?: string
|
||||
reasoningEffort?: ReasoningEffortFlagOptions
|
||||
exitOnComplete: boolean
|
||||
waitOnComplete: boolean
|
||||
ephemeral: boolean
|
||||
tui: boolean
|
||||
oneshot: boolean
|
||||
outputFormat?: OutputFormat
|
||||
}
|
||||
|
||||
export enum OnboardingProviderChoice {
|
||||
|
|
@ -40,10 +43,22 @@ export enum OnboardingProviderChoice {
|
|||
|
||||
export interface OnboardingResult {
|
||||
choice: OnboardingProviderChoice
|
||||
authenticated?: boolean
|
||||
token?: string
|
||||
skipped: boolean
|
||||
}
|
||||
|
||||
export interface CliSettings {
|
||||
onboardingProviderChoice?: OnboardingProviderChoice
|
||||
/** Default mode to use (e.g., "code", "architect", "ask", "debug") */
|
||||
mode?: string
|
||||
/** Default provider to use */
|
||||
provider?: SupportedProvider
|
||||
/** Default model to use */
|
||||
model?: string
|
||||
/** Default reasoning effort level */
|
||||
reasoningEffort?: ReasoningEffortFlagOptions
|
||||
/** Auto-approve all prompts (use with caution) */
|
||||
dangerouslySkipPermissions?: boolean
|
||||
/** Exit upon task completion */
|
||||
oneshot?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,33 +59,33 @@ import ScrollIndicator from "./components/ScrollIndicator.js"
|
|||
const PICKER_HEIGHT = 10
|
||||
|
||||
export interface TUIAppProps extends ExtensionHostOptions {
|
||||
initialPrompt: string
|
||||
debug: boolean
|
||||
exitOnComplete: boolean
|
||||
initialPrompt?: string
|
||||
version: string
|
||||
// Create extension host factory for dependency injection.
|
||||
createExtensionHost: (options: ExtensionHostOptions) => ExtensionHostInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner App component that uses the terminal size context
|
||||
*/
|
||||
function AppInner({
|
||||
initialPrompt,
|
||||
workspacePath,
|
||||
extensionPath,
|
||||
user,
|
||||
provider,
|
||||
apiKey,
|
||||
model,
|
||||
mode,
|
||||
nonInteractive = false,
|
||||
debug,
|
||||
exitOnComplete,
|
||||
reasoningEffort,
|
||||
ephemeral,
|
||||
version,
|
||||
createExtensionHost,
|
||||
}: TUIAppProps) {
|
||||
function AppInner({ createExtensionHost, ...extensionHostOptions }: TUIAppProps) {
|
||||
const {
|
||||
initialPrompt,
|
||||
workspacePath,
|
||||
extensionPath,
|
||||
user,
|
||||
provider,
|
||||
apiKey,
|
||||
model,
|
||||
mode,
|
||||
nonInteractive = false,
|
||||
debug,
|
||||
exitOnComplete,
|
||||
reasoningEffort,
|
||||
ephemeral,
|
||||
version,
|
||||
} = extensionHostOptions
|
||||
|
||||
const { exit } = useApp()
|
||||
|
||||
const {
|
||||
|
|
@ -455,12 +455,8 @@ function AppInner({
|
|||
{/* Header - fixed size */}
|
||||
<Box flexShrink={0}>
|
||||
<Header
|
||||
cwd={workspacePath}
|
||||
user={user}
|
||||
provider={provider}
|
||||
model={model}
|
||||
{...extensionHostOptions}
|
||||
mode={currentMode || mode}
|
||||
reasoningEffort={reasoningEffort}
|
||||
version={version}
|
||||
tokenUsage={tokenUsage}
|
||||
contextWindow={contextWindow}
|
||||
|
|
|
|||
|
|
@ -4,32 +4,27 @@ import { Text, Box } from "ink"
|
|||
import type { TokenUsage } from "@roo-code/types"
|
||||
|
||||
import { ASCII_ROO } from "@/types/constants.js"
|
||||
import { User } from "@/lib/sdk/types.js"
|
||||
|
||||
import { ExtensionHostOptions } from "@/agent/index.js"
|
||||
import { useTerminalSize } from "../hooks/TerminalSizeContext.js"
|
||||
import * as theme from "../theme.js"
|
||||
|
||||
import MetricsDisplay from "./MetricsDisplay.js"
|
||||
|
||||
interface HeaderProps {
|
||||
cwd: string
|
||||
user: User | null
|
||||
provider: string
|
||||
model: string
|
||||
mode: string
|
||||
reasoningEffort?: string
|
||||
interface HeaderProps extends ExtensionHostOptions {
|
||||
version: string
|
||||
tokenUsage?: TokenUsage | null
|
||||
contextWindow?: number
|
||||
}
|
||||
|
||||
function Header({
|
||||
cwd,
|
||||
workspacePath,
|
||||
user,
|
||||
provider,
|
||||
model,
|
||||
mode,
|
||||
reasoningEffort,
|
||||
nonInteractive,
|
||||
version,
|
||||
tokenUsage,
|
||||
contextWindow,
|
||||
|
|
@ -53,12 +48,16 @@ function Header({
|
|||
<Box flexDirection="column" marginLeft={1} marginTop={1}>
|
||||
{user && <Text color={theme.dimText}>Welcome back, {user.name}</Text>}
|
||||
<Text color={theme.dimText}>
|
||||
cwd: {cwd.startsWith(homeDir) ? cwd.replace(homeDir, "~") : cwd}
|
||||
cwd:{" "}
|
||||
{workspacePath.startsWith(homeDir) ? workspacePath.replace(homeDir, "~") : workspacePath}
|
||||
</Text>
|
||||
<Text color={theme.dimText}>
|
||||
{provider}: {model} [{reasoningEffort}]
|
||||
</Text>
|
||||
<Text color={theme.dimText}>mode: {mode}</Text>
|
||||
<Text color={theme.dimText}>
|
||||
mode: {mode}
|
||||
{nonInteractive && " (YOLO)"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { ExtensionHostInterface, ExtensionHostOptions } from "@/agent/index.js"
|
|||
|
||||
import { useCLIStore } from "../store.js"
|
||||
|
||||
// TODO: Unify with TUIAppProps?
|
||||
export interface UseExtensionHostOptions extends ExtensionHostOptions {
|
||||
initialPrompt?: string
|
||||
exitOnComplete?: boolean
|
||||
onExtensionMessage: (msg: ExtensionMessage) => void
|
||||
createExtensionHost: (options: ExtensionHostOptions) => ExtensionHostInterface
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ export function useExtensionHost({
|
|||
extensionPath,
|
||||
nonInteractive,
|
||||
ephemeral,
|
||||
debug,
|
||||
exitOnComplete,
|
||||
onExtensionMessage,
|
||||
createExtensionHost,
|
||||
|
|
@ -73,8 +74,10 @@ export function useExtensionHost({
|
|||
workspacePath,
|
||||
extensionPath,
|
||||
nonInteractive,
|
||||
disableOutput: true,
|
||||
ephemeral,
|
||||
debug,
|
||||
exitOnComplete,
|
||||
disableOutput: true,
|
||||
})
|
||||
|
||||
hostRef.current = host
|
||||
|
|
|
|||
|
|
@ -1,405 +0,0 @@
|
|||
# E2E Tests for Roo Code
|
||||
|
||||
End-to-end tests for the Roo Code VSCode extension using the VSCode Extension Test Runner.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20.19.2 (or compatible version 20.x)
|
||||
- pnpm 10.8.1+
|
||||
- OpenRouter API key with available credits
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
From the project root:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. Configure API Key
|
||||
|
||||
Create a `.env.local` file in this directory:
|
||||
|
||||
```bash
|
||||
cd apps/vscode-e2e
|
||||
cp .env.local.sample .env.local
|
||||
```
|
||||
|
||||
Edit `.env.local` and add your OpenRouter API key:
|
||||
|
||||
```
|
||||
OPENROUTER_API_KEY=sk-or-v1-your-key-here
|
||||
```
|
||||
|
||||
### 3. Build Dependencies
|
||||
|
||||
The E2E tests require the extension and its dependencies to be built:
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
pnpm -w bundle
|
||||
pnpm --filter @roo-code/vscode-webview build
|
||||
```
|
||||
|
||||
Or use the `test:ci` script which handles this automatically (recommended).
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run All Tests (Recommended)
|
||||
|
||||
```bash
|
||||
cd apps/vscode-e2e
|
||||
pnpm test:ci
|
||||
```
|
||||
|
||||
This command:
|
||||
|
||||
1. Builds the extension bundle
|
||||
2. Builds the webview UI
|
||||
3. Compiles TypeScript test files
|
||||
4. Downloads VSCode test runtime (if needed)
|
||||
5. Runs all tests
|
||||
|
||||
**Expected output**: ~39 passing tests, ~0 skipped tests, ~6-8 minutes
|
||||
|
||||
### Run Specific Test File
|
||||
|
||||
```bash
|
||||
TEST_FILE="task.test" pnpm test:ci
|
||||
```
|
||||
|
||||
Available test files:
|
||||
|
||||
- `extension.test` - Extension activation and command registration
|
||||
- `task.test` - Basic task execution
|
||||
- `modes.test` - Mode switching functionality
|
||||
- `markdown-lists.test` - Markdown rendering
|
||||
- `subtasks.test` - Subtask handling
|
||||
- `tools/write-to-file.test` - File writing tool
|
||||
- `tools/read-file.test` - File reading tool
|
||||
- `tools/search-files.test` - File search tool
|
||||
- `tools/list-files.test` - Directory listing tool
|
||||
- `tools/execute-command.test` - Command execution tool
|
||||
- `tools/apply-diff.test` - Diff application tool
|
||||
- `tools/use-mcp-tool.test` - MCP tool integration
|
||||
|
||||
### Run Tests Matching Pattern
|
||||
|
||||
```bash
|
||||
TEST_GREP="markdown" pnpm test:ci
|
||||
```
|
||||
|
||||
This will run only tests whose names match "markdown".
|
||||
|
||||
### Development Workflow
|
||||
|
||||
For faster iteration during test development:
|
||||
|
||||
1. Build dependencies once:
|
||||
|
||||
```bash
|
||||
pnpm -w bundle
|
||||
pnpm --filter @roo-code/vscode-webview build
|
||||
```
|
||||
|
||||
2. Run tests directly (faster, but requires manual rebuilds):
|
||||
```bash
|
||||
pnpm test:run
|
||||
```
|
||||
|
||||
**Note**: If you modify the extension code, you must rebuild before running `test:run`.
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
apps/vscode-e2e/
|
||||
├── src/
|
||||
│ ├── runTest.ts # Test runner entry point
|
||||
│ ├── suite/
|
||||
│ │ ├── index.ts # Test suite setup and configuration
|
||||
│ │ ├── utils.ts # Test utilities (waitFor, etc.)
|
||||
│ │ ├── test-utils.ts # Test configuration helpers
|
||||
│ │ ├── extension.test.ts
|
||||
│ │ ├── task.test.ts
|
||||
│ │ ├── modes.test.ts
|
||||
│ │ ├── markdown-lists.test.ts
|
||||
│ │ ├── subtasks.test.ts
|
||||
│ │ └── tools/ # Tool-specific tests
|
||||
│ │ ├── write-to-file.test.ts
|
||||
│ │ ├── read-file.test.ts
|
||||
│ │ ├── search-files.test.ts
|
||||
│ │ ├── list-files.test.ts
|
||||
│ │ ├── execute-command.test.ts
|
||||
│ │ ├── apply-diff.test.ts
|
||||
│ │ └── use-mcp-tool.test.ts
|
||||
│ └── types/
|
||||
│ └── global.d.ts # Global type definitions
|
||||
├── .env.local.sample # Sample environment file
|
||||
├── .env.local # Your API key (gitignored)
|
||||
├── package.json
|
||||
├── tsconfig.json # TypeScript config for tests
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## How Tests Work
|
||||
|
||||
1. **Test Runner** ([`runTest.ts`](src/runTest.ts)):
|
||||
|
||||
- Downloads VSCode test runtime (cached in `.vscode-test/`)
|
||||
- Creates temporary workspace directory
|
||||
- Launches VSCode with the extension loaded
|
||||
- Runs Mocha test suite
|
||||
|
||||
2. **Test Setup** ([`suite/index.ts`](src/suite/index.ts)):
|
||||
|
||||
- Activates the extension
|
||||
- Configures API with OpenRouter credentials
|
||||
- Sets up global `api` object for tests
|
||||
- Configures Mocha with 20-minute timeout
|
||||
|
||||
3. **Test Execution**:
|
||||
|
||||
- Tests use the `RooCodeAPI` to programmatically control the extension
|
||||
- Tests can start tasks, send messages, wait for completion, etc.
|
||||
- Tests observe events emitted by the extension
|
||||
|
||||
4. **Cleanup**:
|
||||
- Temporary workspace is deleted after tests complete
|
||||
- VSCode instance is closed
|
||||
|
||||
## Common Issues
|
||||
|
||||
### "Cannot find module '@roo-code/types'"
|
||||
|
||||
**Cause**: The `@roo-code/types` package hasn't been built.
|
||||
|
||||
**Solution**: Use `pnpm test:ci` instead of `pnpm test:run`, or build dependencies manually:
|
||||
|
||||
```bash
|
||||
pnpm -w bundle
|
||||
pnpm --filter @roo-code/vscode-webview build
|
||||
```
|
||||
|
||||
### "Extension not found: RooVeterinaryInc.roo-cline"
|
||||
|
||||
**Cause**: The extension bundle hasn't been created.
|
||||
|
||||
**Solution**: Build the extension:
|
||||
|
||||
```bash
|
||||
pnpm -w bundle
|
||||
```
|
||||
|
||||
### Tests timeout or hang
|
||||
|
||||
**Possible causes**:
|
||||
|
||||
1. Invalid or expired OpenRouter API key
|
||||
2. No credits remaining on OpenRouter account
|
||||
3. Network connectivity issues
|
||||
4. Model is unavailable
|
||||
|
||||
**Solution**:
|
||||
|
||||
- Verify your API key is valid
|
||||
- Check your OpenRouter account has credits
|
||||
- Try running a single test to isolate the issue
|
||||
|
||||
### "OPENROUTER_API_KEY is not defined"
|
||||
|
||||
**Cause**: Missing or incorrect `.env.local` file.
|
||||
|
||||
**Solution**: Create `.env.local` with your API key:
|
||||
|
||||
```bash
|
||||
echo "OPENROUTER_API_KEY=sk-or-v1-your-key-here" > .env.local
|
||||
```
|
||||
|
||||
### VSCode download fails
|
||||
|
||||
**Cause**: Network issues or GitHub rate limiting.
|
||||
|
||||
**Solution**: The test runner has retry logic. If it continues to fail:
|
||||
|
||||
1. Check your internet connection
|
||||
2. Try again later
|
||||
3. Manually download VSCode to `.vscode-test/` directory
|
||||
|
||||
## Current Test Status
|
||||
|
||||
As of the last run:
|
||||
|
||||
- ✅ **39 tests passing** (100% coverage)
|
||||
- ⏭️ **0 tests skipped**
|
||||
- ❌ **0 tests failing**
|
||||
- ⏱️ **~6-8 minutes** total runtime
|
||||
|
||||
### Passing Tests
|
||||
|
||||
1. Task execution and response handling
|
||||
2. Mode switching functionality
|
||||
3. Markdown list rendering (4 tests)
|
||||
4. Extension command registration
|
||||
|
||||
### Skipped Tests
|
||||
|
||||
Most tool tests are currently skipped. These need to be investigated and re-enabled:
|
||||
|
||||
- File operation tools (write, read, list, search)
|
||||
- Command execution tool
|
||||
- Diff application tool
|
||||
- MCP tool integration
|
||||
- Subtask handling
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```typescript
|
||||
import * as assert from "assert"
|
||||
import { RooCodeEventName } from "@roo-code/types"
|
||||
import { waitUntilCompleted } from "./utils"
|
||||
import { setDefaultSuiteTimeout } from "./test-utils"
|
||||
|
||||
suite("My Test Suite", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
test("Should do something", async () => {
|
||||
const api = globalThis.api
|
||||
|
||||
// Start a task
|
||||
const taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
},
|
||||
text: "Your task prompt here",
|
||||
})
|
||||
|
||||
// Wait for completion
|
||||
await waitUntilCompleted({ api, taskId })
|
||||
|
||||
// Assert results
|
||||
assert.ok(true, "Test passed")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Available Utilities
|
||||
|
||||
- `waitFor(condition, options)` - Wait for a condition to be true
|
||||
- `waitUntilCompleted({ api, taskId })` - Wait for task completion
|
||||
- `waitUntilAborted({ api, taskId })` - Wait for task abortion
|
||||
- `sleep(ms)` - Sleep for specified milliseconds
|
||||
- `setDefaultSuiteTimeout(context)` - Set 2-minute timeout for suite
|
||||
|
||||
### API Methods
|
||||
|
||||
The `globalThis.api` object provides:
|
||||
|
||||
```typescript
|
||||
// Task management
|
||||
api.startNewTask({ configuration, text, images })
|
||||
api.resumeTask(taskId)
|
||||
api.cancelCurrentTask()
|
||||
api.clearCurrentTask()
|
||||
|
||||
// Interaction
|
||||
api.sendMessage(text, images)
|
||||
api.pressPrimaryButton()
|
||||
api.pressSecondaryButton()
|
||||
|
||||
// Configuration
|
||||
api.getConfiguration()
|
||||
api.setConfiguration(values)
|
||||
|
||||
// Events
|
||||
api.on(RooCodeEventName.TaskStarted, (taskId) => {})
|
||||
api.on(RooCodeEventName.TaskCompleted, (taskId) => {})
|
||||
api.on(RooCodeEventName.Message, ({ taskId, message }) => {})
|
||||
// ... and many more events
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
The E2E tests run automatically in GitHub Actions on:
|
||||
|
||||
- Pull requests to `main`
|
||||
- Pushes to `main`
|
||||
- Manual workflow dispatch
|
||||
|
||||
See [`.github/workflows/code-qa.yml`](../../.github/workflows/code-qa.yml) for the CI configuration.
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- `OPENROUTER_API_KEY` secret must be configured in GitHub
|
||||
- Tests run on Ubuntu with xvfb for headless display
|
||||
- VSCode 1.101.2 is downloaded and cached
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
Set environment variable to see detailed logs:
|
||||
|
||||
```bash
|
||||
DEBUG=* pnpm test:ci
|
||||
```
|
||||
|
||||
### Check VSCode Logs
|
||||
|
||||
VSCode logs are written to the console during test execution. Look for:
|
||||
|
||||
- Extension activation messages
|
||||
- API configuration logs
|
||||
- Task execution logs
|
||||
- Error messages
|
||||
|
||||
### Inspect Test Workspace
|
||||
|
||||
The test workspace is created in `/tmp/roo-test-workspace-*` and deleted after tests.
|
||||
|
||||
To preserve it for debugging, modify [`runTest.ts`](src/runTest.ts):
|
||||
|
||||
```typescript
|
||||
// Comment out this line:
|
||||
// await fs.rm(testWorkspace, { recursive: true, force: true })
|
||||
```
|
||||
|
||||
### Run Single Test in Isolation
|
||||
|
||||
```bash
|
||||
TEST_FILE="extension.test" pnpm test:ci
|
||||
```
|
||||
|
||||
This helps identify if issues are test-specific or systemic.
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new E2E tests:
|
||||
|
||||
1. Follow the existing test structure
|
||||
2. Use descriptive test names
|
||||
3. Clean up resources in `teardown()` hooks
|
||||
4. Use appropriate timeouts
|
||||
5. Add comments explaining complex test logic
|
||||
6. Ensure tests are deterministic (no flakiness)
|
||||
|
||||
## Resources
|
||||
|
||||
- [VSCode Extension Testing Guide](https://code.visualstudio.com/api/working-with-extensions/testing-extension)
|
||||
- [Mocha Documentation](https://mochajs.org/)
|
||||
- [@vscode/test-electron](https://github.com/microsoft/vscode-test)
|
||||
- [OpenRouter API Documentation](https://openrouter.ai/docs)
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check this README for common issues
|
||||
2. Review test logs for error messages
|
||||
3. Try running tests locally to reproduce
|
||||
4. Check GitHub Actions logs for CI failures
|
||||
5. Ask in the team chat or create an issue
|
||||
|
|
@ -7,18 +7,6 @@ import type { RooCodeAPI } from "@roo-code/types"
|
|||
|
||||
import { waitFor } from "./utils"
|
||||
|
||||
/**
|
||||
* Models to test against - high-performing models from different providers
|
||||
*/
|
||||
const MODELS_TO_TEST = ["openai/gpt-5.2", "anthropic/claude-sonnet-4.5", "google/gemini-3-pro-preview"]
|
||||
|
||||
interface ModelTestResult {
|
||||
model: string
|
||||
failures: number
|
||||
passes: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
const extension = vscode.extensions.getExtension<RooCodeAPI>("RooVeterinaryInc.roo-cline")
|
||||
|
||||
|
|
@ -28,11 +16,10 @@ export async function run() {
|
|||
|
||||
const api = extension.isActive ? extension.exports : await extension.activate()
|
||||
|
||||
// Initial configuration with first model (will be reconfigured per model)
|
||||
await api.setConfiguration({
|
||||
apiProvider: "openrouter" as const,
|
||||
openRouterApiKey: process.env.OPENROUTER_API_KEY!,
|
||||
openRouterModelId: MODELS_TO_TEST[0],
|
||||
openRouterModelId: "openai/gpt-4.1",
|
||||
})
|
||||
|
||||
await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
|
||||
|
|
@ -40,6 +27,17 @@ export async function run() {
|
|||
|
||||
globalThis.api = api
|
||||
|
||||
const mochaOptions: Mocha.MochaOptions = {
|
||||
ui: "tdd",
|
||||
timeout: 20 * 60 * 1_000, // 20m
|
||||
}
|
||||
|
||||
if (process.env.TEST_GREP) {
|
||||
mochaOptions.grep = process.env.TEST_GREP
|
||||
console.log(`Running tests matching pattern: ${process.env.TEST_GREP}`)
|
||||
}
|
||||
|
||||
const mocha = new Mocha(mochaOptions)
|
||||
const cwd = path.resolve(__dirname, "..")
|
||||
|
||||
let testFiles: string[]
|
||||
|
|
@ -59,91 +57,9 @@ export async function run() {
|
|||
throw new Error(`No test files found matching criteria: ${process.env.TEST_FILE || "all tests"}`)
|
||||
}
|
||||
|
||||
const results: ModelTestResult[] = []
|
||||
let totalFailures = 0
|
||||
testFiles.forEach((testFile) => mocha.addFile(path.resolve(cwd, testFile)))
|
||||
|
||||
// Run tests for each model sequentially
|
||||
for (const model of MODELS_TO_TEST) {
|
||||
console.log(`\n${"=".repeat(60)}`)
|
||||
console.log(` TESTING WITH MODEL: ${model}`)
|
||||
console.log(`${"=".repeat(60)}\n`)
|
||||
|
||||
// Reconfigure API for this model
|
||||
await api.setConfiguration({
|
||||
apiProvider: "openrouter" as const,
|
||||
openRouterApiKey: process.env.OPENROUTER_API_KEY!,
|
||||
openRouterModelId: model,
|
||||
})
|
||||
|
||||
// Wait for API to be ready with new configuration
|
||||
await waitFor(() => api.isReady())
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
const mochaOptions: Mocha.MochaOptions = {
|
||||
ui: "tdd",
|
||||
timeout: 20 * 60 * 1_000, // 20m
|
||||
}
|
||||
|
||||
if (process.env.TEST_GREP) {
|
||||
mochaOptions.grep = process.env.TEST_GREP
|
||||
console.log(`Running tests matching pattern: ${process.env.TEST_GREP}`)
|
||||
}
|
||||
|
||||
const mocha = new Mocha(mochaOptions)
|
||||
|
||||
// Add test files fresh for each model run
|
||||
testFiles.forEach((testFile) => mocha.addFile(path.resolve(cwd, testFile)))
|
||||
|
||||
// Run tests for this model
|
||||
const modelResult = await new Promise<{ failures: number; passes: number }>((resolve) => {
|
||||
const runner = mocha.run((failures) => {
|
||||
resolve({
|
||||
failures,
|
||||
passes: runner.stats?.passes ?? 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
results.push({
|
||||
model,
|
||||
failures: modelResult.failures,
|
||||
passes: modelResult.passes,
|
||||
duration,
|
||||
})
|
||||
|
||||
totalFailures += modelResult.failures
|
||||
|
||||
console.log(
|
||||
`\n[${model}] Completed: ${modelResult.passes} passed, ${modelResult.failures} failed (${(duration / 1000).toFixed(1)}s)\n`,
|
||||
)
|
||||
|
||||
// Clear mocha's require cache to allow re-running tests
|
||||
mocha.dispose()
|
||||
testFiles.forEach((testFile) => {
|
||||
const fullPath = path.resolve(cwd, testFile)
|
||||
delete require.cache[require.resolve(fullPath)]
|
||||
})
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(`\n${"=".repeat(60)}`)
|
||||
console.log(` MULTI-MODEL TEST SUMMARY`)
|
||||
console.log(`${"=".repeat(60)}`)
|
||||
|
||||
for (const result of results) {
|
||||
const status = result.failures === 0 ? "✓ PASS" : "✗ FAIL"
|
||||
console.log(` ${status} ${result.model}`)
|
||||
console.log(
|
||||
` ${result.passes} passed, ${result.failures} failed (${(result.duration / 1000).toFixed(1)}s)`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`${"=".repeat(60)}\n`)
|
||||
|
||||
if (totalFailures > 0) {
|
||||
throw new Error(`${totalFailures} total test failures across all models.`)
|
||||
}
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
mocha.run((failures) => (failures === 0 ? resolve() : reject(new Error(`${failures} tests failed.`)))),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,92 +2,73 @@ import * as assert from "assert"
|
|||
|
||||
import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
||||
|
||||
import { waitFor } from "./utils"
|
||||
import { sleep, waitFor, waitUntilCompleted } from "./utils"
|
||||
|
||||
suite("Roo Code Subtasks", () => {
|
||||
test("Should create and complete a subtask successfully", async function () {
|
||||
this.timeout(180_000) // 3 minutes for complex orchestration
|
||||
suite.skip("Roo Code Subtasks", () => {
|
||||
test("Should handle subtask cancellation and resumption correctly", async () => {
|
||||
const api = globalThis.api
|
||||
|
||||
const messages: ClineMessage[] = []
|
||||
let childTaskCompleted = false
|
||||
let parentCompleted = false
|
||||
const messages: Record<string, ClineMessage[]> = {}
|
||||
|
||||
// Listen for messages to detect subtask result
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Log completion messages
|
||||
if (message.type === "say" && message.say === "completion_result") {
|
||||
console.log("Completion result:", message.text?.substring(0, 100))
|
||||
api.on(RooCodeEventName.Message, ({ taskId, message }) => {
|
||||
if (message.type === "say" && message.partial === false) {
|
||||
messages[taskId] = messages[taskId] || []
|
||||
messages[taskId].push(message)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
})
|
||||
|
||||
// Listen for task completion
|
||||
const completionHandler = (taskId: string) => {
|
||||
if (taskId === parentTaskId) {
|
||||
parentCompleted = true
|
||||
console.log("✓ Parent task completed")
|
||||
} else {
|
||||
childTaskCompleted = true
|
||||
console.log("✓ Child task completed:", taskId)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, completionHandler)
|
||||
const childPrompt = "You are a calculator. Respond only with numbers. What is the square root of 9?"
|
||||
|
||||
const childPrompt = "What is 2 + 2? Respond with just the number."
|
||||
|
||||
// Start a parent task that will create a subtask
|
||||
console.log("Starting parent task that will spawn subtask...")
|
||||
// Start a parent task that will create a subtask.
|
||||
const parentTaskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
mode: "ask",
|
||||
alwaysAllowModeSwitch: true,
|
||||
alwaysAllowSubtasks: true,
|
||||
autoApprovalEnabled: true,
|
||||
enableCheckpoints: false,
|
||||
},
|
||||
text: `Create a subtask using the new_task tool with this message: "${childPrompt}". Wait for the subtask to complete, then tell me the result.`,
|
||||
text:
|
||||
"You are the parent task. " +
|
||||
`Create a subtask by using the new_task tool with the message '${childPrompt}'.` +
|
||||
"After creating the subtask, wait for it to complete and then respond 'Parent task resumed'.",
|
||||
})
|
||||
|
||||
try {
|
||||
// Wait for child task to complete
|
||||
console.log("Waiting for child task to complete...")
|
||||
await waitFor(() => childTaskCompleted, { timeout: 90_000 })
|
||||
console.log("✓ Child task completed")
|
||||
let spawnedTaskId: string | undefined = undefined
|
||||
|
||||
// Wait for parent to complete
|
||||
console.log("Waiting for parent task to complete...")
|
||||
await waitFor(() => parentCompleted, { timeout: 90_000 })
|
||||
console.log("✓ Parent task completed")
|
||||
// Wait for the subtask to be spawned and then cancel it.
|
||||
api.on(RooCodeEventName.TaskSpawned, (_, childTaskId) => (spawnedTaskId = childTaskId))
|
||||
await waitFor(() => !!spawnedTaskId)
|
||||
await sleep(1_000) // Give the task a chance to start and populate the history.
|
||||
await api.cancelCurrentTask()
|
||||
|
||||
// Verify the parent task mentions the subtask result (should contain "4")
|
||||
const hasSubtaskResult = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
m.say === "completion_result" &&
|
||||
m.text?.includes("4") &&
|
||||
m.text?.toLowerCase().includes("subtask"),
|
||||
)
|
||||
// Wait a bit to ensure any task resumption would have happened.
|
||||
await sleep(2_000)
|
||||
|
||||
// Verify all events occurred
|
||||
assert.ok(childTaskCompleted, "Child task should have completed")
|
||||
assert.ok(parentCompleted, "Parent task should have completed")
|
||||
assert.ok(hasSubtaskResult, "Parent task should mention the subtask result")
|
||||
// The parent task should not have resumed yet, so we shouldn't see
|
||||
// "Parent task resumed".
|
||||
assert.ok(
|
||||
messages[parentTaskId]?.find(({ type, text }) => type === "say" && text === "Parent task resumed") ===
|
||||
undefined,
|
||||
"Parent task should not have resumed after subtask cancellation",
|
||||
)
|
||||
|
||||
console.log("Test passed! Subtask orchestration working correctly")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, completionHandler)
|
||||
// Start a new task with the same message as the subtask.
|
||||
const anotherTaskId = await api.startNewTask({ text: childPrompt })
|
||||
await waitUntilCompleted({ api, taskId: anotherTaskId })
|
||||
|
||||
// Cancel any remaining tasks
|
||||
try {
|
||||
await api.cancelCurrentTask()
|
||||
} catch {
|
||||
// Task might already be complete
|
||||
}
|
||||
}
|
||||
// Wait a bit to ensure any task resumption would have happened.
|
||||
await sleep(2_000)
|
||||
|
||||
// The parent task should still not have resumed.
|
||||
assert.ok(
|
||||
messages[parentTaskId]?.find(({ type, text }) => type === "say" && text === "Parent task resumed") ===
|
||||
undefined,
|
||||
"Parent task should not have resumed after subtask cancellation",
|
||||
)
|
||||
|
||||
// Clean up - cancel all tasks.
|
||||
await api.clearCurrentTask()
|
||||
await waitUntilCompleted({ api, taskId: parentTaskId })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
|||
import { waitFor, sleep } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite("Roo Code apply_diff Tool", function () {
|
||||
// Testing with more capable AI model to see if it can handle apply_diff complexity
|
||||
suite.skip("Roo Code apply_diff Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let workspaceDir: string
|
||||
|
|
@ -152,36 +151,69 @@ function validateInput(input) {
|
|||
})
|
||||
|
||||
test("Should apply diff to modify existing file content", async function () {
|
||||
// Increase timeout for this specific test
|
||||
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.simpleModify
|
||||
const expectedContent = "Hello Universe\nThis is a test file\nWith multiple lines"
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let errorOccurred: string | null = null
|
||||
let applyDiffExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("apply_diff")) {
|
||||
applyDiffExecuted = true
|
||||
console.log("apply_diff tool executed!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task - let AI read the file first, then apply diff
|
||||
// Start task with apply_diff instruction - file already exists
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -190,66 +222,111 @@ function validateInput(input) {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `The file ${testFile.name} exists in the workspace. Use the apply_diff tool to change "Hello World" to "Hello Universe" in this file.`,
|
||||
})
|
||||
text: `Use apply_diff on the file ${testFile.name} to change "Hello World" to "Hello Universe". The file already exists with this content:
|
||||
${testFile.content}\nAssume the file exists and you can modify it directly.`,
|
||||
}) //Temporary measure since list_files ignores all the files inside a tmp workspace
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 60_000 })
|
||||
|
||||
// Check for early errors
|
||||
if (errorOccurred) {
|
||||
console.error("Early error detected:", errorOccurred)
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 90_000 })
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Check if the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after modification:", actualContent)
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The apply_diff tool should have been executed")
|
||||
assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed")
|
||||
|
||||
// Give time for file system operations
|
||||
await sleep(1000)
|
||||
|
||||
// Verify file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"File content should be modified correctly",
|
||||
)
|
||||
|
||||
console.log("Test passed! File modified successfully")
|
||||
console.log("Test passed! apply_diff tool executed and file modified successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test("Should apply multiple search/replace blocks in single diff", async function () {
|
||||
// Increase timeout for this specific test
|
||||
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.multipleReplace
|
||||
const expectedContent = `function compute(a, b) {
|
||||
const total = a + b
|
||||
const result = a * b
|
||||
return { total: total, result: result }
|
||||
}`
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let applyDiffExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (message.type === "say" && message.text) {
|
||||
console.log("AI response:", message.text.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("apply_diff")) {
|
||||
applyDiffExecuted = true
|
||||
console.log("apply_diff tool executed!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task - let AI read file first
|
||||
// Start task with multiple replacements - file already exists
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -258,39 +335,55 @@ function validateInput(input) {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `The file ${testFile.name} exists in the workspace. Use the apply_diff tool to rename the function "calculate" to "compute" and rename the parameters "x, y" to "a, b". Also rename the variables "sum" to "total" and "product" to "result" throughout the function.`,
|
||||
text: `Use apply_diff on the file ${testFile.name} to make ALL of these changes:
|
||||
1. Rename function "calculate" to "compute"
|
||||
2. Rename parameters "x, y" to "a, b"
|
||||
3. Rename variable "sum" to "total" (including in the return statement)
|
||||
4. Rename variable "product" to "result" (including in the return statement)
|
||||
5. In the return statement, change { sum: sum, product: product } to { total: total, result: result }
|
||||
|
||||
The file already exists with this content:
|
||||
${testFile.content}\nAssume the file exists and you can modify it directly.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task completion with longer timeout
|
||||
await waitFor(() => taskCompleted, { timeout: 90_000 })
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 60_000 })
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Check the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after modification:", actualContent)
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The apply_diff tool should have been executed")
|
||||
assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed")
|
||||
|
||||
// Give time for file system operations
|
||||
await sleep(1000)
|
||||
|
||||
// Verify file was modified - check key changes were made
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
assert.ok(
|
||||
actualContent.includes("function compute(a, b)"),
|
||||
"Function should be renamed to compute with params a, b",
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"All replacements should be applied correctly",
|
||||
)
|
||||
assert.ok(actualContent.includes("const total = a + b"), "Variable sum should be renamed to total")
|
||||
assert.ok(actualContent.includes("const result = a * b"), "Variable product should be renamed to result")
|
||||
// Note: We don't strictly require object keys to be renamed as that's a reasonable interpretation difference
|
||||
|
||||
console.log("Test passed! Multiple replacements applied successfully")
|
||||
console.log("Test passed! apply_diff tool executed and multiple replacements applied successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test("Should handle apply_diff with line number hints", async function () {
|
||||
// Increase timeout for this specific test
|
||||
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.lineNumbers
|
||||
|
|
@ -305,22 +398,42 @@ function keepThis() {
|
|||
}
|
||||
|
||||
// Footer comment`
|
||||
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let applyDiffExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("apply_diff")) {
|
||||
applyDiffExecuted = true
|
||||
console.log("apply_diff tool executed!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
|
|
@ -330,7 +443,7 @@ function keepThis() {
|
|||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task - let AI read file first
|
||||
// Start task with line number context - file already exists
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -339,32 +452,43 @@ function keepThis() {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `The file ${testFile.name} exists in the workspace. Use the apply_diff tool to change the function name "oldFunction" to "newFunction" and update its console.log message to "New implementation". Keep the rest of the file unchanged.`,
|
||||
text: `Use apply_diff on the file ${testFile.name} to change "oldFunction" to "newFunction" and update its console.log to "New implementation". Keep the rest of the file unchanged.
|
||||
|
||||
The file already exists with this content:
|
||||
${testFile.content}\nAssume the file exists and you can modify it directly.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task completion with longer timeout
|
||||
await waitFor(() => taskCompleted, { timeout: 90_000 })
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 60_000 })
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Check the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after modification:", actualContent)
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The apply_diff tool should have been executed")
|
||||
assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed")
|
||||
|
||||
// Give time for file system operations
|
||||
await sleep(1000)
|
||||
|
||||
// Verify file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"Only specified function should be modified",
|
||||
)
|
||||
|
||||
console.log("Test passed! Targeted modification successful")
|
||||
console.log("Test passed! apply_diff tool executed and targeted modification successful")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
|
@ -373,22 +497,51 @@ function keepThis() {
|
|||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.errorHandling
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let errorDetected = false
|
||||
let applyDiffAttempted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for error messages
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorDetected = true
|
||||
console.log("Error detected:", message.text)
|
||||
}
|
||||
|
||||
// Check if AI mentions it couldn't find the content
|
||||
if (message.type === "say" && message.text?.toLowerCase().includes("could not find")) {
|
||||
errorDetected = true
|
||||
console.log("AI reported search failure:", message.text)
|
||||
}
|
||||
|
||||
// Check for tool execution attempt
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("apply_diff")) {
|
||||
applyDiffAttempted = true
|
||||
console.log("apply_diff tool attempted!")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
|
|
@ -398,7 +551,7 @@ function keepThis() {
|
|||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task with invalid search content
|
||||
// Start task with invalid search content - file already exists
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -407,34 +560,46 @@ function keepThis() {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `The file ${testFile.name} exists in the workspace with content "Original content". Use the apply_diff tool to replace "This content does not exist" with "New content".
|
||||
text: `Use apply_diff on the file ${testFile.name} to replace "This content does not exist" with "New content".
|
||||
|
||||
IMPORTANT: The search pattern "This content does not exist" is NOT in the file. When apply_diff cannot find the search pattern, it should fail gracefully. Do NOT try to use write_to_file or any other tool.`,
|
||||
The file already exists with this content:
|
||||
${testFile.content}
|
||||
|
||||
IMPORTANT: The search pattern "This content does not exist" is NOT in the file. When apply_diff cannot find the search pattern, it should fail gracefully and the file content should remain unchanged. Do NOT try to use write_to_file or any other tool to modify the file. Only use apply_diff, and if the search pattern is not found, report that it could not be found.
|
||||
|
||||
Assume the file exists and you can modify it directly.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 90_000 })
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
// Wait for task completion or error
|
||||
await waitFor(() => taskCompleted || errorDetected, { timeout: 90_000 })
|
||||
|
||||
// Verify tool was attempted
|
||||
assert.ok(toolExecuted, "The apply_diff tool should have been attempted")
|
||||
// Give time for any final operations
|
||||
await sleep(2000)
|
||||
|
||||
// Give time for file system operations
|
||||
await sleep(1000)
|
||||
|
||||
// Verify file content remains unchanged
|
||||
// The file content should remain unchanged since the search pattern wasn't found
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
console.log("File content after task:", actualContent)
|
||||
|
||||
// The AI should have attempted to use apply_diff
|
||||
assert.strictEqual(applyDiffAttempted, true, "apply_diff tool should have been attempted")
|
||||
|
||||
// The content should remain unchanged since the search pattern wasn't found
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
testFile.content.trim(),
|
||||
"File content should remain unchanged when search pattern not found",
|
||||
)
|
||||
|
||||
console.log("Test passed! Error handled gracefully")
|
||||
console.log("Test passed! apply_diff attempted and error handled gracefully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
|
@ -461,32 +626,65 @@ function checkInput(input) {
|
|||
}
|
||||
return true
|
||||
}`
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let errorOccurred: string | null = null
|
||||
let applyDiffExecuted = false
|
||||
let applyDiffCount = 0
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("apply_diff")) {
|
||||
applyDiffExecuted = true
|
||||
applyDiffCount++
|
||||
console.log(`apply_diff tool executed! (count: ${applyDiffCount})`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task to edit two separate functions
|
||||
// Start task with instruction to edit two separate functions using multiple search/replace blocks
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -495,13 +693,13 @@ function checkInput(input) {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the apply_diff tool on the file ${testFile.name} to make these changes using TWO SEPARATE search/replace blocks within a SINGLE apply_diff call:
|
||||
text: `Use apply_diff on the file ${testFile.name} to make these changes. You MUST use TWO SEPARATE search/replace blocks within a SINGLE apply_diff call:
|
||||
|
||||
FIRST search/replace block: Edit the processData function to rename it to "transformData" and change "Processing data" to "Transforming data"
|
||||
|
||||
SECOND search/replace block: Edit the validateInput function to rename it to "checkInput" and change "Validating input" to "Checking input"
|
||||
|
||||
Important: Use multiple SEARCH/REPLACE blocks in one apply_diff call, NOT multiple apply_diff calls.
|
||||
Important: Use multiple SEARCH/REPLACE blocks in one apply_diff call, NOT multiple apply_diff calls. Each function should have its own search/replace block.
|
||||
|
||||
The file already exists with this content:
|
||||
${testFile.content}
|
||||
|
|
@ -510,24 +708,42 @@ Assume the file exists and you can modify it directly.`,
|
|||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test filename:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 60_000 })
|
||||
|
||||
// Check for early errors
|
||||
if (errorOccurred) {
|
||||
console.error("Early error detected:", errorOccurred)
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The apply_diff tool should have been executed")
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Give time for file system operations
|
||||
await sleep(1000)
|
||||
|
||||
// Verify file was modified correctly
|
||||
// Check if the file was modified correctly
|
||||
const actualContent = await fs.readFile(testFile.path, "utf-8")
|
||||
assert.strictEqual(actualContent.trim(), expectedContent.trim(), "Both functions should be modified")
|
||||
console.log("File content after modification:", actualContent)
|
||||
|
||||
console.log("Test passed! Multiple search/replace blocks applied successfully")
|
||||
// Verify tool was executed
|
||||
assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed")
|
||||
console.log(`apply_diff was executed ${applyDiffCount} time(s)`)
|
||||
|
||||
// Verify file content
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent.trim(),
|
||||
"Both functions should be modified with separate search/replace blocks",
|
||||
)
|
||||
|
||||
console.log("Test passed! apply_diff tool executed and multiple search/replace blocks applied successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import * as vscode from "vscode"
|
|||
|
||||
import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
||||
|
||||
import { sleep, waitUntilCompleted } from "../utils"
|
||||
import { waitFor, sleep, waitUntilCompleted } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite("Roo Code execute_command Tool", function () {
|
||||
suite.skip("Roo Code execute_command Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let workspaceDir: string
|
||||
|
|
@ -112,36 +112,61 @@ suite("Roo Code execute_command Tool", function () {
|
|||
await sleep(100)
|
||||
})
|
||||
|
||||
test("Should execute pwd command to get current directory", async function () {
|
||||
this.timeout(90_000)
|
||||
test("Should execute simple echo command", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const testFile = testFiles.simpleEcho
|
||||
let taskStarted = false
|
||||
let _taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let errorOccurred: string | null = null
|
||||
let executeCommandToolCalled = false
|
||||
let commandExecuted = ""
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
|
||||
// Check for command request (execute_command uses "command" not "tool")
|
||||
if (message.type === "ask" && message.ask === "command") {
|
||||
toolExecuted = true
|
||||
console.log("✓ execute_command requested!")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("execute_command")) {
|
||||
executeCommandToolCalled = true
|
||||
// The request contains the actual tool execution result
|
||||
commandExecuted = requestData.request
|
||||
console.log("execute_command tool called, full request:", commandExecuted.substring(0, 300))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task - pwd can only be done with execute_command
|
||||
// Start task with execute_command instruction
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -150,64 +175,104 @@ suite("Roo Code execute_command Tool", function () {
|
|||
allowedCommands: ["*"],
|
||||
terminalShellIntegrationDisabled: true,
|
||||
},
|
||||
text: `Use the execute_command tool to run the "pwd" command and tell me what the current working directory is.`,
|
||||
text: `Use the execute_command tool to run this command: echo "Hello from test" > ${testFile.name}
|
||||
|
||||
The file ${testFile.name} will be created in the current workspace directory. Assume you can execute this command directly.
|
||||
|
||||
Then use the attempt_completion tool to complete the task. Do not suggest any commands in the attempt_completion.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test file:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Wait for task completion
|
||||
await waitUntilCompleted({ api, taskId, timeout: 90_000 })
|
||||
await waitUntilCompleted({ api, taskId, timeout: 60_000 })
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The execute_command tool should have been executed")
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`)
|
||||
|
||||
// Verify AI mentioned a directory path
|
||||
const hasPath = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("/tmp/roo-test-workspace") || m.text?.includes("directory")),
|
||||
// Verify tool was called
|
||||
assert.ok(executeCommandToolCalled, "execute_command tool should have been called")
|
||||
assert.ok(
|
||||
commandExecuted.includes("echo") && commandExecuted.includes(testFile.name),
|
||||
`Command should include 'echo' and test file name. Got: ${commandExecuted.substring(0, 200)}`,
|
||||
)
|
||||
assert.ok(hasPath, "AI should have mentioned the working directory")
|
||||
|
||||
console.log("Test passed! pwd command executed successfully")
|
||||
// Verify file was created with correct content
|
||||
const content = await fs.readFile(testFile.path, "utf-8")
|
||||
assert.ok(content.includes("Hello from test"), "File should contain the echoed text")
|
||||
|
||||
console.log("Test passed! Command executed successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
// Clean up event listeners
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test("Should execute date command to get current timestamp", async function () {
|
||||
this.timeout(90_000)
|
||||
test("Should execute command with custom working directory", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let taskStarted = false
|
||||
let _taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let errorOccurred: string | null = null
|
||||
let executeCommandToolCalled = false
|
||||
let cwdUsed = ""
|
||||
|
||||
// Create subdirectory
|
||||
const subDir = path.join(workspaceDir, "test-subdir")
|
||||
await fs.mkdir(subDir, { recursive: true })
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
|
||||
// Check for command request (execute_command uses "command" not "tool")
|
||||
if (message.type === "ask" && message.ask === "command") {
|
||||
toolExecuted = true
|
||||
console.log("✓ execute_command requested!")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("execute_command")) {
|
||||
executeCommandToolCalled = true
|
||||
// Check if the request contains the cwd
|
||||
if (requestData.request.includes(subDir) || requestData.request.includes("test-subdir")) {
|
||||
cwdUsed = subDir
|
||||
}
|
||||
console.log("execute_command tool called, checking for cwd in request")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task - date command can only be done with execute_command
|
||||
// Start task with execute_command instruction using cwd parameter
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -216,66 +281,234 @@ suite("Roo Code execute_command Tool", function () {
|
|||
allowedCommands: ["*"],
|
||||
terminalShellIntegrationDisabled: true,
|
||||
},
|
||||
text: `Use the execute_command tool to run the "date" command and tell me what the current date and time is.`,
|
||||
text: `Use the execute_command tool with these exact parameters:
|
||||
- command: echo "Test in subdirectory" > output.txt
|
||||
- cwd: ${subDir}
|
||||
|
||||
The subdirectory ${subDir} exists in the workspace. Assume you can execute this command directly with the specified working directory.
|
||||
|
||||
Avoid at all costs suggesting a command when using the attempt_completion tool`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Subdirectory:", subDir)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Wait for task completion
|
||||
await waitUntilCompleted({ api, taskId, timeout: 60_000 })
|
||||
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`)
|
||||
|
||||
// Verify tool was called with correct cwd
|
||||
assert.ok(executeCommandToolCalled, "execute_command tool should have been called")
|
||||
assert.ok(
|
||||
cwdUsed.includes(subDir) || cwdUsed.includes("test-subdir"),
|
||||
"Command should have used the subdirectory as cwd",
|
||||
)
|
||||
|
||||
// Verify file was created in subdirectory
|
||||
const outputPath = path.join(subDir, "output.txt")
|
||||
const content = await fs.readFile(outputPath, "utf-8")
|
||||
assert.ok(content.includes("Test in subdirectory"), "File should contain the echoed text")
|
||||
|
||||
// Clean up created file
|
||||
await fs.unlink(outputPath)
|
||||
|
||||
console.log("Test passed! Command executed in custom directory")
|
||||
} finally {
|
||||
// Clean up event listeners
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
// Clean up subdirectory
|
||||
try {
|
||||
await fs.rmdir(subDir)
|
||||
} catch {
|
||||
// Directory might not be empty
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("Should execute multiple commands sequentially", async function () {
|
||||
const api = globalThis.api
|
||||
const testFile = testFiles.multiCommand
|
||||
let taskStarted = false
|
||||
let _taskCompleted = false
|
||||
let errorOccurred: string | null = null
|
||||
let executeCommandCallCount = 0
|
||||
const commandsExecuted: string[] = []
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("execute_command")) {
|
||||
executeCommandCallCount++
|
||||
// Store the full request to check for command content
|
||||
commandsExecuted.push(requestData.request)
|
||||
console.log(`execute_command tool call #${executeCommandCallCount}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task with multiple commands - simplified to just 2 commands
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ["*"],
|
||||
terminalShellIntegrationDisabled: true,
|
||||
},
|
||||
text: `Use the execute_command tool to create a file with multiple lines. Execute these commands one by one:
|
||||
1. echo "Line 1" > ${testFile.name}
|
||||
2. echo "Line 2" >> ${testFile.name}
|
||||
|
||||
The file ${testFile.name} will be created in the current workspace directory. Assume you can execute these commands directly.
|
||||
|
||||
Important: Use only the echo command which is available on all Unix platforms. Execute each command separately using the execute_command tool.
|
||||
|
||||
After both commands are executed, use the attempt_completion tool to complete the task.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Test file:", testFile.name)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 90_000 })
|
||||
|
||||
// Wait for task completion with increased timeout
|
||||
await waitUntilCompleted({ api, taskId, timeout: 90_000 })
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The execute_command tool should have been executed")
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`)
|
||||
|
||||
// Verify AI mentioned date/time information
|
||||
const hasDateTime = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.match(/\d{4}/) ||
|
||||
m.text?.toLowerCase().includes("202") ||
|
||||
m.text?.toLowerCase().includes("time")),
|
||||
// Verify tool was called multiple times (reduced to 2)
|
||||
assert.ok(
|
||||
executeCommandCallCount >= 2,
|
||||
`execute_command tool should have been called at least 2 times, was called ${executeCommandCallCount} times`,
|
||||
)
|
||||
assert.ok(
|
||||
commandsExecuted.some((cmd) => cmd.includes("Line 1")),
|
||||
`Should have executed first command. Commands: ${commandsExecuted.map((c) => c.substring(0, 100)).join(", ")}`,
|
||||
)
|
||||
assert.ok(
|
||||
commandsExecuted.some((cmd) => cmd.includes("Line 2")),
|
||||
"Should have executed second command",
|
||||
)
|
||||
assert.ok(hasDateTime, "AI should have mentioned date/time information")
|
||||
|
||||
console.log("Test passed! date command executed successfully")
|
||||
// Verify file contains outputs
|
||||
const content = await fs.readFile(testFile.path, "utf-8")
|
||||
assert.ok(content.includes("Line 1"), "Should contain first line")
|
||||
assert.ok(content.includes("Line 2"), "Should contain second line")
|
||||
|
||||
console.log("Test passed! Multiple commands executed successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
// Clean up event listeners
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test("Should execute ls command to list directory contents", async function () {
|
||||
this.timeout(90_000)
|
||||
test("Should handle long-running commands", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let taskStarted = false
|
||||
let _taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let _commandCompleted = false
|
||||
let errorOccurred: string | null = null
|
||||
let executeCommandToolCalled = false
|
||||
let commandExecuted = ""
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "say" && message.say === "command_output") {
|
||||
if (message.text?.includes("completed after delay")) {
|
||||
_commandCompleted = true
|
||||
}
|
||||
console.log("Command output:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for command request (execute_command uses "command" not "tool")
|
||||
if (message.type === "ask" && message.ask === "command") {
|
||||
toolExecuted = true
|
||||
console.log("✓ execute_command requested!")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
console.log("API request started:", message.text.substring(0, 200))
|
||||
try {
|
||||
const requestData = JSON.parse(message.text)
|
||||
if (requestData.request && requestData.request.includes("execute_command")) {
|
||||
executeCommandToolCalled = true
|
||||
// The request contains the actual tool execution result
|
||||
commandExecuted = requestData.request
|
||||
console.log("execute_command tool called, full request:", commandExecuted.substring(0, 300))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse api_req_started message:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task - ls can only be done with execute_command
|
||||
// Platform-specific sleep command
|
||||
const sleepCommand = process.platform === "win32" ? "timeout /t 3 /nobreak" : "sleep 3"
|
||||
|
||||
// Start task with long-running command
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -284,97 +517,41 @@ suite("Roo Code execute_command Tool", function () {
|
|||
allowedCommands: ["*"],
|
||||
terminalShellIntegrationDisabled: true,
|
||||
},
|
||||
text: `Use the execute_command tool to run "ls -la" and tell me what files and directories you see.`,
|
||||
text: `Use the execute_command tool to run: ${sleepCommand} && echo "Command completed after delay"
|
||||
|
||||
Assume you can execute this command directly in the current workspace directory.
|
||||
|
||||
Avoid at all costs suggesting a command when using the attempt_completion tool`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
|
||||
// Wait for task completion
|
||||
await waitUntilCompleted({ api, taskId, timeout: 90_000 })
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The execute_command tool should have been executed")
|
||||
// Wait for task completion (the command output check will verify execution)
|
||||
await waitUntilCompleted({ api, taskId, timeout: 45_000 })
|
||||
|
||||
// Verify AI mentioned directory contents
|
||||
const hasListing = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("file") || m.text?.includes("directory") || m.text?.includes("drwx")),
|
||||
// Give a bit of time for final output processing
|
||||
await sleep(1000)
|
||||
|
||||
// Verify no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`)
|
||||
|
||||
// Verify tool was called
|
||||
assert.ok(executeCommandToolCalled, "execute_command tool should have been called")
|
||||
assert.ok(
|
||||
commandExecuted.includes("sleep") || commandExecuted.includes("timeout"),
|
||||
`Command should include sleep or timeout command. Got: ${commandExecuted.substring(0, 200)}`,
|
||||
)
|
||||
assert.ok(hasListing, "AI should have mentioned directory listing")
|
||||
|
||||
console.log("Test passed! ls command executed successfully")
|
||||
// The command output check in the message handler will verify execution
|
||||
|
||||
console.log("Test passed! Long-running command handled successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test("Should execute whoami command to get current user", async function () {
|
||||
this.timeout(90_000)
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let _taskCompleted = false
|
||||
let toolExecuted = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for command request (execute_command uses "command" not "tool")
|
||||
if (message.type === "ask" && message.ask === "command") {
|
||||
toolExecuted = true
|
||||
console.log("✓ execute_command requested!")
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task - whoami can only be done with execute_command
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ["*"],
|
||||
terminalShellIntegrationDisabled: true,
|
||||
},
|
||||
text: `Use the execute_command tool to run "whoami" and tell me what user account is running.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
|
||||
// Wait for task completion
|
||||
await waitUntilCompleted({ api, taskId, timeout: 90_000 })
|
||||
|
||||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The execute_command tool should have been executed")
|
||||
|
||||
// Verify AI mentioned a username
|
||||
const hasUser = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
m.text &&
|
||||
m.text.length > 5,
|
||||
)
|
||||
assert.ok(hasUser, "AI should have mentioned the username")
|
||||
|
||||
console.log("Test passed! whoami command executed successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
// Clean up event listeners
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
|||
import { waitFor, sleep } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite("Roo Code list_files Tool", function () {
|
||||
suite.skip("Roo Code list_files Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let workspaceDir: string
|
||||
|
|
@ -174,20 +174,37 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
})
|
||||
|
||||
test("Should list files in a directory (non-recursive)", async function () {
|
||||
this.timeout(90_000) // Increase timeout for this specific test
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let listResults: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution and capture results
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("list_files")) {
|
||||
toolExecuted = true
|
||||
console.log("list_files tool executed:", text.substring(0, 200))
|
||||
|
||||
// Extract list results from the tool execution
|
||||
try {
|
||||
const jsonMatch = text.match(/\{"request":".*?"\}/)
|
||||
if (jsonMatch) {
|
||||
const requestData = JSON.parse(jsonMatch[0])
|
||||
if (requestData.request && requestData.request.includes("Result:")) {
|
||||
listResults = requestData.request
|
||||
console.log("Captured list results:", listResults?.substring(0, 300))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse list results:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -211,28 +228,45 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the list_files tool with path="${testDirName}" and recursive=false, then tell me what you found.`,
|
||||
text: `I have created a test directory structure in the workspace. Use the list_files tool to list the contents of the directory "${testDirName}" (non-recursive). The directory contains files like root-file-1.txt, root-file-2.js, config.yaml, README.md, and a nested subdirectory. The directory exists in the workspace.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 90_000 })
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify the list_files tool was executed
|
||||
assert.ok(toolExecuted, "The list_files tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned some expected files in its response
|
||||
const hasFiles = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("root-file") ||
|
||||
m.text?.includes("config") ||
|
||||
m.text?.includes("README") ||
|
||||
m.text?.includes("nested")),
|
||||
)
|
||||
assert.ok(hasFiles, "AI should have mentioned the files found in the directory")
|
||||
// Verify the tool returned the expected files (non-recursive)
|
||||
assert.ok(listResults, "Tool execution results should be captured")
|
||||
|
||||
// Check that expected root-level files are present (including hidden files now that bug is fixed)
|
||||
const expectedFiles = ["root-file-1.txt", "root-file-2.js", "config.yaml", "README.md", ".hidden-file"]
|
||||
const expectedDirs = ["nested/"]
|
||||
|
||||
const results = listResults as string
|
||||
for (const file of expectedFiles) {
|
||||
assert.ok(results.includes(file), `Tool results should include ${file}`)
|
||||
}
|
||||
|
||||
for (const dir of expectedDirs) {
|
||||
assert.ok(results.includes(dir), `Tool results should include directory ${dir}`)
|
||||
}
|
||||
|
||||
// Verify hidden files are now included (bug has been fixed)
|
||||
console.log("Verifying hidden files are included in non-recursive mode")
|
||||
assert.ok(results.includes(".hidden-file"), "Hidden files should be included in non-recursive mode")
|
||||
|
||||
// Verify nested files are NOT included (non-recursive)
|
||||
const nestedFiles = ["nested-file-1.md", "nested-file-2.json", "deep-nested-file.ts"]
|
||||
for (const file of nestedFiles) {
|
||||
assert.ok(
|
||||
!results.includes(file),
|
||||
`Tool results should NOT include nested file ${file} in non-recursive mode`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log("Test passed! Directory listing (non-recursive) executed successfully")
|
||||
} finally {
|
||||
|
|
@ -247,15 +281,33 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let listResults: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution and capture results
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("list_files")) {
|
||||
toolExecuted = true
|
||||
console.log("list_files tool executed (recursive):", text.substring(0, 200))
|
||||
|
||||
// Extract list results from the tool execution
|
||||
try {
|
||||
const jsonMatch = text.match(/\{"request":".*?"\}/)
|
||||
if (jsonMatch) {
|
||||
const requestData = JSON.parse(jsonMatch[0])
|
||||
if (requestData.request && requestData.request.includes("Result:")) {
|
||||
listResults = requestData.request
|
||||
console.log("Captured recursive list results:", listResults?.substring(0, 300))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse recursive list results:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -279,7 +331,7 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the list_files tool to list ALL contents of the directory "${testDirName}" recursively (set recursive to true). Tell me what files and directories you find, including any nested content.`,
|
||||
text: `I have created a test directory structure in the workspace. Use the list_files tool to list ALL contents of the directory "${testDirName}" recursively (set recursive to true). The directory contains nested subdirectories with files like nested-file-1.md, nested-file-2.json, and deep-nested-file.ts. The directory exists in the workspace.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
|
|
@ -290,14 +342,41 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
// Verify the list_files tool was executed
|
||||
assert.ok(toolExecuted, "The list_files tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned files/directories in its response
|
||||
const hasContent = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("nested") || m.text?.includes("file") || m.text?.includes("directory")),
|
||||
// Verify the tool returned results for recursive listing
|
||||
assert.ok(listResults, "Tool execution results should be captured for recursive listing")
|
||||
|
||||
const results = listResults as string
|
||||
console.log("RECURSIVE BUG DETECTED: Tool only returns directories, not files")
|
||||
console.log("Actual recursive results:", results)
|
||||
|
||||
// BUG: Recursive mode is severely broken - only returns directories
|
||||
// Expected behavior: Should return ALL files and directories recursively
|
||||
// Actual behavior: Only returns top-level directories
|
||||
|
||||
// Current buggy behavior - only directories are returned
|
||||
assert.ok(results.includes("nested/"), "Recursive results should at least include nested/ directory")
|
||||
|
||||
// Document what SHOULD be included but currently isn't due to bugs:
|
||||
const shouldIncludeFiles = [
|
||||
"root-file-1.txt",
|
||||
"root-file-2.js",
|
||||
"config.yaml",
|
||||
"README.md",
|
||||
".hidden-file",
|
||||
"nested-file-1.md",
|
||||
"nested-file-2.json",
|
||||
"deep-nested-file.ts",
|
||||
]
|
||||
const shouldIncludeDirs = ["nested/", "deep/"]
|
||||
|
||||
console.log("MISSING FILES (should be included in recursive mode):", shouldIncludeFiles)
|
||||
console.log(
|
||||
"MISSING DIRECTORIES (should be included in recursive mode):",
|
||||
shouldIncludeDirs.filter((dir) => !results.includes(dir)),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned the directory contents")
|
||||
|
||||
// Test passes with current buggy behavior, but documents the issues
|
||||
console.log("CRITICAL BUG: Recursive list_files is completely broken - returns almost no files")
|
||||
|
||||
console.log("Test passed! Directory listing (recursive) executed successfully")
|
||||
} finally {
|
||||
|
|
@ -312,15 +391,33 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let listResults: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution and capture results
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("list_files")) {
|
||||
toolExecuted = true
|
||||
console.log("list_files tool executed (symlinks):", text.substring(0, 200))
|
||||
|
||||
// Extract list results from the tool execution
|
||||
try {
|
||||
const jsonMatch = text.match(/\{"request":".*?"\}/)
|
||||
if (jsonMatch) {
|
||||
const requestData = JSON.parse(jsonMatch[0])
|
||||
if (requestData.request && requestData.request.includes("Result:")) {
|
||||
listResults = requestData.request
|
||||
console.log("Captured symlink test results:", listResults?.substring(0, 300))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse symlink test results:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -369,7 +466,7 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the list_files tool to list the contents of the directory "${testDirName}". Tell me what you find.`,
|
||||
text: `I have created a test directory with symlinks at "${testDirName}". Use the list_files tool to list the contents of this directory. It should show both the original files/directories and the symlinked ones. The directory contains symlinks to both a file and a directory.`,
|
||||
})
|
||||
|
||||
console.log("Symlink test Task ID:", taskId)
|
||||
|
|
@ -380,16 +477,23 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
// Verify the list_files tool was executed
|
||||
assert.ok(toolExecuted, "The list_files tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned files/directories in its response
|
||||
const hasContent = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("link") || m.text?.includes("source") || m.text?.includes("file")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned the directory contents")
|
||||
// Verify the tool returned results
|
||||
assert.ok(listResults, "Tool execution results should be captured")
|
||||
|
||||
console.log("Test passed! Symlinked files and directories listed successfully")
|
||||
const results = listResults as string
|
||||
console.log("Symlink test results:", results)
|
||||
|
||||
// Check that symlinked items are visible
|
||||
assert.ok(
|
||||
results.includes("link-to-file.txt") || results.includes("source-file.txt"),
|
||||
"Should see either the symlink or the target file",
|
||||
)
|
||||
assert.ok(
|
||||
results.includes("link-to-dir") || results.includes("source/"),
|
||||
"Should see either the symlink or the target directory",
|
||||
)
|
||||
|
||||
console.log("Test passed! Symlinked files and directories are now visible")
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(testDir, { recursive: true, force: true })
|
||||
|
|
@ -410,10 +514,13 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("list_files")) {
|
||||
toolExecuted = true
|
||||
console.log("list_files tool executed (workspace root):", text.substring(0, 200))
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -436,7 +543,7 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the list_files tool to list the contents of the current workspace directory (use "." as the path). Tell me what you find.`,
|
||||
text: `Use the list_files tool to list the contents of the current workspace directory (use "." as the path). This should show the top-level files and directories in the workspace.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
|
|
@ -447,14 +554,17 @@ This directory contains various files and subdirectories for testing the list_fi
|
|||
// Verify the list_files tool was executed
|
||||
assert.ok(toolExecuted, "The list_files tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned workspace contents in its response
|
||||
const hasContent = messages.some(
|
||||
// Verify the AI mentioned some expected workspace files/directories
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("directory") || m.text?.includes("file") || m.text?.includes("list")),
|
||||
(m.text?.includes("list-files-test-") ||
|
||||
m.text?.includes("directory") ||
|
||||
m.text?.includes("files") ||
|
||||
m.text?.includes("workspace")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned workspace contents")
|
||||
assert.ok(completionMessage, "AI should have mentioned workspace contents")
|
||||
|
||||
console.log("Test passed! Workspace root directory listing executed successfully")
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
|||
import { waitFor, sleep } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite("Roo Code read_file Tool", function () {
|
||||
suite.skip("Roo Code read_file Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let tempDir: string
|
||||
|
|
@ -129,24 +129,16 @@ suite("Roo Code read_file Tool", function () {
|
|||
let toolExecuted = false
|
||||
let toolResult: string | null = null
|
||||
|
||||
// Listen for messages - register BEFORE starting task
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request (ask) - this happens when AI wants to use the tool
|
||||
// With autoApproval, this might be auto-approved so we just check for the ask type
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested (ask):", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for tool execution result (say) - this happens after tool is executed
|
||||
// Check for tool execution and extract result
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
console.log("api_req_started message:", text.substring(0, 200))
|
||||
if (text.includes("read_file")) {
|
||||
toolExecuted = true
|
||||
console.log("Tool executed (say):", text.substring(0, 200))
|
||||
console.log("Tool executed:", text.substring(0, 200))
|
||||
|
||||
// Parse the tool result from the api_req_started message
|
||||
try {
|
||||
|
|
@ -187,11 +179,6 @@ suite("Roo Code read_file Tool", function () {
|
|||
if (message.type === "say" && (message.say === "text" || message.say === "completion_result")) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Log ALL message types for debugging
|
||||
console.log(
|
||||
`Message: type=${message.type}, ${message.type === "ask" ? "ask=" + message.ask : "say=" + message.say}`,
|
||||
)
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
|
|
@ -216,7 +203,7 @@ suite("Roo Code read_file Tool", function () {
|
|||
try {
|
||||
// Start task with a simple read file request
|
||||
const fileName = path.basename(testFiles.simple)
|
||||
// Use a very explicit prompt WITHOUT revealing the content
|
||||
// Use a very explicit prompt
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -224,7 +211,7 @@ suite("Roo Code read_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the read_file tool to read the file named "${fileName}" in the current workspace directory and tell me what it contains.`,
|
||||
text: `Please use the read_file tool to read the file named "${fileName}". This file contains the text "Hello, World!" and is located in the current workspace directory. Assume the file exists and you can read it directly. After reading it, tell me what the file contains.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
|
|
@ -248,7 +235,18 @@ suite("Roo Code read_file Tool", function () {
|
|||
// Check that no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, "No errors should have occurred")
|
||||
|
||||
// Verify the AI mentioned the content in its response
|
||||
// Verify the tool returned the correct content
|
||||
assert.ok(toolResult !== null, "Tool should have returned a result")
|
||||
// The tool returns content with line numbers, so we need to extract just the content
|
||||
// For single line, the format is "1 | Hello, World!"
|
||||
const actualContent = (toolResult as string).replace(/^\d+\s*\|\s*/, "")
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
"Hello, World!",
|
||||
"Tool should have returned the exact file content",
|
||||
)
|
||||
|
||||
// Also verify the AI mentioned the content in its response
|
||||
const hasContent = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
|
|
@ -259,7 +257,6 @@ suite("Roo Code read_file Tool", function () {
|
|||
assert.ok(hasContent, "AI should have mentioned the file content 'Hello, World!'")
|
||||
|
||||
console.log("Test passed! File read successfully with correct content")
|
||||
console.log(`Total messages: ${messages.length}, Tool executed: ${toolExecuted}`)
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -273,15 +270,43 @@ suite("Roo Code read_file Tool", function () {
|
|||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let toolResult: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested for multiline file")
|
||||
// Check for tool execution and extract result
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("read_file")) {
|
||||
toolExecuted = true
|
||||
console.log("Tool executed for multiline file")
|
||||
|
||||
// Parse the tool result
|
||||
try {
|
||||
const requestData = JSON.parse(text)
|
||||
if (requestData.request && requestData.request.includes("[read_file")) {
|
||||
console.log("Full request for debugging:", requestData.request)
|
||||
// Try multiple patterns to extract the content
|
||||
let resultMatch = requestData.request.match(/```[^`]*\n([\s\S]*?)\n```/)
|
||||
if (!resultMatch) {
|
||||
resultMatch = requestData.request.match(/Result:[\s\S]*?\n((?:\d+\s*\|[^\n]*\n?)+)/)
|
||||
}
|
||||
if (!resultMatch) {
|
||||
resultMatch = requestData.request.match(/Result:\s*\n([\s\S]+?)(?:\n\n|$)/)
|
||||
}
|
||||
if (resultMatch) {
|
||||
toolResult = resultMatch[1]
|
||||
console.log("Extracted multiline tool result")
|
||||
} else {
|
||||
console.log("Could not extract tool result from request")
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse tool result:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log AI responses
|
||||
|
|
@ -310,7 +335,7 @@ suite("Roo Code read_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the read_file tool to read the file "${fileName}" in the current workspace directory. Count how many lines it has and tell me what you found.`,
|
||||
text: `Use the read_file tool to read the file "${fileName}" which contains 5 lines of text (Line 1, Line 2, Line 3, Line 4, Line 5). Assume the file exists and you can read it directly. Count how many lines it has and tell me the result.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
|
|
@ -319,16 +344,31 @@ suite("Roo Code read_file Tool", function () {
|
|||
// Verify the read_file tool was executed
|
||||
assert.ok(toolExecuted, "The read_file tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned the correct number of lines
|
||||
// Verify the tool returned the correct multiline content
|
||||
assert.ok(toolResult !== null, "Tool should have returned a result")
|
||||
// The tool returns content with line numbers, so we need to extract just the content
|
||||
const lines = (toolResult as string).split("\n").map((line) => {
|
||||
const match = line.match(/^\d+\s*\|\s*(.*)$/)
|
||||
return match ? match[1] : line
|
||||
})
|
||||
const actualContent = lines.join("\n")
|
||||
const expectedContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
||||
assert.strictEqual(
|
||||
actualContent.trim(),
|
||||
expectedContent,
|
||||
"Tool should have returned the exact multiline content",
|
||||
)
|
||||
|
||||
// Also verify the AI mentioned the correct number of lines
|
||||
const hasLineCount = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("5") || m.text?.toLowerCase().includes("five") || m.text?.includes("Line")),
|
||||
(m.text?.includes("5") || m.text?.toLowerCase().includes("five")),
|
||||
)
|
||||
assert.ok(hasLineCount, "AI should have mentioned the file lines")
|
||||
assert.ok(hasLineCount, "AI should have mentioned the file has 5 lines")
|
||||
|
||||
console.log("Test passed! Multiline file read successfully")
|
||||
console.log("Test passed! Multiline file read successfully with correct content")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -341,15 +381,43 @@ suite("Roo Code read_file Tool", function () {
|
|||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let toolResult: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested for line range")
|
||||
// Check for tool execution and extract result
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("read_file")) {
|
||||
toolExecuted = true
|
||||
console.log("Tool executed:", text.substring(0, 300))
|
||||
|
||||
// Parse the tool result
|
||||
try {
|
||||
const requestData = JSON.parse(text)
|
||||
if (requestData.request && requestData.request.includes("[read_file")) {
|
||||
console.log("Full request for debugging:", requestData.request)
|
||||
// Try multiple patterns to extract the content
|
||||
let resultMatch = requestData.request.match(/```[^`]*\n([\s\S]*?)\n```/)
|
||||
if (!resultMatch) {
|
||||
resultMatch = requestData.request.match(/Result:[\s\S]*?\n((?:\d+\s*\|[^\n]*\n?)+)/)
|
||||
}
|
||||
if (!resultMatch) {
|
||||
resultMatch = requestData.request.match(/Result:\s*\n([\s\S]+?)(?:\n\n|$)/)
|
||||
}
|
||||
if (resultMatch) {
|
||||
toolResult = resultMatch[1]
|
||||
console.log("Extracted line range tool result")
|
||||
} else {
|
||||
console.log("Could not extract tool result from request")
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse tool result:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log AI responses
|
||||
|
|
@ -378,7 +446,7 @@ suite("Roo Code read_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the read_file tool to read the file "${fileName}" in the current workspace directory and show me what's on lines 2, 3, and 4.`,
|
||||
text: `Use the read_file tool to read the file "${fileName}" and show me what's on lines 2, 3, and 4. The file contains lines like "Line 1", "Line 2", etc. Assume the file exists and you can read it directly.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
|
|
@ -387,12 +455,29 @@ suite("Roo Code read_file Tool", function () {
|
|||
// Verify tool was executed
|
||||
assert.ok(toolExecuted, "The read_file tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned the specific lines
|
||||
// Verify the tool returned the correct lines (when line range is used)
|
||||
if (toolResult && (toolResult as string).includes(" | ")) {
|
||||
// The result includes line numbers
|
||||
assert.ok(
|
||||
(toolResult as string).includes("2 | Line 2"),
|
||||
"Tool result should include line 2 with line number",
|
||||
)
|
||||
assert.ok(
|
||||
(toolResult as string).includes("3 | Line 3"),
|
||||
"Tool result should include line 3 with line number",
|
||||
)
|
||||
assert.ok(
|
||||
(toolResult as string).includes("4 | Line 4"),
|
||||
"Tool result should include line 4 with line number",
|
||||
)
|
||||
}
|
||||
|
||||
// Also verify the AI mentioned the specific lines
|
||||
const hasLines = messages.some(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("Line 2") || m.text?.includes("Line 3") || m.text?.includes("Line 4")),
|
||||
m.text?.includes("Line 2"),
|
||||
)
|
||||
assert.ok(hasLines, "AI should have mentioned the requested lines")
|
||||
|
||||
|
|
@ -409,15 +494,22 @@ suite("Roo Code read_file Tool", function () {
|
|||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let _errorHandled = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested for non-existent file")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("read_file")) {
|
||||
toolExecuted = true
|
||||
// Check if error was returned
|
||||
if (text.includes("error") || text.includes("not found")) {
|
||||
_errorHandled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -479,10 +571,13 @@ suite("Roo Code read_file Tool", function () {
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested for XML file")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("read_file")) {
|
||||
toolExecuted = true
|
||||
console.log("Tool executed for XML file")
|
||||
}
|
||||
}
|
||||
|
||||
// Log AI responses
|
||||
|
|
@ -511,7 +606,7 @@ suite("Roo Code read_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the read_file tool to read the XML file "${fileName}" in the current workspace directory and tell me what XML elements you find.`,
|
||||
text: `Use the read_file tool to read the XML file "${fileName}". It contains XML elements including root, child, and data. Assume the file exists and you can read it directly. Tell me what elements you find.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
|
|
@ -538,7 +633,6 @@ suite("Roo Code read_file Tool", function () {
|
|||
})
|
||||
|
||||
test("Should read multiple files in sequence", async function () {
|
||||
this.timeout(90_000) // Increase timeout for multiple file reads
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
|
|
@ -549,9 +643,12 @@ suite("Roo Code read_file Tool", function () {
|
|||
messages.push(message)
|
||||
|
||||
// Count read_file executions
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
readFileCount++
|
||||
console.log(`Read file execution #${readFileCount}`)
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("read_file")) {
|
||||
readFileCount++
|
||||
console.log(`Read file execution #${readFileCount}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -576,11 +673,14 @@ suite("Roo Code read_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the read_file tool to read "${simpleFileName}" and "${multilineFileName}", then tell me what you found.`,
|
||||
text: `Use the read_file tool to read these two files:
|
||||
1. "${simpleFileName}" - contains "Hello, World!"
|
||||
2. "${multilineFileName}" - contains 5 lines of text
|
||||
Assume both files exist and you can read them directly. Read each file and tell me what you found in each one.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 90_000 })
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify multiple read_file executions - AI might read them together
|
||||
assert.ok(
|
||||
|
|
@ -606,9 +706,6 @@ suite("Roo Code read_file Tool", function () {
|
|||
})
|
||||
|
||||
test("Should read large file efficiently", async function () {
|
||||
// Testing with more capable model and increased timeout
|
||||
this.timeout(180_000) // 3 minutes
|
||||
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
|
|
@ -618,10 +715,13 @@ suite("Roo Code read_file Tool", function () {
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested for large file")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("read_file")) {
|
||||
toolExecuted = true
|
||||
console.log("Reading large file...")
|
||||
}
|
||||
}
|
||||
|
||||
// Log AI responses
|
||||
|
|
@ -650,11 +750,11 @@ suite("Roo Code read_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the read_file tool to read "${fileName}" and tell me how many lines it has.`,
|
||||
text: `Use the read_file tool to read the file "${fileName}" which has 100 lines. Each line follows the pattern "Line N: This is a test line with some content". Assume the file exists and you can read it directly. Tell me about the pattern you see.`,
|
||||
})
|
||||
|
||||
// Wait for task completion (longer timeout for large file)
|
||||
await waitFor(() => taskCompleted, { timeout: 120_000 })
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify the read_file tool was executed
|
||||
assert.ok(toolExecuted, "The read_file tool should have been executed")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
|||
import { waitFor, sleep } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite("Roo Code search_files Tool", function () {
|
||||
suite.skip("Roo Code search_files Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let workspaceDir: string
|
||||
|
|
@ -290,20 +290,37 @@ The search should find matches across different file types and provide context f
|
|||
})
|
||||
|
||||
test("Should search for function definitions in JavaScript files", async function () {
|
||||
this.timeout(90_000) // Increase timeout for this specific test
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let searchResults: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution and capture results
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("search_files")) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed:", text.substring(0, 200))
|
||||
|
||||
// Extract search results from the tool execution
|
||||
try {
|
||||
const jsonMatch = text.match(/\{"request":".*?"\}/)
|
||||
if (jsonMatch) {
|
||||
const requestData = JSON.parse(jsonMatch[0])
|
||||
if (requestData.request && requestData.request.includes("Result:")) {
|
||||
searchResults = requestData.request
|
||||
console.log("Captured search results:", searchResults?.substring(0, 300))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse search results:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -319,6 +336,7 @@ The search should find matches across different file types and provide context f
|
|||
let taskId: string
|
||||
try {
|
||||
// Start task to search for function definitions
|
||||
const jsFileName = path.basename(testFiles.jsFile)
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -326,27 +344,57 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with regex="function\\s+\\w+" to search for function declarations, then tell me what you found.`,
|
||||
text: `I have created test files in the workspace including a JavaScript file named "${jsFileName}" that contains function definitions like "calculateTotal" and "validateUser". Use the search_files tool with the regex pattern "function\\s+\\w+" to find all function declarations in JavaScript files. The files exist in the workspace directory.`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 90_000 })
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
|
||||
// Verify search results were captured and contain expected content
|
||||
assert.ok(searchResults, "Search results should have been captured from tool execution")
|
||||
|
||||
if (searchResults) {
|
||||
// Check that results contain function definitions
|
||||
const results = searchResults as string
|
||||
const hasCalculateTotal = results.includes("calculateTotal")
|
||||
const hasValidateUser = results.includes("validateUser")
|
||||
const hasFormatCurrency = results.includes("formatCurrency")
|
||||
const hasDebounce = results.includes("debounce")
|
||||
const hasFunctionKeyword = results.includes("function")
|
||||
const hasResults = results.includes("Found") && !results.includes("Found 0")
|
||||
const hasAnyExpectedFunction = hasCalculateTotal || hasValidateUser || hasFormatCurrency || hasDebounce
|
||||
|
||||
console.log("Search validation:")
|
||||
console.log("- Has calculateTotal:", hasCalculateTotal)
|
||||
console.log("- Has validateUser:", hasValidateUser)
|
||||
console.log("- Has formatCurrency:", hasFormatCurrency)
|
||||
console.log("- Has debounce:", hasDebounce)
|
||||
console.log("- Has function keyword:", hasFunctionKeyword)
|
||||
console.log("- Has results:", hasResults)
|
||||
console.log("- Has any expected function:", hasAnyExpectedFunction)
|
||||
|
||||
assert.ok(hasResults, "Search should return non-empty results")
|
||||
assert.ok(hasFunctionKeyword, "Search results should contain 'function' keyword")
|
||||
assert.ok(hasAnyExpectedFunction, "Search results should contain at least one expected function name")
|
||||
}
|
||||
|
||||
// Verify the AI found function definitions
|
||||
const hasContent = messages.some(
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("function") || m.text?.includes("found") || m.text?.includes("search")),
|
||||
(m.text?.includes("calculateTotal") ||
|
||||
m.text?.includes("validateUser") ||
|
||||
m.text?.includes("function")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned search results")
|
||||
assert.ok(completionMessage, "AI should have found function definitions")
|
||||
|
||||
console.log("Test passed! Function definitions search completed successfully")
|
||||
console.log("Test passed! Function definitions found successfully with validated results")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -364,10 +412,13 @@ The search should find matches across different file types and provide context f
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("search_files")) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed for TODO search")
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -390,7 +441,7 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with the regex pattern "TODO.*" to find all TODO items across all file types. Tell me what you find.`,
|
||||
text: `I have created test files in the workspace that contain TODO comments in JavaScript, TypeScript, and text files. Use the search_files tool with the regex pattern "TODO.*" to find all TODO items across all file types. The files exist in the workspace directory.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
|
|
@ -399,18 +450,18 @@ The search should find matches across different file types and provide context f
|
|||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned search results
|
||||
const hasContent = messages.some(
|
||||
// Verify the AI found TODO comments
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("TODO") ||
|
||||
m.text?.toLowerCase().includes("found") ||
|
||||
m.text?.toLowerCase().includes("search")),
|
||||
m.text?.toLowerCase().includes("results")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned search results")
|
||||
assert.ok(completionMessage, "AI should have found TODO comments")
|
||||
|
||||
console.log("Test passed! TODO comments search completed successfully")
|
||||
console.log("Test passed! TODO comments found successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -428,10 +479,13 @@ The search should find matches across different file types and provide context f
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution with file pattern
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("search_files") && text.includes("*.ts")) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed with TypeScript filter")
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -447,6 +501,7 @@ The search should find matches across different file types and provide context f
|
|||
let taskId: string
|
||||
try {
|
||||
// Start task to search for interfaces in TypeScript files only
|
||||
const tsFileName = path.basename(testFiles.tsFile)
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -454,27 +509,25 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with the regex pattern "interface\\s+\\w+" and file pattern "*.ts" to find interfaces only in TypeScript files. Tell me what you find.`,
|
||||
text: `I have created test files in the workspace including a TypeScript file named "${tsFileName}" that contains interface definitions like "User" and "Product". Use the search_files tool with the regex pattern "interface\\s+\\w+" and file pattern "*.ts" to find interfaces only in TypeScript files. The files exist in the workspace directory.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
// Verify the search_files tool was executed with file pattern
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed with *.ts pattern")
|
||||
|
||||
// Verify the AI mentioned search results
|
||||
const hasContent = messages.some(
|
||||
// Verify the AI found interface definitions
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("interface") ||
|
||||
m.text?.toLowerCase().includes("found") ||
|
||||
m.text?.toLowerCase().includes("search")),
|
||||
(m.text?.includes("User") || m.text?.includes("Product") || m.text?.includes("interface")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned search results")
|
||||
assert.ok(completionMessage, "AI should have found interface definitions in TypeScript files")
|
||||
|
||||
console.log("Test passed! TypeScript interface search completed successfully")
|
||||
console.log("Test passed! TypeScript interfaces found with file pattern filter")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -492,10 +545,13 @@ The search should find matches across different file types and provide context f
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution with JSON file pattern
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("search_files") && text.includes("*.json")) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed for JSON configuration search")
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -518,27 +574,28 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with the regex pattern '"\\w+":\\s*' and file pattern "*.json" to find all configuration keys in JSON files. Tell me what you find.`,
|
||||
text: `Search for configuration keys in JSON files. Use the search_files tool with the regex pattern '"\\w+":\\s*' and file pattern "*.json" to find all configuration keys in JSON files.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed with JSON filter")
|
||||
|
||||
// Verify the AI mentioned search results
|
||||
const hasContent = messages.some(
|
||||
// Verify the AI found configuration keys
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.toLowerCase().includes("found") ||
|
||||
m.text?.toLowerCase().includes("search") ||
|
||||
m.text?.toLowerCase().includes("key")),
|
||||
(m.text?.includes("name") ||
|
||||
m.text?.includes("version") ||
|
||||
m.text?.includes("scripts") ||
|
||||
m.text?.includes("dependencies")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned search results")
|
||||
assert.ok(completionMessage, "AI should have found configuration keys in JSON files")
|
||||
|
||||
console.log("Test passed! JSON configuration search completed successfully")
|
||||
console.log("Test passed! JSON configuration keys found successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -556,10 +613,13 @@ The search should find matches across different file types and provide context f
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("search_files")) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed for nested directory search")
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -582,7 +642,7 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with the regex pattern "function\\s+(format|debounce)" to find utility functions in the current directory and subdirectories. Tell me what you find.`,
|
||||
text: `Search for utility functions in the current directory and subdirectories. Use the search_files tool with the regex pattern "function\\s+(format|debounce)" to find utility functions like formatCurrency and debounce.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
|
|
@ -591,16 +651,14 @@ The search should find matches across different file types and provide context f
|
|||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned search results
|
||||
const hasContent = messages.some(
|
||||
// Verify the AI found utility functions in nested directories
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("function") ||
|
||||
m.text?.toLowerCase().includes("found") ||
|
||||
m.text?.toLowerCase().includes("search")),
|
||||
(m.text?.includes("formatCurrency") || m.text?.includes("debounce") || m.text?.includes("nested")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned search results")
|
||||
assert.ok(completionMessage, "AI should have found utility functions in nested directories")
|
||||
|
||||
console.log("Test passed! Nested directory search completed successfully")
|
||||
} finally {
|
||||
|
|
@ -620,10 +678,16 @@ The search should find matches across different file types and provide context f
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution with complex regex
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (
|
||||
text.includes("search_files") &&
|
||||
(text.includes("import|export") || text.includes("(import|export)"))
|
||||
) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed with complex regex pattern")
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -646,28 +710,25 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with the regex pattern "(import|export).*" and file pattern "*.{js,ts}" to find all import/export statements. Tell me what you find.`,
|
||||
text: `Search for import and export statements in JavaScript and TypeScript files. Use the search_files tool with the regex pattern "(import|export).*" and file pattern "*.{js,ts}" to find all import/export statements.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
|
||||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed with complex regex")
|
||||
|
||||
// Verify the AI mentioned search results
|
||||
const hasContent = messages.some(
|
||||
// Verify the AI found import/export statements
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("export") ||
|
||||
m.text?.includes("import") ||
|
||||
m.text?.toLowerCase().includes("found") ||
|
||||
m.text?.toLowerCase().includes("search")),
|
||||
(m.text?.includes("export") || m.text?.includes("import") || m.text?.includes("module")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned search results")
|
||||
assert.ok(completionMessage, "AI should have found import/export statements")
|
||||
|
||||
console.log("Test passed! Complex regex search completed successfully")
|
||||
console.log("Test passed! Complex regex pattern search completed successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -680,15 +741,38 @@ The search should find matches across different file types and provide context f
|
|||
const messages: ClineMessage[] = []
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let searchResults: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution and capture results
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("search_files")) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed for no-match search")
|
||||
|
||||
// Extract search results from the tool execution
|
||||
try {
|
||||
const jsonMatch = text.match(/\{"request":".*?"\}/)
|
||||
if (jsonMatch) {
|
||||
const requestData = JSON.parse(jsonMatch[0])
|
||||
if (requestData.request && requestData.request.includes("Result:")) {
|
||||
searchResults = requestData.request
|
||||
console.log("Captured no-match search results:", searchResults?.substring(0, 300))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse no-match search results:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log all completion messages for debugging
|
||||
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
|
||||
console.log("AI completion message:", message.text?.substring(0, 300))
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -711,7 +795,7 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with the regex pattern "nonExistentPattern12345" to search for something that won't be found. Tell me what you find.`,
|
||||
text: `Search for a pattern that doesn't exist in any files. Use the search_files tool with the regex pattern "nonExistentPattern12345" to search for something that won't be found.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
|
|
@ -720,15 +804,57 @@ The search should find matches across different file types and provide context f
|
|||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
|
||||
// Verify the AI provided a response
|
||||
const hasContent = messages.some(
|
||||
// Verify search results were captured and show no matches
|
||||
assert.ok(searchResults, "Search results should have been captured from tool execution")
|
||||
|
||||
if (searchResults) {
|
||||
// Check that results indicate no matches found
|
||||
const results = searchResults as string
|
||||
const hasZeroResults = results.includes("Found 0") || results.includes("0 results")
|
||||
const hasNoMatches =
|
||||
results.toLowerCase().includes("no matches") || results.toLowerCase().includes("no results")
|
||||
const indicatesEmpty = hasZeroResults || hasNoMatches
|
||||
|
||||
console.log("No-match search validation:")
|
||||
console.log("- Has zero results indicator:", hasZeroResults)
|
||||
console.log("- Has no matches indicator:", hasNoMatches)
|
||||
console.log("- Indicates empty results:", indicatesEmpty)
|
||||
console.log("- Search results preview:", results.substring(0, 200))
|
||||
|
||||
assert.ok(indicatesEmpty, "Search results should indicate no matches were found")
|
||||
}
|
||||
|
||||
// Verify the AI provided a completion response (the tool was executed successfully)
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
m.text &&
|
||||
m.text.length > 10,
|
||||
m.text.length > 10, // Any substantial response
|
||||
)
|
||||
assert.ok(hasContent, "AI should have provided a response")
|
||||
|
||||
// If we have a completion message, the test passes (AI handled the no-match scenario)
|
||||
if (completionMessage) {
|
||||
console.log("AI provided completion response for no-match scenario")
|
||||
} else {
|
||||
// Fallback: check for specific no-match indicators
|
||||
const noMatchMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.toLowerCase().includes("no matches") ||
|
||||
m.text?.toLowerCase().includes("not found") ||
|
||||
m.text?.toLowerCase().includes("no results") ||
|
||||
m.text?.toLowerCase().includes("didn't find") ||
|
||||
m.text?.toLowerCase().includes("0 results") ||
|
||||
m.text?.toLowerCase().includes("found 0") ||
|
||||
m.text?.toLowerCase().includes("empty") ||
|
||||
m.text?.toLowerCase().includes("nothing")),
|
||||
)
|
||||
assert.ok(noMatchMessage, "AI should have provided a response to the no-match search")
|
||||
}
|
||||
|
||||
assert.ok(completionMessage, "AI should have provided a completion response")
|
||||
|
||||
console.log("Test passed! No-match scenario handled correctly")
|
||||
} finally {
|
||||
|
|
@ -748,10 +874,13 @@ The search should find matches across different file types and provide context f
|
|||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
const text = message.text || ""
|
||||
if (text.includes("search_files") && (text.includes("class") || text.includes("async"))) {
|
||||
toolExecuted = true
|
||||
console.log("search_files tool executed for class/method search")
|
||||
}
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -774,7 +903,7 @@ The search should find matches across different file types and provide context f
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the search_files tool with the regex pattern "(class\\s+\\w+|async\\s+\\w+)" and file pattern "*.ts" to find classes and async methods. Tell me what you find.`,
|
||||
text: `Search for class definitions and async methods in TypeScript files. Use the search_files tool with the regex pattern "(class\\s+\\w+|async\\s+\\w+)" and file pattern "*.ts" to find classes and async methods.`,
|
||||
})
|
||||
|
||||
// Wait for task completion
|
||||
|
|
@ -783,19 +912,19 @@ The search should find matches across different file types and provide context f
|
|||
// Verify the search_files tool was executed
|
||||
assert.ok(toolExecuted, "The search_files tool should have been executed")
|
||||
|
||||
// Verify the AI mentioned search results
|
||||
const hasContent = messages.some(
|
||||
// Verify the AI found class definitions and async methods
|
||||
const completionMessage = messages.find(
|
||||
(m) =>
|
||||
m.type === "say" &&
|
||||
(m.say === "completion_result" || m.say === "text") &&
|
||||
(m.text?.includes("class") ||
|
||||
(m.text?.includes("UserService") ||
|
||||
m.text?.includes("class") ||
|
||||
m.text?.includes("async") ||
|
||||
m.text?.toLowerCase().includes("found") ||
|
||||
m.text?.toLowerCase().includes("search")),
|
||||
m.text?.includes("getUser")),
|
||||
)
|
||||
assert.ok(hasContent, "AI should have mentioned search results")
|
||||
assert.ok(completionMessage, "AI should have found class definitions and async methods")
|
||||
|
||||
console.log("Test passed! Class and method search completed successfully")
|
||||
console.log("Test passed! Class definitions and async methods found successfully")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,7 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
|||
import { waitFor, sleep } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite("Roo Code use_mcp_tool Tool", function () {
|
||||
// Uses the mcp-server-time MCP server via uvx
|
||||
// Provides time-related tools (get_current_time, convert_time) that don't overlap with built-in tools
|
||||
// Requires: uv installed (curl -LsSf https://astral.sh/uv/install.sh | sh)
|
||||
// Configuration is in global MCP settings, not workspace .roo/mcp.json
|
||||
suite.skip("Roo Code use_mcp_tool Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let tempDir: string
|
||||
|
|
@ -30,29 +26,21 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
// Create test files in VSCode workspace directory
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || tempDir
|
||||
|
||||
// Create test files for MCP filesystem operations
|
||||
testFiles = {
|
||||
simple: path.join(workspaceDir, `mcp-test-${Date.now()}.txt`),
|
||||
testData: path.join(workspaceDir, `mcp-data-${Date.now()}.json`),
|
||||
mcpConfig: path.join(workspaceDir, ".roo", "mcp.json"),
|
||||
}
|
||||
|
||||
// Copy MCP configuration from user's global settings to test environment
|
||||
// The test environment uses .vscode-test/user-data instead of ~/.config/Code
|
||||
const testUserDataDir = path.join(
|
||||
process.cwd(),
|
||||
".vscode-test",
|
||||
"user-data",
|
||||
"User",
|
||||
"globalStorage",
|
||||
"rooveterinaryinc.roo-cline",
|
||||
"settings",
|
||||
)
|
||||
const testMcpSettingsPath = path.join(testUserDataDir, "mcp_settings.json")
|
||||
// Create initial test files
|
||||
await fs.writeFile(testFiles.simple, "Initial content for MCP test")
|
||||
await fs.writeFile(testFiles.testData, JSON.stringify({ test: "data", value: 42 }, null, 2))
|
||||
|
||||
// Create the directory structure
|
||||
await fs.mkdir(testUserDataDir, { recursive: true })
|
||||
// Create .roo directory and MCP configuration file
|
||||
const rooDir = path.join(workspaceDir, ".roo")
|
||||
await fs.mkdir(rooDir, { recursive: true })
|
||||
|
||||
// Configure the time MCP server for tests
|
||||
const mcpConfig = {
|
||||
mcpServers: {
|
||||
time: {
|
||||
|
|
@ -62,11 +50,10 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
},
|
||||
},
|
||||
}
|
||||
await fs.writeFile(testFiles.mcpConfig, JSON.stringify(mcpConfig, null, 2))
|
||||
|
||||
await fs.writeFile(testMcpSettingsPath, JSON.stringify(mcpConfig, null, 2))
|
||||
|
||||
console.log("MCP test workspace:", workspaceDir)
|
||||
console.log("MCP settings configured at:", testMcpSettingsPath)
|
||||
console.log("MCP test files created in:", workspaceDir)
|
||||
console.log("Test files:", testFiles)
|
||||
})
|
||||
|
||||
// Clean up temporary directory and files after tests
|
||||
|
|
@ -125,8 +112,7 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
await sleep(100)
|
||||
})
|
||||
|
||||
test("Should request MCP time get_current_time tool and complete successfully", async function () {
|
||||
this.timeout(90_000) // MCP server initialization can take time
|
||||
test("Should request MCP filesystem read_file tool and complete successfully", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let taskStarted = false
|
||||
|
|
@ -199,29 +185,44 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
await sleep(2000) // Wait for Roo Code to fully initialize
|
||||
|
||||
// Trigger MCP server refresh by executing the refresh command
|
||||
// This simulates clicking the "Refresh MCP Servers" button in the UI
|
||||
console.log("Triggering MCP server refresh...")
|
||||
// Trigger MCP server detection by opening and modifying the file
|
||||
console.log("Triggering MCP server detection by modifying the config file...")
|
||||
try {
|
||||
// The webview needs to send a refreshAllMcpServers message
|
||||
// We can't directly call this from the E2E API, so we'll use a workaround:
|
||||
// Execute a VSCode command that might trigger MCP initialization
|
||||
await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
|
||||
await sleep(2000)
|
||||
const mcpConfigUri = vscode.Uri.file(testFiles.mcpConfig)
|
||||
const document = await vscode.workspace.openTextDocument(mcpConfigUri)
|
||||
const editor = await vscode.window.showTextDocument(document)
|
||||
|
||||
// Try to trigger MCP refresh through the extension's internal API
|
||||
// Since we can't directly access the webview message handler, we'll rely on
|
||||
// the MCP servers being initialized when the extension activates
|
||||
console.log("Waiting for MCP servers to initialize...")
|
||||
await sleep(10000) // Give MCP servers time to initialize
|
||||
// Make a small modification to trigger the save event, without this Roo Code won't load the MCP server
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const currentContent = document.getText()
|
||||
const modifiedContent = currentContent.replace(
|
||||
'"alwaysAllow": []',
|
||||
'"alwaysAllow": ["read_file", "read_multiple_files", "write_file", "edit_file", "create_directory", "list_directory", "directory_tree", "move_file", "search_files", "get_file_info", "list_allowed_directories"]',
|
||||
)
|
||||
|
||||
const fullRange = new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length))
|
||||
|
||||
edit.replace(mcpConfigUri, fullRange, modifiedContent)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Save the document to trigger MCP server detection
|
||||
await editor.document.save()
|
||||
|
||||
// Close the editor
|
||||
await vscode.commands.executeCommand("workbench.action.closeActiveEditor")
|
||||
|
||||
console.log("MCP config file modified and saved successfully")
|
||||
} catch (error) {
|
||||
console.error("Failed to trigger MCP refresh:", error)
|
||||
console.error("Failed to modify/save MCP config file:", error)
|
||||
}
|
||||
|
||||
await sleep(5000) // Wait for MCP servers to initialize
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task requesting to use MCP time server's get_current_time tool
|
||||
// Start task requesting to use MCP filesystem read_file tool
|
||||
const fileName = path.basename(testFiles.simple)
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -229,11 +230,11 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
alwaysAllowMcp: true, // Enable MCP auto-approval
|
||||
mcpEnabled: true,
|
||||
},
|
||||
text: `Use the MCP time server's get_current_time tool to get the current time in America/New_York timezone and tell me what time it is there.`,
|
||||
text: `Use the MCP filesystem server's read_file tool to read the file "${fileName}". The file exists in the workspace and contains "Initial content for MCP test".`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Requesting MCP time get_current_time for America/New_York")
|
||||
console.log("Requesting MCP filesystem read_file for:", fileName)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
|
@ -245,32 +246,33 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
assert.ok(mcpToolRequested, "The use_mcp_tool should have been requested")
|
||||
|
||||
// Verify the correct tool was used
|
||||
assert.strictEqual(mcpToolName, "get_current_time", "Should have used the get_current_time tool")
|
||||
assert.strictEqual(mcpToolName, "read_file", "Should have used the read_file tool")
|
||||
|
||||
// Verify we got a response from the MCP server
|
||||
assert.ok(mcpServerResponse, "Should have received a response from the MCP server")
|
||||
|
||||
// Verify the response contains time data (not an error)
|
||||
// Verify the response contains expected file content (not an error)
|
||||
const responseText = mcpServerResponse as string
|
||||
|
||||
// Check for time-related content
|
||||
const hasTimeContent =
|
||||
responseText.includes("time") ||
|
||||
responseText.includes("datetime") ||
|
||||
responseText.includes("2026") || // Current year
|
||||
responseText.includes(":") || // Time format HH:MM
|
||||
responseText.includes("America/New_York") ||
|
||||
responseText.length > 10 // At least some content
|
||||
|
||||
// Check for specific file content keywords
|
||||
assert.ok(
|
||||
hasTimeContent,
|
||||
`MCP server response should contain time data. Got: ${responseText.substring(0, 200)}...`,
|
||||
responseText.includes("Initial content for MCP test"),
|
||||
`MCP server response should contain the exact file content. Got: ${responseText.substring(0, 100)}...`,
|
||||
)
|
||||
|
||||
// Verify it contains the specific words from our test file
|
||||
assert.ok(
|
||||
responseText.includes("Initial") &&
|
||||
responseText.includes("content") &&
|
||||
responseText.includes("MCP") &&
|
||||
responseText.includes("test"),
|
||||
`MCP server response should contain all expected keywords: Initial, content, MCP, test. Got: ${responseText.substring(0, 100)}...`,
|
||||
)
|
||||
|
||||
// Ensure no errors are present
|
||||
assert.ok(
|
||||
!responseText.toLowerCase().includes("error") && !responseText.toLowerCase().includes("failed"),
|
||||
`MCP server response should not contain error messages. Got: ${responseText.substring(0, 200)}...`,
|
||||
`MCP server response should not contain error messages. Got: ${responseText.substring(0, 100)}...`,
|
||||
)
|
||||
|
||||
// Verify task completed successfully
|
||||
|
|
@ -279,7 +281,7 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
// Check that no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, "No errors should have occurred")
|
||||
|
||||
console.log("Test passed! MCP get_current_time tool used successfully and task completed")
|
||||
console.log("Test passed! MCP read_file tool used successfully and task completed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
@ -288,8 +290,7 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
}
|
||||
})
|
||||
|
||||
test("Should request MCP time convert_time tool and complete successfully", async function () {
|
||||
this.timeout(90_000) // MCP server initialization can take time
|
||||
test("Should request MCP filesystem write_file tool and complete successfully", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let _taskCompleted = false
|
||||
|
|
@ -355,7 +356,8 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task requesting to use MCP time server's convert_time tool
|
||||
// Start task requesting to use MCP filesystem write_file tool
|
||||
const newFileName = `mcp-write-test-${Date.now()}.txt`
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
|
|
@ -363,41 +365,43 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
alwaysAllowMcp: true,
|
||||
mcpEnabled: true,
|
||||
},
|
||||
text: `Use the MCP time server's convert_time tool to convert 14:00 from America/New_York timezone to Asia/Tokyo timezone and tell me what time it would be.`,
|
||||
text: `Use the MCP filesystem server's write_file tool to create a new file called "${newFileName}" with the content "Hello from MCP!".`,
|
||||
})
|
||||
|
||||
// Wait for attempt_completion to be called (indicating task finished)
|
||||
await waitFor(() => attemptCompletionCalled, { timeout: 60_000 })
|
||||
await waitFor(() => attemptCompletionCalled, { timeout: 45_000 })
|
||||
|
||||
// Verify the MCP tool was requested
|
||||
assert.ok(mcpToolRequested, "The use_mcp_tool should have been requested")
|
||||
assert.ok(mcpToolRequested, "The use_mcp_tool should have been requested for writing")
|
||||
|
||||
// Verify the correct tool was used
|
||||
assert.strictEqual(mcpToolName, "convert_time", "Should have used the convert_time tool")
|
||||
assert.strictEqual(mcpToolName, "write_file", "Should have used the write_file tool")
|
||||
|
||||
// Verify we got a response from the MCP server
|
||||
assert.ok(mcpServerResponse, "Should have received a response from the MCP server")
|
||||
|
||||
// Verify the response contains time conversion data (not an error)
|
||||
// Verify the response indicates successful file creation (not an error)
|
||||
const responseText = mcpServerResponse as string
|
||||
|
||||
// Check for time conversion content
|
||||
const hasConversionContent =
|
||||
responseText.includes("time") ||
|
||||
responseText.includes(":") || // Time format
|
||||
responseText.includes("Tokyo") ||
|
||||
responseText.includes("Asia/Tokyo") ||
|
||||
responseText.length > 10 // At least some content
|
||||
// Check for specific success indicators
|
||||
const hasSuccessKeyword =
|
||||
responseText.toLowerCase().includes("success") ||
|
||||
responseText.toLowerCase().includes("created") ||
|
||||
responseText.toLowerCase().includes("written") ||
|
||||
responseText.toLowerCase().includes("file written") ||
|
||||
responseText.toLowerCase().includes("successfully")
|
||||
|
||||
const hasFileName = responseText.includes(newFileName) || responseText.includes("mcp-write-test")
|
||||
|
||||
assert.ok(
|
||||
hasConversionContent,
|
||||
`MCP server response should contain time conversion data. Got: ${responseText.substring(0, 200)}...`,
|
||||
hasSuccessKeyword || hasFileName,
|
||||
`MCP server response should indicate successful file creation with keywords like 'success', 'created', 'written' or contain the filename '${newFileName}'. Got: ${responseText.substring(0, 150)}...`,
|
||||
)
|
||||
|
||||
// Ensure no errors are present
|
||||
assert.ok(
|
||||
!responseText.toLowerCase().includes("error") && !responseText.toLowerCase().includes("failed"),
|
||||
`MCP server response should not contain error messages. Got: ${responseText.substring(0, 200)}...`,
|
||||
`MCP server response should not contain error messages. Got: ${responseText.substring(0, 100)}...`,
|
||||
)
|
||||
|
||||
// Verify task completed successfully
|
||||
|
|
@ -406,7 +410,515 @@ suite("Roo Code use_mcp_tool Tool", function () {
|
|||
// Check that no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, "No errors should have occurred")
|
||||
|
||||
console.log("Test passed! MCP convert_time tool used successfully and task completed")
|
||||
console.log("Test passed! MCP write_file tool used successfully and task completed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test("Should request MCP filesystem list_directory tool and complete successfully", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let _taskCompleted = false
|
||||
let mcpToolRequested = false
|
||||
let mcpToolName: string | null = null
|
||||
let mcpServerResponse: string | null = null
|
||||
let attemptCompletionCalled = false
|
||||
let errorOccurred: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for MCP tool request
|
||||
if (message.type === "ask" && message.ask === "use_mcp_server") {
|
||||
mcpToolRequested = true
|
||||
console.log("MCP tool request:", message.text?.substring(0, 300))
|
||||
|
||||
// Parse the MCP request to verify structure and tool name
|
||||
if (message.text) {
|
||||
try {
|
||||
const mcpRequest = JSON.parse(message.text)
|
||||
mcpToolName = mcpRequest.toolName
|
||||
console.log("MCP request parsed:", {
|
||||
type: mcpRequest.type,
|
||||
serverName: mcpRequest.serverName,
|
||||
toolName: mcpRequest.toolName,
|
||||
hasArguments: !!mcpRequest.arguments,
|
||||
})
|
||||
} catch (e) {
|
||||
console.log("Failed to parse MCP request:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for MCP server response
|
||||
if (message.type === "say" && message.say === "mcp_server_response") {
|
||||
mcpServerResponse = message.text || null
|
||||
console.log("MCP server response received:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for attempt_completion
|
||||
if (message.type === "say" && message.say === "completion_result") {
|
||||
attemptCompletionCalled = true
|
||||
console.log("Attempt completion called:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task requesting MCP filesystem list_directory tool
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowMcp: true,
|
||||
mcpEnabled: true,
|
||||
},
|
||||
text: `Use the MCP filesystem server's list_directory tool to list the contents of the current directory. I want to see the files in the workspace.`,
|
||||
})
|
||||
|
||||
// Wait for attempt_completion to be called (indicating task finished)
|
||||
await waitFor(() => attemptCompletionCalled, { timeout: 45_000 })
|
||||
|
||||
// Verify the MCP tool was requested
|
||||
assert.ok(mcpToolRequested, "The use_mcp_tool should have been requested")
|
||||
|
||||
// Verify the correct tool was used
|
||||
assert.strictEqual(mcpToolName, "list_directory", "Should have used the list_directory tool")
|
||||
|
||||
// Verify we got a response from the MCP server
|
||||
assert.ok(mcpServerResponse, "Should have received a response from the MCP server")
|
||||
|
||||
// Verify the response contains directory listing (not an error)
|
||||
const responseText = mcpServerResponse as string
|
||||
|
||||
// Check for specific directory contents - our test files should be listed
|
||||
const hasTestFile =
|
||||
responseText.includes("mcp-test-") || responseText.includes(path.basename(testFiles.simple))
|
||||
const hasDataFile =
|
||||
responseText.includes("mcp-data-") || responseText.includes(path.basename(testFiles.testData))
|
||||
const hasRooDir = responseText.includes(".roo")
|
||||
|
||||
// At least one of our test files or the .roo directory should be present
|
||||
assert.ok(
|
||||
hasTestFile || hasDataFile || hasRooDir,
|
||||
`MCP server response should contain our test files or .roo directory. Expected to find: '${path.basename(testFiles.simple)}', '${path.basename(testFiles.testData)}', or '.roo'. Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
// Check for typical directory listing indicators
|
||||
const hasDirectoryStructure =
|
||||
responseText.includes("name") ||
|
||||
responseText.includes("type") ||
|
||||
responseText.includes("file") ||
|
||||
responseText.includes("directory") ||
|
||||
responseText.includes(".txt") ||
|
||||
responseText.includes(".json")
|
||||
|
||||
assert.ok(
|
||||
hasDirectoryStructure,
|
||||
`MCP server response should contain directory structure indicators like 'name', 'type', 'file', 'directory', or file extensions. Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
// Ensure no errors are present
|
||||
assert.ok(
|
||||
!responseText.toLowerCase().includes("error") && !responseText.toLowerCase().includes("failed"),
|
||||
`MCP server response should not contain error messages. Got: ${responseText.substring(0, 100)}...`,
|
||||
)
|
||||
|
||||
// Verify task completed successfully
|
||||
assert.ok(attemptCompletionCalled, "Task should have completed with attempt_completion")
|
||||
|
||||
// Check that no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, "No errors should have occurred")
|
||||
|
||||
console.log("Test passed! MCP list_directory tool used successfully and task completed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test.skip("Should request MCP filesystem directory_tree tool and complete successfully", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let _taskCompleted = false
|
||||
let mcpToolRequested = false
|
||||
let mcpToolName: string | null = null
|
||||
let mcpServerResponse: string | null = null
|
||||
let attemptCompletionCalled = false
|
||||
let errorOccurred: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for MCP tool request
|
||||
if (message.type === "ask" && message.ask === "use_mcp_server") {
|
||||
mcpToolRequested = true
|
||||
console.log("MCP tool request:", message.text?.substring(0, 200))
|
||||
|
||||
// Parse the MCP request to verify structure and tool name
|
||||
if (message.text) {
|
||||
try {
|
||||
const mcpRequest = JSON.parse(message.text)
|
||||
mcpToolName = mcpRequest.toolName
|
||||
console.log("MCP request parsed:", {
|
||||
type: mcpRequest.type,
|
||||
serverName: mcpRequest.serverName,
|
||||
toolName: mcpRequest.toolName,
|
||||
hasArguments: !!mcpRequest.arguments,
|
||||
})
|
||||
} catch (e) {
|
||||
console.log("Failed to parse MCP request:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for MCP server response
|
||||
if (message.type === "say" && message.say === "mcp_server_response") {
|
||||
mcpServerResponse = message.text || null
|
||||
console.log("MCP server response received:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for attempt_completion
|
||||
if (message.type === "say" && message.say === "completion_result") {
|
||||
attemptCompletionCalled = true
|
||||
console.log("Attempt completion called:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task requesting MCP filesystem directory_tree tool
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowMcp: true,
|
||||
mcpEnabled: true,
|
||||
},
|
||||
text: `Use the MCP filesystem server's directory_tree tool to show me the directory structure of the current workspace. I want to see the folder hierarchy.`,
|
||||
})
|
||||
|
||||
// Wait for attempt_completion to be called (indicating task finished)
|
||||
await waitFor(() => attemptCompletionCalled, { timeout: 45_000 })
|
||||
|
||||
// Verify the MCP tool was requested
|
||||
assert.ok(mcpToolRequested, "The use_mcp_tool should have been requested")
|
||||
|
||||
// Verify the correct tool was used
|
||||
assert.strictEqual(mcpToolName, "directory_tree", "Should have used the directory_tree tool")
|
||||
|
||||
// Verify we got a response from the MCP server
|
||||
assert.ok(mcpServerResponse, "Should have received a response from the MCP server")
|
||||
|
||||
// Verify the response contains directory tree structure (not an error)
|
||||
const responseText = mcpServerResponse as string
|
||||
|
||||
// Check for tree structure elements (be flexible as different MCP servers format differently)
|
||||
const hasTreeStructure =
|
||||
responseText.includes("name") ||
|
||||
responseText.includes("type") ||
|
||||
responseText.includes("children") ||
|
||||
responseText.includes("file") ||
|
||||
responseText.includes("directory")
|
||||
|
||||
// Check for our test files or common file extensions
|
||||
const hasTestFiles =
|
||||
responseText.includes("mcp-test-") ||
|
||||
responseText.includes("mcp-data-") ||
|
||||
responseText.includes(".roo") ||
|
||||
responseText.includes(".txt") ||
|
||||
responseText.includes(".json") ||
|
||||
responseText.length > 10 // At least some content indicating directory structure
|
||||
|
||||
assert.ok(
|
||||
hasTreeStructure,
|
||||
`MCP server response should contain tree structure indicators like 'name', 'type', 'children', 'file', or 'directory'. Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
hasTestFiles,
|
||||
`MCP server response should contain directory contents (test files, extensions, or substantial content). Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
// Ensure no errors are present
|
||||
assert.ok(
|
||||
!responseText.toLowerCase().includes("error") && !responseText.toLowerCase().includes("failed"),
|
||||
`MCP server response should not contain error messages. Got: ${responseText.substring(0, 100)}...`,
|
||||
)
|
||||
|
||||
// Verify task completed successfully
|
||||
assert.ok(attemptCompletionCalled, "Task should have completed with attempt_completion")
|
||||
|
||||
// Check that no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, "No errors should have occurred")
|
||||
|
||||
console.log("Test passed! MCP directory_tree tool used successfully and task completed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test.skip("Should handle MCP server error gracefully and complete task", async function () {
|
||||
// Skipped: This test requires interactive approval for non-whitelisted MCP servers
|
||||
// which cannot be automated in the test environment
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let _taskCompleted = false
|
||||
let _mcpToolRequested = false
|
||||
let _errorHandled = false
|
||||
let attemptCompletionCalled = false
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for MCP tool request
|
||||
if (message.type === "ask" && message.ask === "use_mcp_server") {
|
||||
_mcpToolRequested = true
|
||||
console.log("MCP tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for error handling
|
||||
if (message.type === "say" && (message.say === "error" || message.say === "mcp_server_response")) {
|
||||
if (message.text && (message.text.includes("Error") || message.text.includes("not found"))) {
|
||||
_errorHandled = true
|
||||
console.log("MCP error handled:", message.text.substring(0, 100))
|
||||
}
|
||||
}
|
||||
|
||||
// Check for attempt_completion
|
||||
if (message.type === "say" && message.say === "completion_result") {
|
||||
attemptCompletionCalled = true
|
||||
console.log("Attempt completion called:", message.text?.substring(0, 200))
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task requesting non-existent MCP server
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowMcp: true,
|
||||
mcpEnabled: true,
|
||||
},
|
||||
text: `Use the MCP server "nonexistent-server" to perform some operation. This should trigger an error but the task should still complete gracefully.`,
|
||||
})
|
||||
|
||||
// Wait for attempt_completion to be called (indicating task finished)
|
||||
await waitFor(() => attemptCompletionCalled, { timeout: 45_000 })
|
||||
|
||||
// Verify task completed successfully even with error
|
||||
assert.ok(attemptCompletionCalled, "Task should have completed with attempt_completion even with MCP error")
|
||||
|
||||
console.log("Test passed! MCP error handling verified and task completed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test.skip("Should validate MCP request message format and complete successfully", async function () {
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
let _taskCompleted = false
|
||||
let mcpToolRequested = false
|
||||
let validMessageFormat = false
|
||||
let mcpToolName: string | null = null
|
||||
let mcpServerResponse: string | null = null
|
||||
let attemptCompletionCalled = false
|
||||
let errorOccurred: string | null = null
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for MCP tool request and validate format
|
||||
if (message.type === "ask" && message.ask === "use_mcp_server") {
|
||||
mcpToolRequested = true
|
||||
console.log("MCP tool request:", message.text?.substring(0, 200))
|
||||
|
||||
// Validate the message format matches ClineAskUseMcpServer interface
|
||||
if (message.text) {
|
||||
try {
|
||||
const mcpRequest = JSON.parse(message.text)
|
||||
mcpToolName = mcpRequest.toolName
|
||||
|
||||
// Check required fields
|
||||
const hasType = typeof mcpRequest.type === "string"
|
||||
const hasServerName = typeof mcpRequest.serverName === "string"
|
||||
const validType =
|
||||
mcpRequest.type === "use_mcp_tool" || mcpRequest.type === "access_mcp_resource"
|
||||
|
||||
if (hasType && hasServerName && validType) {
|
||||
validMessageFormat = true
|
||||
console.log("Valid MCP message format detected:", {
|
||||
type: mcpRequest.type,
|
||||
serverName: mcpRequest.serverName,
|
||||
toolName: mcpRequest.toolName,
|
||||
hasArguments: !!mcpRequest.arguments,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to parse MCP request:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for MCP server response
|
||||
if (message.type === "say" && message.say === "mcp_server_response") {
|
||||
mcpServerResponse = message.text || null
|
||||
console.log("MCP server response received:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Check for attempt_completion
|
||||
if (message.type === "say" && message.say === "completion_result") {
|
||||
attemptCompletionCalled = true
|
||||
console.log("Attempt completion called:", message.text?.substring(0, 200))
|
||||
}
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
_taskCompleted = true
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task requesting MCP filesystem get_file_info tool
|
||||
const fileName = path.basename(testFiles.simple)
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowMcp: true,
|
||||
mcpEnabled: true,
|
||||
},
|
||||
text: `Use the MCP filesystem server's get_file_info tool to get information about the file "${fileName}". This file exists in the workspace and will validate proper message formatting.`,
|
||||
})
|
||||
|
||||
// Wait for attempt_completion to be called (indicating task finished)
|
||||
await waitFor(() => attemptCompletionCalled, { timeout: 45_000 })
|
||||
|
||||
// Verify the MCP tool was requested with valid format
|
||||
assert.ok(mcpToolRequested, "The use_mcp_tool should have been requested")
|
||||
assert.ok(validMessageFormat, "The MCP request should have valid message format")
|
||||
|
||||
// Verify the correct tool was used
|
||||
assert.strictEqual(mcpToolName, "get_file_info", "Should have used the get_file_info tool")
|
||||
|
||||
// Verify we got a response from the MCP server
|
||||
assert.ok(mcpServerResponse, "Should have received a response from the MCP server")
|
||||
|
||||
// Verify the response contains file information (not an error)
|
||||
const responseText = mcpServerResponse as string
|
||||
|
||||
// Check for specific file metadata fields
|
||||
const hasSize = responseText.includes("size") && (responseText.includes("28") || /\d+/.test(responseText))
|
||||
const hasTimestamps =
|
||||
responseText.includes("created") ||
|
||||
responseText.includes("modified") ||
|
||||
responseText.includes("accessed")
|
||||
const hasDateInfo =
|
||||
responseText.includes("2025") || responseText.includes("GMT") || /\d{4}-\d{2}-\d{2}/.test(responseText)
|
||||
|
||||
assert.ok(
|
||||
hasSize,
|
||||
`MCP server response should contain file size information. Expected 'size' with a number (like 28 bytes for our test file). Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
hasTimestamps,
|
||||
`MCP server response should contain timestamp information like 'created', 'modified', or 'accessed'. Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
hasDateInfo,
|
||||
`MCP server response should contain date/time information (year, GMT timezone, or ISO date format). Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
// Note: get_file_info typically returns metadata only, not the filename itself
|
||||
// So we'll focus on validating the metadata structure instead of filename reference
|
||||
const hasValidMetadata =
|
||||
(hasSize && hasTimestamps) || (hasSize && hasDateInfo) || (hasTimestamps && hasDateInfo)
|
||||
|
||||
assert.ok(
|
||||
hasValidMetadata,
|
||||
`MCP server response should contain valid file metadata (combination of size, timestamps, and date info). Got: ${responseText.substring(0, 200)}...`,
|
||||
)
|
||||
|
||||
// Ensure no errors are present
|
||||
assert.ok(
|
||||
!responseText.toLowerCase().includes("error") && !responseText.toLowerCase().includes("failed"),
|
||||
`MCP server response should not contain error messages. Got: ${responseText.substring(0, 100)}...`,
|
||||
)
|
||||
|
||||
// Verify task completed successfully
|
||||
assert.ok(attemptCompletionCalled, "Task should have completed with attempt_completion")
|
||||
|
||||
// Check that no errors occurred
|
||||
assert.strictEqual(errorOccurred, null, "No errors should have occurred")
|
||||
|
||||
console.log("Test passed! MCP message format validation successful and task completed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
|
|||
import { waitFor, sleep } from "../utils"
|
||||
import { setDefaultSuiteTimeout } from "../test-utils"
|
||||
|
||||
suite("Roo Code write_to_file Tool", function () {
|
||||
suite.skip("Roo Code write_to_file Tool", function () {
|
||||
setDefaultSuiteTimeout(this)
|
||||
|
||||
let tempDir: string
|
||||
|
|
@ -67,35 +67,71 @@ suite("Roo Code write_to_file Tool", function () {
|
|||
})
|
||||
|
||||
test("Should create a new file with content", async function () {
|
||||
// Increase timeout for this specific test
|
||||
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const fileContent = "Hello, this is a test file!"
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let errorOccurred: string | null = null
|
||||
let writeToFileToolExecuted = false
|
||||
let toolExecutionDetails = ""
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
console.log("Tool execution:", message.text?.substring(0, 200))
|
||||
if (message.text && message.text.includes("write_to_file")) {
|
||||
writeToFileToolExecuted = true
|
||||
toolExecutionDetails = message.text
|
||||
// Try to parse the tool execution details
|
||||
try {
|
||||
const parsed = JSON.parse(message.text)
|
||||
console.log("write_to_file tool called with request:", parsed.request?.substring(0, 300))
|
||||
} catch (_e) {
|
||||
console.log("Could not parse tool execution details")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log important messages for debugging
|
||||
if (message.type === "say" && message.say === "error") {
|
||||
errorOccurred = message.text || "Unknown error"
|
||||
console.error("Error:", message.text)
|
||||
}
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) {
|
||||
console.log("AI response:", message.text?.substring(0, 200))
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
||||
let taskId: string
|
||||
try {
|
||||
// Start task with a simple prompt
|
||||
// Start task with a very simple prompt
|
||||
const baseFileName = path.basename(testFilePath)
|
||||
taskId = await api.startNewTask({
|
||||
configuration: {
|
||||
|
|
@ -105,77 +141,182 @@ suite("Roo Code write_to_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the write_to_file tool to create a file named "${baseFileName}" with the following content:\n${fileContent}`,
|
||||
text: `Create a file named "${baseFileName}" with the following content:\n${fileContent}`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Base filename:", baseFileName)
|
||||
console.log("Expecting file at:", testFilePath)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Check for early errors
|
||||
if (errorOccurred) {
|
||||
console.error("Early error detected:", errorOccurred)
|
||||
}
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
await waitFor(() => taskCompleted, { timeout: 45_000 })
|
||||
|
||||
// Verify the write_to_file tool was executed
|
||||
assert.ok(toolExecuted, "The write_to_file tool should have been executed")
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Give time for file system operations
|
||||
await sleep(1000)
|
||||
// The file might be created in different locations, let's check them all
|
||||
const possibleLocations = [
|
||||
testFilePath, // Expected location
|
||||
path.join(tempDir, baseFileName), // In temp directory
|
||||
path.join(process.cwd(), baseFileName), // In current working directory
|
||||
path.join("/tmp/roo-test-workspace-" + "*", baseFileName), // In workspace created by runTest.ts
|
||||
]
|
||||
|
||||
// Check workspace directory for the file
|
||||
let fileFound = false
|
||||
let actualFilePath = ""
|
||||
let actualContent = ""
|
||||
|
||||
// First check the workspace directory that was created
|
||||
const workspaceDirs = await fs
|
||||
.readdir("/tmp")
|
||||
.then((files) => files.filter((f) => f.startsWith("roo-test-workspace-")))
|
||||
.catch(() => [])
|
||||
|
||||
let fileFound = false
|
||||
let actualContent = ""
|
||||
|
||||
for (const wsDir of workspaceDirs) {
|
||||
const wsFilePath = path.join("/tmp", wsDir, baseFileName)
|
||||
try {
|
||||
await fs.access(wsFilePath)
|
||||
actualContent = await fs.readFile(wsFilePath, "utf-8")
|
||||
fileFound = true
|
||||
console.log("File found in workspace:", wsFilePath)
|
||||
actualFilePath = wsFilePath
|
||||
actualContent = await fs.readFile(wsFilePath, "utf-8")
|
||||
console.log("File found in workspace directory:", wsFilePath)
|
||||
break
|
||||
} catch {
|
||||
// Continue checking
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(fileFound, `File should have been created: ${baseFileName}`)
|
||||
assert.strictEqual(actualContent.trim(), fileContent, "File content should match")
|
||||
// If not found in workspace, check other locations
|
||||
if (!fileFound) {
|
||||
for (const location of possibleLocations) {
|
||||
try {
|
||||
await fs.access(location)
|
||||
fileFound = true
|
||||
actualFilePath = location
|
||||
actualContent = await fs.readFile(location, "utf-8")
|
||||
console.log("File found at:", location)
|
||||
break
|
||||
} catch {
|
||||
// Continue checking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Test passed! File created successfully")
|
||||
// If still not found, list directories to help debug
|
||||
if (!fileFound) {
|
||||
console.log("File not found in expected locations. Debugging info:")
|
||||
|
||||
// List temp directory
|
||||
try {
|
||||
const tempFiles = await fs.readdir(tempDir)
|
||||
console.log("Files in temp directory:", tempFiles)
|
||||
} catch (e) {
|
||||
console.log("Could not list temp directory:", e)
|
||||
}
|
||||
|
||||
// List current working directory
|
||||
try {
|
||||
const cwdFiles = await fs.readdir(process.cwd())
|
||||
console.log(
|
||||
"Files in CWD:",
|
||||
cwdFiles.filter((f) => f.includes("test-file")),
|
||||
)
|
||||
} catch (e) {
|
||||
console.log("Could not list CWD:", e)
|
||||
}
|
||||
|
||||
// List /tmp for test files
|
||||
try {
|
||||
const tmpFiles = await fs.readdir("/tmp")
|
||||
console.log(
|
||||
"Test files in /tmp:",
|
||||
tmpFiles.filter((f) => f.includes("test-file") || f.includes("roo-test")),
|
||||
)
|
||||
} catch (e) {
|
||||
console.log("Could not list /tmp:", e)
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(fileFound, `File should have been created. Expected filename: ${baseFileName}`)
|
||||
assert.strictEqual(actualContent.trim(), fileContent, "File content should match expected content")
|
||||
|
||||
// Verify that write_to_file tool was actually executed
|
||||
assert.ok(writeToFileToolExecuted, "write_to_file tool should have been executed")
|
||||
assert.ok(
|
||||
toolExecutionDetails.includes(baseFileName) || toolExecutionDetails.includes(fileContent),
|
||||
"Tool execution should include the filename or content",
|
||||
)
|
||||
|
||||
console.log("Test passed! File created successfully at:", actualFilePath)
|
||||
console.log("write_to_file tool was properly executed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
||||
test("Should create nested directories when writing file", async function () {
|
||||
// Increase timeout for this specific test
|
||||
|
||||
const api = globalThis.api
|
||||
const messages: ClineMessage[] = []
|
||||
const content = "File in nested directory"
|
||||
const fileName = `file-${Date.now()}.txt`
|
||||
const nestedPath = path.join(tempDir, "nested", "deep", "directory", fileName)
|
||||
let taskStarted = false
|
||||
let taskCompleted = false
|
||||
let toolExecuted = false
|
||||
let writeToFileToolExecuted = false
|
||||
let toolExecutionDetails = ""
|
||||
|
||||
// Listen for messages
|
||||
const messageHandler = ({ message }: { message: ClineMessage }) => {
|
||||
messages.push(message)
|
||||
|
||||
// Check for tool request
|
||||
// Check for tool execution
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
console.log("Tool execution:", message.text?.substring(0, 200))
|
||||
if (message.text && message.text.includes("write_to_file")) {
|
||||
writeToFileToolExecuted = true
|
||||
toolExecutionDetails = message.text
|
||||
// Try to parse the tool execution details
|
||||
try {
|
||||
const parsed = JSON.parse(message.text)
|
||||
console.log("write_to_file tool called with request:", parsed.request?.substring(0, 300))
|
||||
} catch (_e) {
|
||||
console.log("Could not parse tool execution details")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === "ask" && message.ask === "tool") {
|
||||
toolExecuted = true
|
||||
console.log("Tool requested")
|
||||
console.log("Tool request:", message.text?.substring(0, 200))
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.Message, messageHandler)
|
||||
|
||||
// Listen for task completion
|
||||
// Listen for task events
|
||||
const taskStartedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskStarted = true
|
||||
console.log("Task started:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
|
||||
const taskCompletedHandler = (id: string) => {
|
||||
if (id === taskId) {
|
||||
taskCompleted = true
|
||||
console.log("Task completed:", id)
|
||||
}
|
||||
}
|
||||
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
|
|
@ -191,49 +332,116 @@ suite("Roo Code write_to_file Tool", function () {
|
|||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
},
|
||||
text: `Use the write_to_file tool to create a file at path "nested/deep/directory/${fileName}" with the following content:\n${content}`,
|
||||
text: `Create a file named "${fileName}" in a nested directory structure "nested/deep/directory/" with the following content:\n${content}`,
|
||||
})
|
||||
|
||||
console.log("Task ID:", taskId)
|
||||
console.log("Expected nested path:", nestedPath)
|
||||
|
||||
// Wait for task to start
|
||||
await waitFor(() => taskStarted, { timeout: 45_000 })
|
||||
|
||||
// Wait for task completion
|
||||
await waitFor(() => taskCompleted, { timeout: 60_000 })
|
||||
await waitFor(() => taskCompleted, { timeout: 45_000 })
|
||||
|
||||
// Verify the write_to_file tool was executed
|
||||
assert.ok(toolExecuted, "The write_to_file tool should have been executed")
|
||||
// Give extra time for file system operations
|
||||
await sleep(2000)
|
||||
|
||||
// Give time for file system operations
|
||||
await sleep(1000)
|
||||
// Check various possible locations
|
||||
let fileFound = false
|
||||
let actualFilePath = ""
|
||||
let actualContent = ""
|
||||
|
||||
// Check workspace directory for the file
|
||||
// Check workspace directories
|
||||
const workspaceDirs = await fs
|
||||
.readdir("/tmp")
|
||||
.then((files) => files.filter((f) => f.startsWith("roo-test-workspace-")))
|
||||
.catch(() => [])
|
||||
|
||||
let fileFound = false
|
||||
let actualContent = ""
|
||||
|
||||
for (const wsDir of workspaceDirs) {
|
||||
// Check in nested structure within workspace
|
||||
const wsNestedPath = path.join("/tmp", wsDir, "nested", "deep", "directory", fileName)
|
||||
try {
|
||||
await fs.access(wsNestedPath)
|
||||
actualContent = await fs.readFile(wsNestedPath, "utf-8")
|
||||
fileFound = true
|
||||
console.log("File found in nested directory:", wsNestedPath)
|
||||
actualFilePath = wsNestedPath
|
||||
actualContent = await fs.readFile(wsNestedPath, "utf-8")
|
||||
console.log("File found in workspace nested directory:", wsNestedPath)
|
||||
break
|
||||
} catch {
|
||||
// Continue checking
|
||||
// Also check if file was created directly in workspace root
|
||||
const wsFilePath = path.join("/tmp", wsDir, fileName)
|
||||
try {
|
||||
await fs.access(wsFilePath)
|
||||
fileFound = true
|
||||
actualFilePath = wsFilePath
|
||||
actualContent = await fs.readFile(wsFilePath, "utf-8")
|
||||
console.log("File found in workspace root (nested dirs not created):", wsFilePath)
|
||||
break
|
||||
} catch {
|
||||
// Continue checking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(fileFound, `File should have been created in nested directory: ${fileName}`)
|
||||
// If not found in workspace, check the expected location
|
||||
if (!fileFound) {
|
||||
try {
|
||||
await fs.access(nestedPath)
|
||||
fileFound = true
|
||||
actualFilePath = nestedPath
|
||||
actualContent = await fs.readFile(nestedPath, "utf-8")
|
||||
console.log("File found at expected nested path:", nestedPath)
|
||||
} catch {
|
||||
// File not found
|
||||
}
|
||||
}
|
||||
|
||||
// Debug output if file not found
|
||||
if (!fileFound) {
|
||||
console.log("File not found. Debugging info:")
|
||||
|
||||
// List workspace directories and their contents
|
||||
for (const wsDir of workspaceDirs) {
|
||||
const wsPath = path.join("/tmp", wsDir)
|
||||
try {
|
||||
const files = await fs.readdir(wsPath)
|
||||
console.log(`Files in workspace ${wsDir}:`, files)
|
||||
|
||||
// Check if nested directory was created
|
||||
const nestedDir = path.join(wsPath, "nested")
|
||||
try {
|
||||
await fs.access(nestedDir)
|
||||
console.log("Nested directory exists in workspace")
|
||||
} catch {
|
||||
console.log("Nested directory NOT created in workspace")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Could not list workspace ${wsDir}:`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(fileFound, `File should have been created. Expected filename: ${fileName}`)
|
||||
assert.strictEqual(actualContent.trim(), content, "File content should match")
|
||||
|
||||
console.log("Test passed! File created in nested directory successfully")
|
||||
// Verify that write_to_file tool was actually executed
|
||||
assert.ok(writeToFileToolExecuted, "write_to_file tool should have been executed")
|
||||
assert.ok(
|
||||
toolExecutionDetails.includes(fileName) ||
|
||||
toolExecutionDetails.includes(content) ||
|
||||
toolExecutionDetails.includes("nested"),
|
||||
"Tool execution should include the filename, content, or nested directory reference",
|
||||
)
|
||||
|
||||
// Note: We're not checking if the nested directory structure was created,
|
||||
// just that the file exists with the correct content
|
||||
console.log("Test passed! File created successfully at:", actualFilePath)
|
||||
console.log("write_to_file tool was properly executed")
|
||||
} finally {
|
||||
// Clean up
|
||||
api.off(RooCodeEventName.Message, messageHandler)
|
||||
api.off(RooCodeEventName.TaskStarted, taskStartedHandler)
|
||||
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ import { useRooCodeCloudModels } from "@/hooks/use-roo-code-cloud-models"
|
|||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
|
|
@ -111,7 +110,6 @@ export function NewRun() {
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -464,7 +462,6 @@ export function NewRun() {
|
|||
...(runValues.settings || {}),
|
||||
apiProvider: "openrouter",
|
||||
openRouterModelId: selection.model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
|
||||
}
|
||||
|
|
@ -474,7 +471,6 @@ export function NewRun() {
|
|||
...(runValues.settings || {}),
|
||||
apiProvider: "roo",
|
||||
apiModelId: selection.model,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
|
||||
}
|
||||
|
|
@ -485,7 +481,6 @@ export function NewRun() {
|
|||
...EVALS_SETTINGS,
|
||||
...providerSettings,
|
||||
...importedSettings.globalSettings,
|
||||
toolProtocol: useNativeToolProtocol ? "native" : "xml",
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
|
||||
}
|
||||
|
|
@ -512,7 +507,6 @@ export function NewRun() {
|
|||
configSelections,
|
||||
importedSettings,
|
||||
router,
|
||||
useNativeToolProtocol,
|
||||
commandExecutionTimeout,
|
||||
terminalShellIntegrationTimeout,
|
||||
],
|
||||
|
|
@ -688,26 +682,6 @@ export function NewRun() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-3">
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Tool Protocol Options
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2.5 pl-1">
|
||||
<label
|
||||
htmlFor="native-other"
|
||||
className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
id="native-other"
|
||||
checked={useNativeToolProtocol}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setUseNativeToolProtocol(checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">Use Native Tool Calls</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settings && (
|
||||
<SettingsDiff defaultSettings={EVALS_SETTINGS} customSettings={settings} />
|
||||
)}
|
||||
|
|
@ -792,26 +766,6 @@ export function NewRun() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-3">
|
||||
<Label className="text-sm font-medium text-muted-foreground">
|
||||
Tool Protocol Options
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2.5 pl-1">
|
||||
<label
|
||||
htmlFor="native"
|
||||
className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
id="native"
|
||||
checked={useNativeToolProtocol}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setUseNativeToolProtocol(checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">Use Native Tool Calls</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,8 +98,7 @@ const features: Feature[] = [
|
|||
{
|
||||
icon: Brain,
|
||||
title: "Model Agnostic",
|
||||
description:
|
||||
"Bring your own keys or use the Roo Code Router with access to all top models with no markup.",
|
||||
description: "Bring your own keys or use the Roo Code Router with access to all top models with no markup.",
|
||||
},
|
||||
{
|
||||
icon: Github,
|
||||
|
|
@ -115,8 +114,7 @@ const features: Feature[] = [
|
|||
{
|
||||
icon: Router,
|
||||
title: "Roomote Control",
|
||||
description:
|
||||
"Connect to your local VS Code instance and control the extension remotely from the browser.",
|
||||
description: "Connect to your local VS Code instance and control the extension remotely from the browser.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
|
|
@ -153,7 +151,7 @@ export default function CloudPage() {
|
|||
Your AI Team <span className="text-violet-500">in the Cloud</span>
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
|
||||
Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
|
||||
from the web, Slack, Linear, and more.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
|
|
|
|||
|
|
@ -100,6 +100,19 @@ export default function CookiePolicy() {
|
|||
<td className="border border-border px-4 py-3">1 year</td>
|
||||
<td className="border border-border px-4 py-3 font-mono text-sm">ph_*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-border px-4 py-3 font-medium">HubSpot</td>
|
||||
<td className="border border-border px-4 py-3">
|
||||
Marketing automation and visitor tracking
|
||||
</td>
|
||||
<td className="border border-border px-4 py-3">
|
||||
Analytics (only with your consent)
|
||||
</td>
|
||||
<td className="border border-border px-4 py-3">13 months</td>
|
||||
<td className="border border-border px-4 py-3 font-mono text-sm">
|
||||
hubspotutk, __hstc, __hssrc, __hssc
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -122,6 +135,15 @@ export default function CookiePolicy() {
|
|||
PostHog Privacy Policy
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://legal.hubspot.com/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline">
|
||||
HubSpot Privacy Policy
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 className="mt-12 text-2xl font-bold">Essential cookies</h2>
|
||||
<p>
|
||||
|
|
@ -133,10 +155,10 @@ export default function CookiePolicy() {
|
|||
|
||||
<h2 className="mt-12 text-2xl font-bold">Analytics cookies</h2>
|
||||
<p>
|
||||
We use PostHog analytics cookies to understand how visitors interact with our website. This
|
||||
helps us improve our services and user experience. Analytics cookies are placed only if you give
|
||||
consent through our cookie banner. The lawful basis for processing these cookies is your
|
||||
consent, which you can withdraw at any time.
|
||||
We use PostHog and HubSpot analytics cookies to understand how visitors interact with our
|
||||
website. This helps us improve our services, user experience, and marketing efforts. Analytics
|
||||
cookies are placed only if you give consent through our cookie banner. The lawful basis for
|
||||
processing these cookies is your consent, which you can withdraw at any time.
|
||||
</p>
|
||||
|
||||
<h2 className="mt-12 text-2xl font-bold">Third-party services</h2>
|
||||
|
|
|
|||
|
|
@ -239,8 +239,8 @@ export default function PricingPage() {
|
|||
<div className="text-sm text-muted-foreground">
|
||||
<p className="">
|
||||
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
|
||||
Router – curated models to work with Roo with no markup, including the
|
||||
latest Gemini, GPT and Claude. Paid with credits.
|
||||
Router – curated models to work with Roo with no markup, including the latest
|
||||
Gemini, GPT and Claude. Paid with credits.
|
||||
<Link href="/provider" className="underline hover:no-underline ml-1">
|
||||
See per model pricing.
|
||||
</Link>
|
||||
|
|
@ -291,11 +291,7 @@ export default function PricingPage() {
|
|||
<li>To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)</li>
|
||||
<li>
|
||||
To pay for AI model inference costs (
|
||||
<a
|
||||
href="https://app.roocode.com/provider/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline">
|
||||
<a href="/provider" target="_blank" rel="noopener noreferrer" className="underline">
|
||||
varies by model
|
||||
</a>
|
||||
)
|
||||
|
|
|
|||
401
apps/web-roo-code/src/app/slack/page.tsx
Normal file
401
apps/web-roo-code/src/app/slack/page.tsx
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Brain,
|
||||
CreditCard,
|
||||
GitBranch,
|
||||
GraduationCap,
|
||||
Link2,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
Shield,
|
||||
Slack,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { SlackThreadDemo } from "@/components/slack/slack-thread-demo"
|
||||
import { Button } from "@/components/ui"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
|
||||
const TITLE = "Roo Code for Slack"
|
||||
const DESCRIPTION =
|
||||
"Mention @Roomote in any channel to explain code, plan features, or ship a PR, all without leaving the conversation."
|
||||
const OG_DESCRIPTION = "Your AI Team in Slack"
|
||||
const PATH = "/slack"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"slack integration",
|
||||
"slack bot",
|
||||
"AI in slack",
|
||||
"code assistant slack",
|
||||
"@Roomote",
|
||||
"team collaboration",
|
||||
],
|
||||
}
|
||||
|
||||
// Invalidate cache when a request comes in, at most once every hour.
|
||||
export const revalidate = 3600
|
||||
|
||||
type ValueProp = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const VALUE_PROPS: ValueProp[] = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "Discussion to PR.",
|
||||
description:
|
||||
"Your team discusses a feature in Slack. @Roomote turns the discussion into a plan. Then builds it. All without leaving the conversation.",
|
||||
},
|
||||
{
|
||||
icon: Brain,
|
||||
title: "Thread-aware.",
|
||||
description:
|
||||
'@Roomote reads the full thread before responding. Ask "Can we add caching here?" and it knows exactly what code you mean.',
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
title: "Chain agents.",
|
||||
description:
|
||||
"Start with a Planner to spec it out. Then call the Coder to build it. Multi-step workflows, one Slack thread.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Open to all.",
|
||||
description:
|
||||
"Anyone on your team can ask @Roomote to fix bugs, build features, or investigate issues. Engineering gets looped in only when needed.",
|
||||
},
|
||||
{
|
||||
icon: GraduationCap,
|
||||
title: "Built-in learning.",
|
||||
description: "Public channel mentions show everyone how to leverage agents. Learn by watching.",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Safe by design.",
|
||||
description: "Agents never touch main/master directly. They produce branches and PRs. You approve.",
|
||||
},
|
||||
]
|
||||
|
||||
type WorkflowStep = {
|
||||
step: number
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const WORKFLOW_STEPS: WorkflowStep[] = [
|
||||
{
|
||||
step: 1,
|
||||
title: "Turn the discussion into a plan",
|
||||
description: "Your team discusses a feature. When it gets complex, summon the Planner agent.",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Refine the plan in the thread",
|
||||
description:
|
||||
"The team reviews the spec in the thread, suggests changes, asks questions. Mention @Roomote again to refine.",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Build the plan",
|
||||
description: "Once the plan looks good, hand it off to the Coder agent to implement.",
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Review and ship",
|
||||
description: "The Coder creates a branch and opens a PR. The team reviews, and the feature ships.",
|
||||
},
|
||||
]
|
||||
|
||||
type OnboardingStep = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
link?: {
|
||||
href: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
const ONBOARDING_STEPS: OnboardingStep[] = [
|
||||
{
|
||||
icon: CreditCard,
|
||||
title: "1. Team Plan",
|
||||
description: "Slack requires a Team plan.",
|
||||
link: {
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL,
|
||||
text: "Start a free trial",
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
title: "2. Connect",
|
||||
description: 'Sign in to Roo Code Cloud and go to Settings. Click "Connect" next to Slack.',
|
||||
},
|
||||
{
|
||||
icon: Slack,
|
||||
title: "3. Authorize",
|
||||
description: "Authorize the Roo Code app to access your Slack workspace.",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "4. Add to channels",
|
||||
description: "Add @Roomote to the channels where you want it available.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function SlackPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
|
||||
<div className="text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium mb-6">
|
||||
<Slack className="size-4" />
|
||||
Powered by Roo Code Cloud
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
|
||||
<span className="text-violet-500">@Roomote:</span> Your AI Team in Slack
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto lg:mx-0">
|
||||
Mention @Roomote in any channel to explain code, plan features, or ship a PR, all
|
||||
without leaving the conversation.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Get Started
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SLACK_DOCS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Read the Docs
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center lg:justify-end">
|
||||
<SlackThreadDemo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Value Props Section */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Why your team will love using Roo Code in Slack
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
AI agents that understand context, chain together for complex work, and keep your team in
|
||||
control.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto relative">
|
||||
{VALUE_PROPS.map((prop, index) => {
|
||||
const Icon = prop.icon
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-background p-8 rounded-2xl border border-border hover:shadow-lg transition-all duration-300">
|
||||
<div className="bg-violet-100 dark:bg-violet-900/20 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
|
||||
<Icon className="size-6 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">{prop.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">{prop.description}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Workflow Section */}
|
||||
<section className="relative overflow-hidden border-t border-border py-24 lg:py-32">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 max-w-5xl text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6">
|
||||
<Zap className="size-4" />
|
||||
Featured Workflow
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-4">
|
||||
Thread to Shipped Feature
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Turn Slack discussions into working code. No context lost, no meetings needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-10 items-center">
|
||||
{/* YouTube Video Embed */}
|
||||
<div className="lg:col-span-3 overflow-hidden rounded-2xl border border-border bg-background shadow-lg">
|
||||
<iframe
|
||||
className="aspect-video w-full"
|
||||
src="https://www.youtube-nocookie.com/embed/dJM_8HHGe1E?rel=0"
|
||||
title="Roo Code Slack Integration Demo"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Workflow Steps */}
|
||||
<div className="lg:col-span-2 space-y-3">
|
||||
{WORKFLOW_STEPS.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className="relative border border-border rounded-xl bg-background p-4 transition-all duration-300 hover:shadow-md hover:border-blue-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/30 w-7 h-7 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-xs shrink-0 mt-0.5">
|
||||
{step.step}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-base font-semibold text-foreground mb-0.5">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-snug text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Onboarding Section */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">Get started in minutes</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Connect your Slack workspace and start working with AI agents.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-5xl mx-auto">
|
||||
{ONBOARDING_STEPS.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div key={index} className="text-center">
|
||||
<div className="bg-violet-100 dark:bg-violet-900/20 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Icon className="size-8 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{step.title}</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{step.description}
|
||||
{step.link && (
|
||||
<>
|
||||
{" "}
|
||||
<a
|
||||
href={step.link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-violet-600 dark:text-violet-400 hover:underline">
|
||||
{step.link.text} →
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
|
||||
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Start using Roo Code in Slack
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">
|
||||
Start a free 14 day Team trial.
|
||||
</p>
|
||||
<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start free trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import { EXTERNAL_LINKS } from "@/lib/constants"
|
|||
import { useLogoSrc } from "@/lib/hooks/use-logo-src"
|
||||
import { ScrollButton } from "@/components/ui"
|
||||
import ThemeToggle from "@/components/chromes/theme-toggle"
|
||||
import { Brain, ChevronDown, Cloud, Puzzle, X } from "lucide-react"
|
||||
import { Brain, ChevronDown, Cloud, Puzzle, Slack, X } from "lucide-react"
|
||||
|
||||
interface NavBarProps {
|
||||
stars: string | null
|
||||
|
|
@ -54,6 +54,12 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
<Cloud className="size-3 inline mr-2 -mt-0.5" />
|
||||
Roo Code Cloud
|
||||
</Link>
|
||||
<Link
|
||||
href="/slack"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
<Slack className="size-3 inline mr-2 -mt-0.5" />
|
||||
Roo Code for Slack
|
||||
</Link>
|
||||
<Link
|
||||
href="/provider"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
|
|
@ -190,6 +196,12 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code Cloud
|
||||
</Link>
|
||||
<Link
|
||||
href="/slack"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code for Slack
|
||||
</Link>
|
||||
<Link
|
||||
href="/provider"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Script from "next/script"
|
||||
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
|
||||
|
||||
// HubSpot Account ID
|
||||
const HUBSPOT_ID = "243714031"
|
||||
|
||||
/**
|
||||
* HubSpot Tracking Provider
|
||||
* Loads HubSpot tracking script only after user consent is given, following GDPR requirements
|
||||
*/
|
||||
export function HubSpotProvider({ children }: { children: React.ReactNode }) {
|
||||
const [shouldLoad, setShouldLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial consent status
|
||||
if (hasConsent()) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
|
||||
// Listen for consent changes
|
||||
const unsubscribe = onConsentChange((consented) => {
|
||||
if (consented) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldLoad && (
|
||||
<>
|
||||
{/* HubSpot Embed Code */}
|
||||
<Script
|
||||
id="hs-script-loader"
|
||||
src={`//js-na2.hs-scripts.com/${HUBSPOT_ID}.js`}
|
||||
strategy="afterInteractive"
|
||||
async
|
||||
defer
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
|||
import { ThemeProvider } from "next-themes"
|
||||
|
||||
import { GoogleTagManagerProvider } from "./google-tag-manager-provider"
|
||||
import { HubSpotProvider } from "./hubspot-provider"
|
||||
import { PostHogProvider } from "./posthog-provider"
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
|
@ -12,11 +13,13 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
|
|||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GoogleTagManagerProvider>
|
||||
<PostHogProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</PostHogProvider>
|
||||
<HubSpotProvider>
|
||||
<PostHogProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</PostHogProvider>
|
||||
</HubSpotProvider>
|
||||
</GoogleTagManagerProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
|
|
|||
548
apps/web-roo-code/src/components/slack/slack-thread-demo.tsx
Normal file
548
apps/web-roo-code/src/components/slack/slack-thread-demo.tsx
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { CheckCircle2, Paperclip } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type SlackMessage = {
|
||||
id: string
|
||||
author: string
|
||||
timeLabel: string
|
||||
body: ReactNode
|
||||
avatarText: string
|
||||
avatarClassName: string
|
||||
kind: "human" | "bot"
|
||||
}
|
||||
|
||||
function usePrefersReducedMotion(): boolean {
|
||||
const [reduced, setReduced] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const onChange = () => setReduced(media.matches)
|
||||
onChange()
|
||||
|
||||
if (typeof media.addEventListener === "function") {
|
||||
media.addEventListener("change", onChange)
|
||||
return () => media.removeEventListener("change", onChange)
|
||||
}
|
||||
|
||||
media.addListener?.(onChange)
|
||||
return () => media.removeListener?.(onChange)
|
||||
}, [])
|
||||
|
||||
return reduced
|
||||
}
|
||||
|
||||
type TypingDotsProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function TypingDots({ className }: TypingDotsProps): JSX.Element {
|
||||
return (
|
||||
<span className={cn("inline-flex items-center gap-1", className)} aria-hidden="true">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:0ms]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:180ms]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:360ms]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type FakeLinkProps = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
function FakeLink({ children, className }: FakeLinkProps): JSX.Element {
|
||||
return (
|
||||
<span className={cn("text-violet-300 underline underline-offset-2", "cursor-default", className)}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type SlackMessageRowProps = {
|
||||
message: SlackMessage
|
||||
isNew: boolean
|
||||
reduceMotion: boolean
|
||||
}
|
||||
|
||||
function SlackMessageRow({ message, isNew, reduceMotion }: SlackMessageRowProps): JSX.Element {
|
||||
let animation = ""
|
||||
if (!reduceMotion && isNew) {
|
||||
animation = "animate-in fade-in slide-in-from-bottom-2 duration-500"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-3", animation)}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
|
||||
message.avatarClassName,
|
||||
)}>
|
||||
{message.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<span className="text-[13px] font-semibold text-[#F8F8F9]">{message.author}</span>
|
||||
<span className="text-[11px] text-[#8B8D91]">{message.timeLabel}</span>
|
||||
{message.kind === "bot" && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
App
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-[13px] leading-relaxed text-[#D1D2D3]">{message.body}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type SlackThreadDemoProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SlackThreadDemo({ className }: SlackThreadDemoProps): JSX.Element {
|
||||
const reduceMotion = usePrefersReducedMotion()
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const scrollViewportRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const messages: SlackMessage[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "m1",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 2:56 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<span>We need to add a page to our Marketing site that highlights using Roo Code from Slack.</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 2:58 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
The documentation for using Roo Code from Slack is here:{" "}
|
||||
<FakeLink className="hover:text-violet-200">
|
||||
https://docs.roocode.com/roo-code-cloud/slack-integration
|
||||
</FakeLink>
|
||||
</div>
|
||||
<div className="text-[#B8BBC0]">Here are some pages from our site we can use for guidance:</div>
|
||||
<ol className="list-decimal pl-5 text-[#D1D2D3]">
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com</FakeLink>
|
||||
</li>
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com/extension</FakeLink>
|
||||
</li>
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com/cloud</FakeLink>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m3",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:08 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<div>This is the start of a wireframe I have in mind for this page</div>
|
||||
<div className="w-full max-w-[420px] rounded-lg border border-white/10 bg-black/20 p-3">
|
||||
<div className="flex items-center gap-2 text-[12px] text-[#B8BBC0]">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
IMG_9721.heic
|
||||
</div>
|
||||
<div className="mt-3 h-24 w-full rounded-md bg-gradient-to-br from-white/10 via-white/5 to-white/0" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m4",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:09 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<span>
|
||||
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink> let's create
|
||||
the plan to deliver this
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m5",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:09 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-[#D1D2D3]">
|
||||
Calling <span className="font-semibold text-[#F8F8F9]">Planneroo</span> to get started on
|
||||
your task on{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
RooCodeInc/Roo-Code
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center rounded-md border border-white/10 bg-transparent px-2 py-1 text-[12px] font-medium text-[#D1D2D3] hover:bg-white/5">
|
||||
Cancel ✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m6",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:10 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-x-2">
|
||||
<span>Cool, I'll knock this out real quick.</span>
|
||||
<FakeLink className="hover:text-violet-200">Follow along</FakeLink>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m7",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:12 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-[#F8F8F9]">Todo List:</div>
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Analyze existing page structures and component patterns",
|
||||
"Review marketing content requirements and wireframe details",
|
||||
"Create detailed component architecture plan",
|
||||
"Design page structure and section breakdown",
|
||||
"Plan navigation updates and integration points",
|
||||
"Test the page and verify all sections work",
|
||||
].map((item) => (
|
||||
<li key={item} className="text-[#D1D2D3]">
|
||||
<span className="mr-2">•</span>
|
||||
<span className="line-through opacity-80">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-[12px] text-[#8B8D91]">(edited)</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m8",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:16 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
I've created a comprehensive implementation plan for the Roo Code Slack integration
|
||||
marketing page at{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
plans/slack-marketing-page-plan.md
|
||||
</code>
|
||||
.
|
||||
</p>
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Plan Overview</div>
|
||||
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
|
||||
<li>
|
||||
<span className="mr-2">•</span>Hero + dual CTAs
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>Value props grid
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>“Thread to Shipped Feature” workflow
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>Onboarding steps + CTA
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<span className="text-[#B8BBC0]">Full document:</span>
|
||||
<FakeLink className="hover:text-violet-200">View artifact</FakeLink>
|
||||
</div>
|
||||
<div className="text-[12px] italic text-[#8B8D91]">
|
||||
Want to follow up? Just @-mention me in your response.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m9",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:17 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-x-2">
|
||||
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink>
|
||||
<span>this looks great, let's use Coderoo to build this</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m10",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:23 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
I've built the Roo Code Slack integration marketing page. Here's what was
|
||||
implemented:
|
||||
</p>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Files</div>
|
||||
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/app/slack/page.tsx
|
||||
</code>{" "}
|
||||
— Slack marketing page
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/lib/constants.ts
|
||||
</code>{" "}
|
||||
— added{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
SLACK_DOCS
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/components/chromes/nav-bar.tsx
|
||||
</code>{" "}
|
||||
— added Slack to Product dropdown
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Pull Request</div>
|
||||
<div className="mt-2">
|
||||
<span className="font-semibold text-[#F8F8F9]">PR #10853</span>:{" "}
|
||||
<FakeLink className="hover:text-violet-200">
|
||||
https://github.com/RooCodeInc/Roo-Code/pull/10853
|
||||
</FakeLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[#B8BBC0]">
|
||||
The page is accessible at{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">/slack</code>{" "}
|
||||
and includes navigation links in desktop and mobile.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
type DemoPhase =
|
||||
| { kind: "show"; messageIndex: number }
|
||||
| { kind: "typing"; messageIndex: number }
|
||||
| { kind: "reset" }
|
||||
|
||||
const phases: DemoPhase[] = useMemo(() => {
|
||||
const next: DemoPhase[] = []
|
||||
if (messages.length === 0) return [{ kind: "reset" }]
|
||||
|
||||
next.push({ kind: "typing", messageIndex: 0 })
|
||||
next.push({ kind: "show", messageIndex: 0 })
|
||||
for (let messageIndex = 1; messageIndex < messages.length; messageIndex += 1) {
|
||||
next.push({ kind: "typing", messageIndex })
|
||||
next.push({ kind: "show", messageIndex })
|
||||
}
|
||||
next.push({ kind: "reset" })
|
||||
return next
|
||||
}, [messages])
|
||||
|
||||
const lastShowPhaseIndex = useMemo(() => {
|
||||
let lastIndex = -1
|
||||
for (let idx = 0; idx < phases.length; idx += 1) {
|
||||
if (phases[idx]?.kind === "show") lastIndex = idx
|
||||
}
|
||||
return lastIndex
|
||||
}, [phases])
|
||||
|
||||
useEffect(() => {
|
||||
if (reduceMotion) {
|
||||
setStepIndex(lastShowPhaseIndex >= 0 ? lastShowPhaseIndex : 0)
|
||||
return
|
||||
}
|
||||
|
||||
const active = phases[stepIndex] ?? phases.at(0)
|
||||
const isLastMessageShow = active?.kind === "show" && stepIndex === lastShowPhaseIndex
|
||||
const durationMs = (() => {
|
||||
const base = 2200
|
||||
if (active?.kind === "reset") return 500
|
||||
if (active?.kind === "typing") return 900
|
||||
return isLastMessageShow ? base * 2 : base
|
||||
})()
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setStepIndex((prev) => (prev + 1) % phases.length)
|
||||
}, durationMs)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
|
||||
|
||||
const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "reset" }
|
||||
|
||||
function getVisibleCount(phase: DemoPhase): number {
|
||||
if (phase.kind === "reset") return 0
|
||||
if (phase.kind === "typing") return phase.messageIndex
|
||||
return phase.messageIndex + 1
|
||||
}
|
||||
|
||||
const visibleCount = getVisibleCount(activePhase)
|
||||
const visibleMessages = messages.slice(0, visibleCount)
|
||||
const typingTarget = activePhase.kind === "typing" ? messages[activePhase.messageIndex] : undefined
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = scrollViewportRef.current
|
||||
if (!viewport) return
|
||||
|
||||
if (activePhase.kind === "reset" || visibleCount <= 1) {
|
||||
viewport.scrollTo({ top: 0, behavior: "auto" })
|
||||
return
|
||||
}
|
||||
|
||||
viewport.scrollTo({
|
||||
top: viewport.scrollHeight,
|
||||
behavior: reduceMotion ? "auto" : "smooth",
|
||||
})
|
||||
}, [activePhase.kind, reduceMotion, visibleCount])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full max-w-[620px] h-[520px] sm:h-[560px]", className)}
|
||||
role="img"
|
||||
aria-label="Animated Slack thread showing Roo Code responding as @Roomote">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#1A1D21] shadow-2xl shadow-black/30">
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#F24A4A]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#F2C94C]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#27AE60]" />
|
||||
<div className="ml-3 text-sm font-semibold text-[#F8F8F9]">Thread</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[11px] text-[#8B8D91]">
|
||||
<span className="h-2 w-2 rounded-full bg-[#27AE60]" />
|
||||
Live demo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={scrollViewportRef}
|
||||
className="flex-1 overflow-y-auto px-4 py-5 [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.18)_transparent]">
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-5 transition-opacity duration-300 will-change-opacity",
|
||||
activePhase.kind === "reset" ? "opacity-0" : "opacity-100",
|
||||
)}>
|
||||
{visibleMessages.map((message) => (
|
||||
<SlackMessageRow
|
||||
key={message.id}
|
||||
message={message}
|
||||
reduceMotion={reduceMotion}
|
||||
isNew={
|
||||
activePhase.kind === "show" && messages[activePhase.messageIndex]?.id === message.id
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{typingTarget && (
|
||||
<div className={cn(reduceMotion ? "" : "animate-in fade-in duration-300", "flex gap-3")}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
|
||||
typingTarget.avatarClassName,
|
||||
)}>
|
||||
{typingTarget.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-[13px] font-semibold text-[#F8F8F9]">
|
||||
{typingTarget.author}
|
||||
</span>
|
||||
{typingTarget.kind === "bot" && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
App
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11px] text-[#8B8D91]">typing…</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<TypingDots />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-white/10 px-4 py-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{messages.map((message, idx) => (
|
||||
<span
|
||||
key={message.id}
|
||||
className={cn(
|
||||
"h-1.5 w-5 rounded-full transition-colors duration-300",
|
||||
Math.max(0, visibleCount - 1) === idx ? "bg-violet-300" : "bg-white/10",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ export const EXTERNAL_LINKS = {
|
|||
BLUESKY: "https://bsky.app/profile/roocode.bsky.social",
|
||||
YOUTUBE: "https://www.youtube.com/@RooCodeYT",
|
||||
DOCUMENTATION: "https://docs.roocode.com",
|
||||
SLACK_DOCS: "https://docs.roocode.com/roo-code-cloud/slack-integration",
|
||||
CAREERS: "https://careers.roocode.com",
|
||||
ISSUES: "https://github.com/RooCodeInc/Roo-Code/issues",
|
||||
FEATURE_REQUESTS: "https://github.com/RooCodeInc/Roo-Code/discussions/categories/feature-requests",
|
||||
|
|
@ -28,6 +29,7 @@ export const EXTERNAL_LINKS = {
|
|||
CLOUD_APP_SIGNUP: "https://app.roocode.com/sign-up",
|
||||
CLOUD_APP_SIGNUP_HOME: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
CLOUD_APP_TEAM_TRIAL: "https://app.roocode.com/checkout/team",
|
||||
SUPPORT: "mailto:support@roocode.com",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"vsix:nightly": "turbo vsix:nightly --log-order grouped --output-logs new-only",
|
||||
"clean": "turbo clean --log-order grouped --output-logs new-only && rimraf dist out bin .vite-port .turbo",
|
||||
"install:vsix": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix && node scripts/install-vsix.js",
|
||||
"install:vsix:nightly": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix:nightly && node scripts/install-vsix.js --nightly",
|
||||
"changeset:version": "cp CHANGELOG.md src/CHANGELOG.md && changeset version && cp -vf src/CHANGELOG.md .",
|
||||
"knip": "knip --include files",
|
||||
"evals": "dotenvx run -f packages/evals/.env.development packages/evals/.env.local -- docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@roo-code/types": "workspace:^",
|
||||
"esbuild": "^0.25.0",
|
||||
"execa": "^9.5.2",
|
||||
"ignore": "^7.0.3",
|
||||
"openai": "^5.12.2",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for all fixtures combined 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## simple
|
||||
Description: Simple tool
|
||||
Parameters:
|
||||
- value: (required) The input value (type: string)
|
||||
Usage:
|
||||
<simple>
|
||||
<value>value value here</value>
|
||||
</simple>
|
||||
|
||||
## cached
|
||||
Description: Cached tool
|
||||
Parameters:
|
||||
Usage:
|
||||
<cached>
|
||||
</cached>
|
||||
|
||||
## legacy
|
||||
Description: Legacy tool using args
|
||||
Parameters:
|
||||
- input: (required) The input string (type: string)
|
||||
Usage:
|
||||
<legacy>
|
||||
<input>input value here</input>
|
||||
</legacy>
|
||||
|
||||
## multi_toolA
|
||||
Description: Tool A
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolA>
|
||||
</multi_toolA>
|
||||
|
||||
## multi_toolB
|
||||
Description: Tool B
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolB>
|
||||
</multi_toolB>
|
||||
|
||||
## mixed_validTool
|
||||
Description: Valid
|
||||
Parameters:
|
||||
Usage:
|
||||
<mixed_validTool>
|
||||
</mixed_validTool>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for cached tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## cached
|
||||
Description: Cached tool
|
||||
Parameters:
|
||||
Usage:
|
||||
<cached>
|
||||
</cached>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for legacy tool (using args) 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## legacy
|
||||
Description: Legacy tool using args
|
||||
Parameters:
|
||||
- input: (required) The input string (type: string)
|
||||
Usage:
|
||||
<legacy>
|
||||
<input>input value here</input>
|
||||
</legacy>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for mixed export tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## mixed_validTool
|
||||
Description: Valid
|
||||
Parameters:
|
||||
Usage:
|
||||
<mixed_validTool>
|
||||
</mixed_validTool>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for multi export tools 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## multi_toolA
|
||||
Description: Tool A
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolA>
|
||||
</multi_toolA>
|
||||
|
||||
## multi_toolB
|
||||
Description: Tool B
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolB>
|
||||
</multi_toolB>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for simple tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## simple
|
||||
Description: Simple tool
|
||||
Parameters:
|
||||
- value: (required) The input value (type: string)
|
||||
Usage:
|
||||
<simple>
|
||||
<value>value value here</value>
|
||||
</simple>"
|
||||
`;
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/format-xml.spec.ts
|
||||
|
||||
import { type SerializedCustomToolDefinition, parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
|
||||
import { formatXml } from "../format-xml.js"
|
||||
|
||||
import simpleTool from "./fixtures/simple.js"
|
||||
import cachedTool from "./fixtures/cached.js"
|
||||
import legacyTool from "./fixtures/legacy.js"
|
||||
import { toolA, toolB } from "./fixtures/multi.js"
|
||||
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
|
||||
|
||||
const fixtureTools = {
|
||||
simple: simpleTool,
|
||||
cached: cachedTool,
|
||||
legacy: legacyTool,
|
||||
multi_toolA: toolA,
|
||||
multi_toolB: toolB,
|
||||
mixed_validTool: mixedValidTool,
|
||||
}
|
||||
|
||||
describe("formatXml", () => {
|
||||
it("should return empty string for empty tools array", () => {
|
||||
expect(formatXml([])).toBe("")
|
||||
})
|
||||
|
||||
it("should throw for undefined tools", () => {
|
||||
expect(() => formatXml(undefined as unknown as SerializedCustomToolDefinition[])).toThrow()
|
||||
})
|
||||
|
||||
it("should generate description for a single tool without args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "my_tool",
|
||||
description: "A simple tool that does something",
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("# Custom Tools")
|
||||
expect(result).toContain("## my_tool")
|
||||
expect(result).toContain("Description: A simple tool that does something")
|
||||
expect(result).toContain("Parameters: None")
|
||||
expect(result).toContain("<my_tool>")
|
||||
expect(result).toContain("</my_tool>")
|
||||
})
|
||||
|
||||
it("should generate description for a tool with required args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "greeter",
|
||||
description: "Greets a person by name",
|
||||
parameters: z.object({
|
||||
name: z.string().describe("The name of the person to greet"),
|
||||
}),
|
||||
async execute({ name }) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("## greeter")
|
||||
expect(result).toContain("Description: Greets a person by name")
|
||||
expect(result).toContain("Parameters:")
|
||||
expect(result).toContain("- name: (required) The name of the person to greet (type: string)")
|
||||
expect(result).toContain("<greeter>")
|
||||
expect(result).toContain("<name>name value here</name>")
|
||||
expect(result).toContain("</greeter>")
|
||||
})
|
||||
|
||||
it("should generate description for a tool with optional args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "configurable_tool",
|
||||
description: "A tool with optional configuration",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("The input to process"),
|
||||
format: z.string().optional().describe("Output format"),
|
||||
}),
|
||||
async execute({ input, format }) {
|
||||
return format ? `${input} (${format})` : input
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("- input: (required) The input to process (type: string)")
|
||||
expect(result).toContain("- format: (optional) Output format (type: string)")
|
||||
expect(result).toContain("<input>input value here</input>")
|
||||
expect(result).toContain("<format>optional format value</format>")
|
||||
})
|
||||
|
||||
it("should generate descriptions for multiple tools", () => {
|
||||
const tools = [
|
||||
defineCustomTool({
|
||||
name: "tool_a",
|
||||
description: "First tool",
|
||||
async execute() {
|
||||
return "a"
|
||||
},
|
||||
}),
|
||||
defineCustomTool({
|
||||
name: "tool_b",
|
||||
description: "Second tool",
|
||||
parameters: z.object({
|
||||
value: z.number().describe("A numeric value"),
|
||||
}),
|
||||
async execute() {
|
||||
return "b"
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const serialized = serializeCustomTools(tools)
|
||||
const result = formatXml(serialized)
|
||||
|
||||
expect(result).toContain("## tool_a")
|
||||
expect(result).toContain("Description: First tool")
|
||||
expect(result).toContain("## tool_b")
|
||||
expect(result).toContain("Description: Second tool")
|
||||
expect(result).toContain("- value: (required) A numeric value (type: number)")
|
||||
})
|
||||
|
||||
it("should treat args in required array as required", () => {
|
||||
// Using a raw SerializedToolDefinition to test the required behavior.
|
||||
const tools: SerializedCustomToolDefinition[] = [
|
||||
{
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: {
|
||||
type: "object",
|
||||
description: "Some data",
|
||||
},
|
||||
},
|
||||
required: ["data"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = formatXml(tools)
|
||||
|
||||
expect(result).toContain("- data: (required) Some data (type: object)")
|
||||
expect(result).toContain("<data>data value here</data>")
|
||||
})
|
||||
})
|
||||
|
||||
describe("XML Protocol snapshots", () => {
|
||||
it("should generate correct XML description for simple tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.simple)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for cached tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.cached)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for legacy tool (using args)", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.legacy)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for multi export tools", () => {
|
||||
const serializedA = serializeCustomTool(fixtureTools.multi_toolA)
|
||||
const serializedB = serializeCustomTool(fixtureTools.multi_toolB)
|
||||
const result = formatXml([serializedA, serializedB])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for mixed export tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.mixed_validTool)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for all fixtures combined", () => {
|
||||
const allSerialized = Object.values(fixtureTools).map(serializeCustomTool)
|
||||
const result = formatXml(allSerialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import type { SerializedCustomToolDefinition, SerializedCustomToolParameters } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* Extract the type string from a parameter schema.
|
||||
* Handles both direct `type` property and `anyOf` schemas (used for nullable types).
|
||||
*/
|
||||
function getParameterType(parameter: SerializedCustomToolParameters): string {
|
||||
// Direct type property
|
||||
if (parameter.type) {
|
||||
return String(parameter.type)
|
||||
}
|
||||
|
||||
// Handle anyOf schema (used for nullable types like `string | null`)
|
||||
if (parameter.anyOf && Array.isArray(parameter.anyOf)) {
|
||||
const types = parameter.anyOf
|
||||
.map((schema) => (typeof schema === "object" && schema.type ? String(schema.type) : null))
|
||||
.filter((t): t is string => t !== null && t !== "null")
|
||||
|
||||
if (types.length > 0) {
|
||||
return types.join(" | ")
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
function getParameterDescription(name: string, parameter: SerializedCustomToolParameters, required: string[]): string {
|
||||
const requiredText = required.includes(name) ? "(required)" : "(optional)"
|
||||
const typeText = getParameterType(parameter)
|
||||
return `- ${name}: ${requiredText} ${parameter.description ?? ""} (type: ${typeText})`
|
||||
}
|
||||
|
||||
function getUsage(tool: SerializedCustomToolDefinition): string {
|
||||
const lines: string[] = [`<${tool.name}>`]
|
||||
|
||||
if (tool.parameters) {
|
||||
const required = tool.parameters.required ?? []
|
||||
|
||||
for (const [argName, _argType] of Object.entries(tool.parameters.properties ?? {})) {
|
||||
const placeholder = required.includes(argName) ? `${argName} value here` : `optional ${argName} value`
|
||||
lines.push(`<${argName}>${placeholder}</${argName}>`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`</${tool.name}>`)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function getDescription(tool: SerializedCustomToolDefinition): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push(`## ${tool.name}`)
|
||||
parts.push(`Description: ${tool.description}`)
|
||||
|
||||
if (tool.parameters?.properties) {
|
||||
const required = tool.parameters?.required ?? []
|
||||
parts.push("Parameters:")
|
||||
|
||||
for (const [name, parameter] of Object.entries(tool.parameters.properties)) {
|
||||
// What should we do with `boolean` values for `parameter`?
|
||||
if (typeof parameter !== "object") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts.push(getParameterDescription(name, parameter, required))
|
||||
}
|
||||
} else {
|
||||
parts.push("Parameters: None")
|
||||
}
|
||||
|
||||
parts.push("Usage:")
|
||||
parts.push(getUsage(tool))
|
||||
|
||||
return parts.join("\n")
|
||||
}
|
||||
|
||||
export function formatXml(tools: SerializedCustomToolDefinition[]): string {
|
||||
if (tools.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const descriptions = tools.map((tool) => getDescription(tool))
|
||||
|
||||
return `# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
${descriptions.join("\n\n")}`
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./custom-tool-registry.js"
|
||||
export * from "./serialize.js"
|
||||
export * from "./format-xml.js"
|
||||
export * from "./format-native.js"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./custom-tools/index.js"
|
||||
export * from "./debug-log/index.js"
|
||||
export * from "./message-utils/index.js"
|
||||
export * from "./worktree/index.js"
|
||||
|
|
|
|||
306
packages/core/src/worktree/__tests__/worktree-include.spec.ts
Normal file
306
packages/core/src/worktree/__tests__/worktree-include.spec.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
import { execFile } from "child_process"
|
||||
import { promisify } from "util"
|
||||
|
||||
import { WorktreeIncludeService } from "../worktree-include.js"
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
async function execGit(cwd: string, args: string[]): Promise<string> {
|
||||
const { stdout } = await execFileAsync("git", args, { cwd, encoding: "utf8" })
|
||||
return stdout
|
||||
}
|
||||
|
||||
describe("WorktreeIncludeService", () => {
|
||||
let service: WorktreeIncludeService
|
||||
let tempDir: string
|
||||
|
||||
beforeEach(async () => {
|
||||
service = new WorktreeIncludeService()
|
||||
// Create a temp directory for each test
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "worktree-test-"))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up temp directory
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
describe("hasWorktreeInclude", () => {
|
||||
it("should return true when .worktreeinclude exists", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
|
||||
|
||||
const result = await service.hasWorktreeInclude(tempDir)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when .worktreeinclude does not exist", async () => {
|
||||
const result = await service.hasWorktreeInclude(tempDir)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false for non-existent directory", async () => {
|
||||
const result = await service.hasWorktreeInclude("/non/existent/path")
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("branchHasWorktreeInclude", () => {
|
||||
it("should detect .worktreeinclude on the specified branch", async () => {
|
||||
const repoDir = path.join(tempDir, "repo")
|
||||
await fs.mkdir(repoDir, { recursive: true })
|
||||
|
||||
await execGit(repoDir, ["init"])
|
||||
await execGit(repoDir, ["config", "user.name", "Test User"])
|
||||
await execGit(repoDir, ["config", "user.email", "test@example.com"])
|
||||
|
||||
await fs.writeFile(path.join(repoDir, "README.md"), "test")
|
||||
await execGit(repoDir, ["add", "README.md"])
|
||||
await execGit(repoDir, ["commit", "-m", "init"])
|
||||
|
||||
const baseBranch = (await execGit(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"])).trim()
|
||||
|
||||
expect(await service.branchHasWorktreeInclude(repoDir, baseBranch)).toBe(false)
|
||||
|
||||
await execGit(repoDir, ["checkout", "-b", "with-include"])
|
||||
await fs.writeFile(path.join(repoDir, ".worktreeinclude"), "node_modules")
|
||||
await execGit(repoDir, ["add", ".worktreeinclude"])
|
||||
await execGit(repoDir, ["commit", "-m", "add include"])
|
||||
|
||||
expect(await service.branchHasWorktreeInclude(repoDir, "with-include")).toBe(true)
|
||||
}, 30_000)
|
||||
})
|
||||
|
||||
describe("getStatus", () => {
|
||||
it("should return correct status when both files exist", async () => {
|
||||
const gitignoreContent = "node_modules\n.env\ndist"
|
||||
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
|
||||
await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent)
|
||||
|
||||
const result = await service.getStatus(tempDir)
|
||||
|
||||
expect(result.exists).toBe(true)
|
||||
expect(result.hasGitignore).toBe(true)
|
||||
expect(result.gitignoreContent).toBe(gitignoreContent)
|
||||
})
|
||||
|
||||
it("should return correct status when only .gitignore exists", async () => {
|
||||
const gitignoreContent = "node_modules\n.env"
|
||||
await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent)
|
||||
|
||||
const result = await service.getStatus(tempDir)
|
||||
|
||||
expect(result.exists).toBe(false)
|
||||
expect(result.hasGitignore).toBe(true)
|
||||
expect(result.gitignoreContent).toBe(gitignoreContent)
|
||||
})
|
||||
|
||||
it("should return correct status when only .worktreeinclude exists", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
|
||||
|
||||
const result = await service.getStatus(tempDir)
|
||||
|
||||
expect(result.exists).toBe(true)
|
||||
expect(result.hasGitignore).toBe(false)
|
||||
expect(result.gitignoreContent).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return correct status when neither file exists", async () => {
|
||||
const result = await service.getStatus(tempDir)
|
||||
|
||||
expect(result.exists).toBe(false)
|
||||
expect(result.hasGitignore).toBe(false)
|
||||
expect(result.gitignoreContent).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createWorktreeInclude", () => {
|
||||
it("should create .worktreeinclude file with specified content", async () => {
|
||||
const content = "node_modules\n.env\ndist"
|
||||
|
||||
await service.createWorktreeInclude(tempDir, content)
|
||||
|
||||
const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8")
|
||||
expect(fileContent).toBe(content)
|
||||
})
|
||||
|
||||
it("should overwrite existing .worktreeinclude file", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "old content")
|
||||
const newContent = "new content"
|
||||
|
||||
await service.createWorktreeInclude(tempDir, newContent)
|
||||
|
||||
const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8")
|
||||
expect(fileContent).toBe(newContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe("copyWorktreeIncludeFiles", () => {
|
||||
let sourceDir: string
|
||||
let targetDir: string
|
||||
|
||||
beforeEach(async () => {
|
||||
sourceDir = path.join(tempDir, "source")
|
||||
targetDir = path.join(tempDir, "target")
|
||||
await fs.mkdir(sourceDir, { recursive: true })
|
||||
await fs.mkdir(targetDir, { recursive: true })
|
||||
})
|
||||
|
||||
it("should return empty array when no .worktreeinclude exists", async () => {
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("should return empty array when no .gitignore exists", async () => {
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("should return empty array when patterns do not match", async () => {
|
||||
// .worktreeinclude wants node_modules, .gitignore only ignores .env
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env")
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("should copy files that match both patterns", async () => {
|
||||
// Both files include node_modules
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
|
||||
// Create a file in node_modules
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
await fs.writeFile(path.join(sourceDir, "node_modules", "package.json"), '{"name": "test"}')
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toContain("node_modules")
|
||||
// Verify the file was copied
|
||||
const copiedContent = await fs.readFile(path.join(targetDir, "node_modules", "package.json"), "utf-8")
|
||||
expect(copiedContent).toBe('{"name": "test"}')
|
||||
})
|
||||
|
||||
it("should only copy intersection of patterns", async () => {
|
||||
// .worktreeinclude: node_modules, dist
|
||||
// .gitignore: node_modules, .env
|
||||
// Only node_modules should be copied (intersection)
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\ndist")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env")
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
await fs.mkdir(path.join(sourceDir, "dist"), { recursive: true })
|
||||
await fs.writeFile(path.join(sourceDir, ".env"), "SECRET=123")
|
||||
await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test")
|
||||
await fs.writeFile(path.join(sourceDir, "dist", "main.js"), "console.log('dist')")
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
// Only node_modules should be in the result (matches both)
|
||||
expect(result).toContain("node_modules")
|
||||
expect(result).not.toContain("dist") // only in .worktreeinclude
|
||||
expect(result).not.toContain(".env") // only in .gitignore
|
||||
|
||||
// Verify node_modules was copied
|
||||
const nodeModulesExists = await fs
|
||||
.access(path.join(targetDir, "node_modules"))
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
expect(nodeModulesExists).toBe(true)
|
||||
|
||||
// Verify dist was NOT copied
|
||||
const distExists = await fs
|
||||
.access(path.join(targetDir, "dist"))
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
expect(distExists).toBe(false)
|
||||
})
|
||||
|
||||
it("should skip .git directory", async () => {
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".git")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".git")
|
||||
await fs.mkdir(path.join(sourceDir, ".git"), { recursive: true })
|
||||
await fs.writeFile(path.join(sourceDir, ".git", "config"), "[core]")
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).not.toContain(".git")
|
||||
})
|
||||
|
||||
it("should copy single files", async () => {
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".env.local")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env.local")
|
||||
await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value")
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toContain(".env.local")
|
||||
const copiedContent = await fs.readFile(path.join(targetDir, ".env.local"), "utf-8")
|
||||
expect(copiedContent).toBe("LOCAL_VAR=value")
|
||||
})
|
||||
|
||||
it("should ignore comment lines in pattern files", async () => {
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "# comment\nnode_modules\n# another comment")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toContain("node_modules")
|
||||
})
|
||||
|
||||
it("should call progress callback with bytesCopied progress", async () => {
|
||||
// Set up files to copy
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\n.env.local")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env.local")
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test")
|
||||
await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value")
|
||||
|
||||
const progressCalls: Array<{ bytesCopied: number; itemName: string }> = []
|
||||
const onProgress = vi.fn((progress: { bytesCopied: number; itemName: string }) => {
|
||||
progressCalls.push({ ...progress })
|
||||
})
|
||||
|
||||
await service.copyWorktreeIncludeFiles(sourceDir, targetDir, onProgress)
|
||||
|
||||
// Should be called multiple times (initial + after each copy)
|
||||
expect(onProgress).toHaveBeenCalled()
|
||||
|
||||
// bytesCopied should increase over time
|
||||
expect(progressCalls.length).toBeGreaterThan(0)
|
||||
const finalCall = progressCalls[progressCalls.length - 1]
|
||||
expect(finalCall?.bytesCopied).toBeGreaterThan(0)
|
||||
|
||||
// Each call should have an item name
|
||||
expect(progressCalls.every((p) => typeof p.itemName === "string")).toBe(true)
|
||||
})
|
||||
|
||||
it("should not fail when progress callback is not provided", async () => {
|
||||
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
|
||||
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
|
||||
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
|
||||
|
||||
// Should not throw when no callback is provided
|
||||
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
|
||||
|
||||
expect(result).toContain("node_modules")
|
||||
})
|
||||
})
|
||||
})
|
||||
146
packages/core/src/worktree/__tests__/worktree-service.spec.ts
Normal file
146
packages/core/src/worktree/__tests__/worktree-service.spec.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import * as path from "path"
|
||||
|
||||
import { WorktreeService } from "../worktree-service.js"
|
||||
|
||||
describe("WorktreeService", () => {
|
||||
describe("normalizePath", () => {
|
||||
let service: WorktreeService
|
||||
|
||||
beforeEach(() => {
|
||||
service = new WorktreeService()
|
||||
})
|
||||
|
||||
// Access private method for testing
|
||||
const callNormalizePath = (service: WorktreeService, p: string): string => {
|
||||
// @ts-expect-error - accessing private method for testing
|
||||
return service.normalizePath(p)
|
||||
}
|
||||
|
||||
it("should normalize paths with trailing slashes", () => {
|
||||
const result = callNormalizePath(service, "/home/user/project/")
|
||||
expect(result).toBe(path.normalize("/home/user/project"))
|
||||
})
|
||||
|
||||
it("should normalize paths with multiple trailing slashes", () => {
|
||||
const result = callNormalizePath(service, "/home/user/project///")
|
||||
// path.normalize already handles multiple slashes
|
||||
expect(result).toBe(path.normalize("/home/user/project"))
|
||||
})
|
||||
|
||||
it("should preserve root path /", () => {
|
||||
// This is a critical test - the old regex would turn "/" into ""
|
||||
// On Windows, path.normalize("/") returns "\", on Unix it returns "/"
|
||||
const result = callNormalizePath(service, "/")
|
||||
expect(result).toBe(path.sep)
|
||||
})
|
||||
|
||||
it("should handle paths without trailing slashes", () => {
|
||||
const result = callNormalizePath(service, "/home/user/project")
|
||||
expect(result).toBe(path.normalize("/home/user/project"))
|
||||
})
|
||||
|
||||
it("should handle relative paths", () => {
|
||||
const result = callNormalizePath(service, "./some/path/")
|
||||
expect(result).toBe(path.normalize("./some/path"))
|
||||
})
|
||||
|
||||
it("should handle empty string", () => {
|
||||
const result = callNormalizePath(service, "")
|
||||
expect(result).toBe(".")
|
||||
})
|
||||
|
||||
it("should handle Windows-style paths on non-Windows", () => {
|
||||
// path.normalize will convert separators appropriately
|
||||
const result = callNormalizePath(service, "C:\\Users\\test\\project")
|
||||
// On Unix, this stays as-is; on Windows it would normalize
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseWorktreeOutput", () => {
|
||||
let service: WorktreeService
|
||||
|
||||
beforeEach(() => {
|
||||
service = new WorktreeService()
|
||||
})
|
||||
|
||||
// Access private method for testing
|
||||
const callParseWorktreeOutput = (
|
||||
service: WorktreeService,
|
||||
output: string,
|
||||
currentCwd: string,
|
||||
): ReturnType<WorktreeService["parseWorktreeOutput"]> => {
|
||||
// @ts-expect-error - accessing private method for testing
|
||||
return service.parseWorktreeOutput(output, currentCwd)
|
||||
}
|
||||
|
||||
it("should parse porcelain output correctly", () => {
|
||||
const output = `worktree /home/user/repo
|
||||
HEAD abc123def456
|
||||
branch refs/heads/main
|
||||
|
||||
worktree /home/user/repo-feature
|
||||
HEAD def456abc123
|
||||
branch refs/heads/feature/test
|
||||
`
|
||||
const result = callParseWorktreeOutput(service, output, "/home/user/repo")
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toMatchObject({
|
||||
path: "/home/user/repo",
|
||||
branch: "main",
|
||||
commitHash: "abc123def456",
|
||||
isCurrent: true,
|
||||
})
|
||||
expect(result[1]).toMatchObject({
|
||||
path: "/home/user/repo-feature",
|
||||
branch: "feature/test",
|
||||
commitHash: "def456abc123",
|
||||
isCurrent: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle detached HEAD worktrees", () => {
|
||||
const output = `worktree /home/user/repo-detached
|
||||
HEAD abc123def456
|
||||
detached
|
||||
`
|
||||
const result = callParseWorktreeOutput(service, output, "/home/user/other")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toMatchObject({
|
||||
path: "/home/user/repo-detached",
|
||||
isDetached: true,
|
||||
branch: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle locked worktrees", () => {
|
||||
const output = `worktree /home/user/repo-locked
|
||||
HEAD abc123def456
|
||||
branch refs/heads/locked-branch
|
||||
locked some reason here
|
||||
`
|
||||
const result = callParseWorktreeOutput(service, output, "/home/user/other")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toMatchObject({
|
||||
isLocked: true,
|
||||
lockReason: "some reason here",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle bare worktrees", () => {
|
||||
const output = `worktree /home/user/repo.git
|
||||
bare
|
||||
`
|
||||
const result = callParseWorktreeOutput(service, output, "/home/user/other")
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toMatchObject({
|
||||
path: "/home/user/repo.git",
|
||||
isBare: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
13
packages/core/src/worktree/index.ts
Normal file
13
packages/core/src/worktree/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Worktree Module
|
||||
*
|
||||
* Platform-agnostic git worktree management functionality.
|
||||
* These exports are decoupled from VSCode and can be used by any consumer.
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from "./types.js"
|
||||
|
||||
// Services
|
||||
export { WorktreeService, worktreeService } from "./worktree-service.js"
|
||||
export { WorktreeIncludeService, worktreeIncludeService, type CopyProgressCallback } from "./worktree-include.js"
|
||||
15
packages/core/src/worktree/types.ts
Normal file
15
packages/core/src/worktree/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Worktree Types
|
||||
*
|
||||
* Re-exports platform-agnostic type definitions from @roo-code/types.
|
||||
*/
|
||||
|
||||
export type {
|
||||
Worktree,
|
||||
WorktreeResult,
|
||||
BranchInfo,
|
||||
CreateWorktreeOptions,
|
||||
WorktreeIncludeStatus,
|
||||
WorktreeListResponse,
|
||||
WorktreeDefaultsResponse,
|
||||
} from "@roo-code/types"
|
||||
428
packages/core/src/worktree/worktree-include.ts
Normal file
428
packages/core/src/worktree/worktree-include.ts
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
/**
|
||||
* WorktreeIncludeService
|
||||
*
|
||||
* Platform-agnostic service for handling .worktreeinclude files.
|
||||
* Used to copy untracked files (like node_modules) when creating worktrees.
|
||||
*/
|
||||
|
||||
import { execFile, spawn } from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { promisify } from "util"
|
||||
|
||||
import ignore, { type Ignore } from "ignore"
|
||||
|
||||
import type { WorktreeIncludeStatus } from "./types.js"
|
||||
|
||||
/**
|
||||
* Progress info for copy tracking.
|
||||
* Shows activity without trying to predict total size (which is inaccurate).
|
||||
*/
|
||||
export interface CopyProgress {
|
||||
/** Current bytes copied */
|
||||
bytesCopied: number
|
||||
/** Name of current item being copied */
|
||||
itemName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for reporting copy progress during worktree file copying.
|
||||
*/
|
||||
export type CopyProgressCallback = (progress: CopyProgress) => void
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
/**
|
||||
* Service for managing .worktreeinclude files and copying files to new worktrees.
|
||||
* All methods are platform-agnostic and don't depend on VSCode APIs.
|
||||
*/
|
||||
export class WorktreeIncludeService {
|
||||
/**
|
||||
* Check if .worktreeinclude exists in a directory
|
||||
*/
|
||||
async hasWorktreeInclude(dir: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path.join(dir, ".worktreeinclude"))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific branch has .worktreeinclude file (in git, not local filesystem)
|
||||
* @param cwd - Current working directory (git repo)
|
||||
* @param branch - Branch name to check
|
||||
*/
|
||||
async branchHasWorktreeInclude(cwd: string, branch: string): Promise<boolean> {
|
||||
try {
|
||||
const ref = `${branch}:.worktreeinclude`
|
||||
// Use git cat-file -e to check if the file exists on the branch (without printing contents)
|
||||
await execFileAsync("git", ["cat-file", "-e", "--", ref], { cwd })
|
||||
return true
|
||||
} catch {
|
||||
// File doesn't exist on this branch
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of .worktreeinclude and .gitignore
|
||||
*/
|
||||
async getStatus(dir: string): Promise<WorktreeIncludeStatus> {
|
||||
const worktreeIncludePath = path.join(dir, ".worktreeinclude")
|
||||
const gitignorePath = path.join(dir, ".gitignore")
|
||||
|
||||
let exists = false
|
||||
let hasGitignore = false
|
||||
let gitignoreContent: string | undefined
|
||||
|
||||
try {
|
||||
await fs.access(worktreeIncludePath)
|
||||
exists = true
|
||||
} catch {
|
||||
exists = false
|
||||
}
|
||||
|
||||
try {
|
||||
gitignoreContent = await fs.readFile(gitignorePath, "utf-8")
|
||||
hasGitignore = true
|
||||
} catch {
|
||||
hasGitignore = false
|
||||
}
|
||||
|
||||
return {
|
||||
exists,
|
||||
hasGitignore,
|
||||
gitignoreContent,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a .worktreeinclude file with the specified content
|
||||
*/
|
||||
async createWorktreeInclude(dir: string, content: string): Promise<void> {
|
||||
await fs.writeFile(path.join(dir, ".worktreeinclude"), content, "utf-8")
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy files matching .worktreeinclude patterns from source to target.
|
||||
* Only copies files that are ALSO in .gitignore (to avoid copying tracked files).
|
||||
*
|
||||
* @param sourceDir - The source directory containing the files to copy
|
||||
* @param targetDir - The target directory where files will be copied
|
||||
* @param onProgress - Optional callback to report copy progress (size-based)
|
||||
* @returns Array of copied file/directory paths
|
||||
*/
|
||||
async copyWorktreeIncludeFiles(
|
||||
sourceDir: string,
|
||||
targetDir: string,
|
||||
onProgress?: CopyProgressCallback,
|
||||
): Promise<string[]> {
|
||||
const worktreeIncludePath = path.join(sourceDir, ".worktreeinclude")
|
||||
const gitignorePath = path.join(sourceDir, ".gitignore")
|
||||
|
||||
// Check if both files exist
|
||||
let hasWorktreeInclude = false
|
||||
let hasGitignore = false
|
||||
|
||||
try {
|
||||
await fs.access(worktreeIncludePath)
|
||||
hasWorktreeInclude = true
|
||||
} catch {
|
||||
hasWorktreeInclude = false
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(gitignorePath)
|
||||
hasGitignore = true
|
||||
} catch {
|
||||
hasGitignore = false
|
||||
}
|
||||
|
||||
if (!hasWorktreeInclude || !hasGitignore) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Parse both files
|
||||
const worktreeIncludePatterns = await this.parseIgnoreFile(worktreeIncludePath)
|
||||
const gitignorePatterns = await this.parseIgnoreFile(gitignorePath)
|
||||
|
||||
if (worktreeIncludePatterns.length === 0 || gitignorePatterns.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Create ignore matchers
|
||||
const worktreeIncludeMatcher = ignore().add(worktreeIncludePatterns)
|
||||
const gitignoreMatcher = ignore().add(gitignorePatterns)
|
||||
|
||||
// Find items that match BOTH patterns (intersection)
|
||||
const itemsToCopy = await this.findMatchingItems(sourceDir, worktreeIncludeMatcher, gitignoreMatcher)
|
||||
|
||||
if (itemsToCopy.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
let bytesCopied = 0
|
||||
|
||||
// Report initial progress
|
||||
if (onProgress && itemsToCopy.length > 0) {
|
||||
onProgress({ bytesCopied: 0, itemName: itemsToCopy[0]! })
|
||||
}
|
||||
|
||||
// Copy the items with progress tracking (no total size calculation)
|
||||
const copiedItems: string[] = []
|
||||
for (const item of itemsToCopy) {
|
||||
const sourcePath = path.join(sourceDir, item)
|
||||
const targetPath = path.join(targetDir, item)
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(sourcePath)
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
// Copy directory with progress tracking
|
||||
bytesCopied = await this.copyDirectoryWithProgress(
|
||||
sourcePath,
|
||||
targetPath,
|
||||
item,
|
||||
bytesCopied,
|
||||
onProgress,
|
||||
)
|
||||
} else {
|
||||
// Report progress before copying
|
||||
onProgress?.({ bytesCopied, itemName: item })
|
||||
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
||||
await fs.copyFile(sourcePath, targetPath)
|
||||
|
||||
// Update bytes copied
|
||||
bytesCopied += this.getSizeOnDisk(stats)
|
||||
}
|
||||
|
||||
copiedItems.push(item)
|
||||
|
||||
// Report progress after copying
|
||||
onProgress?.({ bytesCopied, itemName: item })
|
||||
} catch (error) {
|
||||
// Log but don't fail on individual copy errors
|
||||
console.error(`Failed to copy ${item}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return copiedItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size on disk of a file (accounts for filesystem block allocation).
|
||||
* Uses blksize to calculate actual disk usage including block overhead.
|
||||
*/
|
||||
private getSizeOnDisk(stats: { size: number; blksize?: number }): number {
|
||||
// Calculate size on disk using filesystem block size
|
||||
if (stats.blksize !== undefined && stats.blksize > 0) {
|
||||
return stats.blksize * Math.ceil(stats.size / stats.blksize)
|
||||
}
|
||||
// Fallback to logical size when blksize not available
|
||||
return stats.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total size on disk of a file or directory (recursively).
|
||||
* Uses native Node.js fs operations for cross-platform compatibility.
|
||||
*/
|
||||
private async getPathSize(targetPath: string): Promise<number> {
|
||||
try {
|
||||
const stats = await fs.stat(targetPath)
|
||||
|
||||
if (stats.isFile()) {
|
||||
return this.getSizeOnDisk(stats)
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return await this.getDirectorySizeRecursive(targetPath)
|
||||
}
|
||||
|
||||
return 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively calculate directory size on disk using Node.js fs.
|
||||
* Uses parallel processing for better performance on large directories.
|
||||
*/
|
||||
private async getDirectorySizeRecursive(dirPath: string): Promise<number> {
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
const sizes = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(dirPath, entry.name)
|
||||
try {
|
||||
if (entry.isFile()) {
|
||||
const stats = await fs.stat(entryPath)
|
||||
return this.getSizeOnDisk(stats)
|
||||
} else if (entry.isDirectory()) {
|
||||
return await this.getDirectorySizeRecursive(entryPath)
|
||||
}
|
||||
return 0
|
||||
} catch {
|
||||
return 0 // Skip inaccessible files
|
||||
}
|
||||
}),
|
||||
)
|
||||
return sizes.reduce((sum, size) => sum + size, 0)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current size of a directory (for progress tracking).
|
||||
*/
|
||||
private async getCurrentDirectorySize(dirPath: string): Promise<number> {
|
||||
try {
|
||||
await fs.access(dirPath)
|
||||
return await this.getDirectorySizeRecursive(dirPath)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory with progress polling using native cp command.
|
||||
* Starts native copy and polls target directory size to report progress.
|
||||
* Returns the updated bytesCopied count.
|
||||
*/
|
||||
private async copyDirectoryWithProgress(
|
||||
source: string,
|
||||
target: string,
|
||||
itemName: string,
|
||||
bytesCopiedBefore: number,
|
||||
onProgress?: CopyProgressCallback,
|
||||
): Promise<number> {
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
|
||||
// Start the copy process
|
||||
const copyPromise = new Promise<void>((resolve, reject) => {
|
||||
let proc: ReturnType<typeof spawn>
|
||||
|
||||
if (isWindows) {
|
||||
proc = spawn("robocopy", [source, target, "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP"], {
|
||||
windowsHide: true,
|
||||
})
|
||||
} else {
|
||||
proc = spawn("cp", ["-r", "--", source, target])
|
||||
}
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (isWindows) {
|
||||
// robocopy returns non-zero for success (values < 8)
|
||||
if (code !== null && code < 8) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`robocopy failed with code ${code}`))
|
||||
}
|
||||
} else {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`cp failed with code ${code}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
proc.on("error", reject)
|
||||
})
|
||||
|
||||
// Poll progress while copying
|
||||
const pollInterval = 500 // Poll every 500ms
|
||||
let polling = true
|
||||
|
||||
const pollProgress = async () => {
|
||||
while (polling) {
|
||||
const currentSize = await this.getCurrentDirectorySize(target)
|
||||
const totalCopied = bytesCopiedBefore + currentSize
|
||||
|
||||
onProgress?.({
|
||||
bytesCopied: totalCopied,
|
||||
itemName,
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval))
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling and wait for copy to complete
|
||||
const pollPromise = pollProgress()
|
||||
|
||||
try {
|
||||
await copyPromise
|
||||
} finally {
|
||||
polling = false
|
||||
// Wait for final poll iteration to complete
|
||||
await pollPromise.catch(() => {})
|
||||
}
|
||||
|
||||
// Get the final size of the copied directory
|
||||
const finalSize = await this.getPathSize(target)
|
||||
return bytesCopiedBefore + finalSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .gitignore-style file and return the patterns
|
||||
*/
|
||||
private async parseIgnoreFile(filePath: string): Promise<string[]> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
return content
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("#"))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find items in sourceDir that match both matchers
|
||||
*/
|
||||
private async findMatchingItems(
|
||||
sourceDir: string,
|
||||
includeMatcher: Ignore,
|
||||
gitignoreMatcher: Ignore,
|
||||
): Promise<string[]> {
|
||||
const matchingItems: string[] = []
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = entry.name
|
||||
|
||||
// Skip .git directory
|
||||
if (relativePath === ".git") continue
|
||||
|
||||
// Check if this path matches both patterns
|
||||
// For .worktreeinclude, we want items that are "ignored" (matched)
|
||||
// For .gitignore, we want items that are "ignored" (matched)
|
||||
const matchesWorktreeInclude = includeMatcher.ignores(relativePath)
|
||||
const matchesGitignore = gitignoreMatcher.ignores(relativePath)
|
||||
|
||||
if (matchesWorktreeInclude && matchesGitignore) {
|
||||
matchingItems.push(relativePath)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
return matchingItems
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for convenience
|
||||
export const worktreeIncludeService = new WorktreeIncludeService()
|
||||
315
packages/core/src/worktree/worktree-service.ts
Normal file
315
packages/core/src/worktree/worktree-service.ts
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* WorktreeService
|
||||
*
|
||||
* Platform-agnostic service for git worktree operations.
|
||||
* Uses simple-git and native CLI commands - no VSCode dependencies.
|
||||
*/
|
||||
|
||||
import { exec, execFile } from "child_process"
|
||||
import * as path from "path"
|
||||
import { promisify } from "util"
|
||||
|
||||
import type { BranchInfo, CreateWorktreeOptions, Worktree, WorktreeResult } from "./types.js"
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
/**
|
||||
* Service for managing git worktrees.
|
||||
* All methods are platform-agnostic and don't depend on VSCode APIs.
|
||||
*/
|
||||
export class WorktreeService {
|
||||
/**
|
||||
* Check if git is installed on the system
|
||||
*/
|
||||
async checkGitInstalled(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git --version")
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory is a git repository.
|
||||
*/
|
||||
async checkGitRepo(cwd: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --git-dir", { cwd })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the git repository root path.
|
||||
*/
|
||||
async getGitRootPath(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd })
|
||||
return stdout.trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current worktree path.
|
||||
*/
|
||||
async getCurrentWorktreePath(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd })
|
||||
return stdout.trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current branch name.
|
||||
*/
|
||||
async getCurrentBranch(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd })
|
||||
const branch = stdout.trim()
|
||||
return branch === "HEAD" ? null : branch
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all worktrees in the repository
|
||||
*/
|
||||
async listWorktrees(cwd: string): Promise<Worktree[]> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git worktree list --porcelain", { cwd })
|
||||
return this.parseWorktreeOutput(stdout, cwd)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new worktree
|
||||
*/
|
||||
async createWorktree(cwd: string, options: CreateWorktreeOptions): Promise<WorktreeResult> {
|
||||
try {
|
||||
const { path: worktreePath, branch, baseBranch, createNewBranch } = options
|
||||
|
||||
// Build the git worktree add command arguments
|
||||
const args: string[] = ["worktree", "add"]
|
||||
|
||||
if (createNewBranch && branch) {
|
||||
// Create new branch: git worktree add -b <branch> <path> [<base>]
|
||||
args.push("-b", branch, worktreePath)
|
||||
if (baseBranch) {
|
||||
args.push(baseBranch)
|
||||
}
|
||||
} else if (branch) {
|
||||
// Checkout existing branch: git worktree add <path> <branch>
|
||||
args.push(worktreePath, branch)
|
||||
} else {
|
||||
// Detached HEAD at current commit
|
||||
args.push("--detach", worktreePath)
|
||||
}
|
||||
|
||||
await execFileAsync("git", args, { cwd })
|
||||
|
||||
// Get the created worktree info
|
||||
const worktrees = await this.listWorktrees(cwd)
|
||||
const createdWorktree = worktrees.find(
|
||||
(wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath),
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Worktree created at ${worktreePath}`,
|
||||
worktree: createdWorktree,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to create worktree: ${errorMessage}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a worktree
|
||||
*/
|
||||
async deleteWorktree(cwd: string, worktreePath: string, force = false): Promise<WorktreeResult> {
|
||||
try {
|
||||
// Get worktree info BEFORE deletion to capture the branch name
|
||||
const worktrees = await this.listWorktrees(cwd)
|
||||
const worktreeToDelete = worktrees.find(
|
||||
(wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath),
|
||||
)
|
||||
|
||||
const args = ["worktree", "remove"]
|
||||
if (force) {
|
||||
args.push("--force")
|
||||
}
|
||||
args.push(worktreePath)
|
||||
await execFileAsync("git", args, { cwd })
|
||||
|
||||
// Also try to delete the branch if it exists
|
||||
if (worktreeToDelete?.branch) {
|
||||
try {
|
||||
await execFileAsync("git", ["branch", "-d", worktreeToDelete.branch], { cwd })
|
||||
} catch {
|
||||
// Branch deletion is best-effort
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Worktree removed from ${worktreePath}`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to delete worktree: ${errorMessage}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available branches
|
||||
* @param cwd - Current working directory
|
||||
* @param includeWorktreeBranches - If true, include branches already checked out in worktrees (useful for base branch selection)
|
||||
*/
|
||||
async getAvailableBranches(cwd: string, includeWorktreeBranches = false): Promise<BranchInfo> {
|
||||
try {
|
||||
// Run all git commands in parallel for better performance
|
||||
const [worktrees, localResult, remoteResult, currentBranch] = await Promise.all([
|
||||
this.listWorktrees(cwd),
|
||||
execAsync('git branch --format="%(refname:short)"', { cwd }),
|
||||
execAsync('git branch -r --format="%(refname:short)"', { cwd }),
|
||||
this.getCurrentBranch(cwd),
|
||||
])
|
||||
|
||||
const branchesInWorktrees = new Set(worktrees.map((wt) => wt.branch).filter(Boolean))
|
||||
|
||||
// Filter local branches
|
||||
const localBranches = localResult.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((b) => b && (includeWorktreeBranches || !branchesInWorktrees.has(b)))
|
||||
|
||||
// Filter remote branches
|
||||
const remoteBranches = remoteResult.stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(
|
||||
(b) =>
|
||||
b &&
|
||||
!b.includes("HEAD") &&
|
||||
(includeWorktreeBranches || !branchesInWorktrees.has(b.replace(/^origin\//, ""))),
|
||||
)
|
||||
|
||||
return {
|
||||
localBranches,
|
||||
remoteBranches,
|
||||
currentBranch: currentBranch || "",
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
localBranches: [],
|
||||
remoteBranches: [],
|
||||
currentBranch: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout a branch in the current worktree
|
||||
*/
|
||||
async checkoutBranch(cwd: string, branch: string): Promise<WorktreeResult> {
|
||||
try {
|
||||
await execFileAsync("git", ["checkout", branch], { cwd })
|
||||
return {
|
||||
success: true,
|
||||
message: `Checked out branch ${branch}`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to checkout branch: ${errorMessage}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse git worktree list --porcelain output
|
||||
*/
|
||||
private parseWorktreeOutput(output: string, currentCwd: string): Worktree[] {
|
||||
const worktrees: Worktree[] = []
|
||||
const entries = output.trim().split("\n\n")
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.trim()) continue
|
||||
|
||||
const lines = entry.trim().split("\n")
|
||||
const worktree: Partial<Worktree> = {
|
||||
path: "",
|
||||
branch: "",
|
||||
commitHash: "",
|
||||
isCurrent: false,
|
||||
isBare: false,
|
||||
isDetached: false,
|
||||
isLocked: false,
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
worktree.path = line.substring(9).trim()
|
||||
} else if (line.startsWith("HEAD ")) {
|
||||
worktree.commitHash = line.substring(5).trim()
|
||||
} else if (line.startsWith("branch ")) {
|
||||
// branch refs/heads/main -> main
|
||||
const branchRef = line.substring(7).trim()
|
||||
worktree.branch = branchRef.replace(/^refs\/heads\//, "")
|
||||
} else if (line === "bare") {
|
||||
worktree.isBare = true
|
||||
} else if (line === "detached") {
|
||||
worktree.isDetached = true
|
||||
} else if (line === "locked") {
|
||||
worktree.isLocked = true
|
||||
} else if (line.startsWith("locked ")) {
|
||||
worktree.isLocked = true
|
||||
worktree.lockReason = line.substring(7).trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (worktree.path) {
|
||||
worktree.isCurrent = this.normalizePath(worktree.path) === this.normalizePath(currentCwd)
|
||||
worktrees.push(worktree as Worktree)
|
||||
}
|
||||
}
|
||||
|
||||
return worktrees
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a path for comparison (handle trailing slashes, etc.)
|
||||
*/
|
||||
private normalizePath(p: string): string {
|
||||
// normalize resolves ./.. segments, removes duplicate slashes, and standardizes path separators
|
||||
let normalized = path.normalize(p)
|
||||
// however it doesn't remove trailing slashes
|
||||
// remove trailing slash, except for root paths (handles both / and \)
|
||||
if (normalized.length > 1 && (normalized.endsWith("/") || normalized.endsWith("\\"))) {
|
||||
normalized = normalized.slice(0, -1)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for convenience
|
||||
export const worktreeService = new WorktreeService()
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "node:os"
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ import { mergeToolUsage, waitForSubprocessWithTimeout } from "./utils.js"
|
|||
*/
|
||||
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 promptSourcePath = path.resolve(EVALS_REPO_PATH, `prompts/${language}.md`)
|
||||
const workspacePath = path.resolve(EVALS_REPO_PATH, language, exercise)
|
||||
const ipcSocketPath = path.resolve(os.tmpdir(), `evals-cli-${run.id}-${task.id}.sock`)
|
||||
|
||||
|
|
@ -40,32 +39,31 @@ export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: R
|
|||
"--filter",
|
||||
"@roo-code/cli",
|
||||
"start",
|
||||
"--yes",
|
||||
"--exit-on-complete",
|
||||
"--reasoning-effort",
|
||||
"disabled",
|
||||
"--prompt-file",
|
||||
promptSourcePath,
|
||||
"--workspace",
|
||||
workspacePath,
|
||||
"--yes",
|
||||
"--reasoning-effort",
|
||||
"disabled",
|
||||
"--oneshot",
|
||||
]
|
||||
|
||||
if (run.settings?.mode) {
|
||||
cliArgs.push("-M", run.settings.mode)
|
||||
cliArgs.push("--mode", run.settings.mode)
|
||||
}
|
||||
|
||||
if (run.settings?.apiProvider) {
|
||||
cliArgs.push("-p", run.settings.apiProvider)
|
||||
cliArgs.push("--provider", run.settings.apiProvider)
|
||||
}
|
||||
|
||||
const modelId = run.settings?.apiModelId || run.settings?.openRouterModelId
|
||||
|
||||
if (modelId) {
|
||||
cliArgs.push("-m", modelId)
|
||||
cliArgs.push("--model", 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.
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ export class TelemetryService {
|
|||
this.captureEvent(TelemetryEventName.MODE_SWITCH, { taskId, newMode })
|
||||
}
|
||||
|
||||
public captureToolUsage(taskId: string, tool: string, toolProtocol: string): void {
|
||||
this.captureEvent(TelemetryEventName.TOOL_USED, { taskId, tool, toolProtocol })
|
||||
public captureToolUsage(taskId: string, tool: string): void {
|
||||
this.captureEvent(TelemetryEventName.TOOL_USED, { taskId, tool })
|
||||
}
|
||||
|
||||
public captureCheckpointCreated(taskId: string): void {
|
||||
|
|
@ -127,17 +127,11 @@ export class TelemetryService {
|
|||
this.captureEvent(TelemetryEventName.CHECKPOINT_RESTORED, { taskId })
|
||||
}
|
||||
|
||||
public captureContextCondensed(
|
||||
taskId: string,
|
||||
isAutomaticTrigger: boolean,
|
||||
usedCustomPrompt?: boolean,
|
||||
usedCustomApiHandler?: boolean,
|
||||
): void {
|
||||
public captureContextCondensed(taskId: string, isAutomaticTrigger: boolean, usedCustomPrompt?: boolean): void {
|
||||
this.captureEvent(TelemetryEventName.CONTEXT_CONDENSED, {
|
||||
taskId,
|
||||
isAutomaticTrigger,
|
||||
...(usedCustomPrompt !== undefined && { usedCustomPrompt }),
|
||||
...(usedCustomApiHandler !== undefined && { usedCustomApiHandler }),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,6 @@ describe("getApiProtocol", () => {
|
|||
expect(getApiProtocol("anthropic", "gpt-4")).toBe("anthropic")
|
||||
})
|
||||
|
||||
it("should return 'anthropic' for claude-code provider", () => {
|
||||
expect(getApiProtocol("claude-code")).toBe("anthropic")
|
||||
expect(getApiProtocol("claude-code", "some-model")).toBe("anthropic")
|
||||
})
|
||||
|
||||
it("should return 'anthropic' for bedrock provider", () => {
|
||||
expect(getApiProtocol("bedrock")).toBe("anthropic")
|
||||
expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic")
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ export type OrganizationAllowList = z.infer<typeof organizationAllowListSchema>
|
|||
export const organizationDefaultSettingsSchema = globalSettingsSchema
|
||||
.pick({
|
||||
enableCheckpoints: true,
|
||||
fuzzyMatchThreshold: true,
|
||||
maxOpenTabsContext: true,
|
||||
maxReadFileLine: true,
|
||||
maxWorkspaceFiles: true,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
|
|||
|
||||
export const experimentIds = [
|
||||
"powerSteering",
|
||||
"multiFileApplyDiff",
|
||||
"preventFocusDisruption",
|
||||
"imageGeneration",
|
||||
"runSlashCommand",
|
||||
|
|
@ -26,7 +25,6 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
|
|||
|
||||
export const experimentsSchema = z.object({
|
||||
powerSteering: z.boolean().optional(),
|
||||
multiFileApplyDiff: z.boolean().optional(),
|
||||
preventFocusDisruption: z.boolean().optional(),
|
||||
imageGeneration: z.boolean().optional(),
|
||||
runSlashCommand: z.boolean().optional(),
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ export const globalSettingsSchema = z.object({
|
|||
openRouterImageApiKey: z.string().optional(),
|
||||
openRouterImageGenerationSelectedModel: z.string().optional(),
|
||||
|
||||
condensingApiConfigId: z.string().optional(),
|
||||
customCondensingPrompt: z.string().optional(),
|
||||
|
||||
autoApprovalEnabled: z.boolean().optional(),
|
||||
|
|
@ -163,8 +162,6 @@ export const globalSettingsSchema = z.object({
|
|||
diagnosticsEnabled: z.boolean().optional(),
|
||||
|
||||
rateLimitSeconds: z.number().optional(),
|
||||
diffEnabled: z.boolean().optional(),
|
||||
fuzzyMatchThreshold: z.number().optional(),
|
||||
experiments: experimentsSchema.optional(),
|
||||
|
||||
codebaseIndexModels: codebaseIndexModelsSchema.optional(),
|
||||
|
|
@ -197,6 +194,15 @@ export const globalSettingsSchema = z.object({
|
|||
hasOpenedModeSelector: z.boolean().optional(),
|
||||
lastModeExportPath: z.string().optional(),
|
||||
lastModeImportPath: z.string().optional(),
|
||||
lastSettingsExportPath: z.string().optional(),
|
||||
lastTaskExportPath: z.string().optional(),
|
||||
lastImageSavePath: z.string().optional(),
|
||||
|
||||
/**
|
||||
* Path to worktree to auto-open after switching workspaces.
|
||||
* Used by the worktree feature to open the Roo Code sidebar in a new window.
|
||||
*/
|
||||
worktreeAutoOpenPath: z.string().optional(),
|
||||
})
|
||||
|
||||
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
|
||||
|
|
@ -341,9 +347,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
|
|||
|
||||
diagnosticsEnabled: true,
|
||||
|
||||
diffEnabled: true,
|
||||
fuzzyMatchThreshold: 1,
|
||||
|
||||
enableCheckpoints: false,
|
||||
|
||||
rateLimitSeconds: 0,
|
||||
|
|
|
|||
|
|
@ -19,16 +19,6 @@ export const historyItemSchema = z.object({
|
|||
size: z.number().optional(),
|
||||
workspace: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
/**
|
||||
* The tool protocol used by this task. Once a task uses tools with a specific
|
||||
* protocol (XML or Native), it is permanently locked to that protocol.
|
||||
*
|
||||
* - "xml": Tool calls are parsed from XML text (no tool IDs)
|
||||
* - "native": Tool calls come as tool_call chunks with IDs
|
||||
*
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@ export * from "./tool-params.js"
|
|||
export * from "./type-fu.js"
|
||||
export * from "./vscode-extension-host.js"
|
||||
export * from "./vscode.js"
|
||||
export * from "./worktree.js"
|
||||
|
||||
export * from "./providers/index.js"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Maximum number of MCP tools that can be enabled before showing a warning.
|
||||
* LLMs tend to perform poorly when given too many tools to choose from.
|
||||
*/
|
||||
export const MAX_MCP_TOOLS_THRESHOLD = 60
|
||||
|
||||
/**
|
||||
* McpServerUse
|
||||
*/
|
||||
|
|
@ -128,3 +134,53 @@ export type McpErrorEntry = {
|
|||
timestamp: number
|
||||
level: "error" | "warn" | "info"
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of counting enabled MCP tools across servers.
|
||||
*/
|
||||
export interface EnabledMcpToolsCount {
|
||||
/** Number of enabled and connected MCP servers */
|
||||
enabledServerCount: number
|
||||
/** Total number of enabled tools across all enabled servers */
|
||||
enabledToolCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of enabled MCP tools across all enabled and connected servers.
|
||||
* This is a pure function that can be used in both backend and frontend contexts.
|
||||
*
|
||||
* @param servers - Array of MCP server objects
|
||||
* @returns Object with enabledToolCount and enabledServerCount
|
||||
*
|
||||
* @example
|
||||
* const { enabledToolCount, enabledServerCount } = countEnabledMcpTools(mcpServers)
|
||||
* if (enabledToolCount > MAX_MCP_TOOLS_THRESHOLD) {
|
||||
* // Show warning
|
||||
* }
|
||||
*/
|
||||
export function countEnabledMcpTools(servers: McpServer[]): EnabledMcpToolsCount {
|
||||
let serverCount = 0
|
||||
let toolCount = 0
|
||||
|
||||
for (const server of servers) {
|
||||
// Skip disabled servers
|
||||
if (server.disabled) continue
|
||||
|
||||
// Skip servers that are not connected
|
||||
if (server.status !== "connected") continue
|
||||
|
||||
serverCount++
|
||||
|
||||
// Count enabled tools on this server
|
||||
if (server.tools) {
|
||||
for (const tool of server.tools) {
|
||||
// Tool is enabled if enabledForPrompt is undefined (default) or true
|
||||
if (tool.enabledForPrompt !== false) {
|
||||
toolCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { enabledToolCount: toolCount, enabledServerCount: serverCount }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
|
|||
* - `condense_context`: Context condensation/summarization has started
|
||||
* - `condense_context_error`: Error occurred during context condensation
|
||||
* - `codebase_search_result`: Results from searching the codebase
|
||||
* - `too_many_tools_warning`: Warning that too many MCP tools are enabled, which may confuse the LLM
|
||||
*/
|
||||
export const clineSays = [
|
||||
"error",
|
||||
|
|
@ -180,6 +181,7 @@ export const clineSays = [
|
|||
"sliding_window_truncation",
|
||||
"codebase_search_result",
|
||||
"user_edit_todos",
|
||||
"too_many_tools_warning",
|
||||
] as const
|
||||
|
||||
export const clineSaySchema = z.enum(clineSays)
|
||||
|
|
|
|||
|
|
@ -110,10 +110,6 @@ export const modelInfoSchema = z.object({
|
|||
isStealthModel: z.boolean().optional(),
|
||||
// Flag to indicate if the model is free (no cost)
|
||||
isFree: z.boolean().optional(),
|
||||
// Flag to indicate if the model supports native tool calling (OpenAI-style function calling)
|
||||
supportsNativeTools: z.boolean().optional(),
|
||||
// Default tool protocol preferred by this model (if not specified, falls back to capability/provider defaults)
|
||||
defaultToolProtocol: z.enum(["xml", "native"]).optional(),
|
||||
// Exclude specific native tools from being available (only applies to native protocol)
|
||||
// These tools will be removed from the set of tools available to the model
|
||||
excludedTools: z.array(z.string()).optional(),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
basetenModels,
|
||||
bedrockModels,
|
||||
cerebrasModels,
|
||||
claudeCodeModels,
|
||||
deepSeekModels,
|
||||
doubaoModels,
|
||||
featherlessModels,
|
||||
|
|
@ -123,7 +122,6 @@ export const providerNames = [
|
|||
"bedrock",
|
||||
"baseten",
|
||||
"cerebras",
|
||||
"claude-code",
|
||||
"doubao",
|
||||
"deepseek",
|
||||
"featherless",
|
||||
|
|
@ -170,9 +168,7 @@ export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
|
|||
|
||||
const baseProviderSettingsSchema = z.object({
|
||||
includeMaxTokens: z.boolean().optional(),
|
||||
diffEnabled: z.boolean().optional(),
|
||||
todoListEnabled: z.boolean().optional(),
|
||||
fuzzyMatchThreshold: z.number().optional(),
|
||||
modelTemperature: z.number().nullish(),
|
||||
rateLimitSeconds: z.number().optional(),
|
||||
consecutiveMistakeLimit: z.number().min(0).optional(),
|
||||
|
|
@ -185,9 +181,6 @@ const baseProviderSettingsSchema = z.object({
|
|||
|
||||
// Model verbosity.
|
||||
verbosity: verbosityLevelsSchema.optional(),
|
||||
|
||||
// Tool protocol override for this profile.
|
||||
toolProtocol: z.enum(["xml", "native"]).optional(),
|
||||
})
|
||||
|
||||
// Several of the providers share common model config properties.
|
||||
|
|
@ -202,8 +195,6 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
|
|||
anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
|
||||
})
|
||||
|
||||
const claudeCodeSchema = apiModelIdProviderModelSchema.extend({})
|
||||
|
||||
const openRouterSchema = baseProviderSettingsSchema.extend({
|
||||
openRouterApiKey: z.string().optional(),
|
||||
openRouterModelId: z.string().optional(),
|
||||
|
|
@ -432,7 +423,6 @@ const defaultSchema = z.object({
|
|||
|
||||
export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
|
||||
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
|
||||
claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
|
||||
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
|
||||
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
|
||||
vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
|
||||
|
|
@ -474,7 +464,6 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
|
|||
export const providerSettingsSchema = z.object({
|
||||
apiProvider: providerNamesSchema.optional(),
|
||||
...anthropicSchema.shape,
|
||||
...claudeCodeSchema.shape,
|
||||
...openRouterSchema.shape,
|
||||
...bedrockSchema.shape,
|
||||
...vertexSchema.shape,
|
||||
|
|
@ -563,7 +552,6 @@ export const isTypicalProvider = (key: unknown): key is TypicalProvider =>
|
|||
|
||||
export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
|
||||
anthropic: "apiModelId",
|
||||
"claude-code": "apiModelId",
|
||||
openrouter: "openRouterModelId",
|
||||
bedrock: "apiModelId",
|
||||
vertex: "apiModelId",
|
||||
|
|
@ -603,7 +591,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
|
|||
*/
|
||||
|
||||
// Providers that use Anthropic-style API protocol.
|
||||
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock", "minimax"]
|
||||
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"]
|
||||
|
||||
export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
|
||||
if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {
|
||||
|
|
@ -650,7 +638,6 @@ export const MODELS_BY_PROVIDER: Record<
|
|||
label: "Cerebras",
|
||||
models: Object.keys(cerebrasModels),
|
||||
},
|
||||
"claude-code": { id: "claude-code", label: "Claude Code", models: Object.keys(claudeCodeModels) },
|
||||
deepseek: {
|
||||
id: "deepseek",
|
||||
label: "DeepSeek",
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import { normalizeClaudeCodeModelId } from "../claude-code.js"
|
||||
|
||||
describe("normalizeClaudeCodeModelId", () => {
|
||||
test("should return valid model IDs unchanged", () => {
|
||||
expect(normalizeClaudeCodeModelId("claude-sonnet-4-5")).toBe("claude-sonnet-4-5")
|
||||
expect(normalizeClaudeCodeModelId("claude-opus-4-5")).toBe("claude-opus-4-5")
|
||||
expect(normalizeClaudeCodeModelId("claude-haiku-4-5")).toBe("claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("should normalize sonnet models with date suffix to claude-sonnet-4-5", () => {
|
||||
// Sonnet 4.5 with date
|
||||
expect(normalizeClaudeCodeModelId("claude-sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
|
||||
// Sonnet 4 (legacy)
|
||||
expect(normalizeClaudeCodeModelId("claude-sonnet-4-20250514")).toBe("claude-sonnet-4-5")
|
||||
// Claude 3.7 Sonnet
|
||||
expect(normalizeClaudeCodeModelId("claude-3-7-sonnet-20250219")).toBe("claude-sonnet-4-5")
|
||||
// Claude 3.5 Sonnet
|
||||
expect(normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022")).toBe("claude-sonnet-4-5")
|
||||
})
|
||||
|
||||
test("should normalize opus models with date suffix to claude-opus-4-5", () => {
|
||||
// Opus 4.5 with date
|
||||
expect(normalizeClaudeCodeModelId("claude-opus-4-5-20251101")).toBe("claude-opus-4-5")
|
||||
// Opus 4.1 (legacy)
|
||||
expect(normalizeClaudeCodeModelId("claude-opus-4-1-20250805")).toBe("claude-opus-4-5")
|
||||
// Opus 4 (legacy)
|
||||
expect(normalizeClaudeCodeModelId("claude-opus-4-20250514")).toBe("claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("should normalize haiku models with date suffix to claude-haiku-4-5", () => {
|
||||
// Haiku 4.5 with date
|
||||
expect(normalizeClaudeCodeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5")
|
||||
// Claude 3.5 Haiku
|
||||
expect(normalizeClaudeCodeModelId("claude-3-5-haiku-20241022")).toBe("claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("should handle case-insensitive model family matching", () => {
|
||||
expect(normalizeClaudeCodeModelId("Claude-Sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
|
||||
expect(normalizeClaudeCodeModelId("CLAUDE-OPUS-4-5-20251101")).toBe("claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("should fallback to default for unrecognized models", () => {
|
||||
expect(normalizeClaudeCodeModelId("unknown-model")).toBe("claude-sonnet-4-5")
|
||||
expect(normalizeClaudeCodeModelId("gpt-4")).toBe("claude-sonnet-4-5")
|
||||
})
|
||||
})
|
||||
|
|
@ -11,8 +11,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
|
||||
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
|
||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||
|
|
@ -34,8 +32,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
|
||||
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
|
||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||
|
|
@ -57,8 +53,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 5.0, // $5 per million input tokens
|
||||
outputPrice: 25.0, // $25 per million output tokens
|
||||
cacheWritesPrice: 6.25, // $6.25 per million tokens
|
||||
|
|
@ -70,8 +64,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 15.0, // $15 per million input tokens
|
||||
outputPrice: 75.0, // $75 per million output tokens
|
||||
cacheWritesPrice: 18.75, // $18.75 per million tokens
|
||||
|
|
@ -83,8 +75,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 15.0, // $15 per million input tokens
|
||||
outputPrice: 75.0, // $75 per million output tokens
|
||||
cacheWritesPrice: 18.75, // $18.75 per million tokens
|
||||
|
|
@ -96,8 +86,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0, // $3 per million input tokens
|
||||
outputPrice: 15.0, // $15 per million output tokens
|
||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||
|
|
@ -110,8 +98,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0, // $3 per million input tokens
|
||||
outputPrice: 15.0, // $15 per million output tokens
|
||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||
|
|
@ -122,8 +108,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0, // $3 per million input tokens
|
||||
outputPrice: 15.0, // $15 per million output tokens
|
||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||
|
|
@ -134,8 +118,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 5.0,
|
||||
cacheWritesPrice: 1.25,
|
||||
|
|
@ -146,8 +128,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 15.0,
|
||||
outputPrice: 75.0,
|
||||
cacheWritesPrice: 18.75,
|
||||
|
|
@ -158,8 +138,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 1.25,
|
||||
cacheWritesPrice: 0.3,
|
||||
|
|
@ -170,8 +148,6 @@ export const anthropicModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 5.0,
|
||||
cacheWritesPrice: 1.25,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export const basetenModels = {
|
|||
contextWindow: 262_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -21,7 +20,6 @@ export const basetenModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.2,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -33,7 +31,6 @@ export const basetenModels = {
|
|||
contextWindow: 163_840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 2.55,
|
||||
outputPrice: 5.95,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -45,7 +42,6 @@ export const basetenModels = {
|
|||
contextWindow: 163_840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 2.55,
|
||||
outputPrice: 5.95,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -57,7 +53,6 @@ export const basetenModels = {
|
|||
contextWindow: 163_840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.77,
|
||||
outputPrice: 0.77,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -69,7 +64,6 @@ export const basetenModels = {
|
|||
contextWindow: 163_840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.5,
|
||||
outputPrice: 1.5,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -82,7 +76,6 @@ export const basetenModels = {
|
|||
contextWindow: 163_840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.45,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -95,7 +88,6 @@ export const basetenModels = {
|
|||
contextWindow: 128_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.5,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -107,7 +99,6 @@ export const basetenModels = {
|
|||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.22,
|
||||
outputPrice: 0.8,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -119,7 +110,6 @@ export const basetenModels = {
|
|||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.38,
|
||||
outputPrice: 1.53,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -131,7 +121,6 @@ export const basetenModels = {
|
|||
contextWindow: 262_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ export const bedrockModels = {
|
|||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
@ -34,7 +32,6 @@ export const bedrockModels = {
|
|||
contextWindow: 300_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 3.2,
|
||||
cacheWritesPrice: 0.8, // per million tokens
|
||||
|
|
@ -48,7 +45,6 @@ export const bedrockModels = {
|
|||
contextWindow: 300_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 4.0,
|
||||
cacheWritesPrice: 1.0, // per million tokens
|
||||
|
|
@ -60,7 +56,6 @@ export const bedrockModels = {
|
|||
contextWindow: 300_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.06,
|
||||
outputPrice: 0.24,
|
||||
cacheWritesPrice: 0.06, // per million tokens
|
||||
|
|
@ -74,7 +69,6 @@ export const bedrockModels = {
|
|||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.33,
|
||||
outputPrice: 2.75,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -89,7 +83,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.035,
|
||||
outputPrice: 0.14,
|
||||
cacheWritesPrice: 0.035, // per million tokens
|
||||
|
|
@ -104,8 +97,6 @@ export const bedrockModels = {
|
|||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
@ -120,8 +111,6 @@ export const bedrockModels = {
|
|||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 15.0,
|
||||
outputPrice: 75.0,
|
||||
cacheWritesPrice: 18.75,
|
||||
|
|
@ -136,8 +125,6 @@ export const bedrockModels = {
|
|||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 25.0,
|
||||
cacheWritesPrice: 6.25,
|
||||
|
|
@ -152,8 +139,6 @@ export const bedrockModels = {
|
|||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 15.0,
|
||||
outputPrice: 75.0,
|
||||
cacheWritesPrice: 18.75,
|
||||
|
|
@ -168,8 +153,6 @@ export const bedrockModels = {
|
|||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
@ -183,8 +166,6 @@ export const bedrockModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
@ -198,8 +179,6 @@ export const bedrockModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 4.0,
|
||||
cacheWritesPrice: 1.0,
|
||||
|
|
@ -214,8 +193,6 @@ export const bedrockModels = {
|
|||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 5.0,
|
||||
cacheWritesPrice: 1.25, // 5m cache writes
|
||||
|
|
@ -229,8 +206,6 @@ export const bedrockModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
|
|
@ -239,8 +214,6 @@ export const bedrockModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 15.0,
|
||||
outputPrice: 75.0,
|
||||
},
|
||||
|
|
@ -249,8 +222,6 @@ export const bedrockModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
|
|
@ -259,8 +230,6 @@ export const bedrockModels = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 1.25,
|
||||
},
|
||||
|
|
@ -269,7 +238,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 1.35,
|
||||
outputPrice: 5.4,
|
||||
},
|
||||
|
|
@ -278,7 +246,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.5,
|
||||
outputPrice: 1.5,
|
||||
description: "GPT-OSS 20B - Optimized for low latency and local/specialized use cases",
|
||||
|
|
@ -288,7 +255,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 6.0,
|
||||
description: "GPT-OSS 120B - Production-ready, general-purpose, high-reasoning model",
|
||||
|
|
@ -298,7 +264,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.72,
|
||||
outputPrice: 0.72,
|
||||
description: "Llama 3.3 Instruct (70B)",
|
||||
|
|
@ -308,7 +273,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.72,
|
||||
outputPrice: 0.72,
|
||||
description: "Llama 3.2 Instruct (90B)",
|
||||
|
|
@ -318,7 +282,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.16,
|
||||
outputPrice: 0.16,
|
||||
description: "Llama 3.2 Instruct (11B)",
|
||||
|
|
@ -328,7 +291,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.15,
|
||||
description: "Llama 3.2 Instruct (3B)",
|
||||
|
|
@ -338,7 +300,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.1,
|
||||
description: "Llama 3.2 Instruct (1B)",
|
||||
|
|
@ -348,7 +309,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 2.4,
|
||||
outputPrice: 2.4,
|
||||
description: "Llama 3.1 Instruct (405B)",
|
||||
|
|
@ -358,7 +318,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.72,
|
||||
outputPrice: 0.72,
|
||||
description: "Llama 3.1 Instruct (70B)",
|
||||
|
|
@ -368,7 +327,6 @@ export const bedrockModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.9,
|
||||
outputPrice: 0.9,
|
||||
description: "Llama 3.1 Instruct (70B) (w/ latency optimized inference)",
|
||||
|
|
@ -378,7 +336,6 @@ export const bedrockModels = {
|
|||
contextWindow: 8_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.22,
|
||||
outputPrice: 0.22,
|
||||
description: "Llama 3.1 Instruct (8B)",
|
||||
|
|
@ -388,7 +345,6 @@ export const bedrockModels = {
|
|||
contextWindow: 8_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 2.65,
|
||||
outputPrice: 3.5,
|
||||
},
|
||||
|
|
@ -397,7 +353,6 @@ export const bedrockModels = {
|
|||
contextWindow: 4_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.6,
|
||||
},
|
||||
|
|
@ -406,7 +361,6 @@ export const bedrockModels = {
|
|||
contextWindow: 8_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.2,
|
||||
description: "Amazon Titan Text Lite",
|
||||
|
|
@ -416,7 +370,6 @@ export const bedrockModels = {
|
|||
contextWindow: 8_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.6,
|
||||
description: "Amazon Titan Text Express",
|
||||
|
|
@ -426,8 +379,6 @@ export const bedrockModels = {
|
|||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
preserveReasoning: true,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
|
|
@ -438,8 +389,6 @@ export const bedrockModels = {
|
|||
contextWindow: 196_608,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
preserveReasoning: true,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 1.2,
|
||||
|
|
@ -450,8 +399,6 @@ export const bedrockModels = {
|
|||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 1.2,
|
||||
description: "Qwen3 Next 80B (MoE model with 3B active parameters)",
|
||||
|
|
@ -461,8 +408,6 @@ export const bedrockModels = {
|
|||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.45,
|
||||
outputPrice: 1.8,
|
||||
description: "Qwen3 Coder 480B (MoE model with 35B active parameters)",
|
||||
|
|
|
|||
|
|
@ -6,24 +6,11 @@ export type CerebrasModelId = keyof typeof cerebrasModels
|
|||
export const cerebrasDefaultModelId: CerebrasModelId = "gpt-oss-120b"
|
||||
|
||||
export const cerebrasModels = {
|
||||
"zai-glm-4.6": {
|
||||
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: "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",
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
@ -34,8 +21,6 @@ export const cerebrasModels = {
|
|||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Intelligent model with ~1400 tokens/s",
|
||||
|
|
@ -45,8 +30,6 @@ export const cerebrasModels = {
|
|||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Powerful model with ~2600 tokens/s",
|
||||
|
|
@ -56,8 +39,6 @@ export const cerebrasModels = {
|
|||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "SOTA coding performance with ~2500 tokens/s",
|
||||
|
|
@ -67,8 +48,6 @@ export const cerebrasModels = {
|
|||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -51,8 +51,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek R1 0528 model.",
|
||||
|
|
@ -62,8 +60,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek R1 model.",
|
||||
|
|
@ -73,8 +69,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek V3 model.",
|
||||
|
|
@ -84,8 +78,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek V3.1 model.",
|
||||
|
|
@ -95,8 +87,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.23,
|
||||
outputPrice: 0.9,
|
||||
description:
|
||||
|
|
@ -107,8 +97,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 3.0,
|
||||
description:
|
||||
|
|
@ -119,8 +107,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 0.35,
|
||||
description:
|
||||
|
|
@ -131,8 +117,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072, // From Groq
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Unsloth Llama 3.3 70B Instruct model.",
|
||||
|
|
@ -142,8 +126,6 @@ export const chutesModels = {
|
|||
contextWindow: 512000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "ChutesAI Llama 4 Scout 17B Instruct model, 512K context.",
|
||||
|
|
@ -153,8 +135,6 @@ export const chutesModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Unsloth Mistral Nemo Instruct model.",
|
||||
|
|
@ -164,8 +144,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Unsloth Gemma 3 12B IT model.",
|
||||
|
|
@ -175,8 +153,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Nous DeepHermes 3 Llama 3 8B Preview model.",
|
||||
|
|
@ -186,8 +162,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Unsloth Gemma 3 4B IT model.",
|
||||
|
|
@ -197,8 +171,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Nvidia Llama 3.3 Nemotron Super 49B model.",
|
||||
|
|
@ -208,8 +180,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Nvidia Llama 3.1 Nemotron Ultra 253B model.",
|
||||
|
|
@ -219,8 +189,6 @@ export const chutesModels = {
|
|||
contextWindow: 256000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "ChutesAI Llama 4 Maverick 17B Instruct FP8 model.",
|
||||
|
|
@ -230,8 +198,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek V3 Base model.",
|
||||
|
|
@ -241,8 +207,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek R1 Zero model.",
|
||||
|
|
@ -252,8 +216,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek V3 (0324) model.",
|
||||
|
|
@ -263,8 +225,6 @@ export const chutesModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 235B A22B Instruct 2507 model with 262K context window.",
|
||||
|
|
@ -274,8 +234,6 @@ export const chutesModels = {
|
|||
contextWindow: 40960,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 235B A22B model.",
|
||||
|
|
@ -285,8 +243,6 @@ export const chutesModels = {
|
|||
contextWindow: 40960,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 32B model.",
|
||||
|
|
@ -296,8 +252,6 @@ export const chutesModels = {
|
|||
contextWindow: 40960,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 30B A3B model.",
|
||||
|
|
@ -307,8 +261,6 @@ export const chutesModels = {
|
|||
contextWindow: 40960,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 14B model.",
|
||||
|
|
@ -318,8 +270,6 @@ export const chutesModels = {
|
|||
contextWindow: 40960,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 8B model.",
|
||||
|
|
@ -329,8 +279,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Microsoft MAI-DS-R1 FP8 model.",
|
||||
|
|
@ -340,8 +288,6 @@ export const chutesModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "TNGTech DeepSeek R1T Chimera model.",
|
||||
|
|
@ -351,8 +297,6 @@ export const chutesModels = {
|
|||
contextWindow: 151329,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
@ -363,8 +307,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
@ -375,8 +317,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 1,
|
||||
outputPrice: 3,
|
||||
description: "GLM-4.5-turbo model with 128K token context window, optimized for fast inference.",
|
||||
|
|
@ -386,8 +326,6 @@ export const chutesModels = {
|
|||
contextWindow: 202752,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
@ -398,8 +336,6 @@ export const chutesModels = {
|
|||
contextWindow: 202752,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 1.15,
|
||||
outputPrice: 3.25,
|
||||
description: "GLM-4.6-turbo model with 200K-token context window, optimized for fast inference.",
|
||||
|
|
@ -409,8 +345,6 @@ export const chutesModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
@ -421,8 +355,6 @@ export const chutesModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 Coder 480B A35B Instruct FP8 model, optimized for coding tasks.",
|
||||
|
|
@ -432,8 +364,6 @@ export const chutesModels = {
|
|||
contextWindow: 75000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.1481,
|
||||
outputPrice: 0.5926,
|
||||
description: "Moonshot AI Kimi K2 Instruct model with 75k context window.",
|
||||
|
|
@ -443,8 +373,6 @@ export const chutesModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.1999,
|
||||
outputPrice: 0.8001,
|
||||
description: "Moonshot AI Kimi K2 Instruct 0905 model with 256k context window.",
|
||||
|
|
@ -454,8 +382,6 @@ export const chutesModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.077968332,
|
||||
outputPrice: 0.31202496,
|
||||
description: "Qwen3 235B A22B Thinking 2507 model with 262K context window.",
|
||||
|
|
@ -465,8 +391,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
@ -477,8 +401,6 @@ export const chutesModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
|
|
@ -489,8 +411,6 @@ export const chutesModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.16,
|
||||
outputPrice: 0.65,
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -1,160 +0,0 @@
|
|||
import type { ModelInfo } from "../model.js"
|
||||
|
||||
/**
|
||||
* Rate limit information from Claude Code API
|
||||
*/
|
||||
export interface ClaudeCodeRateLimitInfo {
|
||||
// 5-hour limit info
|
||||
fiveHour: {
|
||||
status: string
|
||||
utilization: number
|
||||
resetTime: number // Unix timestamp
|
||||
}
|
||||
// 7-day (weekly) limit info (Sonnet-specific)
|
||||
weekly?: {
|
||||
status: string
|
||||
utilization: number
|
||||
resetTime: number // Unix timestamp
|
||||
}
|
||||
// 7-day unified limit info
|
||||
weeklyUnified?: {
|
||||
status: string
|
||||
utilization: number
|
||||
resetTime: number // Unix timestamp
|
||||
}
|
||||
// Representative claim type
|
||||
representativeClaim?: string
|
||||
// Overage status
|
||||
overage?: {
|
||||
status: string
|
||||
disabledReason?: string
|
||||
}
|
||||
// Fallback percentage
|
||||
fallbackPercentage?: number
|
||||
// Organization ID
|
||||
organizationId?: string
|
||||
// Timestamp when this was fetched
|
||||
fetchedAt: number
|
||||
}
|
||||
|
||||
// Regex pattern to strip date suffix from model names
|
||||
const DATE_SUFFIX_PATTERN = /-\d{8}$/
|
||||
|
||||
// Models that work with Claude Code OAuth tokens
|
||||
// See: https://docs.anthropic.com/en/docs/claude-code
|
||||
// NOTE: Claude Code is subscription-based with no per-token cost - pricing fields are 0
|
||||
export const claudeCodeModels = {
|
||||
"claude-haiku-4-5": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsReasoningEffort: ["disable", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
description: "Claude Haiku 4.5 - Fast and efficient with thinking",
|
||||
},
|
||||
"claude-sonnet-4-5": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsReasoningEffort: ["disable", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
description: "Claude Sonnet 4.5 - Balanced performance with thinking",
|
||||
},
|
||||
"claude-opus-4-5": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsReasoningEffort: ["disable", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
description: "Claude Opus 4.5 - Most capable with thinking",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Claude Code - Only models that work with Claude Code OAuth tokens
|
||||
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
|
||||
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-5"
|
||||
|
||||
/**
|
||||
* Model family patterns for normalization.
|
||||
* Maps regex patterns to their canonical Claude Code model IDs.
|
||||
*
|
||||
* Order matters - more specific patterns should come first.
|
||||
*/
|
||||
const MODEL_FAMILY_PATTERNS: Array<{ pattern: RegExp; target: ClaudeCodeModelId }> = [
|
||||
// Opus models (any version) → claude-opus-4-5
|
||||
{ pattern: /opus/i, target: "claude-opus-4-5" },
|
||||
// Haiku models (any version) → claude-haiku-4-5
|
||||
{ pattern: /haiku/i, target: "claude-haiku-4-5" },
|
||||
// Sonnet models (any version) → claude-sonnet-4-5
|
||||
{ pattern: /sonnet/i, target: "claude-sonnet-4-5" },
|
||||
]
|
||||
|
||||
/**
|
||||
* Normalizes a Claude model ID to a valid Claude Code model ID.
|
||||
*
|
||||
* This function handles backward compatibility for legacy model names
|
||||
* that may include version numbers or date suffixes. It maps:
|
||||
* - claude-sonnet-4-5-20250929, claude-sonnet-4-20250514, claude-3-7-sonnet-20250219, claude-3-5-sonnet-20241022 → claude-sonnet-4-5
|
||||
* - claude-opus-4-5-20251101, claude-opus-4-1-20250805, claude-opus-4-20250514 → claude-opus-4-5
|
||||
* - claude-haiku-4-5-20251001, claude-3-5-haiku-20241022 → claude-haiku-4-5
|
||||
*
|
||||
* @param modelId - The model ID to normalize (may be a legacy format)
|
||||
* @returns A valid ClaudeCodeModelId, or the original ID if already valid
|
||||
*
|
||||
* @example
|
||||
* normalizeClaudeCodeModelId("claude-sonnet-4-5") // returns "claude-sonnet-4-5"
|
||||
* normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022") // returns "claude-sonnet-4-5"
|
||||
* normalizeClaudeCodeModelId("claude-opus-4-1-20250805") // returns "claude-opus-4-5"
|
||||
*/
|
||||
export function normalizeClaudeCodeModelId(modelId: string): ClaudeCodeModelId {
|
||||
// If already a valid model ID, return as-is
|
||||
// Use Object.hasOwn() instead of 'in' operator to avoid matching inherited properties like 'toString'
|
||||
if (Object.hasOwn(claudeCodeModels, modelId)) {
|
||||
return modelId as ClaudeCodeModelId
|
||||
}
|
||||
|
||||
// Strip date suffix if present (e.g., -20250514)
|
||||
const withoutDate = modelId.replace(DATE_SUFFIX_PATTERN, "")
|
||||
|
||||
// Check if stripping the date makes it valid
|
||||
if (Object.hasOwn(claudeCodeModels, withoutDate)) {
|
||||
return withoutDate as ClaudeCodeModelId
|
||||
}
|
||||
|
||||
// Match by model family
|
||||
for (const { pattern, target } of MODEL_FAMILY_PATTERNS) {
|
||||
if (pattern.test(modelId)) {
|
||||
return target
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default if no match (shouldn't happen with valid Claude models)
|
||||
return claudeCodeDefaultModelId
|
||||
}
|
||||
|
||||
/**
|
||||
* Reasoning effort configuration for Claude Code thinking mode.
|
||||
* Maps reasoning effort level to budget_tokens for the thinking process.
|
||||
*
|
||||
* Note: With interleaved thinking (enabled via beta header), budget_tokens
|
||||
* can exceed max_tokens as the token limit becomes the entire context window.
|
||||
* The max_tokens is drawn from the model's maxTokens definition.
|
||||
*
|
||||
* @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#interleaved-thinking
|
||||
*/
|
||||
export const claudeCodeReasoningConfig = {
|
||||
low: { budgetTokens: 16_000 },
|
||||
medium: { budgetTokens: 32_000 },
|
||||
high: { budgetTokens: 64_000 },
|
||||
} as const
|
||||
|
||||
export type ClaudeCodeReasoningLevel = keyof typeof claudeCodeReasoningConfig
|
||||
|
|
@ -8,7 +8,6 @@ export const deepInfraDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 1.2,
|
||||
description: "Qwen 3 Coder 480B A35B Instruct Turbo model, 256K context.",
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ export const deepSeekModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
|
||||
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025
|
||||
cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
|
||||
|
|
@ -27,8 +25,6 @@ export const deepSeekModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
preserveReasoning: true,
|
||||
inputPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
|
||||
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ export const doubaoModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.0001, // $0.0001 per million tokens (cache miss)
|
||||
outputPrice: 0.0004, // $0.0004 per million tokens
|
||||
cacheWritesPrice: 0.0001, // $0.0001 per million tokens (cache miss)
|
||||
|
|
@ -21,8 +19,6 @@ export const doubaoModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.0002, // $0.0002 per million tokens
|
||||
outputPrice: 0.0008, // $0.0008 per million tokens
|
||||
cacheWritesPrice: 0.0002, // $0.0002 per million
|
||||
|
|
@ -34,8 +30,6 @@ export const doubaoModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.00015, // $0.00015 per million tokens
|
||||
outputPrice: 0.0006, // $0.0006 per million tokens
|
||||
cacheWritesPrice: 0.00015, // $0.00015 per million
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export const featherlessModels = {
|
|||
contextWindow: 32678,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek V3 0324 model.",
|
||||
|
|
@ -23,7 +22,6 @@ export const featherlessModels = {
|
|||
contextWindow: 32678,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "DeepSeek R1 0528 model.",
|
||||
|
|
@ -33,7 +31,6 @@ export const featherlessModels = {
|
|||
contextWindow: 32678,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Kimi K2 Instruct model.",
|
||||
|
|
@ -43,7 +40,6 @@ export const featherlessModels = {
|
|||
contextWindow: 32678,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "GPT-OSS 120B model.",
|
||||
|
|
@ -53,7 +49,6 @@ export const featherlessModels = {
|
|||
contextWindow: 32678,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Qwen3 Coder 480B A35B Instruct model.",
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ export const fireworksModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
cacheReadsPrice: 0.15,
|
||||
|
|
@ -37,8 +35,6 @@ export const fireworksModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
description:
|
||||
|
|
@ -49,7 +45,6 @@ export const fireworksModels = {
|
|||
contextWindow: 256000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
supportsTemperature: true,
|
||||
preserveReasoning: true,
|
||||
defaultTemperature: 1.0,
|
||||
|
|
@ -64,8 +59,6 @@ export const fireworksModels = {
|
|||
contextWindow: 204800,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 1.2,
|
||||
description:
|
||||
|
|
@ -76,8 +69,6 @@ export const fireworksModels = {
|
|||
contextWindow: 256000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.22,
|
||||
outputPrice: 0.88,
|
||||
description: "Latest Qwen3 thinking model, competitive against the best closed source models in Jul 2025.",
|
||||
|
|
@ -87,8 +78,6 @@ export const fireworksModels = {
|
|||
contextWindow: 256000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.45,
|
||||
outputPrice: 1.8,
|
||||
description: "Qwen3's most agentic code model to date.",
|
||||
|
|
@ -98,8 +87,6 @@ export const fireworksModels = {
|
|||
contextWindow: 160000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3,
|
||||
outputPrice: 8,
|
||||
description:
|
||||
|
|
@ -110,8 +97,6 @@ export const fireworksModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.9,
|
||||
outputPrice: 0.9,
|
||||
description:
|
||||
|
|
@ -122,8 +107,6 @@ export const fireworksModels = {
|
|||
contextWindow: 163840,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.56,
|
||||
outputPrice: 1.68,
|
||||
description:
|
||||
|
|
@ -134,8 +117,6 @@ export const fireworksModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.55,
|
||||
outputPrice: 2.19,
|
||||
description:
|
||||
|
|
@ -146,8 +127,6 @@ export const fireworksModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.55,
|
||||
outputPrice: 2.19,
|
||||
description:
|
||||
|
|
@ -158,8 +137,6 @@ export const fireworksModels = {
|
|||
contextWindow: 198000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.55,
|
||||
outputPrice: 2.19,
|
||||
description:
|
||||
|
|
@ -170,8 +147,6 @@ export const fireworksModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.07,
|
||||
outputPrice: 0.3,
|
||||
description:
|
||||
|
|
@ -182,8 +157,6 @@ export const fireworksModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.6,
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["low", "high"],
|
||||
reasoningEffort: "low",
|
||||
|
|
@ -20,16 +18,19 @@ export const geminiModels = {
|
|||
defaultTemperature: 1,
|
||||
inputPrice: 4.0,
|
||||
outputPrice: 18.0,
|
||||
cacheReadsPrice: 0.4,
|
||||
tiers: [
|
||||
{
|
||||
contextWindow: 200_000,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 12.0,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
{
|
||||
contextWindow: Infinity,
|
||||
inputPrice: 4.0,
|
||||
outputPrice: 18.0,
|
||||
cacheReadsPrice: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -37,26 +38,21 @@ export const geminiModels = {
|
|||
maxTokens: 65_536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
|
||||
supportsTemperature: true,
|
||||
defaultTemperature: 1,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 2.5,
|
||||
cacheReadsPrice: 0.075,
|
||||
cacheWritesPrice: 1.0,
|
||||
inputPrice: 0.5,
|
||||
outputPrice: 3.0,
|
||||
cacheReadsPrice: 0.05,
|
||||
},
|
||||
// 2.5 Pro models
|
||||
"gemini-2.5-pro": {
|
||||
maxTokens: 64_000,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
|
||||
|
|
@ -85,8 +81,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_535,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
|
||||
|
|
@ -114,8 +108,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_535,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
|
||||
|
|
@ -141,8 +133,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_535,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
|
||||
|
|
@ -172,8 +162,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 0.3,
|
||||
|
|
@ -187,8 +175,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 0.3,
|
||||
|
|
@ -202,8 +188,6 @@ export const geminiModels = {
|
|||
maxTokens: 64_000,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 0.3,
|
||||
|
|
@ -219,8 +203,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 0.1,
|
||||
|
|
@ -234,8 +216,6 @@ export const geminiModels = {
|
|||
maxTokens: 65_536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsPromptCache: true,
|
||||
|
||||
inputPrice: 0.1,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ export const groqModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.05,
|
||||
outputPrice: 0.08,
|
||||
description: "Meta Llama 3.1 8B Instant model, 128K context.",
|
||||
|
|
@ -30,8 +28,6 @@ export const groqModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.59,
|
||||
outputPrice: 0.79,
|
||||
description: "Meta Llama 3.3 70B Versatile model, 128K context.",
|
||||
|
|
@ -41,8 +37,6 @@ export const groqModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.11,
|
||||
outputPrice: 0.34,
|
||||
description: "Meta Llama 4 Scout 17B Instruct model, 128K context.",
|
||||
|
|
@ -52,8 +46,6 @@ export const groqModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.29,
|
||||
outputPrice: 0.59,
|
||||
description: "Alibaba Qwen 3 32B model, 128K context.",
|
||||
|
|
@ -63,8 +55,6 @@ export const groqModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
cacheReadsPrice: 0.15,
|
||||
|
|
@ -76,8 +66,6 @@ export const groqModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.75,
|
||||
description:
|
||||
|
|
@ -88,8 +76,6 @@ export const groqModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.5,
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export * from "./baseten.js"
|
|||
export * from "./bedrock.js"
|
||||
export * from "./cerebras.js"
|
||||
export * from "./chutes.js"
|
||||
export * from "./claude-code.js"
|
||||
export * from "./deepseek.js"
|
||||
export * from "./doubao.js"
|
||||
export * from "./featherless.js"
|
||||
|
|
@ -19,6 +18,7 @@ export * from "./moonshot.js"
|
|||
export * from "./ollama.js"
|
||||
export * from "./openai.js"
|
||||
export * from "./openai-codex.js"
|
||||
export * from "./openai-codex-rate-limits.js"
|
||||
export * from "./openrouter.js"
|
||||
export * from "./qwen-code.js"
|
||||
export * from "./requesty.js"
|
||||
|
|
@ -38,7 +38,6 @@ import { basetenDefaultModelId } from "./baseten.js"
|
|||
import { bedrockDefaultModelId } from "./bedrock.js"
|
||||
import { cerebrasDefaultModelId } from "./cerebras.js"
|
||||
import { chutesDefaultModelId } from "./chutes.js"
|
||||
import { claudeCodeDefaultModelId } from "./claude-code.js"
|
||||
import { deepSeekDefaultModelId } from "./deepseek.js"
|
||||
import { doubaoDefaultModelId } from "./doubao.js"
|
||||
import { featherlessDefaultModelId } from "./featherless.js"
|
||||
|
|
@ -127,8 +126,6 @@ export function getProviderDefaultModelId(
|
|||
return deepInfraDefaultModelId
|
||||
case "vscode-lm":
|
||||
return vscodeLlmDefaultModelId
|
||||
case "claude-code":
|
||||
return claudeCodeDefaultModelId
|
||||
case "cerebras":
|
||||
return cerebrasDefaultModelId
|
||||
case "sambanova":
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export const ioIntelligenceModels = {
|
|||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
description: "DeepSeek R1 reasoning model",
|
||||
},
|
||||
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": {
|
||||
|
|
@ -26,7 +25,6 @@ export const ioIntelligenceModels = {
|
|||
contextWindow: 430000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
description: "Llama 4 Maverick 17B model",
|
||||
},
|
||||
"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": {
|
||||
|
|
@ -34,7 +32,6 @@ export const ioIntelligenceModels = {
|
|||
contextWindow: 106000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
description: "Qwen3 Coder 480B specialized for coding",
|
||||
},
|
||||
"openai/gpt-oss-120b": {
|
||||
|
|
@ -42,7 +39,6 @@ export const ioIntelligenceModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
description: "OpenAI GPT-OSS 120B model",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ export const litellmDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ export const lMStudioDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ export const minimaxModels = {
|
|||
contextWindow: 192_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["search_and_replace"],
|
||||
excludedTools: ["apply_diff"],
|
||||
preserveReasoning: true,
|
||||
|
|
@ -30,8 +28,6 @@ export const minimaxModels = {
|
|||
contextWindow: 192_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["search_and_replace"],
|
||||
excludedTools: ["apply_diff"],
|
||||
preserveReasoning: true,
|
||||
|
|
@ -47,8 +43,6 @@ export const minimaxModels = {
|
|||
contextWindow: 192_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["search_and_replace"],
|
||||
excludedTools: ["apply_diff"],
|
||||
preserveReasoning: true,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ export const mistralModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 5.0,
|
||||
},
|
||||
|
|
@ -21,8 +19,6 @@ export const mistralModels = {
|
|||
contextWindow: 131_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.4,
|
||||
outputPrice: 2.0,
|
||||
},
|
||||
|
|
@ -31,8 +27,6 @@ export const mistralModels = {
|
|||
contextWindow: 131_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.4,
|
||||
outputPrice: 2.0,
|
||||
},
|
||||
|
|
@ -41,8 +35,6 @@ export const mistralModels = {
|
|||
contextWindow: 256_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.9,
|
||||
},
|
||||
|
|
@ -51,8 +43,6 @@ export const mistralModels = {
|
|||
contextWindow: 131_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 6.0,
|
||||
},
|
||||
|
|
@ -61,8 +51,6 @@ export const mistralModels = {
|
|||
contextWindow: 131_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.1,
|
||||
},
|
||||
|
|
@ -71,8 +59,6 @@ export const mistralModels = {
|
|||
contextWindow: 131_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.04,
|
||||
outputPrice: 0.04,
|
||||
},
|
||||
|
|
@ -81,8 +67,6 @@ export const mistralModels = {
|
|||
contextWindow: 32_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.6,
|
||||
},
|
||||
|
|
@ -91,8 +75,6 @@ export const mistralModels = {
|
|||
contextWindow: 131_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 6.0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ export const moonshotModels = {
|
|||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.6, // $0.60 per million tokens (cache miss)
|
||||
outputPrice: 2.5, // $2.50 per million tokens
|
||||
cacheWritesPrice: 0, // $0 per million tokens (cache miss)
|
||||
|
|
@ -24,8 +22,6 @@ export const moonshotModels = {
|
|||
contextWindow: 262144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
cacheReadsPrice: 0.15,
|
||||
|
|
@ -37,8 +33,6 @@ export const moonshotModels = {
|
|||
contextWindow: 262_144,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 2.4, // $2.40 per million tokens (cache miss)
|
||||
outputPrice: 10, // $10.00 per million tokens
|
||||
cacheWritesPrice: 0, // $0 per million tokens (cache miss)
|
||||
|
|
@ -50,8 +44,6 @@ export const moonshotModels = {
|
|||
contextWindow: 262_144, // 262,144 tokens
|
||||
supportsImages: false, // Text-only (no image/vision support)
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.6, // $0.60 per million tokens (cache miss)
|
||||
outputPrice: 2.5, // $2.50 per million tokens
|
||||
cacheWritesPrice: 0, // $0 per million tokens (cache miss)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const ollamaDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
|
|||
29
packages/types/src/providers/openai-codex-rate-limits.ts
Normal file
29
packages/types/src/providers/openai-codex-rate-limits.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* OpenAI Codex usage/rate limit information (ChatGPT subscription)
|
||||
*/
|
||||
export interface OpenAiCodexRateLimitInfo {
|
||||
primary?: {
|
||||
/** Used percent in 0–100 */
|
||||
usedPercent: number
|
||||
/** Window length in minutes, when provided */
|
||||
windowMinutes?: number
|
||||
/** Reset time (unix ms since epoch), when provided */
|
||||
resetsAt?: number
|
||||
}
|
||||
secondary?: {
|
||||
/** Used percent in 0–100 */
|
||||
usedPercent: number
|
||||
/** Window length in minutes, when provided */
|
||||
windowMinutes?: number
|
||||
/** Reset time (unix ms since epoch), when provided */
|
||||
resetsAt?: number
|
||||
}
|
||||
credits?: {
|
||||
hasCredits: boolean
|
||||
unlimited: boolean
|
||||
balance?: string
|
||||
}
|
||||
planType?: string
|
||||
/** Timestamp when this was fetched (unix ms since epoch) */
|
||||
fetchedAt: number
|
||||
}
|
||||
|
|
@ -27,8 +27,6 @@ export const openAiCodexModels = {
|
|||
"gpt-5.1-codex-max": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -41,11 +39,24 @@ export const openAiCodexModels = {
|
|||
supportsTemperature: false,
|
||||
description: "GPT-5.1 Codex Max: Maximum capability coding model via ChatGPT subscription",
|
||||
},
|
||||
"gpt-5.1-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
// Subscription-based: no per-token costs
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
supportsTemperature: false,
|
||||
description: "GPT-5.1 Codex: GPT-5.1 optimized for agentic coding via ChatGPT subscription",
|
||||
},
|
||||
"gpt-5.2-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -57,11 +68,71 @@ export const openAiCodexModels = {
|
|||
supportsTemperature: false,
|
||||
description: "GPT-5.2 Codex: OpenAI's flagship coding model via ChatGPT subscription",
|
||||
},
|
||||
"gpt-5.1": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["none", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
// Subscription-based: no per-token costs
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
description: "GPT-5.1: General GPT-5.1 model via ChatGPT subscription",
|
||||
},
|
||||
"gpt-5": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
// Subscription-based: no per-token costs
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
description: "GPT-5: General GPT-5 model via ChatGPT subscription",
|
||||
},
|
||||
"gpt-5-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
// Subscription-based: no per-token costs
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
supportsTemperature: false,
|
||||
description: "GPT-5 Codex: GPT-5 optimized for agentic coding via ChatGPT subscription",
|
||||
},
|
||||
"gpt-5-codex-mini": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
// Subscription-based: no per-token costs
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
supportsTemperature: false,
|
||||
description: "GPT-5 Codex Mini: Faster coding model via ChatGPT subscription",
|
||||
},
|
||||
"gpt-5.1-codex-mini": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -76,8 +147,6 @@ export const openAiCodexModels = {
|
|||
"gpt-5.2": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5.1-codex-max": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -29,8 +27,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5.2": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -52,8 +48,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5.2-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -72,8 +66,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5.2-chat-latest": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 128_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -86,8 +78,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5.1": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -109,8 +99,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5.1-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -128,8 +116,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5.1-codex-mini": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -146,8 +132,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -168,8 +152,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5-mini": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -190,8 +172,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -208,8 +188,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5-nano": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -227,8 +205,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5-chat-latest": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -241,8 +217,6 @@ export const openAiNativeModels = {
|
|||
"gpt-4.1": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 1_047_576,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -258,8 +232,6 @@ export const openAiNativeModels = {
|
|||
"gpt-4.1-mini": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 1_047_576,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -275,8 +247,6 @@ export const openAiNativeModels = {
|
|||
"gpt-4.1-nano": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 1_047_576,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -292,8 +262,6 @@ export const openAiNativeModels = {
|
|||
o3: {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.0,
|
||||
|
|
@ -310,8 +278,6 @@ export const openAiNativeModels = {
|
|||
"o3-high": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.0,
|
||||
|
|
@ -323,8 +289,6 @@ export const openAiNativeModels = {
|
|||
"o3-low": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.0,
|
||||
|
|
@ -336,8 +300,6 @@ export const openAiNativeModels = {
|
|||
"o4-mini": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
|
|
@ -354,8 +316,6 @@ export const openAiNativeModels = {
|
|||
"o4-mini-high": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
|
|
@ -367,8 +327,6 @@ export const openAiNativeModels = {
|
|||
"o4-mini-low": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
|
|
@ -380,8 +338,6 @@ export const openAiNativeModels = {
|
|||
"o3-mini": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
|
|
@ -394,8 +350,6 @@ export const openAiNativeModels = {
|
|||
"o3-mini-high": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
|
|
@ -407,8 +361,6 @@ export const openAiNativeModels = {
|
|||
"o3-mini-low": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
|
|
@ -420,8 +372,6 @@ export const openAiNativeModels = {
|
|||
o1: {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 15,
|
||||
|
|
@ -432,8 +382,6 @@ export const openAiNativeModels = {
|
|||
"o1-preview": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 128_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 15,
|
||||
|
|
@ -444,8 +392,6 @@ export const openAiNativeModels = {
|
|||
"o1-mini": {
|
||||
maxTokens: 65_536,
|
||||
contextWindow: 128_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
|
|
@ -456,8 +402,6 @@ export const openAiNativeModels = {
|
|||
"gpt-4o": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 128_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.5,
|
||||
|
|
@ -471,8 +415,6 @@ export const openAiNativeModels = {
|
|||
"gpt-4o-mini": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 128_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.15,
|
||||
|
|
@ -486,8 +428,6 @@ export const openAiNativeModels = {
|
|||
"codex-mini-latest": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 200_000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
|
|
@ -501,8 +441,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -523,8 +461,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5-mini-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -545,8 +481,6 @@ export const openAiNativeModels = {
|
|||
"gpt-5-nano-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
includedTools: ["apply_patch"],
|
||||
excludedTools: ["apply_diff", "write_to_file"],
|
||||
supportsImages: true,
|
||||
|
|
@ -570,8 +504,6 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
|
|||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const openRouterDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ export const qwenCodeModels = {
|
|||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
@ -23,8 +21,6 @@ export const qwenCodeModels = {
|
|||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ export const requestyDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ export const sambaNovaModels = {
|
|||
contextWindow: 16384,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.2,
|
||||
description: "Meta Llama 3.1 8B Instruct model with 16K context window.",
|
||||
|
|
@ -30,8 +28,6 @@ export const sambaNovaModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 1.2,
|
||||
description: "Meta Llama 3.3 70B Instruct model with 128K context window.",
|
||||
|
|
@ -42,8 +38,6 @@ export const sambaNovaModels = {
|
|||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsReasoningBudget: true,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 7.0,
|
||||
description: "DeepSeek R1 reasoning model with 32K context window.",
|
||||
|
|
@ -53,8 +47,6 @@ export const sambaNovaModels = {
|
|||
contextWindow: 32768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 4.5,
|
||||
description: "DeepSeek V3 model with 32K context window.",
|
||||
|
|
@ -64,8 +56,6 @@ export const sambaNovaModels = {
|
|||
contextWindow: 32768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 4.5,
|
||||
description: "DeepSeek V3.1 model with 32K context window.",
|
||||
|
|
@ -75,8 +65,6 @@ export const sambaNovaModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.63,
|
||||
outputPrice: 1.8,
|
||||
description: "Meta Llama 4 Maverick 17B 128E Instruct model with 128K context window.",
|
||||
|
|
@ -86,8 +74,6 @@ export const sambaNovaModels = {
|
|||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.4,
|
||||
outputPrice: 0.8,
|
||||
description: "Alibaba Qwen 3 32B model with 8K context window.",
|
||||
|
|
@ -97,8 +83,6 @@ export const sambaNovaModels = {
|
|||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: true,
|
||||
defaultToolProtocol: "native",
|
||||
inputPrice: 0.22,
|
||||
outputPrice: 0.59,
|
||||
description: "OpenAI gpt oss 120b model with 128k context window.",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export const unboundDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ export const vercelAiGatewayDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 3,
|
||||
outputPrice: 15,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue