fix(search): stop pinning the omnibox to a paid engine, and make it choosable - #75
Merged
Conversation
…hoosable The address bar was hardcoded to Kagi, which is subscription-only past its trial — so a fresh install landed on a login wall instead of results, and the Settings dropdown didn't help because it only ever drove the new-tab box. - The omnibox engine now comes from `$DATA/search-engine`, written by `tron search <engine>`. Default is DuckDuckGo: it answers without an account. - Drop the Kagi `suggestions_url`. It fired a request on every keystroke in the address bar, which leaks the query before you press enter and stalls typing when the endpoint is slow or answers 401. - Only ever overwrite a search engine that is absent or that we wrote ourselves. Profiles we had already pinned to Kagi get repaired; a choice the user made in chrome://settings/search is left alone. An explicit `tron search` outranks both. - Write Preferences via a temp file and rename. It holds the whole profile, and a truncated in-place write loses every setting in it. - Say plainly in both Settings copies which box each picker governs. They are separate because an MV3 extension cannot set the browser's default engine — there is no API, and chrome_settings_overrides isn't available on Linux. The new-tab box defaults to DuckDuckGo too; leaving it on Kagi would reproduce the same paywall in the other search box. Tests: 9 launcher cases covering the default, the no-suggest rule, the repair, the don't-stomp guard, explicit override and apply-once; 7 for `tron search`, extracted from install.sh's heredoc since `sh -n install.sh` never parses it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan93 finding(s) HIGH/CRITICAL: 6 | MEDIUM: 87
…and 43 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
marked this pull request as ready for review
August 4, 2026 04:55
ralyodio
added a commit
that referenced
this pull request
Aug 4, 2026
) * fix(launcher): never replace a profile state file we could not read A profile that had worked for months came up unusable: a blank side panel, a New Tab that never finished loading. A fresh profile on the same machine, same build, same engine, with the same extension loaded, was fine — so the profile was the fault, not the code it ran. Three blocks here edit Chromium's JSON state. Every one of them did: try: d = json.load(open(p)) if os.path.exists(p) else {} except Exception: d = {} ... json.dump(d, open(p, "w")) Both halves are wrong, and they feed each other. The write is in-place and truncating, so an interrupted launch leaves a half-written file. The read then treats that file as absent and writes a stub holding only the key that block cared about. Default/Preferences IS the profile — search engine, startup, every extension's state — so the second launch after an interrupted one silently factory-resets it, and nothing says so. So: a state file that exists and does not parse is now left exactly as found, with a line on stderr naming it. Refusing to write is not refusing to start — the browser still launches, just without that block's setting applied. And all three writes now land as a rename (fsync, then os.replace) instead of in place, so there is no longer a truncated file for the next launch to misread. #75 fixed the write half for the search block alone and left its read, and left the other two blocks untouched; this finishes the job. Tests: 5 cases — a corrupt Preferences and a corrupt Local State survive a launch byte-for-byte, the refusal is announced, the browser still starts, and setting restore_on_startup keeps every unrelated key. Verified 3 of the 5 fail against the pre-fix launcher. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ci): drop a committed node_modules symlink, and ignore it properly `pnpm install --frozen-lockfile` failed on this branch with ENOENT trying to mkdir apps/desktop/node_modules — because the path was checked in as a symlink pointing at an absolute path that exists on no runner. It got committed because .gitignore said `node_modules/`, and a trailing slash matches directories only. A symlink named node_modules — which is what running the suite against a hoisted store leaves behind — is not a directory, so it was never ignored. Dropping the slash covers both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Reported as "stuck on kagi.com no matter which engine we choose", plus GUI lockup.
Two separate causes, both from the Kagi default (#69):
Preferences. Kagi is subscription-only past its trial, so a fresh install hits a login wall instead of results.And the likely lockup: the launcher also set
suggestions_urltokagi.com/api/autosuggest, which fires a request on every keystroke in the address bar. That both leaks the query before you press enter and stalls typing whenever the endpoint is slow or answers 401 — which it does when you aren't a subscriber.What changed
tron search <engine>writes$DATA/search-engine; the launcher applies it on the next start.tron searchwith no argument prints the current one and the list.suggestions_url, ever. Nothing fires per keystroke.chrome://settings/searchis left alone. An explicittron searchoutranks both. Without this guard, fixing our default would stomp everyone who'd already worked around it.Preferencesis written via temp file + rename. It holds the entire profile; a truncated in-place write loses every setting in it.tron searchfor the other one.Why they're still two settings
An MV3 extension can't set the browser's default search engine: there's no runtime API, and manifest
chrome_settings_overridesisn't available on Linux. The launcher is the only thing in this project that can write it. So rather than pretend they're unified, both pickers now state which box they control.Tests
apps/desktop/test/launcher.test.ts— 9 new cases: default engine, the no-suggest rule,tron searchhonored, unknown engine rejected, the Kagi repair, the don't-stomp guard, explicit override, apply-once, and Preferences surviving the rewrite. 17 pass.apps/web/test/install-search.test.ts— 7 new cases fortron search, extracted from install.sh's<<'TRON'heredoc, whichsh -n install.shnever parses. 15 pass with the existing clean tests.sh -nanddash -nclean on the launcher, install.sh, and the generated CLI.Not covered / pre-existing
extensions/ai-sidebar/moshpit-drift.test.jsfails on unmodified main, unrelated to this change: the local copies now reject a dashed TLD (blue.lazy-loaded) that the published@moshcoder/moshpit-resolvestill accepts. Worth a separate look.I could not reproduce on the reporter's desktop — this is diagnosed from the code paths, not from a captured repro.