opencode-webui 1.0.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -7
- package/dist/assets/index-BHminhzR.js +128 -0
- package/dist/assets/index-d4KcyqrZ.css +1 -0
- package/dist/assets/report-9wOQi_Kx.js +2 -0
- package/dist/index.html +16 -2
- package/package.json +2 -3
- package/server/ext/kv.ts +82 -0
- package/server/ext/registry.ts +279 -0
- package/server/ext/types.ts +100 -0
- package/server/index.ts +364 -58
- package/server/userExtensions.ts +175 -48
- package/skills/webui/SKILL.md +113 -96
- package/webui-extensions/README.md +356 -0
- package/dist/assets/index-D_1wPDvx.css +0 -1
- package/dist/assets/index-v2k5q9l4.js +0 -122
- package/dist/assets/report-CHS9-Gsw.js +0 -2
- package/ui-extensions/README.md +0 -285
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./index-v2k5q9l4.js";var s=`report`,c=`AbdelftahZowail/opencode-webui`,l=10,u=window;function d(e){let t=u.__reportErrors??=[];t.push(e),t.length>l&&t.splice(0,t.length-l)}u.__reportErrorsInstalled||(u.__reportErrorsInstalled=!0,window.onerror=(e,t,n,r,i)=>{d({message:String(e).slice(0,500),source:t??``,line:n??0,stack:i instanceof Error?i.stack?.slice(0,500):void 0})},window.addEventListener(`unhandledrejection`,e=>{let t=e.reason;d({message:(t instanceof Error?t.message:String(t)).slice(0,500),source:`unhandledrejection`,line:0,stack:t instanceof Error?t.stack?.slice(0,500):void 0})}));async function f(e){try{let t=await fetch(e);return t.ok?await t.json():null}catch{return null}}async function p(e){let[t,n]=await Promise.all([f(`/api/webui/config`),f(`/api/webui/extensions`)]);return{url:location.href,userAgent:navigator.userAgent,language:navigator.language,viewport:`${window.innerWidth}x${window.innerHeight}`,version:t?.version??`unknown`,reportRepo:t?.reportRepo??c,builtins:[...a],runtimeExtensions:(n?.data??[]).map(e=>e.id??``).filter(Boolean),errors:u.__reportErrors??[],sessionID:e,timestamp:new Date().toISOString()}}function m(e,t){let n=`WebUI bug: ${e?e.slice(0,80):`user report`}`,r=[`### What happened`,e||``,``,`### Steps to reproduce`,`1. `,``,`### Expected`,``,`### Actual`,``,`<details><summary>Diagnostics</summary>`,``,"```json",JSON.stringify(t,null,2),"```",`</details>`].join(`
|
|
2
|
-
`);return{title:n,body:r,url:`https://github.com/${t.reportRepo}/issues/new?title=${encodeURIComponent(n)}&body=${encodeURIComponent(r)}`}}async function h(e){try{return await navigator.clipboard.writeText(e),!0}catch{return!1}}async function g(r,a){let s=r.trim(),c=s.startsWith(`--agent`),l=c?s.replace(/^--agent\b/,``).trim():s,u=await p(a.sessionID??n().currentSessionID??null),{title:d,body:f,url:g}=m(l,u);if(c){let r=`File a bug against ${u.reportRepo} using the gh CLI: create an issue with the title and body below, then reply with the issue URL. Do not include any private session content beyond the diagnostics JSON.\n\nTitle: ${d}\n\n${f}`,s=a.sessionID??n().currentSessionID??null;if(s&&i(s))try{s=await t(``)}catch{s=null}if(!s){let e=await h(r);o({title:e?`No active session — report prompt copied`:`No active session`,description:e?`Paste it into a session to have the agent file the issue.`:`Could not copy the report prompt either.`,variant:e?`default`:`destructive`});return}o({title:`Report handed to the agent`,description:`It will file the issue and reply with the URL.`}),await e(s,r);return}if(window.open(g,`_blank`))o({title:`Opening GitHub issue…`});else{let e=await h(g);o({title:e?`Report ready — link copied`:`Report link`,description:e?`Popup blocked; paste the link in a browser.`:`Popup blocked and the clipboard is unavailable.`,variant:e?`default`:`destructive`})}}r({kind:`slash`,id:`report`,name:`report`,aliases:[`bug`],description:`Report a webui bug on GitHub — diag bundle prefilled`,run:g});export{s as id};
|
package/ui-extensions/README.md
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
# UI Extensions
|
|
2
|
-
|
|
3
|
-
Frontend additions live in `ui-extensions/<name>/` and are **plain React
|
|
4
|
-
code, compiled into the app** — no plugin framework, no manifests, no dynamic
|
|
5
|
-
loading. This is a deliberate choice: extensions keep full type safety, hot
|
|
6
|
-
reload, and complete access to the app (store, API client, components).
|
|
7
|
-
|
|
8
|
-
## The kind contract (stable API)
|
|
9
|
-
|
|
10
|
-
Extensions register **kinds** via `register()` from `src/extensions/registry.tsx`.
|
|
11
|
-
This list is versioned — the app only breaks an extension when a kind is
|
|
12
|
-
deliberately changed. Every kind is gated per-id by `enabled` in `ui-extensions/config.ts`
|
|
13
|
-
(ancestry-aware: `my-ext.sub` is on when `my-ext` is enabled).
|
|
14
|
-
|
|
15
|
-
| Kind | What it does | Where it surfaces |
|
|
16
|
-
| --- | --- | --- |
|
|
17
|
-
| `region` | render into any `<Slot region="…">` marker placed by core (see generated table below) | wherever core placed a `<Slot>` |
|
|
18
|
-
| `command` | entry in the palette's "Extension commands" group (`run({ sessionID })`; `keybind` like `ctrl+shift+k` for global hotkey) | command palette (⌘/ctrl-K) + keybind |
|
|
19
|
-
| `slash` | **UI-only** slash entry for Composer `/name` (local `run(args,{sessionID})`; not engine) | Composer autocomplete (`/` menu) |
|
|
20
|
-
| `message` | full replacement for any message type (`type:"system"\|"synthetic"\|"shell"\|"compaction"\|"user"\|"assistant"\|…\|"*"`, `render({message, sessionID}) => node\|null`); first non-null wins, else core `renderMessageBody` | `MessageItem` per message |
|
|
21
|
-
| `message.decoration` | small extras under message rows; `render({ messageID, message }) => node\|null` | under every message row |
|
|
22
|
-
| `message.part` | inject after *each* part (text/tool/reasoning) inside a message; `render({messageID, message, part, partIndex})` | inside `MessageItem` per part |
|
|
23
|
-
| `tool.renderer` | custom card for a specific tool name (`toolName:"bash"\|"edit"\|…`, `render(part)`) | `ToolCard` per tool call |
|
|
24
|
-
| `contextMenu` | right-click menu item (`target:"message"\|"session"\|"file"`, `label`, `run`, `order`) | context menu |
|
|
25
|
-
| `hook` | intercept/behavior (`event:string`, `handler(ctx,next)`) — see Hook events below | store / Composer / MessageItem |
|
|
26
|
-
| `page` | full surface at `/ext/{id}` (route derived from the id) | sidebar links + direct URL |
|
|
27
|
-
| `settings` | titled section inside Settings › Extensions (`render: () => ReactNode`) | Settings dialog |
|
|
28
|
-
|
|
29
|
-
Reference implementation of every kind was `ui-extensions/dev-sandbox/` (removed — clean launch; see git history for numbered examples).
|
|
30
|
-
|
|
31
|
-
### Hook events
|
|
32
|
-
|
|
33
|
-
`hook` is open — `event` is a `string`, known values are versioned but you can
|
|
34
|
-
register any string and core will call `getHooks(event)` at the seam when it
|
|
35
|
-
exists. Today:
|
|
36
|
-
|
|
37
|
-
| Event | `ctx` shape | When |
|
|
38
|
-
| --- | --- | --- |
|
|
39
|
-
| `session.prompt` | `{ text: string, sessionID: string }` — mutate `ctx.text` to transform; call `next()` to continue | Composer `submit` before `POST /api/session/{id}/prompt` |
|
|
40
|
-
| `message.render` | `{ message: MessageInfo, sessionID?: string }` — mutate `ctx.message` (shallow clone) before render | `MessageItem` before `message` renderer |
|
|
41
|
-
| `store.dispatch` | `{ action, ... }` | store middleware observer |
|
|
42
|
-
|
|
43
|
-
Adding a new seam (e.g. `composer.submit`, `tool.started`) is one `getHooks("new.event")` call in core — no registry bump for existing extensions.
|
|
44
|
-
|
|
45
|
-
### Slash: engine vs UI
|
|
46
|
-
|
|
47
|
-
Composer's `/` menu `src/components/Composer.tsx:576` is a merge:
|
|
48
|
-
|
|
49
|
-
1. **UI built-ins** `slashActions[]` `Composer.tsx:403` (`/new`, `/undo`, `/thinking`… — local, never hits engine)
|
|
50
|
-
2. **UI extensions** `kind:"slash"` `registry.tsx:62` (`/bench` → local `run(args,{sessionID})`)
|
|
51
|
-
3. **Engine** `GET /api/command` + `GET /api/skill` `src/api/client.ts:677` (`commands`/`skills` → `POST /api/session/{id}/command`)
|
|
52
|
-
4. Sorted + fuzzy `filterSlashEntries` `Composer.tsx:125`, capped `SLASH_MENU_LIMIT=10`.
|
|
53
|
-
|
|
54
|
-
* Want `/` to run **on the server** (tool, agent work)? Add an **engine plugin** (provides `Command`/`Skill` via `GET /api/plugin` — engine docs). It appears automatically, no UI change.
|
|
55
|
-
* Want `/` to run **locally in the UI** (toggle panel, run `api.*`, `useStore` action, `notify()`)? Use `kind:"slash"` in a UI extension. Keep the name `^[a-z0-9_-]+$`; on clash engine wins (UI extension warns and is skipped).
|
|
56
|
-
|
|
57
|
-
Palette `kind:"command"` stays the place for `⌘K` actions; `kind:"slash"` is only for the `/` autocomplete.
|
|
58
|
-
|
|
59
|
-
## Anatomy of an extension
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
ui-extensions/
|
|
63
|
-
index.ts ← auto-discovery: every <name>/index.{ts,tsx} is loaded,
|
|
64
|
-
no manual imports (visibility gated by config.ts)
|
|
65
|
-
hello/
|
|
66
|
-
index.tsx ← registers against one or more kinds
|
|
67
|
-
my-feature/
|
|
68
|
-
index.tsx
|
|
69
|
-
components.tsx ← anything else; it's just your code
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`ui-extensions/index.ts` is the only thing the app imports; each extension
|
|
73
|
-
folder self-registers:
|
|
74
|
-
|
|
75
|
-
```tsx
|
|
76
|
-
// ui-extensions/hello/index.tsx
|
|
77
|
-
import { register } from "../../src/extensions/registry";
|
|
78
|
-
|
|
79
|
-
register({
|
|
80
|
-
kind: "region",
|
|
81
|
-
id: "hello",
|
|
82
|
-
region: "footer",
|
|
83
|
-
render: () => <span>Hello!</span>,
|
|
84
|
-
});
|
|
85
|
-
if (import.meta.hot) import.meta.hot.accept();
|
|
86
|
-
export const id = "hello";
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
A tool renderer receives the full tool part:
|
|
90
|
-
|
|
91
|
-
```tsx
|
|
92
|
-
register({
|
|
93
|
-
kind: "tool.renderer",
|
|
94
|
-
id: "my-bash-card",
|
|
95
|
-
toolName: "bash",
|
|
96
|
-
render: (part) => <pre>{part.state.content}</pre>,
|
|
97
|
-
});
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
A UI-only slash:
|
|
101
|
-
|
|
102
|
-
```tsx
|
|
103
|
-
register({
|
|
104
|
-
kind: "slash",
|
|
105
|
-
id: "my.slash",
|
|
106
|
-
name: "bench",
|
|
107
|
-
description: "Run bench locally",
|
|
108
|
-
aliases: ["b"],
|
|
109
|
-
run: (args, { sessionID }) => console.log("bench", args, sessionID),
|
|
110
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
A message replacement (own `system`/`synthetic` without touching core):
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
register({
|
|
117
|
-
kind: "message",
|
|
118
|
-
id: "my.instructions",
|
|
119
|
-
type: "system",
|
|
120
|
-
render: ({ message }) => {
|
|
121
|
-
// return null to fall back to core's InstructionCard
|
|
122
|
-
if (!String((message as any).text ?? "").includes("The Code Mode tool catalog")) return null;
|
|
123
|
-
const title = (message as any).description ?? "Instructions updated";
|
|
124
|
-
return <div className="rounded-md border px-3 py-1.5 text-xs">{title}</div>;
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## What extensions can use
|
|
130
|
-
|
|
131
|
-
Everything the app can — it's the same build:
|
|
132
|
-
|
|
133
|
-
- `useStore` / store actions from `src/store.ts` (session state, sending prompts, permissions, `selectSession`, `sendPromptTo`, `interrupt`, …)
|
|
134
|
-
- `api` from `src/api/client.ts` (any endpoint: `listSessions`, `messages`, `fsRead`, `shellCreate`, …)
|
|
135
|
-
- `window.__opencodeUI.notify({title, description, variant})` — toasts (also `import {notify} from "../../src/lib/notify"` in built-ins)
|
|
136
|
-
- `window.__opencodeUI.getHooks` — inspect registered hooks (runtime bridge)
|
|
137
|
-
- UI primitives from `src/components/ui/` (shadcn: `Button`, `Dialog`,
|
|
138
|
-
`DropdownMenu`, `Command`, `Tooltip`, `ContextMenu`, …) — always build on these so
|
|
139
|
-
extensions look native
|
|
140
|
-
- The OC-2 design tokens in `src/styles.css` — consume as
|
|
141
|
-
`var(--background-base)`, `var(--text-weak)`, `var(--border-base)`, etc.
|
|
142
|
-
**Never hardcode colors**; tokens keep extensions theme-compatible
|
|
143
|
-
- Any component, hook, or CSS class
|
|
144
|
-
- Right-click menus and per-part injections need no extra setup — just register `contextMenu`/`message.part` kinds.
|
|
145
|
-
|
|
146
|
-
## Add / remove / disable
|
|
147
|
-
|
|
148
|
-
| Action | How |
|
|
149
|
-
| --- | --- |
|
|
150
|
-
| Add | Create `ui-extensions/<name>/`, add one import to `ui-extensions/index.ts`. Appears instantly via HMR. |
|
|
151
|
-
| Remove | Delete the import line and the folder. |
|
|
152
|
-
| Disable | Remove its id from the `enabled` list in `ui-extensions/config.ts` — one line, applies instantly via HMR, no reload. |
|
|
153
|
-
|
|
154
|
-
The `enabled` list in `ui-extensions/config.ts` is the runtime switch: only
|
|
155
|
-
ids listed there are rendered, even if the code is bundled. (A settings-panel
|
|
156
|
-
UI could drive the same list later — the mechanism is already in place.)
|
|
157
|
-
|
|
158
|
-
## Sharing with others
|
|
159
|
-
|
|
160
|
-
An extension is a React component, so npm is the sharing format:
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
bun add @someone/opencode-webui-status-bar
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
// ui-extensions/index.ts
|
|
168
|
-
import { register } from "../src/extensions/registry";
|
|
169
|
-
import { StatusBar } from "@someone/opencode-webui-status-bar";
|
|
170
|
-
register({ kind: "region", id: "status-bar", region: "footer", render: () => <StatusBar /> });
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
That's it — no plugin API to learn, nothing to build. Publishing a shared
|
|
174
|
-
extension is just publishing a component library.
|
|
175
|
-
|
|
176
|
-
## Preview before production
|
|
177
|
-
|
|
178
|
-
The dev server (localhost:5173) is the preview: every edit hot-reloads there,
|
|
179
|
-
and production (the built app on 4097) only changes when you run
|
|
180
|
-
`bun run build && bun start`. For a second, isolated preview page of WIP
|
|
181
|
-
changes, run `bun run sandbox` (localhost:5175, proxy :4099) — same loopback
|
|
182
|
-
isolation whether you're in a repo checkout or running the installed package
|
|
183
|
-
(`bunx opencode-webui sandbox`).
|
|
184
|
-
|
|
185
|
-
### User-dir extensions: scratch first, ship by copy
|
|
186
|
-
|
|
187
|
-
The sandbox works WITHOUT a repo checkout: loopback-only (`127.0.0.1:4099`),
|
|
188
|
-
no password (the bind address is the guarantee; a non-loopback sandbox is
|
|
189
|
-
refused), same engine/sessions as your main instance, and extensions load
|
|
190
|
-
from an isolated scratch dir
|
|
191
|
-
(`~/.local/state/opencode-webui/sandbox-extensions/`) instead of the real
|
|
192
|
-
ones. Iterate there; "shipping" = copying the folder into
|
|
193
|
-
`~/.config/opencode/webui-extensions/<name>/` (or the project's
|
|
194
|
-
`.opencode/webui-extensions/<name>/`) — the main instance picks it up within
|
|
195
|
-
its poll cycle.
|
|
196
|
-
|
|
197
|
-
### Hot reload
|
|
198
|
-
|
|
199
|
-
Extension entries end with `if (import.meta.hot) import.meta.hot.accept();` and
|
|
200
|
-
export their `id` — keep both. Editing an extension hot-swaps it live (same-id
|
|
201
|
-
registry swap, slots repaint); ADDING a folder is hot too (`index.ts`
|
|
202
|
-
re-discovers without a reload). DELETING a folder is hot too: Vite accepts the
|
|
203
|
-
removal at the `index.ts` HMR boundary and `pruneExtensions` drops the slots
|
|
204
|
-
without a reload (proven by `scripts/uitest/extensions-check.ts` Phase C).
|
|
205
|
-
Flipping `config.ts` still reloads (it is imported by core). Slots removed by
|
|
206
|
-
an edit disappear cleanly and instantly.
|
|
207
|
-
|
|
208
|
-
## Region markers
|
|
209
|
-
|
|
210
|
-
Drop-in render points addressed by string. Empty regions cost nothing;
|
|
211
|
-
register against one with `{ kind: "region", region: "...", render }`.
|
|
212
|
-
|
|
213
|
-
<!-- regions:auto:start -->
|
|
214
|
-
| Region | Render point |
|
|
215
|
-
| --- | --- |
|
|
216
|
-
| `app.header` | `src/App.tsx:238` |
|
|
217
|
-
| `composer.above` | `src/components/Composer.tsx:932` |
|
|
218
|
-
| `composer.below` | `src/components/Composer.tsx:1252` |
|
|
219
|
-
| `composer.toolbar` | `src/components/Composer.tsx:1240` |
|
|
220
|
-
| `footer` | `src/App.tsx:303` |
|
|
221
|
-
| `header.session.actions` | `src/components/Conversation.tsx:572` |
|
|
222
|
-
| `header.session.before` | `src/components/Conversation.tsx:520` |
|
|
223
|
-
| `message.after` | `src/components/MessageItem.tsx:258` |
|
|
224
|
-
| `message.before` | `src/components/MessageItem.tsx:239` |
|
|
225
|
-
| `sidebar` | `src/components/Sidebar.tsx:682` |
|
|
226
|
-
| `sidebar.session.after` | `src/components/Sidebar.tsx:982` |
|
|
227
|
-
| `sidebar.session.before` | `src/components/Sidebar.tsx:914` |
|
|
228
|
-
| `tool.after` | `src/components/ToolCard.tsx:58` |
|
|
229
|
-
| `tool.before` | `src/components/ToolCard.tsx:57` |
|
|
230
|
-
| `transcript.above` | `src/components/Conversation.tsx:128` |
|
|
231
|
-
| `transcript.below` | `src/components/Conversation.tsx:155` |
|
|
232
|
-
| `transcript.empty` | `src/components/Conversation.tsx:434` |
|
|
233
|
-
<!-- regions:auto:end -->
|
|
234
|
-
|
|
235
|
-
Run `bun run regions` after adding a `<Slot region="…">` in core — the table is auto-generated.
|
|
236
|
-
|
|
237
|
-
## Limitations & when to use what
|
|
238
|
-
|
|
239
|
-
Extensions are **not a second engine**. These stay engine-owned:
|
|
240
|
-
|
|
241
|
-
| Area | UI extension can do | Engine plugin must do |
|
|
242
|
-
| --- | --- | --- |
|
|
243
|
-
| Slash that runs on server | Show it as UI-only `kind:"slash"` but it won't hit `POST /api/session/{id}/command` | Provide `Command`/`Skill` via engine plugin (`GET /api/plugin` → `GET /api/command`) |
|
|
244
|
-
| New tool that the model can call | Render it differently via `kind:"tool.renderer"` | Provide the tool itself (engine `Tool` + execution) |
|
|
245
|
-
| New permission/form/question kind | Render extra decoration, auto-reply via `api` + `hook:store.dispatch` observer | Define it on engine |
|
|
246
|
-
| Session/model/agent lifecycle | Read/trigger via `useStore`/`api` | Own it |
|
|
247
|
-
|
|
248
|
-
**No snooping needed:** if a region/kind isn't in the tables above, it doesn't exist. Adding a region is one line `<Slot region="area.thing" />` in core + `bun run regions`; adding a kind is a deliberate contract change in `src/extensions/registry.tsx` (+ docs here + `src/extensions/registry.tsx:5` header + `AGENTS.md`). Don't invent speculative kinds — add a `region` first.
|
|
249
|
-
|
|
250
|
-
**What you don't need to fork core for anymore:**
|
|
251
|
-
|
|
252
|
-
* Verbose `Instructions updated` / catalog dump `src/components/MessageItem.tsx:268` → `kind:"message"` `type:"system"|"synthetic"` with `return null` fallback
|
|
253
|
-
* Per-session badges / cost / PR status `src/components/Sidebar.tsx:892` → `region:"sidebar.session.before/after"` with `sessionID` ctx
|
|
254
|
-
* Extra header buttons `src/components/Conversation.tsx:518` → `region:"header.session.before/after"`
|
|
255
|
-
* Composer buttons next to `Send` `src/components/Composer.tsx:1238` → `region:"composer.toolbar"`
|
|
256
|
-
* Wrapping a tool or message `src/components/ToolCard.tsx:29` → `region:"tool.before/after"` / `region:"message.before/after"` or `kind:"message"` / `kind:"tool.renderer"` for full replacement
|
|
257
|
-
* Intercepting a prompt `src/components/Composer.tsx:727` → `hook:"session.prompt"` mutate `ctx.text`; observing renders → `hook:"message.render"` mutate `ctx.message`
|
|
258
|
-
|
|
259
|
-
## Runtime plugin extensions (plugin-shipped UI)
|
|
260
|
-
|
|
261
|
-
An opencode v2 plugin can carry a WEB UI half that this app loads at runtime —
|
|
262
|
-
no webui rebuild, ever.
|
|
263
|
-
|
|
264
|
-
**Convention**: for a plugin whose entry is `<dir>/foo.ts`, the UI entry is
|
|
265
|
-
`<dir>/ui/main.tsx` (or a sibling `<dir>/foo.ui.tsx`). Only plugins loaded
|
|
266
|
-
from LOCAL sources (`Plugin.Source {type:"local"}`) are discovered in v1;
|
|
267
|
-
npm-package plugins would need node resolution and are future work.
|
|
268
|
-
|
|
269
|
-
**Pipeline**: `GET /api/plugin` (engine) → proxy finds UI entries →
|
|
270
|
-
`Bun.build` bundles each entry as a SELF-CONTAINED ES module (it bundles its
|
|
271
|
-
own React copy) → served at `/api/webui/extensions/{id}/bundle.js?v=mtime`.
|
|
272
|
-
The page lists them from `GET /api/webui/extensions`, allow-lists their ids in
|
|
273
|
-
the registry, and `import()`s each bundle. Bundles reach the app ONLY through
|
|
274
|
-
the `window.__opencodeUI` bridge (`{ version, register, react, jsxRuntime,
|
|
275
|
-
useStore, api }`) — that object is the versioned public API for runtime
|
|
276
|
-
extensions; deep internal imports are not available to them.
|
|
277
|
-
|
|
278
|
-
**Gating**: default ON (installing the plugin means you wanted it). Toggle any
|
|
279
|
-
of them in Settings › Extensions — persisted per browser, applies within one
|
|
280
|
-
poll cycle (~8s; off = unregistered immediately). Built-in `ui-extensions/`
|
|
281
|
-
are not listed there; they stay gated by `config.ts`.
|
|
282
|
-
|
|
283
|
-
**Talking to the engine half**: the UI half and the engine half of a plugin
|
|
284
|
-
communicate through the engine's normal HTTP/SSE surface — call routes via
|
|
285
|
-
`api`, subscribe to events via the store's SSE connection, exactly like core.
|