diff --git a/bun.lock b/bun.lock index 2a79552b9e..dc715b527a 100644 --- a/bun.lock +++ b/bun.lock @@ -536,9 +536,9 @@ "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.2.8", - "@opentui/keymap": ">=0.2.8", - "@opentui/solid": ">=0.2.8", + "@opentui/core": ">=0.2.9", + "@opentui/keymap": ">=0.2.9", + "@opentui/solid": ">=0.2.9", }, "optionalPeers": [ "@opentui/core", @@ -721,9 +721,9 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.2.8", - "@opentui/keymap": "0.2.8", - "@opentui/solid": "0.2.8", + "@opentui/core": "0.2.9", + "@opentui/keymap": "0.2.9", + "@opentui/solid": "0.2.9", "@pierre/diffs": "1.1.0-beta.18", "@playwright/test": "1.59.1", "@sentry/solid": "10.36.0", @@ -1590,23 +1590,23 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.2.8", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.8", "@opentui/core-darwin-x64": "0.2.8", "@opentui/core-linux-arm64": "0.2.8", "@opentui/core-linux-x64": "0.2.8", "@opentui/core-win32-arm64": "0.2.8", "@opentui/core-win32-x64": "0.2.8" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-bRRiCXuwjS8/6mN1oA5iVaf55z9APyalm7FnoxkLkEyIU1VDaQeTpYtElBbfo1rxtcO6Rj53XywH9oW8auNO9A=="], + "@opentui/core": ["@opentui/core@0.2.9", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.9", "@opentui/core-darwin-x64": "0.2.9", "@opentui/core-linux-arm64": "0.2.9", "@opentui/core-linux-x64": "0.2.9", "@opentui/core-win32-arm64": "0.2.9", "@opentui/core-win32-x64": "0.2.9" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Kmeqi+yiDau+P45xDeX08GS50FK917qVwuPTN7HGxsQ9Byt7Iifq/6OMiSnFULBzoZtECdKLgQF1XwLsNm1wig=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Qh6VCMQgW3hWh/7MR51y+XuQezh8NOLwKS8EQSoKzAr4VOc/W5P0/DvgMKgwaqXw2Mz0AIba/BvZ6by20yc4zA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-D2ne8Xgyrg71L/9lF7vPh30Sxz6+3yAqpT0m87WiI+040J7sQEyK3YM/7w5JKuVemQ4H54HSPjofrUHjfibjoQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-wQjJ38C3IiVx/gwwBYxnCarzgD75FdS7IyUErt3lhn57XriNiCbb7ScphWnRMwwtL8CI+bBGzClroDRA2lCfvg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ymbbt/wN/vgB8g+kbHospJclVKHq6cdgfEYg9qgsSHp2vqMFBqlQQ692MS3BcZfX9jrKROK7NvC6Hj37X5K/7Q=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-fx4ADeWSSSVU1O/MkMnklCRxtWRy6CLeAvktLlNdPb+BhmQIDg1kpZcdv7m/3cgD1/ksFEXIwO6VTvfKYE0umw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-0RIrVe4+42oELHtSJBaaYhngUeMKwSeqfdtKeSwEFwCzrqrNXxCpXQdOo8QvjOKGgng4Smn6O6KM8sgCj4SSPQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-4ekUyzopBj2ClsUbneLnUOrmZtvU67FCVFLgmBfKL4IvVl/P0YobGNg71gN1JNiYpY7hK77qOpidVLHcNMIE7w=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-fjCZP1IOLWm68FYl2PRzFg1vfu226FPfiJsdNtLbhaYF2uEZOB/v1BQph21OKnB7GC7X8GQatvhM5sS3DQ2MSQ=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-63K046wpzTzQOLOG9LTsp3+Ld0TNTxeQczexkg0pKSBxZFhws+/9YIGjTctZmJUfE1g1X4tI31dO+KNRpXRHQw=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-u8SP3u2QEJqcGIULYZ7Lkht9ss7wcN4/LnMuqt9rPOiCduFn/VW4r8lQCftZ6DRSqyoP9mJ1xLzOSFl98UYyEw=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-+WDiTlTyDpgkis8rPAhW1fS7TwXJih+fk+RYXS2bC3tAKsRD+O3PRSkVABRbjkuXbtfJZf2cjOHZFGN4Vf5qDg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.9", "", { "os": "win32", "cpu": "x64" }, "sha512-un7iSy9XHLwa6ouVpUj3eEGnXfPG50OMUJ2Dt30Jvn2vhNwIU2VO4RGx06l5OUD6GGVpHb0RqmG/384oo9i+HA=="], - "@opentui/keymap": ["@opentui/keymap@0.2.8", "", { "dependencies": { "@opentui/core": "0.2.8" }, "peerDependencies": { "@opentui/react": "0.2.8", "@opentui/solid": "0.2.8", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-/H9j8fP64cf3/nFDCvVP8+7cwU/oRh4sgfQH2NhcPp8illgBb/e9pG5x3vM0nK4RVyTqUvkPXsOeIX5u7vltlg=="], + "@opentui/keymap": ["@opentui/keymap@0.2.9", "", { "dependencies": { "@opentui/core": "0.2.9" }, "peerDependencies": { "@opentui/react": "0.2.9", "@opentui/solid": "0.2.9", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-yCc6L0Jqa8aVaNAVniTV5bNygJayUE6mxWfaBQY5VV5QwsZemXSeQQc4vP2eetH4Rrm1gGA59gLP+zh6+s5fvw=="], - "@opentui/solid": ["@opentui/solid@0.2.8", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.8", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-f2g0riBuzk4/ZmcJnp1k13odUmNZcfA3nF7RzdSlEfpkwNDfc4xqnRAwYbNNDwGNrJX0JDCTEZY5ZEhuL155MQ=="], + "@opentui/solid": ["@opentui/solid@0.2.9", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.9", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-qpNSCELxRvBAx8Zneqz46FYYTvJNFjDvhqzAAZRNoaHathfU6X6iPxWMUqP/9ls5VcHFW1TDJdgtpsq1N/nHMQ=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/package.json b/package.json index f1cc7da5c3..50406e1f4a 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.2.8", - "@opentui/keymap": "0.2.8", - "@opentui/solid": "0.2.8", + "@opentui/core": "0.2.9", + "@opentui/keymap": "0.2.9", + "@opentui/solid": "0.2.9", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", diff --git a/packages/core/src/util/log.ts b/packages/core/src/util/log.ts index 83060b29c6..3b5249cdc3 100644 --- a/packages/core/src/util/log.ts +++ b/packages/core/src/util/log.ts @@ -20,6 +20,7 @@ const levelPriority: Record = { ERROR: 3, } const keep = 10 +const initializedRunID = "OPENCODE_LOG_INITIALIZED_RUN_ID" let level: Level = "INFO" @@ -70,7 +71,10 @@ export async function init(options: Options) { Global.Path.log, options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", ) - await fs.truncate(logpath).catch(() => {}) + const runID = process.env.OPENCODE_RUN_ID + const shouldTruncate = !options.dev || !runID || process.env[initializedRunID] !== runID + if (shouldTruncate) await fs.truncate(logpath).catch(() => {}) + if (options.dev && runID) process.env[initializedRunID] = runID const stream = createWriteStream(logpath, { flags: "a" }) write = async (msg: any) => { return new Promise((resolve, reject) => { diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 562b369db1..0d4be41dfc 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -3,9 +3,8 @@ export * as TuiConfig from "./tui" import path from "path" import { createBindingLookup } from "@opentui/keymap/extras" import { mergeDeep, unique } from "remeda" -import { Context, Effect, Fiber, Layer, Schema } from "effect" +import { Cause, Context, Effect, Fiber, Layer, Schema } from "effect" import { ConfigParse } from "@/config/parse" -import { InvalidError } from "@/config/error" import * as ConfigPaths from "@/config/paths" import { migrateTuiConfig } from "./tui-migrate" import { KeymapLeaderTimeoutDefault, resolveAttentionSoundPaths, TuiInfo } from "./tui-schema" @@ -24,6 +23,7 @@ import { ConfigVariable } from "@/config/variable" import { Npm } from "@opencode-ai/core/npm" import type { DeepMutable } from "@opencode-ai/core/schema" import type { TuiAttentionSoundName } from "@opencode-ai/plugin/tui" +import { FormatError, FormatUnknownError } from "@/cli/error" const log = Log.create({ service: "tui.config" }) @@ -79,8 +79,26 @@ function normalize(raw: Record) { } } +function dropUnknownKeybinds(input: Record, configFilepath: string) { + if (!isRecord(input.keybinds)) return input + + const invalid = TuiKeybind.unknownKeys(input.keybinds) + if (!invalid.length) return input + + log.warn("ignored unknown tui keybinds", { + path: configFilepath, + keybinds: invalid, + hint: "Remove these entries or rename them to keys from the tui.json schema.", + }) + return { + ...input, + keybinds: Object.fromEntries(Object.entries(input.keybinds).filter(([key]) => !invalid.includes(key))), + } +} + const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) { const afs = yield* AppFileSystem.Service + let appliedOrder = 0 const resolvePlugins = (config: Info, configFilepath: string): Effect.Effect => Effect.gen(function* () { @@ -101,16 +119,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: if (!isRecord(data)) return {} as Info // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json // (mirroring the old opencode.json shape) still get their settings applied. - const normalized = normalize(data) - if (isRecord(normalized.keybinds)) { - const invalid = TuiKeybind.unknownKeys(normalized.keybinds) - if (invalid.length) { - throw new InvalidError({ - path: configFilepath, - message: `Unrecognized keybind${invalid.length === 1 ? "" : "s"}: ${invalid.join(", ")}`, - }) - } - } + const normalized = dropUnknownKeybinds(normalize(data), configFilepath) const parsed = ConfigParse.schema(Info, normalized, configFilepath) const validated = parsed.attention?.sounds ? { @@ -127,7 +136,12 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: // can sync-throw — those become defects, which orElseSucceed wouldn't catch. Effect.catchCause((cause) => Effect.sync(() => { - log.warn("invalid tui config", { path: configFilepath, cause }) + const error = Cause.squash(cause) + const reason = FormatError(error) ?? FormatUnknownError(error) + log.warn("skipping invalid tui config", { + path: configFilepath, + reason, + }) return {} as Info }), ), @@ -141,18 +155,28 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: const text = yield* afs.readFileStringSafe(filepath).pipe( Effect.catchCause((cause) => Effect.sync(() => { - log.warn("failed to read tui config", { path: filepath, cause }) + const error = Cause.squash(cause) + const reason = FormatError(error) ?? FormatUnknownError(error) + log.warn("failed to read tui config", { + path: filepath, + reason, + }) return undefined }), ), ) if (!text) return {} as Info + log.info("loading tui config", { path: filepath }) return yield* load(text, filepath) }) const mergeFile = (acc: Acc, file: string) => Effect.gen(function* () { const data = yield* loadFile(file) + if (Object.keys(data).length) { + appliedOrder += 1 + log.info("applying tui config", { path: file, order: appliedOrder }) + } acc.result = mergeDeep(acc.result, data) if (!data.plugin?.length) return diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index 30c5a65fbd..4eb96b9576 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -439,6 +439,25 @@ it.instance("merges keybind overrides across precedence layers", () => ), ) +it.instance("ignores unknown keybind names without dropping valid overrides from the same file", () => + withCleanState( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const test = yield* TestInstance + yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { + keybinds: { + session_delete: "ctrl+d", + not_a_real_keybind: "ctrl+q", + }, + }) + + const config = yield* getTuiConfig(test.directory) + expect(config.keybinds.get("session.delete")?.[0]?.key).toBe("ctrl+d") + expect(config.keybinds.get("not_a_real_keybind")).toEqual([]) + }), + ), +) + it.instance("resolves keybind lookup from canonical keybinds", () => withCleanState( Effect.gen(function* () { diff --git a/packages/opencode/test/util/log.test.ts b/packages/opencode/test/util/log.test.ts index defd8c981e..62dc1d61c2 100644 --- a/packages/opencode/test/util/log.test.ts +++ b/packages/opencode/test/util/log.test.ts @@ -47,3 +47,31 @@ it.live("init cleanup keeps the newest timestamped logs", () => expect(next).toContain(list.at(-1)!) }), ) + +it.live("local dev log is not truncated twice for the same run", () => + Effect.gen(function* () { + const log = Global.Path.log + const runID = process.env.OPENCODE_RUN_ID + const initialized = process.env.OPENCODE_LOG_INITIALIZED_RUN_ID + yield* Effect.addFinalizer(() => + Effect.sync(() => { + Global.Path.log = log + if (runID === undefined) delete process.env.OPENCODE_RUN_ID + else process.env.OPENCODE_RUN_ID = runID + if (initialized === undefined) delete process.env.OPENCODE_LOG_INITIALIZED_RUN_ID + else process.env.OPENCODE_LOG_INITIALIZED_RUN_ID = initialized + }), + ) + + const dir = yield* tmpdirScoped() + Global.Path.log = dir + process.env.OPENCODE_RUN_ID = "run-1" + delete process.env.OPENCODE_LOG_INITIALIZED_RUN_ID + + yield* Effect.promise(() => Log.init({ print: false, dev: true })) + yield* Effect.promise(() => fs.writeFile(path.join(dir, "dev.log"), "main startup\n")) + yield* Effect.promise(() => Log.init({ print: false, dev: true })) + + expect(yield* Effect.promise(() => fs.readFile(path.join(dir, "dev.log"), "utf8"))).toContain("main startup") + }), +) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index baff4cddba..82fdc0ab83 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -22,9 +22,9 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.2.8", - "@opentui/keymap": ">=0.2.8", - "@opentui/solid": ">=0.2.8" + "@opentui/core": ">=0.2.9", + "@opentui/keymap": ">=0.2.9", + "@opentui/solid": ">=0.2.9" }, "peerDependenciesMeta": { "@opentui/core": {