From c526dcda529a617a1365f06728e2b0a29f21acb3 Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Tue, 25 Aug 2026 09:24:29 +0700 Subject: [PATCH 1/2] feat(desktop): sign-in toast for stalled session uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Session upload is paused" warning was an OS notification with nothing to click: the user had to find the sign-in themselves, and Electron only renders notification action buttons on macOS — on Linux the notification can be dropped by the daemon entirely. Main now reads shouldNudgeSignIn() once at startup into a one-shot flag handed out over gateway-sso-consume-upload-nudge, and the configure gate raises a persistent in-app toast whose Log in action opens the same DialogConfigure the menu item does. Consuming the flag keeps the toast to one window instead of one per window. The menu item it points at is renamed "Get a new API Key..." -> "Log in/Configure" (macOS app menu, Windows/Linux File menu, and the command palette via command.gateway.apiKey.new in en/es/vi). The command id is unchanged — it is the wire name the desktop menus dispatch. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 39 ++++++++++++------- DIVERGENCES.md | 4 +- .../gateway/gateway-configure-gate.tsx | 22 ++++++++++- packages/app/src/desktop-menu.ts | 6 +-- packages/app/src/gateway/types.ts | 5 +++ packages/app/src/i18n/en.ts | 7 +++- packages/app/src/i18n/es.ts | 7 +++- packages/app/src/i18n/vi.ts | 7 +++- packages/desktop/src/main/gateway-sso/ipc.ts | 9 ++++- packages/desktop/src/main/index.ts | 31 ++++++++------- packages/desktop/src/preload/index.ts | 1 + 11 files changed, 101 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c34aa5a7..03335ab64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,26 +169,37 @@ file is the human-readable summary of what changed and why. auto-upload stay quiet. No sessions are lost while stalled — everything pending ships on the first upload after signing in. ([#312](https://github.com/quickbeard/codev-code/pull/312)) -- **CoDev Desktop uploads sessions too** — the background session upload used - to exist only in the terminal app; the desktop's local sessions never +- **CoDev Desktop uploads sessions too** — the background session upload + used to exist only in the terminal app; the desktop's local sessions never reached the analysis backend. The desktop's embedded server now runs the same uploader at every app start, under the same rules: signed-in machines only, deduplicated, shared lock with the terminal app and the hub so nothing double-uploads, and `CODEV_DISABLE_AUTO_UPLOAD=1` turns it off. Signing in from the desktop's gateway setup uploads the pending backlog immediately, and when uploads stall because the CoDev sign-in expired, the - desktop shows a system notification at startup — its version of the - terminal app's "Session upload is paused" warning. Work (sandbox) sessions - are covered too: each sandbox keeps its sessions in its own guest database - that the host cannot read as a file, so whenever a sandbox comes up, the - desktop reads its sessions over the sandbox's own authenticated API and - uploads them like any others — filed under the project folder the sandbox - belongs to, alongside that project's local sessions. A sandbox that is off - simply uploads on its next start; nothing is lost in between. An **Upload - Session Logs** menu item (under "Get a new API Key..." in the CoDev menu on - macOS, the File menu on Windows/Linux) runs the same sweep on demand — for - when you want a session analyzed now rather than at the next launch. + desktop shows a "Session upload is paused" toast at startup — its version + of the terminal app's warning. Work (sandbox) sessions are covered too: + each sandbox keeps its sessions in its own guest database that the host + cannot read as a file, so whenever a sandbox comes up, the desktop reads + its sessions over the sandbox's own authenticated API and uploads them + like any others — filed under the project folder the sandbox belongs to, + alongside that project's local sessions. A sandbox that is off simply + uploads on its next start; nothing is lost in between. An **Upload Session + Logs** menu item (under **Log in/Configure** in the CoDev menu on macOS, + the File menu on Windows/Linux) runs the same sweep on demand — for when + you want a session analyzed now rather than at the next launch. ([#313](https://github.com/quickbeard/codev-code/pull/313)) +- **The "Session upload is paused" warning can now sign you back in** — the + warning that your sessions stopped uploading arrived as a system + notification with nothing to click: you had to go find the sign-in yourself, + and on Linux the notification often never appeared at all. It is now a toast + inside the app with a **Log in** button that opens the same sign-in dialog + the menu item does, and it stays put until you dismiss it instead of fading + while you are reading code. That menu item is now called **Log + in/Configure** — in the **CoDev** menu on macOS, under **File** on Windows + and Linux, and in the command palette everywhere — since signing back in, + rather than minting a key, is what it is usually reached for. + ([#319](https://github.com/quickbeard/codev-code/pull/319)) - **A dropped model connection retries instead of silently stopping the agent** — when the connection to the model died mid-response (corporate proxies are fond of cutting long-lived streams), the session just went idle: no error, no @@ -501,7 +512,7 @@ already in use` and no way forward short of finding the file in a temporary it starts: if it has been rejected, it quietly gets a new one using your existing session, and you never see it happen. Only when that is not possible — no key at all, or a session too old to renew — does it ask you to sign in. - And there is now a **Get a new API Key...** item to do it yourself at any + And there is now a **Log in/Configure** item to do it yourself at any time: in the **CoDev** menu on macOS, under **File** on Windows and Linux, both next to Settings, and in the command palette on every platform. A gateway that cannot be reached is never treated as a bad key, so a flaky diff --git a/DIVERGENCES.md b/DIVERGENCES.md index d0ad1ce1a5..14275003c0 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -95,7 +95,7 @@ from the squash message first, or use a plain merge. | `package.json` (root) | `name` `opencode` → `codev-code`; `repository.url` → `github.com/quickbeard/codev-code`; added `release` script delegating to packages/opencode; added `bump` script pointing at the fork-only `script/bump-version.ts`; removed the `sso`, `dev:stats`, and `random` scripts; added the `lint-staged` devDependency and a `lint-staged` config block (`"*": "prettier --write --ignore-unknown"`) driving the fork-only `.husky/pre-commit` | The root manifest carries the fork's name (nothing reads it — root is private and unpublished; `packages/script` reads only `packageManager` — so this is cosmetic and also removes the name collision with the `packages/opencode` workspace member, whose name MUST stay `opencode`: build.ts derives bun compile targets and dist dir names from it). `bun run release` and `bun run bump` work from the repo root. `sso` logs into upstream's AWS org (`--sso-session=opencode`) and `dev:stats` runs inside an sst shell against upstream's production stage — neither works without anomalyco credentials; `random` is inert cruft (`echo 'Random script'`). `lint-staged` keeps staged files at Prettier's fixed point on commit, so the formatting drift PR #261 had to clean up cannot silently reaccumulate; `--ignore-unknown` skips files Prettier has no parser for (`bun.lock`, `.husky/*`, binaries) and `.prettierignore` still exempts `DIVERGENCES.md` and the docs merge base. | Keep our name + release/bump lines and the removals (drop any new upstream-infra-only scripts too); take upstream's other fields. | | `packages/opencode/package.json` | bin key `opencode` → `codev` (shim path `./bin/opencode` unchanged); added `release` script; added dependencies `"@codev/brand": "workspace:*"`, `"@codev/gateway": "workspace:*"` (the latter for the gateway-key migration's shared-auth-file read, see the config.ts migration row), and `"@codev/upload": "workspace:*"` (the session uploader — see the session-upload wiring row) | Installed command is `codev` — matches the CLI's `scriptName` and help text (the hub's command is `codevhub`; it relinquished the `codev` bin in codev-ai ≥ 0.4.0). `bun run release` publishes the version in this file (upstream-synced; script/release.ts is fork-only). | Keep our bin key, release line, and dependency lines; take upstream's other fields including `version`. | | Session-upload wiring: `packages/opencode/src/cli/cmd/tui.ts`, `packages/opencode/src/cli/cmd/run.ts`, `packages/opencode/src/cli/cmd/debug/index.ts` | tui.ts + run.ts: `spawnUploadDaemon()` (from the fork-only `@codev/upload` package) fire-and-forgotten at handler start — spawns a detached, unref'd, stdio-ignored `codev debug upload` child. debug/index.ts: registered the fork-only `UploadCommand` (debug/upload.ts, new file), the entry that child runs. | Standalone parity with the hub's launch-time upload daemon: every agent launch ships prior codev sessions to the analysis backend in the background. All gating (opt-out env `CODEV_DISABLE_AUTO_UPLOAD`, signed-in check, the hub-shared lockfile) lives inside `@codev/upload` — the wiring is one guarded call per entry point that can never block or fail the launch. | Keep the import + `spawnUploadDaemon()` line in both handlers and the debug registration; take upstream's surrounding changes. | -| Desktop session upload: `packages/desktop/src/main/sidecar.ts`, `packages/desktop/src/main/server.ts`, `packages/desktop/src/main/index.ts`, `packages/desktop/package.json` | sidecar.ts: after `Server.listen` succeeds, fire `runStartupUpload()` (from the fork-only `@codev/upload`, statically imported so electron-vite bundles it), and handle a `{type: "upload", servers?}` sidecar command that re-runs it — runs are chained on an in-process promise (the hub-shared lockfile makes a concurrent run skip, not queue, so an unchained sandbox run posted right after startup's local run would silently drop its work sessions). server.ts: `SidecarListener` gains `upload(servers?)` (posts that command with structured-clone-safe `ServerSource` records; no-op once exited). index.ts: `sandboxUploadSources()` maps every `ready` sandbox to a source (`runtime.url`/`username`/`password` + `config.projectDir`, the HOST dir, so a project's local and sandboxed sessions land in one analysis bucket); a `sandboxes.subscribe` diff fires `server?.upload(sources)` whenever a sandbox newly reaches ready (its whole history becomes readable at that moment), with a post-`initialize` catch-up for sandboxes ready before the sidecar listened; the gateway-sso `onConfigured` hook also calls `server?.upload(sandboxUploadSources())` so a fresh sign-in ships the backlog (work sessions included) immediately; an "Upload Session Logs" menu item (desktop-menu.ts: the fork-only `upload.sessions` `DesktopMenuAction`, placed under "Get a new API Key..." in the macOS app menu and the Windows/Linux File menu; threaded through desktop-menu-actions.ts, menu.ts and ipc.ts's handler deps, generic renderer dispatch already covers it) runs the same sweep on demand with an "Uploading session logs" Notification as feedback; and startup shows an OS `Notification` ("Session upload is paused") when `@codev/upload/nudge`'s `shouldNudgeSignIn()` reports the stall — the desktop counterpart of the TUI toast, every launch until sign-in. package.json: `"@codev/upload": "workspace:*"` under devDependencies — deliberately, like `@codev/gateway`: electron-vite externalizes `dependencies` into the (unshipped) runtime node_modules, and only devDependency workspace packages get bundled. | The desktop runs the server in-process under Electron's Node with no `codev` CLI in the bundle, so the CLI's detached `codev debug upload` spawn cannot cover it; running the uploader inside the sidecar utility process (never Electron main, which is UI-latency sensitive) reuses the whole pipeline — same store (the sidecar only overrides `XDG_STATE_HOME`), same shared auth file, same hub-shared lockfile, so desktop, CLI, and hub can never upload concurrently. The `#sqlite` bun/node conditional import inside `@codev/upload` (same pattern as the server core's `#db`) is what lets the identical package run on `bun:sqlite` in the CLI binary and `node:sqlite` in the sidecar. | Keep the sidecar `upload` command + startup trigger, the listener's `upload()`, the `onConfigured` call + startup Notification, and the devDependency placement; take upstream's surrounding changes. | +| Desktop session upload: `packages/desktop/src/main/sidecar.ts`, `packages/desktop/src/main/server.ts`, `packages/desktop/src/main/index.ts`, `packages/desktop/package.json` | sidecar.ts: after `Server.listen` succeeds, fire `runStartupUpload()` (from the fork-only `@codev/upload`, statically imported so electron-vite bundles it), and handle a `{type: "upload", servers?}` sidecar command that re-runs it — runs are chained on an in-process promise (the hub-shared lockfile makes a concurrent run skip, not queue, so an unchained sandbox run posted right after startup's local run would silently drop its work sessions). server.ts: `SidecarListener` gains `upload(servers?)` (posts that command with structured-clone-safe `ServerSource` records; no-op once exited). index.ts: `sandboxUploadSources()` maps every `ready` sandbox to a source (`runtime.url`/`username`/`password` + `config.projectDir`, the HOST dir, so a project's local and sandboxed sessions land in one analysis bucket); a `sandboxes.subscribe` diff fires `server?.upload(sources)` whenever a sandbox newly reaches ready (its whole history becomes readable at that moment), with a post-`initialize` catch-up for sandboxes ready before the sidecar listened; the gateway-sso `onConfigured` hook also calls `server?.upload(sandboxUploadSources())` so a fresh sign-in ships the backlog (work sessions included) immediately; an "Upload Session Logs" menu item (desktop-menu.ts: the fork-only `upload.sessions` `DesktopMenuAction`, placed under "Get a new API Key..." in the macOS app menu and the Windows/Linux File menu; threaded through desktop-menu-actions.ts, menu.ts and ipc.ts's handler deps, generic renderer dispatch already covers it) runs the same sweep on demand with an "Uploading session logs" Notification as feedback; and `@codev/upload/nudge`'s `shouldNudgeSignIn()` is read once at startup into a one-shot `let` that `registerGatewaySsoIpcHandlers`' second argument hands to the first renderer that asks (see the gateway key recovery row), which raises the "Session upload is paused" toast — the desktop counterpart of the TUI toast, every launch until sign-in. Consumed rather than re-read per request so the answer cannot flip mid-session and so a second window does not repeat a toast the user is already looking at; it replaced an OS `Notification`, which carries no action button outside macOS and on Linux depends on a notification daemon that may drop it entirely. package.json: `"@codev/upload": "workspace:*"` under devDependencies — deliberately, like `@codev/gateway`: electron-vite externalizes `dependencies` into the (unshipped) runtime node_modules, and only devDependency workspace packages get bundled. | The desktop runs the server in-process under Electron's Node with no `codev` CLI in the bundle, so the CLI's detached `codev debug upload` spawn cannot cover it; running the uploader inside the sidecar utility process (never Electron main, which is UI-latency sensitive) reuses the whole pipeline — same store (the sidecar only overrides `XDG_STATE_HOME`), same shared auth file, same hub-shared lockfile, so desktop, CLI, and hub can never upload concurrently. The `#sqlite` bun/node conditional import inside `@codev/upload` (same pattern as the server core's `#db`) is what lets the identical package run on `bun:sqlite` in the CLI binary and `node:sqlite` in the sidecar. | Keep the sidecar `upload` command + startup trigger, the listener's `upload()`, the `onConfigured` call + the startup nudge flag, and the devDependency placement; take upstream's surrounding changes. | | `bun.lock` | Mirrors the bin key rename above, the fork-only workspace packages/deps (`@codev/gateway`, `@codev/brand`, `@codev/upload`), and the fork-only `lint-staged` devDependency | Lockfile records workspace bin mappings and workspace deps. Known quirk: the root workspace stanza still says `"name": "opencode"` — bun never syncs the root manifest's name into an existing lock (verified: neither `bun install` nor `--lockfile-only` rewrites it); it self-corrects only on a from-scratch regeneration and is harmless meanwhile. Don't hand-edit it. | Regenerate with `bun install`; never hand-merge. | | `.prettierignore` | Appended `DIVERGENCES.md` and `packages/web/src/content/docs/` (each with a comment saying why) | Prettier realigns every row of this file's changed-files table, so a one-row edit churns all ~120 rows — noise in the one file that records upstream-conflict guidance, and a guaranteed conflict on the next sync. Nothing in the repo formatted it before (the pre-push hook only runs `bun typecheck`), so this pins the status quo rather than changing it. The docs directory is the upstream merge base (see `packages/web/FORK-DOCS.md`): nothing builds it, its whole job is to surface upstream doc diffs during a sync, and formatting it only adds conflict surface — plus Prettier's MDX printer mangles `index.mdx`, collapsing the fenced code blocks inside `` into inline text. | Keep our lines; take upstream's other entries. | | `packages/tui/package.json` | Added dependencies `"@codev/gateway": "workspace:*"`, `"@codev/brand": "workspace:*"`, and `"@codev/upload": "workspace:*"` (the latter's `/nudge` + `/spawn` subpaths only — sqlite-free modules for the upload sign-in toast and the post-configure daemon respawn) | The fork's gateway configure dialog imports SSO/backend/config-block logic from the fork-only `packages/codev-gateway`; the "Open docs" command takes its URL from `DOCS_URL` in the fork-only `packages/codev-brand` instead of hardcoding the host. | Keep our dependency lines; take upstream's other fields. | @@ -295,7 +295,7 @@ from the squash message first, or use a plain merge. | Dialogs close on a button, never on an outside click: `packages/ui/src/components/dialog.tsx`, `packages/ui/src/components/dialog.css`, `packages/ui/src/v2/components/dialog-v2.tsx`, `packages/ui/src/v2/components/dialog-v2.css`, `packages/ui/src/components/image-preview.tsx`, plus the `*.stories.tsx` beside the two dialogs | All three `Kobalte.Content`s take `onInteractOutside={(e) => e.preventDefault()}`, which is what actually blocks Kobalte's dismissable layer (the context row above only removes the explicit overlay click). Both `Dialog`s gained `closeLabel?`/`hideClose?` and now render the `Kobalte.CloseButton` themselves, as the last child of their content, absolutely positioned at the content's top-right — v1 at 16px/20px (12px/12px when `data-no-header`), v2 at 14px/14px, both at `z-index: 30` so they clear the settings tab header's sticky `z-index: 10`. The button was removed from v2's `DialogHeader` (which loses its `hideClose`/`closeLabel` props and its `data-hide-close` attribute) and from v1's header `Switch`, whose `action` branch used to *replace* it; `action` now simply renders. Headers reserve the corner in `padding-right` (v1 52px, v2 42px) and v1's title takes `flex: 1`; both containers take a `min-height` (v1 52px, v2 48px) so a short `fit` dialog cannot clip the button. `aria-label` defaults to `ui.common.close` through `useI18n` — the first v2 component to use the UI i18n context, which has an English fallback. | Every dialog needed a visible way out once the backdrop stopped being one, including the dozen whose call sites passed `hideClose`. Pinning the button to the content corner rather than laying it out in the header is what lets header-less dialogs (settings, release notes, the command palettes) have one at all; keeping it inside `Kobalte.Content` keeps it inside the focus trap and the modal's pointer-events layer. | Keep the corner button and the `onInteractOutside` guards. Upstream owns the header/body/footer layout — take its changes and re-apply the `padding-right` reserve and the close button on top. If upstream adds its own close button to `DialogHeader`, drop ours from `Dialog` rather than shipping two. | | Dialog close buttons at the call sites: `packages/app/src/sandbox/{dialog-add,dialog-add-default,dialog-delete,dialog-mounts}.tsx`, `packages/app/src/wsl/{dialog-add-server,dialog-delete}.tsx`, `packages/app/src/components/{dialog-manage-models,dialog-connect-provider,dialog-select-model-unpaid-v2,session-actions-menu,settings-v2/dialog-server-v2}.tsx`, `packages/app/src/components/gateway/dialog-configure.tsx`, `packages/app/src/components/dialog-select-file.tsx`, `packages/app/src/components/dialog-command-palette-v2.css` | Dropped every `hideClose`/`closeLabel` prop from `DialogHeader` (the button moved to `Dialog`, and hiding it is no longer wanted anywhere it was hidden). `dialog-configure` passes `hideClose` on its `Dialog` instead — it is the configure gate, which the dialog stack also marks `persistent`, so a close button there would be inert. The two search-first dialogs reserve the corner the button now occupies: `dialog-select-file` via `[&_[data-slot=list-search-wrapper]]:pr-10`, the v2 command palette via `padding-right` on `.command-palette-v2-search`. | Fork-only sandbox/WSL/gateway dialogs aside, these are upstream files whose only change is a removed prop. | Take upstream's version of these call sites and re-drop `hideClose`. The `hideClose` on `dialog-configure` is fork-only and must stay. | -| Gateway key recovery: `packages/app/src/desktop-menu.ts`, `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/preload/{index,types}.ts` | A "Get a new API Key..." item dispatching the fork-only `gateway.apiKey.new` command — in the macOS app menu under Settings, and in the Windows/Linux **File** menu (`platforms: ["windows"]`) under its Settings, the same split Settings itself uses since those builds render no app menu. i18n gains `command.gateway.apiKey.new`. Preload gains the `gateway-sso-ensure-key` channel on the existing `gatewaySso` namespace. The logic is fork-only: `packages/desktop/src/main/gateway-sso/controller.ts` gains `ensureKey()`, the desktop twin of codev-hub's `ensureFreshGatewayKey` — validate the stored key against the gateway's `/key/info` (`@codev/gateway`'s already-exported `validateApiKey`, previously with no production caller), bounded by a 2.5s race so a slow gateway cannot delay startup; a definite 401/403 tries `silentSso` + `fetchApiKey` and re-saves the record with every other field intact, returning the credential for the renderer to merge into the local server's auth registry (only the renderer writes to the server); anything else is left alone. `packages/app/src/components/gateway/gateway-configure-gate.tsx` (fork-only) registers the command behind its existing driver election and runs the pre-flight once per window after sync settles, opening `DialogConfigure` only on `needs-login`. `DialogConfigure` gains a `dismissable` prop driving both `setPersistent` and `hideClose`: the gate's copy stays trapped (the provider-setup screen the dialog rework deliberately exempts), while the two new entry points — the menu command and the pre-flight — are escapable, since a provider is configured in both cases and the dialog otherwise has no close button, no backdrop dismissal and no Escape. | Gateway keys expire on a schedule CoDev does not control, and nothing in the desktop app noticed: `DialogConfigure` had exactly one caller, the gate, which fires only when no provider is connected — and a dead key is still a connected provider, so every message 401'd with the sign-in flow unreachable. The silent re-mint mirrors what the hub already does for CLI launches, so the common case costs the user nothing. | Keep the menu item on both platforms (dropping the `platforms: ["windows"]` twin loses it entirely on Windows/Linux, which render no app menu) and keep the pre-flight's "only a definite rejection acts" rule — treating an unreachable gateway as a dead key would lock people out on a flaky network. Take upstream's surrounding menu/i18n/preload changes. | +| Gateway key recovery: `packages/app/src/desktop-menu.ts`, `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/preload/{index,types}.ts` | A "Log in/Configure" item dispatching the fork-only `gateway.apiKey.new` command — in the macOS app menu under Settings, and in the Windows/Linux **File** menu (`platforms: ["windows"]`) under its Settings, the same split Settings itself uses since those builds render no app menu. The label names the sign-in, not the key: re-authenticating is what the item is reached for, minting a key is the side effect, and the session-upload toast's **Log in** button dispatches the same command. The command id stays `gateway.apiKey.new` — it is the wire name the desktop menus dispatch. i18n gains `command.gateway.apiKey.new` and `toast.sessionUpload.paused.{title,description,action.login}`. Preload gains the `gateway-sso-ensure-key` and `gateway-sso-consume-upload-nudge` channels on the existing `gatewaySso` namespace (the nudge rides this namespace because signing back in is its entire remedy, and `GatewaySsoPlatform` in the fork-only `packages/app/src/gateway/types.ts` is where it can be typed without touching upstream's `platform.tsx`). The logic is fork-only: `packages/desktop/src/main/gateway-sso/controller.ts` gains `ensureKey()`, the desktop twin of codev-hub's `ensureFreshGatewayKey` — validate the stored key against the gateway's `/key/info` (`@codev/gateway`'s already-exported `validateApiKey`, previously with no production caller), bounded by a 2.5s race so a slow gateway cannot delay startup; a definite 401/403 tries `silentSso` + `fetchApiKey` and re-saves the record with every other field intact, returning the credential for the renderer to merge into the local server's auth registry (only the renderer writes to the server); anything else is left alone. `packages/app/src/components/gateway/gateway-configure-gate.tsx` (fork-only) registers the command behind its existing driver election and runs the pre-flight once per window after sync settles, opening `DialogConfigure` only on `needs-login`; on mount it also consumes the session-upload sign-in nudge (see the desktop session upload row) and raises a persistent toast whose **Log in** action is that same `openConfigure`. `DialogConfigure` gains a `dismissable` prop driving both `setPersistent` and `hideClose`: the gate's copy stays trapped (the provider-setup screen the dialog rework deliberately exempts), while the two new entry points — the menu command and the pre-flight — are escapable, since a provider is configured in both cases and the dialog otherwise has no close button, no backdrop dismissal and no Escape. | Gateway keys expire on a schedule CoDev does not control, and nothing in the desktop app noticed: `DialogConfigure` had exactly one caller, the gate, which fires only when no provider is connected — and a dead key is still a connected provider, so every message 401'd with the sign-in flow unreachable. The silent re-mint mirrors what the hub already does for CLI launches, so the common case costs the user nothing. | Keep the menu item on both platforms (dropping the `platforms: ["windows"]` twin loses it entirely on Windows/Linux, which render no app menu) and keep the pre-flight's "only a definite rejection acts" rule — treating an unreachable gateway as a dead key would lock people out on a flaky network. Take upstream's surrounding menu/i18n/preload changes. Keep the "Log in/Configure" label paired with the toast action label — they name one flow. | | `packages/core/src/filesystem/search.ts`, `packages/app/e2e/utils/mock-server.ts` | `fffLayer`'s `find` drops the worktree-root entry: fff ranks the root itself into an empty-query result, where it leads every list as a bare `/` — a mention naming no file — while the ripgrep fallback never emits it. The e2e mock server gains an `agents` config field (defaulting to the previous single primary agent) so a spec can supply subagents; the `@` menu hides primary agents, so the default list left nothing to rank files against. | Empty-query `@` menus (see the `@` autocomplete rows) made the root visible for the first time, at the top of the list. Both search layers should answer the same way. | Keep the root filter and the `agents` default; take upstream's surrounding changes. | diff --git a/packages/app/src/components/gateway/gateway-configure-gate.tsx b/packages/app/src/components/gateway/gateway-configure-gate.tsx index 7e30d2fda1..2551a31600 100644 --- a/packages/app/src/components/gateway/gateway-configure-gate.tsx +++ b/packages/app/src/components/gateway/gateway-configure-gate.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, onCleanup } from "solid-js" +import { createEffect, createSignal, onCleanup, onMount } from "solid-js" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" @@ -7,6 +7,7 @@ import { useServer } from "@/context/server" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { shouldOpenConfigureGate } from "@/gateway/configure-gate" +import { showToast } from "@/utils/toast" import { DialogConfigure } from "./dialog-configure" // ServerScopedProviders can be mounted by the legacy and new shells at the @@ -93,6 +94,25 @@ export function GatewayConfigureGate() { ], ) + // The background session upload stalls when the CoDev SSO session dies, and + // nothing else notices — the gateway api key it runs on keeps working. Main + // reports that stall once per launch (index.ts consumes the flag), so this + // fires in exactly one window even though the gate mounts in each. Persistent + // rather than timed: a toast that auto-dismisses while the user is reading + // code is the same silence the nudge exists to break. + onMount(() => { + void platform.gatewaySso?.consumeUploadSignInNudge().then((stalled) => { + if (!stalled) return + showToast({ + persistent: true, + icon: "cloud-upload", + title: language.t("toast.sessionUpload.paused.title"), + description: language.t("toast.sessionUpload.paused.description"), + actions: [{ label: language.t("toast.sessionUpload.paused.action.login"), onClick: openConfigure }], + }) + }) + }) + // Startup key pre-flight. The gate above only fires when no provider is // connected, so a stored key that the gateway has since rejected left the app // configured on paper and 401ing on every message, with no way back to the diff --git a/packages/app/src/desktop-menu.ts b/packages/app/src/desktop-menu.ts index be61f1140d..4602053b7a 100644 --- a/packages/app/src/desktop-menu.ts +++ b/packages/app/src/desktop-menu.ts @@ -82,7 +82,7 @@ export const DESKTOP_MENU: DesktopMenu[] = [ { type: "item", role: "about" }, { type: "item", label: "Check for Updates...", action: "app.checkForUpdates", enabled: "updater" }, { type: "item", label: "Settings", command: "settings.open", accelerator: { macos: "Cmd+," } }, - { type: "item", label: "Get a new API Key...", command: "gateway.apiKey.new" }, + { type: "item", label: "Log in/Configure", command: "gateway.apiKey.new" }, { type: "item", label: "Upload Session Logs", action: "upload.sessions" }, { type: "item", label: "Reload Webview", action: "view.reload" }, { type: "item", label: "Restart", action: "app.relaunch" }, @@ -117,11 +117,11 @@ export const DESKTOP_MENU: DesktopMenu[] = [ // Windows/Linux menu does not render — same split Settings itself uses. { type: "item", - label: "Get a new API Key...", + label: "Log in/Configure", command: "gateway.apiKey.new", platforms: ["windows"], }, - // macOS carries this in the app menu below its API-key sibling. + // macOS carries this in the app menu below its sign-in sibling. { type: "item", label: "Upload Session Logs", diff --git a/packages/app/src/gateway/types.ts b/packages/app/src/gateway/types.ts index c340783b0d..9001641c3d 100644 --- a/packages/app/src/gateway/types.ts +++ b/packages/app/src/gateway/types.ts @@ -72,4 +72,9 @@ export type GatewaySsoPlatform = { // Validates the stored key, refreshing it silently when it is rejected and // the SSO session still allows it. Never prompts. ensureKey(): Promise + // Whether the background session upload stalled because the CoDev SSO + // session died, and this launch should say so. Lives on the SSO namespace + // because signing back in is the whole remedy. Consumed: the main process + // hands the stall to the first asker, so a second window stays quiet. + consumeUploadSignInNudge(): Promise } diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index e977a6e60b..c57687d0e5 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -28,7 +28,7 @@ export const dict = { "command.project.index": "Switch to project {{index}}", "command.provider.connect": "Connect provider", "command.server.switch": "Switch server", - "command.gateway.apiKey.new": "Get a new API key", + "command.gateway.apiKey.new": "Log in/Configure", "command.settings.open": "Open settings", "command.session.previous": "Previous session", "command.session.next": "Next session", @@ -658,6 +658,11 @@ export const dict = { "toast.update.action.installRestart": "Install and restart", "toast.update.action.notYet": "Not yet", + "toast.sessionUpload.paused.title": "Session upload is paused", + "toast.sessionUpload.paused.description": + "Your CoDev sign-in expired, so sessions are no longer uploaded for analysis. Log in again to resume — pending sessions upload automatically afterwards.", + "toast.sessionUpload.paused.action.login": "Log in", + "error.page.title": "Something went wrong", "error.page.description": "An error occurred while loading the application.", "error.page.description.localServerStartup": "An error occurred while starting the local server.", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index a39b68e8a3..ad692477b0 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -28,7 +28,7 @@ export const dict = { "command.project.index": "Cambiar al proyecto {{index}}", "command.provider.connect": "Conectar proveedor", "command.server.switch": "Cambiar servidor", - "command.gateway.apiKey.new": "Obtener una nueva clave de API", + "command.gateway.apiKey.new": "Iniciar sesión/Configurar", "command.settings.open": "Abrir ajustes", "command.session.previous": "Sesión anterior", "command.session.next": "Siguiente sesión", @@ -648,6 +648,11 @@ export const dict = { "toast.update.action.installRestart": "Instalar y reiniciar", "toast.update.action.notYet": "Todavía no", + "toast.sessionUpload.paused.title": "La subida de sesiones está pausada", + "toast.sessionUpload.paused.description": + "Tu sesión de CoDev caducó, así que las sesiones ya no se suben para su análisis. Vuelve a iniciar sesión para reanudarla: las sesiones pendientes se subirán automáticamente después.", + "toast.sessionUpload.paused.action.login": "Iniciar sesión", + "error.page.title": "Algo salió mal", "error.page.description": "Ocurrió un error al cargar la aplicación.", "error.page.description.localServerStartup": "Ocurrió un error al iniciar el servidor local.", diff --git a/packages/app/src/i18n/vi.ts b/packages/app/src/i18n/vi.ts index dc5ea718cc..6da1d3ad36 100644 --- a/packages/app/src/i18n/vi.ts +++ b/packages/app/src/i18n/vi.ts @@ -28,7 +28,7 @@ export const dict = { "command.project.index": "Chuyển đến dự án {{index}}", "command.provider.connect": "Kết nối provider", "command.server.switch": "Chuyển máy chủ", - "command.gateway.apiKey.new": "Lấy API key mới", + "command.gateway.apiKey.new": "Đăng nhập/Cấu hình", "command.settings.open": "Mở cài đặt", "command.session.previous": "Session trước", "command.session.next": "Session tiếp theo", @@ -654,6 +654,11 @@ export const dict = { "toast.update.action.installRestart": "Cài đặt và khởi động lại", "toast.update.action.notYet": "Để sau", + "toast.sessionUpload.paused.title": "Việc tải phiên lên đang tạm dừng", + "toast.sessionUpload.paused.description": + "Phiên đăng nhập CoDev của bạn đã hết hạn nên các phiên làm việc không còn được tải lên để phân tích. Hãy đăng nhập lại để tiếp tục — các phiên đang chờ sẽ tự động được tải lên sau đó.", + "toast.sessionUpload.paused.action.login": "Đăng nhập", + "error.page.title": "Đã xảy ra sự cố", "error.page.description": "Đã xảy ra lỗi khi tải ứng dụng.", "error.page.description.localServerStartup": "Đã xảy ra lỗi khi khởi động server cục bộ.", diff --git a/packages/desktop/src/main/gateway-sso/ipc.ts b/packages/desktop/src/main/gateway-sso/ipc.ts index 4015372fc8..271265d3a7 100644 --- a/packages/desktop/src/main/gateway-sso/ipc.ts +++ b/packages/desktop/src/main/gateway-sso/ipc.ts @@ -2,7 +2,13 @@ import { app, ipcMain } from "electron" import type { IpcMainInvokeEvent } from "electron" import type { GatewaySsoController } from "./controller" -export function registerGatewaySsoIpcHandlers(controller: GatewaySsoController) { +// consumeUploadSignInNudge reports whether the background session upload +// stalled for lack of an SSO session, once per app launch (see index.ts) — the +// renderer turns it into the "Session upload is paused" toast. +export function registerGatewaySsoIpcHandlers( + controller: GatewaySsoController, + consumeUploadSignInNudge: () => boolean, +) { const subscriptions = new Map void>() const unsubscribe = (id: number) => { const off = subscriptions.get(id) @@ -44,6 +50,7 @@ export function registerGatewaySsoIpcHandlers(controller: GatewaySsoController) ) ipcMain.handle("gateway-sso-fetch-models", () => controller.fetchModels()) ipcMain.handle("gateway-sso-ensure-key", () => controller.ensureKey()) + ipcMain.handle("gateway-sso-consume-upload-nudge", () => consumeUploadSignInNudge()) ipcMain.handle("gateway-sso-complete", (_event: IpcMainInvokeEvent, model: unknown) => controller.complete(requireGatewayIpcString("model", model)), ) diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index 2ab632fc24..dc4a25b935 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -232,6 +232,19 @@ const main = Effect.gen(function* () { body: "New and changed sessions are being uploaded for analysis in the background.", }).show() } + // Fork: the background session upload needs a live CoDev SSO session, but + // the app itself runs on the gateway api key alone — so when the session + // dies, uploads stop silently while everything else keeps working. The + // sidecar's runner records that stall; the renderer turns it into a toast + // whose action opens the same sign-in dialog the Log in/Configure menu item + // does (the desktop counterpart of the TUI's startup toast, and like it, + // every launch until the sign-in happens). Read once here rather than per + // request, so a sign-in mid-session cannot flip the answer between windows, + // and handed to the first asker only — a second window would otherwise + // repeat a toast the user is already looking at. shouldNudgeSignIn is + // sqlite-free and cheap, so it is safe on the main process. + let uploadSignInNudge = shouldNudgeSignIn() + const stopSidecars = async () => { await killSidecar() wslServers.stopAll() @@ -409,21 +422,13 @@ const main = Effect.gen(function* () { server?.upload(sandboxUploadSources()) }, }), + () => { + const stalled = uploadSignInNudge + uploadSignInNudge = false + return stalled + }, ) void updater.start() - // Fork: the background session upload needs a live CoDev SSO session, but - // the app itself runs on the gateway api key alone — so when the session - // dies, uploads stop silently while everything else keeps working. The - // sidecar's runner records that stall; surface it as an OS notification - // (the desktop's counterpart of the TUI's startup toast, and like it, - // every launch until the sign-in happens). shouldNudgeSignIn is sqlite-free - // and cheap, so it is safe on the main process. - if (shouldNudgeSignIn()) { - new Notification({ - title: "Session upload is paused", - body: "Your CoDev sign-in expired, so sessions are no longer uploaded for analysis. Sign in again from the gateway setup to resume — pending sessions upload automatically afterwards.", - }).show() - } // Checked at fire time rather than captured: turning automatic checks off // has to stop the timer that is already running, not the next app launch. const updateTimer = setInterval( diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 40d41be1f2..b8ea71996c 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -92,6 +92,7 @@ const api: ElectronAPI = { setManualApiKey: (apiKey) => ipcRenderer.invoke("gateway-sso-set-manual-api-key", apiKey), fetchModels: () => ipcRenderer.invoke("gateway-sso-fetch-models"), ensureKey: () => ipcRenderer.invoke("gateway-sso-ensure-key"), + consumeUploadSignInNudge: () => ipcRenderer.invoke("gateway-sso-consume-upload-nudge"), complete: (model) => ipcRenderer.invoke("gateway-sso-complete", model), }, updater: { From cfdd5a9d85541fb8de64d65f06d2a93fb748a74d Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Tue, 25 Aug 2026 09:25:03 +0700 Subject: [PATCH 2/2] docs: point the changelog entry at the real PR number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03335ab64b..fd3e66ca28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,7 +199,7 @@ file is the human-readable summary of what changed and why. in/Configure** — in the **CoDev** menu on macOS, under **File** on Windows and Linux, and in the command palette everywhere — since signing back in, rather than minting a key, is what it is usually reached for. - ([#319](https://github.com/quickbeard/codev-code/pull/319)) + ([#320](https://github.com/quickbeard/codev-code/pull/320)) - **A dropped model connection retries instead of silently stopping the agent** — when the connection to the model died mid-response (corporate proxies are fond of cutting long-lived streams), the session just went idle: no error, no