mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-15 17:13:12 +00:00
fix keymap fallback priority and TUI config diagnostics (#27384)
This commit is contained in:
30
bun.lock
30
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=="],
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -20,6 +20,7 @@ const levelPriority: Record<Level, number> = {
|
||||
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) => {
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
}
|
||||
}
|
||||
|
||||
function dropUnknownKeybinds(input: Record<string, unknown>, 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<Info> =>
|
||||
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
|
||||
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user