Skip to content

Commit ba43706

Browse files
fix(opencode): advertise configured shell timeout (#28998)
Co-authored-by: Nabs <nabil@instafork.com>
1 parent 387c5a0 commit ba43706

3 files changed

Lines changed: 36 additions & 19 deletions

File tree

packages/opencode/src/tool/shell.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ export const ShellTool = Tool.define(
340340
const trunc = yield* Truncate.Service
341341
const plugin = yield* Plugin.Service
342342
const flags = yield* RuntimeFlags.Service
343-
const defaultTimeout = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000
343+
const defaultTimeoutMs = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000
344344

345345
const cygpath = Effect.fn("ShellTool.cygpath")(function* (shell: string, text: string) {
346346
const lines = yield* spawner
@@ -601,7 +601,7 @@ export const ShellTool = Tool.define(
601601
const shell = Shell.acceptable(cfg.shell)
602602
const name = Shell.name(shell)
603603
const limits = yield* trunc.limits()
604-
const prompt = ShellPrompt.render(name, process.platform, limits)
604+
const prompt = ShellPrompt.render(name, process.platform, limits, defaultTimeoutMs)
605605
log.info("shell tool using shell", { shell })
606606

607607
return {
@@ -616,7 +616,7 @@ export const ShellTool = Tool.define(
616616
if (params.timeout !== undefined && params.timeout < 0) {
617617
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
618618
}
619-
const timeout = params.timeout ?? defaultTimeout
619+
const timeout = params.timeout ?? defaultTimeoutMs
620620
const ps = Shell.ps(shell)
621621
yield* Effect.scoped(
622622
Effect.gen(function* () {

packages/opencode/src/tool/shell/prompt.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function chainGuidance(name: string) {
8383
return "If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead."
8484
}
8585

86-
function bashCommandSection(chain: string, limits: Limits) {
86+
function bashCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) {
8787
return `Before executing the command, please follow these steps:
8888
8989
1. Directory Verification:
@@ -102,7 +102,7 @@ function bashCommandSection(chain: string, limits: Limits) {
102102
103103
Usage notes:
104104
- The command argument is required.
105-
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
105+
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
106106
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
107107
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`head\`, \`tail\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
108108
@@ -127,7 +127,13 @@ Usage notes:
127127
</bad-example>`
128128
}
129129

130-
function powershellCommandSection(name: string, chain: string, pathSep: string, limits: Limits) {
130+
function powershellCommandSection(
131+
name: string,
132+
chain: string,
133+
pathSep: string,
134+
limits: Limits,
135+
defaultTimeoutMs: number,
136+
) {
131137
return `${powershellNotes(name)}
132138
133139
Before executing the command, please follow these steps:
@@ -148,7 +154,7 @@ Before executing the command, please follow these steps:
148154
149155
Usage notes:
150156
- The command argument is required.
151-
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
157+
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
152158
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
153159
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`Select-Object -First\`, \`Select-Object -Last\`, or other truncation commands to limit output; the full output will already be captured to a file for more precise searching.
154160
@@ -173,7 +179,7 @@ Usage notes:
173179
</bad-example>`
174180
}
175181

176-
function cmdCommandSection(chain: string, limits: Limits) {
182+
function cmdCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) {
177183
return `# cmd.exe shell notes
178184
- Use double quotes for paths with spaces.
179185
- Use %VAR% for environment variables.
@@ -198,7 +204,7 @@ Before executing the command, please follow these steps:
198204
199205
Usage notes:
200206
- The command argument is required.
201-
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
207+
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
202208
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
203209
- If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use \`more\` or other pagination commands to limit output; the full output will already be captured to a file for more precise searching.
204210
@@ -223,15 +229,15 @@ Usage notes:
223229
</bad-example>`
224230
}
225231

226-
function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
232+
function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) {
227233
const isPowerShell = PS.has(name)
228234
const chain = chainGuidance(name)
229235
if (CMD.has(name)) {
230236
return {
231237
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
232238
workdirSection:
233239
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
234-
commandSection: cmdCommandSection(chain, limits),
240+
commandSection: cmdCommandSection(chain, limits, defaultTimeoutMs),
235241
gitCommands: "git commands",
236242
gitCommandRestriction: "git commands",
237243
createPrInstruction: "Create PR using a temporary body file so cmd.exe quoting stays simple.",
@@ -244,7 +250,13 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
244250
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
245251
workdirSection:
246252
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
247-
commandSection: powershellCommandSection(name, chain, platform === "win32" ? "\\" : "/", limits),
253+
commandSection: powershellCommandSection(
254+
name,
255+
chain,
256+
platform === "win32" ? "\\" : "/",
257+
limits,
258+
defaultTimeoutMs,
259+
),
248260
gitCommands: "git commands",
249261
gitCommandRestriction: "git commands",
250262
createPrInstruction: "Create PR using gh pr create with a PowerShell here-string to pass the body correctly.",
@@ -260,7 +272,7 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
260272
"Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.",
261273
workdirSection:
262274
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd <directory> && <command>` patterns - use `workdir` instead.",
263-
commandSection: bashCommandSection(chain, limits),
275+
commandSection: bashCommandSection(chain, limits, defaultTimeoutMs),
264276
gitCommands: "bash commands",
265277
gitCommandRestriction: "git bash commands",
266278
createPrInstruction:
@@ -272,8 +284,8 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
272284
}
273285
}
274286

275-
export function render(name: string, platform: NodeJS.Platform, limits: Limits) {
276-
const selected = profile(name, platform, limits)
287+
export function render(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) {
288+
const selected = profile(name, platform, limits, defaultTimeoutMs)
277289
return {
278290
description: renderPrompt(DESCRIPTION, {
279291
intro: selected.intro,

packages/opencode/test/tool/shell.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,10 +1085,15 @@ describe("tool.shell abort", () => {
10851085
runIn(
10861086
projectRoot,
10871087
Effect.gen(function* () {
1088-
const result = yield* run({
1089-
command: `echo started && sleep 60`,
1090-
description: "Default timeout test",
1091-
})
1088+
const tool = yield* initShell()
1089+
expect(tool.description).toContain("commands will time out after 500ms")
1090+
const result = yield* tool.execute(
1091+
{
1092+
command: `echo started && sleep 60`,
1093+
description: "Default timeout test",
1094+
},
1095+
ctx,
1096+
)
10921097
expect(result.output).toContain("started")
10931098
expect(result.output).toContain("exceeding timeout 500 ms")
10941099
}),

0 commit comments

Comments
 (0)