From 719814fb765889123d73e731aee3a985c7ece55c Mon Sep 17 00:00:00 2001 From: Cline Evaluation Date: Wed, 4 Jun 2025 18:15:19 -0700 Subject: [PATCH] terminal setting for reusing terminal commands --- src/core/controller/index.ts | 13 ++++ src/core/storage/state-keys.ts | 1 + src/core/storage/state.ts | 4 +- src/core/task/index.ts | 2 + src/integrations/terminal/TerminalManager.ts | 67 ++++++++++--------- src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + .../src/components/settings/SettingsView.tsx | 21 +++++- .../settings/TerminalSettingsSection.tsx | 30 ++++++++- .../src/context/ExtensionStateContext.tsx | 7 ++ 10 files changed, 113 insertions(+), 34 deletions(-) diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index 270eacad83..20cdbb27c3 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -151,6 +151,7 @@ export class Controller { browserSettings, chatSettings, shellIntegrationTimeout, + terminalReuseEnabled, enableCheckpointsSetting, isNewUser, taskHistory, @@ -185,6 +186,7 @@ export class Controller { browserSettings, chatSettings, shellIntegrationTimeout, + terminalReuseEnabled ?? true, enableCheckpointsSetting ?? true, customInstructions, task, @@ -401,6 +403,15 @@ export class Controller { } } + // terminal settings + if (typeof message.shellIntegrationTimeout === "number") { + await updateGlobalState(this.context, "shellIntegrationTimeout", message.shellIntegrationTimeout) + } + + if (typeof message.terminalReuseEnabled === "boolean") { + await updateGlobalState(this.context, "terminalReuseEnabled", message.terminalReuseEnabled) + } + // after settings are updated, post state to webview await this.postStateToWebview() @@ -1171,6 +1182,7 @@ export class Controller { globalClineRulesToggles, globalWorkflowToggles, shellIntegrationTimeout, + terminalReuseEnabled, isNewUser, } = await getAllExtensionState(this.context) @@ -1215,6 +1227,7 @@ export class Controller { localWorkflowToggles: localWorkflowToggles || {}, globalWorkflowToggles: globalWorkflowToggles || {}, shellIntegrationTimeout, + terminalReuseEnabled, isNewUser, } } diff --git a/src/core/storage/state-keys.ts b/src/core/storage/state-keys.ts index 8723b11ea1..50b8788eae 100644 --- a/src/core/storage/state-keys.ts +++ b/src/core/storage/state-keys.ts @@ -91,6 +91,7 @@ export type GlobalStateKey = | "favoritedModelIds" | "requestTimeoutMs" | "shellIntegrationTimeout" + | "terminalReuseEnabled" | "isNewUser" export type LocalStateKey = "localClineRulesToggles" diff --git a/src/core/storage/state.ts b/src/core/storage/state.ts index 4476a5df83..0247b2d4ec 100644 --- a/src/core/storage/state.ts +++ b/src/core/storage/state.ts @@ -165,6 +165,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) { enableCheckpointsSettingRaw, mcpMarketplaceEnabledRaw, globalWorkflowToggles, + terminalReuseEnabled, ] = await Promise.all([ getGlobalState(context, "isNewUser") as Promise, getGlobalState(context, "apiProvider") as Promise, @@ -255,7 +256,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) { getGlobalState(context, "enableCheckpointsSetting") as Promise, getGlobalState(context, "mcpMarketplaceEnabled") as Promise, getGlobalState(context, "globalWorkflowToggles") as Promise, - fetch, + getGlobalState(context, "terminalReuseEnabled") as Promise, ]) let apiProvider: ApiProvider @@ -390,6 +391,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) { planActSeparateModelsSetting, enableCheckpointsSetting: enableCheckpointsSetting, shellIntegrationTimeout: shellIntegrationTimeout || 4000, + terminalReuseEnabled: terminalReuseEnabled ?? true, globalWorkflowToggles: globalWorkflowToggles || {}, } } diff --git a/src/core/task/index.ts b/src/core/task/index.ts index 03b4cecda0..6b374d5950 100644 --- a/src/core/task/index.ts +++ b/src/core/task/index.ts @@ -199,6 +199,7 @@ export class Task { browserSettings: BrowserSettings, chatSettings: ChatSettings, shellIntegrationTimeout: number, + terminalReuseEnabled: boolean, enableCheckpointsSetting: boolean, customInstructions?: string, task?: string, @@ -218,6 +219,7 @@ export class Task { // Initialization moved to startTask/resumeTaskFromHistory this.terminalManager = new TerminalManager() this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout) + this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true) this.urlContentFetcher = new UrlContentFetcher(context) this.browserSession = new BrowserSession(context, browserSettings) this.contextManager = new ContextManager() diff --git a/src/integrations/terminal/TerminalManager.ts b/src/integrations/terminal/TerminalManager.ts index fa902c3e91..ac7fafda89 100644 --- a/src/integrations/terminal/TerminalManager.ts +++ b/src/integrations/terminal/TerminalManager.ts @@ -94,6 +94,7 @@ export class TerminalManager { private processes: Map = new Map() private disposables: vscode.Disposable[] = [] private shellIntegrationTimeout: number = 4000 + private terminalReuseEnabled: boolean = true constructor() { let disposable: vscode.Disposable | undefined @@ -234,42 +235,44 @@ export class TerminalManager { return matchingTerminal } - // If no matching terminal exists, try to find any non-busy terminal - const availableTerminal = terminals.find((t) => !t.busy) - if (availableTerminal) { - // Set up promise and tracking for CWD change - const cwdPromise = new Promise((resolve, reject) => { - availableTerminal.pendingCwdChange = cwd - availableTerminal.cwdResolved = { resolve, reject } - }) + // If no matching terminal exists and terminal reuse is enabled, try to find any non-busy terminal + if (this.terminalReuseEnabled) { + const availableTerminal = terminals.find((t) => !t.busy) + if (availableTerminal) { + // Set up promise and tracking for CWD change + const cwdPromise = new Promise((resolve, reject) => { + availableTerminal.pendingCwdChange = cwd + availableTerminal.cwdResolved = { resolve, reject } + }) - // Navigate back to the desired directory - await this.runCommand(availableTerminal, `cd "${cwd}"`) + // Navigate back to the desired directory + await this.runCommand(availableTerminal, `cd "${cwd}"`) - // Either resolve immediately if CWD already updated or wait for event/timeout - if (this.isCwdMatchingExpected(availableTerminal)) { - if (availableTerminal.cwdResolved) { - availableTerminal.cwdResolved.resolve() - } - availableTerminal.pendingCwdChange = undefined - availableTerminal.cwdResolved = undefined - } else { - try { - // Wait with a timeout for state change event to resolve - await Promise.race([ - cwdPromise, - new Promise((_, reject) => - setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000), - ), - ]) - } catch (err) { - // Clear pending state on timeout + // Either resolve immediately if CWD already updated or wait for event/timeout + if (this.isCwdMatchingExpected(availableTerminal)) { + if (availableTerminal.cwdResolved) { + availableTerminal.cwdResolved.resolve() + } availableTerminal.pendingCwdChange = undefined availableTerminal.cwdResolved = undefined + } else { + try { + // Wait with a timeout for state change event to resolve + await Promise.race([ + cwdPromise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000), + ), + ]) + } catch (err) { + // Clear pending state on timeout + availableTerminal.pendingCwdChange = undefined + availableTerminal.cwdResolved = undefined + } } + this.terminalIds.add(availableTerminal.id) + return availableTerminal } - this.terminalIds.add(availableTerminal.id) - return availableTerminal } // If all terminals are busy, create a new one @@ -311,4 +314,8 @@ export class TerminalManager { setShellIntegrationTimeout(timeout: number): void { this.shellIntegrationTimeout = timeout } + + setTerminalReuseEnabled(enabled: boolean): void { + this.terminalReuseEnabled = enabled + } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ececf31550..d7e24e4fea 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -114,6 +114,7 @@ export interface ExtensionState { taskHistory: HistoryItem[] telemetrySetting: TelemetrySetting shellIntegrationTimeout: number + terminalReuseEnabled?: boolean uriScheme?: string userInfo?: { displayName: string | null diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index b17854f51a..26a82a42de 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -80,6 +80,7 @@ export interface WebviewMessage { offset?: number shellIntegrationTimeout?: number + terminalReuseEnabled?: boolean } export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index e62402002b..e0e2a6bf60 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -126,6 +126,10 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => { setEnableCheckpointsSetting, mcpMarketplaceEnabled, setMcpMarketplaceEnabled, + shellIntegrationTimeout, + setShellIntegrationTimeout, + terminalReuseEnabled, + setTerminalReuseEnabled, setApiConfiguration, } = useExtensionState() @@ -138,6 +142,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => { enableCheckpointsSetting, mcpMarketplaceEnabled, chatSettings, + shellIntegrationTimeout, + terminalReuseEnabled, }) const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) @@ -178,6 +184,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => { telemetrySetting, enableCheckpointsSetting, mcpMarketplaceEnabled, + shellIntegrationTimeout, + terminalReuseEnabled, apiConfiguration: apiConfigurationToSubmit, }) @@ -200,7 +208,9 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => { planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting || enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting || mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled || - JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) + JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) || + shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout || + terminalReuseEnabled !== originalState.current.terminalReuseEnabled setHasUnsavedChanges(hasChanges) }, [ @@ -211,6 +221,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => { enableCheckpointsSetting, mcpMarketplaceEnabled, chatSettings, + shellIntegrationTimeout, + terminalReuseEnabled, ]) // Handle cancel button click @@ -241,6 +253,13 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => { : false, ) } + // Reset terminal settings + if (typeof setShellIntegrationTimeout === "function") { + setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout) + } + if (typeof setTerminalReuseEnabled === "function") { + setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true) + } // Close settings view onDone() } diff --git a/webview-ui/src/components/settings/TerminalSettingsSection.tsx b/webview-ui/src/components/settings/TerminalSettingsSection.tsx index e17301895a..7e62bb54b6 100644 --- a/webview-ui/src/components/settings/TerminalSettingsSection.tsx +++ b/webview-ui/src/components/settings/TerminalSettingsSection.tsx @@ -1,11 +1,12 @@ import React, { useState } from "react" -import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { useExtensionState } from "@/context/ExtensionStateContext" import { StateServiceClient } from "@/services/grpc-client" import { Int64, Int64Request } from "@shared/proto/common" export const TerminalSettingsSection: React.FC = () => { - const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState() + const { shellIntegrationTimeout, setShellIntegrationTimeout, terminalReuseEnabled, setTerminalReuseEnabled } = + useExtensionState() const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString()) const [inputError, setInputError] = useState(null) @@ -48,6 +49,17 @@ export const TerminalSettingsSection: React.FC = () => { } } + const handleTerminalReuseChange = (event: Event) => { + const target = event.target as HTMLInputElement + const checked = target.checked + + // Update local state + setTerminalReuseEnabled(checked) + + // TODO: Send to extension using gRPC when the backend is ready + // For now, we'll just update the local state + } + return (
@@ -73,6 +85,20 @@ export const TerminalSettingsSection: React.FC = () => { you experience terminal connection timeouts.

+ +
+
+ handleTerminalReuseChange(event as Event)}> + Enable terminal reuse across directories + +
+

+ When enabled, Cline will reuse existing terminal windows by changing directories with 'cd' commands. Disable + this if you experience issues with terminal state or directory changes. +

+
) } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index b24be56c0a..ce3334a419 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -51,6 +51,7 @@ interface ExtensionStateContextType extends ExtensionState { setEnableCheckpointsSetting: (value: boolean) => void setMcpMarketplaceEnabled: (value: boolean) => void setShellIntegrationTimeout: (value: number) => void + setTerminalReuseEnabled: (value: boolean) => void setChatSettings: (value: ChatSettings) => void setMcpServers: (value: McpServer[]) => void setGlobalClineRulesToggles: (toggles: Record) => void @@ -171,6 +172,7 @@ export const ExtensionStateContextProvider: React.FC<{ localWorkflowToggles: {}, globalWorkflowToggles: {}, shellIntegrationTimeout: 4000, // default timeout for shell integration + terminalReuseEnabled: true, // default to enabled for backward compatibility isNewUser: false, }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -528,6 +530,11 @@ export const ExtensionStateContextProvider: React.FC<{ ...prevState, shellIntegrationTimeout: value, })), + setTerminalReuseEnabled: (value) => + setState((prevState) => ({ + ...prevState, + terminalReuseEnabled: value, + })), setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers), setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog), setShowMcp,