mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Merge pull request #2 from cannuri/feature/update-main
Feature/update main
This commit is contained in:
commit
42287ad1a0
48 changed files with 584 additions and 244 deletions
5
.changeset/automatic-tags-publish.md
Normal file
5
.changeset/automatic-tags-publish.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Update GitHub Actions workflow to automatically create and push git tags during release
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Revert tool progress for now
|
||||
|
|
@ -2,4 +2,4 @@
|
|||
"roo-cline": patch
|
||||
---
|
||||
|
||||
v3.8.4
|
||||
App tab layout fixes
|
||||
|
|
@ -1,2 +1 @@
|
|||
# PostHog API Keys for telemetry
|
||||
POSTHOG_API_KEY=key-goes-here
|
||||
POSTHOG_API_KEY=key-goes-here
|
||||
|
|
|
|||
25
.github/workflows/marketplace-publish.yml
vendored
25
.github/workflows/marketplace-publish.yml
vendored
|
|
@ -10,6 +10,8 @@ env:
|
|||
jobs:
|
||||
publish-extension:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required for pushing tags
|
||||
if: >
|
||||
( github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.base.ref == 'main' &&
|
||||
|
|
@ -23,24 +25,24 @@ jobs:
|
|||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install -g vsce ovsx
|
||||
npm run install:ci
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
echo "# PostHog API Keys for telemetry" > .env
|
||||
echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .env
|
||||
- name: Package and Publish Extension
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
|
||||
- name: Package Extension
|
||||
run: |
|
||||
current_package_version=$(node -p "require('./package.json').version")
|
||||
|
||||
npm run vsix
|
||||
package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
|
||||
echo "$package"
|
||||
|
|
@ -49,5 +51,18 @@ jobs:
|
|||
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
|
||||
echo "$package" | grep -q ".env" || exit 1
|
||||
|
||||
- name: Create and Push Git Tag
|
||||
run: |
|
||||
current_package_version=$(node -p "require('./package.json').version")
|
||||
git tag -a "v${current_package_version}" -m "Release v${current_package_version}"
|
||||
git push origin "v${current_package_version}"
|
||||
echo "Successfully created and pushed git tag v${current_package_version}"
|
||||
|
||||
- name: Publish Extension
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
run: |
|
||||
current_package_version=$(node -p "require('./package.json').version")
|
||||
npm run publish:marketplace
|
||||
echo "Successfully published version $current_package_version to VS Code Marketplace"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.8.4] - 2025-03-09
|
||||
|
||||
- Roll back multi-diff progress indicator temporarily to fix a double-confirmation in saving edits
|
||||
- Add an option in the prompts tab to save tokens by disabling the ability to ask Roo to create/edit custom modes for you (thanks @hannesrudolph!)
|
||||
|
||||
## [3.8.3] - 2025-03-09
|
||||
|
||||
- Fix VS Code LM API model picker truncation issue
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const copyWasmFiles = {
|
|||
"java",
|
||||
"php",
|
||||
"swift",
|
||||
"kotlin",
|
||||
]
|
||||
|
||||
languages.forEach((lang) => {
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "roo-cline",
|
||||
"version": "3.8.3",
|
||||
"version": "3.8.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "roo-cline",
|
||||
"version": "3.8.3",
|
||||
"version": "3.8.4",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"displayName": "Roo Code (prev. Roo Cline)",
|
||||
"description": "A whole dev team of AI agents in your editor.",
|
||||
"publisher": "RooVeterinaryInc",
|
||||
"version": "3.8.3",
|
||||
"version": "3.8.4",
|
||||
"icon": "assets/icons/rocket.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ describe("RequestyHandler", () => {
|
|||
contextWindow: 4000,
|
||||
supportsPromptCache: false,
|
||||
supportsImages: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
inputPrice: 1,
|
||||
outputPrice: 10,
|
||||
cacheReadsPrice: 0.1,
|
||||
cacheWritesPrice: 1.5,
|
||||
},
|
||||
openAiStreamingEnabled: true,
|
||||
includeMaxTokens: true, // Add this to match the implementation
|
||||
|
|
@ -83,8 +85,12 @@ describe("RequestyHandler", () => {
|
|||
yield {
|
||||
choices: [{ delta: { content: " world" } }],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 5,
|
||||
prompt_tokens: 30,
|
||||
completion_tokens: 10,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: 15,
|
||||
caching_tokens: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
@ -105,10 +111,11 @@ describe("RequestyHandler", () => {
|
|||
{ type: "text", text: " world" },
|
||||
{
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
cacheWriteTokens: undefined,
|
||||
cacheReadTokens: undefined,
|
||||
inputTokens: 30,
|
||||
outputTokens: 10,
|
||||
cacheWriteTokens: 5,
|
||||
cacheReadTokens: 15,
|
||||
totalCost: 0.000119, // (10 * 1 / 1,000,000) + (5 * 1.5 / 1,000,000) + (15 * 0.1 / 1,000,000) + (10 * 10 / 1,000,000)
|
||||
},
|
||||
])
|
||||
|
||||
|
|
@ -182,6 +189,9 @@ describe("RequestyHandler", () => {
|
|||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000)
|
||||
},
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
}
|
||||
if (chunk.usage) {
|
||||
yield this.processUsageMetrics(chunk.usage)
|
||||
yield this.processUsageMetrics(chunk.usage, modelInfo)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -139,11 +139,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
type: "text",
|
||||
text: response.choices[0]?.message.content || "",
|
||||
}
|
||||
yield this.processUsageMetrics(response.usage)
|
||||
yield this.processUsageMetrics(response.usage, modelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
protected processUsageMetrics(usage: any): ApiStreamUsageChunk {
|
||||
protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
|
||||
return {
|
||||
type: "usage",
|
||||
inputTokens: usage?.prompt_tokens || 0,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
import axios from "axios"
|
||||
|
||||
import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
|
||||
import { parseApiPrice } from "../../utils/cost"
|
||||
import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost"
|
||||
import { ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
|
||||
import OpenAI from "openai"
|
||||
|
||||
// Requesty usage includes an extra field for Anthropic use cases.
|
||||
// Safely cast the prompt token details section to the appropriate structure.
|
||||
interface RequestyUsage extends OpenAI.CompletionUsage {
|
||||
prompt_tokens_details?: {
|
||||
caching_tokens?: number
|
||||
cached_tokens?: number
|
||||
}
|
||||
total_cost?: number
|
||||
}
|
||||
|
||||
export class RequestyHandler extends OpenAiHandler {
|
||||
constructor(options: OpenAiHandlerOptions) {
|
||||
|
|
@ -27,13 +38,22 @@ export class RequestyHandler extends OpenAiHandler {
|
|||
}
|
||||
}
|
||||
|
||||
protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
|
||||
protected override processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
|
||||
const requestyUsage = usage as RequestyUsage
|
||||
const inputTokens = requestyUsage?.prompt_tokens || 0
|
||||
const outputTokens = requestyUsage?.completion_tokens || 0
|
||||
const cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0
|
||||
const cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0
|
||||
const totalCost = modelInfo
|
||||
? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
: 0
|
||||
return {
|
||||
type: "usage",
|
||||
inputTokens: usage?.prompt_tokens || 0,
|
||||
outputTokens: usage?.completion_tokens || 0,
|
||||
cacheWriteTokens: usage?.cache_creation_input_tokens,
|
||||
cacheReadTokens: usage?.cache_read_input_tokens,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
|||
import * as vscode from "vscode"
|
||||
|
||||
import { SingleCompletionHandler } from "../"
|
||||
import { calculateApiCost } from "../../utils/cost"
|
||||
import { calculateApiCostAnthropic } from "../../utils/cost"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
|
||||
|
|
@ -462,7 +462,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
|
|||
type: "usage",
|
||||
inputTokens: totalInputTokens,
|
||||
outputTokens: totalOutputTokens,
|
||||
totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
|
||||
totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens),
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
this.ensureCleanState()
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ import { ClineAskResponse } from "../shared/WebviewMessage"
|
|||
import { GlobalFileNames } from "../shared/globalFileNames"
|
||||
import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
|
||||
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
|
||||
import { calculateApiCost } from "../utils/cost"
|
||||
import { calculateApiCostAnthropic } from "../utils/cost"
|
||||
import { fileExistsAtPath } from "../utils/fs"
|
||||
import { arePathsEqual, getReadablePath } from "../utils/path"
|
||||
import { parseMentions } from "./mentions"
|
||||
|
|
@ -875,7 +875,7 @@ export class Cline {
|
|||
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
|
||||
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
|
||||
|
||||
//const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
|
||||
//const totalCost = this.calculateApiCostAnthropic(totalInputTokens, totalOutputTokens)
|
||||
if (didEndLoop) {
|
||||
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
|
||||
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
|
||||
|
|
@ -3173,7 +3173,7 @@ export class Cline {
|
|||
cacheReads: cacheReadTokens,
|
||||
cost:
|
||||
totalCost ??
|
||||
calculateApiCost(
|
||||
calculateApiCostAnthropic(
|
||||
this.api.getModel().info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
|
|
@ -3798,6 +3798,8 @@ export class Cline {
|
|||
return
|
||||
}
|
||||
|
||||
telemetryService.captureCheckpointDiffed(this.taskId)
|
||||
|
||||
if (!previousCommitHash && mode === "checkpoint") {
|
||||
const previousCheckpoint = this.clineMessages
|
||||
.filter(({ say }) => say === "checkpoint_saved")
|
||||
|
|
@ -3849,6 +3851,8 @@ export class Cline {
|
|||
return
|
||||
}
|
||||
|
||||
telemetryService.captureCheckpointCreated(this.taskId)
|
||||
|
||||
// Start the checkpoint process in the background.
|
||||
service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
|
||||
console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
|
||||
|
|
@ -3880,6 +3884,8 @@ export class Cline {
|
|||
try {
|
||||
await service.restoreCheckpoint(commitHash)
|
||||
|
||||
telemetryService.captureCheckpointRestored(this.taskId)
|
||||
|
||||
await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
|
||||
|
||||
if (mode === "restore") {
|
||||
|
|
|
|||
|
|
@ -198,9 +198,9 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
|
|||
}
|
||||
}
|
||||
|
||||
function getWorkspaceProblems(cwd: string): string {
|
||||
async function getWorkspaceProblems(cwd: string): Promise<string> {
|
||||
const diagnostics = vscode.languages.getDiagnostics()
|
||||
const result = diagnosticsToProblemsString(
|
||||
const result = await diagnosticsToProblemsString(
|
||||
diagnostics,
|
||||
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
|
||||
cwd,
|
||||
|
|
|
|||
|
|
@ -2567,6 +2567,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
properties.apiProvider = apiConfiguration.apiProvider
|
||||
}
|
||||
|
||||
// Add model ID if available
|
||||
const currentCline = this.getCurrentCline()
|
||||
if (currentCline?.api) {
|
||||
const { id: modelId } = currentCline.api.getModel()
|
||||
if (modelId) {
|
||||
properties.modelId = modelId
|
||||
}
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1652,3 +1652,62 @@ describe("ContextProxy integration", () => {
|
|||
expect(mockContextProxy.setValues).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTelemetryProperties", () => {
|
||||
let provider: ClineProvider
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let mockOutputChannel: vscode.OutputChannel
|
||||
let mockCline: any
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Setup basic mocks
|
||||
mockContext = {
|
||||
globalState: {
|
||||
get: jest.fn().mockImplementation((key: string) => {
|
||||
if (key === "mode") return "code"
|
||||
if (key === "apiProvider") return "anthropic"
|
||||
return undefined
|
||||
}),
|
||||
update: jest.fn(),
|
||||
keys: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
|
||||
extensionUri: {} as vscode.Uri,
|
||||
globalStorageUri: { fsPath: "/test/path" },
|
||||
extension: { packageJSON: { version: "1.0.0" } },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
|
||||
provider = new ClineProvider(mockContext, mockOutputChannel)
|
||||
|
||||
// Setup Cline instance with mocked getModel method
|
||||
const { Cline } = require("../../Cline")
|
||||
mockCline = new Cline()
|
||||
mockCline.api = {
|
||||
getModel: jest.fn().mockReturnValue({
|
||||
id: "claude-3-7-sonnet-20250219",
|
||||
info: { contextWindow: 200000 },
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
test("includes basic properties in telemetry", async () => {
|
||||
const properties = await provider.getTelemetryProperties()
|
||||
|
||||
expect(properties).toHaveProperty("vscodeVersion")
|
||||
expect(properties).toHaveProperty("platform")
|
||||
expect(properties).toHaveProperty("appVersion", "1.0.0")
|
||||
})
|
||||
|
||||
test("includes model ID from current Cline instance if available", async () => {
|
||||
// Add mock Cline to stack
|
||||
await provider.addClineToStack(mockCline)
|
||||
|
||||
const properties = await provider.getTelemetryProperties()
|
||||
|
||||
expect(properties).toHaveProperty("modelId", "claude-3-7-sonnet-20250219")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -70,11 +70,12 @@ export function getNewDiagnostics(
|
|||
// // - New error in file3 (1:1)
|
||||
|
||||
// will return empty string if no problems with the given severity are found
|
||||
export function diagnosticsToProblemsString(
|
||||
export async function diagnosticsToProblemsString(
|
||||
diagnostics: [vscode.Uri, vscode.Diagnostic[]][],
|
||||
severities: vscode.DiagnosticSeverity[],
|
||||
cwd: string,
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const documents = new Map<vscode.Uri, vscode.TextDocument>()
|
||||
let result = ""
|
||||
for (const [uri, fileDiagnostics] of diagnostics) {
|
||||
const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
|
||||
|
|
@ -100,7 +101,10 @@ export function diagnosticsToProblemsString(
|
|||
}
|
||||
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
|
||||
const source = diagnostic.source ? `${diagnostic.source} ` : ""
|
||||
result += `\n- [${source}${label}] Line ${line}: ${diagnostic.message}`
|
||||
const document = documents.get(uri) || (await vscode.workspace.openTextDocument(uri))
|
||||
documents.set(uri, document)
|
||||
const lineContent = document.lineAt(diagnostic.range.start.line).text
|
||||
result += `\n- [${source}${label}] ${line} | ${lineContent} : ${diagnostic.message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export class DiffViewProvider {
|
|||
initial fix is usually correct and it may just take time for linters to catch up.
|
||||
*/
|
||||
const postDiagnostics = vscode.languages.getDiagnostics()
|
||||
const newProblems = diagnosticsToProblemsString(
|
||||
const newProblems = await diagnosticsToProblemsString(
|
||||
getNewDiagnostics(this.preDiagnostics, postDiagnostics),
|
||||
[
|
||||
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
|
|||
"pkg",
|
||||
"Pods",
|
||||
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
|
||||
].map((dir) => `**/${dir}/**`)
|
||||
].map((dir) => `${dirPath}/**/${dir}/**`)
|
||||
|
||||
const options = {
|
||||
cwd: dirPath,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ class PostHogClient {
|
|||
CONVERSATION_MESSAGE: "Conversation Message",
|
||||
MODE_SWITCH: "Mode Switched",
|
||||
TOOL_USED: "Tool Used",
|
||||
CHECKPOINT_CREATED: "Checkpoint Created",
|
||||
CHECKPOINT_RESTORED: "Checkpoint Restored",
|
||||
CHECKPOINT_DIFFED: "Checkpoint Diffed",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +249,18 @@ class TelemetryService {
|
|||
})
|
||||
}
|
||||
|
||||
public captureCheckpointCreated(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_CREATED, { taskId })
|
||||
}
|
||||
|
||||
public captureCheckpointDiffed(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_DIFFED, { taskId })
|
||||
}
|
||||
|
||||
public captureCheckpointRestored(taskId: string): void {
|
||||
this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_RESTORED, { taskId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if telemetry is currently enabled
|
||||
* @returns Whether telemetry is enabled
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ describe("Tree-sitter Service", () => {
|
|||
"/test/path/main.rs",
|
||||
"/test/path/program.cpp",
|
||||
"/test/path/code.go",
|
||||
"/test/path/app.kt",
|
||||
"/test/path/script.kts",
|
||||
]
|
||||
|
||||
;(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()])
|
||||
|
|
@ -197,6 +199,8 @@ describe("Tree-sitter Service", () => {
|
|||
rs: { parser: mockParser, query: mockQuery },
|
||||
cpp: { parser: mockParser, query: mockQuery },
|
||||
go: { parser: mockParser, query: mockQuery },
|
||||
kt: { parser: mockParser, query: mockQuery },
|
||||
kts: { parser: mockParser, query: mockQuery },
|
||||
})
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue("function test() {}")
|
||||
|
||||
|
|
@ -207,6 +211,8 @@ describe("Tree-sitter Service", () => {
|
|||
expect(result).toContain("main.rs")
|
||||
expect(result).toContain("program.cpp")
|
||||
expect(result).toContain("code.go")
|
||||
expect(result).toContain("app.kt")
|
||||
expect(result).toContain("script.kts")
|
||||
})
|
||||
|
||||
it("should normalize paths in output", async () => {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,17 @@ describe("Language Parser", () => {
|
|||
expect(parsers.hpp).toBeDefined()
|
||||
})
|
||||
|
||||
it("should handle Kotlin files correctly", async () => {
|
||||
const files = ["test.kt", "test.kts"]
|
||||
const parsers = await loadRequiredLanguageParsers(files)
|
||||
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(expect.stringContaining("tree-sitter-kotlin.wasm"))
|
||||
expect(parsers.kt).toBeDefined()
|
||||
expect(parsers.kts).toBeDefined()
|
||||
expect(parsers.kt.query).toBeDefined()
|
||||
expect(parsers.kts.query).toBeDefined()
|
||||
})
|
||||
|
||||
it("should throw error for unsupported file extensions", async () => {
|
||||
const files = ["test.unsupported"]
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingF
|
|||
"java",
|
||||
"php",
|
||||
"swift",
|
||||
// Kotlin
|
||||
"kt",
|
||||
"kts",
|
||||
].map((e) => `.${e}`)
|
||||
const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
|
||||
const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
javaQuery,
|
||||
phpQuery,
|
||||
swiftQuery,
|
||||
kotlinQuery,
|
||||
} from "./queries"
|
||||
|
||||
export interface LanguageParser {
|
||||
|
|
@ -120,6 +121,11 @@ export async function loadRequiredLanguageParsers(filesToParse: string[]): Promi
|
|||
language = await loadLanguage("swift")
|
||||
query = language.query(swiftQuery)
|
||||
break
|
||||
case "kt":
|
||||
case "kts":
|
||||
language = await loadLanguage("kotlin")
|
||||
query = language.query(kotlinQuery)
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported language: ${ext}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ export { default as cQuery } from "./c"
|
|||
export { default as csharpQuery } from "./c-sharp"
|
||||
export { default as goQuery } from "./go"
|
||||
export { default as swiftQuery } from "./swift"
|
||||
export { default as kotlinQuery } from "./kotlin"
|
||||
|
|
|
|||
28
src/services/tree-sitter/queries/kotlin.ts
Normal file
28
src/services/tree-sitter/queries/kotlin.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
- class declarations (including interfaces)
|
||||
- function declarations
|
||||
- object declarations
|
||||
- property declarations
|
||||
- type alias declarations
|
||||
*/
|
||||
export default `
|
||||
(class_declaration
|
||||
(type_identifier) @name.definition.class
|
||||
) @definition.class
|
||||
|
||||
(function_declaration
|
||||
(simple_identifier) @name.definition.function
|
||||
) @definition.function
|
||||
|
||||
(object_declaration
|
||||
(type_identifier) @name.definition.object
|
||||
) @definition.object
|
||||
|
||||
(property_declaration
|
||||
(simple_identifier) @name.definition.property
|
||||
) @definition.property
|
||||
|
||||
(type_alias
|
||||
(type_identifier) @name.definition.type
|
||||
) @definition.type
|
||||
`
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { calculateApiCost } from "../cost"
|
||||
import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost"
|
||||
import { ModelInfo } from "../../shared/api"
|
||||
|
||||
describe("Cost Utility", () => {
|
||||
describe("calculateApiCost", () => {
|
||||
describe("calculateApiCostAnthropic", () => {
|
||||
const mockModelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
|
|
@ -14,7 +14,7 @@ describe("Cost Utility", () => {
|
|||
}
|
||||
|
||||
it("should calculate basic input/output costs correctly", () => {
|
||||
const cost = calculateApiCost(mockModelInfo, 1000, 500)
|
||||
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
|
|
@ -23,7 +23,7 @@ describe("Cost Utility", () => {
|
|||
})
|
||||
|
||||
it("should handle cache writes cost", () => {
|
||||
const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000)
|
||||
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
|
|
@ -33,7 +33,7 @@ describe("Cost Utility", () => {
|
|||
})
|
||||
|
||||
it("should handle cache reads cost", () => {
|
||||
const cost = calculateApiCost(mockModelInfo, 1000, 500, undefined, 3000)
|
||||
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, undefined, 3000)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
|
|
@ -43,7 +43,7 @@ describe("Cost Utility", () => {
|
|||
})
|
||||
|
||||
it("should handle all cost components together", () => {
|
||||
const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000, 3000)
|
||||
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000, 3000)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
|
|
@ -60,17 +60,17 @@ describe("Cost Utility", () => {
|
|||
supportsPromptCache: true,
|
||||
}
|
||||
|
||||
const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000)
|
||||
const cost = calculateApiCostAnthropic(modelWithoutPrices, 1000, 500, 2000, 3000)
|
||||
expect(cost).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle zero tokens", () => {
|
||||
const cost = calculateApiCost(mockModelInfo, 0, 0, 0, 0)
|
||||
const cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)
|
||||
expect(cost).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle undefined cache values", () => {
|
||||
const cost = calculateApiCost(mockModelInfo, 1000, 500)
|
||||
const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
|
|
@ -85,7 +85,7 @@ describe("Cost Utility", () => {
|
|||
cacheReadsPrice: undefined,
|
||||
}
|
||||
|
||||
const cost = calculateApiCost(modelWithoutCachePrices, 1000, 500, 2000, 3000)
|
||||
const cost = calculateApiCostAnthropic(modelWithoutCachePrices, 1000, 500, 2000, 3000)
|
||||
|
||||
// Should only include input and output costs
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
|
|
@ -94,4 +94,97 @@ describe("Cost Utility", () => {
|
|||
expect(cost).toBe(0.0105)
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateApiCostOpenAI", () => {
|
||||
const mockModelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0, // $3 per million tokens
|
||||
outputPrice: 15.0, // $15 per million tokens
|
||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||
cacheReadsPrice: 0.3, // $0.30 per million tokens
|
||||
}
|
||||
|
||||
it("should calculate basic input/output costs correctly", () => {
|
||||
const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Total: 0.003 + 0.0075 = 0.0105
|
||||
expect(cost).toBe(0.0105)
|
||||
})
|
||||
|
||||
it("should handle cache writes cost", () => {
|
||||
const cost = calculateApiCostOpenAI(mockModelInfo, 3000, 500, 2000)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * (3000 - 2000) = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
|
||||
// Total: 0.003 + 0.0075 + 0.0075 = 0.018
|
||||
expect(cost).toBeCloseTo(0.018, 6)
|
||||
})
|
||||
|
||||
it("should handle cache reads cost", () => {
|
||||
const cost = calculateApiCostOpenAI(mockModelInfo, 4000, 500, undefined, 3000)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * (4000 - 3000) = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
|
||||
// Total: 0.003 + 0.0075 + 0.0009 = 0.0114
|
||||
expect(cost).toBe(0.0114)
|
||||
})
|
||||
|
||||
it("should handle all cost components together", () => {
|
||||
const cost = calculateApiCostOpenAI(mockModelInfo, 6000, 500, 2000, 3000)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
|
||||
// Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
|
||||
// Total: 0.003 + 0.0075 + 0.0075 + 0.0009 = 0.0189
|
||||
expect(cost).toBe(0.0189)
|
||||
})
|
||||
|
||||
it("should handle missing prices gracefully", () => {
|
||||
const modelWithoutPrices: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsPromptCache: true,
|
||||
}
|
||||
|
||||
const cost = calculateApiCostOpenAI(modelWithoutPrices, 1000, 500, 2000, 3000)
|
||||
expect(cost).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle zero tokens", () => {
|
||||
const cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)
|
||||
expect(cost).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle undefined cache values", () => {
|
||||
const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
|
||||
|
||||
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Total: 0.003 + 0.0075 = 0.0105
|
||||
expect(cost).toBe(0.0105)
|
||||
})
|
||||
|
||||
it("should handle missing cache prices", () => {
|
||||
const modelWithoutCachePrices: ModelInfo = {
|
||||
...mockModelInfo,
|
||||
cacheWritesPrice: undefined,
|
||||
cacheReadsPrice: undefined,
|
||||
}
|
||||
|
||||
const cost = calculateApiCostOpenAI(modelWithoutCachePrices, 6000, 500, 2000, 3000)
|
||||
|
||||
// Should only include input and output costs
|
||||
// Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
|
||||
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Total: 0.003 + 0.0075 = 0.0105
|
||||
expect(cost).toBe(0.0105)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,26 +1,57 @@
|
|||
import { ModelInfo } from "../shared/api"
|
||||
|
||||
export function calculateApiCost(
|
||||
function calculateApiCostInternal(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheCreationInputTokens?: number,
|
||||
cacheReadInputTokens?: number,
|
||||
cacheCreationInputTokens: number,
|
||||
cacheReadInputTokens: number,
|
||||
): number {
|
||||
const modelCacheWritesPrice = modelInfo.cacheWritesPrice
|
||||
let cacheWritesCost = 0
|
||||
if (cacheCreationInputTokens && modelCacheWritesPrice) {
|
||||
cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
|
||||
}
|
||||
const modelCacheReadsPrice = modelInfo.cacheReadsPrice
|
||||
let cacheReadsCost = 0
|
||||
if (cacheReadInputTokens && modelCacheReadsPrice) {
|
||||
cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
|
||||
}
|
||||
const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens
|
||||
const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens
|
||||
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
|
||||
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
|
||||
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
|
||||
return totalCost
|
||||
}
|
||||
|
||||
// For Anthropic compliant usage, the input tokens count does NOT include the cached tokens
|
||||
export function calculateApiCostAnthropic(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheCreationInputTokens?: number,
|
||||
cacheReadInputTokens?: number,
|
||||
): number {
|
||||
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
|
||||
const cacheReadInputTokensNum = cacheReadInputTokens || 0
|
||||
return calculateApiCostInternal(
|
||||
modelInfo,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheCreationInputTokensNum,
|
||||
cacheReadInputTokensNum,
|
||||
)
|
||||
}
|
||||
|
||||
// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens
|
||||
export function calculateApiCostOpenAI(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheCreationInputTokens?: number,
|
||||
cacheReadInputTokens?: number,
|
||||
): number {
|
||||
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
|
||||
const cacheReadInputTokensNum = cacheReadInputTokens || 0
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum)
|
||||
return calculateApiCostInternal(
|
||||
modelInfo,
|
||||
nonCachedInputTokens,
|
||||
outputTokens,
|
||||
cacheCreationInputTokensNum,
|
||||
cacheReadInputTokensNum,
|
||||
)
|
||||
}
|
||||
|
||||
export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
|
|||
|
||||
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
|
||||
|
||||
type HumanRelayDialogState = {
|
||||
isOpen: boolean
|
||||
requestId: string
|
||||
promptText: string
|
||||
}
|
||||
|
||||
const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
|
||||
chatButtonClicked: "chat",
|
||||
settingsButtonClicked: "settings",
|
||||
|
|
@ -24,24 +30,21 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
|
|||
mcpButtonClicked: "mcp",
|
||||
historyButtonClicked: "history",
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
|
||||
useExtensionState()
|
||||
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
const [tab, setTab] = useState<Tab>("chat")
|
||||
const settingsRef = useRef<SettingsViewRef>(null)
|
||||
|
||||
// Human Relay Dialog Status
|
||||
const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
|
||||
isOpen: boolean
|
||||
requestId: string
|
||||
promptText: string
|
||||
}>({
|
||||
const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
|
||||
isOpen: false,
|
||||
requestId: "",
|
||||
promptText: "",
|
||||
})
|
||||
|
||||
const settingsRef = useRef<SettingsViewRef>(null)
|
||||
|
||||
const switchTab = useCallback((newTab: Tab) => {
|
||||
if (settingsRef.current?.checkUnsaveChanges) {
|
||||
settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
|
||||
|
|
@ -74,23 +77,6 @@ const App = () => {
|
|||
[switchTab],
|
||||
)
|
||||
|
||||
// Processing Human Relay Dialog Submission
|
||||
const handleHumanRelaySubmit = (requestId: string, text: string) => {
|
||||
vscode.postMessage({
|
||||
type: "humanRelayResponse",
|
||||
requestId,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle Human Relay dialog box cancel
|
||||
const handleHumanRelayCancel = (requestId: string) => {
|
||||
vscode.postMessage({
|
||||
type: "humanRelayCancel",
|
||||
requestId,
|
||||
})
|
||||
}
|
||||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -106,7 +92,7 @@ const App = () => {
|
|||
}
|
||||
}, [telemetrySetting, telemetryKey, machineId, didHydrateState])
|
||||
|
||||
// Tell Extension that we are ready to receive messages
|
||||
// Tell the extension that we are ready to receive messages.
|
||||
useEffect(() => {
|
||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||
}, [])
|
||||
|
|
@ -121,24 +107,23 @@ const App = () => {
|
|||
<WelcomeView />
|
||||
) : (
|
||||
<>
|
||||
{tab === "settings" && <SettingsView ref={settingsRef} onDone={() => setTab("chat")} />}
|
||||
{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
|
||||
{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
|
||||
{tab === "prompts" && <PromptsView onDone={() => switchTab("chat")} />}
|
||||
{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
|
||||
{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
|
||||
{tab === "settings" && <SettingsView ref={settingsRef} onDone={() => setTab("chat")} />}
|
||||
<ChatView
|
||||
isHidden={tab !== "chat"}
|
||||
showAnnouncement={showAnnouncement}
|
||||
hideAnnouncement={() => setShowAnnouncement(false)}
|
||||
showHistoryView={() => switchTab("history")}
|
||||
/>
|
||||
{/* Human Relay Dialog */}
|
||||
<HumanRelayDialog
|
||||
isOpen={humanRelayDialogState.isOpen}
|
||||
requestId={humanRelayDialogState.requestId}
|
||||
promptText={humanRelayDialogState.promptText}
|
||||
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
|
||||
onSubmit={handleHumanRelaySubmit}
|
||||
onCancel={handleHumanRelayCancel}
|
||||
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
|
||||
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
|||
</p>
|
||||
|
||||
<h3 style={{ margin: "12px 0 8px" }}>What's New</h3>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
<div style={{ margin: "5px 0px" }}>
|
||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||
<li>• Faster asynchronous checkpoints</li>
|
||||
<li>• Support for .rooignore files</li>
|
||||
|
|
@ -44,7 +44,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
|||
<li>• Updated DeepSeek provider</li>
|
||||
<li>• New "Human Relay" provider</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style={{ margin: "10px 0px 0px" }}>
|
||||
Get more details and discuss in{" "}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
|
||||
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||
import { Mode, getAllModes } from "../../../../src/shared/modes"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import {
|
||||
ContextMenuOptionType,
|
||||
getContextMenuOptions,
|
||||
insertMention,
|
||||
removeMention,
|
||||
shouldShowContextMenu,
|
||||
} from "../../utils/context-mentions"
|
||||
} from "@/utils/context-mentions"
|
||||
import { SelectDropdown, DropdownOptionType } from "@/components/ui"
|
||||
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import { convertToMentionPath } from "../../utils/path-mentions"
|
||||
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
||||
import ContextMenu from "./ContextMenu"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||
import { Mode, getAllModes } from "../../../../src/shared/modes"
|
||||
import { convertToMentionPath } from "../../utils/path-mentions"
|
||||
import { SelectDropdown, DropdownOptionType } from "../ui"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
|
|
|
|||
|
|
@ -1275,7 +1275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
modeShortcutText={modeShortcutText}
|
||||
/>
|
||||
|
||||
<div id="chat-view-portal" />
|
||||
<div id="roo-portal" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useState, useEffect, useCallback } from "react"
|
||||
import { useState, useCallback } from "react"
|
||||
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
|
||||
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
|
||||
import { useRooPortal } from "@/components/ui/hooks"
|
||||
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
import { Checkpoint } from "./schema"
|
||||
|
|
@ -14,9 +15,9 @@ type CheckpointMenuProps = {
|
|||
}
|
||||
|
||||
export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
|
||||
const [portalContainer, setPortalContainer] = useState<HTMLElement>()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isConfirming, setIsConfirming] = useState(false)
|
||||
const portalContainer = useRooPortal("roo-portal")
|
||||
|
||||
const isCurrent = currentHash === commitHash
|
||||
const isFirst = checkpoint.isFirst
|
||||
|
|
@ -42,15 +43,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
|
|||
setIsOpen(false)
|
||||
}, [ts, commitHash])
|
||||
|
||||
useEffect(() => {
|
||||
// The dropdown menu uses a portal from @shadcn/ui which by default renders
|
||||
// at the document root. This causes the menu to remain visible even when
|
||||
// the parent ChatView component is hidden (during settings/history view).
|
||||
// By moving the portal inside ChatView, the menu will properly hide when
|
||||
// its parent is hidden.
|
||||
setPortalContainer(document.getElementById("chat-view-portal") || undefined)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-1">
|
||||
{isDiffAvailable && (
|
||||
|
|
|
|||
15
webview-ui/src/components/common/Alert.tsx
Normal file
15
webview-ui/src/components/common/Alert.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { cn } from "@/lib/utils"
|
||||
import { HTMLAttributes } from "react"
|
||||
|
||||
type AlertProps = HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const Alert = ({ className, children, ...props }: AlertProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"text-vscode-inputValidation-infoForeground bg-vscode-inputValidation-infoBackground border border-vscode-inputValidation-infoBorder rounded-xs p-2",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
23
webview-ui/src/components/common/Tab.tsx
Normal file
23
webview-ui/src/components/common/Tab.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type TabProps = HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const Tab = ({ className, children, ...props }: TabProps) => (
|
||||
<div className={cn("fixed inset-0 flex flex-col overflow-hidden", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const TabHeader = ({ className, children, ...props }: TabProps) => (
|
||||
<div className={cn("px-5 py-2.5 border-b border-vscode-panel-border", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const TabContent = ({ className, children, ...props }: TabProps) => (
|
||||
<div className={cn("flex-1 overflow-auto p-5", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -9,6 +9,7 @@ import { formatLargeNumber, formatDate } from "@/utils/format"
|
|||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui"
|
||||
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { useTaskSearch } from "./useTaskSearch"
|
||||
import { ExportButton } from "./ExportButton"
|
||||
import { CopyButton } from "./CopyButton"
|
||||
|
|
@ -25,8 +26,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col">
|
||||
<div className="flex flex-col gap-2 px-5 py-2.5 border-b border-vscode-panel-border">
|
||||
<Tab>
|
||||
<TabHeader className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-vscode-foreground m-0">History</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
|
|
@ -81,8 +82,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, overflowY: "auto", margin: 0 }}>
|
||||
</TabHeader>
|
||||
|
||||
<TabContent className="p-0">
|
||||
<Virtuoso
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
|
|
@ -312,11 +314,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
|
||||
{deleteTaskId && (
|
||||
<DeleteTaskDialog taskId={deleteTaskId} onOpenChange={(open) => !open && setDeleteTaskId(null)} open />
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState } from "react"
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeCheckbox,
|
||||
|
|
@ -6,14 +7,17 @@ import {
|
|||
VSCodePanelTab,
|
||||
VSCodePanelView,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
|
||||
import { McpServer } from "../../../../src/shared/mcp"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui"
|
||||
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import McpToolRow from "./McpToolRow"
|
||||
import McpResourceRow from "./McpResourceRow"
|
||||
import McpEnabledToggle from "./McpEnabledToggle"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog"
|
||||
|
||||
type McpViewProps = {
|
||||
onDone: () => void
|
||||
|
|
@ -29,12 +33,13 @@ const McpView = ({ onDone }: McpViewProps) => {
|
|||
} = useExtensionState()
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col">
|
||||
<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
|
||||
<Tab>
|
||||
<TabHeader className="flex justify-between items-center">
|
||||
<h3 className="text-vscode-foreground m-0">MCP Servers</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-5">
|
||||
</TabHeader>
|
||||
|
||||
<TabContent>
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
|
|
@ -103,12 +108,11 @@ const McpView = ({ onDone }: McpViewProps) => {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabContent>
|
||||
</Tab>
|
||||
)
|
||||
}
|
||||
|
||||
// Server Row Component
|
||||
const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
|
||||
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
|
||||
// Get all available groups that should show in prompts view
|
||||
const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
|
||||
|
|
@ -419,12 +420,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col">
|
||||
<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
|
||||
<Tab>
|
||||
<TabHeader className="flex justify-between items-center">
|
||||
<h3 className="text-vscode-foreground m-0">Prompts</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-5">
|
||||
</TabHeader>
|
||||
|
||||
<TabContent>
|
||||
<div className="pb-5 border-b border-vscode-input-border">
|
||||
<div className="mb-5">
|
||||
<div className="font-bold mb-1">Preferred Language</div>
|
||||
|
|
@ -946,6 +948,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: "40px",
|
||||
|
|
@ -1213,7 +1216,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabContent>
|
||||
|
||||
{isCreateModeDialogOpen && (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -1437,6 +1441,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDialogOpen && (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -1504,6 +1509,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCustomLanguage && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96">
|
||||
|
|
@ -1538,7 +1544,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
Button,
|
||||
} from "@/components/ui"
|
||||
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { SetCachedStateField, SetExperimentEnabled } from "./types"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
import ApiConfigManager from "./ApiConfigManager"
|
||||
|
|
@ -263,53 +264,41 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => ref.current?.scrollIntoView()
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
||||
<div className="px-5 py-2.5 border-b border-vscode-panel-border">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-vscode-foreground m-0">Settings</h3>
|
||||
<div className="hidden [@media(min-width:400px)]:flex items-center">
|
||||
{sections.map(({ id, icon: Icon, ref }) => (
|
||||
<Button
|
||||
key={id}
|
||||
variant="ghost"
|
||||
onClick={() => scrollToSection(ref)}
|
||||
className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
|
||||
<Icon />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<VSCodeButton
|
||||
appearance={isSettingValid ? "primary" : "secondary"}
|
||||
className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
|
||||
title={
|
||||
!isSettingValid
|
||||
? errorMessage
|
||||
: isChangeDetected
|
||||
? "Save changes"
|
||||
: "Nothing changed"
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
disabled={!isChangeDetected || !isSettingValid}>
|
||||
Save
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
title="Discard unsaved changes and close settings panel"
|
||||
onClick={() => checkUnsaveChanges(onDone)}>
|
||||
Done
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<Tab>
|
||||
<TabHeader className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-vscode-foreground m-0">Settings</h3>
|
||||
<div className="hidden [@media(min-width:400px)]:flex items-center">
|
||||
{sections.map(({ id, icon: Icon, ref }) => (
|
||||
<Button
|
||||
key={id}
|
||||
variant="ghost"
|
||||
onClick={() => scrollToSection(ref)}
|
||||
className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
|
||||
<Icon />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<VSCodeButton
|
||||
appearance={isSettingValid ? "primary" : "secondary"}
|
||||
className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
|
||||
title={!isSettingValid ? errorMessage : isChangeDetected ? "Save changes" : "Nothing changed"}
|
||||
onClick={handleSubmit}
|
||||
disabled={!isChangeDetected || !isSettingValid}>
|
||||
Save
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
title="Discard unsaved changes and close settings panel"
|
||||
onClick={() => checkUnsaveChanges(onDone)}>
|
||||
Done
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</TabHeader>
|
||||
|
||||
<div
|
||||
className="flex flex-col flex-1 overflow-auto divide-y divide-vscode-sideBar-background"
|
||||
onScroll={handleScroll}>
|
||||
<TabContent className="p-0 divide-y divide-vscode-sideBar-background" onScroll={handleScroll}>
|
||||
<div ref={providersRef}>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -425,7 +414,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
telemetrySetting={telemetrySetting}
|
||||
setTelemetrySetting={setTelemetrySetting}
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
|
||||
<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
|
||||
<AlertDialogContent>
|
||||
|
|
@ -442,7 +431,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</Tab>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { PortalProps } from "@radix-ui/react-portal"
|
||||
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
|
@ -53,9 +54,7 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
|
|||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
|
||||
container?: HTMLElement
|
||||
}
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & Pick<PortalProps, "container">
|
||||
>(({ className, sideOffset = 4, container, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal container={container}>
|
||||
<DropdownMenuPrimitive.Content
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "./useClipboard"
|
||||
export * from "./useRooPortal"
|
||||
|
|
|
|||
10
webview-ui/src/components/ui/hooks/useRooPortal.ts
Normal file
10
webview-ui/src/components/ui/hooks/useRooPortal.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useState } from "react"
|
||||
import { useMount } from "react-use"
|
||||
|
||||
export const useRooPortal = (id: string) => {
|
||||
const [container, setContainer] = useState<HTMLElement>()
|
||||
|
||||
useMount(() => setContainer(document.getElementById(id) ?? undefined))
|
||||
|
||||
return container
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import * as React from "react"
|
||||
import { PortalProps } from "@radix-ui/react-portal"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
|
@ -11,9 +12,7 @@ const PopoverAnchor = PopoverPrimitive.Anchor
|
|||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||
container?: HTMLElement
|
||||
}
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Pick<PortalProps, "container">
|
||||
>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal container={container}>
|
||||
<PopoverPrimitive.Content
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import { useRooPortal } from "./hooks/useRooPortal"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -6,10 +10,7 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "./dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
// Constants for option types
|
||||
export enum DropdownOptionType {
|
||||
ITEM = "item",
|
||||
SEPARATOR = "separator",
|
||||
|
|
@ -20,7 +21,7 @@ export interface DropdownOption {
|
|||
value: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
type?: DropdownOptionType // Optional type to specify special behaviors
|
||||
type?: DropdownOptionType
|
||||
}
|
||||
|
||||
export interface SelectDropdownProps {
|
||||
|
|
@ -58,34 +59,19 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
|
|||
},
|
||||
ref,
|
||||
) => {
|
||||
// Track open state
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [portalContainer, setPortalContainer] = useState<HTMLElement>()
|
||||
const portalContainer = useRooPortal("roo-portal")
|
||||
|
||||
useEffect(() => {
|
||||
// The dropdown menu uses a portal from @shadcn/ui which by default renders
|
||||
// at the document root. This causes the menu to remain visible even when
|
||||
// the parent ChatView component is hidden (during settings/history view).
|
||||
// By moving the portal inside ChatView, the menu will properly hide when
|
||||
// its parent is hidden.
|
||||
setPortalContainer(document.getElementById("chat-view-portal") || undefined)
|
||||
}, [])
|
||||
|
||||
// Find the selected option label
|
||||
const selectedOption = options.find((option) => option.value === value)
|
||||
const displayText = selectedOption?.label || placeholder || ""
|
||||
|
||||
// Handle menu item click
|
||||
const handleSelect = (option: DropdownOption) => {
|
||||
// Check if this is an action option by its explicit type
|
||||
if (option.type === DropdownOptionType.ACTION) {
|
||||
window.postMessage({
|
||||
type: "action",
|
||||
action: option.value,
|
||||
})
|
||||
window.postMessage({ type: "action", action: option.value })
|
||||
setOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
onChange(option.value)
|
||||
setOpen(false)
|
||||
}
|
||||
|
|
@ -103,7 +89,7 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
|
|||
triggerClassName,
|
||||
)}
|
||||
style={{
|
||||
width: "100%", // Take full width of parent
|
||||
width: "100%", // Take full width of parent.
|
||||
minWidth: "0",
|
||||
maxWidth: "100%",
|
||||
}}>
|
||||
|
|
@ -136,12 +122,10 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
|
|||
contentClassName,
|
||||
)}>
|
||||
{options.map((option, index) => {
|
||||
// Handle separator type
|
||||
if (option.type === DropdownOptionType.SEPARATOR) {
|
||||
return <DropdownMenuSeparator key={`sep-${index}`} />
|
||||
}
|
||||
|
||||
// Handle shortcut text type (disabled label for keyboard shortcuts)
|
||||
if (
|
||||
option.type === DropdownOptionType.SHORTCUT ||
|
||||
(option.disabled && shortcutText && option.label.includes(shortcutText))
|
||||
|
|
@ -153,7 +137,6 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
|
|||
)
|
||||
}
|
||||
|
||||
// Regular menu items
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`item-${option.value}`}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as React from "react"
|
||||
import { PortalProps } from "@radix-ui/react-portal"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
|
|
@ -37,10 +38,11 @@ function SelectContent({
|
|||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
container,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content> & Pick<PortalProps, "container">) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Portal container={container}>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { validateApiConfiguration } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import ApiOptions from "../settings/ApiOptions"
|
||||
import { Tab, TabContent } from "../common/Tab"
|
||||
import { Alert } from "../common/Alert"
|
||||
|
||||
const WelcomeView = () => {
|
||||
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
|
||||
|
|
@ -23,18 +26,16 @@ const WelcomeView = () => {
|
|||
}, [apiConfiguration, currentApiConfigName])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen px-0 pb-5">
|
||||
<h2>Hi, I'm Roo!</h2>
|
||||
<p>
|
||||
I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access
|
||||
to tools that let me create & edit files, explore complex projects, use the browser, and execute
|
||||
terminal commands (with your permission, of course). I can even use MCP to create new tools and extend
|
||||
my own capabilities.
|
||||
</p>
|
||||
|
||||
<b>To get started, this extension needs an API provider.</b>
|
||||
|
||||
<div className="mt-3">
|
||||
<Tab>
|
||||
<TabContent className="flex flex-col gap-5">
|
||||
<h2 className="m-0 p-0">Hi, I'm Roo!</h2>
|
||||
<div>
|
||||
I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and
|
||||
access to tools that let me create & edit files, explore complex projects, use the browser, and
|
||||
execute terminal commands (with your permission, of course). I can even use MCP to create new tools
|
||||
and extend my own capabilities.
|
||||
</div>
|
||||
<Alert className="font-bold text-sm">To get started, this extension needs an API provider.</Alert>
|
||||
<ApiOptions
|
||||
fromWelcomeView
|
||||
apiConfiguration={apiConfiguration || {}}
|
||||
|
|
@ -43,15 +44,14 @@ const WelcomeView = () => {
|
|||
errorMessage={errorMessage}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 bg-[var(--vscode-sideBar-background)] py-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
</TabContent>
|
||||
<div className="sticky bottom-0 bg-vscode-sideBar-background p-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<VSCodeButton onClick={handleSubmit}>Let's go!</VSCodeButton>
|
||||
{errorMessage && <span className="text-destructive">{errorMessage}</span>}
|
||||
{errorMessage && <div className="text-vscode-errorForeground">{errorMessage}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@
|
|||
|
||||
--color-vscode-charts-green: var(--vscode-charts-green);
|
||||
--color-vscode-charts-yellow: var(--vscode-charts-yellow);
|
||||
|
||||
--color-vscode-inputValidation-infoForeground: var(--vscode-inputValidation-infoForeground);
|
||||
--color-vscode-inputValidation-infoBackground: var(--vscode-inputValidation-infoBackground);
|
||||
--color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue