Răsfoiți Sursa

fix(opencode): advertise configured shell timeout (#28998)

Co-authored-by: Nabs <nabil@instafork.com>
Aiden Cline 2 luni în urmă
părinte
comite
ba437069e6

+ 3 - 3
packages/opencode/src/tool/shell.ts

@@ -340,7 +340,7 @@ export const ShellTool = Tool.define(
     const trunc = yield* Truncate.Service
     const trunc = yield* Truncate.Service
     const plugin = yield* Plugin.Service
     const plugin = yield* Plugin.Service
     const flags = yield* RuntimeFlags.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 cygpath = Effect.fn("ShellTool.cygpath")(function* (shell: string, text: string) {
       const lines = yield* spawner
       const lines = yield* spawner
@@ -601,7 +601,7 @@ export const ShellTool = Tool.define(
         const shell = Shell.acceptable(cfg.shell)
         const shell = Shell.acceptable(cfg.shell)
         const name = Shell.name(shell)
         const name = Shell.name(shell)
         const limits = yield* trunc.limits()
         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 })
         log.info("shell tool using shell", { shell })
 
 
         return {
         return {
@@ -616,7 +616,7 @@ export const ShellTool = Tool.define(
               if (params.timeout !== undefined && params.timeout < 0) {
               if (params.timeout !== undefined && params.timeout < 0) {
                 throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
                 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)
               const ps = Shell.ps(shell)
               yield* Effect.scoped(
               yield* Effect.scoped(
                 Effect.gen(function* () {
                 Effect.gen(function* () {

+ 24 - 12
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."
   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:
   return `Before executing the command, please follow these steps:
 
 
 1. Directory Verification:
 1. Directory Verification:
@@ -102,7 +102,7 @@ function bashCommandSection(chain: string, limits: Limits) {
 
 
 Usage notes:
 Usage notes:
   - The command argument is required.
   - 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.
   - 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.
   - 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:
     </bad-example>`
     </bad-example>`
 }
 }
 
 
-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)}
   return `${powershellNotes(name)}
 
 
 Before executing the command, please follow these steps:
 Before executing the command, please follow these steps:
@@ -148,7 +154,7 @@ Before executing the command, please follow these steps:
 
 
 Usage notes:
 Usage notes:
   - The command argument is required.
   - 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.
   - 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.
   - 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:
     </bad-example>`
     </bad-example>`
 }
 }
 
 
-function cmdCommandSection(chain: string, limits: Limits) {
+function cmdCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) {
   return `# cmd.exe shell notes
   return `# cmd.exe shell notes
 - Use double quotes for paths with spaces.
 - Use double quotes for paths with spaces.
 - Use %VAR% for environment variables.
 - Use %VAR% for environment variables.
@@ -198,7 +204,7 @@ Before executing the command, please follow these steps:
 
 
 Usage notes:
 Usage notes:
   - The command argument is required.
   - 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.
   - 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.
   - 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:
     </bad-example>`
     </bad-example>`
 }
 }
 
 
-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 isPowerShell = PS.has(name)
   const chain = chainGuidance(name)
   const chain = chainGuidance(name)
   if (CMD.has(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.`,
       intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
       workdirSection:
       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.",
         "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",
       gitCommands: "git commands",
       gitCommandRestriction: "git commands",
       gitCommandRestriction: "git commands",
       createPrInstruction: "Create PR using a temporary body file so cmd.exe quoting stays simple.",
       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.`,
       intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
       workdirSection:
       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.",
         "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",
       gitCommands: "git commands",
       gitCommandRestriction: "git commands",
       gitCommandRestriction: "git commands",
       createPrInstruction: "Create PR using gh pr create with a PowerShell here-string to pass the body correctly.",
       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.",
       "Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.",
     workdirSection:
     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 <directory> && <command>` patterns - use `workdir` instead.",
       "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.",
-    commandSection: bashCommandSection(chain, limits),
+    commandSection: bashCommandSection(chain, limits, defaultTimeoutMs),
     gitCommands: "bash commands",
     gitCommands: "bash commands",
     gitCommandRestriction: "git bash commands",
     gitCommandRestriction: "git bash commands",
     createPrInstruction:
     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 {
   return {
     description: renderPrompt(DESCRIPTION, {
     description: renderPrompt(DESCRIPTION, {
       intro: selected.intro,
       intro: selected.intro,

+ 9 - 4
packages/opencode/test/tool/shell.test.ts

@@ -1085,10 +1085,15 @@ describe("tool.shell abort", () => {
       runIn(
       runIn(
         projectRoot,
         projectRoot,
         Effect.gen(function* () {
         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("started")
           expect(result.output).toContain("exceeding timeout 500 ms")
           expect(result.output).toContain("exceeding timeout 500 ms")
         }),
         }),