feat(desktop): five Settings additions — CLI install, config folder, shortcuts, resets - #130
Conversation
…shortcuts, resets Surfaces work that already existed but was undiscoverable, and adds the support-flow escape hatches Settings was missing. - Show in Finder for the config folder, over a fixed-path trusted IPC channel (no renderer input at all, unlike reveal-file). - Install Command Line Tool in General, reusing the bridge the Connect Agent dialog already consumes. The two cli channels widen to both window roles because Settings is its own window. - Reset All for the Indexing limits: one PATCH with every catalog key null, so a full reset costs one round trip and one re-index. - A keyboard shortcuts reference, assembled from the three real binding sites (App.tsx handlers, Files.tsx save, menu.mjs accelerators). - Export and reset settings.json. Export flushes the write queue and writes to a natively chosen path; reset confirms natively, then replaces the file rather than merging over it — uiState, geometry and the metrics choice go too, which persistSettings cannot express. Sync bookkeeping survives with the synced fields marked dirty so a signed-in account learns about the reset instead of undoing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Four independent reviewers each returned DO NOT SHIP. The blocking findings, and what changed: - The export wrote `_sync` verbatim, which carries the account UUID and the last synced blob (layer names, repo slugs — `scrubSettings` keeps those). Empty in an accounts-disabled build, so the leak would have arrived silently the day accounts ship, into a file whose own UI copy promises it is safe to attach to a bug report. Now stripped, and written 0600 like every other writer of this content rather than the default 0644. - The export flushed before opening the save panel, so a preference changed while the panel sat open was missed. Flush moved after. - The reset dialog promised window position would return to defaults while the live window writes its bounds back on close and quit — a reset that reports success and visibly does nothing. Geometry is now preserved deliberately and the copy no longer claims otherwise. - The reset cleared `anonymousMetrics`, silently discarding a consent answer and re-prompting at the next launch. A privacy decision is not a preference; it survives, and the dialog says so. - The reset never called scheduleSettingsPush(), so a second Mac kept pulling pre-reset values until something else happened to write. - The shortcuts list advertised the Review queue's S/R/D in the Mac app, where store.route() returns immediately outside demo mode and there are no signals at all — three keys that cannot fire. Now demo-only. - The list hardcoded ⌘ while every binding is CmdOrCtrl and the console ships as a public Web Demo. The glyph follows the platform. - Settings-only rows gated on bridge presence, not window role. One preload serves both windows, so the main window would have rendered controls whose IPC answers "Untrusted IPC sender" — and for Reset that error raised the "could not be saved" banner while nothing was reset. - CliControl's `Promise.resolve(bridge.getStatus())` did not cover what its comment claimed: a preload missing getStatus throws synchronously, before any promise exists, and with no error boundary that blanks the whole Settings window. Now a real try/catch. - Reset All raced the blur-save of the field the user had just edited, so two PATCHes flew and the later one won. Inputs disable during it. Tests: mutation testing proved four of the new cases were theater — resetSettings passed with its `_sync` carry-over deleted, the export cancel path passed with cancel wrongly rendering an error, reset-cancel was untested entirely, and the CLI gate was untested for every refusal state. All now fail against those mutations, verified one at a time. The exact-JSON PATCH assertion became a parsed toEqual so a harmless catalog reordering no longer fails it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
06a0685 to
bf97387
Compare
Adversarial review roundFour independent reviewers examined this with distinct lenses (Electron trust boundary, settings/persistence correctness, console/React behavior, and mutation-testing the tests themselves). All four returned DO NOT SHIP. Fixes are in Blocking findings, fixedExport leaked account data into a file the UI calls bug-report-safe. The handler wrote Two promises in the reset dialog were false.
Reset never pushed to a signed-in account. It marked the file dirty but omitted The shortcuts list documented keys that cannot fire. Settings-only rows gated on bridge presence, not window role. One preload serves both windows, so
Reset All raced the field the user had just edited. Mousedown blurs the focused input, firing its The tests were partly theater — proven by mutationReviewer four mutated the source and re-ran, rather than reading. Four new cases passed against defects they were named for:
The Every fix above was mutation-verified individually after the fact. Filed separately, not fixed here
VerificationConsole: typecheck clean, 446/446. Desktop: 106/106 on Node 22 specifically, plus |
main moved ahead by four PRs while this branch was open. Three of them interacted with it directly: #131 added packages/core/tests/sidecar-state.test.mjs to the test chain. The only conflict was package.json's "test" script — this branch replaced the chain with scripts/test.mjs, so a mechanical "keep ours" would have silently dropped that suite from the gate. It is registered now, and the runner grew assertEverySuiteRegistered(): on a full run it walks packages/core/tests and scripts/tests and refuses to start if a test file exists that no suite names. The old chain had the same failure mode and no way to notice it. #132 removed two stray NUL bytes from service.mjs and main.mjs — the actual cause of the grep blindness this branch had documented in CONTRIBUTING as a locale problem. That diagnosis was wrong: a NUL byte makes grep treat a file as binary regardless of LANG, and both files now grep cleanly with LANG unset. The section is corrected to name the real cause, and scripts/test.mjs gained assertNoNulBytes() so a reintroduced NUL fails the gate instead of quietly hiding a file from every search. #132 fixed the instances; nothing stopped the next one. #130/#129 touched console and desktop only and merged clean. Also aligns packages/core/package.json's node floor with the root's new >=22. Verified after the merge: npm test 33/33; console typecheck + 453 tests; desktop 106 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Five Settings ideas brainstormed after #128 but never built. Each surfaces work that already existed but was undiscoverable, or adds a support-flow escape hatch Settings was missing.
What changed
1. Show in Finder for the config folder. A fixed-path trusted IPC channel (
contextcake:reveal-config-dir). Unlikereveal-file, no renderer input crosses the bridge at all — the path isconfigDir()on the main-process side, so the only folder this can ever show is the one the app owns. A missing folder answers{ok: false, error}as data rather than throwing.2. Install Command Line Tool in General. The menu item and the full IPC already existed; Settings just never offered it. The new row reuses
window.__CC_DESKTOP.cli— the same bridgeConnectAgentDialogconsumes — and reports all seven states, offering Install/Reinstall only formissing/stale. The refusal states (blocked,conflict, permission failures) keep their native dialogs fromcli-install.mjs, so this row never duplicates that copy.contextcake:cli-status/cli-installwiden from['main']to both window roles, because Settings is its own window. The capability itself does not widen: the install only ever symlinks the bundled shim, whichever window asks.3. Reset All for the Indexing limits. No engine change needed —
validateSettingsPatchalready treatsnullas "drop the stored value" per key, so one PATCH carrying every catalog key as null resets everything in one round trip and one re-index, rather than N sequential per-field resets. Shown only when at least one limit is actually stored; failures (e.g. the 409 when an invalid layer blocks a manifest write) render in place.4. Keyboard shortcuts reference. A new section on General, assembled from the three real binding sites —
App.tsx's handlers,Files.tsx's save, andmenu.mjs's accelerators — including the Review queue's S/R/D with the actual button copy. A comment names all three sites so a future chord change knows this list exists.5. Export and reset settings.json. Two Settings-only channels. Export flushes the write queue first (so the copy is what the process believes, not one patch behind) and writes to a path chosen in a native save dialog — the renderer never names a destination. Reset confirms in a native dialog beside the destructive act, so no renderer state can skip it.
Reset needed a new
resetSettings()because it is a replacement, not a patch:uiState, window geometry and the metrics choice must go too, whichpersistSettings(a spread over the current file) cannot express. Sync bookkeeping survives with the revision bumped and the synced fields marked dirty, so a signed-in account learns about the reset instead of writing the old values back on the next pull. The write is followed by the same side effects aspreferences:set— appearance re-applies and broadcasts, the updater re-reads its flag, the menu rebuilds, and any pending first-launch metrics report is cancelled since its consent is gone.Boundaries respected
TRUSTED_IPC_ROLESentry (trustedRolesForChannelthrows without one) and is asserted intrusted-windows.test.mjs.__CC_DESKTOPandsettingsFileas possibly absent, so the console keeps working in plain browsers. Three tests pin the browser-side absence.packages/core.Verification
apps/console:npm run typecheckclean, 440/440 tests pass (25 inSettingsView.test.tsx, 13 inIndexingSettings.test.tsx).apps/desktop: 105/105 on Node 22 specifically, per the CI-version gotcha inapps/desktop/CLAUDE.md.npm run test:navigation,npm run test:cli-status, andnpm run smokeall pass.Not verified in a packaged build: the native save/confirm dialogs and the real symlink install path are behind
app.isPackagedor need a signed bundle.🤖 Generated with Claude Code