Skip to content

Commit 19cc8bc

Browse files
celestial-vaultElephant Lumps
andauthored
Add terminal connection timeout (RooCodeInc#3218)
* add terminal connection timeout * changeset --------- Co-authored-by: Elephant Lumps <[email protected]>
1 parent 08c04a3 commit 19cc8bc

File tree

11 files changed

+156
-9
lines changed

11 files changed

+156
-9
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Add a timeout setting for the terminal connection, allowing users to adjust this if they are having timeout issues

src/core/controller/index.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,14 @@ export class Controller {
136136

137137
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
138138
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
139-
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
140-
await getAllExtensionState(this.context)
139+
const {
140+
apiConfiguration,
141+
customInstructions,
142+
autoApprovalSettings,
143+
browserSettings,
144+
chatSettings,
145+
shellIntegrationTimeout,
146+
} = await getAllExtensionState(this.context)
141147

142148
if (autoApprovalSettings) {
143149
const updatedAutoApprovalSettings = {
@@ -159,6 +165,7 @@ export class Controller {
159165
autoApprovalSettings,
160166
browserSettings,
161167
chatSettings,
168+
shellIntegrationTimeout,
162169
customInstructions,
163170
task,
164171
images,
@@ -784,6 +791,21 @@ export class Controller {
784791
}
785792
break
786793
}
794+
case "updateTerminalConnectionTimeout": {
795+
if (message.shellIntegrationTimeout !== undefined) {
796+
const timeout = message.shellIntegrationTimeout
797+
798+
if (typeof timeout === "number" && !isNaN(timeout) && timeout > 0) {
799+
await updateGlobalState(this.context, "shellIntegrationTimeout", timeout)
800+
await this.postStateToWebview()
801+
} else {
802+
console.warn(
803+
`Invalid shell integration timeout value received: ${timeout}. ` + `Expected a positive number.`,
804+
)
805+
}
806+
}
807+
break
808+
}
787809
// Add more switch case statements here as more webview message commands
788810
// are created within the webview context (i.e. inside media/main.js)
789811
}
@@ -1769,6 +1791,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
17691791
telemetrySetting,
17701792
planActSeparateModelsSetting,
17711793
globalClineRulesToggles,
1794+
shellIntegrationTimeout,
17721795
} = await getAllExtensionState(this.context)
17731796

17741797
const localClineRulesToggles =
@@ -1798,6 +1821,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
17981821
vscMachineId: vscode.env.machineId,
17991822
globalClineRulesToggles: globalClineRulesToggles || {},
18001823
localClineRulesToggles: localClineRulesToggles || {},
1824+
shellIntegrationTimeout,
18011825
}
18021826
}
18031827

src/core/storage/state-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ export type GlobalStateKey =
7676
| "planActSeparateModelsSetting"
7777
| "favoritedModelIds"
7878
| "requestTimeoutMs"
79+
| "shellIntegrationTimeout"
7980

8081
export type LocalStateKey = "localClineRulesToggles"

src/core/storage/state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
126126
favoritedModelIds,
127127
globalClineRulesToggles,
128128
requestTimeoutMs,
129+
shellIntegrationTimeout,
129130
] = await Promise.all([
130131
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
131132
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -200,6 +201,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
200201
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
201202
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
202203
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
204+
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
203205
])
204206

205207
let apiProvider: ApiProvider
@@ -319,6 +321,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
319321
mcpMarketplaceEnabled,
320322
telemetrySetting: telemetrySetting || "unset",
321323
planActSeparateModelsSetting,
324+
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
322325
}
323326
}
324327

src/core/task/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export class Task {
173173
autoApprovalSettings: AutoApprovalSettings,
174174
browserSettings: BrowserSettings,
175175
chatSettings: ChatSettings,
176+
shellIntegrationTimeout: number,
176177
customInstructions?: string,
177178
task?: string,
178179
images?: string[],
@@ -191,6 +192,7 @@ export class Task {
191192
console.error("Failed to initialize ClineIgnoreController:", error)
192193
})
193194
this.terminalManager = new TerminalManager()
195+
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
194196
this.urlContentFetcher = new UrlContentFetcher(context)
195197
this.browserSession = new BrowserSession(context, browserSettings)
196198
this.contextManager = new ContextManager()

src/integrations/terminal/TerminalManager.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const terminalManager = new TerminalManager(context);
3939
const process = terminalManager.runCommand('npm install', '/path/to/project');
4040
4141
process.on('line', (line) => {
42-
console.log(line);
42+
console.log(line);
4343
});
4444
4545
// To wait for the process to complete naturally:
@@ -93,6 +93,7 @@ export class TerminalManager {
9393
private terminalIds: Set<number> = new Set()
9494
private processes: Map<number, TerminalProcess> = new Map()
9595
private disposables: vscode.Disposable[] = []
96+
private shellIntegrationTimeout: number = 4000
9697

9798
constructor() {
9899
let disposable: vscode.Disposable | undefined
@@ -144,13 +145,30 @@ export class TerminalManager {
144145
process.run(terminalInfo.terminal, command)
145146
} else {
146147
// docs recommend waiting 3s for shell integration to activate
147-
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
148-
const existingProcess = this.processes.get(terminalInfo.id)
149-
if (existingProcess && existingProcess.waitForShellIntegration) {
150-
existingProcess.waitForShellIntegration = false
151-
existingProcess.run(terminalInfo.terminal, command)
152-
}
148+
console.log(
149+
`[TerminalManager Test] Waiting for shell integration for terminal ${terminalInfo.id} with timeout ${this.shellIntegrationTimeout}ms`,
150+
)
151+
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, {
152+
timeout: this.shellIntegrationTimeout,
153153
})
154+
.then(() => {
155+
console.log(
156+
`[TerminalManager Test] Shell integration activated for terminal ${terminalInfo.id} within timeout.`,
157+
)
158+
})
159+
.catch((err) => {
160+
console.warn(
161+
`[TerminalManager Test] Shell integration timed out or failed for terminal ${terminalInfo.id}: ${err.message}`,
162+
)
163+
})
164+
.finally(() => {
165+
console.log(`[TerminalManager Test] Proceeding with command execution for terminal ${terminalInfo.id}.`)
166+
const existingProcess = this.processes.get(terminalInfo.id)
167+
if (existingProcess && existingProcess.waitForShellIntegration) {
168+
existingProcess.waitForShellIntegration = false
169+
existingProcess.run(terminalInfo.terminal, command)
170+
}
171+
})
154172
}
155173

156174
return mergePromise(process, promise)
@@ -219,4 +237,8 @@ export class TerminalManager {
219237
this.disposables.forEach((disposable) => disposable.dispose())
220238
this.disposables = []
221239
}
240+
241+
setShellIntegrationTimeout(timeout: number): void {
242+
this.shellIntegrationTimeout = timeout
243+
}
222244
}

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export interface ExtensionState {
132132
shouldShowAnnouncement: boolean
133133
taskHistory: HistoryItem[]
134134
telemetrySetting: TelemetrySetting
135+
shellIntegrationTimeout: number
135136
uriScheme?: string
136137
userInfo?: {
137138
displayName: string | null

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface WebviewMessage {
7171
| "toggleClineRule"
7272
| "deleteClineRule"
7373
| "copyToClipboard"
74+
| "updateTerminalConnectionTimeout"
7475

7576
// | "relaunchChromeDebugMode"
7677
text?: string
@@ -121,6 +122,7 @@ export interface WebviewMessage {
121122
filename?: string
122123

123124
offset?: number
125+
shellIntegrationTimeout?: number
124126
}
125127

126128
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TabButton } from "../mcp/configuration/McpConfigurationView"
99
import { useEvent } from "react-use"
1010
import { ExtensionMessage } from "@shared/ExtensionMessage"
1111
import BrowserSettingsSection from "./BrowserSettingsSection"
12+
import TerminalSettingsSection from "./TerminalSettingsSection"
1213

1314
const { IS_DEV } = process.env
1415

@@ -240,6 +241,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
240241
{/* Browser Settings Section */}
241242
<BrowserSettingsSection />
242243

244+
{/* Terminal Settings Section */}
245+
<TerminalSettingsSection />
246+
243247
<div className="mt-auto pr-2 flex justify-center">
244248
<SettingsButton
245249
onClick={() => vscode.postMessage({ type: "openExtensionSettings" })}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React, { useState } from "react"
2+
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3+
import { useExtensionState } from "@/context/ExtensionStateContext"
4+
import { vscode } from "@/utils/vscode"
5+
6+
export const TerminalSettingsSection: React.FC = () => {
7+
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
8+
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
9+
const [inputError, setInputError] = useState<string | null>(null)
10+
11+
const handleTimeoutChange = (event: Event) => {
12+
const target = event.target as HTMLInputElement
13+
const value = target.value
14+
15+
setInputValue(value)
16+
17+
const seconds = parseFloat(value)
18+
if (isNaN(seconds) || seconds <= 0) {
19+
setInputError("Please enter a positive number")
20+
return
21+
}
22+
23+
setInputError(null)
24+
const timeout = Math.round(seconds * 1000) // Convert to milliseconds
25+
26+
// Update local state
27+
setShellIntegrationTimeout(timeout)
28+
29+
// Send to extension
30+
vscode.postMessage({
31+
type: "updateTerminalConnectionTimeout",
32+
shellIntegrationTimeout: timeout,
33+
})
34+
}
35+
36+
const handleInputBlur = () => {
37+
// If there was an error, reset the input to the current valid value
38+
if (inputError) {
39+
setInputValue((shellIntegrationTimeout / 1000).toString())
40+
setInputError(null)
41+
}
42+
}
43+
44+
return (
45+
<div
46+
id="terminal-settings-section"
47+
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
48+
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Terminal Settings</h3>
49+
<div style={{ marginBottom: 15 }}>
50+
<div style={{ marginBottom: 8 }}>
51+
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
52+
Shell integration timeout (seconds)
53+
</label>
54+
<div style={{ display: "flex", alignItems: "center" }}>
55+
<VSCodeTextField
56+
style={{ width: "100%" }}
57+
value={inputValue}
58+
placeholder="Enter timeout in seconds"
59+
onChange={(event) => handleTimeoutChange(event as Event)}
60+
onBlur={handleInputBlur}
61+
/>
62+
</div>
63+
{inputError && (
64+
<div style={{ color: "var(--vscode-errorForeground)", fontSize: "12px", marginTop: 5 }}>{inputError}</div>
65+
)}
66+
</div>
67+
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
68+
Set how long Cline waits for shell integration to activate before executing commands. Increase this value if
69+
you experience terminal connection timeouts.
70+
</p>
71+
</div>
72+
</div>
73+
)
74+
}
75+
76+
export default TerminalSettingsSection

0 commit comments

Comments
 (0)