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/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts
index a0eb78a13..dbb820b82 100644
--- a/packages/core/src/flag/flag.ts
+++ b/packages/core/src/flag/flag.ts
@@ -31,7 +31,7 @@ export const Flag = {
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
- OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"),
+ OPENCODE_DISABLE_FFF: fff === undefined ? true : truthy("OPENCODE_DISABLE_FFF"),
// Experimental
OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe(
diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts
index c439da0fd..4660a62c3 100644
--- a/packages/core/src/project.ts
+++ b/packages/core/src/project.ts
@@ -2,6 +2,7 @@ export * as ProjectV2 from "./project"
export * as Project from "./project"
import { Context, Effect, Layer, Schema } from "effect"
+import os from "os"
import path from "path"
import { AbsolutePath } from "./schema"
import { FSUtil } from "./fs-util"
@@ -109,7 +110,11 @@ export const layer = Layer.effect(
const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) {
const repo = yield* git.find(input)
- if (!repo) return { id: ID.global, directory: AbsolutePath.make(path.parse(input).root), vcs: undefined }
+ if (!repo) {
+ const filesystemRoot = AbsolutePath.make(path.parse(input).root)
+ const directory = input === filesystemRoot ? AbsolutePath.make(os.homedir()) : input
+ return { id: ID.global, directory, vcs: undefined }
+ }
const previous = yield* cached(repo.store)
const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo))
diff --git a/packages/core/test/project.test.ts b/packages/core/test/project.test.ts
index 45fba0d18..3c0dae0ab 100644
--- a/packages/core/test/project.test.ts
+++ b/packages/core/test/project.test.ts
@@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { $ } from "bun"
import fs from "fs/promises"
+import os from "os"
import path from "path"
import { Effect, Layer, Schema } from "effect"
import { ProjectV2 } from "@opencode-ai/core/project"
@@ -55,7 +56,7 @@ async function rootCommit(dir: string) {
}
describe("ProjectV2.resolve", () => {
- it.live("returns global for non-git directory", () =>
+ it.live("returns global for non-git directory without widening to filesystem root", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
@@ -66,7 +67,20 @@ describe("ProjectV2.resolve", () => {
const result = yield* project.resolve(abs(tmp.path))
expect(result.id).toBe(ProjectV2.ID.make("global"))
- expect(path.resolve(result.directory)).toBe(path.parse(tmp.path).root)
+ expect(result.directory).toBe(abs(tmp.path))
+ expect(result.previous).toBeUndefined()
+ expect(result.vcs).toBeUndefined()
+ }),
+ )
+
+ it.live("clamps explicit filesystem root to home for the global project", () =>
+ Effect.gen(function* () {
+ const project = yield* ProjectV2.Service
+
+ const result = yield* project.resolve(abs(path.parse(os.homedir()).root))
+
+ expect(result.id).toBe(ProjectV2.ID.make("global"))
+ expect(result.directory).toBe(abs(os.homedir()))
expect(result.previous).toBeUndefined()
expect(result.vcs).toBeUndefined()
}),
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(/