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
33 KiB
33 KiB
Troubleshooting: Desktop And Release
A published stable release is missing from the public changelog
- Symptom:
The stable GitHub Release and its installers are public, but
https://tutti.sh/changelogskips that version and the aggregatechangelog.jsondoes not contain its tag. - Quick checks:
Inspect the public
changelog.json, then confirm the GitHub Release is a published non-prerelease with arelease-summary.jsonasset. Check the original promotion run'sUpdate stable changelog metadatastep 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 Changelogwith 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/changelogboth 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 statussucceeds, but a new terminal reportscommand not found: tutti. - Quick checks:
Run
command -v tutti, print the login-shellPATH, and inspect~/.tutti/bin/tutti. Check~/.tutti/logs/tutti-desktop.logfortutti cli shim is not discoverable on user PATHand check whether an unrelatedtuttiexecutable already exists earlier onPATH. - Root cause:
Creating the canonical shim under the state root does not make it shell
discoverable unless
<state-dir>/binis already onPATH. Desktop apps launched from Finder may also inherit a smallerPATHthan 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/tutticanonical, and install a forwarding shim only in writable~/.local/binor~/bindirectories already present on the login shell'sPATH. 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 tuttiresolves to a user bin directory andtutti statussucceeds. 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 aliasstep fails withCommitter identity unknown, the GitHub Releases page no longer has astableentry after a failed RC publish, or the GitHub Releases list still puts a newer RC abovestable. - Quick checks:
Inspect the failed
Desktop Releaserun'sRefresh stable release aliasstep. If the log showsgit tag -aorgh release delete stable --cleanup-tag, the workflow is using the unsafe annotated-tag refresh path. Also checkgh release view stableandgit ls-remote --tags origin stableto confirm whether the release, tag, or both are missing. For ordering failures, list public prereleases withgh api 'repos/$GITHUB_REPOSITORY/releases?per_page=100' --jq '.[] | select(.prerelease and (.draft | not)) | .tag_name'and confirm the workflow ranArchive 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 floatingstablerelease. Delete only the old stable alias (gh release delete stable --yes) and never pass--cleanup-tag; keep the concrete stable release asLatest. - Validation:
Run
node --test ./tools/scripts/desktop-release-config.test.mjsand 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 rejectsgit tag -a,--cleanup-tag, and deletingrefs/tags/stable. After a live release, confirm the GitHub Releases page listsstablefirst while/releases/lateststill 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 Draftfails inUpdate release notes with summarywith HTTP 422 andbody 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 generatedWhat's Changedsection 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'sprevious_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 Draftremains inInstall AWS CLIuntil GitHub cancels the job near its six-hour runtime limit. The last output comes fromapt-get updateand contains repeatedIgnlines for Ubuntu package indexes. - Quick checks:
Inspect the start of
Install AWS CLI. If the shell still runssudo apt-get update, the Actions run used a workflow revision from before the bounded bootstrap fix. Confirm no latercurloraws --versionline 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 rejectapt-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-guiexits during startup before the desktop window is usable. The early form reportspnpm <version> installation did not succeed; the later form reachesstart electron app...and thenmakeexits while desktop logs saysecondary tutti instance detected. A database form reportsno such table: model_plan_first_use_candidates, followed bytuttid exited before it published its listener info. Another early form exits while checking prerequisites because a stalepnpmshim reports that its bundled../node/bin/nodeno longer exists. A catalog form starts successfully but selects a legacy workspace-app release because the daemon reports the development package placeholder version0.0.0. - Quick checks:
Run
DEV_GUI_SKIP_START=1 make dev-guito isolate prerequisite setup from Electron startup. If full startup exits afterstart electron app..., inspect~/.tutti-dev/logs/tutti-desktop.logand check whether/Applications/Tutti.appor another Tutti instance is already running. - Root cause:
Shells launched by tools can put another
pnpmearlier onPATHthan corepack's shim, socorepack preparesucceeds but the script still validates the wrongpnpm. That earlier shim can also be a symlink into a relocated runtime cache, so invokingpnpm --versionfails 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 inheritTUTTI_ENV=production, somake dev-guimust force the development environment instead of preserving that inherited value. Electron development reads the placeholder version fromapps/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 recordedmodel_plans_v1before 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 --versionwithout letting a broken shim abort startup, discover Corepack from the active or locally installed Node runtime, prefer that Corepack shim directory before checking or runningpnpm, and set development Electron userData to an environment-specific path before requesting the single-instance lock. Ensure the dev-gui script exportsTUTTI_ENV=developmentbefore resolving pid files, installing the dev CLI, or launching Electron. Resolve and exportTUTTI_APP_VERSIONfrom the repository Git description before launch, while preserving an explicit developer override. Apply a new, idempotentmodel_plan_first_use_candidates_v1forward migration aftermodel_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-guiand confirm it logs a SemVer-compatible Git-derived desktop version rather than0.0.0. Then run fullmake dev-guiwhile the packaged app is open and confirm the renderer dev server and developmenttuttidstart. Also runpnpm --filter @tutti-os/desktop test,pnpm --filter @tutti-os/desktop typecheck, andpnpm check:electron-runtime-boundaries. Retain a migration test that starts withmodel_plans_v1recorded 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
tuttidcommand, 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
tuttidprocesses and compare their command paths. Inspect~/.tutti/logs/tuttid.logfor overlapping startup records, then check the command run by the Agent for a bare daemon binary withoutTUTTI_ENV=developmentorTUTTI_STATE_DIR. A historicaltuttid --helpinvocation is significant because older binaries treated it as normal startup. - Root cause:
Bare daemon execution selects the production root. Older
tuttidbinaries did not parse--helpand 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
tuttidprocesses after verifying their command paths, then reopen production Tutti. Do not deletetuttid.db; normal daemon startup reseeds its own system records. For further local daemon work, use the managed development command or explicitly setTUTTI_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 --helpexits 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.logforprocess.execPath, updater errors, or managed daemon start lines that point under/Volumes. Confirm whether the user launched Tutti directly from a mounted.dmginstead 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
tuttidprocess. - Fix:
In packaged macOS builds, detect
/Volumesstartup before desktop services and managedtuttidare 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
codexmay harm the computer. - Quick checks:
Resolve the active Codex binary from
tuttidlogs or by running with the same daemon environment. Then inspect the native binary behind the npm shim withspctl --assess --type execute -vv <native-codex-path>. If it reportsCSSMERR_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 whenwhich codexin 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
spctlexplicitly reports as certificate revoked, and should keep a backup. - Validation:
Run
go test ./runtimecmdandgo test ./runtimefrompackages/agent/daemon, plusgo test ./service/agentstatusfromservices/tuttid. Verify provider status logs resolvecodexto 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 underpackages/.../src. - Quick checks:
Inspect the package named in the stack trace and any bundled workspace package
that imports it.
Run
pnpm check:electron-runtime-boundariesto 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-vitebundles them. If a bundled workspace package imports another workspace package that is not also listed in theexternalizeDepsPluginexcludelist, Node may try to load the transitive package's raw.tsexport 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.tsexternalizeDepsPlugin({ 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-boundariesandpnpm --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 oldertuttidprocess holding the database or a loopback listener. - Quick checks:
Inspect
~/.tutti-dev/run/tuttid.pid, runlsofon~/.tutti-dev/tuttid.db, and check whether the daemon process has parent PID1. That combination means the Electron parent no longer owns the process even though the daemon survived. On Windows, aligndesktop parent process disappeared,http server shutdown error,process did not exit after kill,stopping stale tuttid process, anddisk I/O error (1546)acrosstuttid.logandtutti-desktop.log. A latermanaged tuttid restartedafterfailed to start managed tuttididentifies two competing startup owners rather than a second user action fixing data. - Root cause:
In development, launching through
go runcan 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 withSQLITE_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/tuttidbinary in development when present, kill managed daemon process groups during desktop shutdown, write and cleartuttid.pid, and injectTUTTI_DESKTOP_PARENT_PIDsotuttidcan 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 transientSQLITE_IOERR_TRUNCATEWAL 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
tuttidprocess and that~/.tutti-dev/run/tuttid.pidmatches 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 daemonRundoes not return until the request drains. Also run the desktop daemon-manager tests andcd 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 updatenext toagent.gui.composer_defaults.remembered). Local unpackaged dev usually hides this because update checks are unsupported unless a sharedDESKTOP_UPDATE_ADMISSION_*development scenario is enabled. - Quick checks:
Confirm packaged/
supportsUpdates. Correlateagent.gui.composer_defaults.rememberedwithchecking for application updates/application updater static feed configured. VerifyupdateChannel/updatePolicydid not change. - Root cause:
Remembering composer defaults writes desktop preferences and emits
preferences.desktop.updated. The main-process host preferences stream used to callupdateService.configure()on every preferences event;configure()always runs a background update check and surfaces the checking status. - Fix:
Only call
updateService.configure()whenupdateChannelorupdatePolicyactually 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
desktopHostPreferencesEventStreamtests. - References: desktopHostPreferencesEventStream.ts appUpdateService.ts
App update diagnostics flood with identical download progress states
- Symptom:
Renderer diagnostics show hundreds of
app_update.state_appliedevents per minute during update downloads, often with identical payloads.AppUpdateStatusmay re-render without visible UI changes. - Quick checks:
Compare consecutive diagnostic payloads for
status,downloadPercent, anddownloadedBytes. Inspect whether main-processapplyStateand rendererapplyUpdateStateboth commit every IPC event. - Root cause:
electron-updatercan emit high-frequency download progress callbacks. Without a sharedAppUpdateStateequality 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 mainapplyStateand rendererapplyUpdateState. 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=-9orApp 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.logforAborting update attempt because there are 1 running instances of the target app. Compare/Applications/Tutti.app/Contents/Info.plistwith the cached update under~/Library/Caches/@tutti-osdesktop-updater/pending. - Root cause:
Squirrel.Mac refuses to replace
/Applications/Tutti.appwhile any target app instance is still running. StoppingtuttidbeforequitAndInstall()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 callquitAndInstall(). Thebefore-quitgate should still run for pending update installs: prevent the first quit, stop managedtuttid, destroy all windows, then callapp.quit()again so the app process exits and ShipIt can replace the bundle. - Validation:
Run
src/main/desktopAppLifecycle.test.tsandsrc/main/update/appUpdateService.test.ts, including updater error and synchronousquitAndInstall()failure cases. Then install a downloaded update in a packaged macOS build; the app should relaunch on the new version and ShipIt should not logApp 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 exceededor V8CALL_AND_RETRY_LASTOOM while the desktop app is running throughmake 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 withTUTTI_ELECTRON_REMOTE_DEBUGGING_PORT=9223 TUTTI_ELECTRON_JS_FLAGS=--max-old-space-size=8192. Confirm the port withcurl 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-viteis not reliable enough for these diagnostics, so the desktop main process owns the Electron command line switches. - Fix:
Prefer CDP
Tracing.startwithtransferMode: "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 presentedin the Desktop log. ComparewindowBoundswithdisplayBoundsanddisplayWorkArea, then inspectsimpleFullScreen. A work-area-sized window withsimpleFullScreen: falseconfirms that the native selector never entered simple full-screen; rendererfixed inset-0styles cannot draw outside that native window. - Root cause:
Electron's macOS
simpleFullscreenconstructor option chooses the pre-Lion implementation for a later full-screen request. It does not enter full-screen by itself. CombiningsimpleFullscreen: truewithfullscreen: falsetherefore leaves a normal frameless window that macOS can present within the work area. - Fix:
Request
fullscreen: truetogether with macOSsimpleFullscreen: true, then explicitly call or verifysetSimpleFullScreen(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: trueandwindowBoundsequal todisplayBounds. Complete a crop and verify the floating Composer still uses its remembered work-area-clamped position. - References: desktopCaptureService.ts captureSelectionFullscreen.ts