diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index b936f54e4e..f107e5bff5 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -133,6 +133,8 @@ export const TuiCommand = cmd({ Log.Default.info("tui", { cmd, }) + // Collect logs in the background to prevent them from messing up the TUI + Log.setBackgroundMode(true) const proc = Bun.spawn({ cmd: [ ...cmd, @@ -178,6 +180,8 @@ export const TuiCommand = cmd({ })() await proc.exited + Log.setBackgroundMode(false) + Log.flushBackgroundLogs() server.stop() return "done" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 7018978e2e..db2d3c0c19 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1223,7 +1223,7 @@ export namespace SessionPrompt { } } } catch (e) { - log.error("process", { + log.opt({ important: false }).error("process", { error: e, }) const error = MessageV2.fromError(e, { providerID: input.providerID }) diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 463069562e..623d71637d 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -21,6 +21,7 @@ export namespace Log { } export type Logger = { + log(level: Level, message?: any, extra?: Record): void, debug(message?: any, extra?: Record): void info(message?: any, extra?: Record): void error(message?: any, extra?: Record): void @@ -34,6 +35,16 @@ export namespace Log { stop(): void [Symbol.dispose](): void } + /** Clone the logger with the specified options. */ + opt(options: LoggerOptions): Logger + } + + type LoggerOptions = { + /** + * If true, the logger will print to stderr even if printing to stderr was not explicitly enabled. + * When undefined, error messages will be printed to stderr by default. + */ + important?: boolean } const loggers = new Map() @@ -51,22 +62,23 @@ export namespace Log { return logpath } + let printToStderr = false + let logFileWriter: Bun.FileSink | null = null + export async function init(options: Options) { if (options.level) level = options.level cleanup(Global.Path.log) - if (options.print) return + if (options.print) { + printToStderr = true + return + } logpath = path.join( Global.Path.log, options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", ) const logfile = Bun.file(logpath) + logFileWriter = logfile.writer() await fs.truncate(logpath).catch(() => {}) - const writer = logfile.writer() - process.stderr.write = (msg) => { - writer.write(msg) - writer.flush() - return true - } } async function cleanup(dir: string) { @@ -120,26 +132,26 @@ export namespace Log { last = next.getTime() return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" } - const result: Logger = { - debug(message?: any, extra?: Record) { - if (shouldLog("DEBUG")) { - process.stderr.write("DEBUG " + build(message, extra)) + const result: Logger & { _options: LoggerOptions } = { + _options: { + important: undefined, + }, + log(level: Level, message?: any, extra?: Record) { + if (shouldLog(level)) { + write(level, level + " " + build(message, extra), this._options) } }, + debug(message?: any, extra?: Record) { + this.log("DEBUG", message, extra) + }, info(message?: any, extra?: Record) { - if (shouldLog("INFO")) { - process.stderr.write("INFO " + build(message, extra)) - } + this.log("INFO", message, extra) }, error(message?: any, extra?: Record) { - if (shouldLog("ERROR")) { - process.stderr.write("ERROR " + build(message, extra)) - } + this.log("ERROR", message, extra) }, warn(message?: any, extra?: Record) { - if (shouldLog("WARN")) { - process.stderr.write("WARN " + build(message, extra)) - } + this.log("WARN", message, extra) }, tag(key: string, value: string) { if (tags) tags[key] = value @@ -165,6 +177,11 @@ export namespace Log { }, } }, + opt(options: LoggerOptions) { + const logger = this.clone() as Logger & { _options: LoggerOptions } + logger._options = options + return logger + } } if (service && typeof service === "string") { @@ -173,4 +190,38 @@ export namespace Log { return result } + + let messageQueue: { level: Level, message: string }[] = [] + let backgroundMode = false + + function write(level: Level, message: string, options?: { ignoreFile?: boolean, important?: boolean }) { + const shouldWriteToFile = !options?.ignoreFile && !printToStderr && !!logFileWriter + const isImportant = options?.important ?? (levelPriority[level] >= levelPriority["ERROR"]) + const shouldWriteToStderr = printToStderr || isImportant + if (shouldWriteToFile) { + logFileWriter!.write(message) + logFileWriter!.flush() + } + if (shouldWriteToStderr) { + if (backgroundMode) messageQueue.push({ level, message }) + else process.stderr.write(message) + } + } + + /** + * Collect log messages in the background, to be flushed to stderr on-demand later. + */ + export function setBackgroundMode(value: boolean) { + backgroundMode = value + } + + /** + * Flush collected background log messages to stderr. + */ + export function flushBackgroundLogs() { + for (const entry of messageQueue) { + write(entry.level, entry.message, { ignoreFile: true }) + } + messageQueue = [] + } }