Plugin command API shim (#26564)
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
# TUI Command Shim Removal
|
||||||
|
|
||||||
|
Problem:
|
||||||
|
|
||||||
|
- v1 keeps a deprecated `api.command` TUI plugin shim so older plugins do not fail during initialization
|
||||||
|
- v2 should expose only the keymap command API
|
||||||
|
- tests and fixtures should not encode legacy command behavior as expected behavior
|
||||||
|
|
||||||
|
## Remove Public Types
|
||||||
|
|
||||||
|
In `packages/plugin/src/tui.ts`, remove:
|
||||||
|
|
||||||
|
- `TuiCommand`
|
||||||
|
- `TuiCommandApi`
|
||||||
|
- `TuiPluginApi.command`
|
||||||
|
|
||||||
|
Keep `api.keymap` as the only TUI command registration and execution surface.
|
||||||
|
|
||||||
|
## Remove Runtime Shim
|
||||||
|
|
||||||
|
Delete `packages/opencode/src/cli/cmd/tui/plugin/command-shim.ts`.
|
||||||
|
|
||||||
|
In `packages/opencode/src/cli/cmd/tui/plugin/api.tsx`, remove:
|
||||||
|
|
||||||
|
- the `createCommandShim` import
|
||||||
|
- the `command: createCommandShim(...)` field from `createTuiApi(...)`
|
||||||
|
|
||||||
|
In `packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`, remove:
|
||||||
|
|
||||||
|
- the `createCommandShim` import
|
||||||
|
- the `command: createCommandShim(...)` field from `pluginApi(...)`
|
||||||
|
|
||||||
|
## Migration Target
|
||||||
|
|
||||||
|
Plugin authors should replace old calls with keymap calls:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
api.keymap.registerLayer({
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "plugin.command",
|
||||||
|
title: "Plugin Command",
|
||||||
|
namespace: "palette",
|
||||||
|
slashName: "plugin",
|
||||||
|
run() {
|
||||||
|
api.ui.dialog.clear()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bindings: [{ key: "ctrl+shift+p", cmd: "plugin.command" }],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct replacements:
|
||||||
|
|
||||||
|
- `api.command.register(cb)` -> `api.keymap.registerLayer({ commands, bindings })`
|
||||||
|
- `api.command.trigger(name)` -> `api.keymap.dispatchCommand(name)`
|
||||||
|
- `api.command.show()` -> `api.keymap.dispatchCommand("command.palette.show")`
|
||||||
|
- `onSelect(dialog)` -> use `api.ui.dialog` from the plugin API closure
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After removal, run from package directories:
|
||||||
|
|
||||||
|
- `bun typecheck` in `packages/plugin`
|
||||||
|
- `bun typecheck` in `packages/opencode`
|
||||||
|
- TUI plugin loader tests in `packages/opencode` if runtime plugin API wiring changed
|
||||||
@@ -17,6 +17,7 @@ import { Slot as HostSlot } from "./slots"
|
|||||||
import type { useToast } from "../ui/toast"
|
import type { useToast } from "../ui/toast"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import * as Keymap from "../keymap"
|
import * as Keymap from "../keymap"
|
||||||
|
import { createCommandShim } from "./command-shim"
|
||||||
|
|
||||||
type RouteEntry = {
|
type RouteEntry = {
|
||||||
key: symbol
|
key: symbol
|
||||||
@@ -200,6 +201,8 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
app: appApi(),
|
app: appApi(),
|
||||||
|
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
|
||||||
|
command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
|
||||||
keys: {
|
keys: {
|
||||||
formatSequence(parts) {
|
formatSequence(parts) {
|
||||||
return Keymap.formatKeySequence(parts, input.tuiConfig)
|
return Keymap.formatKeySequence(parts, input.tuiConfig)
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Legacy `api.command` bridge for v1 plugins; remove in v2.
|
||||||
|
import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||||
|
import { TuiKeybind } from "../config/keybind"
|
||||||
|
import type { DialogContext } from "../ui/dialog"
|
||||||
|
|
||||||
|
const COMMAND_PALETTE_SHOW = "command.palette.show"
|
||||||
|
const warned = new Set<string>()
|
||||||
|
|
||||||
|
type Warn = (api: string, replacement: string) => void
|
||||||
|
type LegacyDialog = TuiPluginApi["ui"]["dialog"]
|
||||||
|
type CommandShimDialog = DialogContext | LegacyDialog
|
||||||
|
type LegacyKeybinds = TuiPluginApi["tuiConfig"]["keybinds"]
|
||||||
|
|
||||||
|
function warnCommandShim(api: string, replacement: string) {
|
||||||
|
// Warn v1 plugins about deprecated `api.command`; remove this shim path in v2.
|
||||||
|
console.warn("[tui.plugin] deprecated TUI plugin API", { api, replacement })
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCommandShimDialog(dialog: CommandShimDialog): LegacyDialog {
|
||||||
|
if (!("stack" in dialog)) return dialog
|
||||||
|
return {
|
||||||
|
replace(render, onClose) {
|
||||||
|
dialog.replace(render, onClose)
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
dialog.clear()
|
||||||
|
},
|
||||||
|
setSize(size) {
|
||||||
|
dialog.setSize(size)
|
||||||
|
},
|
||||||
|
get size() {
|
||||||
|
return dialog.size
|
||||||
|
},
|
||||||
|
get depth() {
|
||||||
|
return dialog.stack.length
|
||||||
|
},
|
||||||
|
get open() {
|
||||||
|
return dialog.stack.length > 0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function warnOnce(api: string, replacement: string, warn: Warn) {
|
||||||
|
if (warned.has(api)) return
|
||||||
|
warned.add(api)
|
||||||
|
warn(api, replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCommand(item: TuiCommand, dialog: LegacyDialog) {
|
||||||
|
return {
|
||||||
|
namespace: "palette",
|
||||||
|
name: item.value,
|
||||||
|
title: item.title,
|
||||||
|
desc: item.description,
|
||||||
|
category: item.category,
|
||||||
|
suggested: item.suggested,
|
||||||
|
hidden: item.hidden,
|
||||||
|
enabled: item.enabled,
|
||||||
|
slashName: item.slash?.name,
|
||||||
|
slashAliases: item.slash?.aliases,
|
||||||
|
run() {
|
||||||
|
return item.onSelect?.(dialog)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBindings(commands: TuiCommand[], keybinds: LegacyKeybinds) {
|
||||||
|
return commands.flatMap((item) =>
|
||||||
|
item.keybind
|
||||||
|
? keybinds.has(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
|
||||||
|
? keybinds
|
||||||
|
.get(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
|
||||||
|
.map((binding) => ({ ...binding, cmd: item.value, desc: binding.desc ?? item.title }))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: item.keybind,
|
||||||
|
cmd: item.value,
|
||||||
|
desc: item.title,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCommandShim(
|
||||||
|
keymap: TuiPluginApi["keymap"],
|
||||||
|
dialog: CommandShimDialog,
|
||||||
|
keybinds: LegacyKeybinds,
|
||||||
|
): TuiPluginApi["command"] {
|
||||||
|
const shimDialog = createCommandShimDialog(dialog)
|
||||||
|
return {
|
||||||
|
register(cb) {
|
||||||
|
warnOnce("api.command.register", "api.keymap.registerLayer({ commands, bindings })", warnCommandShim)
|
||||||
|
const commands = cb()
|
||||||
|
return keymap.registerLayer({
|
||||||
|
commands: commands.map((item) => toCommand(item, shimDialog)),
|
||||||
|
bindings: toBindings(commands, keybinds),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
trigger(value) {
|
||||||
|
warnOnce("api.command.trigger", "api.keymap.dispatchCommand(name)", warnCommandShim)
|
||||||
|
keymap.dispatchCommand(value)
|
||||||
|
},
|
||||||
|
show() {
|
||||||
|
warnOnce("api.command.show", `api.keymap.dispatchCommand("${COMMAND_PALETTE_SHOW}")`, warnCommandShim)
|
||||||
|
keymap.dispatchCommand(COMMAND_PALETTE_SHOW)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
|
|||||||
import { setupSlots, Slot as View } from "./slots"
|
import { setupSlots, Slot as View } from "./slots"
|
||||||
import type { HostPluginApi, HostSlots } from "./slots"
|
import type { HostPluginApi, HostSlots } from "./slots"
|
||||||
import { ConfigPlugin } from "@/config/plugin"
|
import { ConfigPlugin } from "@/config/plugin"
|
||||||
|
import { createCommandShim } from "./command-shim"
|
||||||
|
|
||||||
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
|
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
|
||||||
|
|
||||||
@@ -576,6 +577,8 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
app: api.app,
|
app: api.app,
|
||||||
|
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
|
||||||
|
command: createCommandShim(keymap, api.ui.dialog, api.tuiConfig.keybinds),
|
||||||
keys: api.keys,
|
keys: api.keys,
|
||||||
keymap,
|
keymap,
|
||||||
route,
|
route,
|
||||||
|
|||||||
@@ -77,6 +77,42 @@ export type TuiKeys = {
|
|||||||
|
|
||||||
export type TuiKeymap = Keymap<Renderable, KeyEvent>
|
export type TuiKeymap = Keymap<Renderable, KeyEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy `api.command` shape kept so v1 plugins can initialize. Remove in v2.
|
||||||
|
*
|
||||||
|
* @deprecated Use `api.keymap.registerLayer({ commands, bindings })` instead.
|
||||||
|
*/
|
||||||
|
export type TuiCommand = {
|
||||||
|
title: string
|
||||||
|
value: string
|
||||||
|
description?: string
|
||||||
|
category?: string
|
||||||
|
keybind?: string
|
||||||
|
suggested?: boolean
|
||||||
|
hidden?: boolean
|
||||||
|
enabled?: boolean
|
||||||
|
slash?: {
|
||||||
|
name: string
|
||||||
|
aliases?: string[]
|
||||||
|
}
|
||||||
|
onSelect?: (dialog?: TuiDialogStack) => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy `api.command` API kept so v1 plugins can initialize. Remove in v2.
|
||||||
|
*
|
||||||
|
* @deprecated Use `api.keymap.registerLayer`, `api.keymap.dispatchCommand`, and
|
||||||
|
* `api.keymap.dispatchCommand("command.palette.show")` instead.
|
||||||
|
*/
|
||||||
|
export type TuiCommandApi = {
|
||||||
|
/** @deprecated Use `api.keymap.registerLayer({ commands, bindings })` instead. */
|
||||||
|
register: (cb: () => TuiCommand[]) => () => void
|
||||||
|
/** @deprecated Use `api.keymap.dispatchCommand(name)` instead. */
|
||||||
|
trigger: (value: string) => void
|
||||||
|
/** @deprecated Use `api.keymap.dispatchCommand("command.palette.show")` instead. */
|
||||||
|
show: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export type TuiDialogProps = {
|
export type TuiDialogProps = {
|
||||||
size?: "medium" | "large" | "xlarge"
|
size?: "medium" | "large" | "xlarge"
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -461,6 +497,13 @@ export type TuiWorkspace = {
|
|||||||
|
|
||||||
export type TuiPluginApi = {
|
export type TuiPluginApi = {
|
||||||
app: TuiApp
|
app: TuiApp
|
||||||
|
/**
|
||||||
|
* Legacy `api.command` API kept so v1 plugins can initialize. Remove in v2.
|
||||||
|
*
|
||||||
|
* @deprecated Use `api.keymap.registerLayer`, `api.keymap.dispatchCommand`, and
|
||||||
|
* `api.keymap.dispatchCommand("command.palette.show")` instead.
|
||||||
|
*/
|
||||||
|
command?: TuiCommandApi
|
||||||
keys: TuiKeys
|
keys: TuiKeys
|
||||||
keymap: TuiKeymap
|
keymap: TuiKeymap
|
||||||
route: {
|
route: {
|
||||||
|
|||||||
Reference in New Issue
Block a user