From ba437069e6eb6f615456cda2038c9237f6161ee7 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 23 May 2026 10:43:39 -0500 Subject: [PATCH] fix(opencode): advertise configured shell timeout (#28998) Co-authored-by: Nabs --- packages/opencode/src/tool/shell.ts | 6 ++-- packages/opencode/src/tool/shell/prompt.ts | 36 ++++++++++++++-------- packages/opencode/test/tool/shell.test.ts | 13 +++++--- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 506d98466e..b6a95b5c09 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -340,7 +340,7 @@ export const ShellTool = Tool.define( const trunc = yield* Truncate.Service const plugin = yield* Plugin.Service const flags = yield* RuntimeFlags.Service - const defaultTimeout = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000 + const defaultTimeoutMs = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000 const cygpath = Effect.fn("ShellTool.cygpath")(function* (shell: string, text: string) { const lines = yield* spawner @@ -601,7 +601,7 @@ export const ShellTool = Tool.define( const shell = Shell.acceptable(cfg.shell) const name = Shell.name(shell) const limits = yield* trunc.limits() - const prompt = ShellPrompt.render(name, process.platform, limits) + const prompt = ShellPrompt.render(name, process.platform, limits, defaultTimeoutMs) log.info("shell tool using shell", { shell }) return { @@ -616,7 +616,7 @@ export const ShellTool = Tool.define( if (params.timeout !== undefined && params.timeout < 0) { throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) } - const timeout = params.timeout ?? defaultTimeout + const timeout = params.timeout ?? defaultTimeoutMs const ps = Shell.ps(shell) yield* Effect.scoped( Effect.gen(function* () { diff --git a/packages/opencode/src/tool/shell/prompt.ts b/packages/opencode/src/tool/shell/prompt.ts index f26e364b61..bec50d98d9 100644 --- a/packages/opencode/src/tool/shell/prompt.ts +++ b/packages/opencode/src/tool/shell/prompt.ts @@ -83,7 +83,7 @@ function chainGuidance(name: string) { 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." } -function bashCommandSection(chain: string, limits: Limits) { +function bashCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) { return `Before executing the command, please follow these steps: 1. Directory Verification: @@ -102,7 +102,7 @@ function bashCommandSection(chain: string, limits: Limits) { Usage notes: - The command argument is required. - - You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes). + - You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms. - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - 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. @@ -127,7 +127,13 @@ Usage notes: ` } -function powershellCommandSection(name: string, chain: string, pathSep: string, limits: Limits) { +function powershellCommandSection( + name: string, + chain: string, + pathSep: string, + limits: Limits, + defaultTimeoutMs: number, +) { return `${powershellNotes(name)} Before executing the command, please follow these steps: @@ -148,7 +154,7 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - - You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes). + - You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms. - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - 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. @@ -173,7 +179,7 @@ Usage notes: ` } -function cmdCommandSection(chain: string, limits: Limits) { +function cmdCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) { return `# cmd.exe shell notes - Use double quotes for paths with spaces. - Use %VAR% for environment variables. @@ -198,7 +204,7 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - - You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes). + - You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms. - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - 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. @@ -223,7 +229,7 @@ Usage notes: ` } -function profile(name: string, platform: NodeJS.Platform, limits: Limits) { +function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) { const isPowerShell = PS.has(name) const chain = chainGuidance(name) if (CMD.has(name)) { @@ -231,7 +237,7 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) { intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`, workdirSection: "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.", - commandSection: cmdCommandSection(chain, limits), + commandSection: cmdCommandSection(chain, limits, defaultTimeoutMs), gitCommands: "git commands", gitCommandRestriction: "git commands", 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) { intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`, workdirSection: "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.", - commandSection: powershellCommandSection(name, chain, platform === "win32" ? "\\" : "/", limits), + commandSection: powershellCommandSection( + name, + chain, + platform === "win32" ? "\\" : "/", + limits, + defaultTimeoutMs, + ), gitCommands: "git commands", gitCommandRestriction: "git commands", 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) { "Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.", workdirSection: "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 && ` patterns - use `workdir` instead.", - commandSection: bashCommandSection(chain, limits), + commandSection: bashCommandSection(chain, limits, defaultTimeoutMs), gitCommands: "bash commands", gitCommandRestriction: "git bash commands", createPrInstruction: @@ -272,8 +284,8 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) { } } -export function render(name: string, platform: NodeJS.Platform, limits: Limits) { - const selected = profile(name, platform, limits) +export function render(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) { + const selected = profile(name, platform, limits, defaultTimeoutMs) return { description: renderPrompt(DESCRIPTION, { intro: selected.intro, diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index fe4f5a4834..ddaa5c2ec7 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -1085,10 +1085,15 @@ describe("tool.shell abort", () => { runIn( projectRoot, Effect.gen(function* () { - const result = yield* run({ - command: `echo started && sleep 60`, - description: "Default timeout test", - }) + const tool = yield* initShell() + expect(tool.description).toContain("commands will time out after 500ms") + const result = yield* tool.execute( + { + command: `echo started && sleep 60`, + description: "Default timeout test", + }, + ctx, + ) expect(result.output).toContain("started") expect(result.output).toContain("exceeding timeout 500 ms") }),