mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
More progress
This commit is contained in:
parent
5ec80f94eb
commit
3d7117bb7b
15 changed files with 1097 additions and 610 deletions
|
|
@ -1,8 +1,13 @@
|
|||
// pnpm --filter @roo-code/cli test src/__tests__/extension-host.test.ts
|
||||
|
||||
import { ExtensionHost, type ExtensionHostOptions } from "../extension-host.js"
|
||||
import { EventEmitter } from "events"
|
||||
import type { ProviderName } from "@roo-code/types"
|
||||
import fs from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
|
||||
import type { ProviderName, WebviewMessage } from "@roo-code/types"
|
||||
|
||||
import { ExtensionHost, type ExtensionHostOptions } from "../extension-host.js"
|
||||
|
||||
vi.mock("@roo-code/vscode-shim", () => ({
|
||||
createVSCodeAPI: vi.fn(() => ({
|
||||
|
|
@ -369,15 +374,15 @@ describe("ExtensionHost", () => {
|
|||
const emitSpy = vi.spyOn(host, "emit")
|
||||
|
||||
// Queue messages before ready
|
||||
host.sendToExtension({ type: "test1" })
|
||||
host.sendToExtension({ type: "test2" })
|
||||
host.sendToExtension({ type: "requestModes" })
|
||||
host.sendToExtension({ type: "requestCommands" })
|
||||
|
||||
// Mark ready (should flush)
|
||||
host.markWebviewReady()
|
||||
|
||||
// Check that webviewMessage events were emitted for pending messages
|
||||
expect(emitSpy).toHaveBeenCalledWith("webviewMessage", { type: "test1" })
|
||||
expect(emitSpy).toHaveBeenCalledWith("webviewMessage", { type: "test2" })
|
||||
expect(emitSpy).toHaveBeenCalledWith("webviewMessage", { type: "requestModes" })
|
||||
expect(emitSpy).toHaveBeenCalledWith("webviewMessage", { type: "requestCommands" })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -385,7 +390,7 @@ describe("ExtensionHost", () => {
|
|||
describe("sendToExtension", () => {
|
||||
it("should queue message when webview not ready", () => {
|
||||
const host = createTestHost()
|
||||
const message = { type: "test" }
|
||||
const message: WebviewMessage = { type: "requestModes" }
|
||||
|
||||
host.sendToExtension(message)
|
||||
|
||||
|
|
@ -396,7 +401,7 @@ describe("ExtensionHost", () => {
|
|||
it("should emit webviewMessage event when webview is ready", () => {
|
||||
const host = createTestHost()
|
||||
const emitSpy = vi.spyOn(host, "emit")
|
||||
const message = { type: "test" }
|
||||
const message: WebviewMessage = { type: "requestModes" }
|
||||
|
||||
host.markWebviewReady()
|
||||
host.sendToExtension(message)
|
||||
|
|
@ -408,7 +413,7 @@ describe("ExtensionHost", () => {
|
|||
const host = createTestHost()
|
||||
|
||||
host.markWebviewReady()
|
||||
host.sendToExtension({ type: "test" })
|
||||
host.sendToExtension({ type: "requestModes" })
|
||||
|
||||
const pending = getPrivate<unknown[]>(host, "pendingMessages")
|
||||
expect(pending).toHaveLength(0)
|
||||
|
|
@ -792,7 +797,7 @@ describe("ExtensionHost", () => {
|
|||
callPrivate(host, "handleFollowupQuestionWithTimeout", 123, text)
|
||||
|
||||
// Should show prompt with timeout hint
|
||||
expect(stdoutWriteSpy).toHaveBeenCalledWith(expect.stringContaining("auto-select in 10s"))
|
||||
expect(stdoutWriteSpy).toHaveBeenCalledWith(expect.stringContaining("auto-select in 60s"))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1144,9 +1149,8 @@ describe("ExtensionHost", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("handleStateMessage - mode change detection", () => {
|
||||
describe("handleStateMessage - mode tracking", () => {
|
||||
let host: ExtensionHost
|
||||
let sendToExtensionSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
host = createTestHost({
|
||||
|
|
@ -1157,99 +1161,194 @@ describe("ExtensionHost", () => {
|
|||
})
|
||||
// Mock process.stdout.write which is used by output()
|
||||
vi.spyOn(process.stdout, "write").mockImplementation(() => true)
|
||||
sendToExtensionSpy = vi.spyOn(host, "sendToExtension")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should re-apply API configuration when mode changes in state", () => {
|
||||
// First state update establishes current mode
|
||||
it("should track current mode when state updates with a mode", () => {
|
||||
// Initial state update establishes current mode
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "code", clineMessages: [] } })
|
||||
sendToExtensionSpy.mockClear()
|
||||
expect(getPrivate(host, "currentMode")).toBe("code")
|
||||
|
||||
// Second state update with different mode should trigger re-apply
|
||||
// Second state update should update tracked mode
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "architect", clineMessages: [] } })
|
||||
|
||||
// Should have sent updateSettings with the API configuration
|
||||
expect(sendToExtensionSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "updateSettings",
|
||||
updatedSettings: expect.objectContaining({
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
apiModelId: "test-model",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(getPrivate(host, "currentMode")).toBe("architect")
|
||||
})
|
||||
|
||||
it("should not re-apply API configuration when mode stays the same", () => {
|
||||
// First state update establishes current mode
|
||||
it("should not change current mode when state has no mode", () => {
|
||||
// Initial state update establishes current mode
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "code", clineMessages: [] } })
|
||||
sendToExtensionSpy.mockClear()
|
||||
expect(getPrivate(host, "currentMode")).toBe("code")
|
||||
|
||||
// Second state update with same mode should not trigger re-apply
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "code", clineMessages: [] } })
|
||||
|
||||
// Should not have sent updateSettings
|
||||
expect(sendToExtensionSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should re-apply API configuration without apiKey when mode changes", () => {
|
||||
// Create host without apiKey - API configuration should still be sent
|
||||
// to preserve provider/model settings across mode switches
|
||||
const hostNoKey = createTestHost({
|
||||
mode: "code",
|
||||
apiProvider: "anthropic",
|
||||
model: "test-model",
|
||||
})
|
||||
const sendSpy = vi.spyOn(hostNoKey, "sendToExtension")
|
||||
|
||||
// First state update establishes current mode
|
||||
callPrivate(hostNoKey, "handleStateMessage", { type: "state", state: { mode: "code", clineMessages: [] } })
|
||||
sendSpy.mockClear()
|
||||
|
||||
// Second state update with different mode
|
||||
callPrivate(hostNoKey, "handleStateMessage", {
|
||||
type: "state",
|
||||
state: { mode: "architect", clineMessages: [] },
|
||||
})
|
||||
|
||||
// Should have sent updateSettings with provider and model (but no apiKey)
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "updateSettings",
|
||||
updatedSettings: expect.objectContaining({
|
||||
apiProvider: "anthropic",
|
||||
apiModelId: "test-model",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
// Verify apiKey is NOT in the config
|
||||
const call = sendSpy.mock.calls[0]?.[0] as { updatedSettings: { apiKey?: string } } | undefined
|
||||
expect(call?.updatedSettings.apiKey).toBeUndefined()
|
||||
// State without mode should not change tracked mode
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { clineMessages: [] } })
|
||||
expect(getPrivate(host, "currentMode")).toBe("code")
|
||||
})
|
||||
|
||||
it("should track current mode across multiple changes", () => {
|
||||
// Start with code mode
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "code", clineMessages: [] } })
|
||||
sendToExtensionSpy.mockClear()
|
||||
expect(getPrivate(host, "currentMode")).toBe("code")
|
||||
|
||||
// Change to architect
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "architect", clineMessages: [] } })
|
||||
expect(sendToExtensionSpy).toHaveBeenCalledTimes(1)
|
||||
sendToExtensionSpy.mockClear()
|
||||
expect(getPrivate(host, "currentMode")).toBe("architect")
|
||||
|
||||
// Change to debug
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "debug", clineMessages: [] } })
|
||||
expect(sendToExtensionSpy).toHaveBeenCalledTimes(1)
|
||||
expect(getPrivate(host, "currentMode")).toBe("debug")
|
||||
|
||||
// Another state update with debug
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "debug", clineMessages: [] } })
|
||||
expect(getPrivate(host, "currentMode")).toBe("debug")
|
||||
})
|
||||
|
||||
it("should not send updateSettings on mode change (CLI settings are applied once during runTask)", () => {
|
||||
// This test ensures mode changes don't trigger automatic re-application of API settings.
|
||||
// CLI settings are applied once during runTask() via updateSettings.
|
||||
// Mode-specific provider profiles are handled by the extension's handleModeSwitch.
|
||||
const sendToExtensionSpy = vi.spyOn(host, "sendToExtension")
|
||||
|
||||
// Initial state
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "code", clineMessages: [] } })
|
||||
sendToExtensionSpy.mockClear()
|
||||
|
||||
// Stay on debug
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "debug", clineMessages: [] } })
|
||||
// Mode change should NOT trigger sendToExtension
|
||||
callPrivate(host, "handleStateMessage", { type: "state", state: { mode: "architect", clineMessages: [] } })
|
||||
expect(sendToExtensionSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("ephemeral mode", () => {
|
||||
describe("constructor", () => {
|
||||
it("should store ephemeral option", () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
const options = getPrivate<ExtensionHostOptions>(host, "options")
|
||||
expect(options.ephemeral).toBe(true)
|
||||
})
|
||||
|
||||
it("should default ephemeral to undefined", () => {
|
||||
const host = createTestHost()
|
||||
const options = getPrivate<ExtensionHostOptions>(host, "options")
|
||||
expect(options.ephemeral).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should initialize ephemeralStorageDir to null", () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
expect(getPrivate(host, "ephemeralStorageDir")).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createEphemeralStorageDir", () => {
|
||||
let createdDirs: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up any directories created during tests
|
||||
for (const dir of createdDirs) {
|
||||
try {
|
||||
await fs.promises.rm(dir, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
createdDirs = []
|
||||
})
|
||||
|
||||
it("should create a directory in the system temp folder", async () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
const tmpDir = await callPrivate<Promise<string>>(host, "createEphemeralStorageDir")
|
||||
createdDirs.push(tmpDir)
|
||||
|
||||
expect(tmpDir).toContain(os.tmpdir())
|
||||
expect(tmpDir).toContain("roo-cli-")
|
||||
expect(fs.existsSync(tmpDir)).toBe(true)
|
||||
})
|
||||
|
||||
it("should create a unique directory each time", async () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
const dir1 = await callPrivate<Promise<string>>(host, "createEphemeralStorageDir")
|
||||
const dir2 = await callPrivate<Promise<string>>(host, "createEphemeralStorageDir")
|
||||
createdDirs.push(dir1, dir2)
|
||||
|
||||
expect(dir1).not.toBe(dir2)
|
||||
expect(fs.existsSync(dir1)).toBe(true)
|
||||
expect(fs.existsSync(dir2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should include timestamp and random id in directory name", async () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
const tmpDir = await callPrivate<Promise<string>>(host, "createEphemeralStorageDir")
|
||||
createdDirs.push(tmpDir)
|
||||
|
||||
const dirName = path.basename(tmpDir)
|
||||
// Format: roo-cli-{timestamp}-{randomId}
|
||||
expect(dirName).toMatch(/^roo-cli-\d+-[a-z0-9]+$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose - ephemeral cleanup", () => {
|
||||
it("should clean up ephemeral storage directory on dispose", async () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
|
||||
// Create the ephemeral directory
|
||||
const tmpDir = await callPrivate<Promise<string>>(host, "createEphemeralStorageDir")
|
||||
;(host as unknown as Record<string, unknown>).ephemeralStorageDir = tmpDir
|
||||
|
||||
// Verify directory exists
|
||||
expect(fs.existsSync(tmpDir)).toBe(true)
|
||||
|
||||
// Dispose the host
|
||||
await host.dispose()
|
||||
|
||||
// Directory should be removed
|
||||
expect(fs.existsSync(tmpDir)).toBe(false)
|
||||
expect(getPrivate(host, "ephemeralStorageDir")).toBeNull()
|
||||
})
|
||||
|
||||
it("should not fail dispose if ephemeral directory doesn't exist", async () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
|
||||
// Set a non-existent directory
|
||||
;(host as unknown as Record<string, unknown>).ephemeralStorageDir = "/non/existent/path/roo-cli-test"
|
||||
|
||||
// Dispose should not throw
|
||||
await expect(host.dispose()).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it("should clean up ephemeral directory with contents", async () => {
|
||||
const host = createTestHost({ ephemeral: true })
|
||||
|
||||
// Create the ephemeral directory with some content
|
||||
const tmpDir = await callPrivate<Promise<string>>(host, "createEphemeralStorageDir")
|
||||
;(host as unknown as Record<string, unknown>).ephemeralStorageDir = tmpDir
|
||||
|
||||
// Add some files and subdirectories
|
||||
await fs.promises.writeFile(path.join(tmpDir, "test.txt"), "test content")
|
||||
await fs.promises.mkdir(path.join(tmpDir, "subdir"))
|
||||
await fs.promises.writeFile(path.join(tmpDir, "subdir", "nested.txt"), "nested content")
|
||||
|
||||
// Verify content exists
|
||||
expect(fs.existsSync(path.join(tmpDir, "test.txt"))).toBe(true)
|
||||
expect(fs.existsSync(path.join(tmpDir, "subdir", "nested.txt"))).toBe(true)
|
||||
|
||||
// Dispose the host
|
||||
await host.dispose()
|
||||
|
||||
// Directory and all contents should be removed
|
||||
expect(fs.existsSync(tmpDir)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not clean up anything if not in ephemeral mode", async () => {
|
||||
const host = createTestHost({ ephemeral: false })
|
||||
|
||||
// ephemeralStorageDir should be null
|
||||
expect(getPrivate(host, "ephemeralStorageDir")).toBeNull()
|
||||
|
||||
// Dispose should complete normally
|
||||
await expect(host.dispose()).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,8 +16,15 @@ import fs from "fs"
|
|||
import os from "os"
|
||||
import readline from "readline"
|
||||
|
||||
import { ProviderName, ReasoningEffortExtended, RooCodeSettings, ExtensionMessage } from "@roo-code/types"
|
||||
import {
|
||||
ProviderName,
|
||||
ReasoningEffortExtended,
|
||||
RooCodeSettings,
|
||||
ExtensionMessage,
|
||||
WebviewMessage,
|
||||
} from "@roo-code/types"
|
||||
import { createVSCodeAPI, setRuntimeConfigValues } from "@roo-code/vscode-shim"
|
||||
import { debugLog } from "@roo-code/core/debug-log"
|
||||
|
||||
// Get the CLI package root directory (for finding node_modules/@vscode/ripgrep)
|
||||
// When bundled, import.meta.url points to dist/index.js, so go up to package root
|
||||
|
|
@ -40,6 +47,11 @@ export interface ExtensionHostOptions {
|
|||
* Use this when running in TUI mode where Ink controls the terminal.
|
||||
*/
|
||||
disableOutput?: boolean
|
||||
/**
|
||||
* When true, uses a temporary storage directory that is cleaned up on exit.
|
||||
* No state persists between runs - all configuration from CLI flags/environment.
|
||||
*/
|
||||
ephemeral?: boolean
|
||||
}
|
||||
|
||||
interface ExtensionModule {
|
||||
|
|
@ -54,8 +66,6 @@ interface WebviewViewProvider {
|
|||
resolveWebviewView?(webviewView: unknown, context: unknown, token: unknown): void | Promise<void>
|
||||
}
|
||||
|
||||
const DEBUG_LOG_PATH = path.join(os.homedir(), ".roo", "cli-debug.log")
|
||||
|
||||
export class ExtensionHost extends EventEmitter {
|
||||
private vscode: ReturnType<typeof createVSCodeAPI> | null = null
|
||||
private extensionModule: ExtensionModule | null = null
|
||||
|
|
@ -94,6 +104,9 @@ export class ExtensionHost extends EventEmitter {
|
|||
// Track the current mode to detect mode changes
|
||||
private currentMode: string | null = null
|
||||
|
||||
// Track ephemeral storage directory for cleanup
|
||||
private ephemeralStorageDir: string | null = null
|
||||
|
||||
constructor(options: ExtensionHostOptions) {
|
||||
super()
|
||||
this.options = options
|
||||
|
|
@ -105,48 +118,7 @@ export class ExtensionHost extends EventEmitter {
|
|||
* This avoids console output which breaks the TUI.
|
||||
*/
|
||||
private log(message: string, data?: unknown): void {
|
||||
try {
|
||||
const logDir = path.dirname(DEBUG_LOG_PATH)
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
const entry = data
|
||||
? `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`
|
||||
: `[${timestamp}] ${message}\n`
|
||||
|
||||
fs.appendFileSync(DEBUG_LOG_PATH, entry)
|
||||
} catch {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shape (keys) of an object for logging
|
||||
*/
|
||||
private getMessageShape(msg: unknown): Record<string, string> {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
return { _type: typeof msg }
|
||||
}
|
||||
|
||||
const shape: Record<string, string> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(msg as Record<string, unknown>)) {
|
||||
if (value === null) {
|
||||
shape[key] = "null"
|
||||
} else if (Array.isArray(value)) {
|
||||
shape[key] = `array[${value.length}]`
|
||||
} else if (typeof value === "object") {
|
||||
shape[key] = `object{${Object.keys(value).join(",")}}`
|
||||
} else {
|
||||
shape[key] = typeof value
|
||||
}
|
||||
}
|
||||
|
||||
return shape
|
||||
debugLog(message, data)
|
||||
}
|
||||
|
||||
private suppressNodeWarnings(): void {
|
||||
|
|
@ -204,6 +176,17 @@ export class ExtensionHost extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique ephemeral storage directory in the system temp folder.
|
||||
* This directory will be cleaned up when dispose() is called.
|
||||
*/
|
||||
private async createEphemeralStorageDir(): Promise<string> {
|
||||
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
|
||||
const tmpDir = path.join(os.tmpdir(), `roo-cli-${uniqueId}`)
|
||||
await fs.promises.mkdir(tmpDir, { recursive: true })
|
||||
return tmpDir
|
||||
}
|
||||
|
||||
async activate(): Promise<void> {
|
||||
// Suppress Node.js warnings (like MaxListenersExceededWarning) before anything else
|
||||
this.suppressNodeWarnings()
|
||||
|
|
@ -219,12 +202,20 @@ export class ExtensionHost extends EventEmitter {
|
|||
throw new Error(`Extension bundle not found at: ${bundlePath}`)
|
||||
}
|
||||
|
||||
// Create ephemeral storage directory if ephemeral mode is enabled
|
||||
let storageDir: string | undefined
|
||||
|
||||
if (this.options.ephemeral) {
|
||||
storageDir = await this.createEphemeralStorageDir()
|
||||
this.ephemeralStorageDir = storageDir
|
||||
}
|
||||
|
||||
// 1. Create VSCode API mock
|
||||
this.vscode = createVSCodeAPI(
|
||||
this.options.extensionPath,
|
||||
this.options.workspacePath,
|
||||
undefined, // identity
|
||||
{ appRoot: CLI_PACKAGE_ROOT }, // options - point appRoot to CLI package for ripgrep
|
||||
{ appRoot: CLI_PACKAGE_ROOT, storageDir }, // options - point appRoot to CLI package for ripgrep, custom storageDir for ephemeral mode
|
||||
)
|
||||
|
||||
// 2. Set global vscode reference for the extension
|
||||
|
|
@ -332,7 +323,7 @@ export class ExtensionHost extends EventEmitter {
|
|||
/**
|
||||
* Send a message to the extension (simulating webview -> extension communication).
|
||||
*/
|
||||
sendToExtension(message: unknown): void {
|
||||
sendToExtension(message: WebviewMessage): void {
|
||||
if (!this.isWebviewReady) {
|
||||
this.pendingMessages.push(message)
|
||||
return
|
||||
|
|
@ -554,99 +545,55 @@ export class ExtensionHost extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
// Set up message listener for extension responses
|
||||
this.setupMessageListener()
|
||||
// Should this only be done once?
|
||||
this.messageListener = (message: ExtensionMessage) => this.handleExtensionMessage(message)
|
||||
this.on("extensionWebviewMessage", this.messageListener)
|
||||
|
||||
let defaultSettings: RooCodeSettings
|
||||
|
||||
// Configure approval settings based on mode
|
||||
// In non-interactive mode (-y flag), enable auto-approval for everything
|
||||
// In interactive mode (default), we'll prompt the user for each action
|
||||
if (this.options.nonInteractive) {
|
||||
const settings: RooCodeSettings = {
|
||||
defaultSettings = {
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowReadOnlyOutsideWorkspace: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowWriteOutsideWorkspace: true,
|
||||
alwaysAllowWriteProtected: false, // Keep protected files safe.
|
||||
alwaysAllowWriteProtected: false,
|
||||
alwaysAllowBrowser: true,
|
||||
alwaysAllowMcp: true,
|
||||
alwaysAllowModeSwitch: true,
|
||||
alwaysAllowSubtasks: true,
|
||||
alwaysAllowExecute: true,
|
||||
alwaysAllowFollowupQuestions: true,
|
||||
// NOTE: Setting to 0 should disable extension's internal timeout,
|
||||
// but we need to verify this is working correctly.
|
||||
followupAutoApproveTimeoutMs: 0,
|
||||
allowedCommands: ["*"],
|
||||
commandExecutionTimeout: 20,
|
||||
enableCheckpoints: false, // Checkpoints disabled until CLI UI is implemented.
|
||||
}
|
||||
|
||||
this.applyRuntimeSettings(settings)
|
||||
this.sendToExtension({ type: "updateSettings", updatedSettings: settings })
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 100))
|
||||
} else {
|
||||
const settings: RooCodeSettings = {
|
||||
defaultSettings = {
|
||||
autoApprovalEnabled: false,
|
||||
enableCheckpoints: false, // Checkpoints disabled until CLI UI is implemented.
|
||||
enableCheckpoints: false,
|
||||
}
|
||||
|
||||
this.applyRuntimeSettings(settings)
|
||||
this.sendToExtension({ type: "updateSettings", updatedSettings: settings })
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
// Always send API configuration - it may include API key from environment variables
|
||||
const apiConfig = this.buildApiConfiguration()
|
||||
this.sendToExtension({ type: "updateSettings", updatedSettings: apiConfig })
|
||||
const settings = { ...defaultSettings, ...this.buildApiConfiguration() }
|
||||
this.applyRuntimeSettings(settings)
|
||||
|
||||
this.sendToExtension({
|
||||
type: "updateSettings",
|
||||
updatedSettings: settings,
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
this.sendToExtension({ type: "newTask", text: prompt })
|
||||
await this.waitForCompletion()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up listener for messages from the extension
|
||||
*/
|
||||
private setupMessageListener(): void {
|
||||
this.messageListener = (message: ExtensionMessage) => this.handleExtensionMessage(message)
|
||||
this.on("extensionWebviewMessage", this.messageListener)
|
||||
}
|
||||
|
||||
private handleExtensionMessage(msg: ExtensionMessage): void {
|
||||
// Log all incoming messages for debugging
|
||||
this.log(`[MSG] type=${msg.type}`, this.getMessageShape(msg))
|
||||
|
||||
// For state messages, log additional details about the state
|
||||
if (msg.type === "state" && msg.state) {
|
||||
const state = msg.state
|
||||
// Extract model ID based on provider (different providers use different fields)
|
||||
const apiConfig = state.apiConfiguration
|
||||
let modelId = apiConfig?.apiModelId
|
||||
|
||||
if (apiConfig?.apiProvider === "openrouter") {
|
||||
modelId = apiConfig?.openRouterModelId
|
||||
} else if (apiConfig?.apiProvider === "openai") {
|
||||
modelId = apiConfig?.openAiModelId
|
||||
} else if (apiConfig?.apiProvider === "ollama") {
|
||||
modelId = apiConfig?.ollamaModelId
|
||||
}
|
||||
|
||||
this.log(`[STATE] mode=${state.mode}, clineMessages=${state.clineMessages?.length || 0}`, {
|
||||
apiProvider: apiConfig?.apiProvider,
|
||||
modelId: modelId,
|
||||
mode: state.mode,
|
||||
cliProvider: this.options.apiProvider,
|
||||
cliModel: this.options.model,
|
||||
})
|
||||
|
||||
// Log any ask messages in the state that might indicate resume
|
||||
if (state.clineMessages) {
|
||||
for (const clineMsg of state.clineMessages) {
|
||||
if (clineMsg?.ask === "resume_task" || clineMsg?.ask === "resume_completed_task") {
|
||||
this.log(`[RESUME DETECTED] ask=${clineMsg.ask}, ts=${clineMsg.ts}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case "state":
|
||||
this.handleStateMessage(msg)
|
||||
|
|
@ -658,12 +605,11 @@ export class ExtensionHost extends EventEmitter {
|
|||
break
|
||||
|
||||
case "modes":
|
||||
// Forward modes list to the TUI.
|
||||
this.emit("extensionWebviewMessage", msg)
|
||||
// Forward modes list to the TUI via a dedicated event.
|
||||
// DO NOT emit to "extensionWebviewMessage" - that would create an infinite loop
|
||||
// since messageListener listens on that event and calls this function.
|
||||
this.emit("modesUpdated", msg)
|
||||
break
|
||||
|
||||
default:
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -752,29 +698,11 @@ export class ExtensionHost extends EventEmitter {
|
|||
// Track current mode for mode switch detection (in tool execution).
|
||||
const newMode = state.mode
|
||||
|
||||
if (newMode && this.currentMode !== null && this.currentMode !== newMode) {
|
||||
this.log(`[MODE CHANGE] from=${this.currentMode} to=${newMode}, re-applying CLI settings`)
|
||||
const updatedSettings = this.buildApiConfiguration()
|
||||
this.sendToExtension({ type: "updateSettings", updatedSettings })
|
||||
}
|
||||
|
||||
if (newMode) {
|
||||
this.currentMode = newMode
|
||||
}
|
||||
|
||||
// Detect when the model in state differs from CLI-specified model.
|
||||
// This catches task resume scenarios where the extension loads stored apiConfiguration
|
||||
// which may differ from CLI-provided settings. We re-apply CLI settings proactively.
|
||||
const apiConfig = state.apiConfiguration as Record<string, unknown> | undefined
|
||||
const stateModelId = this.getStateModelId(apiConfig)
|
||||
const expectedModelId = this.getExpectedModelId()
|
||||
|
||||
if (expectedModelId && stateModelId && stateModelId !== expectedModelId) {
|
||||
this.log(`[MODEL MISMATCH] state has ${stateModelId}, CLI expects ${expectedModelId}, re-applying settings`)
|
||||
const updatedSettings = this.buildApiConfiguration()
|
||||
this.sendToExtension({ type: "updateSettings", updatedSettings })
|
||||
}
|
||||
|
||||
// Why do we do this?
|
||||
const clineMessages = state.clineMessages
|
||||
|
||||
if (clineMessages && clineMessages.length > 0) {
|
||||
|
|
@ -807,28 +735,19 @@ export class ExtensionHost extends EventEmitter {
|
|||
* Handle messageUpdated - individual streaming updates for a single message
|
||||
* This is where real-time streaming happens!
|
||||
*/
|
||||
private handleMessageUpdated(msg: ExtensionMessage): void {
|
||||
const clineMessage = msg.clineMessage
|
||||
private handleMessageUpdated({ clineMessage: msg }: ExtensionMessage): void {
|
||||
// if (msg?.ask) {
|
||||
// this.log(`[MSG] ts=${msg.ts}, ask=${msg.ask} -> ${msg.partial ? "partial" : msg.text}`)
|
||||
// } else if (msg?.say) {
|
||||
// this.log(`[MSG] ts=${msg.ts}, say=${msg.say} -> ${msg.partial ? "partial" : msg.text}`)
|
||||
// } else if (msg) {
|
||||
// this.log(`[MSG] ts=${msg.ts}, type=${msg.type}, text=${msg.text}`)
|
||||
// }
|
||||
|
||||
if (!clineMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
const ts = clineMessage.ts
|
||||
const isPartial = clineMessage.partial
|
||||
const text = clineMessage.text
|
||||
const type = clineMessage.type
|
||||
const say = clineMessage.say
|
||||
const ask = clineMessage.ask
|
||||
|
||||
if (!ts) {
|
||||
return
|
||||
}
|
||||
|
||||
if (type === "say" && say && typeof text === "string") {
|
||||
this.handleSayMessage(ts, say, text, isPartial)
|
||||
} else if (type === "ask" && ask && typeof text === "string") {
|
||||
this.handleAskMessage(ts, ask, text, isPartial)
|
||||
if (msg?.type === "say" && msg.say && typeof msg.text === "string") {
|
||||
this.handleSayMessage(msg.ts, msg.say, msg.text, msg.partial)
|
||||
} else if (msg?.type === "ask" && msg.ask && typeof msg.text === "string") {
|
||||
this.handleAskMessage(msg.ts, msg.ask, msg.text, msg.partial)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -903,6 +822,7 @@ export class ExtensionHost extends EventEmitter {
|
|||
const delta = text.slice(streamed.text.length)
|
||||
this.writeStream(delta)
|
||||
}
|
||||
|
||||
this.finishStream(ts)
|
||||
} else {
|
||||
// Not streamed yet - output complete message
|
||||
|
|
@ -1005,37 +925,40 @@ export class ExtensionHost extends EventEmitter {
|
|||
* In non-interactive mode: auto-approve (handled by extension settings)
|
||||
*/
|
||||
private handleAskMessage(ts: number, ask: string, text: string, isPartial: boolean | undefined): void {
|
||||
// Special handling for command_output - stream it in real-time
|
||||
// This needs to happen before the isPartial skip
|
||||
// Special handling for command_output - stream it in real-time.
|
||||
// This needs to happen before the isPartial skip.
|
||||
if (ask === "command_output") {
|
||||
this.handleCommandOutputAsk(ts, text, isPartial)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip partial messages - wait for the complete ask
|
||||
// Skip partial messages - wait for the complete ask.
|
||||
if (isPartial) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we already handled this ask
|
||||
// Check if we already handled this ask.
|
||||
if (this.pendingAsks.has(ts)) {
|
||||
return
|
||||
}
|
||||
|
||||
// In non-interactive mode, the extension's auto-approval settings handle everything
|
||||
// We just need to display the action being taken
|
||||
// IMPORTANT: Check disableOutput FIRST, before nonInteractive!
|
||||
// In TUI mode (disableOutput=true), don't handle asks here - let the TUI handle them.
|
||||
// The TUI listens to the same extensionWebviewMessage events and renders its own UI.
|
||||
// If we don't check this first, the non-interactive handler would capture stdin input
|
||||
// that's meant for the TUI (e.g., arrow keys show as "[B[B[B[B" escape codes).
|
||||
if (this.options.disableOutput) {
|
||||
return
|
||||
}
|
||||
|
||||
// In non-interactive mode (without TUI), the extension's auto-approval settings handle
|
||||
// most things, but followup questions need special handling with timeout.
|
||||
if (this.options.nonInteractive) {
|
||||
this.handleAskMessageNonInteractive(ts, ask, text)
|
||||
return
|
||||
}
|
||||
|
||||
// In TUI mode (disableOutput), don't handle asks here - let the TUI handle them
|
||||
// The TUI listens to the same extensionWebviewMessage events and renders its own UI
|
||||
if (this.options.disableOutput) {
|
||||
return
|
||||
}
|
||||
|
||||
// Interactive mode - prompt user for input
|
||||
// Interactive mode - prompt user for input.
|
||||
this.handleAskMessageInteractive(ts, ask, text)
|
||||
}
|
||||
|
||||
|
|
@ -1070,9 +993,12 @@ export class ExtensionHost extends EventEmitter {
|
|||
|
||||
case "tool":
|
||||
if (!alreadyDisplayed && text) {
|
||||
let toolInfo
|
||||
let toolName
|
||||
|
||||
try {
|
||||
const toolInfo = JSON.parse(text)
|
||||
const toolName = toolInfo.tool || "unknown"
|
||||
toolInfo = JSON.parse(text)
|
||||
toolName = toolInfo.tool || "unknown"
|
||||
this.output(`\n[tool] ${toolName}`)
|
||||
|
||||
// Display all tool parameters (excluding 'tool' which is the name).
|
||||
|
|
@ -1241,8 +1167,10 @@ export class ExtensionHost extends EventEmitter {
|
|||
|
||||
// Check if user entered a number corresponding to a suggestion
|
||||
const num = parseInt(responseText, 10)
|
||||
|
||||
if (!isNaN(num) && num >= 1 && num <= suggestions.length) {
|
||||
const selectedSuggestion = suggestions[num - 1]
|
||||
|
||||
if (selectedSuggestion) {
|
||||
responseText = selectedSuggestion.answer || String(selectedSuggestion)
|
||||
this.output(`Selected: ${responseText}`)
|
||||
|
|
@ -1267,8 +1195,9 @@ export class ExtensionHost extends EventEmitter {
|
|||
|
||||
/**
|
||||
* Handle followup questions with a timeout (for non-interactive mode)
|
||||
* Shows the prompt but auto-selects the first option after 10 seconds
|
||||
* Shows the prompt but auto-selects the first option after the timeout
|
||||
* if the user doesn't type anything. Cancels the timeout on any keypress.
|
||||
* Default timeout is 60 seconds, configurable via followupAutoApproveTimeoutMs.
|
||||
*/
|
||||
private async handleFollowupQuestionWithTimeout(ts: number, text: string): Promise<void> {
|
||||
let question = text
|
||||
|
|
@ -1289,33 +1218,40 @@ export class ExtensionHost extends EventEmitter {
|
|||
// Show numbered suggestions
|
||||
if (suggestions.length > 0) {
|
||||
this.output("\nSuggested answers:")
|
||||
|
||||
suggestions.forEach((suggestion, index) => {
|
||||
const suggestionText = suggestion.answer || String(suggestion)
|
||||
const modeHint = suggestion.mode ? ` (mode: ${suggestion.mode})` : ""
|
||||
this.output(` ${index + 1}. ${suggestionText}${modeHint}`)
|
||||
})
|
||||
|
||||
this.output("")
|
||||
}
|
||||
|
||||
// Default to first suggestion or empty string
|
||||
// Default to first suggestion or empty string.
|
||||
const firstSuggestion = suggestions.length > 0 ? suggestions[0] : null
|
||||
const defaultAnswer = firstSuggestion?.answer ?? ""
|
||||
|
||||
// Default timeout is 10 seconds for testing (will be configurable later).
|
||||
const timeoutMs = 60_000
|
||||
|
||||
try {
|
||||
const answer = await this.promptForInputWithTimeout(
|
||||
suggestions.length > 0
|
||||
? `Enter number (1-${suggestions.length}) or type your answer (auto-select in 10s): `
|
||||
: "Your answer (auto-select in 10s): ",
|
||||
10000, // 10 second timeout
|
||||
? `Enter number (1-${suggestions.length}) or type your answer (auto-select in ${Math.round(timeoutMs / 1000)}s): `
|
||||
: `Your answer (auto-select in ${Math.round(timeoutMs / 1000)}s): `,
|
||||
timeoutMs,
|
||||
defaultAnswer,
|
||||
)
|
||||
|
||||
let responseText = answer.trim()
|
||||
|
||||
// Check if user entered a number corresponding to a suggestion
|
||||
// Check if user entered a number corresponding to a suggestion.
|
||||
const num = parseInt(responseText, 10)
|
||||
|
||||
if (!isNaN(num) && num >= 1 && num <= suggestions.length) {
|
||||
const selectedSuggestion = suggestions[num - 1]
|
||||
|
||||
if (selectedSuggestion) {
|
||||
responseText = selectedSuggestion.answer || String(selectedSuggestion)
|
||||
this.output(`Selected: ${responseText}`)
|
||||
|
|
@ -1323,8 +1259,8 @@ export class ExtensionHost extends EventEmitter {
|
|||
}
|
||||
|
||||
this.sendFollowupResponse(responseText)
|
||||
} catch {
|
||||
// If prompt fails, use default
|
||||
} catch (_error) {
|
||||
// If prompt fails, use default.
|
||||
this.output(`[Using default: ${defaultAnswer || "(empty)"}]`)
|
||||
this.sendFollowupResponse(defaultAnswer)
|
||||
}
|
||||
|
|
@ -1339,15 +1275,18 @@ export class ExtensionHost extends EventEmitter {
|
|||
return new Promise((resolve) => {
|
||||
// Temporarily restore console for interactive prompts
|
||||
const wasQuiet = this.options.quiet
|
||||
|
||||
if (wasQuiet) {
|
||||
this.restoreConsole()
|
||||
}
|
||||
|
||||
// Put stdin in raw mode to detect individual keypresses
|
||||
const wasRaw = process.stdin.isRaw
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true)
|
||||
}
|
||||
|
||||
process.stdin.resume()
|
||||
|
||||
let inputBuffer = ""
|
||||
|
|
@ -1371,10 +1310,13 @@ export class ExtensionHost extends EventEmitter {
|
|||
const cleanup = () => {
|
||||
clearTimeout(timeout)
|
||||
process.stdin.removeListener("data", onData)
|
||||
|
||||
if (process.stdin.isTTY && wasRaw !== undefined) {
|
||||
process.stdin.setRawMode(wasRaw)
|
||||
}
|
||||
|
||||
process.stdin.pause()
|
||||
|
||||
if (wasQuiet) {
|
||||
this.setupQuietMode()
|
||||
}
|
||||
|
|
@ -1486,13 +1428,14 @@ export class ExtensionHost extends EventEmitter {
|
|||
try {
|
||||
const approved = await this.promptForYesNo("Approve this action? (y/n): ")
|
||||
this.sendApprovalResponse(approved)
|
||||
// Note: Mode switch detection and API config re-application is handled in handleStateMessage
|
||||
// This works for both interactive and non-interactive (auto-approved) modes
|
||||
// Note: Mode switch detection and API config re-application is handled in handleStateMessage.
|
||||
// This works for both interactive and non-interactive (auto-approved) modes.
|
||||
} catch {
|
||||
this.output("[Defaulting to: no]")
|
||||
this.sendApprovalResponse(false)
|
||||
}
|
||||
// Note: Don't delete from pendingAsks - see handleFollowupQuestion comment
|
||||
|
||||
// Note: Don't delete from pendingAsks - see handleFollowupQuestion comment.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1776,22 +1719,22 @@ export class ExtensionHost extends EventEmitter {
|
|||
* Clean up resources
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
// Clear pending asks
|
||||
// Clear pending asks.
|
||||
this.pendingAsks.clear()
|
||||
|
||||
// Close readline interface if open
|
||||
// Close readline interface if open.
|
||||
if (this.rl) {
|
||||
this.rl.close()
|
||||
this.rl = null
|
||||
}
|
||||
|
||||
// Remove message listener
|
||||
// Remove message listener.
|
||||
if (this.messageListener) {
|
||||
this.off("extensionWebviewMessage", this.messageListener)
|
||||
this.messageListener = null
|
||||
}
|
||||
|
||||
// Deactivate extension if it has a deactivate function
|
||||
// Deactivate extension if it has a deactivate function.
|
||||
if (this.extensionModule?.deactivate) {
|
||||
try {
|
||||
await this.extensionModule.deactivate()
|
||||
|
|
@ -1800,17 +1743,27 @@ export class ExtensionHost extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// Clear references
|
||||
// Clear references.
|
||||
this.vscode = null
|
||||
this.extensionModule = null
|
||||
this.extensionAPI = null
|
||||
this.webviewProviders.clear()
|
||||
|
||||
// Clear globals
|
||||
// Clear globals.
|
||||
delete (global as Record<string, unknown>).vscode
|
||||
delete (global as Record<string, unknown>).__extensionHost
|
||||
|
||||
// Restore console if it was suppressed
|
||||
// Restore console if it was suppressed.
|
||||
this.restoreConsole()
|
||||
|
||||
// Clean up ephemeral storage directory if it exists.
|
||||
if (this.ephemeralStorageDir) {
|
||||
try {
|
||||
await fs.promises.rm(this.ephemeralStorageDir, { recursive: true, force: true })
|
||||
this.ephemeralStorageDir = null
|
||||
} catch (_error) {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ program
|
|||
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
||||
DEFAULTS.reasoningEffort,
|
||||
)
|
||||
.option("--ephemeral", "Run without persisting state (uses temporary storage)", false)
|
||||
.option("--no-tui", "Disable TUI, use plain text output")
|
||||
.action(
|
||||
async (
|
||||
|
|
@ -74,6 +75,7 @@ program
|
|||
model?: string
|
||||
mode?: string
|
||||
reasoningEffort?: ReasoningEffortExtended | "unspecified" | "disabled"
|
||||
ephemeral: boolean
|
||||
tui: boolean
|
||||
},
|
||||
) => {
|
||||
|
|
@ -153,6 +155,7 @@ program
|
|||
quiet: boolean
|
||||
nonInteractive: boolean
|
||||
disableOutput: boolean
|
||||
ephemeral?: boolean
|
||||
}) => {
|
||||
return new ExtensionHost({
|
||||
mode: opts.mode,
|
||||
|
|
@ -169,6 +172,7 @@ program
|
|||
quiet: opts.quiet,
|
||||
nonInteractive: opts.nonInteractive,
|
||||
disableOutput: opts.disableOutput,
|
||||
ephemeral: opts.ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -186,6 +190,7 @@ program
|
|||
debug: options.debug,
|
||||
exitOnComplete: options.exitOnComplete,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
ephemeral: options.ephemeral,
|
||||
createExtensionHost: createExtensionHost,
|
||||
version: packageJson.version,
|
||||
}),
|
||||
|
|
@ -219,6 +224,7 @@ program
|
|||
verbose: options.debug,
|
||||
quiet: !options.verbose && !options.debug,
|
||||
nonInteractive: options.yes,
|
||||
ephemeral: options.ephemeral,
|
||||
})
|
||||
|
||||
// Handle SIGINT (Ctrl+C)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react"
|
|||
import { EventEmitter } from "events"
|
||||
import { randomUUID } from "crypto"
|
||||
|
||||
import type { ClineMessage, TodoItem, WebviewMessage } from "@roo-code/types"
|
||||
// Import only message-utils to avoid custom-tools dependencies (execa/child_process)
|
||||
import { consolidateTokenUsage, consolidateApiRequests, consolidateCommands } from "@roo-code/core/message-utils"
|
||||
import type { ClineMessage, TodoItem } from "@roo-code/types"
|
||||
|
||||
import { useCLIStore } from "./store.js"
|
||||
import { getContextWindow } from "../utils/getContextWindow.js"
|
||||
|
|
@ -56,7 +56,7 @@ const PICKER_HEIGHT = 10 // Max height for picker when open
|
|||
interface ExtensionHostInterface extends EventEmitter {
|
||||
activate(): Promise<void>
|
||||
runTask(prompt: string): Promise<void>
|
||||
sendToExtension(message: unknown): void
|
||||
sendToExtension(message: WebviewMessage): void
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +77,7 @@ interface ExtensionHostOptions {
|
|||
quiet: boolean
|
||||
nonInteractive: boolean
|
||||
disableOutput: boolean
|
||||
ephemeral?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -155,6 +156,7 @@ function AppInner({
|
|||
debug,
|
||||
exitOnComplete,
|
||||
reasoningEffort,
|
||||
ephemeral,
|
||||
createExtensionHost,
|
||||
version,
|
||||
}: TUIAppProps) {
|
||||
|
|
@ -228,6 +230,11 @@ function AppInner({
|
|||
const exitHintTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||
const pendingExit = useRef(false)
|
||||
|
||||
// Countdown timer for auto-accepting followup questions (10 seconds for testing)
|
||||
const FOLLOWUP_TIMEOUT_SECONDS = 10
|
||||
const [countdownSeconds, setCountdownSeconds] = useState<number | null>(null)
|
||||
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Track whether user wants to type custom response for followup questions
|
||||
const [showCustomInput, setShowCustomInput] = useState(false)
|
||||
// Ref to track transition state (handles async state update timing)
|
||||
|
|
@ -313,10 +320,7 @@ function AppInner({
|
|||
return
|
||||
}
|
||||
|
||||
hostRef.current.sendToExtension({
|
||||
type: "searchFiles",
|
||||
query,
|
||||
})
|
||||
hostRef.current.sendToExtension({ type: "searchFiles", query })
|
||||
}, [])
|
||||
|
||||
// Create autocomplete triggers
|
||||
|
|
@ -413,9 +417,64 @@ function AppInner({
|
|||
if (exitHintTimeout.current) {
|
||||
clearTimeout(exitHintTimeout.current)
|
||||
}
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Countdown timer for auto-accepting followup questions
|
||||
// Start countdown when a followup question with suggestions appears
|
||||
useEffect(() => {
|
||||
// Clear any existing countdown
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current)
|
||||
countdownIntervalRef.current = null
|
||||
}
|
||||
|
||||
// Only start countdown for followup questions with suggestions (not custom input mode)
|
||||
if (
|
||||
pendingAsk?.type === "followup" &&
|
||||
pendingAsk.suggestions &&
|
||||
pendingAsk.suggestions.length > 0 &&
|
||||
!showCustomInput
|
||||
) {
|
||||
// Start countdown
|
||||
setCountdownSeconds(FOLLOWUP_TIMEOUT_SECONDS)
|
||||
|
||||
countdownIntervalRef.current = setInterval(() => {
|
||||
setCountdownSeconds((prev) => {
|
||||
if (prev === null || prev <= 1) {
|
||||
// Time's up! Auto-select first option
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current)
|
||||
countdownIntervalRef.current = null
|
||||
}
|
||||
// Auto-submit the first suggestion
|
||||
if (pendingAsk?.suggestions && pendingAsk.suggestions.length > 0) {
|
||||
const firstSuggestion = pendingAsk.suggestions[0]
|
||||
if (firstSuggestion) {
|
||||
handleSubmit(firstSuggestion.answer)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
} else {
|
||||
// No countdown needed
|
||||
setCountdownSeconds(null)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current)
|
||||
countdownIntervalRef.current = null
|
||||
}
|
||||
}
|
||||
}, [pendingAsk?.id, pendingAsk?.type, showCustomInput]) // Re-run when pendingAsk changes or user switches to custom input
|
||||
|
||||
// Refresh search results when fileSearchResults changes while file picker is open
|
||||
// This handles the async timing where API results arrive after initial search
|
||||
// IMPORTANT: Only run when fileSearchResults array identity changes (new API response)
|
||||
|
|
@ -774,6 +833,7 @@ function AppInner({
|
|||
quiet: !verbose && !debug,
|
||||
nonInteractive,
|
||||
disableOutput: true,
|
||||
ephemeral,
|
||||
})
|
||||
|
||||
hostRef.current = host
|
||||
|
|
@ -943,6 +1003,28 @@ function AppInner({
|
|||
}
|
||||
})
|
||||
|
||||
// Cancel countdown timer when user navigates in the followup suggestion menu
|
||||
// This provides better UX - any user interaction cancels the auto-accept timer
|
||||
const showFollowupSuggestions =
|
||||
pendingAsk?.type === "followup" &&
|
||||
pendingAsk.suggestions &&
|
||||
pendingAsk.suggestions.length > 0 &&
|
||||
!showCustomInput
|
||||
|
||||
useInput((_input, key) => {
|
||||
// Only handle when followup suggestions are shown and countdown is active
|
||||
if (showFollowupSuggestions && countdownSeconds !== null) {
|
||||
// Cancel countdown on any arrow key navigation
|
||||
if (key.upArrow || key.downArrow) {
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current)
|
||||
countdownIntervalRef.current = null
|
||||
}
|
||||
setCountdownSeconds(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle picker state changes from AutocompleteInput
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handlePickerStateChange = useCallback((state: AutocompletePickerState<any>) => setPickerState(state), [])
|
||||
|
|
@ -999,9 +1081,10 @@ function AppInner({
|
|||
}
|
||||
|
||||
// Status bar content
|
||||
// Don't show spinner when waiting for user input (pendingAsk is set)
|
||||
const statusBarMessage = showExitHint ? (
|
||||
<Text color="yellow">Press Ctrl+C again to exit</Text>
|
||||
) : isLoading ? (
|
||||
) : isLoading && !pendingAsk ? (
|
||||
<Box>
|
||||
<LoadingText>{view === "ToolUse" ? "Using tool" : "Thinking"}</LoadingText>
|
||||
<Text color={theme.dimText}> • </Text>
|
||||
|
|
@ -1082,6 +1165,13 @@ function AppInner({
|
|||
if (showCustomInput || isTransitioningToCustomInput.current) return
|
||||
|
||||
if (value === "__CUSTOM__") {
|
||||
// Clear countdown timer synchronously BEFORE state update
|
||||
// This prevents race condition where interval fires before useEffect cleanup
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current)
|
||||
countdownIntervalRef.current = null
|
||||
}
|
||||
setCountdownSeconds(null)
|
||||
isTransitioningToCustomInput.current = true
|
||||
setShowCustomInput(true)
|
||||
} else if (value.trim()) {
|
||||
|
|
@ -1090,7 +1180,12 @@ function AppInner({
|
|||
}}
|
||||
/>
|
||||
<HorizontalLine active={true} />
|
||||
<Text color={theme.dimText}>↑↓ navigate • Enter select</Text>
|
||||
<Text color={theme.dimText}>
|
||||
↑↓ navigate • Enter select
|
||||
{countdownSeconds !== null && (
|
||||
<Text color="yellow"> • Auto-select in {countdownSeconds}s</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ export interface AppProps {
|
|||
debug: boolean
|
||||
exitOnComplete: boolean
|
||||
reasoningEffort?: string
|
||||
/** Run in ephemeral mode - no state persists after this session */
|
||||
ephemeral?: boolean
|
||||
version: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./message-utils": "./src/message-utils/index.ts"
|
||||
"./message-utils": "./src/message-utils/index.ts",
|
||||
"./debug-log": "./src/debug-log/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src --ext=ts --max-warnings=0",
|
||||
|
|
|
|||
91
packages/core/src/debug-log/index.ts
Normal file
91
packages/core/src/debug-log/index.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* File-based debug logging utility
|
||||
*
|
||||
* This writes logs to ~/.roo/cli-debug.log, avoiding stdout/stderr
|
||||
* which would break TUI applications. The log format is timestamped JSON.
|
||||
*
|
||||
* Usage:
|
||||
* import { debugLog, DebugLogger } from "@roo-code/core/debug-log"
|
||||
*
|
||||
* // Simple logging
|
||||
* debugLog("handleModeSwitch", { mode: newMode, configId })
|
||||
*
|
||||
* // Or create a named logger for a component
|
||||
* const log = new DebugLogger("ClineProvider")
|
||||
* log.info("handleModeSwitch", { mode: newMode })
|
||||
*/
|
||||
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
|
||||
const DEBUG_LOG_PATH = path.join(os.homedir(), ".roo", "cli-debug.log")
|
||||
|
||||
/**
|
||||
* Simple file-based debug log function.
|
||||
* Writes timestamped entries to ~/.roo/cli-debug.log
|
||||
*/
|
||||
export function debugLog(message: string, data?: unknown): void {
|
||||
try {
|
||||
const logDir = path.dirname(DEBUG_LOG_PATH)
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
const entry = data
|
||||
? `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`
|
||||
: `[${timestamp}] ${message}\n`
|
||||
|
||||
fs.appendFileSync(DEBUG_LOG_PATH, entry)
|
||||
} catch {
|
||||
// NO-OP - don't let logging errors break functionality
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug logger with component context.
|
||||
* Prefixes all messages with the component name.
|
||||
*/
|
||||
export class DebugLogger {
|
||||
private component: string
|
||||
|
||||
constructor(component: string) {
|
||||
this.component = component
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug message with optional data
|
||||
*/
|
||||
debug(message: string, data?: unknown): void {
|
||||
debugLog(`[${this.component}] ${message}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for debug
|
||||
*/
|
||||
info(message: string, data?: unknown): void {
|
||||
this.debug(message, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning
|
||||
*/
|
||||
warn(message: string, data?: unknown): void {
|
||||
debugLog(`[${this.component}] WARN: ${message}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error
|
||||
*/
|
||||
error(message: string, data?: unknown): void {
|
||||
debugLog(`[${this.component}] ERROR: ${message}`, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured logger for provider/mode debugging
|
||||
*/
|
||||
export const providerDebugLog = new DebugLogger("ProviderSettings")
|
||||
|
|
@ -1,11 +1,18 @@
|
|||
import type { GlobalSettings } from "./global-settings.js"
|
||||
import { z } from "zod"
|
||||
|
||||
import type { GlobalSettings, RooCodeSettings } from "./global-settings.js"
|
||||
import type { ProviderSettings, ProviderSettingsEntry } from "./provider-settings.js"
|
||||
import type { HistoryItem } from "./history.js"
|
||||
import type { ModeConfig } from "./mode.js"
|
||||
import type { ModeConfig, PromptComponent } from "./mode.js"
|
||||
import type { TelemetrySetting } from "./telemetry.js"
|
||||
import type { Experiments } from "./experiment.js"
|
||||
import type { ClineMessage, QueuedMessage } from "./message.js"
|
||||
import type { MarketplaceItem, MarketplaceInstalledMetadata } from "./marketplace.js"
|
||||
import {
|
||||
type MarketplaceItem,
|
||||
type MarketplaceInstalledMetadata,
|
||||
type InstallMarketplaceItemOptions,
|
||||
marketplaceItemSchema,
|
||||
} from "./marketplace.js"
|
||||
import type { TodoItem } from "./todo.js"
|
||||
import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList, ShareVisibility } from "./cloud.js"
|
||||
import type { SerializedCustomToolDefinition } from "./custom-tool.js"
|
||||
|
|
@ -13,7 +20,10 @@ import type { GitCommit } from "./git.js"
|
|||
import type { McpServer } from "./mcp.js"
|
||||
import type { ModelRecord, RouterModels } from "./model.js"
|
||||
|
||||
// Represents JSON data that is sent from extension to the webview or cli.
|
||||
/**
|
||||
* ExtensionMessage
|
||||
* Extension -> Webview | CLI
|
||||
*/
|
||||
export interface ExtensionMessage {
|
||||
type:
|
||||
| "action"
|
||||
|
|
@ -325,3 +335,310 @@ export interface Command {
|
|||
description?: string
|
||||
argumentHint?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* WebviewMessage
|
||||
* Webview | CLI -> Extension
|
||||
*/
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" | "objectResponse"
|
||||
|
||||
export type AudioType = "notification" | "celebration" | "progress_loop"
|
||||
|
||||
export interface UpdateTodoListPayload {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
todos: any[]
|
||||
}
|
||||
|
||||
export type EditQueuedMessagePayload = Pick<QueuedMessage, "id" | "text" | "images">
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "updateTodoList"
|
||||
| "deleteMultipleTasksWithIds"
|
||||
| "currentApiConfigName"
|
||||
| "saveApiConfiguration"
|
||||
| "upsertApiConfiguration"
|
||||
| "deleteApiConfiguration"
|
||||
| "loadApiConfiguration"
|
||||
| "loadApiConfigurationById"
|
||||
| "renameApiConfiguration"
|
||||
| "getListApiConfiguration"
|
||||
| "customInstructions"
|
||||
| "webviewDidLaunch"
|
||||
| "newTask"
|
||||
| "askResponse"
|
||||
| "terminalOperation"
|
||||
| "clearTask"
|
||||
| "didShowAnnouncement"
|
||||
| "selectImages"
|
||||
| "exportCurrentTask"
|
||||
| "shareCurrentTask"
|
||||
| "showTaskWithId"
|
||||
| "deleteTaskWithId"
|
||||
| "exportTaskWithId"
|
||||
| "importSettings"
|
||||
| "exportSettings"
|
||||
| "resetState"
|
||||
| "flushRouterModels"
|
||||
| "requestRouterModels"
|
||||
| "requestOpenAiModels"
|
||||
| "requestOllamaModels"
|
||||
| "requestLmStudioModels"
|
||||
| "requestRooModels"
|
||||
| "requestRooCreditBalance"
|
||||
| "requestVsCodeLmModels"
|
||||
| "requestHuggingFaceModels"
|
||||
| "openImage"
|
||||
| "saveImage"
|
||||
| "openFile"
|
||||
| "openMention"
|
||||
| "cancelTask"
|
||||
| "cancelAutoApproval"
|
||||
| "updateVSCodeSetting"
|
||||
| "getVSCodeSetting"
|
||||
| "vsCodeSetting"
|
||||
| "updateCondensingPrompt"
|
||||
| "playSound"
|
||||
| "playTts"
|
||||
| "stopTts"
|
||||
| "ttsEnabled"
|
||||
| "ttsSpeed"
|
||||
| "openKeyboardShortcuts"
|
||||
| "openMcpSettings"
|
||||
| "openProjectMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "refreshAllMcpServers"
|
||||
| "toggleToolAlwaysAllow"
|
||||
| "toggleToolEnabledForPrompt"
|
||||
| "toggleMcpServer"
|
||||
| "updateMcpTimeout"
|
||||
| "enhancePrompt"
|
||||
| "enhancedPrompt"
|
||||
| "draggedImages"
|
||||
| "deleteMessage"
|
||||
| "deleteMessageConfirm"
|
||||
| "submitEditedMessage"
|
||||
| "editMessageConfirm"
|
||||
| "enableMcpServerCreation"
|
||||
| "remoteControlEnabled"
|
||||
| "taskSyncEnabled"
|
||||
| "searchCommits"
|
||||
| "setApiConfigPassword"
|
||||
| "mode"
|
||||
| "updatePrompt"
|
||||
| "getSystemPrompt"
|
||||
| "copySystemPrompt"
|
||||
| "systemPrompt"
|
||||
| "enhancementApiConfigId"
|
||||
| "autoApprovalEnabled"
|
||||
| "updateCustomMode"
|
||||
| "deleteCustomMode"
|
||||
| "setopenAiCustomModelInfo"
|
||||
| "openCustomModesSettings"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "deleteMcpServer"
|
||||
| "codebaseIndexEnabled"
|
||||
| "telemetrySetting"
|
||||
| "testBrowserConnection"
|
||||
| "browserConnectionResult"
|
||||
| "searchFiles"
|
||||
| "toggleApiConfigPin"
|
||||
| "hasOpenedModeSelector"
|
||||
| "clearCloudAuthSkipModel"
|
||||
| "cloudButtonClicked"
|
||||
| "rooCloudSignIn"
|
||||
| "cloudLandingPageSignIn"
|
||||
| "rooCloudSignOut"
|
||||
| "rooCloudManualUrl"
|
||||
| "claudeCodeSignIn"
|
||||
| "claudeCodeSignOut"
|
||||
| "switchOrganization"
|
||||
| "condenseTaskContextRequest"
|
||||
| "requestIndexingStatus"
|
||||
| "startIndexing"
|
||||
| "clearIndexData"
|
||||
| "indexingStatusUpdate"
|
||||
| "indexCleared"
|
||||
| "focusPanelRequest"
|
||||
| "openExternal"
|
||||
| "filterMarketplaceItems"
|
||||
| "marketplaceButtonClicked"
|
||||
| "installMarketplaceItem"
|
||||
| "installMarketplaceItemWithParameters"
|
||||
| "cancelMarketplaceInstall"
|
||||
| "removeInstalledMarketplaceItem"
|
||||
| "marketplaceInstallResult"
|
||||
| "fetchMarketplaceData"
|
||||
| "switchTab"
|
||||
| "shareTaskSuccess"
|
||||
| "exportMode"
|
||||
| "exportModeResult"
|
||||
| "importMode"
|
||||
| "importModeResult"
|
||||
| "checkRulesDirectory"
|
||||
| "checkRulesDirectoryResult"
|
||||
| "saveCodeIndexSettingsAtomic"
|
||||
| "requestCodeIndexSecretStatus"
|
||||
| "requestCommands"
|
||||
| "openCommandFile"
|
||||
| "deleteCommand"
|
||||
| "createCommand"
|
||||
| "insertTextIntoTextarea"
|
||||
| "showMdmAuthRequiredNotification"
|
||||
| "imageGenerationSettings"
|
||||
| "queueMessage"
|
||||
| "removeQueuedMessage"
|
||||
| "editQueuedMessage"
|
||||
| "dismissUpsell"
|
||||
| "getDismissedUpsells"
|
||||
| "updateSettings"
|
||||
| "allowedCommands"
|
||||
| "deniedCommands"
|
||||
| "killBrowserSession"
|
||||
| "openBrowserSessionPanel"
|
||||
| "showBrowserSessionPanelAtStep"
|
||||
| "refreshBrowserSessionPanel"
|
||||
| "browserPanelDidLaunch"
|
||||
| "openDebugApiHistory"
|
||||
| "openDebugUiHistory"
|
||||
| "downloadErrorDiagnostics"
|
||||
| "requestClaudeCodeRateLimits"
|
||||
| "refreshCustomTools"
|
||||
| "requestModes"
|
||||
| "switchMode"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
disabled?: boolean
|
||||
context?: string
|
||||
dataUri?: string
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ProviderSettings
|
||||
images?: string[]
|
||||
bool?: boolean
|
||||
value?: number
|
||||
stepIndex?: number
|
||||
isLaunchAction?: boolean
|
||||
forceShow?: boolean
|
||||
commands?: string[]
|
||||
audioType?: AudioType
|
||||
serverName?: string
|
||||
toolName?: string
|
||||
alwaysAllow?: boolean
|
||||
isEnabled?: boolean
|
||||
mode?: string
|
||||
promptMode?: string | "enhance"
|
||||
customPrompt?: PromptComponent
|
||||
dataUrls?: string[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
values?: Record<string, any>
|
||||
query?: string
|
||||
setting?: string
|
||||
slug?: string
|
||||
modeConfig?: ModeConfig
|
||||
timeout?: number
|
||||
payload?: WebViewMessagePayload
|
||||
source?: "global" | "project"
|
||||
requestId?: string
|
||||
ids?: string[]
|
||||
hasSystemPromptOverride?: boolean
|
||||
terminalOperation?: "continue" | "abort"
|
||||
messageTs?: number
|
||||
restoreCheckpoint?: boolean
|
||||
historyPreviewCollapsed?: boolean
|
||||
filters?: { type?: string; search?: string; tags?: string[] }
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settings?: any
|
||||
url?: string // For openExternal
|
||||
mpItem?: MarketplaceItem
|
||||
mpInstallOptions?: InstallMarketplaceItemOptions
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config?: Record<string, any> // Add config to the payload
|
||||
visibility?: ShareVisibility // For share visibility
|
||||
hasContent?: boolean // For checkRulesDirectoryResult
|
||||
checkOnly?: boolean // For deleteCustomMode check
|
||||
upsellId?: string // For dismissUpsell
|
||||
list?: string[] // For dismissedUpsells response
|
||||
organizationId?: string | null // For organization switching
|
||||
useProviderSignup?: boolean // For rooCloudSignIn to use provider signup flow
|
||||
codeIndexSettings?: {
|
||||
// Global state settings
|
||||
codebaseIndexEnabled: boolean
|
||||
codebaseIndexQdrantUrl: string
|
||||
codebaseIndexEmbedderProvider:
|
||||
| "openai"
|
||||
| "ollama"
|
||||
| "openai-compatible"
|
||||
| "gemini"
|
||||
| "mistral"
|
||||
| "vercel-ai-gateway"
|
||||
| "bedrock"
|
||||
| "openrouter"
|
||||
codebaseIndexEmbedderBaseUrl?: string
|
||||
codebaseIndexEmbedderModelId: string
|
||||
codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers
|
||||
codebaseIndexOpenAiCompatibleBaseUrl?: string
|
||||
codebaseIndexBedrockRegion?: string
|
||||
codebaseIndexBedrockProfile?: string
|
||||
codebaseIndexSearchMaxResults?: number
|
||||
codebaseIndexSearchMinScore?: number
|
||||
codebaseIndexOpenRouterSpecificProvider?: string // OpenRouter provider routing
|
||||
|
||||
// Secret settings
|
||||
codeIndexOpenAiKey?: string
|
||||
codeIndexQdrantApiKey?: string
|
||||
codebaseIndexOpenAiCompatibleApiKey?: string
|
||||
codebaseIndexGeminiApiKey?: string
|
||||
codebaseIndexMistralApiKey?: string
|
||||
codebaseIndexVercelAiGatewayApiKey?: string
|
||||
codebaseIndexOpenRouterApiKey?: string
|
||||
}
|
||||
updatedSettings?: RooCodeSettings
|
||||
}
|
||||
|
||||
export const checkoutDiffPayloadSchema = z.object({
|
||||
ts: z.number().optional(),
|
||||
previousCommitHash: z.string().optional(),
|
||||
commitHash: z.string(),
|
||||
mode: z.enum(["full", "checkpoint", "from-init", "to-current"]),
|
||||
})
|
||||
|
||||
export type CheckpointDiffPayload = z.infer<typeof checkoutDiffPayloadSchema>
|
||||
|
||||
export const checkoutRestorePayloadSchema = z.object({
|
||||
ts: z.number(),
|
||||
commitHash: z.string(),
|
||||
mode: z.enum(["preview", "restore"]),
|
||||
})
|
||||
|
||||
export type CheckpointRestorePayload = z.infer<typeof checkoutRestorePayloadSchema>
|
||||
|
||||
export interface IndexingStatusPayload {
|
||||
state: "Standby" | "Indexing" | "Indexed" | "Error"
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface IndexClearedPayload {
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const installMarketplaceItemWithParametersPayloadSchema = z.object({
|
||||
item: marketplaceItemSchema,
|
||||
parameters: z.record(z.string(), z.any()),
|
||||
})
|
||||
|
||||
export type InstallMarketplaceItemWithParametersPayload = z.infer<
|
||||
typeof installMarketplaceItemWithParametersPayloadSchema
|
||||
>
|
||||
|
||||
export type WebViewMessagePayload =
|
||||
| CheckpointDiffPayload
|
||||
| CheckpointRestorePayload
|
||||
| IndexingStatusPayload
|
||||
| IndexClearedPayload
|
||||
| InstallMarketplaceItemWithParametersPayload
|
||||
| UpdateTodoListPayload
|
||||
| EditQueuedMessagePayload
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ export interface VSCodeAPIMockOptions {
|
|||
* Defaults to the directory containing this module.
|
||||
*/
|
||||
appRoot?: string
|
||||
|
||||
/**
|
||||
* Custom storage directory for persistent state.
|
||||
* Defaults to ~/.vscode-mock.
|
||||
* Set to a temp directory for ephemeral/no-persist mode.
|
||||
*/
|
||||
storageDir?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,6 +89,7 @@ export function createVSCodeAPIMock(
|
|||
const context = new ExtensionContextImpl({
|
||||
extensionPath: extensionRootPath,
|
||||
workspacePath: workspacePath,
|
||||
storageDir: options?.storageDir,
|
||||
})
|
||||
const workspace = new WorkspaceAPI(workspacePath, context)
|
||||
const window = new WindowAPI()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { ExtensionContext } from "vscode"
|
|||
import { z, ZodError } from "zod"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
|
||||
import { debugLog } from "../../utils/debug-log"
|
||||
|
||||
import {
|
||||
type ProviderSettingsWithId,
|
||||
providerSettingsWithIdSchema,
|
||||
|
|
@ -377,12 +379,21 @@ export class ProviderSettingsManager {
|
|||
return await this.lock(async () => {
|
||||
const providerProfiles = await this.load()
|
||||
|
||||
return Object.entries(providerProfiles.apiConfigs).map(([name, apiConfig]) => ({
|
||||
const configs = Object.entries(providerProfiles.apiConfigs).map(([name, apiConfig]) => ({
|
||||
name,
|
||||
id: apiConfig.id || "",
|
||||
apiProvider: apiConfig.apiProvider,
|
||||
modelId: this.cleanModelId(getModelId(apiConfig)),
|
||||
}))
|
||||
|
||||
// DEBUG: Log listed configs
|
||||
debugLog("[ProviderSettingsManager.listConfig]", {
|
||||
configCount: configs.length,
|
||||
configs: configs.map((c) => ({ name: c.name, id: c.id, provider: c.apiProvider })),
|
||||
modeApiConfigs: providerProfiles.modeApiConfigs,
|
||||
})
|
||||
|
||||
return configs
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list configs: ${error}`)
|
||||
|
|
@ -458,16 +469,40 @@ export class ProviderSettingsManager {
|
|||
public async activateProfile(
|
||||
params: { name: string } | { id: string },
|
||||
): Promise<ProviderSettingsWithId & { name: string }> {
|
||||
// DEBUG: Log entry point
|
||||
debugLog("[ProviderSettingsManager.activateProfile] START", { params })
|
||||
|
||||
const { name, ...providerSettings } = await this.getProfile(params)
|
||||
|
||||
// DEBUG: Log what was retrieved from getProfile
|
||||
debugLog("[ProviderSettingsManager.activateProfile] getProfile result", {
|
||||
name,
|
||||
id: providerSettings.id,
|
||||
apiProvider: providerSettings.apiProvider,
|
||||
hasSettings: Object.keys(providerSettings).length > 0,
|
||||
settingsKeys: Object.keys(providerSettings).filter(
|
||||
(k) => providerSettings[k as keyof typeof providerSettings] !== undefined,
|
||||
),
|
||||
})
|
||||
|
||||
try {
|
||||
return await this.lock(async () => {
|
||||
const providerProfiles = await this.load()
|
||||
providerProfiles.currentApiConfigName = name
|
||||
await this.store(providerProfiles)
|
||||
|
||||
debugLog("[ProviderSettingsManager.activateProfile] END - profile activated", {
|
||||
name,
|
||||
id: providerSettings.id,
|
||||
})
|
||||
|
||||
return { name, ...providerSettings }
|
||||
})
|
||||
} catch (error) {
|
||||
debugLog("[ProviderSettingsManager.activateProfile] ERROR", {
|
||||
params,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
throw new Error(`Failed to activate profile: ${error instanceof Error ? error.message : error}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -536,10 +571,27 @@ export class ProviderSettingsManager {
|
|||
public async getModeConfigId(mode: Mode) {
|
||||
try {
|
||||
return await this.lock(async () => {
|
||||
const { modeApiConfigs } = await this.load()
|
||||
return modeApiConfigs?.[mode]
|
||||
const providerProfiles = await this.load()
|
||||
const configId = providerProfiles.modeApiConfigs?.[mode]
|
||||
|
||||
// DEBUG: Log mode config lookup
|
||||
debugLog("[ProviderSettingsManager.getModeConfigId]", {
|
||||
mode,
|
||||
configId,
|
||||
allModeApiConfigs: providerProfiles.modeApiConfigs,
|
||||
availableConfigIds: Object.entries(providerProfiles.apiConfigs).map(([name, c]) => ({
|
||||
name,
|
||||
id: c.id,
|
||||
})),
|
||||
})
|
||||
|
||||
return configId
|
||||
})
|
||||
} catch (error) {
|
||||
debugLog("[ProviderSettingsManager.getModeConfigId] ERROR", {
|
||||
mode,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
throw new Error(`Failed to get mode config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -610,7 +662,15 @@ export class ProviderSettingsManager {
|
|||
try {
|
||||
const content = await this.context.secrets.get(this.secretsKey)
|
||||
|
||||
// DEBUG: Log raw secrets content
|
||||
debugLog("[ProviderSettingsManager.load] secrets.get result", {
|
||||
secretsKey: this.secretsKey,
|
||||
hasContent: !!content,
|
||||
contentLength: content?.length ?? 0,
|
||||
})
|
||||
|
||||
if (!content) {
|
||||
debugLog("[ProviderSettingsManager.load] returning default profiles (no content found)")
|
||||
return this.defaultProviderProfiles
|
||||
}
|
||||
|
||||
|
|
@ -631,12 +691,31 @@ export class ProviderSettingsManager {
|
|||
{} as Record<string, ProviderSettingsWithId>,
|
||||
)
|
||||
|
||||
return {
|
||||
const result = {
|
||||
...providerProfiles,
|
||||
apiConfigs: Object.fromEntries(
|
||||
Object.entries(apiConfigs).filter(([_, apiConfig]) => apiConfig !== null),
|
||||
),
|
||||
}
|
||||
|
||||
// DEBUG: Log loaded profiles summary
|
||||
debugLog("[ProviderSettingsManager.load] loaded profiles", {
|
||||
currentApiConfigName: result.currentApiConfigName,
|
||||
apiConfigNames: Object.keys(result.apiConfigs),
|
||||
modeApiConfigs: result.modeApiConfigs,
|
||||
configDetails: Object.entries(result.apiConfigs).map(([name, config]) => ({
|
||||
name,
|
||||
id: config.id,
|
||||
apiProvider: config.apiProvider,
|
||||
hasApiKey: !!(
|
||||
(config as any)?.apiKey ||
|
||||
(config as any)?.openRouterApiKey ||
|
||||
(config as any)?.requestyApiKey
|
||||
),
|
||||
})),
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
TelemetryService.instance.captureSchemaValidationError({
|
||||
|
|
@ -645,6 +724,10 @@ export class ProviderSettingsManager {
|
|||
})
|
||||
}
|
||||
|
||||
debugLog("[ProviderSettingsManager.load] ERROR", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
|
||||
throw new Error(`Failed to read provider profiles from secrets: ${error}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import * as path from "path"
|
|||
import fs from "fs/promises"
|
||||
import EventEmitter from "events"
|
||||
|
||||
import { debugLog, DebugLogger } from "../../utils/debug-log"
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import delay from "delay"
|
||||
import axios from "axios"
|
||||
|
|
@ -904,7 +906,24 @@ export class ClineProvider
|
|||
|
||||
if (profile?.name) {
|
||||
try {
|
||||
await this.activateProviderProfile({ name: profile.name })
|
||||
// Check if the profile has actual API configuration (not just an id).
|
||||
// In CLI mode, the ProviderSettingsManager may return empty default profiles
|
||||
// that only contain 'id' and 'name' fields. Activating such a profile would
|
||||
// overwrite the CLI's working API configuration with empty settings.
|
||||
const fullProfile = await this.providerSettingsManager.getProfile({ name: profile.name })
|
||||
const hasActualSettings = !!fullProfile.apiProvider
|
||||
|
||||
if (hasActualSettings) {
|
||||
await this.activateProviderProfile({ name: profile.name })
|
||||
} else {
|
||||
debugLog(
|
||||
"[createTaskWithHistoryItem] SKIPPING profile activation - profile has no apiProvider",
|
||||
{
|
||||
savedConfigId,
|
||||
profileName: profile.name,
|
||||
},
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but continue with task restoration.
|
||||
this.log(
|
||||
|
|
@ -1241,6 +1260,15 @@ export class ClineProvider
|
|||
* @param newMode The mode to switch to
|
||||
*/
|
||||
public async handleModeSwitch(newMode: Mode) {
|
||||
// DEBUG: Log entry point with current state
|
||||
const currentApiConfigName = this.getGlobalState("currentApiConfigName")
|
||||
const currentMode = this.getGlobalState("mode")
|
||||
debugLog("[handleModeSwitch] START", {
|
||||
newMode,
|
||||
currentMode,
|
||||
currentApiConfigName,
|
||||
})
|
||||
|
||||
const task = this.getCurrentTask()
|
||||
|
||||
if (task) {
|
||||
|
|
@ -1279,6 +1307,14 @@ export class ClineProvider
|
|||
const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
|
||||
const listApiConfig = await this.providerSettingsManager.listConfig()
|
||||
|
||||
// DEBUG: Log mode config lookup results
|
||||
debugLog("[handleModeSwitch] getModeConfigId result", {
|
||||
newMode,
|
||||
savedConfigId,
|
||||
listApiConfigCount: listApiConfig.length,
|
||||
listApiConfigNames: listApiConfig.map((c) => ({ name: c.name, id: c.id, provider: c.apiProvider })),
|
||||
})
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data.
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
|
||||
|
|
@ -1286,22 +1322,80 @@ export class ClineProvider
|
|||
if (savedConfigId) {
|
||||
const profile = listApiConfig.find(({ id }) => id === savedConfigId)
|
||||
|
||||
// DEBUG: Log profile activation attempt
|
||||
debugLog("[handleModeSwitch] activating saved config", {
|
||||
savedConfigId,
|
||||
foundProfile: profile ? { name: profile.name, id: profile.id, provider: profile.apiProvider } : null,
|
||||
})
|
||||
|
||||
if (profile?.name) {
|
||||
await this.activateProviderProfile({ name: profile.name })
|
||||
// Check if the profile has actual API configuration (not just an id).
|
||||
// In CLI mode, the ProviderSettingsManager may return empty default profiles
|
||||
// that only contain 'id' and 'name' fields. Activating such a profile would
|
||||
// overwrite the CLI's working API configuration with empty settings.
|
||||
// Skip activation if the profile has no apiProvider set - this indicates
|
||||
// an unconfigured/empty profile.
|
||||
const fullProfile = await this.providerSettingsManager.getProfile({ name: profile.name })
|
||||
const hasActualSettings = !!fullProfile.apiProvider
|
||||
|
||||
if (hasActualSettings) {
|
||||
await this.activateProviderProfile({ name: profile.name })
|
||||
} else {
|
||||
debugLog(
|
||||
"[handleModeSwitch] SKIPPING profile activation - profile has no apiProvider (CLI mode workaround)",
|
||||
{
|
||||
savedConfigId,
|
||||
profileName: profile.name,
|
||||
profileKeys: Object.keys(fullProfile).filter(
|
||||
(k) => fullProfile[k as keyof typeof fullProfile] !== undefined,
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
debugLog("[handleModeSwitch] WARNING: savedConfigId exists but profile not found in listApiConfig", {
|
||||
savedConfigId,
|
||||
availableIds: listApiConfig.map((c) => c.id),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If no saved config for this mode, save current config as default.
|
||||
const currentApiConfigName = this.getGlobalState("currentApiConfigName")
|
||||
const currentApiConfigNameAfter = this.getGlobalState("currentApiConfigName")
|
||||
|
||||
if (currentApiConfigName) {
|
||||
const config = listApiConfig.find((c) => c.name === currentApiConfigName)
|
||||
// DEBUG: Log no saved config case
|
||||
debugLog("[handleModeSwitch] no saved config for mode, using current", {
|
||||
newMode,
|
||||
currentApiConfigNameAfter,
|
||||
})
|
||||
|
||||
if (currentApiConfigNameAfter) {
|
||||
const config = listApiConfig.find((c) => c.name === currentApiConfigNameAfter)
|
||||
|
||||
if (config?.id) {
|
||||
debugLog("[handleModeSwitch] saving current config as mode default", {
|
||||
newMode,
|
||||
configId: config.id,
|
||||
configName: config.name,
|
||||
})
|
||||
await this.providerSettingsManager.setModeConfig(newMode, config.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG: Log final state after mode switch
|
||||
const finalState = await this.getState()
|
||||
debugLog("[handleModeSwitch] END - final state", {
|
||||
newMode,
|
||||
finalApiProvider: finalState.apiConfiguration?.apiProvider,
|
||||
finalApiConfigName: finalState.currentApiConfigName,
|
||||
// Check various provider API keys to see if any are set
|
||||
hasAnyApiKey: !!(
|
||||
(finalState.apiConfiguration as any)?.apiKey ||
|
||||
(finalState.apiConfiguration as any)?.openRouterApiKey ||
|
||||
(finalState.apiConfiguration as any)?.requestyApiKey
|
||||
),
|
||||
})
|
||||
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
|
|
@ -1441,8 +1535,27 @@ export class ClineProvider
|
|||
}
|
||||
|
||||
async activateProviderProfile(args: { name: string } | { id: string }) {
|
||||
// DEBUG: Log entry point
|
||||
debugLog("[activateProviderProfile] START", { args })
|
||||
|
||||
const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
|
||||
|
||||
// DEBUG: Log what was returned from activateProfile
|
||||
debugLog("[activateProviderProfile] activateProfile result", {
|
||||
name,
|
||||
id,
|
||||
apiProvider: providerSettings.apiProvider,
|
||||
hasApiKey: !!(
|
||||
(providerSettings as any)?.apiKey ||
|
||||
(providerSettings as any)?.openRouterApiKey ||
|
||||
(providerSettings as any)?.requestyApiKey
|
||||
),
|
||||
// Log all keys that have values (but not the values themselves for security)
|
||||
settingsKeys: Object.keys(providerSettings).filter(
|
||||
(k) => providerSettings[k as keyof typeof providerSettings] !== undefined,
|
||||
),
|
||||
})
|
||||
|
||||
// See `upsertProviderProfile` for a description of what this is doing.
|
||||
await Promise.all([
|
||||
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
|
||||
|
|
@ -1452,6 +1565,9 @@ export class ClineProvider
|
|||
|
||||
const { mode } = await this.getState()
|
||||
|
||||
// DEBUG: Log mode config update
|
||||
debugLog("[activateProviderProfile] setting mode config", { mode, id })
|
||||
|
||||
if (id) {
|
||||
await this.providerSettingsManager.setModeConfig(mode, id)
|
||||
}
|
||||
|
|
@ -1460,6 +1576,18 @@ export class ClineProvider
|
|||
|
||||
await this.postStateToWebview()
|
||||
|
||||
// DEBUG: Log final state
|
||||
const finalState = this.contextProxy.getProviderSettings()
|
||||
debugLog("[activateProviderProfile] END - final provider settings", {
|
||||
name,
|
||||
apiProvider: finalState.apiProvider,
|
||||
hasApiKey: !!(
|
||||
(finalState as any)?.apiKey ||
|
||||
(finalState as any)?.openRouterApiKey ||
|
||||
(finalState as any)?.requestyApiKey
|
||||
),
|
||||
})
|
||||
|
||||
if (providerSettings.apiProvider) {
|
||||
this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,13 @@ import {
|
|||
type TelemetrySetting,
|
||||
type UserSettingsConfig,
|
||||
type ModelRecord,
|
||||
type WebviewMessage,
|
||||
type EditQueuedMessagePayload,
|
||||
TelemetryEventName,
|
||||
RooCodeSettings,
|
||||
ExperimentId,
|
||||
checkoutDiffPayloadSchema,
|
||||
checkoutRestorePayloadSchema,
|
||||
} from "@roo-code/types"
|
||||
import { customToolRegistry } from "@roo-code/core"
|
||||
import { CloudService } from "@roo-code/cloud"
|
||||
|
|
@ -33,12 +37,6 @@ import { Package } from "../../shared/package"
|
|||
import { type RouterName, toRouterName } from "../../shared/api"
|
||||
import { MessageEnhancer } from "./messageEnhancer"
|
||||
|
||||
import {
|
||||
type WebviewMessage,
|
||||
type EditQueuedMessagePayload,
|
||||
checkoutDiffPayloadSchema,
|
||||
checkoutRestorePayloadSchema,
|
||||
} from "../../shared/WebviewMessage"
|
||||
import { checkExistKey } from "../../shared/checkExistApiConfig"
|
||||
import { experimentDefault } from "../../shared/experiments"
|
||||
import { Terminal } from "../../integrations/terminal/Terminal"
|
||||
|
|
|
|||
|
|
@ -1,315 +1,3 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
type RooCodeSettings,
|
||||
type ProviderSettings,
|
||||
type PromptComponent,
|
||||
type ModeConfig,
|
||||
type InstallMarketplaceItemOptions,
|
||||
type MarketplaceItem,
|
||||
type ShareVisibility,
|
||||
type QueuedMessage,
|
||||
marketplaceItemSchema,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { Mode } from "./modes"
|
||||
export type { WebviewMessage, WebViewMessagePayload } from "@roo-code/types"
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" | "objectResponse"
|
||||
|
||||
export type PromptMode = Mode | "enhance"
|
||||
|
||||
export type AudioType = "notification" | "celebration" | "progress_loop"
|
||||
|
||||
export interface UpdateTodoListPayload {
|
||||
todos: any[]
|
||||
}
|
||||
|
||||
export type EditQueuedMessagePayload = Pick<QueuedMessage, "id" | "text" | "images">
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "updateTodoList"
|
||||
| "deleteMultipleTasksWithIds"
|
||||
| "currentApiConfigName"
|
||||
| "saveApiConfiguration"
|
||||
| "upsertApiConfiguration"
|
||||
| "deleteApiConfiguration"
|
||||
| "loadApiConfiguration"
|
||||
| "loadApiConfigurationById"
|
||||
| "renameApiConfiguration"
|
||||
| "getListApiConfiguration"
|
||||
| "customInstructions"
|
||||
| "webviewDidLaunch"
|
||||
| "newTask"
|
||||
| "askResponse"
|
||||
| "terminalOperation"
|
||||
| "clearTask"
|
||||
| "didShowAnnouncement"
|
||||
| "selectImages"
|
||||
| "exportCurrentTask"
|
||||
| "shareCurrentTask"
|
||||
| "showTaskWithId"
|
||||
| "deleteTaskWithId"
|
||||
| "exportTaskWithId"
|
||||
| "importSettings"
|
||||
| "exportSettings"
|
||||
| "resetState"
|
||||
| "flushRouterModels"
|
||||
| "requestRouterModels"
|
||||
| "requestOpenAiModels"
|
||||
| "requestOllamaModels"
|
||||
| "requestLmStudioModels"
|
||||
| "requestRooModels"
|
||||
| "requestRooCreditBalance"
|
||||
| "requestVsCodeLmModels"
|
||||
| "requestHuggingFaceModels"
|
||||
| "openImage"
|
||||
| "saveImage"
|
||||
| "openFile"
|
||||
| "openMention"
|
||||
| "cancelTask"
|
||||
| "cancelAutoApproval"
|
||||
| "updateVSCodeSetting"
|
||||
| "getVSCodeSetting"
|
||||
| "vsCodeSetting"
|
||||
| "updateCondensingPrompt"
|
||||
| "playSound"
|
||||
| "playTts"
|
||||
| "stopTts"
|
||||
| "ttsEnabled"
|
||||
| "ttsSpeed"
|
||||
| "openKeyboardShortcuts"
|
||||
| "openMcpSettings"
|
||||
| "openProjectMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "refreshAllMcpServers"
|
||||
| "toggleToolAlwaysAllow"
|
||||
| "toggleToolEnabledForPrompt"
|
||||
| "toggleMcpServer"
|
||||
| "updateMcpTimeout"
|
||||
| "enhancePrompt"
|
||||
| "enhancedPrompt"
|
||||
| "draggedImages"
|
||||
| "deleteMessage"
|
||||
| "deleteMessageConfirm"
|
||||
| "submitEditedMessage"
|
||||
| "editMessageConfirm"
|
||||
| "enableMcpServerCreation"
|
||||
| "remoteControlEnabled"
|
||||
| "taskSyncEnabled"
|
||||
| "searchCommits"
|
||||
| "setApiConfigPassword"
|
||||
| "mode"
|
||||
| "updatePrompt"
|
||||
| "getSystemPrompt"
|
||||
| "copySystemPrompt"
|
||||
| "systemPrompt"
|
||||
| "enhancementApiConfigId"
|
||||
| "autoApprovalEnabled"
|
||||
| "updateCustomMode"
|
||||
| "deleteCustomMode"
|
||||
| "setopenAiCustomModelInfo"
|
||||
| "openCustomModesSettings"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "deleteMcpServer"
|
||||
| "codebaseIndexEnabled"
|
||||
| "telemetrySetting"
|
||||
| "testBrowserConnection"
|
||||
| "browserConnectionResult"
|
||||
| "searchFiles"
|
||||
| "toggleApiConfigPin"
|
||||
| "hasOpenedModeSelector"
|
||||
| "clearCloudAuthSkipModel"
|
||||
| "cloudButtonClicked"
|
||||
| "rooCloudSignIn"
|
||||
| "cloudLandingPageSignIn"
|
||||
| "rooCloudSignOut"
|
||||
| "rooCloudManualUrl"
|
||||
| "claudeCodeSignIn"
|
||||
| "claudeCodeSignOut"
|
||||
| "switchOrganization"
|
||||
| "condenseTaskContextRequest"
|
||||
| "requestIndexingStatus"
|
||||
| "startIndexing"
|
||||
| "clearIndexData"
|
||||
| "indexingStatusUpdate"
|
||||
| "indexCleared"
|
||||
| "focusPanelRequest"
|
||||
| "openExternal"
|
||||
| "filterMarketplaceItems"
|
||||
| "marketplaceButtonClicked"
|
||||
| "installMarketplaceItem"
|
||||
| "installMarketplaceItemWithParameters"
|
||||
| "cancelMarketplaceInstall"
|
||||
| "removeInstalledMarketplaceItem"
|
||||
| "marketplaceInstallResult"
|
||||
| "fetchMarketplaceData"
|
||||
| "switchTab"
|
||||
| "shareTaskSuccess"
|
||||
| "exportMode"
|
||||
| "exportModeResult"
|
||||
| "importMode"
|
||||
| "importModeResult"
|
||||
| "checkRulesDirectory"
|
||||
| "checkRulesDirectoryResult"
|
||||
| "saveCodeIndexSettingsAtomic"
|
||||
| "requestCodeIndexSecretStatus"
|
||||
| "requestCommands"
|
||||
| "openCommandFile"
|
||||
| "deleteCommand"
|
||||
| "createCommand"
|
||||
| "insertTextIntoTextarea"
|
||||
| "showMdmAuthRequiredNotification"
|
||||
| "imageGenerationSettings"
|
||||
| "queueMessage"
|
||||
| "removeQueuedMessage"
|
||||
| "editQueuedMessage"
|
||||
| "dismissUpsell"
|
||||
| "getDismissedUpsells"
|
||||
| "updateSettings"
|
||||
| "allowedCommands"
|
||||
| "deniedCommands"
|
||||
| "killBrowserSession"
|
||||
| "openBrowserSessionPanel"
|
||||
| "showBrowserSessionPanelAtStep"
|
||||
| "refreshBrowserSessionPanel"
|
||||
| "browserPanelDidLaunch"
|
||||
| "openDebugApiHistory"
|
||||
| "openDebugUiHistory"
|
||||
| "downloadErrorDiagnostics"
|
||||
| "requestClaudeCodeRateLimits"
|
||||
| "refreshCustomTools"
|
||||
| "requestModes"
|
||||
| "switchMode"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
disabled?: boolean
|
||||
context?: string
|
||||
dataUri?: string
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ProviderSettings
|
||||
images?: string[]
|
||||
bool?: boolean
|
||||
value?: number
|
||||
stepIndex?: number
|
||||
isLaunchAction?: boolean
|
||||
forceShow?: boolean
|
||||
commands?: string[]
|
||||
audioType?: AudioType
|
||||
serverName?: string
|
||||
toolName?: string
|
||||
alwaysAllow?: boolean
|
||||
isEnabled?: boolean
|
||||
mode?: Mode
|
||||
promptMode?: PromptMode
|
||||
customPrompt?: PromptComponent
|
||||
dataUrls?: string[]
|
||||
values?: Record<string, any>
|
||||
query?: string
|
||||
setting?: string
|
||||
slug?: string
|
||||
modeConfig?: ModeConfig
|
||||
timeout?: number
|
||||
payload?: WebViewMessagePayload
|
||||
source?: "global" | "project"
|
||||
requestId?: string
|
||||
ids?: string[]
|
||||
hasSystemPromptOverride?: boolean
|
||||
terminalOperation?: "continue" | "abort"
|
||||
messageTs?: number
|
||||
restoreCheckpoint?: boolean
|
||||
historyPreviewCollapsed?: boolean
|
||||
filters?: { type?: string; search?: string; tags?: string[] }
|
||||
settings?: any
|
||||
url?: string // For openExternal
|
||||
mpItem?: MarketplaceItem
|
||||
mpInstallOptions?: InstallMarketplaceItemOptions
|
||||
config?: Record<string, any> // Add config to the payload
|
||||
visibility?: ShareVisibility // For share visibility
|
||||
hasContent?: boolean // For checkRulesDirectoryResult
|
||||
checkOnly?: boolean // For deleteCustomMode check
|
||||
upsellId?: string // For dismissUpsell
|
||||
list?: string[] // For dismissedUpsells response
|
||||
organizationId?: string | null // For organization switching
|
||||
useProviderSignup?: boolean // For rooCloudSignIn to use provider signup flow
|
||||
codeIndexSettings?: {
|
||||
// Global state settings
|
||||
codebaseIndexEnabled: boolean
|
||||
codebaseIndexQdrantUrl: string
|
||||
codebaseIndexEmbedderProvider:
|
||||
| "openai"
|
||||
| "ollama"
|
||||
| "openai-compatible"
|
||||
| "gemini"
|
||||
| "mistral"
|
||||
| "vercel-ai-gateway"
|
||||
| "bedrock"
|
||||
| "openrouter"
|
||||
codebaseIndexEmbedderBaseUrl?: string
|
||||
codebaseIndexEmbedderModelId: string
|
||||
codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers
|
||||
codebaseIndexOpenAiCompatibleBaseUrl?: string
|
||||
codebaseIndexBedrockRegion?: string
|
||||
codebaseIndexBedrockProfile?: string
|
||||
codebaseIndexSearchMaxResults?: number
|
||||
codebaseIndexSearchMinScore?: number
|
||||
codebaseIndexOpenRouterSpecificProvider?: string // OpenRouter provider routing
|
||||
|
||||
// Secret settings
|
||||
codeIndexOpenAiKey?: string
|
||||
codeIndexQdrantApiKey?: string
|
||||
codebaseIndexOpenAiCompatibleApiKey?: string
|
||||
codebaseIndexGeminiApiKey?: string
|
||||
codebaseIndexMistralApiKey?: string
|
||||
codebaseIndexVercelAiGatewayApiKey?: string
|
||||
codebaseIndexOpenRouterApiKey?: string
|
||||
}
|
||||
updatedSettings?: RooCodeSettings
|
||||
}
|
||||
|
||||
export const checkoutDiffPayloadSchema = z.object({
|
||||
ts: z.number().optional(),
|
||||
previousCommitHash: z.string().optional(),
|
||||
commitHash: z.string(),
|
||||
mode: z.enum(["full", "checkpoint", "from-init", "to-current"]),
|
||||
})
|
||||
|
||||
export type CheckpointDiffPayload = z.infer<typeof checkoutDiffPayloadSchema>
|
||||
|
||||
export const checkoutRestorePayloadSchema = z.object({
|
||||
ts: z.number(),
|
||||
commitHash: z.string(),
|
||||
mode: z.enum(["preview", "restore"]),
|
||||
})
|
||||
|
||||
export type CheckpointRestorePayload = z.infer<typeof checkoutRestorePayloadSchema>
|
||||
|
||||
export interface IndexingStatusPayload {
|
||||
state: "Standby" | "Indexing" | "Indexed" | "Error"
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface IndexClearedPayload {
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const installMarketplaceItemWithParametersPayloadSchema = z.object({
|
||||
item: marketplaceItemSchema,
|
||||
parameters: z.record(z.string(), z.any()),
|
||||
})
|
||||
|
||||
export type InstallMarketplaceItemWithParametersPayload = z.infer<
|
||||
typeof installMarketplaceItemWithParametersPayloadSchema
|
||||
>
|
||||
|
||||
export type WebViewMessagePayload =
|
||||
| CheckpointDiffPayload
|
||||
| CheckpointRestorePayload
|
||||
| IndexingStatusPayload
|
||||
| IndexClearedPayload
|
||||
| InstallMarketplaceItemWithParametersPayload
|
||||
| UpdateTodoListPayload
|
||||
| EditQueuedMessagePayload
|
||||
|
|
|
|||
19
src/utils/debug-log.ts
Normal file
19
src/utils/debug-log.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* File-based debug logging utility
|
||||
*
|
||||
* Re-exports from @roo-code/core/debug-log for consistency across the codebase.
|
||||
* This writes logs to ~/.roo/cli-debug.log, avoiding stdout/stderr
|
||||
* which would break TUI applications.
|
||||
*
|
||||
* Usage:
|
||||
* import { debugLog, DebugLogger } from "../utils/debug-log"
|
||||
*
|
||||
* // Simple logging
|
||||
* debugLog("handleModeSwitch", { mode: newMode, configId })
|
||||
*
|
||||
* // Or create a named logger for a component
|
||||
* const log = new DebugLogger("ClineProvider")
|
||||
* log.info("handleModeSwitch", { mode: newMode })
|
||||
*/
|
||||
|
||||
export { debugLog, DebugLogger, providerDebugLog } from "@roo-code/core/debug-log"
|
||||
|
|
@ -11,7 +11,7 @@ import { Trans } from "react-i18next"
|
|||
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
|
||||
import { appendImages } from "@src/utils/imageUtils"
|
||||
|
||||
import type { ClineAsk, ClineMessage, ExtensionMessage } from "@roo-code/types"
|
||||
import type { ClineAsk, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types"
|
||||
|
||||
import { ClineSayTool } from "@roo/ExtensionMessage"
|
||||
import { findLast } from "@roo/array"
|
||||
|
|
@ -19,7 +19,6 @@ import { SuggestionItem } from "@roo-code/types"
|
|||
import { combineApiRequests } from "@roo/combineApiRequests"
|
||||
import { combineCommandSequences } from "@roo/combineCommandSequences"
|
||||
import { getApiMetrics } from "@roo/getApiMetrics"
|
||||
import { AudioType } from "@roo/WebviewMessage"
|
||||
import { getAllModes } from "@roo/modes"
|
||||
import { ProfileValidator } from "@roo/ProfileValidator"
|
||||
import { getLatestTodo } from "@roo/todo"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue