Compare commits

..
Author SHA1 Message Date
Kit Langton 2b9b3d8b0b chore(tui): add diff close help changeset 2026-08-27 16:01:46 -04:00
Kit Langton 80e5c0cba8 fix(tui): show configured diff close shortcut 2026-08-27 15:19:59 -04:00
6 changed files with 58 additions and 54 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@opencode-ai/tui": patch
---
Show the configured close shortcut in the diff viewer help dialog.
-5
View File
@@ -1,5 +0,0 @@
---
"@opencode-ai/core": patch
---
Preserve reference insertion order when later config documents override an existing reference.
+3 -1
View File
@@ -20,13 +20,14 @@ export const Plugin = define({
const global = yield* Global.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.reference.reload())
yield* ctx.reference.transform((draft) => {
const entries = new Map<string, Reference.Source>()
for (const doc of loaded.entries.filter((entry): entry is Document => entry.type === "document")) {
const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue
const description = typeof entry === "string" ? undefined : entry.description
const hidden = typeof entry === "string" ? undefined : entry.hidden
draft.add(
entries.set(
name,
local(entry)
? Reference.LocalSource.make({
@@ -47,6 +48,7 @@ export const Plugin = define({
)
}
}
for (const [name, source] of entries) draft.add(name, source)
})
}),
})
-43
View File
@@ -34,41 +34,6 @@ const decode = Schema.decodeUnknownSync(Info)
const document = path.join(import.meta.dir, "opencode.json")
describe("config plugin reloads", () => {
it.effect("preserves reference precedence and insertion order across documents", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const references = yield* Reference.Service
const host = yield* PluginHost.make(plugins)
yield* references.transform((draft) =>
draft.add(
"external",
Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/references/external") }),
),
)
yield* ConfigReferencePlugin.Plugin.effect(host)
const result = yield* references.list()
expect(result.map((reference) => reference.name)).toEqual(["external", "shared", "first", "second"])
expect(result.find((reference) => reference.name === "shared")?.path).toBe(
AbsolutePath.make(path.resolve("/config/second/shared")),
)
}).pipe(
Effect.provide(
Config.testLayer([
referenceConfig("/config/first/opencode.json", {
shared: "./shared",
first: "./first",
}),
referenceConfig("/config/second/opencode.json", {
shared: "./shared",
second: "./second",
}),
]),
),
Effect.provideService(Global.Service, Global.Service.of(Global.make())),
),
)
it.live("reloads config-backed domains without reloading external plugins", () =>
Effect.gen(function* () {
const agents = yield* Agent.Service
@@ -137,14 +102,6 @@ function config(name: string) {
})
}
function referenceConfig(file: string, references: Record<string, string>) {
return new Document({
type: "document",
path: AbsolutePath.make(file),
info: decode({ references }),
})
}
function title(value: string) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
@@ -949,7 +949,7 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context }) {
const shortcut = (id: string) => () => props.context.keymap.shortcuts(id)[0]
const rows = [
{
shortcut: () => "q",
shortcut: shortcut("diff.close"),
action: "Close viewer",
description: "Quit the diff viewer",
},
+49 -4
View File
@@ -5,6 +5,7 @@ import { testRender } from "@opentui/solid"
import type {
Context,
Destination,
DialogOptions,
KeymapCommand,
KeymapLayer,
Page,
@@ -20,7 +21,7 @@ import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { TestTuiContexts } from "../../fixture/tui-environment"
import { createApi, createEventStream, createFetch, json } from "../../fixture/tui-client"
import { DialogProvider } from "../../../src/ui/dialog"
import { DialogProvider, useDialog } from "../../../src/ui/dialog"
import { ToastProvider } from "../../../src/ui/toast"
import { createSignal } from "solid-js"
@@ -61,6 +62,43 @@ test("ctrl+c closes the diff viewer without exiting the application", async () =
}
})
test("diff help advertises and runs the configured close shortcut", async () => {
const viewer = await renderDiffViewer([], { keybinds: { "diff.close": "x" } })
try {
viewer.commands.get("diff.help")!.run()
await viewer.app.waitForFrame((frame) => frame.includes("Diff shortcuts"))
expect(viewer.app.captureCharFrame()).toContain("x Close viewer")
viewer.clearDialog()
await viewer.app.waitForFrame((frame) => !frame.includes("Diff shortcuts"))
expect(viewer.current().type).toBe("plugin")
viewer.app.mockInput.pressKey("x")
await viewer.app.waitFor(() => viewer.current().type !== "plugin")
expect(viewer.current()).toEqual(startRoute)
} finally {
viewer.app.renderer.destroy()
}
})
test.each([
[undefined, "escapClose viewer"],
["none" as const, "- Close viewer"],
])("diff help preserves the default and unbound close display", async (binding, expected) => {
const viewer = await renderDiffViewer([], {
keybinds: binding === undefined ? undefined : { "diff.close": binding },
})
try {
viewer.commands.get("diff.help")!.run()
await viewer.app.waitForFrame((frame) => frame.includes("Diff shortcuts"))
expect(viewer.app.captureCharFrame()).toContain(expected)
} finally {
viewer.app.renderer.destroy()
}
})
test("shows an error instead of an empty diff when loading fails", async () => {
const viewer = await renderDiffViewer([], { fail: true })
try {
@@ -163,6 +201,7 @@ async function renderDiffViewer(
let renderCommands: SlotClaim<"app">["render"] | undefined
let vcsDiffInput: unknown
let shortcut: (command: string) => string | undefined = () => undefined
let clearDialog = () => {}
const config = createTuiResolvedConfig({ keybinds: options.keybinds })
const transport = createFetch((url) => {
if (url.pathname !== "/api/vcs/diff") return
@@ -182,6 +221,8 @@ async function renderDiffViewer(
function Content() {
const keymap = Keymap.use()
const shortcuts = Keymap.useShortcuts()
const dialog = useDialog()
clearDialog = dialog.clear
shortcut = shortcuts.get
theme = useThemes().currentTokens()
const context = {
@@ -207,9 +248,12 @@ async function renderDiffViewer(
},
ui: {
dialog: {
show: () => () => {},
set() {},
clear() {},
show: dialog.replace,
set(options: DialogOptions) {
dialog.setSize(options.size ?? "medium")
dialog.setCentered(options.centered ?? false)
},
clear: dialog.clear,
},
router: {
register(page: Page) {
@@ -272,6 +316,7 @@ async function renderDiffViewer(
app,
commands,
current,
clearDialog: () => clearDialog(),
shortcut: (command: string) => shortcut(command),
vcsDiffInput: () => vcsDiffInput,
}