From 238a4137d3ca72ec942c0b3638d6eb40c15c89a8 Mon Sep 17 00:00:00 2001 From: cte Date: Wed, 2 Apr 2025 14:08:34 -0700 Subject: [PATCH] Add mode switch event --- benchmark/packages/types/src/ipc.ts | 5 ++ benchmark/packages/types/src/roo-code.ts | 2 + benchmark/scripts/setup.sh | 62 +++++++++++------------- e2e/src/suite/modes.test.ts | 41 +++++----------- src/core/Cline.ts | 1 + src/core/webview/ClineProvider.ts | 7 +-- src/exports/api.ts | 2 + src/exports/roo-code.d.ts | 2 + src/exports/types.ts | 1 + src/schemas/index.ts | 2 + src/schemas/ipc.ts | 4 ++ 11 files changed, 62 insertions(+), 67 deletions(-) diff --git a/benchmark/packages/types/src/ipc.ts b/benchmark/packages/types/src/ipc.ts index c6791f67d1..96a2fb6884 100644 --- a/benchmark/packages/types/src/ipc.ts +++ b/benchmark/packages/types/src/ipc.ts @@ -71,6 +71,11 @@ export const taskEventSchema = z.discriminatedUnion("eventName", [ payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted], taskId: z.number().optional(), }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskModeSwitched), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskModeSwitched], + taskId: z.number().optional(), + }), z.object({ eventName: z.literal(RooCodeEventName.TaskPaused), payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused], diff --git a/benchmark/packages/types/src/roo-code.ts b/benchmark/packages/types/src/roo-code.ts index c25102ecc4..732d5af815 100644 --- a/benchmark/packages/types/src/roo-code.ts +++ b/benchmark/packages/types/src/roo-code.ts @@ -795,6 +795,7 @@ export enum RooCodeEventName { Message = "message", TaskCreated = "taskCreated", TaskStarted = "taskStarted", + TaskModeSwitched = "taskModeSwitched", TaskPaused = "taskPaused", TaskUnpaused = "taskUnpaused", TaskAskResponded = "taskAskResponded", @@ -814,6 +815,7 @@ export const rooCodeEventsSchema = z.object({ ]), [RooCodeEventName.TaskCreated]: z.tuple([z.string()]), [RooCodeEventName.TaskStarted]: z.tuple([z.string()]), + [RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]), [RooCodeEventName.TaskPaused]: z.tuple([z.string()]), [RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]), [RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]), diff --git a/benchmark/scripts/setup.sh b/benchmark/scripts/setup.sh index 9ed9ca74f8..b9cff07303 100755 --- a/benchmark/scripts/setup.sh +++ b/benchmark/scripts/setup.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash menu() { - echo -e "\nWhich eval types would you like to support?\n" + echo -e "\nšŸ“‹ Which eval types would you like to support?\n" for i in ${!options[@]}; do - printf "%d %6s [%s]" $((i + 1)) "${options[i]}" "${choices[i]:- }" + printf " %d) %-6s [%s]" $((i + 1)) "${options[i]}" "${choices[i]:- }" if [[ $i == 0 ]]; then printf " (required)" @@ -13,11 +13,11 @@ menu() { printf "\n" done - echo + echo -e " q) quit\n" } build_extension() { - echo "Building the Roo Code extension..." + echo "šŸ”Ø Building the Roo Code extension..." cd .. mkdir -p bin npm run install-extension -- --silent --no-audit || exit 1 @@ -29,7 +29,7 @@ build_extension() { } if [[ "$(uname -s)" != "Darwin" ]]; then - echo "Only macOS is currently supported." + echo "āš ļø Only macOS is currently supported." exit 1 fi @@ -43,19 +43,11 @@ for i in "${!options[@]}"; do choices[i]="*" done -prompt="Type šŸ”¢ to select, 'a' for all, 'q' to quit, āŽ to continue: " +prompt="Type 1-5 to select, 'q' to quit, āŽ to continue: " while menu && read -rp "$prompt" num && [[ "$num" ]]; do [[ "$num" == "q" ]] && exit 0 - [[ "$num" == "a" ]] && { - for i in ${!options[@]}; do - choices[i]="*" - done - - continue - } - [[ "$num" != *[![:digit:]]* ]] && ((num > 1 && num <= ${#options[@]})) || { @@ -81,14 +73,14 @@ printf "\n" if ! command -v brew &>/dev/null; then if [[ -f "/opt/homebrew/bin/brew" ]]; then - echo "Homebrew is installed but not in your PATH" + echo "āš ļø Homebrew is installed but not in your PATH" exit 1 fi - read -p "Homebrew (https://brew.sh) is required. Install it? (Y/n): " install_brew + read -p "šŸŗ Homebrew (https://brew.sh) is required. Install it? (Y/n): " install_brew if [[ "$install_brew" =~ ^[Yy]|^$ ]]; then - echo "ā˜• Installing Homebrew..." + echo "šŸŗ Installing Homebrew..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || exit 1 # Can be undone with: # /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)" && sudo rm -rvf /opt/homebrew @@ -119,14 +111,14 @@ ASDF_PATH="$(brew --prefix asdf)/libexec/asdf.sh" if ! command -v asdf &>/dev/null; then if [[ -f "$ASDF_PATH" ]]; then - echo "asdf is installed but not in your PATH" + echo "āš ļø asdf is installed but not in your PATH" exit 1 fi - read -p "asdf (https://asdf-vm.com) is required. Install it? (Y/n): " install_asdf + read -p "šŸ› ļø asdf (https://asdf-vm.com) is required. Install it? (Y/n): " install_asdf if [[ "$install_asdf" =~ ^[Yy]|^$ ]]; then - echo "Installing asdf..." + echo "šŸ› ļø Installing asdf..." brew install asdf || exit 1 # Can be undone with: # brew uninstall asdf @@ -152,7 +144,7 @@ else fi if ! command -v gh &>/dev/null; then - read -p "GitHub cli is needed to submit evals results. Install it? (Y/n): " install_gh + read -p "šŸ‘Øā€šŸ’» GitHub cli is needed to submit evals results. Install it? (Y/n): " install_gh if [[ "$install_gh" =~ ^[Yy]|^$ ]]; then brew install gh || exit 1 @@ -173,9 +165,9 @@ for i in "${!options[@]}"; do if [[ "${has_asdf_plugin[$plugin]}" == "true" ]]; then if ! asdf plugin list | grep -q "^${plugin}$" && ! command -v "${binary}" &>/dev/null; then - echo "Installing ${plugin} asdf plugin..." + echo "šŸ“¦ Installing ${plugin} asdf plugin..." asdf plugin add "${plugin}" || exit 1 - echo "āœ… asdf ${plugin} plugin installed" + echo "āœ… asdf ${plugin} plugin installed successfully" fi fi @@ -194,7 +186,7 @@ for i in "${!options[@]}"; do if [[ $(node --version) != "v20.18.1" ]]; then NODE_VERSION=$(node --version) echo "🚨 You have the wrong version of node installed ($NODE_VERSION)." - echo "If you are using nvm then run 'nvm install' to install the version specified by the repo's .nvmrc." + echo "šŸ’” If you are using nvm then run 'nvm install' to install the version specified by the repo's .nvmrc." exit 1 fi ;; @@ -246,7 +238,7 @@ for i in "${!options[@]}"; do "java") if ! command -v javac &>/dev/null || ! javac --version &>/dev/null; then - echo "Installing Java..." + echo "ā˜• Installing Java..." brew install openjdk@17 || exit 1 export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH" @@ -280,7 +272,7 @@ pnpm install --silent || exit 1 if [[ ! -d "../../evals" ]]; then if gh auth status &>/dev/null; then - read -p "Would you like to be able to share eval results? (Y/n): " fork_evals + read -p "šŸ”— Would you like to be able to share eval results? (Y/n): " fork_evals if [[ "$fork_evals" =~ ^[Yy]|^$ ]]; then gh repo fork cte/evals ../../evals || exit 1 @@ -297,14 +289,14 @@ if [[ ! -s .env ]]; then fi if ! grep -q "OPENROUTER_API_KEY" .env; then - read -p "Enter your OpenRouter API Key (sk-or-v1-...): " openrouter_api_key - echo "Validating OpenRouter API Key..." - curl --silent --fail https://openrouter.ai/api/v1/key -H "Authorization: Bearer $openrouter_api_key" | jq || exit 1 + read -p "šŸ” Enter your OpenRouter API key (sk-or-v1-...): " openrouter_api_key + echo "šŸ”‘ Validating..." + curl --silent --fail https://openrouter.ai/api/v1/key -H "Authorization: Bearer $openrouter_api_key" || echo "āš ļø Invalid API key" && exit 1 echo "OPENROUTER_API_KEY=$openrouter_api_key" >>.env fi if ! command -v code &>/dev/null; then - echo "Visual Studio Code cli is not installed" + echo "āš ļø Visual Studio Code cli is not installed" exit 1 else VSCODE_VERSION=$(code --version | head -n 1) @@ -314,7 +306,7 @@ fi if [[ ! -s "../bin/roo-code-latest.vsix" ]]; then build_extension else - read -p "Do you want to build a new version of the Roo Code extension? (y/N): " build_extension + read -p "šŸ’» Do you want to build a new version of the Roo Code extension? (y/N): " build_extension if [[ "$build_extension" =~ ^[Yy]$ ]]; then build_extension @@ -323,16 +315,16 @@ else fi fi -echo -e "\n🤘 You're ready to rock and roll!\n" +echo -e "\nšŸš€ You're ready to rock and roll! \n" if ! nc -z localhost 3000; then - read -p "Would you like to start the evals web app? (y/N): " start_evals + read -p "šŸš€ Would you like to start the evals web app? (y/N): " start_evals if [[ "$start_evals" =~ ^[Yy]$ ]]; then pnpm web else - echo "You can start it anytime with 'pnpm web'." + echo "šŸ’” You can start it anytime with 'pnpm web'." fi else - echo "šŸ¤– The evals web app is running at http://localhost:3000" + echo "🌐 The evals web app is running at http://localhost:3000" fi diff --git a/e2e/src/suite/modes.test.ts b/e2e/src/suite/modes.test.ts index c4ae7f1af6..3a86647186 100644 --- a/e2e/src/suite/modes.test.ts +++ b/e2e/src/suite/modes.test.ts @@ -13,11 +13,17 @@ suite("Roo Code Modes", () => { */ const switchModesPrompt = - "For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode. " + - "Do not start with the current mode." + "For each mode (Architect, Ask, Debug) respond with the mode name and what it specializes in after switching to that mode." let messages: ClineMessage[] = [] + const modeSwitches: string[] = [] + + api.on("taskModeSwitched", (_taskId, mode) => { + console.log("taskModeSwitched", mode) + modeSwitches.push(mode) + }) + api.on("message", ({ message }) => { if (message.type === "say" && message.partial === false) { messages.push(message) @@ -25,36 +31,13 @@ suite("Roo Code Modes", () => { }) const switchModesTaskId = await api.startNewTask({ - configuration: { mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, + configuration: { mode: "code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, text: switchModesPrompt, }) - await waitUntilCompleted({ api, taskId: switchModesTaskId, timeout: 60_000 }) - - /** - * Grade the response. - */ - - const response = messages - .filter(({ type, say, partial }) => say === "text") - .map(({ text }) => text ?? "") - .join("\n") - - const gradePrompt = `Given this prompt: ${switchModesPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)". For example: Grade 7\n\nResponse: ${response}` - - messages = [] - - const gradeTaskId = await api.startNewTask({ configuration: { mode: "Ask" }, text: gradePrompt }) - await waitUntilCompleted({ api, taskId: gradeTaskId }) - - const completion = messages.find(({ type, say, partial }) => say === "completion_result") - const match = completion?.text?.match(/Grade: (\d+)/) - const score = parseInt(match?.[1] ?? "0") - assert.ok( - score >= 7 && score <= 10, - `Grade must be between 7 and 10. DEBUG: score = ${score}, completion = ${completion?.text}`, - ) - + await waitUntilCompleted({ api, taskId: switchModesTaskId }) await api.cancelCurrentTask() + + assert.deepEqual(modeSwitches.sort(), ["architect", "ask", "debug"]) }) }) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 005b00fc0d..c7e9fe66a5 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -87,6 +87,7 @@ type UserContent = Array export type ClineEvents = { message: [{ action: "created" | "updated"; message: ClineMessage }] taskStarted: [] + taskModeSwitched: [taskId: string, mode: string] taskPaused: [] taskUnpaused: [] taskAskResponded: [] diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 8206375d79..c31b77a5e1 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -740,10 +740,11 @@ export class ClineProvider extends EventEmitter implements */ public async handleModeSwitch(newMode: Mode) { // Capture mode switch telemetry event - const currentTaskId = this.getCurrentCline()?.taskId + const cline = this.getCurrentCline() - if (currentTaskId) { - telemetryService.captureModeSwitch(currentTaskId, newMode) + if (cline) { + telemetryService.captureModeSwitch(cline.taskId, newMode) + cline.emit("taskModeSwitched", cline.taskId, newMode) } await this.updateGlobalState("mode", newMode) diff --git a/src/exports/api.ts b/src/exports/api.ts index 6ddac43056..609eda3b6d 100644 --- a/src/exports/api.ts +++ b/src/exports/api.ts @@ -176,6 +176,8 @@ export class API extends EventEmitter implements RooCodeAPI { cline.on("message", (message) => this.emit(RooCodeEventName.Message, { taskId: cline.taskId, ...message })) + cline.on("taskModeSwitched", (taskId, mode) => this.emit(RooCodeEventName.TaskModeSwitched, taskId, mode)) + cline.on("taskTokenUsageUpdated", (_, usage) => this.emit(RooCodeEventName.TaskTokenUsageUpdated, cline.taskId, usage), ) diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 350d0ae5a3..aa9607a012 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -470,6 +470,7 @@ type RooCodeEvents = { ] taskCreated: [string] taskStarted: [string] + taskModeSwitched: [string, string] taskPaused: [string] taskUnpaused: [string] taskAskResponded: [string] @@ -506,6 +507,7 @@ declare enum RooCodeEventName { Message = "message", TaskCreated = "taskCreated", TaskStarted = "taskStarted", + TaskModeSwitched = "taskModeSwitched", TaskPaused = "taskPaused", TaskUnpaused = "taskUnpaused", TaskAskResponded = "taskAskResponded", diff --git a/src/exports/types.ts b/src/exports/types.ts index 20f583fb60..794c120904 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -479,6 +479,7 @@ type RooCodeEvents = { ] taskCreated: [string] taskStarted: [string] + taskModeSwitched: [string, string] taskPaused: [string] taskUnpaused: [string] taskAskResponded: [string] diff --git a/src/schemas/index.ts b/src/schemas/index.ts index e397635d41..a339e9bb54 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -797,6 +797,7 @@ export enum RooCodeEventName { Message = "message", TaskCreated = "taskCreated", TaskStarted = "taskStarted", + TaskModeSwitched = "taskModeSwitched", TaskPaused = "taskPaused", TaskUnpaused = "taskUnpaused", TaskAskResponded = "taskAskResponded", @@ -816,6 +817,7 @@ export const rooCodeEventsSchema = z.object({ ]), [RooCodeEventName.TaskCreated]: z.tuple([z.string()]), [RooCodeEventName.TaskStarted]: z.tuple([z.string()]), + [RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]), [RooCodeEventName.TaskPaused]: z.tuple([z.string()]), [RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]), [RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]), diff --git a/src/schemas/ipc.ts b/src/schemas/ipc.ts index 9a351bdf8a..08ab0c974d 100644 --- a/src/schemas/ipc.ts +++ b/src/schemas/ipc.ts @@ -63,6 +63,10 @@ export const taskEventSchema = z.discriminatedUnion("eventName", [ eventName: z.literal(RooCodeEventName.TaskStarted), payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted], }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskModeSwitched), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskModeSwitched], + }), z.object({ eventName: z.literal(RooCodeEventName.TaskPaused), payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused],