Home

mweinbach / agent-coworker

publicmweinbach/agent-coworker
Overview Code History Branches Pull requestsIssuesInsights
main
HomeOverview Code PRsIssues

Desktop: harden Electron layer and fix UI/UX, a11y, and state bugs

merged
Stack 2/2
#349 opened by mweinbachclaude/loving-edison-t3eun2→main
Conversation75
mweinbachopened this pull requestAuthor· 4 days ago
Commits
0
Files changed…

Audit-driven pass over apps/desktop: the Electron main/preload boundary, window lifecycle, renderer state, and UI/UX + accessibility. Every fix below was checked against the code (and, where noted, in a live Electron 43 run under Xvfb). The branch is made of small Conventional Commits, so each one can be reviewed or reverted on its own.

Security (Electron boundary)

  • Windows NTLM leak via UNC paths. assertPathWithinRoots realpath'd attacker-supplied paths before the root check. A model-rendered [](file://host/share/x.png) or a chat link could open an SMB session with no click. UNC and device-namespace paths are now rejected lexically unless an approved root is on the same share. Outside-workspace preview/open refuses them before any stat.
  • One-click launch of agent-written executables. openPath handed any workspace file to the OS shell, so a .command/.exe/.lnk/.js (Windows) or an exec-bit script ran outside the agent sandbox. The main process now confirms first.
  • Removed 10 unused IPC channels: readFile, writeFile, previewOSFile, window min/max/drag, getPlatform, appendTranscriptEvent. writeFile was a generic write primitive that also followed dangling symlinks out of the root.

Electron lifecycle / platform

  • Menu shortcuts were dead on Windows and Linux. win.setMenu(null) detached every accelerator (Ctrl+N, Ctrl+,, Ctrl+B, zoom, fullscreen). An Electron probe confirmed zero fire with setMenu(null) and all fire with setMenuBarVisibility(false).
  • Canvas popouts were treated as the main window. Opening one could drop Quick Chat threads and let a stale snapshot replace workspaces. They now use popup load/save semantics.
  • Renderer crash recovery. A crashed renderer is reloaded, with a cooldown. Closing a window whose renderer is gone is approved immediately instead of stalling for 15 s.
  • File watcher. An fs.watch error no longer becomes an uncaught main-process exception.
  • Corrupt state.json. It is preserved as state.json.corrupt-<ts> instead of being silently overwritten, and writes use writeFileAtomic (Windows rename retries).
  • Updater. It no longer sticks on "Checking…" when electron-updater resolves null (AppImage without APPIMAGE, snap).
  • Window restore.
    • The restored main window's title bar can no longer sit off-screen.
    • Maximize waits until the window is revealed (no blank flash).
    • The update-ready notification is retained so it isn't garbage-collected.
  • IPC teardown. The IPC disposer now runs at quit.
  • Local crash logging. Main-process crashes are always written to desktop-main.log; previously only Sentry saw them, and crash reporting is off by default.
  • Menu commands. Queued commands are no longer lost to a StrictMode remount or drained by popout windows.
  • Windows smoke (server-manager.test.ts › "unexpected real parent exit…"). This failed deterministically on both Windows jobs, on main too. It wasn't a flake. libuv puts every non-detached Windows child into a job object with KILL_ON_JOB_CLOSE, so the managed sidecar dies with its parent instead of draining on stdin EOF. On Windows the test now asserts what actually happens: the child is reaped and not orphaned. The drain assertion stays on POSIX. The serverManager.ts comment that said EOF covers parent exit on Windows is corrected. App behavior is unchanged: when Electron crashes on Windows, the sidecar still gets no graceful drain. Changing that would mean spawning it detached, which is out of scope here (see below).

Renderer state

  • Server restart. Restarting a workspace server no longer leaves a thread stuck on "Working…"; in-flight turns settle and threads resume on the new socket.
  • Failed sends. A send whose reconnect failed was auto-delivered later even though the UI said "not sent". It is now withdrawn, and the failure bubble keeps the clientMessageId.
  • Error text. Server rejections show their real reason instead of always "Not connected".
  • Rename rollback. A rejected chat rename rolls back with a notification. Clearing the hard cap only records success after the server confirms it.
  • Local cache. The localStorage warm-start cache is bounded to 12 session snapshots; it used to write every opened chat and fail silently at quota. Navigation-intent entries are pruned.

UI/UX + accessibility

  • Focus rings. Keyboard focus is visible again on the top-bar buttons and settings nav; unlayered resets had erased the ring.
  • Forced colors. In Windows High Contrast the active sidebar chat rendered as a blank bar (Chromium text backplate). The title is readable now.
  • Composer at 800px. The composer <fieldset> overflowed (UA min-inline-size: min-content) and clipped Send. This is the adaptive-surfaces failure CI hits on main.
  • Toasts render above dialogs and keep a persistent live region. AlertDialog registers with the overlay stack.
  • Cmd/Ctrl+K now:
    • works with Caps Lock on
    • ignores IME composition
    • doesn't stack over other dialogs
  • Settings pages.
    • MCP Save is disabled until the draft is valid.
    • Usage shows a loading skeleton instead of zeros.
    • Workspace Restart shows progress and confirms when a chat is running.
  • Labelling and motion.
    • Unlabeled controls are named.
    • Segmented toggles expose aria-pressed.
    • Focus returns to the row after a rename.
    • framer-motion honors reduced motion.
    • Failed "open" actions are reported.

Quality-gate baselines

CI's Electron UI quality gates job already fails on main. It hits the same 16 failures this container reproduced before any change, with identical pixel counts. 15 of them come from baselines that predate the collapsible Projects/Chats sidebar headers; the 16th is the 800px clipping bug fixed above. Rendering here is pixel-identical to CI, so the baselines are re-approved in a dedicated commit. That commit also captures the now-visible focus ring on the top-bar thread-actions button, and docs/assets/desktop-product.png follows the 1240px light baseline.

Verification

  • bun run typecheck, bun run check, bun run docs:check: pass.
  • bun run test: all pass except test/mcp.oauth-provider.test.ts › "answers on ::1", which needs IPv6 loopback. This container has no IPv6; the branch doesn't touch src/ or root tests.
  • bun run desktop:quality (real Electron under Xvfb): 76 passed, 4 skipped, 0 failed. The base was 60 passed / 16 failed.
  • Regression tests were added for the security, IPC, lifecycle and state fixes; most were confirmed failing before the fix.
  • The Windows branch of the parent-exit test only runs in CI; it can't run in this Linux container.

Not in this PR

  • Content-Security-Policy and Electron fuses (RunAsNode, NODE_OPTIONS, inspect, asar integrity) are recommended but not shipped. Both can break the packaged app in ways that can't be verified without macOS/Windows packaging.
  • Archive state and auto-delete retention live only in the desktop client, although thread/archived/set exists on the server. Moving them into the harness is a separate design change.
  • Graceful drain after an Electron crash on Windows. That needs the sidecar spawned detached, but a detached process has no console. 11 of the 17 spawn sites in src/ don't set windowsHide, so tools would pop up visible console windows.

🤖 Generated with Claude Code

claude.ai/code/session_01849ySz1jKxBXrehirFbNYx


[!NOTE] High Risk Changes authentication-adjacent path validation, removes IPC surface area, and alters window close/crash, persistence, and chat send/reconnect behavior across the desktop stack.

Overview Hardens the Electron main/preload boundary and tightens desktop reliability, security, and UI behavior in one audit-driven pass.

Security: Windows UNC/device paths are rejected before any filesystem touch (blocking NTLM-style SMB triggers from links or previews). openPath now warns when opening OS-executable file types. Several unused or risky IPC endpoints are removed (readFile/writeFile, previewOSFile, custom window drag/min/max, single-event transcript append).

Main process: Dev builds expose CDP on loopback by default (docs updated). Menu accelerators stay wired on Win/Linux by hiding the menu bar instead of setMenu(null). Renderer crashes get cooldown-gated reload; close coordination settles when the renderer is gone. File watchers restart on error; corrupt state.json is preserved; persistence uses atomic writes; main always logs uncaught errors locally; IPC unregisters on quit; updater handles inactive installs; update notifications and maximize-on-show are fixed.

Renderer: Workspace server restart disconnects threads cleanly; failed first sends are withdrawn instead of silently retried; rename and usage-cap actions reconcile with server outcomes; localStorage warm-start caps cached session snapshots; menu commands survive StrictMode; overlays/toasts/command palette and assorted a11y fixes (focus rings, forced-colors sidebar, composer narrow-width, dialog labels).

Reviewed by Cursor Bugbot for commit 8249cfd935be63b213d60771552ff318adbb904b. Bugbot is set up for automated code reviews on this repo. Configure here.

chatgpt-codex-connector[bot]commented· 4 days ago

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code Review✅ Completed 2026-09-24T22:46:23.923717Z8249cfdNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Review

chatgpt-codex-connector[bot] · 4 days ago · 3 file comments

3 open

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9db031f8e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

mweinbachcommented· 4 days ago

Both Windows smoke (x64) and Windows smoke (arm64) fail on one test that this PR doesn't cause:

  • Test: apps/desktop/test/server-manager.test.ts › desktop server manager bun crash detection › unexpected real parent exit closes the pipe and drains its managed child
  • Error: Child fixture did not publish its result. from waitForFixtureFile (server-manager.test.ts:136)

Why it isn't this PR's: the same test fails the same way on main at this branch's base, e2d5f95 (run 34554446091). This PR doesn't change server-manager.test.ts, electron/services/serverManager.ts or the server entrypoint.

Fix: none exists yet. It's a Windows-only timing issue in a real-subprocess test, so I'm not widening this PR to cover it. Every other test in both jobs passes (493 pass, 1 fail on x64). I'm re-running the failed jobs once.


Generated by Claude Code

Review

cursor[bot] · 4 days ago · 1 file comment

1 open

Stale Bugbot comment from a previous run.

Review

mweinbach · 4 days ago

Review

chatgpt-codex-connector[bot] · 4 days ago · 7 file comments

7 open

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f74d5befd0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

Review

mweinbach · 4 days ago

Review

cursor[bot] · 4 days ago · 1 file comment

1 open

Stale Bugbot comment from a previous run.

Review

mweinbach · 4 days ago

Review

chatgpt-codex-connector[bot] · 3 days ago · 5 file comments

5 open

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09b83033f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Review

mweinbach · 3 days ago

Review

mweinbach · 3 days ago

Review

mweinbach · 3 days ago

Review

mweinbach · 3 days ago

Review

mweinbach · 3 days ago

Review

cursor[bot] · 3 days ago · 1 file comment

1 open

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f62f31e13047357cc891a73dcc064344fc2fb142. Configure here.

Review

mweinbach · 3 days ago

Review

chatgpt-codex-connector[bot] · 3 days ago · 4 file comments

4 open

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26c4d0f1c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Review

mweinbach · 3 days ago

Review

mweinbach · 3 days ago

Review

mweinbach · 3 days ago

Review

mweinbach · 3 days ago

Sign in to comment.

Stack

2/2
1

Merge readiness

Checking mergeability after the indexing worker computes the current branch state.

Autopilot

Debug

Reviews

Approved0
ReviewersNone yet

Configure an OpenRouter key in repository settings.

apps/desktop/src/app/store.actions/thread.ts
lines 1307-1308
View file
  1. chatgpt-codex-connector[bot]· 4 days ago
  • apps/desktop/electron/services/crashReporting.tslines 76-78
  • apps/desktop/electron/services/workspaceDirectoryWatcher.tslines 105-107
  • apps/desktop/src/app/store.actions/thread.ts
    line 1341
    View file
    1. cursor[bot]· 4 days ago

      Stale rename success overwrites confirmed title

      Medium Severity

      A successful older renameThread always writes tracking.confirmed, even after a newer rename has already settled. A later failure of the newest rename then restores that stale title, so the UI and persisted state can diverge from the title the server last accepted.

      Fix in Cursor Fix in Web

      Reviewed by Cursor Bugbot for commit 9e4a5c1f3b0e6a553bac5fbf6ef6012dca734763. Configure here.

    apps/desktop/src/app/store.actions/thread.ts
    lines 1341-1345
    View file
    1. chatgpt-codex-connector[bot]· 4 days ago
  • apps/desktop/electron/services/workspaceDirectoryWatcher.tslines 159-163
  • apps/desktop/electron/services/launchableFiles.tslines 89-90
  • apps/desktop/src/app/store.helpers/threadEventReducer/messaging.tslines 115-118
  • apps/desktop/src/app/store.helpers/persistence.tsline 144
  • apps/desktop/src/app/store.actions/workspace.tslines 506-508
  • apps/desktop/src/ui/settings/pages/UsagePage.tsxline 256
  • apps/desktop/electron/services/workspaceDirectoryWatcher.ts
    line 160
    View file
    1. cursor[bot]· 4 days ago

      Watcher retries reset forever

      Medium Severity

      After a watcher has stayed healthy past healthyResetMs, a later startWatcher throw (deleted root, persistent ENOSPC) still sees the old restartedAtMs and zeroes restartAttempts on every call. The 2s retry never reaches the final close, so the scope keeps cycling instead of handing off to periodic revalidation.

      Fix in Cursor Fix in Web

      Reviewed by Cursor Bugbot for commit 5d1efc562ae01ecb036db8aabfb3c132ae972617. Configure here.

    apps/desktop/electron/services/launchableFiles.ts
    lines 118-122
    View file
    1. chatgpt-codex-connector[bot]· 3 days ago
  • apps/desktop/electron/services/crashReporting.tslines 72-74
  • apps/desktop/src/App.tsxlines 269-270
  • apps/desktop/src/app/store.actions/thread.tslines 2466-2470
  • apps/desktop/electron/services/windowCloseCoordinator.tslines 183-185
  • apps/desktop/electron/main.ts
    line 894
    View file
    1. cursor[bot]· 3 days ago

      Crash recovery reloads a closing window

      Medium Severity

      A renderer crash during a user close now does two opposing things in the same handler: rendererGone approves the pending request and calls window.close(), then recoverGoneRenderer reloads that same webContents. close() has not destroyed the window yet, so the reload can keep or bring back the window the user was already closing.

      Additional Locations (1)
      • apps/desktop/electron/services/windowCloseCoordinator.ts#L149-L153
      Fix in Cursor Fix in Web

      Reviewed by Cursor Bugbot for commit f62f31e13047357cc891a73dcc064344fc2fb142. Configure here.

    apps/desktop/electron/ipc/files.ts
    lines 526-529
    View file
    1. chatgpt-codex-connector[bot]· 3 days ago
  • apps/desktop/electron/services/crashReporting.tslines 74-78
  • apps/desktop/src/styles.csslines 289-294
  • apps/desktop/src/app/store.actions/thread.tslines 1326-1327
  • Update README.md

    #125 mobile-fixes ← main

    Updated 4 months ago

    merged
    2

    Desktop: harden Electron layer and fix UI/UX, a11y, and state bugs

    P2 Badge Roll back overlapping renames to the confirmed title

    If a user submits rename A and then rename B before either RPC settles, and both requests reject with A rejecting first, A's rollback is skipped because B is displayed; B's rollback then restores its captured previous.title of A even though A was also rejected. That unconfirmed title is subsequently persisted, so rollback needs a mutation version or last-confirmed title rather than the immediately preceding optimistic value.

    Useful? React with 👍 / 👎.

  • mweinbach· 4 days ago

    Fixed in 8ea7e8c3. Renames now track the last title the server confirmed for each chat, and only the newest pending rename can change the title; an older rejection just shows a notification. I added a regression test for your A-then-B case. It fails before this commit and passes after.


    Generated by Claude Code

  • View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P2 Badge Preserve fatal handling after logging unhandled rejections

      When crash reporting is off, as it is by default, this unconditional unhandledRejection listener logs and returns. Under Node's default --unhandled-rejections=throw behavior, installing such a listener suppresses the otherwise-fatal exception, so an unexpected main-process rejection now leaves the desktop app running in potentially inconsistent state instead of terminating; log through a mechanism that retains the fatal behavior or explicitly exit afterward.

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      This doesn't reproduce in Electron. Electron's main process doesn't treat unhandled rejections as fatal: it logs UnhandledPromiseRejectionWarning and keeps running. I checked this on Electron 43 with a probe that calls Promise.reject(new Error("boom")) from app.whenReady() and has no listener registered. The process stayed alive and printed the warning. So this listener doesn't suppress any termination; it only sends the rejection to desktop-main.log instead of stderr. It also matches the existing Sentry handler, which already registers unhandledRejection whenever crash reporting is enabled. No change needed.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P2 Badge Notify the renderer when a directory watch dies

      When an established FSWatcher later emits an error, this silently removes the watch and all subscribers without informing the renderer or retrying. WorkspaceFileExplorer has already recorded the successful registration as watchSupported === true, so it never registers again and continues using the slower 30-second revalidation interval; after a transient error such as ENOSPC clears, subsequent file changes remain delayed until the explorer remounts. Signal the failed subscription or re-establish it so the renderer can enter its fallback-refresh mode.

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      Fixed in 9e4a5c1f. An errored watcher is no longer dropped. It is re-created after 2s, 10s and 30s, keeping its subscribers. Each time it comes back it sends a root modify event, so the explorer reloads anything that changed while it was down. Only after the last attempt does the watch close. From then on the explorer's periodic revalidation still picks up changes. The test covers the restart, the root reload, and closing once retries run out.


      Generated by Claude Code

  • mweinbach· 4 days ago

    Fixed in f74d5bef. The tracker now records which rename's generation produced the confirmed title, and only a newer success can replace it. I added a regression test for this exact order: rename 2 succeeds, then rename 1 succeeds late, then rename 3 is rejected. The chat ends up on rename 2. The test fails without the fix.


    Generated by Claude Code

  • P2 Badge Reconcile a late successful rename after the newest one fails

    When rename A is pending, rename B is submitted, B rejects first, and A then succeeds, B restores the original title but A's success only updates the temporary tracking.confirmed value and returns; once A settles the tracking entry is deleted, leaving the persisted client title at the original value while the server has accepted A. Fresh evidence beyond the earlier overlapping-rejection report is this current success branch, which never reconciles the visible title after a newer request has already rolled it back.

    Useful? React with 👍 / 👎.

  • mweinbach· 4 days ago

    Fixed in 53a98e2f. Once every pending rename has settled, the chat is set to the last confirmed title. This only applies when the displayed title is one the tracker set itself, so an unrelated title update is never overwritten. The new test covers your order: B is rejected first, then A succeeds, and the chat shows A. It fails before this commit and passes after.


    Generated by Claude Code

  • View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P2 Badge Reset watcher retries after a successful restart

      After a watcher is successfully recreated, restartAttempts remains incremented, so separate transient errors over the lifetime of the workspace consume the 2s, 10s, and 30s slots cumulatively; the fourth error closes the watch and drops all subscribers even if every prior restart recovered immediately. Fresh evidence beyond the earlier watcher report is that the new successful startWatcher path never resets this counter, causing the original permanent-staleness behavior to return after nonconsecutive errors.

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      Fixed in 4854e288. A watcher that stays up for 60 seconds after a restart gets its full retry budget back, so unrelated errors spread over a long session no longer use up the 2s/10s/30s retries. Back-to-back failures still hit the cap. The new test uses an injected clock and fails before this commit.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P1 Badge Check Windows file associations before opening unlisted files

      On Windows this treats every suffix outside the hard-coded set as non-launchable, but shell.openPath follows the machine's registered file association. On a developer machine where .py, .sh, .ahk, or another script type is associated with its interpreter, clicking an agent-written file executes it outside the sandbox without showing the new confirmation. Determine whether the actual association launches code, or conservatively include associated script types, instead of returning false solely from the suffix allowlist.

      AGENTS.md reference: apps/desktop/AGENTS.md:L30-L33

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      Fixed in 256f05c4 by taking the conservative option. Common interpreted-script extensions now need confirmation on every platform: .py/.pyw/.pyz/.pyc, .sh/.bash/.zsh/.ksh/.csh/.fish, .ahk/.au3, .pl/.pm, .rb, .php, .lua, .tcl, .r and .psm1. They're added on top of each platform's launcher list. Reading the real Windows association (AssocQueryString/registry) would need a native call from the main process, so I kept this to a static list. The test covers .py/.SH on win32 and .ahk on darwin.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P2 Badge Withdraw failed first messages from brand-new chats

      For a brand-new chat created without a preset clientMessageId or composer submission, queueOptimisticFirstThreadMessage() generates an ID internally, but the ensureThreadSocket() call does not receive it. If thread/start then fails, both values checked here are undefined, so the pending message remains queued and a later reconnect sends it even though the UI reported that starting the chat failed. Propagate the generated ID into the failure cleanup just as the reconnect path now does.

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      Fixed in 256c7850. queueOptimisticFirstThreadMessage now returns the id it queued, and newThread passes it to ensureThreadSocket as pendingFirstMessageClientMessageId. A failed thread/start therefore withdraws the queued first message. The existing thread/start-failure test now also asserts that nothing is left queued. It fails before this commit and passes after.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P2 Badge Bound the warm-start cache by serialized size

      Limiting the cache to 12 snapshots does not bound the localStorage payload: each snapshot may contain up to 2,000 feed items, including large tool or assistant content, so one long chat—or several moderately large chats—can still exceed the browser quota and make the entire cache write fail. The new cache should select or trim snapshots against a serialized byte budget rather than relying only on an entry count.

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      Fixed in 5d1efc56. Cached snapshots are now also capped at 2M serialized characters, as well as the 12-snapshot count. The cap is well below Chromium's roughly 5M-character localStorage quota, and a snapshot that wouldn't fit is skipped so the rest of the cache still writes. The new test uses a snapshot of more than 2M characters and fails before this commit.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P2 Badge Preserve reconnect candidates before closing sessions

      If a thread is already in the socket's reconnecting state when the user restarts its workspace server, its runtime has connected === false but its ID is retained in the reconnect set. The preceding closeThreadSession() call removes that ID via forgetThreadForReconnect, so this new call computes an empty set, neither clears the preserved busy state nor schedules the thread on the replacement socket; the repair action can therefore leave the chat stuck on “Working…” and disconnected. Capture or mark the reconnect candidates before closing the sessions.

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      Fixed in bb258248. Restart now calls markWorkspaceThreadsDisconnected before closeThreadSession. That settles in-flight turns and captures the resume candidates, including threads already waiting to reconnect. Restart takes a copy of that set, because the live set is emptied as sessions close, and re-queues it afterwards through a new threadIds option. The new test covers a thread that was already reconnecting when the server restarted. It fails before this commit and passes after.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 4 days ago

      P2 Badge Keep usage totals loading until all transcripts are read

      If even one thread already has cached sessionUsage—commonly the currently open chat—hasUsage becomes true immediately and disables the loading UI while loadAllThreadUsage() is still reading the remaining transcripts. The page therefore presents partial token, cost, turn, provider, and session totals as final values until the asynchronous load completes; loading should remain visible, or the partial state otherwise be identified, for the full duration of usageLoading.

      Useful? React with 👍 / 👎.

    2. mweinbach· 4 days ago

      Fixed in fc2bdef8. The loading state, including the provider breakdown, now stays up for the whole transcript read, so a cached chat can't make partial totals look final. There's a new test where one chat has cached usage while the load is still pending. It fails before this commit and passes after.


      Generated by Claude Code

  • mweinbach· 4 days ago

    Fixed in b83b81c7. scheduleRestart now clears restartedAtMs whenever it checks it, and only a restart that actually comes back up sets it again. So a healthy stretch resets the retry budget once, and later startWatcher failures count up and close the scope. I added a regression test: a healthy stretch followed by restarts that keep failing ends with the scope closed. It fails before this commit and passes after.


    Generated by Claude Code

  • P1 Badge Fail closed when launchability inspection fails

    On POSIX, if an extensionless workspace file is removed or replaced after path authorization, fs.stat() can fail here and this helper classifies the path as safe; openPath then reopens the same pathname with shell.openPath, so a concurrently recreated executable can run outside the sandbox without confirmation. Treat inspection failures as launchable or otherwise bind validation to the same file identity that is opened.

    AGENTS.md reference: apps/desktop/AGENTS.md:L31-L31

    Useful? React with 👍 / 👎.

  • mweinbach· 3 days ago

    Fixed in f62f31e1. On POSIX, if the file can't be inspected (a failed stat), it is now treated as launchable, so openPath asks for confirmation. A path that was swapped or recreated after authorization can no longer open without a prompt. The new test covers a real 0644 file (not launchable), a 0755 file (launchable) and a missing path (launchable). It fails before this commit.


    Generated by Claude Code

  • View file
    1. chatgpt-codex-connector[bot]· 3 days ago

      P2 Badge Persist fatal crash logs synchronously

      When an actual uncaught exception reaches this monitor, logError only queues promise-based filesystem work and returns; the process then follows its fatal path and can exit before the append runs, leaving no record of the crash that this handler was added to preserve. Use a synchronous emergency write or another mechanism guaranteed to complete before fatal termination.

      Useful? React with 👍 / 👎.

    2. mweinbach· 3 days ago

      Fixed in f62f31e1. The uncaughtExceptionMonitor handler now calls the new logErrorSync, which uses mkdirSync/appendFileSync to write desktop-main.log. The record is on disk before the fatal path continues. It uses the same redaction and record format as the async logger, skips rotation, and clears the cached file size so the next async write re-reads it. Unhandled rejections still use the async queue, because they aren't fatal in Electron's main process.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 3 days ago

      P2 Badge Apply the overlay guard to menu commands

      When the packaged app handles CmdOrCtrl+K through the native menu accelerator, Electron dispatches openCommandPalette, whose handler still opens the palette unconditionally. This bypasses the new keyboard guard here, so invoking the accelerator or menu item while another dialog is open can stack a second focus-trapping overlay; apply the same hasOpenOverlay() check in the menu-command path.

      Useful? React with 👍 / 👎.

    2. mweinbach· 3 days ago

      Fixed in f62f31e1. The openCommandPalette menu command now runs setCommandPaletteOpen((open) => open || !hasOpenOverlay()). An already-open palette stays open, and a new one won't open over another dialog. This matches the keydown guard.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 3 days ago

      P2 Badge Skip transcript writes after thread deletion

      If a user clears the hard cap and deletes the chat before the JSON-RPC request succeeds, this late callback runs after removeThread has unlinked the transcript and removed its runtime. appendThreadTranscript then persists an event with no session ID, and fs.appendFile recreates the deleted thread's JSONL file, leaving conversation-related state behind after deletion; capture the session and verify the thread is still current before appending.

      Useful? React with 👍 / 👎.

    2. mweinbach· 3 days ago

      Fixed in f62f31e1. The session ID is now captured when the request is sent. On success, the callback returns early if the thread was deleted in the meantime, so a removed chat's JSONL is never recreated. The new test holds usageBudget/set open, deletes the chat, then resolves the request and checks that no budget event reaches the transcript. It fails before this commit.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 3 days ago

      P2 Badge Settle pending close requests when the renderer crashes

      If the renderer crashes after this timer is armed, the coordinator does not notice until the full 15-second response timeout. During application quit this stalls shutdown even though the renderer can no longer save, while during an ordinary close the new automatic reload can clear isCrashed() before the timer fires, causing an unresponsive/discard prompt for a replacement renderer that never received the old request ID; notify the coordinator from render-process-gone and settle or reissue the pending request immediately.

      Useful? React with 👍 / 👎.

    2. mweinbach· 3 days ago

      Fixed in f62f31e1. NativeWindowCloseCoordinator.rendererGone(webContents) approves the window's pending close request immediately. render-process-gone in main.ts calls it before recoverGoneRenderer reloads, so the reloaded renderer never inherits the old request ID and a quit doesn't wait out the 15s timeout. There are two new tests, one for an ordinary close and one for quit. Both use a 60s response timeout and settle with no wait. A late reply to the old request ID is ignored.


      Generated by Claude Code

  • mweinbach· 3 days ago

    Fixed in 26c4d0f1. rendererGone now returns true when settling the pending request closes the window, either a user close or a quit. render-process-gone passes that to recoverGoneRenderer, which skips the reload in that case, so a reload can't race the close it just approved. A crash with no close in flight still reloads as before. Tests cover the ordinary close, quit, and no-pending-request cases.


    Generated by Claude Code

  • P1 Badge Bind launchability validation to the opened file

    On POSIX, if an untrusted process changes the executable bits or atomically replaces an ordinary file after isLaunchableFile() successfully stats it but before shell.openPath() reopens the pathname, this check returns false and the replacement can execute outside the sandbox without confirmation. Fresh evidence beyond the earlier stat-failure thread is that the successful-stat path still performs validation and use as separate filesystem operations; bind them to the same file identity or conservatively require confirmation when that cannot be guaranteed.

    AGENTS.md reference: apps/desktop/AGENTS.md:L31-L31

    Useful? React with 👍 / 👎.

  • mweinbach· 3 days ago

    Not fixed in this PR; I'm leaving the thread open for the owner to decide. shell.openPath only takes a pathname. There's no fd- or handle-based way to open, so the check can't be tied to the file that actually gets opened. The only full fix is the conservative one: confirm before opening any file on POSIX. That would add a prompt to every ordinary document open. It's a UX trade-off for the owner, not something to slip into a review fix.

    What's left is narrow. The agent process would have to swap the file or flip its exec bit in the milliseconds between our stat and openPath resolving the path, timed against a user's click. And the failure case (a failed stat) is already handled: it now fails closed (f62f31e1).

    A cheaper middle option: always confirm extensionless files on POSIX, which is where the exec bit decides the handler. Files with a known extension open in their registered app whatever the mode bits say. I can add that if you want the extra coverage.


    Generated by Claude Code

  • View file
    1. chatgpt-codex-connector[bot]· 3 days ago

      P2 Badge Redact arbitrary workspace paths from crash logs

      When an uncaught filesystem error or rejected promise includes a workspace path outside the redactor's hard-coded home/temp prefixes—for example /mnt/client-project/... or D:\repos\customer\...—these new process-wide handlers persist the exact path in desktop-main.log because they call logErrorSync/logError without a workspacePaths redaction context. Pass the approved workspace roots into this logging path or redact arbitrary absolute paths before writing the record.

      AGENTS.md reference: apps/desktop/AGENTS.md:L32-L32

      Useful? React with 👍 / 👎.

    2. mweinbach· 3 days ago

      Fixed in 8249cfd9. localLogs now has a workspace-path provider (setLocalLogWorkspacePaths), and main.ts wires it to the approved workspace roots. Every desktop-main.log record, sync or async, redacts those roots by default, so the crash handlers are covered and so is every other logger call. An explicit per-call context still takes precedence. The new test logs errors that contain /mnt/client-project/... through both logError and logErrorSync, and checks that the path never reaches the file.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 3 days ago

      P2 Badge Scope forced-color suppression to the title text

      In Windows forced-colors mode this selector applies forced-color-adjust: none to every span in every selected row, not just the chat-title span whose Chromium backplate it is fixing. A selected chat with a pending-interaction badge, busy indicator, or draft dot therefore keeps author colors such as bg-secondary or bg-primary while its foreground is forced to HighlightText, which can make those statuses unreadable in the user's high-contrast palette; target an explicit title-text class instead.

      AGENTS.md reference: apps/desktop/AGENTS.md:L56-L56

      Useful? React with 👍 / 👎.

    2. mweinbach· 3 days ago

      Fixed in 8249cfd9. The rule now targets an explicit .app-selected-row-text class instead of every span. That class is on the title and caption spans of selected thread, workspace and task rows. Status dots, busy indicators and badges keep their normal forced-colors treatment. desktop:quality, which includes the forced-colors snapshots, passes locally: 76 passed, 4 skipped.


      Generated by Claude Code

    View file
    1. chatgpt-codex-connector[bot]· 3 days ago

      P2 Badge Refresh the rename baseline after server title updates

      If another client publishes a manual title while rename A is pending, the session_info handler adopts that server title, but a subsequent local rename B reuses this tracker without updating confirmed; when B rejects, rollback restores the pre-A title and overwrites the newer server-confirmed title. Fresh evidence beyond the earlier overlapping-rename reports is that incoming manual session_info titles can change the visible title while this tracker remains alive, so the baseline should be refreshed when the current title was not produced by the tracker.

      Useful? React with 👍 / 👎.

    2. mweinbach· 3 days ago

      Fixed in 8249cfd9. When a rename starts while the tracker is alive and the displayed title isn't one the tracker set, that title becomes the confirmed baseline. Its generation sits ahead of every rename already in flight, so a late success from an older rename can't replace it either. The new test covers your sequence: rename A is pending, a server title arrives, rename B is rejected (the chat shows the server title), then A succeeds late (the server title stays). It fails before this commit.


      Generated by Claude Code

    #349 main ← claude/loving-edison-t3eun2

    Updated 3 days ago

    merged