diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 9724726f..253683d2 100644 --- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx @@ -37,11 +37,14 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ // Reset window title before destroying renderer renderer.setTerminalTitle("") renderer.destroy() - // SGR reset + show cursor + OSC 110/111/112 reset terminal fg/bg/cursor color. - // Without the OSC resets, whatever fg/bg the active mimocode theme pushed - // via OSC 10/11/12 would persist in the terminal session, leaving the - // shell prompt unreadable (e.g. white-on-white). - process.stdout.write("\x1b[0m\x1b[?25h\x1b]110\x07\x1b]111\x07\x1b]112\x07") + // Disable mouse event tracking (X10/buttons/all-motion/SGR) + SGR reset + + // show cursor + OSC 110/111/112 reset terminal fg/bg/cursor color. + // Without mouse disable, abnormal exit (e.g. killed by signal) leaves the + // terminal in mouse-tracking mode, producing garbage [row;colM sequences on + // every mouse move. Without the OSC resets, whatever fg/bg the active + // mimocode theme pushed via OSC 10/11/12 would persist in the terminal + // session, leaving the shell prompt unreadable (e.g. white-on-white). + process.stdout.write("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[0m\x1b[?25h\x1b]110\x07\x1b]111\x07\x1b]112\x07") win32FlushInputBuffer() if (reason) { const formatted = FormatError(reason) ?? FormatUnknownError(reason) @@ -60,6 +63,9 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ }, ) process.on("SIGHUP", () => exit()) + process.on("SIGINT", () => exit()) + process.on("SIGTERM", () => exit()) + process.on("beforeExit", () => exit()) return exit }, }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 487554ab..72b7690f 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -168,6 +168,8 @@ export const TuiThreadCommand = cmd({ process.on("uncaughtException", error) process.on("unhandledRejection", error) process.on("SIGUSR2", reload) + process.on("SIGINT", () => stop()) + process.on("SIGTERM", () => stop()) let stopped = false const stop = async () => { diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 1970afdb..4fc609ce 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -19,6 +19,8 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" +// Best-effort: never let project-id bookkeeping crash startup (e.g. EEXIST/EPERM +// from mkdir on Windows network drives or read-only .git dirs). async function setupProjectIdEnvironment(workingDir: string): Promise { const mainGit = resolveMainGitDir(workingDir) if (!mainGit) return @@ -29,19 +31,19 @@ async function setupProjectIdEnvironment(workingDir: string): Promise { if (await Bun.file(localFile).exists()) { if (!(await Bun.file(idFile).exists())) { const id = await Bun.file(localFile).text() - await Bun.write(idFile, id) + await Bun.write(idFile, id).catch(() => {}) } await nodeFs.unlink(localFile).catch(() => {}) } // Belt-and-suspenders: ensure .git/info/exclude lists .mimocode-project-id const excludeFile = nodePath.join(mainGit, "info", "exclude") - await nodeFs.mkdir(nodePath.dirname(excludeFile), { recursive: true }) + await nodeFs.mkdir(nodePath.dirname(excludeFile), { recursive: true }).catch(() => {}) const existing = await Bun.file(excludeFile) .text() .catch(() => "") if (!existing.includes(".mimocode-project-id")) { - await nodeFs.appendFile(excludeFile, "\n.mimocode-project-id\n") + await nodeFs.appendFile(excludeFile, "\n.mimocode-project-id\n").catch(() => {}) } } diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 09675e27..44bd9721 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -15,6 +15,9 @@ const levelPriority: Record = { ERROR: 3, } const keep = 10 +// Cap individual log files so runaway logging can't fill the disk; with `keep` +// pruning this bounds the log directory to roughly keep * maxFileSize. +const maxFileSize = 20 * 1024 * 1024 let level: Level = "INFO" @@ -79,7 +82,21 @@ export async function init(options: Options) { await fs.truncate(logpath).catch(() => {}) } const stream = createWriteStream(logpath, { flags: "a" }) + let written = (await fs.stat(logpath).catch(() => null))?.size ?? 0 + let rotations = 0 + const rotate = () => { + stream.end() + rotations++ + const stamp = new Date().toISOString().split(".")[0].replace(/:/g, "") + // Counter suffix keeps the name unique even when rotating within a second + logpath = path.join(Global.Path.log, `${stamp}_${rotations}.log`) + stream = createWriteStream(logpath, { flags: "a" }) + written = 0 + void cleanup(Global.Path.log) + } write = async (msg: any) => { + if (!options.dev && written >= maxFileSize) rotate() + written += Buffer.byteLength(msg) return new Promise((resolve, reject) => { stream.write(msg, (err) => { if (err) reject(err) @@ -91,7 +108,7 @@ export async function init(options: Options) { async function cleanup(dir: string) { const files = ( - await Glob.scan("????-??-??T??????.log", { + await Glob.scan("????-??-??T??????*.log", { cwd: dir, absolute: false, include: "file",