diff --git a/packages/opencode/src/effect/cross-spawn-spawner.ts b/packages/opencode/src/effect/cross-spawn-spawner.ts index 5e25263a08bb..951ba6a32519 100644 --- a/packages/opencode/src/effect/cross-spawn-spawner.ts +++ b/packages/opencode/src/effect/cross-spawn-spawner.ts @@ -268,19 +268,34 @@ export const make = Effect.gen(function* () { const proc = launch(command.command, command.args, opts) let end = false let exit: readonly [code: number | null, signal: NodeJS.Signals | null] | undefined + let spawned = false proc.on("error", (err) => { - resume(Effect.fail(toPlatformError("spawn", err, command))) + if (!spawned) { + spawned = true + resume(Effect.fail(toPlatformError("spawn", err, command))) + } }) proc.on("exit", (...args) => { + if (!spawned) { + spawned = true + resume(Effect.succeed([proc, signal])) + } exit = args }) proc.on("close", (...args) => { + if (!spawned) { + spawned = true + resume(Effect.succeed([proc, signal])) + } if (end) return end = true Deferred.doneUnsafe(signal, Exit.succeed(exit ?? args)) }) proc.on("spawn", () => { - resume(Effect.succeed([proc, signal])) + if (!spawned) { + spawned = true + resume(Effect.succeed([proc, signal])) + } }) return Effect.sync(() => { proc.kill("SIGTERM") diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 150cafbd761a..c39390d03016 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -16,7 +16,7 @@ import { Shell } from "@/shell/shell" import { BashArity } from "@/permission/arity" import { Truncate } from "./truncate" import { Plugin } from "@/plugin" -import { Effect, Stream } from "effect" +import { Effect, Fiber, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" @@ -384,6 +384,7 @@ export const BashTool = Tool.define( let output = "" let expired = false let aborted = false + let last = 0 yield* ctx.metadata({ metadata: { @@ -396,16 +397,20 @@ export const BashTool = Tool.define( Effect.gen(function* () { const handle = yield* spawner.spawn(cmd(input.shell, input.name, input.command, input.cwd, input.env)) - yield* Effect.forkScoped( - Stream.runForEach(Stream.decodeText(handle.all), (chunk) => { - output += chunk - return ctx.metadata({ - metadata: { - output: preview(output), - description: input.description, - }, - }) - }), + const streamFiber = yield* Effect.forkScoped( + Stream.runForEach(Stream.decodeText(handle.all), (chunk) => + Effect.gen(function* () { + output += chunk + if (Date.now() - last <= 250) return + last = Date.now() + yield* ctx.metadata({ + metadata: { + output: preview(output), + description: input.description, + }, + }) + }), + ), ) const abort = Effect.callback((resume) => { @@ -431,6 +436,15 @@ export const BashTool = Tool.define( expired = true yield* handle.kill({ forceKillAfter: "3 seconds" }).pipe(Effect.orDie) } + if (exit.kind === "exit") { + yield* Fiber.join(streamFiber).pipe(Effect.ignore) + yield* ctx.metadata({ + metadata: { + output: preview(output), + description: input.description, + }, + }) + } return exit.kind === "exit" ? exit.code : null }), diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 839c066c6ca6..c6abee1dfce4 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -1077,7 +1077,7 @@ describe("tool.bash abort", () => { const result = await Effect.runPromise( bash.execute( { - command: `echo first && sleep 0.1 && echo second`, + command: `echo first && sleep 0.3 && echo second`, description: "Streaming test", }, { diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index f7daa1e971c3..8bd85ea16be8 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -148,7 +148,8 @@ describe("tool.write", () => { if (process.platform !== "win32") { const stats = yield* Effect.promise(() => fs.stat(filepath)) - expect(stats.mode & 0o777).toBe(0o644) + const mode = stats.mode & 0o777 + expect(mode === 0o644 || mode === 0o664).toBe(true) } }), ),