Skip to content

Commit 8341e18

Browse files
ralyodioclaude
andauthored
fix(search): stop pinning the omnibox to a paid engine, and make it choosable (#75)
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>
1 parent 809e8a3 commit 8341e18

9 files changed

Lines changed: 366 additions & 29 deletions

File tree

apps/desktop/extensions/ai-sidebar/newtab.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ const DEFAULT_TICKERS = 'SPY, AAPL, NVDA, BTC-USD';
1111
const DEFAULT_LEAGUES = 'nfl, nba';
1212
const splitList = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean);
1313

14-
// --- Search: Web (Kagi by default) or AI (sidebar) ---
14+
// --- Search: Web (DuckDuckGo by default) or AI (sidebar) ---
15+
// This is the NEW-TAB box only. The address bar's engine is a browser-level
16+
// setting the extension cannot reach; `tron search <engine>` sets that one.
17+
// Default is DuckDuckGo because it answers without an account — Kagi is
18+
// subscription-only past its trial, so it cannot be the out-of-box default.
1519
const SEARCH_ENGINES = {
1620
kagi: { name: 'Kagi', url: 'https://kagi.com/search?q=' },
1721
neosearch: { name: 'NeoSearch', url: 'https://neosearch.org/?q=' },
@@ -37,15 +41,15 @@ const TOR_SEARCH_ENGINES = {
3741
excavator: { name: 'Excavator', url: 'http://2fd6cemt4gmccflhm6imvdfvli3nf7zn6rfrwpsy7uhxrgbypvwf5fad.onion/?q=' },
3842
};
3943
let searchMode = 'web';
40-
let searchEngine = 'kagi';
44+
let searchEngine = 'ddg';
4145
let torSearchEngine = 'ahmia';
4246
let torEnabled = false;
4347

4448
// Resolve the engine to use right now: the Tor default while the onion toggle
4549
// is on, otherwise the clearnet default.
4650
function activeEngine() {
4751
if (torEnabled) return TOR_SEARCH_ENGINES[torSearchEngine] || TOR_SEARCH_ENGINES.ahmia;
48-
return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.kagi;
52+
return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.ddg;
4953
}
5054

5155
function setMode(mode) {

apps/desktop/extensions/ai-sidebar/options.html

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,16 @@ <h2>bittorrented.com — Live TV, Radio &amp; Podcasts</h2>
130130
<span id="btrMsg" class="saved"></span>
131131

132132
<h2>Search</h2>
133-
<p class="hint">Default search engine for the new-tab search box. Kagi is the
134-
default; NeoSearch, Xprivo, DuckDuckGo, and others are available as alternatives. (The omnibox default
135-
is set separately in <code>chrome://settings/search</code>.)</p>
133+
<p class="hint">This sets the <strong>new-tab search box</strong> only.
134+
The address bar is a browser-level setting this page cannot reach — change that
135+
one with <code>tron search &lt;engine&gt;</code>, or in
136+
<code>chrome://settings/search</code>.</p>
136137
<label for="searchEngine">New-tab search engine (clearnet)</label>
137138
<select id="searchEngine">
138-
<option value="kagi">Kagi (default)</option>
139+
<option value="ddg">DuckDuckGo (default)</option>
140+
<option value="kagi">Kagi (needs a Kagi subscription)</option>
139141
<option value="neosearch">NeoSearch (private)</option>
140142
<option value="xprivo">Xprivo (private)</option>
141-
<option value="ddg">DuckDuckGo</option>
142143
<option value="altpower">Altpower</option>
143144
<option value="oxiverse">Oxiverse</option>
144145
</select>

apps/desktop/extensions/ai-sidebar/settings-sections.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function mountSettingsSections({ store, el, flash }) {
4242
const cur = await store.get(['feeds', 'tickers', 'leagues', 'searchEngine', 'torSearchEngine']);
4343

4444
// Populate current values (on every mount, e.g. after a cloud pull).
45-
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'kagi';
45+
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'ddg';
4646
if (el('torSearchEngine')) el('torSearchEngine').value = cur.torSearchEngine || 'ahmia';
4747
if (el('tickers')) el('tickers').value = cur.tickers ?? '';
4848
if (el('leagues')) el('leagues').value = cur.leagues ?? '';

apps/desktop/launcher/tronbrowser

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -186,29 +186,96 @@ if flag not in exp:
186186
PY
187187
fi
188188

189-
# Default the omnibox to Kagi once per profile, then respect the user's
190-
# chrome://settings/search choice. The sentinel filename carries the engine so
191-
# that changing the default here re-applies once on profiles created earlier.
192-
if command -v python3 >/dev/null 2>&1 && [ ! -f "$DATA/.tron-search-kagi" ]; then
193-
TB_PREFS="$DATA/Default/Preferences" python3 - <<'PY' 2>/dev/null || true
194-
import json, os
189+
# --- Omnibox search engine -------------------------------------------------
190+
# The address bar's engine is a browser-level setting. An MV3 extension cannot
191+
# change it at runtime and manifest `chrome_settings_overrides` isn't available
192+
# on Linux, so the launcher is the only thing here that can set it — which is
193+
# why `tron search <engine>` writes a file we read rather than the extension
194+
# doing it. The new-tab box keeps its own picker in Settings; the two are
195+
# separate on purpose and both say so in their UI.
196+
#
197+
# No suggestions_url anywhere below. A suggest endpoint fires a request on every
198+
# keystroke in the address bar, which both leaks the query before you press
199+
# enter and stalls typing when the endpoint is slow or answers 401.
200+
search_engine_spec() {
201+
case "$1" in
202+
ddg) echo "DuckDuckGo|duckduckgo.com|https://duckduckgo.com/?q={searchTerms}" ;;
203+
kagi) echo "Kagi|kagi.com|https://kagi.com/search?q={searchTerms}" ;;
204+
neosearch) echo "NeoSearch|neosearch.org|https://neosearch.org/?q={searchTerms}" ;;
205+
xprivo) echo "Xprivo|xprivo.com|https://www.xprivo.com/search/?q={searchTerms}" ;;
206+
oxiverse) echo "Oxiverse|search.oxiverse.com|https://search.oxiverse.com/?q={searchTerms}&tab=web" ;;
207+
# Altpower is deliberately absent: its query lives in the URL fragment, which
208+
# never reaches a server as a search. It works from the new-tab box, which
209+
# builds the URL itself, but not as an omnibox engine.
210+
*) return 1 ;;
211+
esac
212+
}
213+
214+
# What the user asked for (`tron search <engine>`), or a default that works
215+
# without an account. Kagi is subscription-only past its trial, so defaulting
216+
# the omnibox to it leaves a fresh install unable to search at all.
217+
SEARCH_WANT="$(cat "$DATA/search-engine" 2>/dev/null || true)"
218+
SEARCH_WANT="$(printf '%s' "$SEARCH_WANT" | tr -d '[:space:]')"
219+
SEARCH_EXPLICIT=1
220+
if [ -z "$SEARCH_WANT" ]; then SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0; fi
221+
if ! search_engine_spec "$SEARCH_WANT" >/dev/null 2>&1; then
222+
echo "TronBrowser: unknown search engine '$SEARCH_WANT' — using ddg. See 'tron search'." >&2
223+
SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0
224+
fi
225+
226+
# Which engine we last set. Profiles from before this file existed recorded it
227+
# in the sentinel's NAME; treat that as "we set Kagi" so the repair below can
228+
# recognise its own handiwork.
229+
SEARCH_MARK="$DATA/.tron-search"
230+
SEARCH_PREV="$(cat "$SEARCH_MARK" 2>/dev/null || true)"
231+
SEARCH_PREV="$(printf '%s' "$SEARCH_PREV" | tr -d '[:space:]')"
232+
if [ -z "$SEARCH_PREV" ] && [ -f "$DATA/.tron-search-kagi" ]; then SEARCH_PREV="kagi"; fi
233+
234+
if command -v python3 >/dev/null 2>&1 && [ "$SEARCH_PREV" != "$SEARCH_WANT" ]; then
235+
SEARCH_PREV_URL=""
236+
if [ -n "$SEARCH_PREV" ]; then
237+
SEARCH_PREV_URL="$(search_engine_spec "$SEARCH_PREV" 2>/dev/null | cut -d'|' -f3 || true)"
238+
fi
239+
TB_PREFS="$DATA/Default/Preferences" \
240+
TB_ENGINE="$(search_engine_spec "$SEARCH_WANT")" \
241+
TB_PREV_URL="$SEARCH_PREV_URL" \
242+
TB_FORCE="$SEARCH_EXPLICIT" \
243+
python3 - <<'PY' 2>/dev/null || true
244+
import json, os, tempfile
195245
p = os.environ["TB_PREFS"]
246+
name, keyword, url = os.environ["TB_ENGINE"].split("|")
247+
prev_url = os.environ.get("TB_PREV_URL", "")
248+
force = os.environ.get("TB_FORCE") == "1"
196249
os.makedirs(os.path.dirname(p), exist_ok=True)
197250
try:
198251
d = json.load(open(p)) if os.path.exists(p) else {}
199252
except Exception:
200253
d = {}
201-
d.setdefault("default_search_provider_data", {})["template_url_data"] = {
202-
"short_name": "Kagi",
203-
"keyword": "kagi.com",
204-
"url": "https://kagi.com/search?q={searchTerms}",
205-
"suggestions_url": "https://kagi.com/api/autosuggest?q={searchTerms}",
206-
"favicon_url": "https://kagi.com/favicon.ico",
207-
"safe_for_autoreplace": False,
208-
}
209-
json.dump(d, open(p, "w"))
254+
255+
# Only ever overwrite an engine that is absent or that we put there ourselves.
256+
# Without this, changing the default below would stomp the choice of everyone
257+
# who had already picked their own in chrome://settings/search. An explicit
258+
# `tron search` is the one case that outranks whatever is currently set.
259+
cur = d.get("default_search_provider_data", {}).get("template_url_data") or {}
260+
cur_url = cur.get("url", "")
261+
if force or not cur_url or (prev_url and cur_url == prev_url):
262+
d.setdefault("default_search_provider_data", {})["template_url_data"] = {
263+
"short_name": name,
264+
"keyword": keyword,
265+
"url": url,
266+
"favicon_url": "https://" + keyword.split("/")[0] + "/favicon.ico",
267+
"safe_for_autoreplace": False,
268+
}
269+
# Preferences is the profile. A truncated write loses every setting in it,
270+
# so land it as a rename rather than in place.
271+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p))
272+
with os.fdopen(fd, "w") as f:
273+
json.dump(d, f)
274+
os.replace(tmp, p)
210275
PY
211-
mkdir -p "$DATA"; : > "$DATA/.tron-search-kagi"
276+
mkdir -p "$DATA"
277+
printf '%s\n' "$SEARCH_WANT" > "$SEARCH_MARK" 2>/dev/null || true
278+
rm -f "$DATA/.tron-search-kagi" 2>/dev/null || true
212279
fi
213280

214281
# Show the feed (ai-sidebar New Tab page) on startup. We can't pass a startup URL —

apps/desktop/test/launcher.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,116 @@ describe('launcher flags', () => {
116116
});
117117
});
118118

119+
// --- Omnibox search engine ---------------------------------------------------
120+
// The launcher is the only thing that can set Chromium's default search engine
121+
// (no MV3 API for it, and chrome_settings_overrides isn't available on Linux),
122+
// so the rules about when it may overwrite one live here.
123+
124+
const KAGI_URL = 'https://kagi.com/search?q={searchTerms}';
125+
126+
const prefsPath = (home: string) => join(home, 'profile', 'Default', 'Preferences');
127+
128+
/** The default_search_provider_data the launcher left in the profile. */
129+
function dse(home: string): Record<string, string> {
130+
const p = prefsPath(home);
131+
if (!existsSync(p)) return {};
132+
const json = JSON.parse(readFileSync(p, 'utf8'));
133+
return json.default_search_provider_data?.template_url_data ?? {};
134+
}
135+
136+
/** A profile that already has a search engine set, and a record of who set it. */
137+
function seed(opts: { url?: string; marker?: string; legacyKagi?: boolean; want?: string }): string {
138+
const home = mkdtempSync(join(tmpdir(), 'tron-launcher-'));
139+
homes.push(home);
140+
const data = join(home, 'profile');
141+
mkdirSync(join(data, 'Default'), { recursive: true });
142+
if (opts.url) {
143+
writeFileSync(
144+
prefsPath(home),
145+
JSON.stringify({
146+
default_search_provider_data: { template_url_data: { short_name: 'Seeded', url: opts.url } },
147+
// A real profile has more than the one key; it must survive our write.
148+
bookmark_bar: { show_on_all_tabs: true },
149+
}),
150+
);
151+
}
152+
if (opts.legacyKagi) writeFileSync(join(data, '.tron-search-kagi'), '');
153+
if (opts.marker) writeFileSync(join(data, '.tron-search'), `${opts.marker}\n`);
154+
if (opts.want) writeFileSync(join(data, 'search-engine'), `${opts.want}\n`);
155+
return home;
156+
}
157+
158+
describe('omnibox search engine', () => {
159+
it('defaults a fresh profile to an engine that works without an account', () => {
160+
// Kagi is subscription-only past its trial, so defaulting to it left a new
161+
// install unable to search at all — it just landed on a login wall.
162+
const { home } = run([]);
163+
expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
164+
});
165+
166+
it('never sets a suggestions_url', () => {
167+
// A suggest endpoint fires per keystroke in the address bar: it leaks the
168+
// query before you hit enter, and stalls typing when it is slow or 401s.
169+
const { home } = run([]);
170+
expect(dse(home)).not.toHaveProperty('suggestions_url');
171+
});
172+
173+
it('honors the engine chosen with `tron search`', () => {
174+
const home = seed({ want: 'neosearch' });
175+
run([], { home });
176+
expect(dse(home).url).toBe('https://neosearch.org/?q={searchTerms}');
177+
});
178+
179+
it('falls back and says so when the chosen engine is unknown', () => {
180+
const home = seed({ want: 'notanengine' });
181+
const { stderr } = run([], { home });
182+
expect(stderr).toContain("unknown search engine 'notanengine'");
183+
expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
184+
});
185+
186+
it('repairs a profile it had previously pinned to Kagi', () => {
187+
// The actual bug: everyone who installed before this had Kagi written into
188+
// their profile by us, and nothing moved them off it.
189+
const home = seed({ url: KAGI_URL, legacyKagi: true });
190+
run([], { home });
191+
expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
192+
});
193+
194+
it('leaves an engine the user picked themselves alone', () => {
195+
// We may correct our own default. We may not overwrite a deliberate choice.
196+
const chosen = 'https://www.google.com/search?q={searchTerms}';
197+
const home = seed({ url: chosen, legacyKagi: true });
198+
run([], { home });
199+
expect(dse(home).url).toBe(chosen);
200+
});
201+
202+
it('lets an explicit `tron search` override even a user-set engine', () => {
203+
const home = seed({ url: 'https://www.google.com/search?q={searchTerms}', want: 'kagi' });
204+
run([], { home });
205+
expect(dse(home).url).toBe(KAGI_URL);
206+
});
207+
208+
it('stops touching the setting once it has applied it', () => {
209+
// Second launch must not re-apply, or changing the engine in
210+
// chrome://settings/search would be undone on every start.
211+
const first = run([]);
212+
const chosen = 'https://www.startpage.com/sp/search?q={searchTerms}';
213+
writeFileSync(
214+
prefsPath(first.home),
215+
JSON.stringify({ default_search_provider_data: { template_url_data: { url: chosen } } }),
216+
);
217+
run([], { home: first.home });
218+
expect(dse(first.home).url).toBe(chosen);
219+
});
220+
221+
it('keeps the rest of Preferences when it rewrites the engine', () => {
222+
const home = seed({ url: KAGI_URL, legacyKagi: true });
223+
run([], { home });
224+
const json = JSON.parse(readFileSync(prefsPath(home), 'utf8'));
225+
expect(json.bookmark_bar?.show_on_all_tabs).toBe(true);
226+
});
227+
});
228+
119229
describe('engine reporting', () => {
120230
it('names the engine it is about to run', () => {
121231
const { stderr } = run([], { version: 'Chromium 141.0.0.0' });

apps/web/public/install.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Usage:
9797
tron replay <bundle> Replay a recorded trace against the session
9898
tron upgrade Update to the latest release
9999
tron clean Clear browser caches (keeps bookmarks, logins, history)
100+
tron search [engine] Show or set the ADDRESS BAR's search engine
101+
(the new-tab box is set in TronBrowser Settings)
100102
tron remove Uninstall TronBrowser (keeps your profile data)
101103
tron version Print the installed version
102104
tron help Show this help
@@ -268,6 +270,38 @@ case "${1:-}" in
268270
rm -rf "$_data/Default/Cache" "$_data/Default/Code Cache" "$_data/Default/Service Worker"
269271
echo "Freed ~${_freed}MB from $_data. Bookmarks, passwords and logins untouched."
270272
done ;;
273+
search)
274+
# The ADDRESS BAR's engine. Chromium reads it from the profile at startup and
275+
# only the launcher can write it there — an MV3 extension has no API for this
276+
# on Linux — so this records the choice and the next launch applies it. The
277+
# new-tab box has its own separate picker in TronBrowser Settings.
278+
_engines="ddg kagi neosearch xprivo oxiverse"
279+
_dirs="${TRONBROWSER_DATA:-$HOME/.tronbrowser}"
280+
if [ -d "$HOME/TronBrowser" ]; then _dirs="$_dirs $HOME/TronBrowser"; fi
281+
_want="${2:-}"
282+
if [ -z "$_want" ]; then
283+
for _d in $_dirs; do
284+
_cur="$(cat "$_d/search-engine" 2>/dev/null || true)"
285+
echo "Address bar: ${_cur:-ddg (default)} [$_d]"
286+
done
287+
echo "Available: $_engines"
288+
echo "Set with: tron search ddg"
289+
echo "(The new-tab search box is set separately in TronBrowser Settings.)"
290+
exit 0
291+
fi
292+
_ok=0
293+
for _e in $_engines; do
294+
if [ "$_e" = "$_want" ]; then _ok=1; fi
295+
done
296+
if [ "$_ok" != "1" ]; then
297+
echo "tron search: unknown engine '$_want'. Available: $_engines" >&2
298+
exit 1
299+
fi
300+
for _d in $_dirs; do
301+
mkdir -p "$_d"
302+
printf '%s\n' "$_want" > "$_d/search-engine"
303+
done
304+
echo "Address bar set to '$_want'. Restart TronBrowser to apply ('tron restart')." ;;
271305
remove|uninstall)
272306
rm -rf "$APP_DIR"
273307
rm -f "$PREFIX/bin/tron" "$PREFIX/bin/tronbrowser" "$PREFIX/share/applications/tronbrowser.desktop"

apps/web/public/settings-sections.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function mountSettingsSections({ store, el, flash }) {
4242
const cur = await store.get(['feeds', 'tickers', 'leagues', 'searchEngine', 'torSearchEngine']);
4343

4444
// Populate current values (on every mount, e.g. after a cloud pull).
45-
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'kagi';
45+
if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'ddg';
4646
if (el('torSearchEngine')) el('torSearchEngine').value = cur.torSearchEngine || 'ahmia';
4747
if (el('tickers')) el('tickers').value = cur.tickers ?? '';
4848
if (el('leagues')) el('leagues').value = cur.leagues ?? '';

apps/web/public/settings.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,16 @@ <h2>API keys (bring your own)</h2>
7777

7878
<!-- Shared sections (identical to the extension options page, via settings-sections.js) -->
7979
<h2>Search</h2>
80-
<p class="hint">Default search engine for the new-tab search box.</p>
80+
<p class="hint">This sets the <strong>new-tab search box</strong> only.
81+
The address bar is a browser-level setting this page cannot reach — change that
82+
one with <code>tron search &lt;engine&gt;</code>, or in
83+
<code>chrome://settings/search</code>.</p>
8184
<label for="searchEngine">New-tab search engine (clearnet)</label>
8285
<select id="searchEngine">
83-
<option value="kagi">Kagi (default)</option>
86+
<option value="ddg">DuckDuckGo (default)</option>
87+
<option value="kagi">Kagi (needs a Kagi subscription)</option>
8488
<option value="neosearch">NeoSearch (private)</option>
8589
<option value="xprivo">Xprivo (private)</option>
86-
<option value="ddg">DuckDuckGo</option>
8790
<option value="altpower">Altpower</option>
8891
<option value="oxiverse">Oxiverse</option>
8992
</select>

0 commit comments

Comments
 (0)