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.
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.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.readFile, writeFile, previewOSFile, window min/max/drag, getPlatform, appendTranscriptEvent. writeFile was a generic write primitive that also followed dangling symlinks out of the root.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).fs.watch error no longer becomes an uncaught main-process exception.state.json. It is preserved as state.json.corrupt-<ts> instead of being silently overwritten, and writes use writeFileAtomic (Windows rename retries).null (AppImage without APPIMAGE, snap).desktop-main.log; previously only Sentry saw them, and crash reporting is off by default.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).clientMessageId.<fieldset> overflowed (UA min-inline-size: min-content) and clipped Send. This is the adaptive-surfaces failure CI hits on main.aria-pressed.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.
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.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.thread/archived/set exists on the server. Moving them into the harness is a separate design change.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).
openPathnow 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; corruptstate.jsonis 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.
This comment shows the latest Codex review activity on this pull request.
| Review | Status | Commit | Review trigger |
|---|---|---|---|
| 📝 Code Review | ✅ Completed 2026-09-24T22:46:23.923717Z | 8249cfd | New commits |
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.
chatgpt-codex-connector[bot] · · 3 file comments
Here are some automated review suggestions for this pull request.
Reviewed commit: d9db031f8e
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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".
Both Windows smoke (x64) and Windows smoke (arm64) fail on one test that this PR doesn't cause:
apps/desktop/test/server-manager.test.ts › desktop server manager bun crash detection › unexpected real parent exit closes the pipe and drains its managed childChild 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
cursor[bot] · · 1 file comment
Stale Bugbot comment from a previous run.
chatgpt-codex-connector[bot] · · 7 file comments
Here are some automated review suggestions for this pull request.
Reviewed commit: f74d5befd0
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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".
cursor[bot] · · 1 file comment
Stale Bugbot comment from a previous run.
chatgpt-codex-connector[bot] · · 5 file comments
Here are some automated review suggestions for this pull request.
Reviewed commit: 09b83033f0
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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".
cursor[bot] · · 1 file comment
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
chatgpt-codex-connector[bot] · · 4 file comments
Here are some automated review suggestions for this pull request.
Reviewed commit: 26c4d0f1c6
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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".
Sign in to comment.
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.
Reviewed by Cursor Bugbot for commit 9e4a5c1f3b0e6a553bac5fbf6ef6012dca734763. Configure here.
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.
Reviewed by Cursor Bugbot for commit 5d1efc562ae01ecb036db8aabfb3c132ae972617. Configure here.
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.
Reviewed by Cursor Bugbot for commit f62f31e13047357cc891a73dcc064344fc2fb142. Configure here.
Update README.md
#125 mobile-fixes ← main
Updated 4 months ago
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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
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 👍 / 👎.
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