From 8a3fb5ea4b8653478412c436327a0e19045450bc Mon Sep 17 00:00:00 2001
From: G626 <103378730+Krablante@users.noreply.github.com>
Date: Fri, 19 Jun 2026 12:20:03 +0000
Subject: [PATCH 1/2] feat(web): seed default servers from env
---
CHANGELOG.md | 5 ++
docs/opencodez.md | 42 +++++++++++++
packages/opencode/src/server/shared/ui.ts | 72 ++++++++++++++++++++++-
3 files changed, 117 insertions(+), 2 deletions(-)
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..ba625cec2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## 1.17.8+opencodez.2
+
+Web can seed default server connections from environment config.
diff --git a/docs/opencodez.md b/docs/opencodez.md
index 6551125bc..9de31a83d 100644
--- a/docs/opencodez.md
+++ b/docs/opencodez.md
@@ -399,6 +399,48 @@ For existing sessions, web prompt choices are written to session metadata. For a
new session before the first message, the choice stays in composer draft
metadata and is sent when the session is created.
+## Web Default Servers
+
+OpenCodez Web can seed its Settings -> Servers list from environment config.
+This is a generic OpenCodez feature for multiserver setups; it is not tied to
+Politia, BURO, or any private host names.
+
+The server process accepts either plain JSON or base64-encoded JSON:
+
+```text
+OPENCODE_WEB_SERVERS_JSON
+OPENCODE_WEB_SERVERS_JSON_B64
+```
+
+The JSON value is an array of entries. Two shapes are accepted:
+
+```json
+[
+ { "name": "ser", "url": "http://192.168.1.215:4096" }
+]
+```
+
+or the internal OpenCode Web connection shape:
+
+```json
+[
+ {
+ "type": "http",
+ "displayName": "ser",
+ "http": { "url": "http://192.168.1.215:4096" }
+ }
+]
+```
+
+On page load, OpenCodez merges these entries into the browser's normal server
+storage by URL. Existing user entries are preserved unless a configured entry
+uses the same URL, in which case the configured display name wins. The UI still
+uses its normal health checks and project selection behavior after seeding.
+
+When multiple web origins are used, the remote `opencodez serve` processes also
+need matching `--cors` origins so browser health checks and API calls can reach
+the selected server.
+
## Compact Indicator
The TUI shows the active System and Tone while the user works.
diff --git a/packages/opencode/src/server/shared/ui.ts b/packages/opencode/src/server/shared/ui.ts
index c2fd3b863..a7d9e338f 100644
--- a/packages/opencode/src/server/shared/ui.ts
+++ b/packages/opencode/src/server/shared/ui.ts
@@ -12,6 +12,66 @@ export const csp = (hash = "") =>
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src * data:`
export const DEFAULT_CSP = csp()
+const WEB_SERVERS_SCRIPT_PATH = "/opencode-web-servers.js"
+
+function configuredWebServers() {
+ const raw =
+ process.env.OPENCODE_WEB_SERVERS_JSON ||
+ (process.env.OPENCODE_WEB_SERVERS_JSON_B64
+ ? Buffer.from(process.env.OPENCODE_WEB_SERVERS_JSON_B64, "base64").toString("utf8")
+ : "")
+ if (!raw) return []
+
+ try {
+ const parsed = JSON.parse(raw)
+ if (!Array.isArray(parsed)) return []
+ return parsed.flatMap((server) => {
+ if (server && server.type === "http" && typeof server.displayName === "string" && typeof server.http?.url === "string") {
+ return [{ type: "http", displayName: server.displayName, http: { url: server.http.url } }]
+ }
+ if (server && typeof server.name === "string" && typeof server.url === "string") {
+ return [{ type: "http", displayName: server.name, http: { url: server.url } }]
+ }
+ return []
+ })
+ } catch {
+ return []
+ }
+}
+
+function webServersScript() {
+ const servers = configuredWebServers()
+ if (servers.length === 0) return ""
+ return `(() => {
+ try {
+ const key = "opencode.global.dat:server";
+ const seed = ${JSON.stringify(servers)};
+ const raw = localStorage.getItem(key);
+ const state = raw ? JSON.parse(raw) : {};
+ const byUrl = new Map();
+ for (const item of state.list || []) {
+ if (item && item.http && item.http.url) byUrl.set(item.http.url, item);
+ }
+ for (const item of seed) {
+ byUrl.set(item.http.url, { ...byUrl.get(item.http.url), ...item });
+ }
+ state.list = Array.from(byUrl.values());
+ state.projects = state.projects || {};
+ state.lastProject = state.lastProject || {};
+ localStorage.setItem(key, JSON.stringify(state));
+ } catch (error) {
+ console.warn("failed to apply configured OpenCode web servers", error);
+ }
+})();`
+}
+
+function injectWebServersScript(body: string) {
+ if (configuredWebServers().length === 0 || body.includes(WEB_SERVERS_SCRIPT_PATH)) return body
+ const script = ``
+ if (body.includes("")) return body.replace("", `${script}`)
+ return `${script}${body}`
+}
+
export function themePreloadHash(body: string) {
return body.match(/