mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
fix(cli): normalize canonical keybind duplicates
This commit is contained in:
committed by
opencode-agent[bot]
parent
d7ccc7f9ad
commit
7399dca484
@@ -42,9 +42,8 @@ export const run = Effect.fn("cli.config.migrate")(function* (input: {
|
||||
const updated = Object.keys(keybinds).reduce((text, name) => {
|
||||
const target =
|
||||
TuiKeybind.CommandMap[name as keyof typeof TuiKeybind.CommandMap] ??
|
||||
(LegacyKeybindTargets.has(name) ? name : undefined)
|
||||
(name in Definitions || LegacyKeybindTargets.has(name) ? name : undefined)
|
||||
if (target === undefined) return text
|
||||
if (target === name && target in Definitions) return text
|
||||
const properties = findKeybindProperties(text, name)
|
||||
if (!properties.length) return text
|
||||
const remove = !(target in Definitions) || (target !== name && target in keybinds)
|
||||
@@ -54,6 +53,7 @@ export const run = Effect.fn("cli.config.migrate")(function* (input: {
|
||||
return property === undefined ? text : removeProperty(text, property)
|
||||
}, text)
|
||||
if (remove) return updated
|
||||
if (target === name) return updated
|
||||
const key = findKeybindProperties(updated, name)[0]?.children?.[0]
|
||||
if (key === undefined) return text
|
||||
return updated.slice(0, key.offset) + JSON.stringify(target) + updated.slice(key.offset + key.length)
|
||||
|
||||
@@ -262,7 +262,7 @@ test("preserves the effective value when migrating duplicate legacy keybinds", a
|
||||
}
|
||||
})
|
||||
|
||||
test("migrates the effective duplicate top-level keybinds", async () => {
|
||||
test("migrates and updates the effective duplicate top-level keybinds", async () => {
|
||||
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
|
||||
const file = path.join(directory, "cli.json")
|
||||
await Bun.write(file, `{"keybinds":{"session_delete":"first"},"keybinds":{"session_delete":"last"}}`)
|
||||
@@ -272,55 +272,20 @@ test("migrates the effective duplicate top-level keybinds", async () => {
|
||||
directory,
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Config.Service
|
||||
return yield* service.get()
|
||||
expect((yield* service.get()).keybinds).toEqual({ "session.delete": "last" })
|
||||
return yield* service.update((draft) => {
|
||||
draft.keybinds = { ...draft.keybinds, "session.delete": "changed" }
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
expect(config.keybinds).toEqual({ "session.delete": "last" })
|
||||
expect(parse(await Bun.file(file).text()).keybinds).toEqual({ "session.delete": "last" })
|
||||
expect(config.keybinds).toEqual({ "session.delete": "changed" })
|
||||
expect(parse(await Bun.file(file).text()).keybinds).toEqual({ "session.delete": "changed" })
|
||||
} finally {
|
||||
await Bun.$`rm -rf ${directory}`
|
||||
}
|
||||
})
|
||||
|
||||
test("does not overwrite a concurrent config update during migration", async () => {
|
||||
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
|
||||
const file = path.join(directory, "cli.json")
|
||||
const initial = `{"keybinds":{"session_delete":"ctrl+d"}}`
|
||||
await Bun.write(file, initial)
|
||||
const gated = await Effect.runPromise(gateMigrationWrite(file, initial).pipe(Effect.provide(NodeFileSystem.layer)))
|
||||
|
||||
try {
|
||||
const config = await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Config.Service
|
||||
const reading = yield* service.get().pipe(Effect.forkChild({ startImmediately: true }))
|
||||
expect(gated.state.writes).toBe(1)
|
||||
const updating = yield* service
|
||||
.update((draft) => {
|
||||
draft.mouse = false
|
||||
})
|
||||
.pipe(Effect.forkChild({ startImmediately: true }))
|
||||
expect(gated.state.writes).toBe(1)
|
||||
yield* gated.release.open
|
||||
yield* Fiber.join(reading)
|
||||
yield* Fiber.join(updating)
|
||||
return yield* service.get()
|
||||
}).pipe(
|
||||
Effect.provide(Config.layer),
|
||||
Effect.provide(Global.layerWith({ config: directory, state: directory })),
|
||||
Effect.provideService(FileSystem.FileSystem, gated.fs),
|
||||
),
|
||||
)
|
||||
|
||||
expect(config).toMatchObject({ keybinds: { "session.delete": "ctrl+d" }, mouse: false })
|
||||
expect(await Bun.file(file).json()).toMatchObject({ keybinds: { "session.delete": "ctrl+d" }, mouse: false })
|
||||
} finally {
|
||||
gated.release.openUnsafe()
|
||||
await Bun.$`rm -rf ${directory}`
|
||||
}
|
||||
})
|
||||
|
||||
test("does not overwrite a concurrent update from another config layer", async () => {
|
||||
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
|
||||
const file = path.join(directory, "cli.json")
|
||||
@@ -366,25 +331,31 @@ test("does not overwrite a concurrent update from another config layer", async (
|
||||
}
|
||||
})
|
||||
|
||||
test("updates the effective duplicate top-level keybinds", async () => {
|
||||
test("updates effective duplicate canonical keybinds", async () => {
|
||||
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
|
||||
const file = path.join(directory, "cli.json")
|
||||
await Bun.write(file, `{"keybinds":{"session_delete":"first"},"keybinds":{"session_delete":"last"}}`)
|
||||
await Bun.write(
|
||||
file,
|
||||
`{"keybinds":{"session.delete":"first","session.delete":"last","permission.mode":"off","permission.mode":"on"}}`,
|
||||
)
|
||||
|
||||
try {
|
||||
const config = await run(
|
||||
directory,
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Config.Service
|
||||
yield* service.get()
|
||||
expect((yield* service.get()).keybinds).toEqual({ "session.delete": "last", "permission.mode": "on" })
|
||||
return yield* service.update((draft) => {
|
||||
draft.keybinds = { ...draft.keybinds, "session.delete": "changed" }
|
||||
draft.keybinds = { ...draft.keybinds, "session.delete": "changed", "permission.mode": "changed" }
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
expect(config.keybinds).toEqual({ "session.delete": "changed" })
|
||||
expect(parse(await Bun.file(file).text()).keybinds).toEqual({ "session.delete": "changed" })
|
||||
expect(config.keybinds).toEqual({ "session.delete": "changed", "permission.mode": "changed" })
|
||||
expect(parse(await Bun.file(file).text()).keybinds).toEqual({
|
||||
"session.delete": "changed",
|
||||
"permission.mode": "changed",
|
||||
})
|
||||
} finally {
|
||||
await Bun.$`rm -rf ${directory}`
|
||||
}
|
||||
|
||||
@@ -243,7 +243,10 @@ async function renderDiffViewer(
|
||||
}
|
||||
|
||||
const app = await testRender(() => <Harness />, { width: 80, height: options.height ?? 20 })
|
||||
await waitForCommand(app, commands, "diff.close")
|
||||
await app.waitFor(async () => {
|
||||
await Bun.sleep(25)
|
||||
return commands.has("diff.close")
|
||||
})
|
||||
await app.waitFor(() => vcsDiffInput !== undefined)
|
||||
return {
|
||||
app,
|
||||
@@ -344,15 +347,3 @@ test("branch diff source requests branch VCS diff", async () => {
|
||||
viewer.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
async function waitForCommand(
|
||||
app: Awaited<ReturnType<typeof testRender>>,
|
||||
commands: Map<string, unknown>,
|
||||
command: string,
|
||||
) {
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
await app.renderOnce()
|
||||
if (commands.has(command)) return
|
||||
await new Promise((resolve) => setTimeout(resolve, 25))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,14 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
import type { Resolved } from "../../src/config"
|
||||
import { resolveMiniSettings, resolveModelInfo, resolveRunTuiConfig } from "../../src/mini/runtime.boot"
|
||||
import { catalogModel, catalogProvider } from "./fixture/catalog"
|
||||
import { createTuiResolvedConfig } from "../fixture/tui-runtime"
|
||||
|
||||
function config(input?: {
|
||||
leader?: string
|
||||
leaderTimeout?: number
|
||||
}): Resolved {
|
||||
return createTuiResolvedConfig({
|
||||
leader: input?.leaderTimeout === undefined ? undefined : { timeout: input.leaderTimeout },
|
||||
keybinds: input?.leader ? { leader: input.leader } : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
describe("run runtime boot", () => {
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
test("returns supplied resolved config", async () => {
|
||||
const input = config({ leader: "ctrl+g" })
|
||||
|
||||
const result = await resolveRunTuiConfig(input)
|
||||
|
||||
expect(result).toBe(input)
|
||||
})
|
||||
|
||||
test("falls back to default tui keymap config when config load fails", async () => {
|
||||
const result = await resolveRunTuiConfig(Promise.reject(new Error("boom")))
|
||||
|
||||
@@ -44,12 +25,6 @@ describe("run runtime boot", () => {
|
||||
expect(result.keybinds.get("prompt.queue")?.[0]?.key).toBe("alt+return")
|
||||
})
|
||||
|
||||
test("preserves disabled leader from resolved tui config", async () => {
|
||||
const result = await resolveRunTuiConfig(config({ leader: "none" }))
|
||||
|
||||
expect(result.keybinds.get("leader")).toEqual([])
|
||||
})
|
||||
|
||||
test("preserves shared config while resolving independent Mini defaults", async () => {
|
||||
const result = await resolveRunTuiConfig(
|
||||
createTuiResolvedConfig({
|
||||
|
||||
Reference in New Issue
Block a user