diff --git a/.gitignore b/.gitignore index ad5008ab73..cc3df8e1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ roo-cline-*.vsix # Prompts prompts - +.clinerules diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 38d70a6ccc..bf1033ea34 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -98,6 +98,8 @@ export class Cline { apiConfiguration: ApiConfiguration, customInstructions?: string, diffEnabled?: boolean, + isInteractiveMode?: boolean, + browserPort?: string, task?: string, images?: string[], historyItem?: HistoryItem, @@ -108,6 +110,8 @@ export class Cline { this.urlContentFetcher = new UrlContentFetcher(provider.context) this.browserSession = new BrowserSession(provider.context) this.diffViewProvider = new DiffViewProvider(cwd) + this.isInteractiveMode = isInteractiveMode ?? false + this.browserPort = browserPort ?? "7333" this.customInstructions = customInstructions if (diffEnabled && this.api.getModel().id) { this.diffStrategy = getDiffStrategy(this.api.getModel().id) @@ -633,48 +637,29 @@ export class Cline { if (responseImages && responseImages.length > 0) { newUserContent.push(...formatResponse.imageBlocks(responseImages)) } - const wasInteractiveBrowser = (lastRelevantMessageIndex > 0) ? modifiedClineMessages[lastRelevantMessageIndex - 1].text?.includes("interactive mode") : false + const state = await this.providerRef.deref()?.getState() + const wasInteractiveBrowser = state?.isInteractiveMode let hadBrowserPort = ''; if ( wasInteractiveBrowser ) { - const match = modifiedClineMessages[lastRelevantMessageIndex - 1].text?.match(/\(browserPort\s*=\s*(\d+)\)/) - if (match) { - hadBrowserPort = match[1] ?? this.browserPort; - this.providerRef.deref()?.outputChannel.appendLine(`resumeTaskFromHistory :: browserPort :: ${hadBrowserPort}`) - } + hadBrowserPort = state?.browserPort; + this.providerRef.deref()?.outputChannel.appendLine(`resumeTaskFromHistory :: browserPort :: ${hadBrowserPort}`) } await this.overwriteApiConversationHistory(modifiedApiConversationHistory) await this.initiateTaskLoop(newUserContent, wasInteractiveBrowser, hadBrowserPort) } private async initiateTaskLoop(userContent: UserContent, wasInteractiveBrowser: boolean = false, hadBrowserPort: string = ''): Promise { - // Check if any text block contains "interactive mode" - const hasInteractiveMode = userContent.some((block) => { - if (block.type === "text" && typeof block.text === "string") { - return (block.type === "text" && - typeof block.text === "string" && - block.text.toLowerCase().includes("interactive mode")) - } else { - return false - } - } - ) || wasInteractiveBrowser; + const state = await this.providerRef.deref()?.getState() + const hasInteractiveMode = state?.isInteractiveMode ?? wasInteractiveBrowser + this.providerRef.deref()?.outputChannel.appendLine(`initiateTaskLoop :: hasInteractiveMode :: ${hasInteractiveMode}`) // Set interactive mode flag if found in text if (hasInteractiveMode) { this.isInteractiveMode = true; - - // Parse browserPort if specified in text blocks - userContent.forEach((block) => { - if (block.type === "text" && typeof block.text === "string") { - const match = block.text.match(/\(browserPort\s*=\s*(\d+)\)/) - if (match) { - this.browserPort = match[1] ?? hadBrowserPort; - this.providerRef.deref()?.outputChannel.appendLine(`initiateTaskLoop :: browserPort :: ${this.browserPort}`) - } - } - }) + this.browserPort = state?.browserPort ?? hadBrowserPort + this.providerRef.deref()?.outputChannel.appendLine(`initiateTaskLoop :: browserPort :: ${this.browserPort}`) } let nextUserContent = userContent; @@ -1863,12 +1848,25 @@ export class Cline { userContent: UserContent, includeFileDetails: boolean = false, isInteractiveMode: boolean = false, - browserPort: string = '7333' + browserPort?: string ): Promise { + if (this.presentAssistantMessageHasPendingUpdates) { + this.presentAssistantMessage() + } + + const state = await this.providerRef.deref()?.getState() + // Use optional chaining and provide defaults + this.isInteractiveMode = isInteractiveMode ?? state?.isInteractiveMode ?? false + this.browserPort = browserPort ?? state?.browserPort ?? "7333" + + // Store interactive mode state + if (state?.isInteractiveMode !== undefined) { + this.isInteractiveMode = state.isInteractiveMode + } + if (this.abort) { throw new Error("Cline instance aborted") } - // Store interactive mode state this.isInteractiveMode = isInteractiveMode; diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index e7e2fe6c91..ec205e54c7 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -237,6 +237,8 @@ describe('Cline', () => { mockApiConfig, 'custom instructions', false, + true, // isInteractiveMode + '7333', // browserPort 'test task' ); diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7779436bde..8c7319313a 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -65,6 +65,8 @@ type GlobalStateKey = | "allowedCommands" | "soundEnabled" | "diffEnabled" + | "isInteractiveMode" + | "browserPort" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -203,13 +205,17 @@ export class ClineProvider implements vscode.WebviewViewProvider { apiConfiguration, customInstructions, diffEnabled, + isInteractiveMode, + browserPort, } = await this.getState() - + this.cline = new Cline( this, apiConfiguration, customInstructions, diffEnabled, + isInteractiveMode, + browserPort, task, images ) @@ -221,6 +227,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { apiConfiguration, customInstructions, diffEnabled, + isInteractiveMode, + browserPort, } = await this.getState() this.cline = new Cline( @@ -228,6 +236,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { apiConfiguration, customInstructions, diffEnabled, + isInteractiveMode, + browserPort, undefined, undefined, historyItem, @@ -547,6 +557,14 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("diffEnabled", diffEnabled) await this.postStateToWebview() break + case "isInteractiveMode": + await this.updateGlobalState("isInteractiveMode", message.bool ?? false) + await this.postStateToWebview() + break + case "browserPort": + await this.updateGlobalState("browserPort", message.text ?? "7333") + await this.postStateToWebview() + break } }, null, @@ -855,6 +873,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundEnabled, diffEnabled, taskHistory, + isInteractiveMode, + browserPort, } = await this.getState() const allowedCommands = vscode.workspace @@ -878,6 +898,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { diffEnabled: diffEnabled ?? false, shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, allowedCommands, + isInteractiveMode: isInteractiveMode ?? false, + browserPort: browserPort ?? "7333", } } @@ -969,6 +991,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { allowedCommands, soundEnabled, diffEnabled, + isInteractiveMode, + browserPort, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1005,6 +1029,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("allowedCommands") as Promise, this.getGlobalState("soundEnabled") as Promise, this.getGlobalState("diffEnabled") as Promise, + this.getGlobalState("isInteractiveMode") as Promise, + this.getGlobalState("browserPort") as Promise ]) let apiProvider: ApiProvider @@ -1059,6 +1085,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { allowedCommands, soundEnabled, diffEnabled, + isInteractiveMode: isInteractiveMode ?? false, + browserPort: browserPort ?? "7333" } } diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 91bd4b286e..f0ab1d26fd 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -23,6 +23,7 @@ export class BrowserSession { async launchBrowser(interactive: boolean = false, port?: string) { console.log("launch browser called") this.isInteractive = interactive + this.browserPort = port ?? this.browserPort // Set browserPort if provided, otherwise use default if (port) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index f9937280ea..c2f9077ffa 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -43,6 +43,8 @@ export interface ExtensionState { allowedCommands?: string[] soundEnabled?: boolean diffEnabled?: boolean + isInteractiveMode?: boolean + browserPort?: string } export interface ClineMessage { @@ -99,6 +101,8 @@ export interface ClineSayTool { content?: string regex?: string filePattern?: string + isInteractiveMode?: boolean + browserPort?: string } // must keep in sync with system prompt diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 31b40fc783..da23381e04 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -32,6 +32,8 @@ export interface WebviewMessage { | "playSound" | "soundEnabled" | "diffEnabled" + | "isInteractiveMode" + | "browserPort" text?: string askResponse?: ClineAskResponse apiConfiguration?: ApiConfiguration diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 0aaae53233..9cc0c0581f 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -32,6 +32,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { openRouterModels, setAllowedCommands, allowedCommands, + isInteractiveMode, + setInteractiveBrowserMode, + browserPort, + setBrowserPort, } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) @@ -53,6 +57,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] }) vscode.postMessage({ type: "soundEnabled", bool: soundEnabled }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) + vscode.postMessage({ type: "isInteractiveMode", bool: isInteractiveMode }) + vscode.postMessage({ type: "browserPort", text: browserPort }) onDone() } } @@ -323,6 +329,45 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { +
+

Browser Settings

+ +
+ setInteractiveBrowserMode(e.target.checked)} + > + Interactive Browser Mode + +

+ When enabled, connects to an existing Chrome instance instead of launching a new browser. +

+
+ + {isInteractiveMode && ( +
+ setBrowserPort(e.target.value)} + placeholder="Browser debugging port (default: 7333)" + > + Browser Port + +

+ Port number for Chrome's remote debugging. Launch Chrome with --remote-debugging-port=PORT +

+
+ )} +
+ {IS_DEV && ( <>
Debug
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 78439635eb..1d0b405e30 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -27,6 +27,8 @@ export interface ExtensionStateContextType extends ExtensionState { setAllowedCommands: (value: string[]) => void setSoundEnabled: (value: boolean) => void setDiffEnabled: (value: boolean) => void + setInteractiveBrowserMode: (value: boolean) => void + setBrowserPort: (value: string) => void } const ExtensionStateContext = createContext(undefined) @@ -40,6 +42,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode allowedCommands: [], soundEnabled: false, diffEnabled: false, + isInteractiveMode: false, + browserPort: "7333", }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) @@ -130,6 +134,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })), + setInteractiveBrowserMode: (value) => setState((prevState) => ({ ...prevState, isInteractiveMode: value })), + setBrowserPort: (value) => setState((prevState) => ({ ...prevState, browserPort: value })), } return {children}