diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e60fe6aff..f5c92102cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -184,7 +184,10 @@ file is the human-readable summary of what changed and why. 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. + 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. ([#313](https://github.com/quickbeard/codev-code/pull/313)) - **A dropped model connection retries instead of silently stopping the agent** — when the connection to the model died mid-response (corporate proxies are fond diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 088d8cbfbc..3b8c158064 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, 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 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. | | `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. | diff --git a/packages/app/src/desktop-menu.ts b/packages/app/src/desktop-menu.ts index ee60811f95..be61f1140d 100644 --- a/packages/app/src/desktop-menu.ts +++ b/packages/app/src/desktop-menu.ts @@ -6,6 +6,7 @@ export type DesktopMenuPlatform = "macos" | "windows" export type DesktopMenuAction = | "app.checkForUpdates" | "app.relaunch" + | "upload.sessions" | "edit.undo" | "edit.redo" | "edit.cut" @@ -82,6 +83,7 @@ export const DESKTOP_MENU: DesktopMenu[] = [ { 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: "Upload Session Logs", action: "upload.sessions" }, { type: "item", label: "Reload Webview", action: "view.reload" }, { type: "item", label: "Restart", action: "app.relaunch" }, { type: "item", label: "Export Logs...", command: "logs.export" }, @@ -119,6 +121,13 @@ export const DESKTOP_MENU: DesktopMenu[] = [ command: "gateway.apiKey.new", platforms: ["windows"], }, + // macOS carries this in the app menu below its API-key sibling. + { + type: "item", + label: "Upload Session Logs", + action: "upload.sessions", + platforms: ["windows"], + }, { type: "item", label: "New Window", diff --git a/packages/desktop/src/main/desktop-menu-actions.ts b/packages/desktop/src/main/desktop-menu-actions.ts index aa15e05e27..dc684893cf 100644 --- a/packages/desktop/src/main/desktop-menu-actions.ts +++ b/packages/desktop/src/main/desktop-menu-actions.ts @@ -5,6 +5,7 @@ import { createMainWindow, updateTitlebar } from "./windows" export type DesktopMenuActionHandlers = Partial<{ checkForUpdates: () => void relaunch: () => void + uploadSessions: () => void }> export function runDesktopMenuAction( @@ -19,6 +20,9 @@ export function runDesktopMenuAction( case "app.relaunch": handlers.relaunch?.() return + case "upload.sessions": + handlers.uploadSessions?.() + return case "window.new": createMainWindow() return diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index b77c1943b3..b01ee3a414 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -215,6 +215,18 @@ const main = Effect.gen(function* () { readySandboxes = ready if (newlyReady) server?.upload(sandboxUploadSources()) }) + // Fork: the "Upload Session Logs" menu item — a user-triggered sweep of + // local and work sessions through the sidecar's uploader (same gates: + // opt-out env, sign-in, shared lock). The notification is the only + // feedback a background run can give from here; the sidecar posts no + // completion message back. + const uploadSessionLogs = () => { + server?.upload(sandboxUploadSources()) + new Notification({ + title: "Uploading session logs", + body: "New and changed sessions are being uploaded for analysis in the background.", + }).show() + } const stopSidecars = async () => { await killSidecar() wslServers.stopAll() @@ -330,6 +342,7 @@ const main = Effect.gen(function* () { registerIpcHandlers({ killSidecar: () => killSidecar(), relaunch, + uploadSessions: uploadSessionLogs, awaitInitialization: Effect.fnUntraced( function* () { logger.log("awaiting server ready") @@ -519,6 +532,7 @@ const main = Effect.gen(function* () { checkForUpdates: () => { void showUpdaterDialog(updater, true) }, + uploadSessions: uploadSessionLogs, relaunch: () => { relaunch() }, diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 1c6bf1572b..69755c78f4 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -39,6 +39,7 @@ type Deps = { resolveAppPath: (appName: string) => Promise updater: UpdaterController showUpdater: () => Promise | void + uploadSessions: () => void setBackgroundColor: (color: string) => void exportDebugLogs: () => Promise recordFatalRendererError: (error: FatalRendererError) => Promise | void @@ -301,6 +302,7 @@ export function registerIpcHandlers(deps: Deps) { runDesktopMenuAction(BrowserWindow.fromWebContents(event.sender), action, { checkForUpdates: () => void deps.showUpdater(), relaunch: deps.relaunch, + uploadSessions: deps.uploadSessions, }) }) } diff --git a/packages/desktop/src/main/menu.ts b/packages/desktop/src/main/menu.ts index 7adc65a429..5d367a5cf7 100644 --- a/packages/desktop/src/main/menu.ts +++ b/packages/desktop/src/main/menu.ts @@ -14,6 +14,7 @@ type Deps = { trigger: (id: string) => void checkForUpdates: () => void relaunch: () => void + uploadSessions: () => void } export function createMenu(deps: Deps) { @@ -52,6 +53,7 @@ function nativeItem(entry: DesktopMenuEntry, deps: Deps): MenuItemConstructorOpt runDesktopMenuAction(BrowserWindow.getFocusedWindow(), action, { checkForUpdates: deps.checkForUpdates, relaunch: deps.relaunch, + uploadSessions: deps.uploadSessions, }) } if (entry.href) {