From 6484dc9008448637ebdebd21f83d876aaac127c8 Mon Sep 17 00:00:00 2001 From: Eddie Santos <9561596+eddie-santos@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:01:06 -0700 Subject: [PATCH] Add Windsurf in edit tool to modify changes, if installed (#853) --- .../messages/ToolConfirmationMessage.tsx | 18 +++++++++++ packages/core/src/core/coreToolScheduler.ts | 2 ++ packages/core/src/tools/edit.ts | 22 ++++++++++--- packages/core/src/tools/tools.ts | 2 ++ packages/core/src/utils/editor.test.ts | 32 ++++++++++++++++++- packages/core/src/utils/editor.ts | 31 ++++++++++++------ 6 files changed, 92 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index c46d36f7..af9aba6a 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -98,6 +98,24 @@ export const ToolConfirmationMessage: React.FC< }); } + if ( + checkHasEditor('windsurf') && + notUsingSandbox && + externalEditorsEnabled + ) { + options.push({ + label: 'Modify with Windsurf', + value: ToolConfirmationOutcome.ModifyWindsurf, + }); + } + + if (checkHasEditor('cursor') && notUsingSandbox && externalEditorsEnabled) { + options.push({ + label: 'Modify with Cursor', + value: ToolConfirmationOutcome.ModifyCursor, + }); + } + if (checkHasEditor('vim') && externalEditorsEnabled) { options.push({ label: 'Modify with vim', diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index 679d86aa..fe409fb1 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -486,6 +486,8 @@ export class CoreToolScheduler { ); } else if ( outcome === ToolConfirmationOutcome.ModifyVSCode || + outcome === ToolConfirmationOutcome.ModifyWindsurf || + outcome === ToolConfirmationOutcome.ModifyCursor || outcome === ToolConfirmationOutcome.ModifyVim ) { const waitingToolCall = toolCall as WaitingToolCall; diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index fdabc5b6..cb569f80 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -25,6 +25,7 @@ import { ensureCorrectEdit } from '../utils/editCorrector.js'; import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js'; import { openDiff } from '../utils/editor.js'; import { ReadFileTool } from './read-file.js'; +import { EditorType } from '../utils/editor.js'; /** * Parameters for the Edit tool @@ -467,6 +468,19 @@ Expectation for required parameters: } } + async getEditor(outcome: ToolConfirmationOutcome): Promise { + switch (outcome) { + case ToolConfirmationOutcome.ModifyVSCode: + return 'vscode'; + case ToolConfirmationOutcome.ModifyWindsurf: + return 'windsurf'; + case ToolConfirmationOutcome.ModifyCursor: + return 'cursor'; + default: + return 'vim'; + } + } + /** * Creates temp files for the current and proposed file contents and opens a diff tool. * When the diff tool is closed, the tool will check if the file has been modified and provide the updated params. @@ -483,11 +497,9 @@ Expectation for required parameters: this.tempOldDiffPath = oldPath; this.tempNewDiffPath = newPath; - await openDiff( - this.tempOldDiffPath, - this.tempNewDiffPath, - outcome === ToolConfirmationOutcome.ModifyVSCode ? 'vscode' : 'vim', - ); + const editor = await this.getEditor(outcome); + + await openDiff(this.tempOldDiffPath, this.tempNewDiffPath, editor); return await this.getUpdatedParamsIfModified(params, _abortSignal); } diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index 6a1be9c9..e80047df 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -233,6 +233,8 @@ export enum ToolConfirmationOutcome { ProceedAlwaysServer = 'proceed_always_server', ProceedAlwaysTool = 'proceed_always_tool', ModifyVSCode = 'modify_vscode', + ModifyWindsurf = 'modify_windsurf', + ModifyCursor = 'modify_cursor', ModifyVim = 'modify_vim', Cancel = 'cancel', } diff --git a/packages/core/src/utils/editor.test.ts b/packages/core/src/utils/editor.test.ts index 20917c0f..e7c2f55a 100644 --- a/packages/core/src/utils/editor.test.ts +++ b/packages/core/src/utils/editor.test.ts @@ -35,6 +35,36 @@ describe('checkHasEditor', () => { expect(checkHasEditor('vscode')).toBe(false); }); + it('should return true for windsurf if "windsurf" command exists', () => { + (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/windsurf')); + expect(checkHasEditor('windsurf')).toBe(true); + expect(execSync).toHaveBeenCalledWith('command -v windsurf', { + stdio: 'ignore', + }); + }); + + it('should return false for windsurf if "windsurf" command does not exist', () => { + (execSync as Mock).mockImplementation(() => { + throw new Error(); + }); + expect(checkHasEditor('windsurf')).toBe(false); + }); + + it('should return true for cursor if "cursor" command exists', () => { + (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/cursor')); + expect(checkHasEditor('cursor')).toBe(true); + expect(execSync).toHaveBeenCalledWith('command -v cursor', { + stdio: 'ignore', + }); + }); + + it('should return false for cursor if "cursor" command does not exist', () => { + (execSync as Mock).mockImplementation(() => { + throw new Error(); + }); + expect(checkHasEditor('cursor')).toBe(false); + }); + it('should return true for vim if "vim" command exists', () => { (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim')); expect(checkHasEditor('vim')).toBe(true); @@ -71,7 +101,7 @@ describe('getDiffCommand', () => { it('should return null for an unsupported editor', () => { // @ts-expect-error Testing unsupported editor - const command = getDiffCommand('old.txt', 'new.txt', 'nano'); + const command = getDiffCommand('old.txt', 'new.txt', 'foobar'); expect(command).toBeNull(); }); }); diff --git a/packages/core/src/utils/editor.ts b/packages/core/src/utils/editor.ts index 6be5cffb..4d09e3a9 100644 --- a/packages/core/src/utils/editor.ts +++ b/packages/core/src/utils/editor.ts @@ -6,7 +6,7 @@ import { execSync, spawn } from 'child_process'; -type EditorType = 'vscode' | 'vim'; +export type EditorType = 'vscode' | 'windsurf' | 'cursor' | 'vim'; interface DiffCommand { command: string; @@ -25,15 +25,18 @@ function commandExists(cmd: string): boolean { } } +const editorCommands: Record = { + vscode: { win32: 'code.cmd', default: 'code' }, + windsurf: { win32: 'windsurf', default: 'windsurf' }, + cursor: { win32: 'cursor', default: 'cursor' }, + vim: { win32: 'vim', default: 'vim' }, +}; + export function checkHasEditor(editor: EditorType): boolean { - if (editor === 'vscode') { - return process.platform === 'win32' - ? commandExists('code.cmd') - : commandExists('code'); - } else if (editor === 'vim') { - return commandExists('vim'); - } - return false; + const commandConfig = editorCommands[editor]; + const command = + process.platform === 'win32' ? commandConfig.win32 : commandConfig.default; + return commandExists(command); } /** @@ -50,6 +53,16 @@ export function getDiffCommand( command: 'code', args: ['--wait', '--diff', oldPath, newPath], }; + case 'windsurf': + return { + command: 'windsurf', + args: ['--wait', '--diff', oldPath, newPath], + }; + case 'cursor': + return { + command: 'cursor', + args: ['--wait', '--diff', oldPath, newPath], + }; case 'vim': return { command: 'vim',