| … | … | @@ -48,6 +48,7 @@ The key above is **content-addressed and stable**: when we ship a new release, t |
| 48 | 48 | - Per-tab back/forward history; tabs persist across launches and survive panel switches |
| 49 | 49 | - URL bar autocomplete from your bookmarks + history Hyperbees (↑↓ to navigate, Enter to pick) |
| 50 | 50 | - "About this site" panel (ⓘ button): drive key in hex + z-base-32 with copy buttons, scheme + path, one-click bookmark toggle |
| 51 | - **Private-search home:** the active first tab on launch opens a browser-owned search page powered by DuckDuckGo. PearBrowser sends no search analytics and excludes submitted queries from the optional persistent visit log; the on-page disclosure makes clear that DuckDuckGo still receives the query and network address, and that private search is not anonymity. | |
| 51 | 52 | - **Ask Browser:** click **✦ Ask** to question a local QVAC/Qwen model about the active page, with private on-device streaming, cancellation, short follow-ups, source provenance, and no page access to the browser-owned AI channel |
| 52 | 53 | - **Local AI widget:** every blank new tab (`⌘T`) hosts a QVAC quick-ask card — zero page context, streamed on-device answers, model picker, and load-on-first-use progress ([docs](./docs/ASK_BROWSER.md)) |
| 53 | 54 | - **Content Shield:** Brave-style ad/tracker blocking enforced inside the browser-owned proxy — blocked requests never reach a peer, relay, or the network; cosmetic element hiding + scriptlets ride the CSP-safe HTML injection path; per-drive allowlist/strict modes, toggle + live counters in Settings ([plan](./docs/BROWSER_PARITY_PLAN.md)) |
| … | … | @@ -63,7 +64,7 @@ The key above is **content-addressed and stable**: when we ship a new release, t |
| 63 | 64 | - **My Catalog:** create a writable personal catalog, add apps from loaded catalogs or installed apps, rename it, edit saved metadata (name, description, version, author, categories), and share the catalog key; copies opened without the writer key stay read-only |
| 64 | 65 | - Safe catalogue normalization accepts `apps[]`, `items[]`, or `entries[]`, preserves safe link-only rows (`hyper://`, `pear://`, `file://`), rejects malformed targets, and strips prototype-pollution keys before rendering |
| 65 | 66 | - Default catalog auto-loads on first Apps-tab visit (the "PearBrowser Network" Hyperbee `hyperbee://f5fb7500bccd…` — PearBrowser, peerit, HiveRelay, P2P Builders, Pear Dealroom, Paste, PearPoker, Keet, PearPass, Peercord, anonGPT, Pear POS, Pear Tickets, HiveWorm), generated from a single source manifest and mirrored into the offline seed |
| 66 | - **peerit — "the front page of the P2P internet"** (a peer-to-peer Reddit, `hyper://ec6e2d6d…/`): a browsable `hypersite` whose communities, posts, threaded comments and votes live in a shared Autobase+Hyperbee log. It opens as the **active front tab on every fresh launch** (alongside the landing page) and is **pinned to the top** of the Sites discovery grid. Source: `02-apps/peerit`. | |
| 67 | - **peerit — "the front page of the P2P internet"** (a peer-to-peer Reddit, `hyper://ec6e2d6d…/`): a browsable `hypersite` whose communities, posts, threaded comments and votes live in a shared Autobase+Hyperbee log. It opens as a startup tab beside the private-search home and landing page, and is **pinned to the top** of the Sites discovery grid. Source: `02-apps/peerit`. | |
| 67 | 68 | - Featured apps: **Keet** · **PearPass** · **anonGPT** · **Paste** · **[Peercord](https://git.churchofmalware.org/mastercodeon/Peercord)**. Peercord's current Pear release is a full desktop app (`pear.json` `type:"desktop"`), so PearBrowser launches it via the standalone `pear://` window path; it should only move to headless "Run in tab" once Peercord publishes a pear-request worker entry. |
| 68 | 69 | |
| 69 | 70 | ### Publish (P2P Sites) |
| … | … | @@ -79,9 +80,10 @@ The key above is **content-addressed and stable**: when we ship a new release, t |
| 79 | 80 | - Cross-launch persistence; powers the URL-bar autocomplete |
| 80 | 81 | |
| 81 | 82 | ### Search |
| 83 | - Private web search is available from the browser-owned home page through DuckDuckGo over HTTPS with Content Shield enabled; PearBrowser does not send search analytics or add these queries to its optional persistent visit log. | |
| 82 | 84 | - Local-first personal index returns first-paint results immediately from local browser/catalogue data |
| 83 | 85 | - Optional trusted-peer federation enriches results in the background through `QueryPlanner`, digest checks, provenance, and stale-query suppression |
| 84 | - Query length and result limits are bounded; no query leaves the device unless the user asks for federated search | |
| 86 | - Local-index query length and result limits are bounded; local queries do not leave the device unless the user asks for federated search. Web-search submissions do leave the device for DuckDuckGo, as disclosed on the home page. | |
| 85 | 87 | |
| 86 | 88 | ### Naming |
| 87 | 89 | - `pearname://` and typed names resolve through local petnames, owned registry records, trusted-contact federation, and curated defaults |
| … | … | @@ -357,9 +357,75 @@ p { color: var(--pear-color-muted); margin-bottom: 12px; } |
| 357 | 357 | max-width: 520px; |
| 358 | 358 | text-align: center; |
| 359 | 359 | } |
| 360 | .browse-welcome-inner.start-page { width: min(720px, 100%); max-width: 720px; } | |
| 360 | 361 | .browse-welcome-logo { font-size: 44px; margin-bottom: 14px; } |
| 361 | 362 | .browse-welcome-inner h2 { font-size: 22px; font-weight: 700; letter-spacing: -0.4px; margin-bottom: 12px; color: var(--pear-color-text); } |
| 362 | 363 | .browse-welcome-inner p { color: var(--pear-color-muted); font-size: 14px; line-height: 1.7; margin-bottom: 20px; } |
| 364 | .browse-welcome-inner .start-page-lede { margin: 0 auto 16px; max-width: 590px; } | |
| 365 | .private-search-card { | |
| 366 | padding: 16px; | |
| 367 | margin-bottom: 14px; | |
| 368 | text-align: left; | |
| 369 | background: var(--pear-color-surface); | |
| 370 | border: 1px solid var(--pear-color-line); | |
| 371 | border-radius: var(--pear-radius-card, 12px); | |
| 372 | box-shadow: var(--pear-shadow-card, 0 1px 2px rgba(0, 0, 0, 0.06)); | |
| 373 | } | |
| 374 | .private-search-heading { | |
| 375 | display: flex; | |
| 376 | align-items: center; | |
| 377 | justify-content: space-between; | |
| 378 | gap: 12px; | |
| 379 | margin-bottom: 10px; | |
| 380 | color: var(--pear-color-text); | |
| 381 | font-size: 14px; | |
| 382 | font-weight: 700; | |
| 383 | } | |
| 384 | .private-search-provider { | |
| 385 | padding: 3px 9px; | |
| 386 | color: var(--pear-color-muted); | |
| 387 | border: 1px solid var(--pear-color-line); | |
| 388 | border-radius: var(--pear-radius-pill, 999px); | |
| 389 | font-size: 10px; | |
| 390 | font-weight: 600; | |
| 391 | letter-spacing: 0.03em; | |
| 392 | text-transform: uppercase; | |
| 393 | } | |
| 394 | .private-search-form { display: flex; gap: 8px; } | |
| 395 | .private-search-form input { | |
| 396 | min-width: 0; | |
| 397 | flex: 1; | |
| 398 | padding: 12px 14px; | |
| 399 | color: var(--pear-color-text); | |
| 400 | background: var(--pear-color-canvas); | |
| 401 | border: 1px solid var(--pear-color-line); | |
| 402 | border-radius: var(--pear-radius-control, 8px); | |
| 403 | font: inherit; | |
| 404 | font-size: 15px; | |
| 405 | } | |
| 406 | .private-search-form input:focus { | |
| 407 | outline: none; | |
| 408 | border-color: var(--pear-color-accent); | |
| 409 | box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.12); | |
| 410 | } | |
| 411 | .private-search-submit { | |
| 412 | padding: 0 20px; | |
| 413 | color: white; | |
| 414 | background: var(--pear-color-accent); | |
| 415 | border: 0; | |
| 416 | border-radius: var(--pear-radius-control, 8px); | |
| 417 | font: inherit; | |
| 418 | font-weight: 700; | |
| 419 | cursor: pointer; | |
| 420 | } | |
| 421 | .private-search-submit:disabled { opacity: 0.45; cursor: default; } | |
| 422 | .private-search-disclosure { | |
| 423 | margin-top: 10px; | |
| 424 | color: var(--pear-color-quiet); | |
| 425 | font-size: 11px; | |
| 426 | line-height: 1.5; | |
| 427 | } | |
| 428 | .start-page-p2p { margin-bottom: 14px; color: var(--pear-color-muted); font-size: 12px; line-height: 1.6; } | |
| 363 | 429 | .browse-welcome-actions { display: flex; gap: 8px; justify-content: center; margin-bottom: 18px; flex-wrap: wrap; } |
| 364 | 430 | .browse-welcome-tip { color: var(--pear-color-quiet); font-size: 12px; padding-top: 16px; border-top: 1px solid var(--pear-color-surface-soft); } |
| 365 | 431 | .browse-welcome-tip strong { color: var(--pear-color-text); } |
| … | … | @@ -371,6 +437,11 @@ p { color: var(--pear-color-muted); margin-bottom: 12px; } |
| 371 | 437 | font-size: 12px; |
| 372 | 438 | } |
| 373 | 439 | |
| 440 | @media (max-width: 620px) { | |
| 441 | .private-search-form { flex-direction: column; } | |
| 442 | .private-search-submit { padding: 11px 16px; } | |
| 443 | } | |
| 444 | ||
| 374 | 445 | .webview { flex: 1; border: none; background: var(--pear-color-webview); width: 100%; } |
| 375 | 446 | |
| 376 | 447 | /* ─── Apps tab ─── */ |
| … | … | @@ -1376,7 +1447,8 @@ p { color: var(--pear-color-muted); margin-bottom: 12px; } |
| 1376 | 1447 | visibility: hidden; |
| 1377 | 1448 | pointer-events: none; |
| 1378 | 1449 | } |
| 1379 | .browse-stage .browse-welcome { flex: 1; display: flex; align-items: center; justify-content: center; } | |
| 1450 | .browse-stage .browse-welcome { flex: 1; display: flex; align-items: center; justify-content: center; overflow-y: auto; } | |
| 1451 | .browse-stage .browse-welcome-inner.start-page { margin: auto; } | |
| 1380 | 1452 | |
| 1381 | 1453 | /* --- Ask Browser --------------------------------------------------- */ |
| 1382 | 1454 | .urlbar .ask-browser-toggle { |
| … | … | @@ -33,13 +33,15 @@ test('peerit catalogue source declares the site + drive key', () => { |
| 33 | 33 | assert.equal(app.url, PEERIT_URL) |
| 34 | 34 | }) |
| 35 | 35 | |
| 36 | test('shell.js opens PearBrowser landing first with peerit beside it', () => { | |
| 36 | test('shell.js opens private-search home first with PearBrowser and peerit beside it', () => { | |
| 37 | 37 | const shell = readFileSync(new URL('../ui/shell.js', import.meta.url), 'utf8') |
| 38 | 38 | // The drive key constant exists and is used to build the front-tab URL. |
| 39 | 39 | assert.match(shell, new RegExp(`PEERIT_DRIVE_KEY = '${PEERIT_KEY}'`)) |
| 40 | 40 | assert.match(shell, /const PEERIT_URL = 'hyper:\/\/' \+ PEERIT_DRIVE_KEY \+ '\/'/) |
| 41 | // Fresh launch opens PearBrowser first (tabs[0] == the active tab), then p2pbuilders + peerit. | |
| 41 | // Fresh launch opens the browser-owned home first, then the landing, | |
| 42 | // p2pbuilders, and peerit. | |
| 42 | 43 | assert.match(shell, /const STARTUP_TABS = \[/) |
| 44 | assert.match(shell, /\{ url: '', title: 'PearBrowser Home' \}/) | |
| 43 | 45 | assert.match(shell, /\{ url: DEFAULT_URL, title: 'PearBrowser' \}/) |
| 44 | 46 | assert.match(shell, /\{ url: P2PBUILDERS_URL, title: 'P2P Builders' \}/) |
| 45 | 47 | assert.match(shell, /\{ url: PEERIT_URL, title: 'peerit' \}/) |
| … | … | @@ -0,0 +1,33 @@ |
| 1 | import test from 'node:test' | |
| 2 | import assert from 'node:assert/strict' | |
| 3 | import { readFileSync } from 'node:fs' | |
| 4 | import { | |
| 5 | PRIVATE_SEARCH_PROVIDER, | |
| 6 | normalizePrivateSearchQuery, | |
| 7 | buildPrivateSearchUrl | |
| 8 | } from '../ui/lib/private-search.js' | |
| 9 | ||
| 10 | test('private search uses the declared DuckDuckGo HTTPS endpoint', () => { | |
| 11 | assert.equal(PRIVATE_SEARCH_PROVIDER.name, 'DuckDuckGo') | |
| 12 | assert.equal(PRIVATE_SEARCH_PROVIDER.origin, 'https://duckduckgo.com/') | |
| 13 | const url = new URL(buildPrivateSearchUrl('tracker blocking browser')) | |
| 14 | assert.equal(url.origin, 'https://duckduckgo.com') | |
| 15 | assert.equal(url.searchParams.get('q'), 'tracker blocking browser') | |
| 16 | assert.deepEqual([...url.searchParams.keys()], ['q']) | |
| 17 | }) | |
| 18 | ||
| 19 | test('private search normalizes whitespace, bounds input, and rejects blanks', () => { | |
| 20 | assert.equal(normalizePrivateSearchQuery(' private\n\tsearch '), 'private search') | |
| 21 | assert.equal(normalizePrivateSearchQuery('x'.repeat(3000)).length, 2048) | |
| 22 | assert.equal(buildPrivateSearchUrl(' '), null) | |
| 23 | }) | |
| 24 | ||
| 25 | test('the browser-owned home is first and contains an honest provider disclosure', () => { | |
| 26 | const shell = readFileSync(new URL('../ui/shell.js', import.meta.url), 'utf8') | |
| 27 | assert.match(shell, /\{ url: '', title: 'PearBrowser Home' \},\n\s*\{ url: DEFAULT_URL, title: 'PearBrowser' \}/) | |
| 28 | assert.match(shell, /data-testid="private-search-form"/) | |
| 29 | assert.match(shell, /data-testid="private-search-input"[\s\S]*autoFocus/) | |
| 30 | assert.match(shell, /rememberVisit: false/) | |
| 31 | assert.match(shell, /receives your query and network address/) | |
| 32 | assert.match(shell, /Private search is not anonymity/) | |
| 33 | }) |
| … | … | @@ -100,6 +100,20 @@ test('restoreStartupTabs keeps PearBrowser landing first even when Dealroom was |
| 100 | 100 | assert.equal(restored.activeId, restored.tabs[0].id) |
| 101 | 101 | }) |
| 102 | 102 | |
| 103 | test('restoreStartupTabs keeps a named blank browser home at the front', () => { | |
| 104 | const restored = restoreStartupTabs([ | |
| 105 | { url: DEALROOM, title: 'Pear Dealroom', active: true } | |
| 106 | ], [ | |
| 107 | { url: '', title: 'PearBrowser Home' }, | |
| 108 | { url: A, title: 'PearBrowser' } | |
| 109 | ]) | |
| 110 | assert.equal(restored.tabs[0].url, '') | |
| 111 | assert.equal(restored.tabs[0].title, 'PearBrowser Home') | |
| 112 | assert.equal(restored.tabs[1].url, A) | |
| 113 | assert.equal(restored.tabs[2].url, DEALROOM) | |
| 114 | assert.equal(restored.activeId, restored.tabs[0].id) | |
| 115 | }) | |
| 116 | ||
| 103 | 117 | test('normalizeDefaultTab accepts named startup entries', () => { |
| 104 | 118 | assert.deepEqual(normalizeDefaultTab({ url: A, title: 'PearBrowser' }), { url: A, title: 'PearBrowser' }) |
| 105 | 119 | assert.deepEqual(normalizeDefaultTab(B), { url: B, title: '' }) |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | // Pure helpers for the start-page web search. Keeping the provider contract in | |
| 2 | // one framework-free module makes the destination and disclosure easy to test. | |
| 3 | ||
| 4 | export const PRIVATE_SEARCH_PROVIDER = Object.freeze({ | |
| 5 | name: 'DuckDuckGo', | |
| 6 | origin: 'https://duckduckgo.com/' | |
| 7 | }) | |
| 8 | ||
| 9 | export function normalizePrivateSearchQuery (value) { | |
| 10 | return String(value || '').normalize('NFKC').trim().replace(/\s+/gu, ' ').slice(0, 2048) | |
| 11 | } | |
| 12 | ||
| 13 | export function buildPrivateSearchUrl (value) { | |
| 14 | const query = normalizePrivateSearchQuery(value) | |
| 15 | if (!query) return null | |
| 16 | const url = new URL(PRIVATE_SEARCH_PROVIDER.origin) | |
| 17 | url.searchParams.set('q', query) | |
| 18 | return url.toString() | |
| 19 | } |
| … | … | @@ -126,7 +126,9 @@ export function restoreStartupTabs (savedTabs, defaultUrls = []) { |
| 126 | 126 | |
| 127 | 127 | for (const entry of defaultUrls) { |
| 128 | 128 | const { url, title } = normalizeDefaultTab(entry) |
| 129 | if (url) add(makeTab(url, title ? { title } : {})) | |
| 129 | // A named blank entry is the browser-owned start page. It has no URL to | |
| 130 | // fetch, but still belongs at the front of every restored session. | |
| 131 | if (url || title) add(makeTab(url, title ? { title } : {})) | |
| 130 | 132 | } |
| 131 | 133 | |
| 132 | 134 | const restoredPairs = Array.isArray(savedTabs) |
| … | … | @@ -24,6 +24,7 @@ import { |
| 24 | 24 | describeAiStatus, |
| 25 | 25 | buildQuickAskRequest |
| 26 | 26 | } from './lib/qvac-widget.js' |
| 27 | import { PRIVATE_SEARCH_PROVIDER, buildPrivateSearchUrl } from './lib/private-search.js' | |
| 27 | 28 | |
| 28 | 29 | function copyText (text) { |
| 29 | 30 | try { |
| … | … | @@ -179,6 +180,7 @@ const PEERIT_URL = 'hyper://' + PEERIT_DRIVE_KEY + '/' |
| 179 | 180 | const P2PBUILDERS_DRIVE_KEY = 'ac1977a75cc84b46af0af8bb559cd4ebbe10507eb0f51d863e289d09635f6d74' |
| 180 | 181 | const P2PBUILDERS_URL = 'hyper://' + P2PBUILDERS_DRIVE_KEY + '/' |
| 181 | 182 | const STARTUP_TABS = [ |
| 183 | { url: '', title: 'PearBrowser Home' }, | |
| 182 | 184 | { url: DEFAULT_URL, title: 'PearBrowser' }, |
| 183 | 185 | { url: P2PBUILDERS_URL, title: 'P2P Builders' }, |
| 184 | 186 | { url: PEERIT_URL, title: 'peerit' } |
| … | … | @@ -1152,6 +1154,7 @@ function Browse ({ rpc, C, navUrl, onNavigated, tabs, setTabs, activeId, setActi |
| 1152 | 1154 | const activeIdRef = useRef(activeId) |
| 1153 | 1155 | const autoLoadedRef = useRef(new Set()) |
| 1154 | 1156 | const [editingUrl, setEditingUrl] = useState('') |
| 1157 | const [privateSearchQuery, setPrivateSearchQuery] = useState('') | |
| 1155 | 1158 | // About-this-site modal — true when user clicked the (i) button. |
| 1156 | 1159 | const [aboutOpen, setAboutOpen] = useState(false) |
| 1157 | 1160 | const [askOpen, setAskOpen] = useState(false) |
| … | … | @@ -1434,6 +1437,16 @@ function Browse ({ rpc, C, navUrl, onNavigated, tabs, setTabs, activeId, setActi |
| 1434 | 1437 | setEditingUrl(url || '') |
| 1435 | 1438 | } |
| 1436 | 1439 | |
| 1440 | const submitPrivateSearch = (event) => { | |
| 1441 | event?.preventDefault?.() | |
| 1442 | const target = buildPrivateSearchUrl(privateSearchQuery) | |
| 1443 | if (!target) return | |
| 1444 | setPrivateSearchQuery('') | |
| 1445 | // Search queries are never written to the optional persistent visit log. | |
| 1446 | // The active tab still shows its current results URL, like every browser. | |
| 1447 | go(target, activeId, { rememberVisit: false }) | |
| 1448 | } | |
| 1449 | ||
| 1437 | 1450 | const closeTab = (id) => { |
| 1438 | 1451 | const closing = tabs.find((t) => t.id === id) |
| 1439 | 1452 | const closed = normalizeTabSnapshot(closing) |
| … | … | @@ -1837,12 +1850,37 @@ function Browse ({ rpc, C, navUrl, onNavigated, tabs, setTabs, activeId, setActi |
| 1837 | 1850 | </div> |
| 1838 | 1851 | </div> |
| 1839 | 1852 | </div>` |
| 1840 | // A truly blank tab (⌘T) gets the welcome + quick actions. | |
| 1853 | // A truly blank tab (including the first startup tab) is the | |
| 1854 | // browser-owned home surface: private search + local tools. | |
| 1841 | 1855 | : html`<div key=${t.id} className="browse-welcome"> |
| 1842 | <div className="browse-welcome-inner"> | |
| 1856 | <div className="browse-welcome-inner start-page"> | |
| 1843 | 1857 | <div className="browse-welcome-logo">🍐</div> |
| 1844 | <h2>The peer-to-peer web starts here</h2> | |
| 1845 | <p>Paste any <code>hyper://</code> URL above — hex or z-base-32 — and PearBrowser fetches it directly from its peers. No DNS, no servers, no CDN.</p> | |
| 1858 | <h2>Search without a profile</h2> | |
| 1859 | <p className="start-page-lede">PearBrowser sends no search analytics and never adds a query to its optional persistent visit history.</p> | |
| 1860 | <section className="private-search-card" aria-labelledby="private-search-title"> | |
| 1861 | <div className="private-search-heading"> | |
| 1862 | <span id="private-search-title">Private web search</span> | |
| 1863 | <span className="private-search-provider">${PRIVATE_SEARCH_PROVIDER.name}</span> | |
| 1864 | </div> | |
| 1865 | <form className="private-search-form" data-testid="private-search-form" onSubmit=${submitPrivateSearch}> | |
| 1866 | <input | |
| 1867 | type="search" | |
| 1868 | value=${privateSearchQuery} | |
| 1869 | data-testid="private-search-input" | |
| 1870 | aria-label="Search the web privately" | |
| 1871 | placeholder="Search the web" | |
| 1872 | autoComplete="off" | |
| 1873 | autoFocus | |
| 1874 | spellCheck="false" | |
| 1875 | onInput=${event => setPrivateSearchQuery(event.target.value)} | |
| 1876 | /> | |
| 1877 | <button type="submit" className="private-search-submit" data-testid="private-search-submit" disabled=${!privateSearchQuery.trim()}>Search</button> | |
| 1878 | </form> | |
| 1879 | <div className="private-search-disclosure"> | |
| 1880 | Content Shield stays on. ${PRIVATE_SEARCH_PROVIDER.name} receives your query and network address to return results; its published policy says it does not save or share search history. Private search is not anonymity. | |
| 1881 | </div> | |
| 1882 | </section> | |
| 1883 | <div className="start-page-p2p">Or paste a <code>hyper://</code> address above to fetch a site directly from its peers — no DNS, server, or CDN.</div> | |
| 1846 | 1884 | <div className="browse-welcome-actions"> |
| 1847 | 1885 | <button className="btn primary" onClick=${() => go(DEFAULT_URL)}>Open the PearBrowser site</button> |
| 1848 | 1886 | <button className="btn subtle" onClick=${() => { inputRef.current?.focus(); inputRef.current?.select?.() }}>Focus the URL bar</button> |
| … | … | @@ -6482,10 +6520,10 @@ export function App ({ rpc, C, storagePath }) { |
| 6482 | 6520 | // 1. Switching to Apps/Settings/etc and back doesn't destroy them |
| 6483 | 6521 | // (Browse used to remount with a fresh tabs[] every time) |
| 6484 | 6522 | // 2. We can persist them to user-data and restore across launches |
| 6485 | // Default initial state on launch is the PearBrowser landing page first, | |
| 6486 | // then p2pbuilders, then peerit. Restored session tabs stay behind those | |
| 6487 | // defaults so an app homepage such as Dealroom cannot hijack the release | |
| 6488 | // landing slot. | |
| 6523 | // Default initial state on launch is the browser-owned private-search home | |
| 6524 | // first, then the PearBrowser landing, p2pbuilders, and peerit. Restored | |
| 6525 | // session tabs stay behind those defaults so an app homepage cannot hijack | |
| 6526 | // the home slot. | |
| 6489 | 6527 | const [tabs, setTabs] = useState(() => STARTUP_TABS.map((tab) => makeBrowserTab(tab.url, { title: tab.title }))) |
| 6490 | 6528 | const [browseActiveId, setBrowseActiveId] = useState(() => 'placeholder') |
| 6491 | 6529 | const [closedTabs, setClosedTabs] = useState(() => []) |