Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions packages/opencode/src/cli/cmd/tui/context/exit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
},
})
2 changes: 2 additions & 0 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const mainGit = resolveMainGitDir(workingDir)
if (!mainGit) return
Expand All @@ -29,19 +31,19 @@ async function setupProjectIdEnvironment(workingDir: string): Promise<void> {
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(() => {})
}
}

Expand Down
19 changes: 18 additions & 1 deletion packages/opencode/src/util/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const levelPriority: Record<Level, number> = {
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"

Expand Down Expand Up @@ -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)
Expand All @@ -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",
Expand Down