From 569f5d7aeb7a57d6ab982584791228574418bfcb Mon Sep 17 00:00:00 2001
From: Anthony Ettinger
Date: Sat, 17 Oct 2026 08:54:31 +0000
Subject: [PATCH] feat(search): pit.world is the default search, address bar
and new tab
pit.world keeps no logs or cookies, works without an account, and resolves
Moshpit names, which pairs with the Pit toggle.
- Launcher: new `pit` engine and the new default for the address bar. A
profile still on our previous default (ddg, set by the launcher) moves to
pit.world on next launch; an engine the user picked, or an explicit
`tron search ddg`, is left alone (existing guard). No suggestions URL.
- `tron search`: pit is listed first and reported as the default.
- New tab (extension), options page and tronbrowser.dev settings: pit.world
option, default when nothing was chosen.
- Mobile: plain-text searches go to pit.world.
- Tests updated, plus two for the ddg -> pit migration and the explicit-ddg case.
Co-Authored-By: Claude Opus 5.5
---
README.md | 7 ++++---
apps/desktop/extensions/ai-sidebar/newtab.html | 3 ++-
apps/desktop/extensions/ai-sidebar/newtab.js | 12 +++++++-----
apps/desktop/extensions/ai-sidebar/options.html | 3 ++-
.../extensions/ai-sidebar/settings-sections.js | 2 +-
apps/desktop/launcher/tronbrowser | 12 ++++++++----
apps/desktop/test/launcher.test.ts | 16 ++++++++++++++--
apps/mobile/src/lib/navigation.test.ts | 8 ++++----
apps/mobile/src/lib/navigation.ts | 8 ++++----
apps/mobile/test/browser-event-order.test.tsx | 2 +-
apps/mobile/test/browser-recovery.test.tsx | 2 +-
apps/mobile/test/browser-search.test.tsx | 10 +++++-----
apps/web/public/install.sh | 6 +++---
apps/web/public/settings.html | 3 ++-
apps/web/test/install-search.test.ts | 2 +-
15 files changed, 59 insertions(+), 37 deletions(-)
diff --git a/README.md b/README.md
index 7734da5..d621a43 100644
--- a/README.md
+++ b/README.md
@@ -52,9 +52,10 @@ Linux phones (Librem 5 / PinePhone / Ubuntu Touch). See
## What you get today
-- **De-googled new tab** — TronBrowser page, **Kagi** as the default search
- (no Google in the omnibox; NeoSearch, Xprivo, and DuckDuckGo available as
- alternatives), quick links.
+- **De-googled new tab** — TronBrowser page, **[pit.world](https://pit.world)**
+ as the default search in both the address bar and the new tab (no logs, no
+ cookies, resolves Moshpit names; DuckDuckGo, Kagi, NeoSearch and Xprivo
+ available with `tron search `), quick links.
- **RSS reader** on the new tab — seeded from your OPML; add/remove feeds and
**import/export OPML** in Settings.
- **AI sidebar (bring your own keys)** — Anthropic, OpenAI, Google/Gemini,
diff --git a/apps/desktop/extensions/ai-sidebar/newtab.html b/apps/desktop/extensions/ai-sidebar/newtab.html
index dfb92e5..5db90b9 100644
--- a/apps/desktop/extensions/ai-sidebar/newtab.html
+++ b/apps/desktop/extensions/ai-sidebar/newtab.html
@@ -21,10 +21,11 @@
Web
AI
-
+
+ pit.world
Kagi
NeoSearch
Xprivo
diff --git a/apps/desktop/extensions/ai-sidebar/newtab.js b/apps/desktop/extensions/ai-sidebar/newtab.js
index 1ebc74b..b1ec393 100644
--- a/apps/desktop/extensions/ai-sidebar/newtab.js
+++ b/apps/desktop/extensions/ai-sidebar/newtab.js
@@ -12,12 +12,14 @@ const DEFAULT_TICKERS = 'SPY, AAPL, NVDA, BTC-USD';
const DEFAULT_LEAGUES = 'nfl, nba';
const splitList = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean);
-// --- Search: Web (DuckDuckGo by default) or AI (sidebar) ---
+// --- Search: Web (pit.world by default) or AI (sidebar) ---
// This is the NEW-TAB box only. The address bar's engine is a browser-level
// setting the extension cannot reach; `tron search ` sets that one.
-// Default is DuckDuckGo because it answers without an account — Kagi is
-// subscription-only past its trial, so it cannot be the out-of-box default.
+// Default is pit.world: it answers without an account, keeps no logs or cookies,
+// and resolves Moshpit names. Kagi is subscription-only past its trial, so it
+// cannot be the out-of-box default.
const SEARCH_ENGINES = {
+ pit: { name: 'pit.world', url: 'https://pit.world/search?q=' },
kagi: { name: 'Kagi', url: 'https://kagi.com/search?q=' },
neosearch: { name: 'NeoSearch', url: 'https://neosearch.org/?q=' },
xprivo: { name: 'Xprivo', url: 'https://www.xprivo.com/search/?q=' },
@@ -42,7 +44,7 @@ const TOR_SEARCH_ENGINES = {
excavator: { name: 'Excavator', url: 'http://2fd6cemt4gmccflhm6imvdfvli3nf7zn6rfrwpsy7uhxrgbypvwf5fad.onion/?q=' },
};
let searchMode = 'web';
-let searchEngine = 'ddg';
+let searchEngine = 'pit';
let torSearchEngine = 'ahmia';
let torEnabled = false;
@@ -50,7 +52,7 @@ let torEnabled = false;
// is on, otherwise the clearnet default.
function activeEngine() {
if (torEnabled) return TOR_SEARCH_ENGINES[torSearchEngine] || TOR_SEARCH_ENGINES.ahmia;
- return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.ddg;
+ return SEARCH_ENGINES[searchEngine] || SEARCH_ENGINES.pit;
}
function setMode(mode) {
diff --git a/apps/desktop/extensions/ai-sidebar/options.html b/apps/desktop/extensions/ai-sidebar/options.html
index 18de06b..8b936da 100644
--- a/apps/desktop/extensions/ai-sidebar/options.html
+++ b/apps/desktop/extensions/ai-sidebar/options.html
@@ -142,7 +142,8 @@ Search
chrome://settings/search.
New-tab search engine (clearnet)
- DuckDuckGo (default)
+ pit.world (default, private, Moshpit names)
+ DuckDuckGo
Kagi (needs a Kagi subscription)
NeoSearch (private)
Xprivo (private)
diff --git a/apps/desktop/extensions/ai-sidebar/settings-sections.js b/apps/desktop/extensions/ai-sidebar/settings-sections.js
index 9c2fbdf..0687774 100644
--- a/apps/desktop/extensions/ai-sidebar/settings-sections.js
+++ b/apps/desktop/extensions/ai-sidebar/settings-sections.js
@@ -42,7 +42,7 @@ export async function mountSettingsSections({ store, el, flash }) {
const cur = await store.get(['feeds', 'tickers', 'leagues', 'searchEngine', 'torSearchEngine']);
// Populate current values (on every mount, e.g. after a cloud pull).
- if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'ddg';
+ if (el('searchEngine')) el('searchEngine').value = cur.searchEngine || 'pit';
if (el('torSearchEngine')) el('torSearchEngine').value = cur.torSearchEngine || 'ahmia';
if (el('tickers')) el('tickers').value = cur.tickers ?? '';
if (el('leagues')) el('leagues').value = cur.leagues ?? '';
diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser
index 714a6e5..b68dad1 100755
--- a/apps/desktop/launcher/tronbrowser
+++ b/apps/desktop/launcher/tronbrowser
@@ -460,6 +460,7 @@ fi
# enter and stalls typing when the endpoint is slow or answers 401.
search_engine_spec() {
case "$1" in
+ pit) echo "pit.world|pit.world|https://pit.world/search?q={searchTerms}" ;;
ddg) echo "DuckDuckGo|duckduckgo.com|https://duckduckgo.com/?q={searchTerms}" ;;
kagi) echo "Kagi|kagi.com|https://kagi.com/search?q={searchTerms}" ;;
neosearch) echo "NeoSearch|neosearch.org|https://neosearch.org/?q={searchTerms}" ;;
@@ -474,14 +475,17 @@ search_engine_spec() {
# What the user asked for (`tron search `), or a default that works
# without an account. Kagi is subscription-only past its trial, so defaulting
-# the omnibox to it leaves a fresh install unable to search at all.
+# the omnibox to it leaves a fresh install unable to search at all. The default
+# is pit.world (no logs, no cookies, and it resolves Moshpit names, which pairs
+# with the Pit toggle). A profile still on our previous default (ddg) moves to
+# it on the next launch; an explicit `tron search ddg` stays ddg.
SEARCH_WANT="$(cat "$DATA/search-engine" 2>/dev/null || true)"
SEARCH_WANT="$(printf '%s' "$SEARCH_WANT" | tr -d '[:space:]')"
SEARCH_EXPLICIT=1
-if [ -z "$SEARCH_WANT" ]; then SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0; fi
+if [ -z "$SEARCH_WANT" ]; then SEARCH_WANT="pit"; SEARCH_EXPLICIT=0; fi
if ! search_engine_spec "$SEARCH_WANT" >/dev/null 2>&1; then
- echo "TronBrowser: unknown search engine '$SEARCH_WANT' — using ddg. See 'tron search'." >&2
- SEARCH_WANT="ddg"; SEARCH_EXPLICIT=0
+ echo "TronBrowser: unknown search engine '$SEARCH_WANT' — using pit. See 'tron search'." >&2
+ SEARCH_WANT="pit"; SEARCH_EXPLICIT=0
fi
# Which engine we last set. Profiles from before this file existed recorded it
diff --git a/apps/desktop/test/launcher.test.ts b/apps/desktop/test/launcher.test.ts
index ffbe4b2..058bb88 100644
--- a/apps/desktop/test/launcher.test.ts
+++ b/apps/desktop/test/launcher.test.ts
@@ -161,7 +161,7 @@ describe('omnibox search engine', () => {
// Kagi is subscription-only past its trial, so defaulting to it left a new
// install unable to search at all — it just landed on a login wall.
const { home } = run([]);
- expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
+ expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
});
it('never sets a suggestions_url', () => {
@@ -181,7 +181,7 @@ describe('omnibox search engine', () => {
const home = seed({ want: 'notanengine' });
const { stderr } = run([], { home });
expect(stderr).toContain("unknown search engine 'notanengine'");
- expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
+ expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
});
it('repairs a profile it had previously pinned to Kagi', () => {
@@ -189,6 +189,18 @@ describe('omnibox search engine', () => {
// their profile by us, and nothing moved them off it.
const home = seed({ url: KAGI_URL, legacyKagi: true });
run([], { home });
+ expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
+ });
+
+ it('moves a profile on our previous default (DuckDuckGo) to pit.world', () => {
+ const home = seed({ url: 'https://duckduckgo.com/?q={searchTerms}', marker: 'ddg' });
+ run([], { home });
+ expect(dse(home).url).toBe('https://pit.world/search?q={searchTerms}');
+ });
+
+ it('keeps DuckDuckGo for someone who chose it with `tron search ddg`', () => {
+ const home = seed({ url: 'https://duckduckgo.com/?q={searchTerms}', marker: 'ddg', want: 'ddg' });
+ run([], { home });
expect(dse(home).url).toBe('https://duckduckgo.com/?q={searchTerms}');
});
diff --git a/apps/mobile/src/lib/navigation.test.ts b/apps/mobile/src/lib/navigation.test.ts
index 282f3ba..9b266dc 100644
--- a/apps/mobile/src/lib/navigation.test.ts
+++ b/apps/mobile/src/lib/navigation.test.ts
@@ -23,25 +23,25 @@ describe('normalizeUrl', () => {
it('searches ordinary text', () => {
expect(normalizeUrl('privacy first browser')).toBe(
- 'https://duckduckgo.com/?q=privacy%20first%20browser',
+ 'https://pit.world/search?q=privacy%20first%20browser',
);
});
it('searches unsupported schemes instead of loading them', () => {
expect(normalizeUrl('javascript:alert(1)')).toBe(
- 'https://duckduckgo.com/?q=javascript%3Aalert(1)',
+ 'https://pit.world/search?q=javascript%3Aalert(1)',
);
});
it('does not treat domain-looking text with spaces as a URL', () => {
expect(normalizeUrl('example.com malicious suffix')).toBe(
- 'https://duckduckgo.com/?q=example.com%20malicious%20suffix',
+ 'https://pit.world/search?q=example.com%20malicious%20suffix',
);
});
it('searches with a no-account engine, not Kagi', () => {
// Kagi needs a subscription after its trial; a fresh install must be able
- // to search out of the box, matching the desktop DuckDuckGo default.
+ // to search out of the box, matching the desktop pit.world default.
expect(normalizeUrl('some query')).not.toContain('kagi.com');
});
});
diff --git a/apps/mobile/src/lib/navigation.ts b/apps/mobile/src/lib/navigation.ts
index c992245..2956e99 100644
--- a/apps/mobile/src/lib/navigation.ts
+++ b/apps/mobile/src/lib/navigation.ts
@@ -4,10 +4,10 @@ const DOMAIN_OR_IP =
/^(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}|(?:\d{1,3}\.){3}\d{1,3}|localhost)(?::\d{1,5})?(?:[/?#][^\s]*)?$/i;
function searchUrl(query: string): string {
- // DuckDuckGo answers without an account. Kagi is subscription-only past its
- // trial, so defaulting to it left a fresh install with a broken search box —
- // the same out-of-box failure the desktop launcher already corrects.
- return `https://duckduckgo.com/?q=${encodeURIComponent(query)}`;
+ // pit.world answers without an account and keeps no logs or cookies. Kagi is
+ // subscription-only past its trial, so defaulting to it left a fresh install
+ // with a broken search box; the desktop launcher uses pit.world too.
+ return `https://pit.world/search?q=${encodeURIComponent(query)}`;
}
/**
diff --git a/apps/mobile/test/browser-event-order.test.tsx b/apps/mobile/test/browser-event-order.test.tsx
index cf9a158..e70150b 100644
--- a/apps/mobile/test/browser-event-order.test.tsx
+++ b/apps/mobile/test/browser-event-order.test.tsx
@@ -249,7 +249,7 @@ describe.each(['android', 'ios'] as const)('browser event ordering on %s', platf
n.props.pointerEvents === 'none', 'failed native page');
expect(page.props.accessibilityElementsHidden).toBe(true);
await fire(input(root), 'onSubmitEditing');
- expect(theWebView().props.source?.uri).toBe('https://duckduckgo.com/?q=unfinished%20query');
+ expect(theWebView().props.source?.uri).toBe('https://pit.world/search?q=unfinished%20query');
expect(textContents(root)).not.toContain(ERROR);
expect(button(root, 'Back').props.disabled).toBe(true);
expect(webViewRegistry()).toHaveLength(2);
diff --git a/apps/mobile/test/browser-recovery.test.tsx b/apps/mobile/test/browser-recovery.test.tsx
index 0a1c455..c037f8e 100644
--- a/apps/mobile/test/browser-recovery.test.tsx
+++ b/apps/mobile/test/browser-recovery.test.tsx
@@ -38,7 +38,7 @@ describe.each(['android', 'ios'] as const)('browser recovery on %s', platform =>
await navigate(SECOND);
expect(input(root).props.value).toBe('my unfinished search');
await fire(input(root), 'onSubmitEditing');
- expect(theWebView().props.source?.uri).toBe('https://duckduckgo.com/?q=my%20unfinished%20search');
+ expect(theWebView().props.source?.uri).toBe('https://pit.world/search?q=my%20unfinished%20search');
});
it('restores the current page address when an edit is dismissed without submitting', async () => {
diff --git a/apps/mobile/test/browser-search.test.tsx b/apps/mobile/test/browser-search.test.tsx
index 9224301..ca41ce7 100644
--- a/apps/mobile/test/browser-search.test.tsx
+++ b/apps/mobile/test/browser-search.test.tsx
@@ -1,6 +1,6 @@
/**
* Search through the real BrowserScreen: address-bar input must reach the
- * actual WebView `source` as a no-account DuckDuckGo query — the same
+ * actual WebView `source` as a no-account pit.world query — the same
* correction the desktop launcher already ships — never as Kagi and never as a
* raw unsupported scheme.
*/
@@ -26,14 +26,14 @@ describe('BrowserScreen search', () => {
expect(theWebView().props.source).toEqual({ uri: HOME });
});
- it('sends plain text to DuckDuckGo, which needs no account', async () => {
+ it('sends plain text to pit.world, which needs no account', async () => {
const renderer = await renderScreen( );
await submitAddress(renderer.root, 'privacy first browser');
expect(theWebView().props.source).toEqual({
- uri: 'https://duckduckgo.com/?q=privacy%20first%20browser',
+ uri: 'https://pit.world/search?q=privacy%20first%20browser',
});
expect(addressInput(renderer.root).props.value).toBe(
- 'https://duckduckgo.com/?q=privacy%20first%20browser',
+ 'https://pit.world/search?q=privacy%20first%20browser',
);
});
@@ -47,7 +47,7 @@ describe('BrowserScreen search', () => {
const renderer = await renderScreen( );
await submitAddress(renderer.root, 'javascript:alert(1)');
expect(theWebView().props.source).toEqual({
- uri: 'https://duckduckgo.com/?q=javascript%3Aalert(1)',
+ uri: 'https://pit.world/search?q=javascript%3Aalert(1)',
});
});
});
diff --git a/apps/web/public/install.sh b/apps/web/public/install.sh
index 9574416..b68d16b 100755
--- a/apps/web/public/install.sh
+++ b/apps/web/public/install.sh
@@ -446,17 +446,17 @@ case "${1:-}" in
# only the launcher can write it there — an MV3 extension has no API for this
# on Linux — so this records the choice and the next launch applies it. The
# new-tab box has its own separate picker in TronBrowser Settings.
- _engines="ddg kagi neosearch xprivo oxiverse"
+ _engines="pit ddg kagi neosearch xprivo oxiverse"
_dirs="${TRONBROWSER_DATA:-$HOME/.tronbrowser}"
if [ -d "$HOME/TronBrowser" ]; then _dirs="$_dirs $HOME/TronBrowser"; fi
_want="${2:-}"
if [ -z "$_want" ]; then
for _d in $_dirs; do
_cur="$(cat "$_d/search-engine" 2>/dev/null || true)"
- echo "Address bar: ${_cur:-ddg (default)} [$_d]"
+ echo "Address bar: ${_cur:-pit (default, pit.world)} [$_d]"
done
echo "Available: $_engines"
- echo "Set with: tron search ddg"
+ echo "Set with: tron search pit (or ddg, kagi, ...)"
echo "(The new-tab search box is set separately in TronBrowser Settings.)"
exit 0
fi
diff --git a/apps/web/public/settings.html b/apps/web/public/settings.html
index 15cf1e2..3024efa 100644
--- a/apps/web/public/settings.html
+++ b/apps/web/public/settings.html
@@ -83,7 +83,8 @@ Search
chrome://settings/search.
New-tab search engine (clearnet)
- DuckDuckGo (default)
+ pit.world (default, private, Moshpit names)
+ DuckDuckGo
Kagi (needs a Kagi subscription)
NeoSearch (private)
Xprivo (private)
diff --git a/apps/web/test/install-search.test.ts b/apps/web/test/install-search.test.ts
index d5a80ea..b0de842 100644
--- a/apps/web/test/install-search.test.ts
+++ b/apps/web/test/install-search.test.ts
@@ -67,7 +67,7 @@ describe('tron search', () => {
const h = home();
const { status, out } = search(h);
expect(status).toBe(0);
- expect(out).toContain('ddg');
+ expect(out).toContain('pit (default, pit.world)');
});
it('records the chosen engine where the launcher looks for it', () => {