Skip to content

feat(desktop): five Settings additions — CLI install, config folder, shortcuts, resets - #130

Merged
siracusa5 merged 2 commits into
mainfrom
c/brave-cerf-2352a0
Aug 11, 2026
Merged

feat(desktop): five Settings additions — CLI install, config folder, shortcuts, resets#130
siracusa5 merged 2 commits into
mainfrom
c/brave-cerf-2352a0

Conversation

@siracusa5

Copy link
Copy Markdown
Collaborator

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). Unlike reveal-file, no renderer input crosses the bridge at all — the path is configDir() 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 bridge ConnectAgentDialog consumes — and reports all seven states, offering Install/Reinstall only for missing/stale. The refusal states (blocked, conflict, permission failures) keep their native dialogs from cli-install.mjs, so this row never duplicates that copy.

contextcake:cli-status / cli-install widen 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 — validateSettingsPatch already treats null as "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, and menu.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, which persistSettings (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 as preferences: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

  • Every new channel has an explicit TRUSTED_IPC_ROLES entry (trustedRolesForChannel throws without one) and is asserted in trusted-windows.test.mjs.
  • No new channel accepts a renderer-supplied path. Export's destination comes from a native dialog; reveal-config-dir and reset take no arguments.
  • The preload surface stays narrow and optional; every consumer treats __CC_DESKTOP and settingsFile as possibly absent, so the console keeps working in plain browsers. Three tests pin the browser-side absence.
  • No dependency reaches packages/core.

Verification

  • apps/console: npm run typecheck clean, 440/440 tests pass (25 in SettingsView.test.tsx, 13 in IndexingSettings.test.tsx).
  • apps/desktop: 105/105 on Node 22 specifically, per the CI-version gotcha in apps/desktop/CLAUDE.md.
  • npm run test:navigation, npm run test:cli-status, and npm run smoke all pass.

Not verified in a packaged build: the native save/confirm dialogs and the real symlink install path are behind app.isPackaged or need a signed bundle.

🤖 Generated with Claude Code

siracusa5 and others added 2 commits August 11, 2026 14:32
…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>
@siracusa5
siracusa5 force-pushed the c/brave-cerf-2352a0 branch from 06a0685 to bf97387 Compare August 11, 2026 18:32
@siracusa5

Copy link
Copy Markdown
Collaborator Author

Adversarial review round

Four 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 bf97387.

Blocking findings, fixed

Export leaked account data into a file the UI calls bug-report-safe. The handler wrote readSettings() verbatim, including _sync — which carries ownerUserId (the account UUID) and shadow, the last synced blob holding layer names and repo slugs that scrubSettings deliberately preserves. Today's builds ship {"accounts":"disabled"} so those fields are empty, which is exactly what made this dangerous: the leak would have arrived silently the day accounts ship, in a file whose own row copy promises it "never includes credentials or your documents." Now stripped, and written 0600 to match every other writer of this content rather than the default 0644.

Two promises in the reset dialog were false.

  • It claimed window position returns to defaults, but the live window writes its bounds back on close and before-quit regardless — the reset would report success and visibly do nothing. Geometry is now preserved deliberately, and the copy no longer claims otherwise.
  • It cleared anonymousMetrics, silently discarding a consent answer and re-prompting at the next launch (metrics-consent.mjs prompts on undefined). A privacy decision is not a preference. It survives, and the dialog says so.

Reset never pushed to a signed-in account. It marked the file dirty but omitted scheduleSettingsPush(), so a second Mac kept pulling pre-reset values until something else happened to write. Since macOS keeps the app alive after the red X, that window is open-ended.

The shortcuts list documented keys that cannot fire. store.route() returns immediately outside demo mode and live mode carries no signals at all, so the Review-queue S/R/D group was inert in the only surface the Mac app runs. Now demo-only. The list also hardcoded ⌘ while every binding is CmdOrCtrl and the console ships as a public Web Demo — a Windows visitor was being told to press a key they do not have. The glyph now follows the platform.

Settings-only rows gated on bridge presence, not window role. One preload serves both windows, so revealConfigDir/settingsFile exist in the main window too, while their IPC channels are ['settings']-only. Those rows would have answered "Untrusted IPC sender" — and for Reset, that error path raises the "could not be saved to this Mac" banner while nothing had been reset. Now gated on windowRole === 'settings', so the renderer guard matches the IPC policy instead of merely happening to agree with it.

CliControl's error handling did not do what its comment claimed. Promise.resolve(bridge.getStatus()) does not catch a preload missing getStatus entirely — the call throws synchronously, before any promise exists. With no error boundary in this app, that blanks the whole Settings window rather than hiding one row. Now a real try/catch, with a test that mounts cli: {}.

Reset All raced the field the user had just edited. Mousedown blurs the focused input, firing its onBlur save, so two PATCHes flew and the later one won — the visible outcome was Reset All appearing not to reset. Inputs and unit selects now disable during a reset.

The tests were partly theater — proven by mutation

Reviewer four mutated the source and re-ran, rather than reading. Four new cases passed against defects they were named for:

Mutation Was Now
resetSettings() with its _sync carry-over deleted passed fails
Export rendering an error on user cancel passed fails
Reset treating cancel as a write failure untested fails
Install offered in blocked/conflict/development passed fails
Reset All sending only the first catalog key caught caught
S/R/D listed in live mode untested fails

The _sync one mattered most: settings-sync gates its dirty-push on ownerUserId matching the session, so dropping it turns the next pull into a merge that restores precisely what was just reset — and the test asserted only revision/dirty/dirtyFields. Seeding _sync had to be done on disk rather than through writeLocalSettings, because persistSettings recomputes _sync and discards any patch that tries to set it.

Every fix above was mutation-verified individually after the fact.

Filed separately, not fixed here

apps/desktop/src/main/main.mjs contains a raw NUL byte at line 1321 (service?.origin ?? '<NUL>', where an escape was intended). It predates this branch — present in 85f0873. It makes grep silently return zero matches for the entire file without -a, which misled two independent reviewers and me during this session. Out of scope here; worth its own one-character fix.

Verification

Console: typecheck clean, 446/446. Desktop: 106/106 on Node 22 specifically, plus test:navigation, test:cli-status, and smoke all passing.

@siracusa5
siracusa5 merged commit 41d3256 into main Aug 11, 2026
8 checks passed
@siracusa5
siracusa5 deleted the c/brave-cerf-2352a0 branch August 11, 2026 18:36
siracusa5 added a commit that referenced this pull request Aug 11, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant