opencode-webui 2.0.0 → 2.1.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 +1 -1
- package/dist/assets/brother-agent-C1lqxeTh.js +1 -0
- package/dist/assets/groq-voice-m-OaABoC.js +1 -0
- package/dist/assets/index-Bs1cKqOa.js +129 -0
- package/dist/assets/index-D0Y4JI3u.css +1 -0
- package/dist/assets/jsx-runtime-B-hcVAMW.js +1 -0
- package/dist/assets/{report-9wOQi_Kx.js → report-CczF_91u.js} +1 -1
- package/dist/assets/rich-render-C-7ieXZz.js +1 -0
- package/dist/index.html +3 -2
- package/package.json +2 -1
- package/server/ext/engine.ts +161 -0
- package/server/ext/kv.ts +2 -3
- package/server/ext/registry.ts +3 -3
- package/server/ext/types.ts +24 -0
- package/server/index.ts +70 -1
- package/skills/webui/SKILL.md +17 -13
- package/webui-extensions/README.md +119 -13
- package/dist/assets/index-BHminhzR.js +0 -128
- package/dist/assets/index-d4KcyqrZ.css +0 -1
package/server/index.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { Service } from "@opencode-ai/client/service";
|
|
22
22
|
import type { Server } from "bun";
|
|
23
|
-
import { existsSync, mkdirSync, readFileSync, statSync, watch } from "node:fs";
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, statSync, watch, appendFileSync } from "node:fs";
|
|
24
24
|
import { appendFile } from "node:fs/promises";
|
|
25
25
|
import { basename, dirname, join, resolve } from "node:path";
|
|
26
26
|
import { homedir } from "node:os";
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
runExtRequestMiddleware,
|
|
55
55
|
startExtModules,
|
|
56
56
|
} from "./ext/registry";
|
|
57
|
+
import { resolveEngineOverride } from "./ext/engine";
|
|
57
58
|
|
|
58
59
|
// `sandbox` argv — one command, every runtime: `bun run sandbox` (repo, the
|
|
59
60
|
// script adds Vite), `bunx opencode-webui sandbox`, `./opencode-webui sandbox`
|
|
@@ -116,6 +117,18 @@ async function writeDebug(lines: unknown[]) {
|
|
|
116
117
|
let endpoint: Awaited<ReturnType<typeof Service.ensure>> | null = null;
|
|
117
118
|
|
|
118
119
|
async function serviceEndpoint() {
|
|
120
|
+
// Explicit env wins: WEBUI_ENGINE_URL aims the proxy at a chosen engine.
|
|
121
|
+
// An override URL also SKIPS Service.ensure() — no spawn from a stale
|
|
122
|
+
// service.json pid (the rogue-serve incident), no version-kill of the
|
|
123
|
+
// chosen engine. Same resolution as ctx.engine (see server/ext/engine.ts).
|
|
124
|
+
const override = resolveEngineOverride();
|
|
125
|
+
if (override) {
|
|
126
|
+
if (!endpoint || endpoint.url !== override.url) {
|
|
127
|
+
endpoint = override;
|
|
128
|
+
console.log(`[webui] connected to opencode service at ${override.url} (WEBUI_ENGINE_URL)`);
|
|
129
|
+
}
|
|
130
|
+
return endpoint;
|
|
131
|
+
}
|
|
119
132
|
if (!endpoint) {
|
|
120
133
|
endpoint = await Service.ensure();
|
|
121
134
|
console.log(`[webui] connected to opencode service at ${endpoint.url}`);
|
|
@@ -123,6 +136,40 @@ async function serviceEndpoint() {
|
|
|
123
136
|
return endpoint;
|
|
124
137
|
}
|
|
125
138
|
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Proxy crash-reason persistence (G-T8).
|
|
141
|
+
//
|
|
142
|
+
// One sandbox death left no cause. Fatal reasons are appended to CRASH_LOG
|
|
143
|
+
// (never thrown from there — the crash path must not crash) and the last
|
|
144
|
+
// entry is surfaced on the next boot, so an agent can see why the proxy died
|
|
145
|
+
// without having watched it die. Semantics are unchanged: uncaught exceptions
|
|
146
|
+
// still exit(1) (the Node default), rejections keep the runtime's behavior —
|
|
147
|
+
// only observability is added.
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
const CRASH_LOG =
|
|
151
|
+
process.env.WEBUI_CRASH_LOG ??
|
|
152
|
+
join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "opencode-webui", "proxy-crash.log");
|
|
153
|
+
|
|
154
|
+
function persistCrashReason(kind: "uncaughtException" | "unhandledRejection", reason: unknown): void {
|
|
155
|
+
const detail = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);
|
|
156
|
+
try {
|
|
157
|
+
mkdirSync(dirname(CRASH_LOG), { recursive: true, mode: 0o700 });
|
|
158
|
+
appendFileSync(CRASH_LOG, `${new Date().toISOString()} ${kind}: ${detail}\n`, "utf8");
|
|
159
|
+
} catch {
|
|
160
|
+
/* crash path — never throw */
|
|
161
|
+
}
|
|
162
|
+
console.error(`[webui] ${kind} (recorded in ${CRASH_LOG}):`, detail.split("\n")[0]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
process.on("uncaughtException", (err) => {
|
|
166
|
+
persistCrashReason("uncaughtException", err);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
|
169
|
+
process.on("unhandledRejection", (reason) => {
|
|
170
|
+
persistCrashReason("unhandledRejection", reason);
|
|
171
|
+
});
|
|
172
|
+
|
|
126
173
|
// ---------------------------------------------------------------------------
|
|
127
174
|
// Live-event recorder (catch-up for late-joining browsers).
|
|
128
175
|
//
|
|
@@ -385,6 +432,12 @@ async function bundleUIEntry(entry: string): Promise<string> {
|
|
|
385
432
|
const artifact =
|
|
386
433
|
built.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".js")) ??
|
|
387
434
|
built.outputs.find((o) => o.path.endsWith(".js"));
|
|
435
|
+
for (const log of built.logs) {
|
|
436
|
+
// Build warnings/errors are the ONLY server-side signal for a broken
|
|
437
|
+
// extension — a failing bundle must never be silent (the page just sees
|
|
438
|
+
// a missing entry). Bun.build failures throw below; warnings print here.
|
|
439
|
+
console.warn(`[webui] extension bundle build (${entry}): ${log.message}`);
|
|
440
|
+
}
|
|
388
441
|
if (!artifact) throw new Error(`bun.build produced no js artifact for ${entry}`);
|
|
389
442
|
const js = await artifact.text();
|
|
390
443
|
bundleCache.set(entry, { mtimeMs, js });
|
|
@@ -1009,3 +1062,19 @@ console.log(
|
|
|
1009
1062
|
void startEventRecorder();
|
|
1010
1063
|
startExtensionWatcher();
|
|
1011
1064
|
void startExtModules();
|
|
1065
|
+
|
|
1066
|
+
// Crash-log boot note: if a previous proxy died fatally, its reason is the
|
|
1067
|
+
// last line of CRASH_LOG — surface it so the next boot (or an agent reading
|
|
1068
|
+
// the log) sees why without having watched it die.
|
|
1069
|
+
try {
|
|
1070
|
+
if (existsSync(CRASH_LOG)) {
|
|
1071
|
+
const lines = readFileSync(CRASH_LOG, "utf8").trim().split("\n").filter((l) => l.length > 0);
|
|
1072
|
+
// Entries are multi-line (stacks) — the "last" entry is the last line
|
|
1073
|
+
// starting a new timestamped record, not the log's physical last line.
|
|
1074
|
+
const heads = lines.filter((l) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(l));
|
|
1075
|
+
const last = heads[heads.length - 1] ?? lines[lines.length - 1];
|
|
1076
|
+
if (last) console.log(`[webui] previous proxy crash (${heads.length} entr(ies) in ${CRASH_LOG}) — last: ${last.slice(0, 300)}`);
|
|
1077
|
+
}
|
|
1078
|
+
} catch {
|
|
1079
|
+
/* observability only — never block boot */
|
|
1080
|
+
}
|
package/skills/webui/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: webui
|
|
3
|
-
description: opencode-webui — the browser frontend for the OpenCode engine. Load when the user mentions the webui/web frontend, asks about webui extensions, or wants to report a webui bug (/report does it).
|
|
3
|
+
description: opencode-webui — the browser frontend for the OpenCode engine. Load when the user mentions the webui/web frontend, asks about webui extensions, wants to BUILD, ADD, CHANGE, DEBUG, or TEST a webui extension, or wants to report a webui bug (/report does it).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# OpenCode webui (opencode-webui)
|
|
@@ -11,7 +11,7 @@ service credentials. One port for UI + `/api/*`: http://localhost:4097
|
|
|
11
11
|
(`WEBUI_PROXY_PORT`).
|
|
12
12
|
|
|
13
13
|
- **Repo**: https://github.com/AbdelftahZowail/opencode-webui
|
|
14
|
-
- **This skill's version**: 2.
|
|
14
|
+
- **This skill's version**: 2.1.0 (matches the `v2.1.0` git tag —
|
|
15
15
|
the file links below are pinned to it, so they always describe the code
|
|
16
16
|
this skill was generated with)
|
|
17
17
|
- **A running instance exposes its version** at `GET /api/webui/config` →
|
|
@@ -24,13 +24,13 @@ fetch the exact file at the pinned tag instead of reading a local clone:
|
|
|
24
24
|
|
|
25
25
|
| File | Purpose |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.
|
|
28
|
-
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.
|
|
29
|
-
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.
|
|
30
|
-
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.
|
|
31
|
-
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.
|
|
32
|
-
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.
|
|
33
|
-
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.
|
|
27
|
+
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.1.0/webui-extensions/README.md | Full authoring guide — the source of truth for strata/kinds/hooks/anchors |
|
|
28
|
+
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.1.0/src/extensions/registry.tsx | The extension registry — exact register() shapes per kind |
|
|
29
|
+
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.1.0/src/extensions/hooks.ts | Shared fireHooks runner — how open hook events fire |
|
|
30
|
+
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.1.0/src/lib/domKit.ts | DOM-stratum kit (foreign/watch/styles) + the data-oc-* anchor table |
|
|
31
|
+
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.1.0/server/ext/types.ts | Proxy-stratum types — server.ts routes/middleware/onEvent/pollers shapes |
|
|
32
|
+
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.1.0/docs/extension-system-spec.md | The v2 decision record — strata, precedence, deletions, acceptance checks |
|
|
33
|
+
| https://raw.githubusercontent.com/AbdelftahZowail/opencode-webui/v2.1.0/src/store.ts | The store — actions useStore exposes to extensions |
|
|
34
34
|
|
|
35
35
|
## Environment
|
|
36
36
|
|
|
@@ -40,15 +40,19 @@ fetch the exact file at the pinned tag instead of reading a local clone:
|
|
|
40
40
|
| `WEBUI_HOST` | `127.0.0.1` | Bind address — a wildcard is refused without a password. |
|
|
41
41
|
| `WEBUI_PROXY_PORT` | `4097` | Port for the UI and `/api/*`. |
|
|
42
42
|
| `WEBUI_EXTENSION_DIR` | the global + project dirs | Replace both with ONE directory (the sandbox does this to keep WIP isolated). |
|
|
43
|
+
| `WEBUI_ENGINE_URL` | `service.json` discovery | Aim the proxy at a chosen engine — skips `Service.ensure()`, so a stale pid can never spawn a rogue serve. Explicit env wins. |
|
|
44
|
+
| `WEBUI_ENGINE_PASSWORD` | `service.json` password | Engine password for the override above (no file fallback when the URL is overridden — a chosen engine has its own credential). |
|
|
45
|
+
| `WEBUI_SANDBOX_NOVITE` | unset | `1` — sandbox boots the proxy only, no Vite (`--no-vite` flag does the same). |
|
|
46
|
+
| `WEBUI_CRASH_LOG` | `$XDG_STATE_HOME/opencode-webui/proxy-crash.log` | File fatal proxy errors (`uncaughtException`/`unhandledRejection`) are appended to; the last entry prints on next boot. |
|
|
43
47
|
| `WEBUI_DEBUG` | unset | `1` — server/proxy debug logs to stdout. |
|
|
44
48
|
| `WEBUI_DEBUG_LOG` | `/tmp/webui-debug.log` | File the frontend log sink (`POST /api/debug`) appends to. |
|
|
45
49
|
|
|
46
50
|
### Parallel sandboxes (one per extension under test)
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
scratch dir);
|
|
51
|
-
|
|
52
|
+
Yes — run as many sandboxes at once as you need. `bun run sandbox` stacks
|
|
53
|
+
with no flags: the first instance takes the fixed defaults (`:4099`/`:5175`
|
|
54
|
+
+ shared scratch dir); every further instance detects the busy ports and
|
|
55
|
+
auto-isolates onto free ports + a fresh mkdtemp extension dir, printing what
|
|
52
56
|
it picked. Explicit env (`WEBUI_PROXY_PORT` / `WEBUI_VITE_PORT` /
|
|
53
57
|
`WEBUI_EXTENSION_DIR`) always wins per knob. The engine stays shared
|
|
54
58
|
(same sessions everywhere, by design) — only ports + extension dirs are
|
|
@@ -22,6 +22,11 @@ my-extension/
|
|
|
22
22
|
**Gating — one state, owned by the folder itself:** presence = installed;
|
|
23
23
|
`disabled: true` = paused; delete/move the folder to uninstall. No
|
|
24
24
|
`config.ts` list, no per-browser localStorage gating, no second registry.
|
|
25
|
+
User-facing pause (a settings toggle that stops *behavior*, e.g. an
|
|
26
|
+
extension that idles when its key/feature flag is off) is not manifest
|
|
27
|
+
pausing: the former keeps the entry loaded but quiet, the latter
|
|
28
|
+
(`disabled: true`) is never bundled or imported and its id unregisters —
|
|
29
|
+
use a settings toggle for "off for now", the manifest for "unplug".
|
|
25
30
|
|
|
26
31
|
**Precedence (same id = same swap point, higher wins):**
|
|
27
32
|
|
|
@@ -75,13 +80,56 @@ addressable, no marker placement, no guessing), at leaf granularity (the
|
|
|
75
80
|
timestamp, token readout, cost badge, copy button — not just `MessageItem`),
|
|
76
81
|
with rich props, so wraps and value-overrides stay surgical.
|
|
77
82
|
|
|
83
|
+
### Target inventory (the catalog — grep `autoRegister` if this lags)
|
|
84
|
+
|
|
85
|
+
| Target id | Props (meaningful subset) |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `sidebar` | — (the shell) |
|
|
88
|
+
| `sidebar.sessionRow` | `sessionID`, `title`, `updated`, `active`, `selected`, `subagentsActive`, `onSelect` |
|
|
89
|
+
| `conversation` | full `ConversationProps` |
|
|
90
|
+
| `conversation.header` | full `HeaderProps` |
|
|
91
|
+
| `conversation.empty` | — |
|
|
92
|
+
| `composer` | full `ComposerProps` |
|
|
93
|
+
| `composer.contextReadout` | `parts: string[]` |
|
|
94
|
+
| `composer.sendActions` | `sessionID`, `appendDraft(text)` — space-joins onto the draft + refocuses; prefer over writing drafts directly |
|
|
95
|
+
| `message.timestamp` | `time: number` (consults the `format.timestamp` service) |
|
|
96
|
+
| `message.tokens` | `tokens` |
|
|
97
|
+
| `message.cost` | `cost: number` |
|
|
98
|
+
| `message.copyButton` | `variant: "user" \| "assistant"`, `text` |
|
|
99
|
+
| `message:<type>` / `message:*` | replace-with-fall-through per message type |
|
|
100
|
+
| `tool.card` | `part: ToolPart`, `stateKey?` |
|
|
101
|
+
| `tool.edit` / `write` / `shell` / `subagent` / `execute` / `generic` | per-tool view props |
|
|
102
|
+
| `tool:<name>` | replace-with-fall-through per tool name |
|
|
103
|
+
|
|
104
|
+
Persisted-vs-live guarantee (streaming authors depend on this): persisted
|
|
105
|
+
messages always carry a `[data-oc-message]` ancestor; live projections
|
|
106
|
+
(`LiveAssistantView`) render `MessagePart` directly with none. Code that
|
|
107
|
+
must never touch streaming output can rely on the distinction structurally.
|
|
108
|
+
|
|
109
|
+
### Entry hygiene (two rules that bite silently)
|
|
110
|
+
|
|
111
|
+
- **One entry per id.** Same-id `register()` SWAPS (with a console warning
|
|
112
|
+
when kind/target differ) — a folder's entries need distinct ids or the
|
|
113
|
+
later evicts the earlier, and loaders track folders by single id so extras
|
|
114
|
+
leak on disable/delete. One folder → one id per entry, always.
|
|
115
|
+
- **Runtime code uses the bridge only.** External (user/project-dir) bundles
|
|
116
|
+
are built standalone: `import type` from `src/` is erased at build and
|
|
117
|
+
safe, but any *runtime* `src/` import breaks the copy outside the repo.
|
|
118
|
+
Use `window.__opencodeUI` (`register`, `react`, `api`, `store`, `prefs`,
|
|
119
|
+
`notify`, `services`, `dom`, `kv`) — shipped code consumes the identical
|
|
120
|
+
surface via `getExtensionApi()`.
|
|
121
|
+
- **The `@/` alias works in shipped extensions only.** Same repo, same
|
|
122
|
+
tsconfig (`@/*` → `./src/*`, e.g. groq-voice imports
|
|
123
|
+
`@/components/ui/dialog`) — external copies must still use the bridge,
|
|
124
|
+
never `@/` or relative `src/` paths.
|
|
125
|
+
|
|
78
126
|
```tsx
|
|
79
127
|
// index.tsx — wrap the timestamp, own nothing else
|
|
80
128
|
import { register } from "../../src/extensions/registry";
|
|
81
129
|
|
|
82
130
|
register({
|
|
83
131
|
kind: "wrap",
|
|
84
|
-
id: "my-timestamps",
|
|
132
|
+
id: "my-timestamps-wrap",
|
|
85
133
|
target: "Timestamp",
|
|
86
134
|
render: (props, next) => (
|
|
87
135
|
<span title={String(props.iso ?? "")}>{next()}</span>
|
|
@@ -90,7 +138,7 @@ register({
|
|
|
90
138
|
|
|
91
139
|
register({
|
|
92
140
|
kind: "service",
|
|
93
|
-
id: "my-timestamps",
|
|
141
|
+
id: "my-timestamps-format",
|
|
94
142
|
service: "format.timestamp",
|
|
95
143
|
value: (iso: string) => new Date(iso).toLocaleTimeString(),
|
|
96
144
|
precedence: 10,
|
|
@@ -266,8 +314,62 @@ An extension folder may carry `engine/` — a valid opencode plugin directory
|
|
|
266
314
|
(tools the model calls, `experimental.chat.system.transform` prompt hints).
|
|
267
315
|
The webui neither loads nor hot-reloads it; the engine's rules apply
|
|
268
316
|
(boot-time load, restart on edit unless the plugin implements its own shell
|
|
269
|
-
pattern
|
|
270
|
-
|
|
317
|
+
pattern — a stable `index.js` that require-cache-busts a `definitions.cjs`
|
|
318
|
+
on mtime works and is the recommended shape). Convention + worked example
|
|
319
|
+
(brother-agent in webui terms — one folder, three strata):
|
|
320
|
+
`docs/engine-payload-convention.md`. Hard-won facts, stated once so no one
|
|
321
|
+
re-discovers them by trial:
|
|
322
|
+
|
|
323
|
+
- **Export shape:** `module.exports = { id, setup }` (v2 — the v1 `{server}`
|
|
324
|
+
/ named-export shape is rejected: "must export a default definition with
|
|
325
|
+
an id and an effect or setup function").
|
|
326
|
+
- **Tool namespace:** the model lists tools as `tools.<name>` — register and
|
|
327
|
+
match on the `tools.`-prefixed name, never bare.
|
|
328
|
+
- **Tool results must resolve `{ output: string }`.** A bare string fails
|
|
329
|
+
result validation (`Unknown tool` in the transcript).
|
|
330
|
+
- **System-hint parts need `{ type: "text", text }`.** Pushing `{text}`
|
|
331
|
+
without `type` fails the whole session drain (schema `MissingKey`).
|
|
332
|
+
- **Session origin tagging** (`metadata: { origin: "…" }`) survives only via
|
|
333
|
+
REST `POST /api/session` create — the setup-bridge create drops it.
|
|
334
|
+
- **Discovery + auth:** the engine registers at
|
|
335
|
+
`$XDG_STATE_HOME/opencode/service.json` (Basic `opencode:password` —
|
|
336
|
+
mirror `@opencode-ai/client`'s service helper); provider credentials live
|
|
337
|
+
under `XDG_DATA_HOME`, so a `STATE`-only sandbox sees the engine but no
|
|
338
|
+
models. When agent testing misbehaves, verify the provider first with
|
|
339
|
+
`POST /session/{id}/generate {"prompt":"OK"}`; when runs fail blank,
|
|
340
|
+
the cause is in `$XDG_DATA_HOME/opencode/log/opencode.log` (`grep drain`).
|
|
341
|
+
- `server.ts` code that must call the engine has no credential helper yet —
|
|
342
|
+
parse `service.json` by hand (node builtins only, no core imports); a
|
|
343
|
+
`ctx.engine` helper is the planned fix (`server/ext/types.ts`).
|
|
344
|
+
|
|
345
|
+
## Loading lifecycle (where an extension travels)
|
|
346
|
+
|
|
347
|
+
One folder becomes pixels through four files — follow them in order:
|
|
348
|
+
|
|
349
|
+
1. **Glob (shipped, repo dev).** `webui-extensions/index.ts` globs
|
|
350
|
+
`./*/index.{ts,tsx}` and tracks each module's `export const id` (Vite
|
|
351
|
+
HMR path — edits hot-swap via same-id registry swap, deletions prune
|
|
352
|
+
owned ids only).
|
|
353
|
+
2. **Discovery (proxy).** `server/userExtensions.ts`
|
|
354
|
+
(`discoverUserUIEntries`) scans the three sources highest-precedence
|
|
355
|
+
first — user root, project root, shipped dir — taking the folder id
|
|
356
|
+
from `manifest.json` (`id`, falling back to the dir name) and the
|
|
357
|
+
entries from `index.tsx`/`dom.ts`. Same id at a lower source is
|
|
358
|
+
skipped with a once-per-process `shadowed` warning.
|
|
359
|
+
3. **Manifest + SSE + bundling (proxy).** `server/index.ts` merges folder
|
|
360
|
+
entries with engine-plugin UI halves, serves
|
|
361
|
+
`GET /api/webui/extensions` (`{ id, url?v=mtime, domUrl?v=mtime,
|
|
362
|
+
source, origin }`), pushes a `{ type: "webui.extensions", version }`
|
|
363
|
+
event per manifest change on `GET /api/webui/extensions/events`, and
|
|
364
|
+
bundles each entry standalone with `Bun.build` (`bundleUIEntry` —
|
|
365
|
+
react external, build logs printed loudly, never silent).
|
|
366
|
+
4. **Import + register (page).** `src/lib/runtimeExtensions.ts` fetches
|
|
367
|
+
the manifest, dynamic-imports each new `?v=` bundle (re-import on
|
|
368
|
+
mtime move → registry same-id-swap → live repaint), mounts `domUrl`
|
|
369
|
+
via the DOM kit, and unregisters ids that vanish or flip
|
|
370
|
+
`disabled: true`. Shipped browser bundles are skipped here (the glob
|
|
371
|
+
owns them — importing twice would run side effects twice) but shipped
|
|
372
|
+
`domUrl` still mounts and `disabled` still pauses them.
|
|
271
373
|
|
|
272
374
|
## What extensions can use (browser stratum)
|
|
273
375
|
|
|
@@ -337,18 +439,22 @@ register({
|
|
|
337
439
|
isolated second instance — loopback-only `127.0.0.1:4099`, passwordless (the
|
|
338
440
|
bind address is the guarantee), same engine/sessions, extensions from an
|
|
339
441
|
isolated scratch dir (`WEBUI_EXTENSION_DIR`,
|
|
340
|
-
default `~/.local/state/opencode-webui/sandbox-extensions/`).
|
|
341
|
-
|
|
442
|
+
default `~/.local/state/opencode-webui/sandbox-extensions/`).
|
|
443
|
+
`WEBUI_EXTENSION_DIR` is a higher-precedence ADD, not a replace: it swaps
|
|
444
|
+
out the user + project roots only — shipped extensions still load
|
|
445
|
+
underneath, and a same-id scratch folder shadows the shipped copy (the
|
|
446
|
+
`shadowed` log line is the only signal). Iterate there; "shipping" =
|
|
447
|
+
copying the folder into the real extension dir.
|
|
342
448
|
|
|
343
449
|
### Parallel sandboxes (agents: read this)
|
|
344
450
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
instance
|
|
349
|
-
it picked. Explicit env
|
|
350
|
-
`WEBUI_EXTENSION_DIR`) always wins
|
|
351
|
-
behavior.
|
|
451
|
+
Yes — run as many sandboxes at once as you need, one per extension under
|
|
452
|
+
test. `bun run sandbox` stacks with no flags: the first instance takes the
|
|
453
|
+
fixed defaults (`:4099`/`:5175` + shared scratch dir); every further
|
|
454
|
+
instance detects the busy ports and auto-isolates onto free ports + a fresh
|
|
455
|
+
mkdtemp extension dir, printing exactly what it picked. Explicit env
|
|
456
|
+
(`WEBUI_PROXY_PORT` / `WEBUI_VITE_PORT` / `WEBUI_EXTENSION_DIR`) always wins
|
|
457
|
+
per knob and disables that knob's auto behavior.
|
|
352
458
|
|
|
353
459
|
Rules: one sandbox per extension, never two writers to one ext dir, never
|
|
354
460
|
reuse a port. The engine stays shared (same sessions everywhere, by
|