terminal setting for reusing terminal commands

This commit is contained in:
Cline Evaluation
2025-06-04 18:15:19 -07:00
parent 4f7dc6fb36
commit 719814fb76
10 changed files with 113 additions and 34 deletions
+13
View File
@@ -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,
}
}
+1
View File
@@ -91,6 +91,7 @@ export type GlobalStateKey =
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "terminalReuseEnabled"
| "isNewUser"
export type LocalStateKey = "localClineRulesToggles"
+3 -1
View File
@@ -165,6 +165,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
globalWorkflowToggles,
terminalReuseEnabled,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
@@ -255,7 +256,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
fetch,
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
])
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 || {},
}
}
+2
View File
@@ -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()
+37 -30
View File
@@ -94,6 +94,7 @@ export class TerminalManager {
private processes: Map<number, TerminalProcess> = 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<void>((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<void>((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<void>((_, 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<void>((_, 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
}
}
+1
View File
@@ -114,6 +114,7 @@ export interface ExtensionState {
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
shellIntegrationTimeout: number
terminalReuseEnabled?: boolean
uriScheme?: string
userInfo?: {
displayName: string | null
+1
View File
@@ -80,6 +80,7 @@ export interface WebviewMessage {
offset?: number
shellIntegrationTimeout?: number
terminalReuseEnabled?: boolean
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
@@ -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<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(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()
}
@@ -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<string | null>(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 (
<div id="terminal-settings-section" style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 15 }}>
@@ -73,6 +85,20 @@ export const TerminalSettingsSection: React.FC = () => {
you experience terminal connection timeouts.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
<VSCodeCheckbox
checked={terminalReuseEnabled ?? true}
onChange={(event) => handleTerminalReuseChange(event as Event)}>
Enable terminal reuse across directories
</VSCodeCheckbox>
</div>
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
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.
</p>
</div>
</div>
)
}
@@ -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<string, boolean>) => 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,