Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

33 KiB

Troubleshooting: Desktop And Release

Back to troubleshooting index

A published stable release is missing from the public changelog

  • Symptom: The stable GitHub Release and its installers are public, but https://tutti.sh/changelog skips that version and the aggregate changelog.json does not contain its tag.
  • Quick checks: Inspect the public changelog.json, then confirm the GitHub Release is a published non-prerelease with a release-summary.json asset. Check the original promotion run's Update stable changelog metadata step and any later release or recovery runs that rewrote the mutable aggregate.
  • Root cause: The public page renders the release-owned aggregate rather than deriving entries from GitHub on each request. If that mutable aggregate loses an entry, later releases preserve the incomplete baseline even though the missing release and its immutable summary remain valid.
  • Fix: Run Repair Desktop Release Changelog with the exact missing stable tag. The workflow validates the published release and staged summary metadata, rebuilds the public entry from the current human-reviewed Release Notes, preserves every existing entry, and upserts the corrected summary without moving release pointers or republishing assets. Do not rerun promotion for an older stable tag because its rollback guard correctly rejects moving the public channel behind the current release.
  • Validation: Confirm the workflow's S3 round-trip verification passes, then verify the public aggregate and https://tutti.sh/changelog both contain the restored version after the configured cache window.
  • References: .github/workflows/desktop-release-changelog-repair.yml upsert-release-changelog.mjs

Packaged Tutti starts but external shells cannot find tutti

  • Symptom: The packaged desktop app starts normally and ~/.tutti/bin/tutti status succeeds, but a new terminal reports command not found: tutti.
  • Quick checks: Run command -v tutti, print the login-shell PATH, and inspect ~/.tutti/bin/tutti. Check ~/.tutti/logs/tutti-desktop.log for tutti cli shim is not discoverable on user PATH and check whether an unrelated tutti executable already exists earlier on PATH.
  • Root cause: Creating the canonical shim under the state root does not make it shell discoverable unless <state-dir>/bin is already on PATH. Desktop apps launched from Finder may also inherit a smaller PATH than the user's login shell, so using only the Electron process environment misses user bin directories.
  • Fix: Reuse the cached login-shell environment resolved for the managed daemon. Keep <state-dir>/bin/tutti canonical, and install a forwarding shim only in writable ~/.local/bin or ~/bin directories already present on the login shell's PATH. Repair Tutti-owned shims on later startups, preserve unrelated commands, and do not edit shell profiles or write /usr/local/bin. Keep this installation best-effort so a slow or invalid shell environment cannot delay desktop window creation.
  • Validation: Start packaged Tutti, open a new login shell, and verify command -v tutti resolves to a user bin directory and tutti status succeeds. Cover creation, repair, canonical-PATH, no-supported-directory, and third-party-command conflict cases in desktop tests.
  • References: cliInstaller.ts userShellEnv.ts desktop-transport.md

Desktop stable release alias disappears or is not first on Releases

  • Symptom: The desktop release workflow publishes a concrete release, but the Refresh stable release alias step fails with Committer identity unknown, the GitHub Releases page no longer has a stable entry after a failed RC publish, or the GitHub Releases list still puts a newer RC above stable.
  • Quick checks: Inspect the failed Desktop Release run's Refresh stable release alias step. If the log shows git tag -a or gh release delete stable --cleanup-tag, the workflow is using the unsafe annotated-tag refresh path. Also check gh release view stable and git ls-remote --tags origin stable to confirm whether the release, tag, or both are missing. For ordering failures, list public prereleases with gh api 'repos/$GITHUB_REPOSITORY/releases?per_page=100' --jq '.[] | select(.prerelease and (.draft | not)) | .tag_name' and confirm the workflow ran Archive public GitHub prereleases.
  • Root cause: Annotated tags require a configured Git committer identity in GitHub Actions. Deleting the old floating release and tag before creating the replacement leaves the repository in a half-refreshed state if tag creation fails. GitHub's public Releases list has no supported pin or explicit order field. Recreating the alias and assigning it a newer commit timestamp does not reliably place it above public prereleases.
  • Fix: Keep RC and beta GitHub Releases as drafts and distribute them through the S3 preview-channel metadata instead. The workflow must archive any older public prereleases with PATCH draft=true, then refresh the floating stable release. Delete only the old stable alias (gh release delete stable --yes) and never pass --cleanup-tag; keep the concrete stable release as Latest.
  • Validation: Run node --test ./tools/scripts/desktop-release-config.test.mjs and verify the workflow test checks that stable is the only release promoted from draft, archives legacy public prereleases, checks the alias tree and parent, and rejects git tag -a, --cleanup-tag, and deleting refs/tags/stable. After a live release, confirm the GitHub Releases page lists stable first while /releases/latest still resolves to the concrete stable semver release.
  • References: .github/workflows/desktop-release.yml desktop-release-config.test.mjs

Desktop release notes exceed GitHub's body limit

  • Symptom: All architecture builds and asset uploads succeed, but Stage Release Draft fails in Update release notes with summary with HTTP 422 and body is too long (maximum is 125000 characters). Promotion and notification jobs are then skipped.
  • Quick checks: Inspect the failed step rather than rebuilding the installers. Run gh release view <tag> --json body --jq '.body | length' and check whether the generated What's Changed section contains repository-wide history.
  • Root cause: GitHub's implicit release-note comparison selects published releases. Tutti intentionally keeps RC and beta GitHub Releases as drafts, so the implicit base can remain far behind and every prerelease accumulates the same old pull requests until the Release body reaches GitHub's limit.
  • Fix: Resolve the previous same-channel tag explicitly and pass it as softprops/action-gh-release's previous_tag. Keep the shared Release body limiter as a fallback: it preserves managed summary and direct-download sections and trims only generated detail entries. Re-running an old Actions run still uses its original workflow revision; merge the workflow fix before starting a new release, or repair the existing draft notes before manually promoting that draft.
  • Validation: Generate notes with the failing tag and its resolved previous tag and confirm the body is bounded to that comparison range. Run node --test tools/scripts/desktop-release-summary.test.mjs tools/scripts/desktop-release-download-links.test.mjs tools/scripts/desktop-release-config.test.mjs.
  • References: .github/workflows/desktop-release.yml resolve-previous-release-tag.mjs githubReleaseBody.mjs

Desktop release stalls after all packages finish building

  • Symptom: Every macOS and Windows build succeeds and the GitHub draft assets are staged, but Stage Release Draft remains in Install AWS CLI until GitHub cancels the job near its six-hour runtime limit. The last output comes from apt-get update and contains repeated Ign lines for Ubuntu package indexes.
  • Quick checks: Inspect the start of Install AWS CLI. If the shell still runs sudo apt-get update, the Actions run used a workflow revision from before the bounded bootstrap fix. Confirm no later curl or aws --version line appears; that distinguishes an APT mirror stall from an AWS CLI download or credential failure.
  • Root cause: The old bootstrap refreshed Ubuntu package indexes before checking for AWS CLI. A transient runner mirror failure could therefore block an otherwise complete release, and the step had no deadline below GitHub's job limit.
  • Fix: Start a new release run from a revision that skips APT, extracts the official AWS CLI archive with python3 -m zipfile, retries bounded downloads, and limits the complete install step to five minutes. Re-running the original Actions run is insufficient because GitHub reuses that run's old workflow revision.
  • Validation: Run node --test tools/scripts/desktop-release-config.test.mjs. Confirm both release workflows reject apt-get, require the download retry/connect/total time limits, use Python extraction, and set the step deadline.
  • References: .github/workflows/desktop-release.yml .github/workflows/desktop-release-promote.yml desktop-release-config.test.mjs

Desktop dev GUI exits before opening

  • Symptom: make dev-gui exits during startup before the desktop window is usable. The early form reports pnpm <version> installation did not succeed; the later form reaches start electron app... and then make exits while desktop logs say secondary tutti instance detected. A database form reports no such table: model_plan_first_use_candidates, followed by tuttid exited before it published its listener info. Another early form exits while checking prerequisites because a stale pnpm shim reports that its bundled ../node/bin/node no longer exists. A catalog form starts successfully but selects a legacy workspace-app release because the daemon reports the development package placeholder version 0.0.0.
  • Quick checks: Run DEV_GUI_SKIP_START=1 make dev-gui to isolate prerequisite setup from Electron startup. If full startup exits after start electron app..., inspect ~/.tutti-dev/logs/tutti-desktop.log and check whether /Applications/Tutti.app or another Tutti instance is already running.
  • Root cause: Shells launched by tools can put another pnpm earlier on PATH than corepack's shim, so corepack prepare succeeds but the script still validates the wrong pnpm. That earlier shim can also be a symlink into a relocated runtime cache, so invoking pnpm --version fails before the script has a chance to run Corepack. Electron's single-instance lock also follows Electron userData; if development and production share userData, a running production app makes the dev app quit as a secondary instance. Agent shells launched from the packaged app may inherit TUTTI_ENV=production, so make dev-gui must force the development environment instead of preserving that inherited value. Electron development reads the placeholder version from apps/desktop/package.json; passing that value to the daemon makes compatibility-gated catalog releases ineligible. The missing-table variant comes from a development database that already recorded model_plans_v1 before the candidate table was added behind that same marker. SQLite correctly skips the recorded migration, so startup reconciliation reaches a physical schema that the marker does not describe.
  • Fix: Probe pnpm --version without letting a broken shim abort startup, discover Corepack from the active or locally installed Node runtime, prefer that Corepack shim directory before checking or running pnpm, and set development Electron userData to an environment-specific path before requesting the single-instance lock. Ensure the dev-gui script exports TUTTI_ENV=development before resolving pid files, installing the dev CLI, or launching Electron. Resolve and export TUTTI_APP_VERSION from the repository Git description before launch, while preserving an explicit developer override. Apply a new, idempotent model_plan_first_use_candidates_v1 forward migration after model_plans_v1; do not require deleting the development database and do not rewrite or reuse the recorded migration identifier.
  • Validation: Run DEV_GUI_SKIP_START=1 make dev-gui and confirm it logs a SemVer-compatible Git-derived desktop version rather than 0.0.0. Then run full make dev-gui while the packaged app is open and confirm the renderer dev server and development tuttid start. Also run pnpm --filter @tutti-os/desktop test, pnpm --filter @tutti-os/desktop typecheck, and pnpm check:electron-runtime-boundaries. Retain a migration test that starts with model_plans_v1 recorded and the candidate table absent, then verifies a full migration pass restores the table and index exactly once.
  • References: dev-gui.sh bootstrap.ts defaults.ts migrations_model_plans.go

Running a development tuttid breaks the production Agent session

  • Symptom: Production Tutti is used to develop Tutti itself. After an Agent runs a newly built tuttid command, sending in a new conversation fails and the workspace returns to the previously selected conversation. Daemon logs may report an unsupported Agent Target launch-ref type immediately after a second daemon starts.
  • Quick checks: List live tuttid processes and compare their command paths. Inspect ~/.tutti/logs/tuttid.log for overlapping startup records, then check the command run by the Agent for a bare daemon binary without TUTTI_ENV=development or TUTTI_STATE_DIR. A historical tuttid --help invocation is significant because older binaries treated it as normal startup.
  • Root cause: Bare daemon execution selects the production root. Older tuttid binaries did not parse --help and overwrote the shared PID file instead of claiming exclusive state ownership. The second process could open the production SQLite database and reseed system Agent Targets with a newer launch-ref discriminator while the packaged daemon still expected the older value. Session creation then failed, and renderer recovery restored the previous conversation.
  • Immediate recovery: Quit Tutti completely, terminate any remaining tuttid processes after verifying their command paths, then reopen production Tutti. Do not delete tuttid.db; normal daemon startup reseeds its own system records. For further local daemon work, use the managed development command or explicitly set TUTTI_ENV=development/TUTTI_STATE_DIR.
  • Fix: Parse help and reject unknown arguments before creating state. Acquire the PID sidecar as an exclusive operating-system lease before logging, lock recovery, database wiring, migrations, or listener publication. Keep the PID text check so a new daemon also refuses a state root owned by a live older daemon, but validate process identity so PID reuse by an unrelated process does not block recovery. Leave the marker for stale-owner recovery instead of deleting it through a read/remove race with an older lockless daemon.
  • Validation: Run focused daemon tests. Verify tuttid --help exits successfully without creating the selected state root, invalid arguments exit nonzero without state, a live legacy PID is rejected, a stale PID is recovered, and a second lease cannot be acquired while the first is held.
  • References: main.go pid_file.go local-state-storage.md

macOS updates fail from a mounted DMG

  • Symptom: A packaged macOS build can check for and download an update, but clicking install appears to do nothing or logs an updater error such as Cannot update while running on a read-only volume. The desktop log shows the app executable under /Volumes/.../Tutti.app, and the daemon may stop briefly because the update install flow began before the updater rejected the read-only volume.
  • Quick checks: Inspect tutti-desktop.log for process.execPath, updater errors, or managed daemon start lines that point under /Volumes. Confirm whether the user launched Tutti directly from a mounted .dmg instead of the copy in /Applications.
  • Root cause: macOS mounts compressed DMG installers as read-only volumes. Electron's macOS updater cannot replace an app bundle that is running from that volume, so the failure is an install-location problem rather than a dead tuttid process.
  • Fix: In packaged macOS builds, detect /Volumes startup before desktop services and managed tuttid are created. Prompt the user to move Tutti to /Applications, call Electron's application-folder move when accepted, and quit rather than continuing from the mounted image. Development builds must skip this guard so local Electron runs keep working.
  • Validation: Cover the guard with tests for development mode, non-macOS platforms, /Applications, /Volumes, declined installation, successful automatic move, and failed automatic move. Run the desktop tests, desktop typecheck, and i18n check because the guard uses Electron dialog copy.
  • References: macosApplicationInstallGuard.ts bootstrap.ts desktop-release.md

macOS Gatekeeper dialogs appear during Codex provider probing

  • Symptom: Opening an app or surface that reads agent composer options, provider status, or capability catalog triggers repeated macOS warnings that codex may harm the computer.
  • Quick checks: Resolve the active Codex binary from tuttid logs or by running with the same daemon environment. Then inspect the native binary behind the npm shim with spctl --assess --type execute -vv <native-codex-path>. If it reports CSSMERR_TP_CERT_REVOKED, remove or reinstall that specific Codex package.
  • Root cause: Provider status and composer capability discovery intentionally start Codex commands such as codex app-server. If the daemon resolves an older nvm global Codex package whose Developer ID certificate has been revoked, each otherwise harmless background probe can become a Gatekeeper dialog. This can happen even when which codex in the user's shell points at a newer working Codex if the daemon command resolver places scanned nvm fallback directories before the real PATH.
  • Fix: Respect the daemon PATH before scanned nvm fallback directories, and sort nvm fallback directories by Node version so fallback resolution does not pick the oldest installed Node first. Do not automatically remove attributes or delete arbitrary user-managed Codex binaries from Tutti; user repair scripts should only move Codex packages that spctl explicitly reports as certificate revoked, and should keep a backup.
  • Validation: Run go test ./runtimecmd and go test ./runtime from packages/agent/daemon, plus go test ./service/agentstatus from services/tuttid. Verify provider status logs resolve codex to the same npm shim the user expects from PATH, unless PATH lacks Codex and the resolver intentionally falls back to a scanned nvm install.
  • References: resolver.go

Electron main/preload crashes on a workspace package .ts export

  • Symptom: Desktop development starts the renderer, then Electron throws TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for a path under packages/.../src.
  • Quick checks: Inspect the package named in the stack trace and any bundled workspace package that imports it. Run pnpm check:electron-runtime-boundaries to confirm whether the package is being externalized in Electron main/preload instead of bundled.
  • Root cause: Local workspace packages intentionally export source files for monorepo development. Electron main/preload can only execute those packages when electron-vite bundles them. If a bundled workspace package imports another workspace package that is not also listed in the externalizeDepsPlugin exclude list, Node may try to load the transitive package's raw .ts export at runtime.
  • Fix: Add the source-exporting workspace package, including transitive workspace dependencies reached by bundled main/preload code, to apps/desktop/electron.vite.config.ts externalizeDepsPlugin({ exclude }). Prefer narrow non-UI package subpaths for Electron runtime imports so the bundle does not pull React-facing barrels into main/preload.
  • Validation: Run pnpm check:electron-runtime-boundaries and pnpm --filter @tutti-os/desktop build.
  • References: apps/desktop/electron.vite.config.ts tools/scripts/check-electron-runtime-boundaries.mjs

Desktop restart leaves an orphan tuttid

  • Symptom: The desktop logs Timed out waiting for tuttid listener info: daemon runtime information is not available yet, or the first restart reports a daemon startup failure while a second restart succeeds. ps, lsof, or Task Manager may still show an older tuttid process holding the database or a loopback listener.
  • Quick checks: Inspect ~/.tutti-dev/run/tuttid.pid, run lsof on ~/.tutti-dev/tuttid.db, and check whether the daemon process has parent PID 1. That combination means the Electron parent no longer owns the process even though the daemon survived. On Windows, align desktop parent process disappeared, http server shutdown error, process did not exit after kill, stopping stale tuttid process, and disk I/O error (1546) across tuttid.log and tutti-desktop.log. A later managed tuttid restarted after failed to start managed tuttid identifies two competing startup owners rather than a second user action fixing data.
  • Root cause: In development, launching through go run can create a wrapper process and a compiled daemon child. Killing only the direct child can leave the compiled daemon alive. If the desktop also removes the listener info file before the next launch, the orphan can keep local state busy while the new managed daemon never publishes runtime info within the startup timeout. The same symptom can occur in packaged builds when HTTP shutdown returns before active handlers and live Agent transports finish closing. A stale-daemon handoff can transiently fail SQLite WAL initialization with SQLITE_IOERR_TRUNCATE. Initial startup must own recovery until the daemon first becomes healthy; runtime supervision must not independently recover the same child exit while bootstrap propagates the original failure.
  • Fix: Prefer a prebuilt apps/desktop/build/tuttid/tuttid binary in development when present, kill managed daemon process groups during desktop shutdown, write and clear tuttid.pid, and inject TUTTI_DESKTOP_PARENT_PID so tuttid can self-shutdown when its desktop parent disappears. Keep one startup recovery owner until health succeeds, wait for HTTP shutdown before closing daemon wiring, and retry only the bounded transient SQLITE_IOERR_TRUNCATE WAL handoff error. Do not delete WAL/SHM files or retry arbitrary SQLite failures.
  • Validation: Repeatedly quit and restart the desktop, then confirm there is at most one tuttid process and that ~/.tutti-dev/run/tuttid.pid matches it. Fault-inject the first WAL initialization attempt and verify the original desktop bootstrap continues after one recovery loop. Hold an HTTP request open during cancellation and verify daemon Run does not return until the request drains. Also run the desktop daemon-manager tests and cd services/tuttid && go test ./....
  • References: tuttidManager.ts main.go

Switching agent permission mode flashes Checking for updates

  • Symptom: In a packaged build, changing composer permission mode (or other remembered composer defaults) briefly shows the top-right Checking for updates / 正在检查更新 badge. Rapid switches may spam updater checks in tutti-desktop.log (Checking for update next to agent.gui.composer_defaults.remembered). Local unpackaged dev usually hides this because update checks are unsupported unless a shared DESKTOP_UPDATE_ADMISSION_* development scenario is enabled.
  • Quick checks: Confirm packaged/supportsUpdates. Correlate agent.gui.composer_defaults.remembered with checking for application updates / application updater static feed configured. Verify updateChannel / updatePolicy did not change.
  • Root cause: Remembering composer defaults writes desktop preferences and emits preferences.desktop.updated. The main-process host preferences stream used to call updateService.configure() on every preferences event; configure() always runs a background update check and surfaces the checking status.
  • Fix: Only call updateService.configure() when updateChannel or updatePolicy actually changed. Other preference syncs (composer defaults, locale, rail, theme) must not reconfigure the updater.
  • Validation: In a packaged build, switch permission mode and confirm the update badge does not appear and desktop logs do not emit a new update check. Cover composer-default vs channel/policy changes in desktopHostPreferencesEventStream tests.
  • References: desktopHostPreferencesEventStream.ts appUpdateService.ts

App update diagnostics flood with identical download progress states

  • Symptom: Renderer diagnostics show hundreds of app_update.state_applied events per minute during update downloads, often with identical payloads. AppUpdateStatus may re-render without visible UI changes.
  • Quick checks: Compare consecutive diagnostic payloads for status, downloadPercent, and downloadedBytes. Inspect whether main-process applyState and renderer applyUpdateState both commit every IPC event.
  • Root cause: electron-updater can emit high-frequency download progress callbacks. Without a shared AppUpdateState equality guard at the commit boundary, identical states still emit IPC, write the valtio store, and log diagnostics.
  • Fix: Compare incoming state with the current snapshot via isSameAppUpdateState() before committing in both main applyState and renderer applyUpdateState. Keep diagnostics on successful commits only.
  • Validation: Run desktop app-update tests and confirm repeated identical progress events produce a single state change.
  • References: appUpdateState.ts appUpdateService.ts appUpdateService.ts

macOS in-app update closes Tutti but does not install the new version

  • Symptom: After downloading a desktop update on macOS, clicking Install closes or relaunches Tutti, but reopening the app still shows the old version. ShipIt logs may contain SQRLInstallerErrorDomain Code=-9 or App Still Running Error.
  • Quick checks: Confirm the packaged build is signed (unsigned or ad-hoc builds disable in-app updates). Inspect ~/Library/Caches/sh.tutti.desktop.ShipIt/ShipIt_stderr.log for Aborting update attempt because there are 1 running instances of the target app. Compare /Applications/Tutti.app/Contents/Info.plist with the cached update under ~/Library/Caches/@tutti-osdesktop-updater/pending.
  • Root cause: Squirrel.Mac refuses to replace /Applications/Tutti.app while any target app instance is still running. Stopping tuttid before quitAndInstall() is not sufficient because the Electron main process and helper windows still need to complete the app quit path.
  • Fix: Keep daemon shutdown in the desktop lifecycle instead of the update service. installUpdate() should mark the install pending and call quitAndInstall(). The before-quit gate should still run for pending update installs: prevent the first quit, stop managed tuttid, destroy all windows, then call app.quit() again so the app process exits and ShipIt can replace the bundle.
  • Validation: Run src/main/desktopAppLifecycle.test.ts and src/main/update/appUpdateService.test.ts, including updater error and synchronous quitAndInstall() failure cases. Then install a downloaded update in a packaged macOS build; the app should relaunch on the new version and ShipIt should not log App Still Running Error.
  • References: appUpdateService.ts desktopAppLifecycle.ts desktopAppServices.ts

Desktop Performance trace export runs out of memory

  • Symptom: Chrome DevTools Performance export or trace parsing fails with Maximum call stack size exceeded or V8 CALL_AND_RETRY_LAST OOM while the desktop app is running through make dev-gui.
  • Quick checks: Keep the trace short and disable renderer diagnostics that inflate tracks: VITE_TUTTI_WHY_DID_YOU_RENDER=0 make dev-gui. For CDP-based trace capture, launch with TUTTI_ELECTRON_REMOTE_DEBUGGING_PORT=9223 TUTTI_ELECTRON_JS_FLAGS=--max-old-space-size=8192. Confirm the port with curl http://127.0.0.1:9223/json/version.
  • Root cause: DevTools can run out of stack or old-space memory while processing large trace payloads. Passing extra CLI args through electron-vite is not reliable enough for these diagnostics, so the desktop main process owns the Electron command line switches.
  • Fix: Prefer CDP Tracing.start with transferMode: "ReturnAsStream" for large captures instead of DevTools UI export. The repository helper uses that path: pnpm trace:desktop -- --duration 15. Record only the smallest repro window.
  • Validation: Restart the desktop app, confirm the remote debugging endpoint responds, record a short trace, and verify the trace JSON is written without opening the Performance export path.

macOS screenshot selector leaves the menu bar and Dock uncovered

  • Symptom: The screenshot selector darkens only the macOS work area. The real menu bar and Dock remain visible while a second captured copy of each appears inside the selector preview.
  • Quick checks: Find screenshot selector presented in the Desktop log. Compare windowBounds with displayBounds and displayWorkArea, then inspect simpleFullScreen. A work-area-sized window with simpleFullScreen: false confirms that the native selector never entered simple full-screen; renderer fixed inset-0 styles cannot draw outside that native window.
  • Root cause: Electron's macOS simpleFullscreen constructor option chooses the pre-Lion implementation for a later full-screen request. It does not enter full-screen by itself. Combining simpleFullscreen: true with fullscreen: false therefore leaves a normal frameless window that macOS can present within the work area.
  • Fix: Request fullscreen: true together with macOS simpleFullscreen: true, then explicitly call or verify setSimpleFullScreen(true) before showing the selector. Keep the renderer preview sized to the native viewport and exit full-screen before resizing the same window into the floating Composer.
  • Validation: On macOS, invoke capture from an external application and confirm the selector covers the physical display, including the menu bar and Dock. The Desktop log should report simpleFullScreen: true and windowBounds equal to displayBounds. Complete a crop and verify the floating Composer still uses its remembered work-area-clamped position.
  • References: desktopCaptureService.ts captureSelectionFullscreen.ts