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.
@@ -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};
@@ -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.