mirror of
https://github.com/cline/cline.git
synced 2026-09-17 17:45:33 +08:00
terminal setting for reusing terminal commands
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ export type GlobalStateKey =
|
||||
| "favoritedModelIds"
|
||||
| "requestTimeoutMs"
|
||||
| "shellIntegrationTimeout"
|
||||
| "terminalReuseEnabled"
|
||||
| "isNewUser"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles"
|
||||
|
||||
@@ -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 || {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ export interface ExtensionState {
|
||||
taskHistory: HistoryItem[]
|
||||
telemetrySetting: TelemetrySetting
|
||||
shellIntegrationTimeout: number
|
||||
terminalReuseEnabled?: boolean
|
||||
uriScheme?: string
|
||||
userInfo?: {
|
||||
displayName: string | null
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user