Compare commits

..
Author SHA1 Message Date
Kit Langton 3369c08ffa docs(plugin): update current API examples 2026-08-27 15:00:32 -04:00
5 changed files with 43 additions and 67 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@opencode-ai/tui": patch
---
Show the configured close shortcut in the diff viewer help dialog.
+21 -7
View File
@@ -5,7 +5,9 @@ The Promise plugin API at `@opencode-ai/plugin` is the async/await equivalent of
- `hook` installs behavior at an OpenCode extension point.
- `reload` reruns every transform hook for a stateful domain.
The only difference from the Effect API is the async boundary: hook callbacks, hook registration, `reload`, and `Registration.dispose` use Promises instead of Effects.
The Promise API uses Promises instead of Effects for setup, runtime hook
callbacks, hook registration, `reload`, and `Registration.dispose`. Transform
draft callbacks remain synchronous.
## Defining A Plugin
@@ -46,12 +48,15 @@ await registration.dispose()
## Transform Hooks
Transform hooks contribute to stateful domains. The draft editor is synchronous; the callback may be `async` when it needs to await other work:
Transform hooks contribute to stateful domains. The draft editor is synchronous,
so load asynchronous data before registering a transform or reloading its domain:
```ts
const description = await loadReviewerDescription()
await ctx.agent.transform((agent) => {
agent.update("reviewer", (item) => {
item.description = "Reviews code for regressions"
item.description = description
item.mode = "subagent"
})
})
@@ -64,8 +69,12 @@ ctx.agent.transform
ctx.catalog.transform
ctx.command.transform
ctx.integration.transform
ctx.mcp.transform
ctx.reference.transform
ctx.skill.transform
ctx.tool.transform
ctx.vcs.transform
ctx.websearch.transform
```
## Runtime Hooks
@@ -81,7 +90,7 @@ await ctx.aisdk.hook("sdk", async (event) => {
await ctx.aisdk.hook("language", (event) => {
if (event.model.providerID !== "xai") return
event.language = event.sdk.responses(event.model.api.id)
event.language = event.sdk.responses(event.model.modelID)
})
```
@@ -94,14 +103,15 @@ await ctx.session.hook("context", (event) => {
})
```
Promise tools use executable tool values with async executors. Registration
supplies the tool's name and options separately:
Promise tools use complete executable tool values with async executors:
```ts
import { Schema } from "effect"
await ctx.tool.transform((tools) => {
tools.add("echo", {
tools.add({
name: "echo",
options: { codemode: false },
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ text: Schema.String }),
@@ -132,6 +142,10 @@ ctx.agent.reload()
ctx.catalog.reload()
ctx.command.reload()
ctx.integration.reload()
ctx.mcp.reload()
ctx.reference.reload()
ctx.skill.reload()
ctx.tool.reload()
ctx.vcs.reload()
ctx.websearch.reload()
```
+17 -5
View File
@@ -31,7 +31,9 @@ Registrations are owned by the plugin scope. Closing the scope removes them auto
## Transform Hooks
Transform hooks contribute to stateful domains:
Transform hooks contribute to stateful domains. Their draft callbacks are
synchronous, so load effectful data before registering a transform or reloading
its domain:
```ts
yield *
@@ -52,8 +54,12 @@ ctx.agent.transform
ctx.catalog.transform
ctx.command.transform
ctx.integration.transform
ctx.mcp.transform
ctx.reference.transform
ctx.skill.transform
ctx.tool.transform
ctx.vcs.transform
ctx.websearch.transform
```
## Runtime Hooks
@@ -72,10 +78,12 @@ yield *
)
yield *
ctx.aisdk.hook("language", (event) => {
if (event.model.providerID !== "xai") return
event.language = event.sdk.responses(event.model.api.id)
})
ctx.aisdk.hook("language", (event) =>
Effect.sync(() => {
if (event.model.providerID !== "xai") return
event.language = event.sdk.responses(event.model.modelID)
}),
)
```
Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.
@@ -117,6 +125,10 @@ ctx.agent.reload()
ctx.catalog.reload()
ctx.command.reload()
ctx.integration.reload()
ctx.mcp.reload()
ctx.reference.reload()
ctx.skill.reload()
ctx.tool.reload()
ctx.vcs.reload()
ctx.websearch.reload()
```
@@ -949,7 +949,7 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context }) {
const shortcut = (id: string) => () => props.context.keymap.shortcuts(id)[0]
const rows = [
{
shortcut: shortcut("diff.close"),
shortcut: () => "q",
action: "Close viewer",
description: "Quit the diff viewer",
},
+4 -49
View File
@@ -5,7 +5,6 @@ import { testRender } from "@opentui/solid"
import type {
Context,
Destination,
DialogOptions,
KeymapCommand,
KeymapLayer,
Page,
@@ -21,7 +20,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, useDialog } from "../../../src/ui/dialog"
import { DialogProvider } from "../../../src/ui/dialog"
import { ToastProvider } from "../../../src/ui/toast"
import { createSignal } from "solid-js"
@@ -62,43 +61,6 @@ 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 {
@@ -201,7 +163,6 @@ 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
@@ -221,8 +182,6 @@ 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 = {
@@ -248,12 +207,9 @@ async function renderDiffViewer(
},
ui: {
dialog: {
show: dialog.replace,
set(options: DialogOptions) {
dialog.setSize(options.size ?? "medium")
dialog.setCentered(options.centered ?? false)
},
clear: dialog.clear,
show: () => () => {},
set() {},
clear() {},
},
router: {
register(page: Page) {
@@ -316,7 +272,6 @@ async function renderDiffViewer(
app,
commands,
current,
clearDialog: () => clearDialog(),
shortcut: (command: string) => shortcut(command),
vcsDiffInput: () => vcsDiffInput,
}