1
0
Эх сурвалжийг харах

fix(core): align shell output limits (#41007)

Aiden Cline 1 долоо хоног өмнө
parent
commit
2092350cfa

+ 13 - 5
packages/core/src/tool/plugin/shell.ts

@@ -6,6 +6,7 @@ import type { Content } from "@opencode-ai/schema/tool"
 import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
 import { Deferred, Effect, Schema, Scope } from "effect"
 import { FSUtil } from "@opencode-ai/util/fs-util"
+import { Config } from "../../config"
 import { LocationMutation } from "../../location-mutation"
 import { Permission } from "../../permission"
 import { PluginRuntime } from "../../plugin/runtime"
@@ -13,10 +14,10 @@ import { NonNegativeInt } from "../../schema"
 import { SessionSchema } from "../../session/schema"
 import { Shell } from "../../shell"
 import { ShellParse } from "../../shell/parse"
+import { ToolOutput } from "../../tool-output"
 
 export const name = "shell"
 export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000
-export const MAX_CAPTURE_BYTES = 1024 * 1024
 
 const BACKGROUND_STARTED = "The command was moved to the background."
 const BACKGROUND_INSTRUCTION =
@@ -86,6 +87,7 @@ export const Plugin = {
     const mutation = yield* LocationMutation.Service
     const shell = yield* Shell.Service
     const permission = yield* Permission.Service
+    const config = yield* Config.Service
 
     const notifyWhenDone = Effect.fn("ShellTool.notifyWhenDone")(function* (
       sessionID: SessionSchema.ID,
@@ -191,15 +193,21 @@ export const Plugin = {
               yield* context.progress({ shellID: info.id })
 
               const captureShell = Effect.fn("ShellTool.captureShell")(function* () {
+                const configured = Config.latest(yield* config.entries(), "tool_output")
+                const maxLines = configured?.max_lines ?? ToolOutput.MAX_LINES
+                const maxBytes = configured?.max_bytes ?? ToolOutput.MAX_BYTES
                 const latest = yield* shell.output(info.id, { cursor: Number.MAX_SAFE_INTEGER })
-                const truncated = latest.size > MAX_CAPTURE_BYTES
                 const page = yield* shell.output(info.id, {
-                  cursor: Math.max(0, latest.size - MAX_CAPTURE_BYTES),
-                  limit: MAX_CAPTURE_BYTES,
+                  cursor: Math.max(0, latest.size - maxBytes),
+                  limit: maxBytes,
                 })
+                const lines = page.output.split("\n")
+                if (page.output.endsWith("\n")) lines.pop()
+                const truncated = latest.size > maxBytes || lines.length > maxLines
+                const output = lines.length > maxLines ? lines.slice(-maxLines).join("\n") : page.output
                 const notice = truncated ? `\n\n[output truncated; full output saved to: ${info.file}]` : ""
                 return {
-                  output: `${page.output || "(no output)"}${notice}`,
+                  output: `${output || "(no output)"}${notice}`,
                   truncated,
                 }
               })

+ 33 - 2
packages/core/test/tool-shell.test.ts

@@ -30,6 +30,7 @@ import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
 import { Shell } from "@opencode-ai/core/shell"
 import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
 import { ShellTool } from "@opencode-ai/core/tool/plugin/shell"
+import { ToolOutput } from "@opencode-ai/core/tool-output"
 import { Tool } from "@opencode-ai/core/tool"
 import { tmpdir } from "./fixture/tmpdir"
 import { testEffect } from "./lib/effect"
@@ -171,6 +172,9 @@ const overflowCommand = (bytes: number) =>
   isWindows
     ? `[Console]::Out.Write('output-start' + ('x' * ${bytes}) + 'output-end'); Start-Sleep -Milliseconds 100`
     : `printf output-start; head -c ${bytes} /dev/zero | tr '\\0' 'x'; printf output-end`
+const lineOverflowCommand = isWindows
+  ? "[Console]::Out.Write('one' + [Environment]::NewLine + 'two' + [Environment]::NewLine + 'three')"
+  : "printf 'one\\ntwo\\nthree'"
 const progressOverflowCommand = (bytes: number, release: string) =>
   isWindows
     ? `[Console]::Out.Write(('x' * ${bytes})); while (!(Test-Path -LiteralPath '${release}')) { Start-Sleep -Milliseconds 50 }`
@@ -477,7 +481,7 @@ describe("ShellTool", () => {
       Effect.promise(() => tmpdir()),
       (tmp) => {
         reset()
-        const bytes = ShellTool.MAX_CAPTURE_BYTES + 1024
+        const bytes = ToolOutput.MAX_BYTES + 1024
         return withSession(tmp.path, (registry) =>
           executeTool(registry, call({ command: overflowCommand(bytes) }, "call-overflow")),
         ).pipe(
@@ -501,6 +505,33 @@ describe("ShellTool", () => {
     { timeout: 15_000 },
   )
 
+  it.live("uses configured line limits", () =>
+    Effect.acquireUseRelease(
+      Effect.promise(() => tmpdir()),
+      (tmp) => {
+        reset()
+        return Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(tmp.path, "opencode.json"),
+              JSON.stringify({ tool_output: { max_lines: 2, max_bytes: 1_000 } }),
+            ),
+          )
+          const settled = yield* withSession(tmp.path, (registry) =>
+            executeTool(registry, call({ command: lineOverflowCommand }, "call-line-overflow")),
+          )
+          expect(settled.metadata).toMatchObject({ exit: 0, truncated: true })
+          const content = settled.content?.[0]
+          if (!content || content.type !== "text") throw new Error("Expected text content")
+          expect(content.text).not.toContain("one")
+          expect(content.text).toStartWith("two\nthree")
+          expect(content.text).toContain("output truncated; full output saved to:")
+        })
+      },
+      (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
+    ),
+  )
+
   it.live(
     "reports the shell ID for a running command",
     () =>
@@ -515,7 +546,7 @@ describe("ShellTool", () => {
               const observed = yield* Deferred.make<string>()
               yield* executeTool(registry, {
                 ...call(
-                  { command: progressOverflowCommand(ShellTool.MAX_CAPTURE_BYTES + 1024, release) },
+                  { command: progressOverflowCommand(ToolOutput.MAX_BYTES + 1024, release) },
                   "call-progress",
                 ),
                 progress: (update) =>