Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ file is the human-readable summary of what changed and why.

### Desktop / Web UI

- **First-run setup on the desktop app** — with no AI provider configured, the
desktop app now opens a setup dialog that cannot be dismissed until a
provider exists, offering the same three paths as the CLI's `codev
configure`: **Login with SSO** (sign in with your CoDev account in the
browser — with a paste-the-link fallback for machines the browser cannot
reach — then pick a default model), **Custom provider** (an OpenAI-compatible
endpoint), or **Connect provider** (Anthropic, OpenAI, and others). The gate
applies to the local server only; escaping any sub-flow returns to it, and
finishing setup in another window (or in the CLI) closes it automatically.
- **Signing in now reaches your sandboxes too** — a sandbox is handed its model
provider when it starts, so signing in afterwards left **Work** with no model
at all while **Code** had the one you just picked, and the only way to fix it
was stopping and starting the sandbox by hand. Finishing setup now restarts
the sandboxes that are up, so the model you chose is there in Work as well.
Only sandboxes that are running are touched, and nothing is interrupted:
setup is only offered while no provider is configured, so those sandboxes had
no model to work with either.
- **Upstream's hosted providers are no longer offered** — the OpenCode Zen
free models and OpenCode Go no longer appear anywhere in the desktop or web
UI (model pickers, connect dialog, settings), and a fresh install no longer
silently chats against them: with nothing configured there is now no model
to pick until a provider is set up.
- **Selecting a folder no longer lands on "This session cannot be found"** —
the app remembers the session you were last in for each folder of a sandbox,
and it kept sending you there after the session itself was gone: resetting a
Expand Down
8 changes: 8 additions & 0 deletions DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,15 @@ from the squash message first, or use a plain merge.
| `packages/app/src/context/server-sdk.tsx` | Event-stream failures log only after 3 consecutive failed attempts (`logStreamIssue`), with the error normalized to `name: message`; any received event resets the count. | The first failed attempt usually races a server being stopped or restarted (sandbox reset, sidecar relaunch): the stream dies before the renderer learns the server is going away and the context is disposed before a real retry, so logging attempt one produced a spurious `[global-sdk] event stream error` for normal lifecycle. A genuinely unreachable server still logs within about a second (250 ms reconnect delay). | Keep the threshold and shared `logStreamIssue`; take upstream's surrounding stream changes. |
| `packages/app/src/context/command.tsx` | The DEV duplicate-command-id warning fires only for duplicates still present 1 s later (`warnIfPersistentDuplicate`); runtime keep-first behavior unchanged. | Router transitions keep the outgoing page mounted while the incoming one registers, so ids both pages provide (`command.palette`, `input.focus`) are briefly duplicated on every draft→session navigation; only a duplicate that survives the transition is a registration bug. | Keep ours; take upstream's surrounding command changes. |
| `packages/desktop/src/main/logging.ts` | Added `initChromiumNoiseFilter`: an electron-log hook dropping two benign Chromium intervention messages (ResizeObserver loop, autofocus blocked) from the console transport for renderer-spied messages; file logs keep them. | The `spyRendererConsole` pipe surfaces Chromium interventions the app can neither cause nor fix as red terminal lines during `bun dev:desktop`. | Keep the hook; take upstream's surrounding logging changes. |
| `packages/app/src/context/global-sync/utils.ts` | `normalizeProviderList` returns through `filterHiddenProviders` (fork-only `src/gateway/hidden-providers.ts`): upstream's hosted providers `opencode` (Zen) and `opencode-go` are dropped from `all`/`default`, and the unauthenticated free-tier `opencode` from `connected` (a deliberately configured paid Zen key still counts as connected, mirroring the TUI's `isConnectedProvider`). | Client-side hiding at the one choke point both the global bootstrap and per-directory catalogs flow through — mirrors the TUI's dialog filters without writing `disabled_providers`. Downstream surfaces keyed off those catalog entries (`dialog-select-model-unpaid{,-v2}.tsx`, the `usage-exceeded-dialogs.tsx` Go upsell, Zen branches in `dialog-connect-provider.tsx`, hosted entries in `use-providers.ts` `popularProviders`, settings badges) become deliberately-dead code left byte-identical to upstream so merges stay cheap. | Keep the import + wrap; take upstream's normalization changes. Do not "clean up" the dead hosted-provider surfaces — they cost nothing and would churn every sync. |
| `packages/ui/src/context/dialog.tsx` | Per-dialog `persistent` flag (precedent: the TUI's persistent dialog, see the `packages/tui/src/ui/dialog.tsx` row): Escape, backdrop click, and Kobalte `onOpenChange` are no-ops for a dialog marked persistent; programmatic `close()` still works; `setPersistent` targets the top dialog so pushed sub-dialogs stay dismissable; `useDialog()` gains `persistent`/`setPersistent`. | The desktop configure gate needs a dialog the user cannot dismiss while no provider is configured. | Keep the flag and the three guards; take upstream's surrounding dialog changes. |
| `packages/app/src/app.tsx` | `<GatewayConfigureGate />` (fork-only `components/gateway/`) mounted as the first child inside `ServerScopedProviders`' `ModelsProvider`. | The desktop configure gate; the placement is load-bearing — completing the flow seeds the live `useModels()` recents store (`buildGatewayConfig` deliberately writes no `config.model` pin). | Keep the mount inside `ModelsProvider`; take upstream's surrounding shell changes. |
| `packages/app/src/context/platform.tsx` | Added optional `gatewaySso?: GatewaySsoPlatform` capability (wire types in fork-only `src/gateway/types.ts`, exported as `@opencode-ai/app/gateway/types` — see the `packages/app/package.json` row). | Desktop-only SSO login for the configure gate; absence of the capability is the web deployment's kill-switch. | Keep the field + import; take upstream's surrounding platform changes. |
| `packages/app/src/components/prompt-input-v2.tsx`, `packages/app/src/components/prompt-input.tsx` | `onUnpaidClick` opens `DialogConnectProvider` (lazy import) instead of the free-models "unpaid" dialogs; their now-unused imports dropped. | With the hosted providers hidden, "unpaid" means nothing is connected, and the free-models dialogs would render empty lists (this path is reachable on the web deployment; on desktop the gate covers it). | Keep the handler swap; take upstream's surrounding prompt-input changes. |
| `packages/desktop/src/renderer/index.tsx` | Calls `installConsoleFormatting()` (fork-only `./console-format`) before anything else logs. | `spyRendererConsole` receives Chromium's flattened console-message string, so any renderer `console.*` with an object argument reached the terminal and the exported debug logs as `[object Object]`; the wrapper serializes structured arguments (Error-aware, circular-safe, truncated) at the source. | Keep the import and call; take upstream's surrounding entry changes. |
| Desktop gateway SSO wiring: `packages/desktop/src/main/index.ts`, `packages/desktop/src/preload/{index,types}.ts`, `packages/desktop/src/renderer/index.tsx` | Main instantiates the fork-only `src/main/gateway-sso/` controller (wrapping `@codev/gateway` `login`/`silentSso` — node-only loopback OAuth — with `shell.openExternal` and a `net.fetch` wrapper) and registers its `gateway-sso-*` IPC channels next to the sandbox ones, passing an `onConfigured` callback that restarts the running sandboxes (`SandboxController.restartRunning`) because a guest is handed its codev config once at boot and cannot see credentials that appear afterwards; preload exposes the `gatewaySso` namespace (subscribe fan-out, sandbox pattern) with types re-exported from `@opencode-ai/app/gateway/types`; the renderer platform maps `gatewaySso: window.api.gatewaySso`. | SSO login for the desktop configure gate must run in the main process (`node:http` loopback server, `~/.codev/auth.json` writes); `net.fetch` so enterprise proxies/PAC apply (egress precedent). | Keep the wiring; take upstream's surrounding changes. Keep channel names `gateway-sso-*` stable. |
| `packages/app/package.json` | Added `"./gateway/types": "./src/gateway/types.ts"` to `exports`. | The desktop preload aliases the app-owned gateway SSO wire types (sandbox/types pattern). | Keep the export; take upstream's other manifest changes. |
| `packages/app/src/i18n/{en,es}.ts` | Added the `gateway.configure.*` key family (configure-gate dialog) and reworded `sidebar.gettingStarted.line1` — the "includes free models" pitch no longer holds with the hosted providers hidden. | Strings for the fork's configure gate; the getting-started card must not advertise models the fork does not offer. | Keep our keys and copy; take upstream's surrounding i18n changes. |
| `packages/desktop/src/main/updater.ts` | `autoUpdater.allowDowngrade` `true` → `false`. | Upstream's `true` is safe against immutable GitHub release tags. The fork's feed is a mutable bucket path, where re-uploading or failing to replace a stale `latest.yml` would actively downgrade every client on the next 10-minute poll. | Keep `false`; take upstream's other updater settings. `channel = "latest"` and `allowPrerelease = false` stay as upstream has them — the first must match `publish.channel` in `electron-builder.config.ts`, the second is inert for the `generic` provider (it only steers GitHub tag resolution). |
| Dual-path WSL install: `packages/desktop/src/main/wsl/{runtime,servers}.ts` | `runtime.ts`: three internals gain `export` (`runPowerShell`, `runInteractiveCommand`, `resolveSystem32Command`) — no behavior change. `servers.ts`: the `install-wsl` job calls `installWslRuntimeDualPath` and the `install-distro` job calls `installWslDistroDualPath` (both from the fork-only `install-bundle.ts`, which holds all new logic) instead of `installWslRuntimeElevated` / `installWslDistro` directly; both return the same `WslCommandResult` shape, so the surrounding job/error handling is untouched. | The customer fleet sits behind whitelist-only proxies that cannot reach Microsoft's servers. The dual path downloads the pinned bundle (`wsl.zip`: runtime MSI + Ubuntu-24.04 image) from the codev storage host, enables VirtualMachinePlatform via offline dism + installs the MSI behind one UAC prompt, and falls back to upstream's `wsl --install` / `--web-download` paths only when the bundle cannot be *obtained* — a completed-but-failed elevated install is returned as-is, never retried behind a second UAC prompt. | Keep the two call swaps and the three `export` keywords; take upstream's surrounding changes. If upstream reworks `installWslRuntimeElevated` / `installWslDistro`, keep them exported from `runtime.ts` — `install-bundle.ts` calls them as the fallback path. |
| Async settings stores: `packages/desktop/src/main/{store,index}.ts`, `packages/desktop/package.json` | `store.ts` no longer uses electron-store: `getStore` now returns the fork-only `store-file.ts` store — in-memory state, flushed write-behind through `store-writer.ts`'s serialized latest-wins async atomic writer (temp → fsync → rename via `fs.promises`, so the fsync runs on the libuv threadpool). Adds `flushStoreWrites(timeoutMs)`; `removeStoreFile`/`removeStoreFileIfEmpty` discard/flush the writer around the delete so a late flush cannot resurrect the file. `index.ts`: `stopSidecars` awaits `flushStoreWrites(1500)`. `package.json`: the `electron-store` dependency is removed. On-disk format is unchanged (flat keys, tab-indented JSON, no extension) — existing files load as-is; a corrupt file now starts empty with a logged error instead of throwing from every accessor. | conf v15's `store.set` is a synchronous fsync on the Electron main thread, and one of those parked the whole app in uninterruptible disk sleep (kernel journal stall + lost ext4 fast-commit wakeup, observed live during a sandbox mount-apply restart): event loop dead, IPC dead, quit impossible. conf also rereads the file on every access, which makes write-behind unsound with the stock class (a set on one key reads a stale file and drops a still-queued update), so the storage layer had to be replaced rather than patched. | Keep the fork store; take upstream's surrounding changes. If upstream adds electron-store options (schema, migrations, dot-notation) or new store call sites, extend `store-file.ts` to match — its API is the superset the desktop actually uses (`get/set/has/delete/clear/store/path`). Do not reintroduce a synchronous write path on the main thread. |
Expand Down
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"./updater": "./src/updater.ts",
"./wsl/types": "./src/wsl/types.ts",
"./sandbox/types": "./src/sandbox/types.ts",
"./gateway/types": "./src/gateway/types.ts",
"./vite": "./vite.js",
"./index.css": "./src/index.css"
},
Expand Down
9 changes: 8 additions & 1 deletion packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { HighlightsProvider } from "@/context/highlights"
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
import { LayoutProvider } from "@/context/layout"
import { ModelsProvider } from "@/context/models"
import { GatewayConfigureGate } from "@/components/gateway/gateway-configure-gate"
import { NotificationProvider } from "@/context/notification"
import { PermissionProvider } from "@/context/permission"
import { usePlatform } from "@/context/platform"
Expand Down Expand Up @@ -350,7 +351,13 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
return (
<LayoutProvider>
{props.serverScoped}
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
{/* Inside ModelsProvider: completing the configure flow seeds the live
models recents store, which is how the picked model becomes the
selection (buildGatewayConfig writes no config.model pin). */}
<ModelsProvider directory={props.directory}>
<GatewayConfigureGate />
{props.children}
</ModelsProvider>
</LayoutProvider>
)
}
Expand Down
Loading
Loading