Skip to content

fix(search): stop pinning the omnibox to a paid engine, and make it choosable - #75

Merged
ralyodio merged 1 commit into
mainfrom
fix/search-engine-choice
Aug 4, 2026
Merged

fix(search): stop pinning the omnibox to a paid engine, and make it choosable#75
ralyodio merged 1 commit into
mainfrom
fix/search-engine-choice

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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):

  1. The address bar was hardcoded to Kagi by the launcher, written straight into the profile's Preferences. Kagi is subscription-only past its trial, so a fresh install hits a login wall instead of results.
  2. The Settings dropdown never governed the address bar. It only ever drove the new-tab search box. Changing it looked like it did nothing, because for the address bar it did nothing.

And the likely lockup: the launcher also set suggestions_url to kagi.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

  • The omnibox engine is now a real setting. tron search <engine> writes $DATA/search-engine; the launcher applies it on the next start. tron search with no argument prints the current one and the list.
  • Default is DuckDuckGo — it answers without an account. Kagi stays available for subscribers.
  • No suggestions_url, ever. Nothing fires per keystroke.
  • We only overwrite an engine that is absent or that we wrote ourselves. Profiles we had already pinned to Kagi get repaired; an engine the user chose in chrome://settings/search is left alone. An explicit tron search outranks both. Without this guard, fixing our default would stomp everyone who'd already worked around it.
  • Preferences is written via temp file + rename. It holds the entire profile; a truncated in-place write loses every setting in it.
  • Both Settings copies now say which box they govern, and point at tron search for the other one.
  • The new-tab box also defaults to DuckDuckGo — leaving it on Kagi would reproduce the same paywall in the other search box.

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_overrides isn'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 search honored, 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 for tron search, extracted from install.sh's <<'TRON' heredoc, which sh -n install.sh never parses. 15 pass with the existing clean tests.
  • Verified the tests bite: forcing the overwrite guard on fails exactly the "leaves an engine the user picked themselves alone" case.
  • sh -n and dash -n clean on the launcher, install.sh, and the generated CLI.
  • Full desktop suite: 144 pass.

Not covered / pre-existing

extensions/ai-sidebar/moshpit-drift.test.js fails on unmodified main, unrelated to this change: the local copies now reject a dashed TLD (blue.lazy-loaded) that the published @moshcoder/moshpit-resolve still 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.

…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>
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

93 finding(s)

HIGH/CRITICAL: 6 | MEDIUM: 87

Severity Rule Location
HIGH js-unescaped-html-sink apps/extensions/public/store.js:545
HIGH manifest-typosquat apps/mobile/package.json:35
HIGH js-unescaped-html-sink apps/web/public/dns.js:101
HIGH secret-generic-api-key packages/storage/src/config.ts:51
HIGH secret-generic-credential packages/storage/src/config.ts:51
HIGH secret-generic-credential services/api/src/store/payments.test.ts:11
MEDIUM js-open-redirect apps/desktop/extensions/ai-sidebar/install-helper.js:96
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:28
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:33
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:56
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:236
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:265
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:335
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/options.js:305
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/settings-sections.js:24
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:76
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:164
MEDIUM insecure-temp-file apps/desktop/src/launcher.test.ts:21
MEDIUM insecure-temp-file apps/desktop/src/tor.test.ts:40
MEDIUM insecure-temp-file apps/desktop/src/tor.test.ts:44
MEDIUM insecure-temp-file apps/desktop/test/launcher.test.ts:92
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:77
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:92
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:93
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:104
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:107
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:110
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:155
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:163
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:175
MEDIUM sql-template-interpolation apps/extensions/public/store.js:176
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:222
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:225
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:227
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:230
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:282
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:289
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:292
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:302
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:307
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:319
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:419
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:425
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:429
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:449
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:468
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:477
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:481
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:497
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:506

…and 43 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review August 4, 2026 04:55
@ralyodio
ralyodio merged commit 8341e18 into main Aug 4, 2026
8 checks passed
@ralyodio
ralyodio deleted the fix/search-engine-choice branch August 4, 2026 04:56
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>
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