Compare commits

..
Author SHA1 Message Date
Kit Langton 3369c08ffa docs(plugin): update current API examples 2026-08-27 15:00:32 -04:00
3 changed files with 57 additions and 16 deletions
+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()
```
@@ -3,8 +3,9 @@ import type { FileDiffInfo } from "@opencode-ai/client"
import { Plugin } from "@opencode-ai/plugin/tui"
import type { KeymapCommand, Route } from "@opencode-ai/plugin/tui/context"
import { TextAttributes, type BorderSides, type BoxRenderable, type ScrollBoxRenderable } from "@opentui/core"
import { filetype } from "../../util/filetype"
import { LANGUAGE_EXTENSIONS } from "../../util/filetype"
import { useTerminalDimensions } from "@opentui/solid"
import path from "path"
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
import { DiffViewerFileTree } from "./diff-viewer-file-tree"
import { Panel, PanelGroup, Separator } from "./diff-viewer-ui"
@@ -57,6 +58,13 @@ const normalizeDiffs = (diffs: readonly FileDiffInfo[]): DiffFile[] =>
status: item.status,
}))
function filetype(input?: string) {
if (!input) return "none"
const language = LANGUAGE_EXTENSIONS[path.extname(input)]
if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript"
return language
}
function storedView(value: unknown): DiffView | undefined {
if (value === "split" || value === "unified") return value
}
@@ -133,7 +141,10 @@ function DiffViewer(props: { context: Plugin.Context }) {
const previousHunkShortcut = shortcut("diff.previous_hunk")
const nextFileShortcut = shortcut("diff.next_file")
const previousFileShortcut = shortcut("diff.previous_file")
const toggleFileTreeShortcut = shortcut("diff.toggle_file_tree")
const singlePatchShortcut = shortcut("diff.single_patch")
const switchSourceShortcut = shortcut("diff.switch_source")
const toggleViewShortcut = shortcut("diff.toggle_view")
const markReviewedShortcut = shortcut("diff.mark_reviewed")
const helpShortcut = shortcut("diff.help")
let scroll: ScrollBoxRenderable | undefined
@@ -283,6 +294,7 @@ function DiffViewer(props: { context: Plugin.Context }) {
setSelectedHunk({ fileIndex: next.fileIndex, hunkIndex: next.hunkIndex, scrollTop: patchScroll.scrollTop })
}
const highlightedPatchFileIndex = () => fileRows().find((row) => row.id === highlightedFileNode())?.fileIndex
const firstPatchFileIndex = () => fileRows().find((row) => row.fileIndex !== undefined)?.fileIndex
const visiblePatchFiles = createMemo(() => {
if (!singlePatch()) {
@@ -669,8 +681,8 @@ function DiffViewer(props: { context: Plugin.Context }) {
},
]
const openSwitchDiffDialog = () => {
const options = [
const switchDiffOptions = createMemo(() => {
return [
{
title: "Working tree",
value: "working" as const,
@@ -682,13 +694,16 @@ function DiffViewer(props: { context: Plugin.Context }) {
description: "Show changes compared to main branch",
},
]
})
const openSwitchDiffDialog = () => {
dialog.show(() => (
<DialogSelect
title="Switch source"
skipFilter={true}
renderFilter={false}
current={mode()}
options={options.map((option) => ({
options={switchDiffOptions().map((option) => ({
...option,
onSelect() {
dialog.clear()