Skip to content

Commit f3997d8

Browse files
authored
Single target plugin entrypoints (anomalyco#19467)
1 parent 02b19bc commit f3997d8

18 files changed

Lines changed: 292 additions & 62 deletions

File tree

.opencode/plugins/tui-smoke.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
/** @jsxImportSource @opentui/solid */
22
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
33
import { RGBA, VignetteEffect } from "@opentui/core"
4-
import type { TuiKeybindSet, TuiPluginApi, TuiPluginMeta, TuiSlotPlugin } from "@opencode-ai/plugin/tui"
4+
import type {
5+
TuiKeybindSet,
6+
TuiPlugin,
7+
TuiPluginApi,
8+
TuiPluginMeta,
9+
TuiPluginModule,
10+
TuiSlotPlugin,
11+
} from "@opencode-ai/plugin/tui"
512

613
const tabs = ["overview", "counter", "help"]
714
const bind = {
@@ -813,7 +820,7 @@ const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => {
813820
])
814821
}
815822

816-
const tui = async (api: TuiPluginApi, options: Record<string, unknown> | null, meta: TuiPluginMeta) => {
823+
const tui: TuiPlugin = async (api, options, meta) => {
817824
if (options?.enabled === false) return
818825

819826
await api.theme.install("./smoke-theme.json")
@@ -846,7 +853,9 @@ const tui = async (api: TuiPluginApi, options: Record<string, unknown> | null, m
846853
}
847854
}
848855

849-
export default {
856+
const plugin: TuiPluginModule & { id: string } = {
850857
id: "tui-smoke",
851858
tui,
852859
}
860+
861+
export default plugin

packages/opencode/specs/tui-plugins.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Technical reference for the current TUI plugin system.
88
- Author package entrypoint is `@opencode-ai/plugin/tui`.
99
- Internal plugins load inside the CLI app the same way external TUI plugins do.
1010
- Package plugins can be installed from CLI or TUI.
11+
- v1 plugin modules are target-exclusive: a module can export `server` or `tui`, never both.
12+
- Server runtime keeps v0 legacy fallback (function exports / enumerated exports) after v1 parsing.
1113

1214
## TUI config
1315

@@ -27,6 +29,7 @@ Example:
2729
- `plugin` entries can be either a string spec or `[spec, options]`.
2830
- Plugin specs can be npm specs, `file://` URLs, relative paths, or absolute paths.
2931
- Relative path specs are resolved relative to the config file that declared them.
32+
- A file module listed in `tui.json` must be a TUI module (`default export { id?, tui }`) and must not export `server`.
3033
- Duplicate npm plugins are deduped by package name; higher-precedence config wins.
3134
- Duplicate file plugins are deduped by exact resolved file spec. This happens while merging config, before plugin modules are loaded.
3235
- `plugin_enabled` is keyed by plugin id, not by plugin spec.
@@ -46,7 +49,7 @@ Minimal module shape:
4649

4750
```tsx
4851
/** @jsxImportSource @opentui/solid */
49-
import type { TuiPlugin } from "@opencode-ai/plugin/tui"
52+
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
5053

5154
const tui: TuiPlugin = async (api, options, meta) => {
5255
api.command.register(() => [
@@ -69,16 +72,20 @@ const tui: TuiPlugin = async (api, options, meta) => {
6972
])
7073
}
7174

72-
export default {
75+
const plugin: TuiPluginModule & { id: string } = {
7376
id: "acme.demo",
7477
tui,
7578
}
79+
80+
export default plugin
7681
```
7782

7883
- Loader only reads the module default export object. Named exports are ignored.
79-
- TUI shape is `default export { id?, tui }`.
84+
- TUI shape is `default export { id?, tui }`; including `server` is rejected.
85+
- A single module cannot export both `server` and `tui`.
8086
- `tui` signature is `(api, options, meta) => Promise<void>`.
8187
- If package `exports` contains `./tui`, the loader resolves that entrypoint. Otherwise it uses the resolved package target.
88+
- If a package supports both server and TUI, use separate files and package `exports` (`./server` and `./tui`) so each target resolves to a target-only module.
8289
- File/path plugins must export a non-empty `id`.
8390
- npm plugins may omit `id`; package `name` is used.
8491
- Runtime identity is the resolved plugin id. Later plugins with the same id are rejected, including collisions with internal plugin ids.
@@ -137,6 +144,7 @@ npm plugins can declare a version compatibility range in `package.json` using th
137144
- With `--force`, replacement matches by package name. If the existing row is `[spec, options]`, those tuple options are kept.
138145
- Tuple targets in `oc-plugin` provide default options written into config.
139146
- A package can target `server`, `tui`, or both.
147+
- If a package targets both, each target must still resolve to a separate target-only module. Do not export `{ server, tui }` from one module.
140148
- There is no uninstall, list, or update CLI command for external plugins.
141149
- Local file plugins are configured directly in `tui.json`.
142150

packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TuiPlugin } from "@opencode-ai/plugin/tui"
1+
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
22
import { createMemo, Show } from "solid-js"
33
import { Tips } from "./tips-view"
44

@@ -42,7 +42,9 @@ const tui: TuiPlugin = async (api) => {
4242
})
4343
}
4444

45-
export default {
45+
const plugin: TuiPluginModule & { id: string } = {
4646
id,
4747
tui,
4848
}
49+
50+
export default plugin

packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
2-
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
2+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
33
import { createMemo } from "solid-js"
44

55
const id = "internal:sidebar-context"
@@ -55,7 +55,9 @@ const tui: TuiPlugin = async (api) => {
5555
})
5656
}
5757

58-
export default {
58+
const plugin: TuiPluginModule & { id: string } = {
5959
id,
6060
tui,
6161
}
62+
63+
export default plugin

packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
1+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
22
import { createMemo, For, Show, createSignal } from "solid-js"
33

44
const id = "internal:sidebar-files"
@@ -54,7 +54,9 @@ const tui: TuiPlugin = async (api) => {
5454
})
5555
}
5656

57-
export default {
57+
const plugin: TuiPluginModule & { id: string } = {
5858
id,
5959
tui,
6060
}
61+
62+
export default plugin

packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
1+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
22
import { createMemo, Show } from "solid-js"
33
import { Global } from "@/global"
44

@@ -85,7 +85,9 @@ const tui: TuiPlugin = async (api) => {
8585
})
8686
}
8787

88-
export default {
88+
const plugin: TuiPluginModule & { id: string } = {
8989
id,
9090
tui,
9191
}
92+
93+
export default plugin

packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
1+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
22
import { createMemo, For, Show, createSignal } from "solid-js"
33

44
const id = "internal:sidebar-lsp"
@@ -58,7 +58,9 @@ const tui: TuiPlugin = async (api) => {
5858
})
5959
}
6060

61-
export default {
61+
const plugin: TuiPluginModule & { id: string } = {
6262
id,
6363
tui,
6464
}
65+
66+
export default plugin

packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
1+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
22
import { createMemo, For, Match, Show, Switch, createSignal } from "solid-js"
33

44
const id = "internal:sidebar-mcp"
@@ -88,7 +88,9 @@ const tui: TuiPlugin = async (api) => {
8888
})
8989
}
9090

91-
export default {
91+
const plugin: TuiPluginModule & { id: string } = {
9292
id,
9393
tui,
9494
}
95+
96+
export default plugin

packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
1+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
22
import { createMemo, For, Show, createSignal } from "solid-js"
33
import { TodoItem } from "../../component/todo-item"
44

@@ -40,7 +40,9 @@ const tui: TuiPlugin = async (api) => {
4040
})
4141
}
4242

43-
export default {
43+
const plugin: TuiPluginModule & { id: string } = {
4444
id,
4545
tui,
4646
}
47+
48+
export default plugin

packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Keybind } from "@/util/keybind"
2-
import type { TuiPlugin, TuiPluginApi, TuiPluginStatus } from "@opencode-ai/plugin/tui"
2+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui"
33
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
44
import { fileURLToPath } from "url"
55
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
6-
import { createEffect, createMemo, createSignal } from "solid-js"
6+
import { Show, createEffect, createMemo, createSignal } from "solid-js"
77

88
const id = "internal:plugin-manager"
99
const key = Keybind.parse("space").at(0)
@@ -53,11 +53,17 @@ function Install(props: { api: TuiPluginApi }) {
5353
<props.api.ui.DialogPrompt
5454
title="Install plugin"
5555
placeholder="npm package name"
56+
busy={busy()}
57+
busyText="Installing plugin..."
5658
description={() => (
5759
<box flexDirection="row" gap={1}>
5860
<text fg={props.api.theme.current.textMuted}>scope:</text>
59-
<text fg={props.api.theme.current.text}>{global() ? "global" : "local"}</text>
60-
<text fg={props.api.theme.current.textMuted}>({Keybind.toString(tab)} toggle)</text>
61+
<text fg={busy() ? props.api.theme.current.textMuted : props.api.theme.current.text}>
62+
{global() ? "global" : "local"}
63+
</text>
64+
<Show when={!busy()}>
65+
<text fg={props.api.theme.current.textMuted}>({Keybind.toString(tab)} toggle)</text>
66+
</Show>
6167
</box>
6268
)}
6369
onConfirm={(raw) => {
@@ -256,7 +262,9 @@ const tui: TuiPlugin = async (api) => {
256262
])
257263
}
258264

259-
export default {
265+
const plugin: TuiPluginModule & { id: string } = {
260266
id,
261267
tui,
262268
}
269+
270+
export default plugin

0 commit comments

Comments
 (0)