feat: add IPC query handlers for commands, modes, and models (#11279)

Add GetCommands, GetModes, and GetModels to the IPC protocol so external
clients can fetch slash commands, available modes, and Roo provider models
without going through the internal webview message channel.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Estreich 2026-02-06 16:29:49 -08:00 committed by GitHub
parent 6d2459c7e8
commit 9b39d2242a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 106 additions and 3 deletions

View file

@ -1,6 +1,7 @@
import { z } from "zod"
import { clineMessageSchema, queuedMessageSchema, tokenUsageSchema } from "./message.js"
import { modelInfoSchema } from "./model.js"
import { toolNamesSchema, toolUsageSchema } from "./tool.js"
/**
@ -45,6 +46,11 @@ export enum RooCodeEventName {
ModeChanged = "modeChanged",
ProviderProfileChanged = "providerProfileChanged",
// Query Responses
CommandsResponse = "commandsResponse",
ModesResponse = "modesResponse",
ModelsResponse = "modelsResponse",
// Evals
EvalPass = "evalPass",
EvalFail = "evalFail",
@ -108,6 +114,20 @@ export const rooCodeEventsSchema = z.object({
[RooCodeEventName.ModeChanged]: z.tuple([z.string()]),
[RooCodeEventName.ProviderProfileChanged]: z.tuple([z.object({ name: z.string(), provider: z.string() })]),
[RooCodeEventName.CommandsResponse]: z.tuple([
z.array(
z.object({
name: z.string(),
source: z.enum(["global", "project", "built-in"]),
filePath: z.string().optional(),
description: z.string().optional(),
argumentHint: z.string().optional(),
}),
),
]),
[RooCodeEventName.ModesResponse]: z.tuple([z.array(z.object({ slug: z.string(), name: z.string() }))]),
[RooCodeEventName.ModelsResponse]: z.tuple([z.record(z.string(), modelInfoSchema)]),
})
export type RooCodeEvents = z.infer<typeof rooCodeEventsSchema>
@ -237,6 +257,23 @@ export const taskEventSchema = z.discriminatedUnion("eventName", [
taskId: z.number().optional(),
}),
// Query Responses
z.object({
eventName: z.literal(RooCodeEventName.CommandsResponse),
payload: rooCodeEventsSchema.shape[RooCodeEventName.CommandsResponse],
taskId: z.number().optional(),
}),
z.object({
eventName: z.literal(RooCodeEventName.ModesResponse),
payload: rooCodeEventsSchema.shape[RooCodeEventName.ModesResponse],
taskId: z.number().optional(),
}),
z.object({
eventName: z.literal(RooCodeEventName.ModelsResponse),
payload: rooCodeEventsSchema.shape[RooCodeEventName.ModelsResponse],
taskId: z.number().optional(),
}),
// Evals
z.object({
eventName: z.literal(RooCodeEventName.EvalPass),

View file

@ -46,6 +46,9 @@ export enum TaskCommandName {
CloseTask = "CloseTask",
ResumeTask = "ResumeTask",
SendMessage = "SendMessage",
GetCommands = "GetCommands",
GetModes = "GetModes",
GetModels = "GetModels",
}
/**
@ -79,6 +82,15 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [
images: z.array(z.string()).optional(),
}),
}),
z.object({
commandName: z.literal(TaskCommandName.GetCommands),
}),
z.object({
commandName: z.literal(TaskCommandName.GetModes),
}),
z.object({
commandName: z.literal(TaskCommandName.GetModels),
}),
])
export type TaskCommand = z.infer<typeof taskCommandSchema>

View file

@ -20,10 +20,13 @@ import {
IpcMessageType,
} from "@roo-code/types"
import { IpcServer } from "@roo-code/ipc"
import { CloudService } from "@roo-code/cloud"
import { Package } from "../shared/package"
import { ClineProvider } from "../core/webview/ClineProvider"
import { openClineInNewTab } from "../activate/registerCommands"
import { getCommands } from "../services/command/commands"
import { getModels } from "../api/providers/fetchers/modelCache"
export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
private readonly outputChannel: vscode.OutputChannel
@ -64,7 +67,15 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
ipc.listen()
this.log(`[API] ipc server started: socketPath=${socketPath}, pid=${process.pid}, ppid=${process.ppid}`)
ipc.on(IpcMessageType.TaskCommand, async (_clientId, command) => {
ipc.on(IpcMessageType.TaskCommand, async (clientId, command) => {
const sendResponse = (eventName: RooCodeEventName, payload: unknown[]) => {
ipc.send(clientId, {
type: IpcMessageType.TaskEvent,
origin: IpcOrigin.Server,
data: { eventName, payload } as TaskEvent,
})
}
switch (command.commandName) {
case TaskCommandName.StartNewTask:
this.log(
@ -88,13 +99,56 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.log(`[API] ResumeTask failed for taskId ${command.data}: ${errorMessage}`)
// Don't rethrow - we want to prevent IPC server crashes
// The error is logged for debugging purposes
// Don't rethrow - we want to prevent IPC server crashes.
// The error is logged for debugging purposes.
}
break
case TaskCommandName.SendMessage:
this.log(`[API] SendMessage -> ${command.data.text}`)
await this.sendMessage(command.data.text, command.data.images)
break
case TaskCommandName.GetCommands:
try {
const commands = await getCommands(this.sidebarProvider.cwd)
sendResponse(RooCodeEventName.CommandsResponse, [
commands.map((cmd) => ({
name: cmd.name,
source: cmd.source,
filePath: cmd.filePath,
description: cmd.description,
argumentHint: cmd.argumentHint,
})),
])
} catch (error) {
sendResponse(RooCodeEventName.CommandsResponse, [[]])
}
break
case TaskCommandName.GetModes:
try {
const modes = await this.sidebarProvider.getModes()
sendResponse(RooCodeEventName.ModesResponse, [modes])
} catch (error) {
sendResponse(RooCodeEventName.ModesResponse, [[]])
}
break
case TaskCommandName.GetModels:
try {
const models = await getModels({
provider: "roo" as const,
baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
apiKey: CloudService.hasInstance()
? CloudService.instance.authService?.getSessionToken()
: undefined,
})
sendResponse(RooCodeEventName.ModelsResponse, [models])
} catch (error) {
sendResponse(RooCodeEventName.ModelsResponse, [{}])
}
break
}
})