mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-10 22:41:14 +00:00
Allow processes to access the Roo Code API via a unix socket (#2232)
This commit is contained in:
parent
0409509002
commit
a73fce9027
24 changed files with 2619 additions and 330 deletions
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
|||
nodejs v20.18.1
|
||||
|
|
@ -156,7 +156,7 @@ while (Date.now() - startTime < timeout) {
|
|||
6. **Grading**: When grading tests, use the `Grade:` format to ensure the test is graded correctly (See modes.test.ts for an example).
|
||||
|
||||
```typescript
|
||||
await globalThis.api.startNewTask(
|
||||
`Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`,
|
||||
)
|
||||
await globalThis.api.startNewTask({
|
||||
text: `Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`,
|
||||
})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cd .. && npm run compile && npm run build:webview",
|
||||
"compile": "rm -rf out && tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext ts",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "npm run compile && npx dotenvx run -f .env.local -- node ./out/runTest.js",
|
||||
"ci": "npm run build && npm run test",
|
||||
"clean": "rimraf out"
|
||||
"test": "npm run build && npx dotenvx run -f .env.local -- node ./out/runTest.js",
|
||||
"ci": "npm run vscode-test && npm run test",
|
||||
"build": "rimraf out && tsc -p tsconfig.json",
|
||||
"vscode-test": "cd .. && npm run vscode-test"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,6 @@ import * as assert from "assert"
|
|||
import * as vscode from "vscode"
|
||||
|
||||
suite("Roo Code Extension", () => {
|
||||
test("OPENROUTER_API_KEY environment variable is set", () => {
|
||||
if (!process.env.OPENROUTER_API_KEY) {
|
||||
assert.fail("OPENROUTER_API_KEY environment variable is not set")
|
||||
}
|
||||
})
|
||||
|
||||
test("Commands should be registered", async () => {
|
||||
const expectedCommands = [
|
||||
"roo-cline.plusButtonClicked",
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import Mocha from "mocha"
|
|||
import { glob } from "glob"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { RooCodeAPI } from "../../../src/exports/roo-code"
|
||||
import type { RooCodeAPI } from "../../../src/exports/roo-code"
|
||||
|
||||
import { waitUntilReady } from "./utils"
|
||||
import { waitFor } from "./utils"
|
||||
|
||||
declare global {
|
||||
var api: RooCodeAPI
|
||||
|
|
@ -18,18 +18,25 @@ export async function run() {
|
|||
throw new Error("Extension not found")
|
||||
}
|
||||
|
||||
// Activate the extension if it's not already active.
|
||||
const api = extension.isActive ? extension.exports : await extension.activate()
|
||||
|
||||
// TODO: We might want to support a "free" model out of the box so
|
||||
// contributors can run the tests locally without having to pay.
|
||||
await api.setConfiguration({
|
||||
apiProvider: "openrouter",
|
||||
apiProvider: "openrouter" as const,
|
||||
openRouterApiKey: process.env.OPENROUTER_API_KEY!,
|
||||
openRouterModelId: "anthropic/claude-3.5-sonnet",
|
||||
openRouterModelId: "google/gemini-2.0-flash-001",
|
||||
openRouterModelInfo: {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1000000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.4,
|
||||
thinking: false,
|
||||
},
|
||||
})
|
||||
|
||||
await waitUntilReady({ api })
|
||||
await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
|
||||
await waitFor(() => api.isReady())
|
||||
|
||||
// Expose the API to the tests.
|
||||
globalThis.api = api
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import * as assert from "assert"
|
||||
|
||||
import { getCompletion, getMessage, sleep, waitForCompletion, waitUntilAborted } from "./utils"
|
||||
import type { ClineMessage } from "../../../src/exports/roo-code"
|
||||
|
||||
import { waitUntilCompleted } from "./utils"
|
||||
|
||||
suite("Roo Code Modes", () => {
|
||||
test("Should handle switching modes correctly", async function () {
|
||||
test("Should handle switching modes correctly", async () => {
|
||||
const api = globalThis.api
|
||||
|
||||
/**
|
||||
|
|
@ -14,30 +16,44 @@ suite("Roo Code Modes", () => {
|
|||
"For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode. " +
|
||||
"Do not start with the current mode."
|
||||
|
||||
await api.setConfiguration({ mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
|
||||
const switchModesTaskId = await api.startNewTask(switchModesPrompt)
|
||||
await waitForCompletion({ api, taskId: switchModesTaskId, timeout: 60_000 })
|
||||
let messages: ClineMessage[] = []
|
||||
|
||||
api.on("message", ({ message }) => {
|
||||
if (message.type === "say" && message.partial === false) {
|
||||
messages.push(message)
|
||||
}
|
||||
})
|
||||
|
||||
const switchModesTaskId = await api.startNewTask({
|
||||
configuration: { mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
|
||||
text: switchModesPrompt,
|
||||
})
|
||||
|
||||
await waitUntilCompleted({ api, taskId: switchModesTaskId, timeout: 60_000 })
|
||||
|
||||
/**
|
||||
* Grade the response.
|
||||
*/
|
||||
|
||||
const gradePrompt =
|
||||
`Given this prompt: ${switchModesPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ` +
|
||||
api
|
||||
.getMessages(switchModesTaskId)
|
||||
.filter(({ type }) => type === "say")
|
||||
.map(({ text }) => text ?? "")
|
||||
.join("\n")
|
||||
const response = messages
|
||||
.filter(({ type, say, partial }) => say === "text")
|
||||
.map(({ text }) => text ?? "")
|
||||
.join("\n")
|
||||
|
||||
await api.setConfiguration({ mode: "Ask" })
|
||||
const gradeTaskId = await api.startNewTask(gradePrompt)
|
||||
await waitForCompletion({ api, taskId: gradeTaskId, timeout: 60_000 })
|
||||
const gradePrompt = `Given this prompt: ${switchModesPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)". For example: Grade 7\n\nResponse: ${response}`
|
||||
|
||||
const completion = getCompletion({ api, taskId: gradeTaskId })
|
||||
messages = []
|
||||
|
||||
const gradeTaskId = await api.startNewTask({ configuration: { mode: "Ask" }, text: gradePrompt })
|
||||
await waitUntilCompleted({ api, taskId: gradeTaskId })
|
||||
|
||||
const completion = messages.find(({ type, say, partial }) => say === "completion_result")
|
||||
const match = completion?.text?.match(/Grade: (\d+)/)
|
||||
const score = parseInt(match?.[1] ?? "0")
|
||||
assert.ok(score >= 7 && score <= 10, `Grade must be between 7 and 10 - ${completion?.text}`)
|
||||
assert.ok(
|
||||
score >= 7 && score <= 10,
|
||||
`Grade must be between 7 and 10. DEBUG: score = ${score}, completion = ${completion?.text}`,
|
||||
)
|
||||
|
||||
await api.cancelCurrentTask()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
import * as assert from "assert"
|
||||
|
||||
import { sleep, waitFor, getMessage, waitForCompletion } from "./utils"
|
||||
import type { ClineMessage } from "../../../src/exports/roo-code"
|
||||
|
||||
import { sleep, waitFor, waitUntilCompleted } from "./utils"
|
||||
|
||||
suite("Roo Code Subtasks", () => {
|
||||
test("Should handle subtask cancellation and resumption correctly", async function () {
|
||||
test("Should handle subtask cancellation and resumption correctly", async () => {
|
||||
const api = globalThis.api
|
||||
|
||||
const messages: Record<string, ClineMessage[]> = {}
|
||||
|
||||
api.on("message", ({ taskId, message }) => {
|
||||
if (message.type === "say" && message.partial === false) {
|
||||
messages[taskId] = messages[taskId] || []
|
||||
messages[taskId].push(message)
|
||||
}
|
||||
})
|
||||
|
||||
await api.setConfiguration({
|
||||
mode: "Code",
|
||||
mode: "ask",
|
||||
alwaysAllowModeSwitch: true,
|
||||
alwaysAllowSubtasks: true,
|
||||
autoApprovalEnabled: true,
|
||||
|
|
@ -17,18 +28,19 @@ suite("Roo Code Subtasks", () => {
|
|||
const childPrompt = "You are a calculator. Respond only with numbers. What is the square root of 9?"
|
||||
|
||||
// Start a parent task that will create a subtask.
|
||||
const parentTaskId = await api.startNewTask(
|
||||
"You are the parent task. " +
|
||||
const parentTaskId = await api.startNewTask({
|
||||
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'.",
|
||||
)
|
||||
})
|
||||
|
||||
let spawnedTaskId: string | undefined = undefined
|
||||
|
||||
// Wait for the subtask to be spawned and then cancel it.
|
||||
api.on("taskSpawned", (_, childTaskId) => (spawnedTaskId = childTaskId))
|
||||
await waitFor(() => !!spawnedTaskId)
|
||||
await sleep(2_000) // Give the task a chance to start and populate the history.
|
||||
await sleep(1_000) // Give the task a chance to start and populate the history.
|
||||
await api.cancelCurrentTask()
|
||||
|
||||
// Wait a bit to ensure any task resumption would have happened.
|
||||
|
|
@ -37,35 +49,27 @@ suite("Roo Code Subtasks", () => {
|
|||
// The parent task should not have resumed yet, so we shouldn't see
|
||||
// "Parent task resumed".
|
||||
assert.ok(
|
||||
getMessage({
|
||||
api,
|
||||
taskId: parentTaskId,
|
||||
include: "Parent task resumed",
|
||||
exclude: "You are the parent task",
|
||||
}) === undefined,
|
||||
messages[parentTaskId].find(({ type, text }) => type === "say" && text === "Parent task resumed") ===
|
||||
undefined,
|
||||
"Parent task should not have resumed after subtask cancellation",
|
||||
)
|
||||
|
||||
// Start a new task with the same message as the subtask.
|
||||
const anotherTaskId = await api.startNewTask(childPrompt)
|
||||
await waitForCompletion({ api, taskId: anotherTaskId })
|
||||
const anotherTaskId = await api.startNewTask({ text: childPrompt })
|
||||
await waitUntilCompleted({ api, taskId: anotherTaskId })
|
||||
|
||||
// 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(
|
||||
getMessage({
|
||||
api,
|
||||
taskId: parentTaskId,
|
||||
include: "Parent task resumed",
|
||||
exclude: "You are the parent task",
|
||||
}) === undefined,
|
||||
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 waitForCompletion({ api, taskId: parentTaskId })
|
||||
await waitUntilCompleted({ api, taskId: parentTaskId })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,33 @@
|
|||
import { waitForMessage } from "./utils"
|
||||
import * as assert from "assert"
|
||||
|
||||
import type { ClineMessage } from "../../../src/exports/roo-code"
|
||||
|
||||
import { waitUntilCompleted } from "./utils"
|
||||
|
||||
suite("Roo Code Task", () => {
|
||||
test("Should handle prompt and response correctly", async function () {
|
||||
test("Should handle prompt and response correctly", async () => {
|
||||
const api = globalThis.api
|
||||
await api.setConfiguration({ mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
|
||||
const taskId = await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
|
||||
await waitForMessage({ api, taskId, include: "My name is Roo" })
|
||||
|
||||
const messages: ClineMessage[] = []
|
||||
|
||||
api.on("message", ({ message }) => {
|
||||
if (message.type === "say" && message.partial === false) {
|
||||
messages.push(message)
|
||||
}
|
||||
})
|
||||
|
||||
const taskId = await api.startNewTask({
|
||||
configuration: { mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
|
||||
text: "Hello world, what is your name? Respond with 'My name is ...'",
|
||||
})
|
||||
|
||||
await waitUntilCompleted({ api, taskId })
|
||||
|
||||
const completion = messages.find(({ say, partial }) => say === "completion_result")
|
||||
|
||||
assert.ok(
|
||||
completion?.text?.includes("My name is Roo"),
|
||||
`Completion should include "My name is Roo" - ${completion?.text}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as vscode from "vscode"
|
||||
|
||||
import { RooCodeAPI } from "../../../src/exports/roo-code"
|
||||
import type { RooCodeAPI } from "../../../src/exports/roo-code"
|
||||
|
||||
type WaitForOptions = {
|
||||
timeout?: number
|
||||
|
|
@ -9,7 +9,7 @@ type WaitForOptions = {
|
|||
|
||||
export const waitFor = (
|
||||
condition: (() => Promise<boolean>) | (() => boolean),
|
||||
{ timeout = 60_000, interval = 250 }: WaitForOptions = {},
|
||||
{ timeout = 30_000, interval = 250 }: WaitForOptions = {},
|
||||
) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
|
|
@ -41,15 +41,6 @@ export const waitFor = (
|
|||
])
|
||||
}
|
||||
|
||||
type WaitUntilReadyOptions = WaitForOptions & {
|
||||
api: RooCodeAPI
|
||||
}
|
||||
|
||||
export const waitUntilReady = async ({ api, ...options }: WaitUntilReadyOptions) => {
|
||||
await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
|
||||
await waitFor(() => api.isReady(), options)
|
||||
}
|
||||
|
||||
type WaitUntilAbortedOptions = WaitForOptions & {
|
||||
api: RooCodeAPI
|
||||
taskId: string
|
||||
|
|
@ -61,39 +52,15 @@ export const waitUntilAborted = async ({ api, taskId, ...options }: WaitUntilAbo
|
|||
await waitFor(() => set.has(taskId), options)
|
||||
}
|
||||
|
||||
export const waitForCompletion = async ({
|
||||
api,
|
||||
taskId,
|
||||
...options
|
||||
}: WaitUntilReadyOptions & {
|
||||
taskId: string
|
||||
}) => waitFor(() => !!getCompletion({ api, taskId }), options)
|
||||
|
||||
export const getCompletion = ({ api, taskId }: { api: RooCodeAPI; taskId: string }) =>
|
||||
api.getMessages(taskId).find(({ say, partial }) => say === "completion_result" && partial === false)
|
||||
|
||||
type WaitForMessageOptions = WaitUntilReadyOptions & {
|
||||
taskId: string
|
||||
include: string
|
||||
exclude?: string
|
||||
}
|
||||
|
||||
export const waitForMessage = async ({ api, taskId, include, exclude, ...options }: WaitForMessageOptions) =>
|
||||
waitFor(() => !!getMessage({ api, taskId, include, exclude }), options)
|
||||
|
||||
type GetMessageOptions = {
|
||||
type WaitUntilCompletedOptions = WaitForOptions & {
|
||||
api: RooCodeAPI
|
||||
taskId: string
|
||||
include: string
|
||||
exclude?: string
|
||||
}
|
||||
|
||||
export const getMessage = ({ api, taskId, include, exclude }: GetMessageOptions) =>
|
||||
api
|
||||
.getMessages(taskId)
|
||||
.find(
|
||||
({ type, text }) =>
|
||||
type === "say" && text && text.includes(include) && (!exclude || !text.includes(exclude)),
|
||||
)
|
||||
export const waitUntilCompleted = async ({ api, taskId, ...options }: WaitUntilCompletedOptions) => {
|
||||
const set = new Set<string>()
|
||||
api.on("taskCompleted", (taskId) => set.add(taskId))
|
||||
await waitFor(() => set.has(taskId), options)
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"benchmark/**",
|
||||
"src/activate/**",
|
||||
"src/exports/**",
|
||||
"src/schemas/ipc.ts",
|
||||
"src/extension.ts",
|
||||
"scripts/**"
|
||||
],
|
||||
|
|
|
|||
1782
package-lock.json
generated
1782
package-lock.json
generated
File diff suppressed because it is too large
Load diff
22
package.json
22
package.json
|
|
@ -312,23 +312,19 @@
|
|||
"build:webview": "cd webview-ui && npm run build",
|
||||
"build:esbuild": "node esbuild.js --production",
|
||||
"compile": "tsc -p . --outDir out && node esbuild.js",
|
||||
"install:all": "npm install npm-run-all && npm run install:_all",
|
||||
"install:_all": "npm-run-all -p install-*",
|
||||
"install:all": "npm install npm-run-all && npm-run-all -l -p install-*",
|
||||
"install-extension": "npm install",
|
||||
"install-webview": "cd webview-ui && npm install",
|
||||
"install-e2e": "cd e2e && npm install",
|
||||
"install-benchmark": "cd benchmark && npm install",
|
||||
"lint": "npm-run-all -p lint:*",
|
||||
"lint": "npm-run-all -l -p lint:*",
|
||||
"lint:extension": "eslint src --ext ts",
|
||||
"lint:webview": "cd webview-ui && npm run lint",
|
||||
"lint:e2e": "cd e2e && npm run lint",
|
||||
"lint:benchmark": "cd benchmark && npm run lint",
|
||||
"check-types": "npm-run-all -p check-types:*",
|
||||
"check-types": "npm-run-all -l -p check-types:*",
|
||||
"check-types:extension": "tsc --noEmit",
|
||||
"check-types:webview": "cd webview-ui && npm run check-types",
|
||||
"check-types:e2e": "cd e2e && npm run check-types",
|
||||
"check-types:benchmark": "cd benchmark && npm run check-types",
|
||||
"package": "npm-run-all -p build:webview build:esbuild check-types lint",
|
||||
"package": "npm-run-all -l -p build:webview build:esbuild check-types lint",
|
||||
"pretest": "npm run compile",
|
||||
"dev": "cd webview-ui && npm run dev",
|
||||
"test": "node scripts/run-tests.js",
|
||||
|
|
@ -340,17 +336,20 @@
|
|||
"version-packages": "changeset version && npm install --package-lock-only",
|
||||
"vscode:prepublish": "npm run package",
|
||||
"vsix": "rimraf bin && mkdirp bin && npx vsce package --out bin",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch": "npm-run-all -l -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"changeset": "changeset",
|
||||
"knip": "knip --include files",
|
||||
"clean": "npm-run-all -p clean:*",
|
||||
"clean": "npm-run-all -l -p clean:*",
|
||||
"clean:extension": "rimraf bin dist out",
|
||||
"clean:webview": "cd webview-ui && npm run clean",
|
||||
"clean:e2e": "cd e2e && npm run clean",
|
||||
"clean:benchmark": "cd benchmark && npm run clean",
|
||||
"vscode-test": "npm-run-all -l -p vscode-test:*",
|
||||
"vscode-test:extension": "tsc -p . --outDir out && node esbuild.js",
|
||||
"vscode-test:webview": "cd webview-ui && npm run build",
|
||||
"update-contributors": "node scripts/update-contributors.js",
|
||||
"generate-types": "tsx scripts/generate-types.mts"
|
||||
},
|
||||
|
|
@ -388,6 +387,7 @@
|
|||
"js-tiktoken": "^1.0.19",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"node-ipc": "^12.0.0",
|
||||
"openai": "^4.78.1",
|
||||
"os-name": "^6.0.0",
|
||||
"p-wait-for": "^5.0.2",
|
||||
|
|
@ -421,9 +421,11 @@
|
|||
"@types/glob": "^8.1.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "20.x",
|
||||
"@types/node-ipc": "^9.2.3",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/vsce": "^3.3.2",
|
||||
"esbuild": "^0.24.0",
|
||||
"eslint": "^8.57.0",
|
||||
"execa": "^9.5.2",
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
|
|||
}
|
||||
}
|
||||
|
||||
const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
|
||||
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
|
||||
// (This example uses webviewProvider activation event which is necessary to
|
||||
// deserialize cached webview, but since we use retainContextWhenHidden, we
|
||||
// don't need to use that event).
|
||||
|
|
@ -139,4 +139,6 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterComman
|
|||
// Lock the editor group so clicking on files doesn't open them over the panel.
|
||||
await delay(100)
|
||||
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
|
||||
|
||||
return tabProvider
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,105 +2,201 @@ import { EventEmitter } from "events"
|
|||
import * as vscode from "vscode"
|
||||
|
||||
import { ClineProvider } from "../core/webview/ClineProvider"
|
||||
import { openClineInNewTab } from "../activate/registerCommands"
|
||||
|
||||
import { RooCodeAPI, RooCodeEvents, TokenUsage, RooCodeSettings } from "./roo-code"
|
||||
import { MessageHistory } from "./message-history"
|
||||
import { RooCodeSettings, RooCodeEvents, RooCodeEventName, ClineMessage } from "../schemas"
|
||||
import { IpcOrigin, IpcMessageType, TaskCommandName, TaskEvent } from "../schemas/ipc"
|
||||
import { RooCodeAPI } from "./interface"
|
||||
import { IpcServer } from "./ipc"
|
||||
import { outputChannelLog } from "./log"
|
||||
|
||||
export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
|
||||
private readonly outputChannel: vscode.OutputChannel
|
||||
private readonly provider: ClineProvider
|
||||
private readonly history: MessageHistory
|
||||
private readonly tokenUsage: Record<string, TokenUsage>
|
||||
private readonly sidebarProvider: ClineProvider
|
||||
private tabProvider?: ClineProvider
|
||||
private readonly context: vscode.ExtensionContext
|
||||
private readonly ipc?: IpcServer
|
||||
private readonly taskMap = new Map<string, ClineProvider>()
|
||||
private readonly log: (...args: unknown[]) => void
|
||||
|
||||
constructor(outputChannel: vscode.OutputChannel, provider: ClineProvider) {
|
||||
constructor(
|
||||
outputChannel: vscode.OutputChannel,
|
||||
provider: ClineProvider,
|
||||
socketPath?: string,
|
||||
enableLogging = false,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.outputChannel = outputChannel
|
||||
this.provider = provider
|
||||
this.history = new MessageHistory()
|
||||
this.tokenUsage = {}
|
||||
this.sidebarProvider = provider
|
||||
this.context = provider.context
|
||||
|
||||
this.provider.on("clineCreated", (cline) => {
|
||||
cline.on("message", (message) => this.emit("message", { taskId: cline.taskId, ...message }))
|
||||
cline.on("taskStarted", () => this.emit("taskStarted", cline.taskId))
|
||||
cline.on("taskPaused", () => this.emit("taskPaused", cline.taskId))
|
||||
cline.on("taskUnpaused", () => this.emit("taskUnpaused", cline.taskId))
|
||||
cline.on("taskAskResponded", () => this.emit("taskAskResponded", cline.taskId))
|
||||
cline.on("taskAborted", () => this.emit("taskAborted", cline.taskId))
|
||||
cline.on("taskSpawned", (childTaskId) => this.emit("taskSpawned", cline.taskId, childTaskId))
|
||||
cline.on("taskCompleted", (_, usage) => this.emit("taskCompleted", cline.taskId, usage))
|
||||
cline.on("taskTokenUsageUpdated", (_, usage) => this.emit("taskTokenUsageUpdated", cline.taskId, usage))
|
||||
this.emit("taskCreated", cline.taskId)
|
||||
})
|
||||
this.log = enableLogging
|
||||
? (...args: unknown[]) => {
|
||||
outputChannelLog(this.outputChannel, ...args)
|
||||
console.log(args)
|
||||
}
|
||||
: () => {}
|
||||
|
||||
this.on("message", ({ taskId, action, message }) => {
|
||||
if (action === "created") {
|
||||
this.history.add(taskId, message)
|
||||
} else if (action === "updated") {
|
||||
this.history.update(taskId, message)
|
||||
}
|
||||
})
|
||||
this.registerListeners(this.sidebarProvider)
|
||||
|
||||
this.on("taskTokenUsageUpdated", (taskId, usage) => (this.tokenUsage[taskId] = usage))
|
||||
if (socketPath) {
|
||||
const ipc = (this.ipc = new IpcServer(socketPath, this.log))
|
||||
|
||||
ipc.listen()
|
||||
this.log(`[API] ipc server started: socketPath=${socketPath}, pid=${process.pid}, ppid=${process.ppid}`)
|
||||
|
||||
ipc.on(IpcMessageType.TaskCommand, async (_clientId, { commandName, data }) => {
|
||||
switch (commandName) {
|
||||
case TaskCommandName.StartNewTask:
|
||||
this.log(`[API] StartNewTask -> ${data.text}, ${JSON.stringify(data.configuration)}`)
|
||||
await this.startNewTask(data)
|
||||
break
|
||||
case TaskCommandName.CancelTask:
|
||||
this.log(`[API] CancelTask -> ${data}`)
|
||||
await this.cancelTask(data)
|
||||
break
|
||||
case TaskCommandName.CloseTask:
|
||||
this.log(`[API] CloseTask -> ${data}`)
|
||||
await vscode.commands.executeCommand("workbench.action.files.saveFiles")
|
||||
await vscode.commands.executeCommand("workbench.action.closeWindow")
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async startNewTask(text?: string, images?: string[]) {
|
||||
await this.provider.removeClineFromStack()
|
||||
await this.provider.postStateToWebview()
|
||||
await this.provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
await this.provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images })
|
||||
public override emit<K extends keyof RooCodeEvents>(
|
||||
eventName: K,
|
||||
...args: K extends keyof RooCodeEvents ? RooCodeEvents[K] : never
|
||||
) {
|
||||
const data = { eventName: eventName as RooCodeEventName, payload: args } as TaskEvent
|
||||
this.ipc?.broadcast({ type: IpcMessageType.TaskEvent, origin: IpcOrigin.Server, data })
|
||||
return super.emit(eventName, ...args)
|
||||
}
|
||||
|
||||
const cline = await this.provider.initClineWithTask(text, images)
|
||||
return cline.taskId
|
||||
public async startNewTask({
|
||||
configuration,
|
||||
text,
|
||||
images,
|
||||
newTab,
|
||||
}: {
|
||||
configuration: RooCodeSettings
|
||||
text?: string
|
||||
images?: string[]
|
||||
newTab?: boolean
|
||||
}) {
|
||||
let provider: ClineProvider
|
||||
|
||||
if (newTab) {
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
|
||||
if (!this.tabProvider) {
|
||||
this.tabProvider = await openClineInNewTab({ context: this.context, outputChannel: this.outputChannel })
|
||||
this.registerListeners(this.tabProvider)
|
||||
}
|
||||
|
||||
provider = this.tabProvider
|
||||
} else {
|
||||
provider = this.sidebarProvider
|
||||
}
|
||||
|
||||
if (configuration) {
|
||||
await provider.setValues(configuration)
|
||||
|
||||
if (configuration.allowedCommands) {
|
||||
await vscode.workspace
|
||||
.getConfiguration("roo-cline")
|
||||
.update("allowedCommands", configuration.allowedCommands, vscode.ConfigurationTarget.Global)
|
||||
}
|
||||
}
|
||||
|
||||
await provider.removeClineFromStack()
|
||||
await provider.postStateToWebview()
|
||||
await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images })
|
||||
|
||||
const { taskId } = await provider.initClineWithTask(text, images)
|
||||
return taskId
|
||||
}
|
||||
|
||||
public getCurrentTaskStack() {
|
||||
return this.provider.getCurrentTaskStack()
|
||||
return this.sidebarProvider.getCurrentTaskStack()
|
||||
}
|
||||
|
||||
public async clearCurrentTask(lastMessage?: string) {
|
||||
await this.provider.finishSubTask(lastMessage)
|
||||
await this.provider.postStateToWebview()
|
||||
await this.sidebarProvider.finishSubTask(lastMessage)
|
||||
await this.sidebarProvider.postStateToWebview()
|
||||
}
|
||||
|
||||
public async cancelCurrentTask() {
|
||||
await this.provider.cancelTask()
|
||||
await this.sidebarProvider.cancelTask()
|
||||
}
|
||||
|
||||
public async cancelTask(taskId: string) {
|
||||
const provider = this.taskMap.get(taskId)
|
||||
|
||||
if (provider) {
|
||||
await provider.cancelTask()
|
||||
this.taskMap.delete(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
public async sendMessage(text?: string, images?: string[]) {
|
||||
await this.provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
|
||||
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
|
||||
}
|
||||
|
||||
public async pressPrimaryButton() {
|
||||
await this.provider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
|
||||
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
|
||||
}
|
||||
|
||||
public async pressSecondaryButton() {
|
||||
await this.provider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
|
||||
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return this.provider.getValues()
|
||||
return this.sidebarProvider.getValues()
|
||||
}
|
||||
|
||||
public async setConfiguration(values: RooCodeSettings) {
|
||||
await this.provider.setValues(values)
|
||||
await this.provider.postStateToWebview()
|
||||
await this.sidebarProvider.setValues(values)
|
||||
await this.sidebarProvider.postStateToWebview()
|
||||
}
|
||||
|
||||
public isReady() {
|
||||
return this.provider.viewLaunched
|
||||
return this.sidebarProvider.viewLaunched
|
||||
}
|
||||
|
||||
public getMessages(taskId: string) {
|
||||
return this.history.getMessages(taskId)
|
||||
}
|
||||
private registerListeners(provider: ClineProvider) {
|
||||
provider.on("clineCreated", (cline) => {
|
||||
cline.on("taskStarted", () => {
|
||||
this.emit(RooCodeEventName.TaskStarted, cline.taskId)
|
||||
this.taskMap.set(cline.taskId, provider)
|
||||
})
|
||||
|
||||
public getTokenUsage(taskId: string) {
|
||||
return this.tokenUsage[taskId]
|
||||
}
|
||||
cline.on("message", (message) => this.emit(RooCodeEventName.Message, { taskId: cline.taskId, ...message }))
|
||||
|
||||
public log(message: string) {
|
||||
this.outputChannel.appendLine(message)
|
||||
cline.on("taskTokenUsageUpdated", (_, usage) =>
|
||||
this.emit(RooCodeEventName.TaskTokenUsageUpdated, cline.taskId, usage),
|
||||
)
|
||||
|
||||
cline.on("taskAskResponded", () => this.emit(RooCodeEventName.TaskAskResponded, cline.taskId))
|
||||
|
||||
cline.on("taskAborted", () => {
|
||||
this.emit(RooCodeEventName.TaskAborted, cline.taskId)
|
||||
this.taskMap.delete(cline.taskId)
|
||||
})
|
||||
|
||||
cline.on("taskCompleted", (_, usage) => {
|
||||
this.emit(RooCodeEventName.TaskCompleted, cline.taskId, usage)
|
||||
this.taskMap.delete(cline.taskId)
|
||||
})
|
||||
|
||||
cline.on("taskSpawned", (childTaskId) => this.emit(RooCodeEventName.TaskSpawned, cline.taskId, childTaskId))
|
||||
cline.on("taskPaused", () => this.emit(RooCodeEventName.TaskPaused, cline.taskId))
|
||||
cline.on("taskUnpaused", () => this.emit(RooCodeEventName.TaskUnpaused, cline.taskId))
|
||||
|
||||
this.emit(RooCodeEventName.TaskCreated, cline.taskId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,13 @@
|
|||
import { EventEmitter } from "events"
|
||||
|
||||
import type { ProviderSettings, GlobalSettings, ClineMessage, TokenUsage } from "./types"
|
||||
import type { ProviderSettings, GlobalSettings, ClineMessage, TokenUsage, RooCodeEvents } from "./types"
|
||||
export type { RooCodeSettings, ProviderSettings, GlobalSettings, ClineMessage, TokenUsage, RooCodeEvents }
|
||||
|
||||
import { RooCodeEventName } from "../schemas"
|
||||
export type { RooCodeEventName }
|
||||
|
||||
type RooCodeSettings = GlobalSettings & ProviderSettings
|
||||
|
||||
export type { RooCodeSettings, ProviderSettings, GlobalSettings, ClineMessage, TokenUsage }
|
||||
|
||||
export interface RooCodeEvents {
|
||||
message: [{ taskId: string; action: "created" | "updated"; message: ClineMessage }]
|
||||
taskCreated: [taskId: string]
|
||||
taskStarted: [taskId: string]
|
||||
taskPaused: [taskId: string]
|
||||
taskUnpaused: [taskId: string]
|
||||
taskAskResponded: [taskId: string]
|
||||
taskAborted: [taskId: string]
|
||||
taskSpawned: [taskId: string, childTaskId: string]
|
||||
taskCompleted: [taskId: string, usage: TokenUsage]
|
||||
taskTokenUsageUpdated: [taskId: string, usage: TokenUsage]
|
||||
}
|
||||
|
||||
export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
|
||||
/**
|
||||
* Starts a new task with an optional initial message and images.
|
||||
|
|
@ -26,7 +15,17 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
|
|||
* @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
|
||||
* @returns The ID of the new task.
|
||||
*/
|
||||
startNewTask(task?: string, images?: string[]): Promise<string>
|
||||
startNewTask({
|
||||
configuration,
|
||||
text,
|
||||
images,
|
||||
newTab,
|
||||
}: {
|
||||
configuration?: RooCodeSettings
|
||||
text?: string
|
||||
images?: string[]
|
||||
newTab?: boolean
|
||||
}): Promise<string>
|
||||
|
||||
/**
|
||||
* Returns the current task stack.
|
||||
|
|
@ -77,24 +76,4 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
|
|||
* Returns true if the API is ready to use.
|
||||
*/
|
||||
isReady(): boolean
|
||||
|
||||
/**
|
||||
* Returns the messages for a given task.
|
||||
* @param taskId The ID of the task.
|
||||
* @returns An array of ClineMessage objects.
|
||||
*/
|
||||
getMessages(taskId: string): ClineMessage[]
|
||||
|
||||
/**
|
||||
* Returns the token usage for a given task.
|
||||
* @param taskId The ID of the task.
|
||||
* @returns A TokenUsage object.
|
||||
*/
|
||||
getTokenUsage(taskId: string): TokenUsage
|
||||
|
||||
/**
|
||||
* Logs a message to the output channel.
|
||||
* @param message The message to log.
|
||||
*/
|
||||
log(message: string): void
|
||||
}
|
||||
|
|
|
|||
138
src/exports/ipc.ts
Normal file
138
src/exports/ipc.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import EventEmitter from "node:events"
|
||||
import { Socket } from "node:net"
|
||||
import * as crypto from "node:crypto"
|
||||
|
||||
import ipc from "node-ipc"
|
||||
|
||||
import { IpcOrigin, IpcMessageType, IpcMessage, ipcMessageSchema, TaskCommand, TaskEvent } from "../schemas/ipc"
|
||||
|
||||
/**
|
||||
* IpcServer
|
||||
*/
|
||||
|
||||
type IpcServerEvents = {
|
||||
[IpcMessageType.Connect]: [clientId: string]
|
||||
[IpcMessageType.Disconnect]: [clientId: string]
|
||||
[IpcMessageType.TaskCommand]: [clientId: string, data: TaskCommand]
|
||||
[IpcMessageType.TaskEvent]: [relayClientId: string | undefined, data: TaskEvent]
|
||||
}
|
||||
|
||||
export class IpcServer extends EventEmitter<IpcServerEvents> {
|
||||
private readonly _socketPath: string
|
||||
private readonly _log: (...args: unknown[]) => void
|
||||
private readonly _clients: Map<string, Socket>
|
||||
|
||||
private _isListening = false
|
||||
|
||||
constructor(socketPath: string, log = console.log) {
|
||||
super()
|
||||
|
||||
this._socketPath = socketPath
|
||||
this._log = log
|
||||
this._clients = new Map()
|
||||
}
|
||||
|
||||
public listen() {
|
||||
this._isListening = true
|
||||
|
||||
ipc.config.silent = true
|
||||
|
||||
ipc.serve(this.socketPath, () => {
|
||||
ipc.server.on("connect", (socket) => this.onConnect(socket))
|
||||
ipc.server.on("socket.disconnected", (socket) => this.onDisconnect(socket))
|
||||
ipc.server.on("message", (data) => this.onMessage(data))
|
||||
})
|
||||
|
||||
ipc.server.start()
|
||||
}
|
||||
|
||||
private onConnect(socket: Socket) {
|
||||
const clientId = crypto.randomBytes(6).toString("hex")
|
||||
this._clients.set(clientId, socket)
|
||||
this.log(`[server#onConnect] clientId = ${clientId}, # clients = ${this._clients.size}`)
|
||||
|
||||
this.send(socket, {
|
||||
type: IpcMessageType.Ack,
|
||||
origin: IpcOrigin.Server,
|
||||
data: { clientId, pid: process.pid, ppid: process.ppid },
|
||||
})
|
||||
|
||||
this.emit(IpcMessageType.Connect, clientId)
|
||||
}
|
||||
|
||||
private onDisconnect(destroyedSocket: Socket) {
|
||||
let disconnectedClientId: string | undefined
|
||||
|
||||
for (const [clientId, socket] of this._clients.entries()) {
|
||||
if (socket === destroyedSocket) {
|
||||
disconnectedClientId = clientId
|
||||
this._clients.delete(clientId)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.log(`[server#socket.disconnected] clientId = ${disconnectedClientId}, # clients = ${this._clients.size}`)
|
||||
|
||||
if (disconnectedClientId) {
|
||||
this.emit(IpcMessageType.Disconnect, disconnectedClientId)
|
||||
}
|
||||
}
|
||||
|
||||
private onMessage(data: unknown) {
|
||||
if (typeof data !== "object") {
|
||||
this.log("[server#onMessage] invalid data", data)
|
||||
return
|
||||
}
|
||||
|
||||
const result = ipcMessageSchema.safeParse(data)
|
||||
|
||||
if (!result.success) {
|
||||
this.log("[server#onMessage] invalid payload", result.error.format(), data)
|
||||
return
|
||||
}
|
||||
|
||||
const payload = result.data
|
||||
|
||||
if (payload.origin === IpcOrigin.Client) {
|
||||
switch (payload.type) {
|
||||
case IpcMessageType.TaskCommand:
|
||||
this.emit(IpcMessageType.TaskCommand, payload.clientId, payload.data)
|
||||
break
|
||||
default:
|
||||
this.log(`[server#onMessage] unhandled payload: ${JSON.stringify(payload)}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private log(...args: unknown[]) {
|
||||
this._log(...args)
|
||||
}
|
||||
|
||||
public broadcast(message: IpcMessage) {
|
||||
// this.log("[server#broadcast] message =", message)
|
||||
ipc.server.broadcast("message", message)
|
||||
}
|
||||
|
||||
public send(client: string | Socket, message: IpcMessage) {
|
||||
// this.log("[server#send] message =", message)
|
||||
|
||||
if (typeof client === "string") {
|
||||
const socket = this._clients.get(client)
|
||||
|
||||
if (socket) {
|
||||
ipc.server.emit(socket, "message", message)
|
||||
}
|
||||
} else {
|
||||
ipc.server.emit(client, "message", message)
|
||||
}
|
||||
}
|
||||
|
||||
public get socketPath() {
|
||||
return this._socketPath
|
||||
}
|
||||
|
||||
public get isListening() {
|
||||
return this._isListening
|
||||
}
|
||||
}
|
||||
32
src/exports/log.ts
Normal file
32
src/exports/log.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import * as vscode from "vscode"
|
||||
|
||||
export function outputChannelLog(outputChannel: vscode.OutputChannel, ...args: unknown[]) {
|
||||
for (const arg of args) {
|
||||
if (arg === null) {
|
||||
outputChannel.appendLine("null")
|
||||
} else if (arg === undefined) {
|
||||
outputChannel.appendLine("undefined")
|
||||
} else if (typeof arg === "string") {
|
||||
outputChannel.appendLine(arg)
|
||||
} else if (arg instanceof Error) {
|
||||
outputChannel.appendLine(`Error: ${arg.message}\n${arg.stack || ""}`)
|
||||
} else {
|
||||
try {
|
||||
outputChannel.appendLine(
|
||||
JSON.stringify(
|
||||
arg,
|
||||
(key, value) => {
|
||||
if (typeof value === "bigint") return `BigInt(${value})`
|
||||
if (typeof value === "function") return `Function: ${value.name || "anonymous"}`
|
||||
if (typeof value === "symbol") return value.toString()
|
||||
return value
|
||||
},
|
||||
2,
|
||||
),
|
||||
)
|
||||
} catch (error) {
|
||||
outputChannel.appendLine(`[Non-serializable object: ${Object.prototype.toString.call(arg)}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { ClineMessage } from "./roo-code"
|
||||
|
||||
export class MessageHistory {
|
||||
private readonly messages: Record<string, Record<number, ClineMessage>>
|
||||
private readonly list: Record<string, number[]>
|
||||
|
||||
constructor() {
|
||||
this.messages = {}
|
||||
this.list = {}
|
||||
}
|
||||
|
||||
public add(taskId: string, message: ClineMessage) {
|
||||
if (!this.messages[taskId]) {
|
||||
this.messages[taskId] = {}
|
||||
}
|
||||
|
||||
this.messages[taskId][message.ts] = message
|
||||
|
||||
if (!this.list[taskId]) {
|
||||
this.list[taskId] = []
|
||||
}
|
||||
|
||||
this.list[taskId].push(message.ts)
|
||||
}
|
||||
|
||||
public update(taskId: string, message: ClineMessage) {
|
||||
if (this.messages[taskId][message.ts]) {
|
||||
this.messages[taskId][message.ts] = message
|
||||
}
|
||||
}
|
||||
|
||||
public getMessages(taskId: string) {
|
||||
return (this.list[taskId] ?? []).map((ts) => this.messages[taskId][ts]).filter(Boolean)
|
||||
}
|
||||
}
|
||||
165
src/exports/roo-code.d.ts
vendored
165
src/exports/roo-code.d.ts
vendored
|
|
@ -397,26 +397,125 @@ type TokenUsage = {
|
|||
contextTokens: number
|
||||
}
|
||||
|
||||
type RooCodeSettings = GlobalSettings & ProviderSettings
|
||||
|
||||
interface RooCodeEvents {
|
||||
type RooCodeEvents = {
|
||||
message: [
|
||||
{
|
||||
taskId: string
|
||||
action: "created" | "updated"
|
||||
message: ClineMessage
|
||||
message: {
|
||||
ts: number
|
||||
type: "ask" | "say"
|
||||
ask?:
|
||||
| (
|
||||
| "followup"
|
||||
| "command"
|
||||
| "command_output"
|
||||
| "completion_result"
|
||||
| "tool"
|
||||
| "api_req_failed"
|
||||
| "resume_task"
|
||||
| "resume_completed_task"
|
||||
| "mistake_limit_reached"
|
||||
| "browser_action_launch"
|
||||
| "use_mcp_server"
|
||||
| "finishTask"
|
||||
)
|
||||
| undefined
|
||||
say?:
|
||||
| (
|
||||
| "task"
|
||||
| "error"
|
||||
| "api_req_started"
|
||||
| "api_req_finished"
|
||||
| "api_req_retried"
|
||||
| "api_req_retry_delayed"
|
||||
| "api_req_deleted"
|
||||
| "text"
|
||||
| "reasoning"
|
||||
| "completion_result"
|
||||
| "user_feedback"
|
||||
| "user_feedback_diff"
|
||||
| "command_output"
|
||||
| "tool"
|
||||
| "shell_integration_warning"
|
||||
| "browser_action"
|
||||
| "browser_action_result"
|
||||
| "command"
|
||||
| "mcp_server_request_started"
|
||||
| "mcp_server_response"
|
||||
| "new_task_started"
|
||||
| "new_task"
|
||||
| "checkpoint_saved"
|
||||
| "rooignore_error"
|
||||
)
|
||||
| undefined
|
||||
text?: string | undefined
|
||||
images?: string[] | undefined
|
||||
partial?: boolean | undefined
|
||||
reasoning?: string | undefined
|
||||
conversationHistoryIndex?: number | undefined
|
||||
checkpoint?:
|
||||
| {
|
||||
[x: string]: unknown
|
||||
}
|
||||
| undefined
|
||||
progressStatus?:
|
||||
| {
|
||||
icon?: string | undefined
|
||||
text?: string | undefined
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
},
|
||||
]
|
||||
taskCreated: [string]
|
||||
taskStarted: [string]
|
||||
taskPaused: [string]
|
||||
taskUnpaused: [string]
|
||||
taskAskResponded: [string]
|
||||
taskAborted: [string]
|
||||
taskSpawned: [string, string]
|
||||
taskCompleted: [
|
||||
string,
|
||||
{
|
||||
totalTokensIn: number
|
||||
totalTokensOut: number
|
||||
totalCacheWrites?: number | undefined
|
||||
totalCacheReads?: number | undefined
|
||||
totalCost: number
|
||||
contextTokens: number
|
||||
},
|
||||
]
|
||||
taskTokenUsageUpdated: [
|
||||
string,
|
||||
{
|
||||
totalTokensIn: number
|
||||
totalTokensOut: number
|
||||
totalCacheWrites?: number | undefined
|
||||
totalCacheReads?: number | undefined
|
||||
totalCost: number
|
||||
contextTokens: number
|
||||
},
|
||||
]
|
||||
taskCreated: [taskId: string]
|
||||
taskStarted: [taskId: string]
|
||||
taskPaused: [taskId: string]
|
||||
taskUnpaused: [taskId: string]
|
||||
taskAskResponded: [taskId: string]
|
||||
taskAborted: [taskId: string]
|
||||
taskSpawned: [taskId: string, childTaskId: string]
|
||||
taskCompleted: [taskId: string, usage: TokenUsage]
|
||||
taskTokenUsageUpdated: [taskId: string, usage: TokenUsage]
|
||||
}
|
||||
|
||||
/**
|
||||
* RooCodeEvent
|
||||
*/
|
||||
declare enum RooCodeEventName {
|
||||
Message = "message",
|
||||
TaskCreated = "taskCreated",
|
||||
TaskStarted = "taskStarted",
|
||||
TaskPaused = "taskPaused",
|
||||
TaskUnpaused = "taskUnpaused",
|
||||
TaskAskResponded = "taskAskResponded",
|
||||
TaskAborted = "taskAborted",
|
||||
TaskSpawned = "taskSpawned",
|
||||
TaskCompleted = "taskCompleted",
|
||||
TaskTokenUsageUpdated = "taskTokenUsageUpdated",
|
||||
}
|
||||
|
||||
type RooCodeSettings = GlobalSettings & ProviderSettings
|
||||
interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
|
||||
/**
|
||||
* Starts a new task with an optional initial message and images.
|
||||
|
|
@ -424,7 +523,17 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
|
|||
* @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
|
||||
* @returns The ID of the new task.
|
||||
*/
|
||||
startNewTask(task?: string, images?: string[]): Promise<string>
|
||||
startNewTask({
|
||||
configuration,
|
||||
text,
|
||||
images,
|
||||
newTab,
|
||||
}: {
|
||||
configuration?: RooCodeSettings
|
||||
text?: string
|
||||
images?: string[]
|
||||
newTab?: boolean
|
||||
}): Promise<string>
|
||||
/**
|
||||
* Returns the current task stack.
|
||||
* @returns An array of task IDs.
|
||||
|
|
@ -466,23 +575,15 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
|
|||
* Returns true if the API is ready to use.
|
||||
*/
|
||||
isReady(): boolean
|
||||
/**
|
||||
* Returns the messages for a given task.
|
||||
* @param taskId The ID of the task.
|
||||
* @returns An array of ClineMessage objects.
|
||||
*/
|
||||
getMessages(taskId: string): ClineMessage[]
|
||||
/**
|
||||
* Returns the token usage for a given task.
|
||||
* @param taskId The ID of the task.
|
||||
* @returns A TokenUsage object.
|
||||
*/
|
||||
getTokenUsage(taskId: string): TokenUsage
|
||||
/**
|
||||
* Logs a message to the output channel.
|
||||
* @param message The message to log.
|
||||
*/
|
||||
log(message: string): void
|
||||
}
|
||||
|
||||
export type { ClineMessage, GlobalSettings, ProviderSettings, RooCodeAPI, RooCodeEvents, RooCodeSettings, TokenUsage }
|
||||
export {
|
||||
type ClineMessage,
|
||||
type GlobalSettings,
|
||||
type ProviderSettings,
|
||||
type RooCodeAPI,
|
||||
RooCodeEventName,
|
||||
type RooCodeEvents,
|
||||
type RooCodeSettings,
|
||||
type TokenUsage,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -405,3 +405,107 @@ type TokenUsage = {
|
|||
}
|
||||
|
||||
export type { TokenUsage }
|
||||
|
||||
type RooCodeEvents = {
|
||||
message: [
|
||||
{
|
||||
taskId: string
|
||||
action: "created" | "updated"
|
||||
message: {
|
||||
ts: number
|
||||
type: "ask" | "say"
|
||||
ask?:
|
||||
| (
|
||||
| "followup"
|
||||
| "command"
|
||||
| "command_output"
|
||||
| "completion_result"
|
||||
| "tool"
|
||||
| "api_req_failed"
|
||||
| "resume_task"
|
||||
| "resume_completed_task"
|
||||
| "mistake_limit_reached"
|
||||
| "browser_action_launch"
|
||||
| "use_mcp_server"
|
||||
| "finishTask"
|
||||
)
|
||||
| undefined
|
||||
say?:
|
||||
| (
|
||||
| "task"
|
||||
| "error"
|
||||
| "api_req_started"
|
||||
| "api_req_finished"
|
||||
| "api_req_retried"
|
||||
| "api_req_retry_delayed"
|
||||
| "api_req_deleted"
|
||||
| "text"
|
||||
| "reasoning"
|
||||
| "completion_result"
|
||||
| "user_feedback"
|
||||
| "user_feedback_diff"
|
||||
| "command_output"
|
||||
| "tool"
|
||||
| "shell_integration_warning"
|
||||
| "browser_action"
|
||||
| "browser_action_result"
|
||||
| "command"
|
||||
| "mcp_server_request_started"
|
||||
| "mcp_server_response"
|
||||
| "new_task_started"
|
||||
| "new_task"
|
||||
| "checkpoint_saved"
|
||||
| "rooignore_error"
|
||||
)
|
||||
| undefined
|
||||
text?: string | undefined
|
||||
images?: string[] | undefined
|
||||
partial?: boolean | undefined
|
||||
reasoning?: string | undefined
|
||||
conversationHistoryIndex?: number | undefined
|
||||
checkpoint?:
|
||||
| {
|
||||
[x: string]: unknown
|
||||
}
|
||||
| undefined
|
||||
progressStatus?:
|
||||
| {
|
||||
icon?: string | undefined
|
||||
text?: string | undefined
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
},
|
||||
]
|
||||
taskCreated: [string]
|
||||
taskStarted: [string]
|
||||
taskPaused: [string]
|
||||
taskUnpaused: [string]
|
||||
taskAskResponded: [string]
|
||||
taskAborted: [string]
|
||||
taskSpawned: [string, string]
|
||||
taskCompleted: [
|
||||
string,
|
||||
{
|
||||
totalTokensIn: number
|
||||
totalTokensOut: number
|
||||
totalCacheWrites?: number | undefined
|
||||
totalCacheReads?: number | undefined
|
||||
totalCost: number
|
||||
contextTokens: number
|
||||
},
|
||||
]
|
||||
taskTokenUsageUpdated: [
|
||||
string,
|
||||
{
|
||||
totalTokensIn: number
|
||||
totalTokensOut: number
|
||||
totalCacheWrites?: number | undefined
|
||||
totalCacheReads?: number | undefined
|
||||
totalCost: number
|
||||
contextTokens: number
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export type { RooCodeEvents }
|
||||
|
|
|
|||
|
|
@ -119,7 +119,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
vscode.commands.executeCommand("roo-cline.activationCompleted")
|
||||
|
||||
// Implements the `RooCodeAPI` interface.
|
||||
return new API(outputChannel, provider)
|
||||
const socketPath = process.env.ROO_CODE_IPC_SOCKET_PATH
|
||||
const enableLogging = typeof socketPath === "string"
|
||||
return new API(outputChannel, provider, socketPath, enableLogging)
|
||||
}
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export class Terminal {
|
|||
console.log(`[Terminal ${this.id}] Shell integration not available. Command execution aborted.`)
|
||||
process.emit(
|
||||
"no_shell_integration",
|
||||
"Shell integration initialization sequence '\\x1b]633;A' was not received within 4 seconds. Shell integration has been disabled for this terminal instance. Increase the timeout in the settings if necessary.",
|
||||
`Shell integration initialization sequence '\\x1b]633;A' was not received within ${Terminal.shellIntegrationTimeout / 1000}s. Shell integration has been disabled for this terminal instance. Increase the timeout in the settings if necessary.`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -626,6 +626,8 @@ export const GLOBAL_SETTINGS_KEYS = Object.keys(globalSettingsRecord) as Keys<Gl
|
|||
* RooCodeSettings
|
||||
*/
|
||||
|
||||
export const rooCodeSettingsSchema = providerSettingsSchema.merge(globalSettingsSchema)
|
||||
|
||||
export type RooCodeSettings = GlobalSettings & ProviderSettings
|
||||
|
||||
/**
|
||||
|
|
@ -787,6 +789,44 @@ export const tokenUsageSchema = z.object({
|
|||
|
||||
export type TokenUsage = z.infer<typeof tokenUsageSchema>
|
||||
|
||||
/**
|
||||
* RooCodeEvent
|
||||
*/
|
||||
|
||||
export enum RooCodeEventName {
|
||||
Message = "message",
|
||||
TaskCreated = "taskCreated",
|
||||
TaskStarted = "taskStarted",
|
||||
TaskPaused = "taskPaused",
|
||||
TaskUnpaused = "taskUnpaused",
|
||||
TaskAskResponded = "taskAskResponded",
|
||||
TaskAborted = "taskAborted",
|
||||
TaskSpawned = "taskSpawned",
|
||||
TaskCompleted = "taskCompleted",
|
||||
TaskTokenUsageUpdated = "taskTokenUsageUpdated",
|
||||
}
|
||||
|
||||
export const rooCodeEventsSchema = z.object({
|
||||
[RooCodeEventName.Message]: z.tuple([
|
||||
z.object({
|
||||
taskId: z.string(),
|
||||
action: z.union([z.literal("created"), z.literal("updated")]),
|
||||
message: clineMessageSchema,
|
||||
}),
|
||||
]),
|
||||
[RooCodeEventName.TaskCreated]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskStarted]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskPaused]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskAborted]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]),
|
||||
[RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema]),
|
||||
[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),
|
||||
})
|
||||
|
||||
export type RooCodeEvents = z.infer<typeof rooCodeEventsSchema>
|
||||
|
||||
/**
|
||||
* TypeDefinition
|
||||
*/
|
||||
|
|
@ -801,6 +841,7 @@ export const typeDefinitions: TypeDefinition[] = [
|
|||
{ schema: globalSettingsSchema, identifier: "GlobalSettings" },
|
||||
{ schema: clineMessageSchema, identifier: "ClineMessage" },
|
||||
{ schema: tokenUsageSchema, identifier: "TokenUsage" },
|
||||
{ schema: rooCodeEventsSchema, identifier: "RooCodeEvents" },
|
||||
]
|
||||
|
||||
// Also export as default for ESM compatibility
|
||||
|
|
|
|||
135
src/schemas/ipc.ts
Normal file
135
src/schemas/ipc.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import { RooCodeEventName, rooCodeEventsSchema, rooCodeSettingsSchema } from "./index"
|
||||
|
||||
/**
|
||||
* Ack
|
||||
*/
|
||||
|
||||
export const ackSchema = z.object({
|
||||
clientId: z.string(),
|
||||
pid: z.number(),
|
||||
ppid: z.number(),
|
||||
})
|
||||
|
||||
export type Ack = z.infer<typeof ackSchema>
|
||||
|
||||
/**
|
||||
* TaskCommand
|
||||
*/
|
||||
|
||||
export enum TaskCommandName {
|
||||
StartNewTask = "StartNewTask",
|
||||
CancelTask = "CancelTask",
|
||||
CloseTask = "CloseTask",
|
||||
}
|
||||
|
||||
export const taskCommandSchema = z.discriminatedUnion("commandName", [
|
||||
z.object({
|
||||
commandName: z.literal(TaskCommandName.StartNewTask),
|
||||
data: z.object({
|
||||
configuration: rooCodeSettingsSchema,
|
||||
text: z.string(),
|
||||
images: z.array(z.string()).optional(),
|
||||
newTab: z.boolean().optional(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
commandName: z.literal(TaskCommandName.CancelTask),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
commandName: z.literal(TaskCommandName.CloseTask),
|
||||
data: z.string(),
|
||||
}),
|
||||
])
|
||||
|
||||
export type TaskCommand = z.infer<typeof taskCommandSchema>
|
||||
|
||||
/**
|
||||
* TaskEvent
|
||||
*/
|
||||
|
||||
export const taskEventSchema = z.discriminatedUnion("eventName", [
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.Message),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.Message],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskCreated),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCreated],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskStarted),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskPaused),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskUnpaused),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnpaused],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskAskResponded),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskAborted),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAborted],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskSpawned),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskSpawned],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskCompleted),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCompleted],
|
||||
}),
|
||||
z.object({
|
||||
eventName: z.literal(RooCodeEventName.TaskTokenUsageUpdated),
|
||||
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskTokenUsageUpdated],
|
||||
}),
|
||||
])
|
||||
|
||||
export type TaskEvent = z.infer<typeof taskEventSchema>
|
||||
|
||||
/**
|
||||
* IpcMessage
|
||||
*/
|
||||
|
||||
export enum IpcMessageType {
|
||||
Connect = "Connect",
|
||||
Disconnect = "Disconnect",
|
||||
Ack = "Ack",
|
||||
TaskCommand = "TaskCommand",
|
||||
TaskEvent = "TaskEvent",
|
||||
}
|
||||
|
||||
export enum IpcOrigin {
|
||||
Client = "client",
|
||||
Server = "server",
|
||||
}
|
||||
|
||||
export const ipcMessageSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal(IpcMessageType.Ack),
|
||||
origin: z.literal(IpcOrigin.Server),
|
||||
data: ackSchema,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(IpcMessageType.TaskCommand),
|
||||
origin: z.literal(IpcOrigin.Client),
|
||||
clientId: z.string(),
|
||||
data: taskCommandSchema,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(IpcMessageType.TaskEvent),
|
||||
origin: z.literal(IpcOrigin.Server),
|
||||
relayClientId: z.string().optional(),
|
||||
data: taskEventSchema,
|
||||
}),
|
||||
])
|
||||
|
||||
export type IpcMessage = z.infer<typeof ipcMessageSchema>
|
||||
Loading…
Add table
Reference in a new issue