feat: add Telegram Bridge MCP Server for remote control of Roo Code

This PR implements the foundation for Issue #11146 - Native Telegram Bot
Integration for Remote Control.

Changes:
- Add ApproveAsk and DenyAsk commands to IPC types
- Add IPC command handlers in extension for approve/deny
- Create @roo-code/telegram-bridge MCP server package
- Add Telegram bot integration with approve/deny buttons
- Add tests for new IPC commands

The Telegram Bridge MCP Server allows users to:
- Receive Roo Code messages on Telegram in real-time
- Approve/reject tool operations via inline buttons
- Send new instructions from Telegram
- Monitor task status remotely
This commit is contained in:
Roo Code 2026-02-02 01:07:59 +00:00
parent ede1d29299
commit c7d31ae5d0
10 changed files with 1041 additions and 1 deletions

View file

@ -108,6 +108,20 @@ export class IpcClient extends EventEmitter<IpcClientEvents> {
})
}
public approveAsk(text?: string, images?: string[]) {
this.sendCommand({
commandName: TaskCommandName.ApproveAsk,
data: { text, images },
})
}
public denyAsk(text?: string, images?: string[]) {
this.sendCommand({
commandName: TaskCommandName.DenyAsk,
data: { text, images },
})
}
public sendMessage(message: IpcMessage) {
ipc.of[this._id]?.emit("message", message)
}

View file

@ -0,0 +1,151 @@
# @roo-code/telegram-bridge
Telegram bridge for remote control of Roo Code. This MCP server enables you to interact with Roo Code via Telegram, allowing you to:
- **Receive messages**: Get real-time updates from Roo Code on your Telegram
- **Approve/Reject operations**: Use inline buttons to approve or deny tool operations
- **Send instructions**: Forward messages to Roo Code as new instructions
- **Monitor task status**: Track task progress remotely
## Prerequisites
1. **Create a Telegram Bot**:
- Open Telegram and search for [@BotFather](https://t.me/BotFather)
- Send `/newbot` and follow the prompts
- Save the bot token you receive
2. **Get your Chat ID**:
- Start a conversation with your new bot
- Send any message to the bot
- Visit `https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates`
- Find your `chat.id` in the response
## Installation
### As an MCP Server
Add the following to your MCP settings file (`~/.roo-code/settings/mcp_settings.json`):
```json
{
"mcpServers": {
"telegram-bridge": {
"command": "node",
"args": ["/path/to/roo-code/packages/telegram-bridge/build/index.js"],
"env": {
"TELEGRAM_BOT_TOKEN": "your-bot-token",
"TELEGRAM_CHAT_ID": "your-chat-id"
}
}
}
}
```
### Build from Source
```bash
cd packages/telegram-bridge
npm install
npm run build
```
## Usage
Once the MCP server is running, you can use the following tools:
### start_bridge
Start the Telegram bridge with your credentials.
```json
{
"botToken": "your-bot-token",
"chatId": "your-chat-id",
"socketPath": "/path/to/roo-code.sock",
"enableTts": false
}
```
### stop_bridge
Stop the Telegram bridge.
### bridge_status
Get the current status of the bridge.
### send_telegram_message
Send a custom message to Telegram.
```json
{
"message": "Hello from Roo Code!"
}
```
## Telegram Commands
Once connected, you can use these commands in Telegram:
- `/start` or `/help` - Show help message
- `/status` - Show connection status
- `/cancel` - Cancel the current task
Any other message will be forwarded to Roo Code as a new instruction.
## How It Works
1. The bridge connects to Roo Code via IPC (Inter-Process Communication)
2. It listens for task events and messages from Roo Code
3. Messages are formatted and sent to your Telegram chat
4. When approval is needed, inline buttons are shown
5. Button clicks and messages are sent back to Roo Code
## Architecture
```
┌─────────────┐ IPC ┌──────────────────┐ Telegram API ┌──────────┐
│ Roo Code │◄────────────►│ Telegram Bridge │◄──────────────────►│ Telegram │
│ Extension │ │ (MCP Server) │ │ App │
└─────────────┘ └──────────────────┘ └──────────┘
```
## Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `TELEGRAM_BOT_TOKEN` | Your Telegram bot token | Yes |
| `TELEGRAM_CHAT_ID` | Your Telegram chat ID | Yes |
| `ROO_CODE_SOCKET_PATH` | Custom IPC socket path | No |
## Security Considerations
- Keep your bot token secret - never commit it to version control
- The bridge only responds to messages from the configured chat ID
- Consider using a private chat or group for better security
- All communication between the bridge and Roo Code happens locally via IPC
## Troubleshooting
### Bridge not connecting
1. Ensure Roo Code is running with IPC enabled
2. Check the socket path is correct
3. Verify your bot token is valid
### Not receiving messages
1. Ensure you've started a conversation with the bot
2. Check the chat ID is correct
3. Look for errors in the MCP server logs
### Buttons not working
1. Make sure the bridge is still connected
2. Check for any error messages in Telegram
3. Try stopping and restarting the bridge
## License
Apache-2.0

View file

@ -0,0 +1,38 @@
{
"name": "@roo-code/telegram-bridge",
"version": "0.1.0",
"description": "Telegram bridge for remote control of Roo Code via IPC",
"type": "module",
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"build": "tsc",
"start": "node build/index.js",
"dev": "tsx src/index.ts",
"clean": "rm -rf build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@roo-code/ipc": "workspace:*",
"@roo-code/types": "workspace:*",
"node-telegram-bot-api": "^0.66.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/node-telegram-bot-api": "^0.64.7",
"tsx": "^4.0.0",
"typescript": "~5.8.0-dev.20250121"
},
"peerDependencies": {
"typescript": "~5.8.0-dev.20250121"
},
"keywords": [
"roo-code",
"telegram",
"mcp",
"remote-control"
],
"license": "Apache-2.0"
}

View file

@ -0,0 +1,190 @@
#!/usr/bin/env node
/**
* Telegram Bridge MCP Server for Roo Code
*
* This server provides remote control of Roo Code via Telegram.
* It connects to Roo Code via IPC and forwards messages to Telegram,
* allowing users to approve/reject operations and send new instructions.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { z } from "zod"
import { TelegramBridge } from "./telegram-bridge.js"
// Environment variables
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID
const ROO_CODE_SOCKET_PATH = process.env.ROO_CODE_SOCKET_PATH
// Create the MCP server
const server = new McpServer({
name: "telegram-bridge",
version: "0.1.0",
})
// Telegram bridge instance (created when start_bridge tool is called)
let bridge: TelegramBridge | null = null
// Tool: Start the Telegram bridge
server.tool(
"start_bridge",
{
botToken: z.string().describe("Telegram bot token from @BotFather"),
chatId: z.string().describe("Telegram chat ID to send messages to"),
socketPath: z.string().optional().describe("Roo Code IPC socket path (optional, uses default if not provided)"),
enableTts: z.boolean().optional().describe("Enable text-to-speech for messages (optional)"),
},
async ({ botToken, chatId, socketPath, enableTts }) => {
try {
if (bridge) {
await bridge.stop()
}
bridge = new TelegramBridge({
botToken: botToken || TELEGRAM_BOT_TOKEN || "",
chatId: chatId || TELEGRAM_CHAT_ID || "",
socketPath: socketPath || ROO_CODE_SOCKET_PATH,
enableTts: enableTts ?? false,
})
await bridge.start()
return {
content: [
{
type: "text",
text: "Telegram bridge started successfully. You can now receive Roo Code messages on Telegram.",
},
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to start Telegram bridge: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
}
}
},
)
// Tool: Stop the Telegram bridge
server.tool("stop_bridge", {}, async () => {
try {
if (bridge) {
await bridge.stop()
bridge = null
}
return {
content: [
{
type: "text",
text: "Telegram bridge stopped.",
},
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to stop Telegram bridge: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
}
}
})
// Tool: Get bridge status
server.tool("bridge_status", {}, async () => {
if (!bridge) {
return {
content: [
{
type: "text",
text: JSON.stringify({ running: false }, null, 2),
},
],
}
}
const status = bridge.getStatus()
return {
content: [
{
type: "text",
text: JSON.stringify(status, null, 2),
},
],
}
})
// Tool: Send a message to Telegram
server.tool(
"send_telegram_message",
{
message: z.string().describe("Message to send to Telegram"),
},
async ({ message }) => {
if (!bridge) {
return {
content: [
{
type: "text",
text: "Telegram bridge is not running. Use start_bridge first.",
},
],
isError: true,
}
}
try {
await bridge.sendMessage(message)
return {
content: [
{
type: "text",
text: "Message sent to Telegram.",
},
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to send message: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
}
}
},
)
// Start the MCP server
const transport = new StdioServerTransport()
await server.connect(transport)
console.error("Telegram Bridge MCP server running on stdio")
// Handle process signals for graceful shutdown
process.on("SIGINT", async () => {
if (bridge) {
await bridge.stop()
}
process.exit(0)
})
process.on("SIGTERM", async () => {
if (bridge) {
await bridge.stop()
}
process.exit(0)
})

View file

@ -0,0 +1,144 @@
/**
* Message formatting utilities for Telegram
*/
import type { ClineMessage } from "@roo-code/types"
// Telegram message length limit
const MAX_MESSAGE_LENGTH = 4096
// Map of ask types to human-readable descriptions
const ASK_TYPE_LABELS: Record<string, string> = {
followup: "❓ Question",
command: "⚡ Command Execution",
command_output: "📄 Command Output",
completion_result: "✅ Task Complete",
tool: "🔧 Tool Operation",
api_req_failed: "⚠️ API Request Failed",
resume_task: "▶️ Resume Task",
resume_completed_task: "🔄 Resume Completed Task",
mistake_limit_reached: "🚫 Mistake Limit Reached",
browser_action_launch: "🌐 Browser Action",
use_mcp_server: "🔌 MCP Server",
auto_approval_max_req_reached: "⏸️ Auto-approval Limit",
}
// Map of say types to human-readable descriptions
const SAY_TYPE_LABELS: Record<string, string> = {
user_feedback: "💬 User Feedback",
user_feedback_diff: "📝 Diff Feedback",
api_req_started: "🔄 Processing...",
api_req_finished: "✅ Processing Complete",
api_req_retried: "🔄 Retrying...",
api_req_failed: "❌ Request Failed",
text: "💬",
reasoning: "🧠 Reasoning",
command: "⚡ Command",
command_output: "📄 Output",
completion_result: "✅ Complete",
tool: "🔧 Tool",
shell_integration_warning: "⚠️ Shell Warning",
browser_action: "🌐 Browser",
browser_action_result: "🌐 Browser Result",
mcp_server_request_started: "🔌 MCP Request",
mcp_server_response: "🔌 MCP Response",
condense_context: "📦 Context Condensed",
checkpoint_saved: "💾 Checkpoint Saved",
roo_message: "🤖 Roo",
error: "❌ Error",
diff: "📝 Diff",
clineignore_error: "⚠️ Ignore Error",
sliding_window_truncation: "✂️ Truncated",
}
/**
* Format a ClineMessage for display in Telegram
*/
export function formatMessageForTelegram(message: ClineMessage): string {
const parts: string[] = []
// Add type indicator
if (message.type === "ask" && message.ask) {
const label = ASK_TYPE_LABELS[message.ask] || `❓ ${message.ask}`
parts.push(`<b>${label}</b>`)
// Add approval request indicator
if (isApprovalRequired(message.ask)) {
parts.push("\n<i>Approval required</i>")
}
} else if (message.type === "say" && message.say) {
const label = SAY_TYPE_LABELS[message.say] || message.say
if (label !== "💬") {
parts.push(`<b>${label}</b>`)
}
}
// Add message text
if (message.text) {
const formattedText = escapeHtml(message.text)
parts.push(formattedText)
}
// Add reasoning if present
if (message.reasoning) {
parts.push(`\n<i>Reasoning: ${escapeHtml(truncateMessage(message.reasoning, 500))}</i>`)
}
return truncateMessage(parts.join("\n"), MAX_MESSAGE_LENGTH)
}
/**
* Check if an ask type requires user approval
*/
function isApprovalRequired(askType: string): boolean {
const approvalAsks = ["command", "tool", "browser_action_launch", "use_mcp_server", "followup"]
return approvalAsks.includes(askType)
}
/**
* Escape HTML special characters for Telegram
*/
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
}
/**
* Truncate a message to a maximum length, adding ellipsis if needed
*/
export function truncateMessage(text: string, maxLength: number): string {
if (text.length <= maxLength) {
return text
}
return text.substring(0, maxLength - 3) + "..."
}
/**
* Format a code block for Telegram
*/
export function formatCodeBlock(code: string, language?: string): string {
const escapedCode = escapeHtml(code)
if (language) {
return `<pre><code class="language-${language}">${escapedCode}</code></pre>`
}
return `<pre>${escapedCode}</pre>`
}
/**
* Format a diff for Telegram display
*/
export function formatDiff(diff: string): string {
const lines = diff.split("\n")
const formattedLines = lines.map((line) => {
if (line.startsWith("+")) {
return `<code>+ ${escapeHtml(line.substring(1))}</code>`
} else if (line.startsWith("-")) {
return `<code>- ${escapeHtml(line.substring(1))}</code>`
}
return escapeHtml(line)
})
return formattedLines.join("\n")
}

View file

@ -0,0 +1,384 @@
/**
* TelegramBridge - Handles communication between Roo Code and Telegram
*
* This class connects to Roo Code via IPC and manages a Telegram bot
* to forward messages and handle user interactions.
*/
import TelegramBot from "node-telegram-bot-api"
import { IpcClient } from "@roo-code/ipc"
import { IpcMessageType, RooCodeEventName, TaskCommandName } from "@roo-code/types"
import type { ClineMessage, TaskEvent } from "@roo-code/types"
import { formatMessageForTelegram, truncateMessage } from "./message-formatter.js"
export interface TelegramBridgeOptions {
botToken: string
chatId: string
socketPath?: string
enableTts?: boolean
}
export interface BridgeStatus {
running: boolean
connected: boolean
botUsername?: string
chatId: string
messagesSent: number
messagesReceived: number
lastError?: string
}
// Callback data format: action:taskId
const CALLBACK_APPROVE = "approve"
const CALLBACK_DENY = "deny"
export class TelegramBridge {
private readonly options: TelegramBridgeOptions
private bot: TelegramBot | null = null
private ipcClient: IpcClient | null = null
private running = false
private botUsername?: string
private messagesSent = 0
private messagesReceived = 0
private lastError?: string
private pendingApprovals: Map<string, { messageId: number; taskId: string }> = new Map()
constructor(options: TelegramBridgeOptions) {
if (!options.botToken) {
throw new Error("Telegram bot token is required")
}
if (!options.chatId) {
throw new Error("Telegram chat ID is required")
}
this.options = options
}
async start(): Promise<void> {
if (this.running) {
return
}
try {
// Initialize Telegram bot
this.bot = new TelegramBot(this.options.botToken, { polling: true })
// Get bot info
const me = await this.bot.getMe()
this.botUsername = me.username
// Set up bot event handlers
this.setupBotHandlers()
// Connect to Roo Code IPC
const socketPath = this.options.socketPath || this.getDefaultSocketPath()
this.ipcClient = new IpcClient(socketPath, console.error)
// Set up IPC event handlers
this.setupIpcHandlers()
this.running = true
// Send startup message
await this.sendMessage("🤖 Roo Code Telegram Bridge connected! You can now control Roo Code remotely.")
} catch (error) {
this.lastError = error instanceof Error ? error.message : String(error)
throw error
}
}
async stop(): Promise<void> {
if (!this.running) {
return
}
try {
// Send shutdown message
await this.sendMessage("👋 Roo Code Telegram Bridge disconnected.")
} catch {
// Ignore errors when sending shutdown message
}
if (this.bot) {
this.bot.stopPolling()
this.bot = null
}
if (this.ipcClient) {
this.ipcClient.disconnect()
this.ipcClient = null
}
this.running = false
this.pendingApprovals.clear()
}
getStatus(): BridgeStatus {
return {
running: this.running,
connected: this.ipcClient?.isConnected ?? false,
botUsername: this.botUsername,
chatId: this.options.chatId,
messagesSent: this.messagesSent,
messagesReceived: this.messagesReceived,
lastError: this.lastError,
}
}
async sendMessage(text: string, options?: TelegramBot.SendMessageOptions): Promise<TelegramBot.Message | null> {
if (!this.bot || !this.running) {
return null
}
try {
const message = await this.bot.sendMessage(this.options.chatId, text, {
parse_mode: "HTML",
...options,
})
this.messagesSent++
return message
} catch (error) {
this.lastError = error instanceof Error ? error.message : String(error)
console.error("Failed to send Telegram message:", error)
return null
}
}
private getDefaultSocketPath(): string {
const platform = process.platform
if (platform === "win32") {
return "\\\\.\\pipe\\roo-code-ipc"
}
// macOS and Linux
const tmpDir = process.env.TMPDIR || process.env.TMP || "/tmp"
return `${tmpDir}/roo-code-ipc.sock`
}
private setupBotHandlers(): void {
if (!this.bot) return
// Handle incoming messages
this.bot.on("message", async (msg) => {
if (msg.chat.id.toString() !== this.options.chatId) {
// Ignore messages from other chats
return
}
this.messagesReceived++
const text = msg.text?.trim()
if (!text) return
// Handle commands
if (text.startsWith("/")) {
await this.handleCommand(text)
return
}
// Forward message to Roo Code as a new instruction
if (this.ipcClient?.isReady) {
this.ipcClient.sendTaskMessage(text)
await this.sendMessage("📤 Message sent to Roo Code")
} else {
await this.sendMessage("❌ Not connected to Roo Code")
}
})
// Handle callback queries (button clicks)
this.bot.on("callback_query", async (query) => {
if (!query.data || !query.message) return
const [action, approvalId] = query.data.split(":")
if (action === CALLBACK_APPROVE) {
await this.handleApproval(approvalId, true, query.message.message_id)
await this.bot?.answerCallbackQuery(query.id, { text: "✅ Approved" })
} else if (action === CALLBACK_DENY) {
await this.handleApproval(approvalId, false, query.message.message_id)
await this.bot?.answerCallbackQuery(query.id, { text: "❌ Denied" })
}
})
// Handle polling errors
this.bot.on("polling_error", (error) => {
this.lastError = error.message
console.error("Telegram polling error:", error)
})
}
private async handleCommand(text: string): Promise<void> {
const [command, ...args] = text.split(" ")
switch (command.toLowerCase()) {
case "/start":
case "/help":
await this.sendMessage(
"🤖 <b>Roo Code Telegram Bridge</b>\n\n" +
"<b>Commands:</b>\n" +
"/status - Show connection status\n" +
"/cancel - Cancel current task\n" +
"/help - Show this help message\n\n" +
"<b>Usage:</b>\n" +
"• Send any message to forward it as an instruction to Roo Code\n" +
"• Use the Approve/Deny buttons when prompted for tool operations",
)
break
case "/status":
const status = this.getStatus()
await this.sendMessage(
`📊 <b>Bridge Status</b>\n\n` +
`Running: ${status.running ? "✅" : "❌"}\n` +
`Connected: ${status.connected ? "✅" : "❌"}\n` +
`Messages Sent: ${status.messagesSent}\n` +
`Messages Received: ${status.messagesReceived}` +
(status.lastError ? `\nLast Error: ${status.lastError}` : ""),
)
break
case "/cancel":
if (this.ipcClient?.isReady) {
// Send cancel command
this.ipcClient.sendCommand({
commandName: TaskCommandName.CancelTask,
data: "", // Current task
})
await this.sendMessage("⏹️ Cancel request sent")
} else {
await this.sendMessage("❌ Not connected to Roo Code")
}
break
default:
await this.sendMessage(`❓ Unknown command: ${command}\nUse /help for available commands.`)
}
}
private async handleApproval(approvalId: string, approved: boolean, messageId: number): Promise<void> {
const approval = this.pendingApprovals.get(approvalId)
if (!approval) {
return
}
// Remove from pending
this.pendingApprovals.delete(approvalId)
// Send approval/denial to Roo Code
if (this.ipcClient?.isReady) {
if (approved) {
this.ipcClient.approveAsk()
} else {
this.ipcClient.denyAsk()
}
}
// Update the message to show the decision
const statusText = approved ? "✅ APPROVED" : "❌ DENIED"
try {
await this.bot?.editMessageReplyMarkup(
{ inline_keyboard: [] },
{
chat_id: this.options.chatId,
message_id: messageId,
},
)
await this.bot?.editMessageText(`${statusText}`, {
chat_id: this.options.chatId,
message_id: messageId,
parse_mode: "HTML",
})
} catch (error) {
// Ignore edit errors
}
}
private setupIpcHandlers(): void {
if (!this.ipcClient) return
this.ipcClient.on(IpcMessageType.Connect, () => {
console.log("Connected to Roo Code IPC")
})
this.ipcClient.on(IpcMessageType.Disconnect, () => {
console.log("Disconnected from Roo Code IPC")
this.sendMessage("⚠️ Lost connection to Roo Code")
})
this.ipcClient.on(IpcMessageType.Ack, (data) => {
console.log("IPC handshake complete:", data.clientId)
this.sendMessage("✅ Connected to Roo Code")
})
this.ipcClient.on(IpcMessageType.TaskEvent, async (event) => {
await this.handleTaskEvent(event)
})
}
private async handleTaskEvent(event: TaskEvent): Promise<void> {
switch (event.eventName) {
case RooCodeEventName.TaskStarted:
await this.sendMessage("🚀 <b>Task Started</b>")
break
case RooCodeEventName.TaskCompleted:
await this.sendMessage("✅ <b>Task Completed</b>")
break
case RooCodeEventName.TaskAborted:
await this.sendMessage("⏹️ <b>Task Aborted</b>")
break
case RooCodeEventName.TaskInteractive:
// Task needs user interaction - this is handled by the Message event
break
case RooCodeEventName.Message:
const [messageData] = event.payload as [{ action: "created" | "updated"; message: ClineMessage }]
await this.handleClineMessage(messageData)
break
case RooCodeEventName.TaskModeSwitched:
const [taskId, mode] = event.payload as [string, string]
await this.sendMessage(`🔄 Mode switched to: <b>${mode}</b>`)
break
}
}
private async handleClineMessage(messageData: { action: string; message: ClineMessage }): Promise<void> {
const { action, message } = messageData
// Only handle new messages
if (action !== "created") {
return
}
// Format the message for Telegram
const formattedMessage = formatMessageForTelegram(message)
if (message.type === "ask" && message.ask) {
// This is an approval request
const approvalId = `${Date.now()}-${Math.random().toString(36).substring(7)}`
const sentMessage = await this.sendMessage(formattedMessage, {
reply_markup: {
inline_keyboard: [
[
{ text: "✅ Approve", callback_data: `${CALLBACK_APPROVE}:${approvalId}` },
{ text: "❌ Deny", callback_data: `${CALLBACK_DENY}:${approvalId}` },
],
],
},
})
if (sentMessage) {
this.pendingApprovals.set(approvalId, {
messageId: sentMessage.message_id,
taskId: String(message.ts),
})
}
} else if (message.type === "say" && message.text) {
// This is an informational message
await this.sendMessage(formattedMessage)
}
}
}

View file

@ -0,0 +1,11 @@
{
"extends": "@roo-code/config-typescript/base.json",
"compilerOptions": {
"outDir": "./build",
"rootDir": "./src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build", "**/*.spec.ts", "**/*.test.ts"]
}

View file

@ -7,7 +7,15 @@ describe("IPC Types", () => {
})
it("should have all expected task commands", () => {
const expectedCommands = ["StartNewTask", "CancelTask", "CloseTask", "ResumeTask"]
const expectedCommands = [
"StartNewTask",
"CancelTask",
"CloseTask",
"ResumeTask",
"SendMessage",
"ApproveAsk",
"DenyAsk",
]
const actualCommands = Object.values(TaskCommandName)
expectedCommands.forEach((command) => {
@ -15,6 +23,14 @@ describe("IPC Types", () => {
})
})
it("should include ApproveAsk command", () => {
expect(TaskCommandName.ApproveAsk).toBe("ApproveAsk")
})
it("should include DenyAsk command", () => {
expect(TaskCommandName.DenyAsk).toBe("DenyAsk")
})
describe("Error Handling", () => {
it("should handle ResumeTask command gracefully when task not found", () => {
// This test verifies the schema validation - the actual error handling
@ -70,5 +86,55 @@ describe("IPC Types", () => {
const result = taskCommandSchema.safeParse(invalidCommand)
expect(result.success).toBe(false)
})
it("should validate ApproveAsk command with optional text and images", () => {
const approveCommand = {
commandName: TaskCommandName.ApproveAsk,
data: { text: "Approved with comment", images: [] },
}
const result = taskCommandSchema.safeParse(approveCommand)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.commandName).toBe("ApproveAsk")
expect(result.data.data.text).toBe("Approved with comment")
}
})
it("should validate ApproveAsk command with empty data", () => {
const approveCommand = {
commandName: TaskCommandName.ApproveAsk,
data: {},
}
const result = taskCommandSchema.safeParse(approveCommand)
expect(result.success).toBe(true)
})
it("should validate DenyAsk command with optional text and images", () => {
const denyCommand = {
commandName: TaskCommandName.DenyAsk,
data: { text: "Denied with reason", images: [] },
}
const result = taskCommandSchema.safeParse(denyCommand)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.commandName).toBe("DenyAsk")
expect(result.data.data.text).toBe("Denied with reason")
}
})
it("should validate DenyAsk command with empty data", () => {
const denyCommand = {
commandName: TaskCommandName.DenyAsk,
data: {},
}
const result = taskCommandSchema.safeParse(denyCommand)
expect(result.success).toBe(true)
})
})
})

View file

@ -46,6 +46,8 @@ export enum TaskCommandName {
CloseTask = "CloseTask",
ResumeTask = "ResumeTask",
SendMessage = "SendMessage",
ApproveAsk = "ApproveAsk",
DenyAsk = "DenyAsk",
}
/**
@ -81,6 +83,20 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [
images: z.array(z.string()).optional(),
}),
}),
z.object({
commandName: z.literal(TaskCommandName.ApproveAsk),
data: z.object({
text: z.string().optional(),
images: z.array(z.string()).optional(),
}),
}),
z.object({
commandName: z.literal(TaskCommandName.DenyAsk),
data: z.object({
text: z.string().optional(),
images: z.array(z.string()).optional(),
}),
}),
])
export type TaskCommand = z.infer<typeof taskCommandSchema>

View file

@ -95,6 +95,14 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
this.log(`[API] SendMessage -> ${data.text}`)
await this.sendMessage(data.text, data.images)
break
case TaskCommandName.ApproveAsk:
this.log(`[API] ApproveAsk -> ${data.text}`)
await this.approveCurrentAsk(data.text, data.images)
break
case TaskCommandName.DenyAsk:
this.log(`[API] DenyAsk -> ${data.text}`)
await this.denyCurrentAsk(data.text, data.images)
break
}
})
}
@ -194,6 +202,24 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
}
public async approveCurrentAsk(text?: string, images?: string[]) {
const currentTask = this.sidebarProvider.getCurrentTask()
if (currentTask) {
currentTask.approveAsk({ text, images })
} else {
this.log("[API] ApproveAsk failed: no current task")
}
}
public async denyCurrentAsk(text?: string, images?: string[]) {
const currentTask = this.sidebarProvider.getCurrentTask()
if (currentTask) {
currentTask.denyAsk({ text, images })
} else {
this.log("[API] DenyAsk failed: no current task")
}
}
public async pressPrimaryButton() {
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
}