Commit Graph
15651 Commits
Author SHA1 Message Date
Claude f2f80cd88d fix: stop clearing Robohash/richtext caches on every app background
TRIM_MEMORY_UI_HIDDEN fires on every app switch, not only under memory
pressure. Clearing Robohash (CPU-intensive SVG assembly) and
CachedRichTextParser to zero there forces a full rebuild on every
resume, causing visible jank.

Restructure the trim tiers to match Android's intent:

  UI_HIDDEN  (20) — just backgrounded, no pressure:
      trim Coil image cache to 1/2 only; leave parsed-text and
      avatar caches warm so resume is instant.

  BACKGROUND (40) — mild background pressure:
      trim images to 1/4, richtext→100, robohash→20, nip11→200.

  MODERATE   (60) — system is hurting:
      clear images, richtext→50, robohash→10, nip11→100.

  COMPLETE   (80) — kill imminent:
      clear everything including nip11.

Foreground levels (RUNNING_LOW/RUNNING_CRITICAL) are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h2c44rAwexuUEP3kky2F3
2026-06-24 22:00:52 +00:00
Claude ea2dbd9a77 fix: keep lastNotes intact on CardFeedContentState.trimToSize()
Clearing lastNotes caused the next additive update to call
refreshSuspended(), reloading the full filter limit (~500 notes) from
LocalCache and immediately undoing the trim.

With lastNotes intact the fast additive path stays active: only
genuinely new notifications are appended, so the card list remains near
maxItems until the next full feed key change or navigation event.

The Note refs in lastNotes are the same object instances already held
by LocalCache. They are freed when MemoryTrimmingService prunes
LocalCache and the following refreshSuspended() replaces lastNotes with
the pruned set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h2c44rAwexuUEP3kky2F3
2026-06-24 21:42:14 +00:00
Claude e17c95eda1 fix: clear lastNotes on CardFeedContentState.trimToSize() to release Note refs
CardFeedContentState.lastNotes holds strong references to every raw Note
that passed the notification filter, used for additive dedup
(filteredNewList.minus(lastNotesCopy)). Trimming only the Card list left
all those Note objects pinned — they couldn't be GC'd even if LocalCache's
SoftCache had evicted them.

Fix: clear lastNotes/lastAccount alongside the Card list truncation.
The next additive update finds lastNotes == null, skips the fast path,
and calls refreshSuspended() — one controlled rebuild from LocalCache
that also resets the dedup set to the current state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h2c44rAwexuUEP3kky2F3
2026-06-24 21:35:14 +00:00
Claude 57a63bd4b2 fix: stop wiping relay info cache on every app background
At UI_HIDDEN (fires on every app switch), nip11Cache.trimToSize(0)
was clearing all successfully-fetched NIP-11 relay documents, forcing
10–50 redundant HTTP fetches on the next foreground resume.

- Change UI_HIDDEN trim target from 0→100; the 100 most-recently-used
  relay docs are kept across a background/foreground cycle.
- Stop trimming relayInformationEmptyCache in Nip11CachedRetriever:
  it holds only lightweight display-name+favicon-url placeholder objects
  (no network data), so trimming saves negligible memory but silently
  re-triggers NIP-11 HTTP fetches for every relay on resume.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h2c44rAwexuUEP3kky2F3
2026-06-24 20:44:00 +00:00
Claude 5435345f7d feat: trim richtext, robohash, and relay-info caches under memory pressure
Add trimToSize(maxItems) to:
- CachedRichTextParser: trims richTextCache (500) and isMarkdownCache
  (200) proportionally
- CachedRobohash: trims the ImageVector LruCache (100)
- Nip11CachedRetriever: trims both the document and empty-placeholder
  caches (1000 each)

Wire all three into AppModules.trim() tiered by OS pressure level:
  RUNNING_LOW      → 50% capacity
  RUNNING_CRITICAL → 20% capacity
  UI_HIDDEN+       → evict all (app not visible, safe to clear)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h2c44rAwexuUEP3kky2F3
2026-06-24 20:11:28 +00:00
Claude 8407684d9d feat: trim Coil memory cache and ExoPlayer warm pool under memory pressure
- ExoPlayerPool.releaseWarmPool(): evicts all paused-with-buffer (warm)
  players back to the cold pool. Safe under pressure because warm players
  are idle; active (checked-out) players are never in either pool.

- PlaybackService.onTrimMemory(): when level >= RUNNING_CRITICAL, drains
  both pool instances' warm slots via releaseWarmPool(). The Service
  receives onTrimMemory() directly from Android so no routing through
  AppModules is needed.

- AppModules.trim(): trims Coil's in-memory image cache proportional to
  OS pressure level:
    RUNNING_LOW  → trimToSize(maxSize / 2)
    RUNNING_CRITICAL → trimToSize(maxSize / 4)
    UI_HIDDEN+   → trimToSize(0)  [app backgrounded, safe to clear]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h2c44rAwexuUEP3kky2F3
2026-06-24 20:11:28 +00:00
Claude 5b5bc5c70c fix: raise critical memory feed trim size from 50 to 200
50 was too aggressive — users would lose most of their scroll history on
any critical-pressure event. 200 is a better balance: still releases
~60% of the 500-note strong references per feed while keeping a
reasonable amount of history visible without a reload.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 20:11:28 +00:00
Claude 057b6266d9 feat: trim feed lists to 50 items under critical memory pressure
Feed lists hold ImmutableList<Note> references. At 40+ feeds × 500 notes
each, that's 20k strong Note references that prevent GC from collecting
objects that LocalCache has already pruned. Under RUNNING_CRITICAL the
feeds are shrunk to 50 items, releasing ~19k references per account.

Implementation:
- FeedContentState.trimToSize(maxItems) — truncates the loaded list in-place
- CardFeedContentState.trimToSize(maxItems) — same for notification card feeds
- AccountFeedContentStates.trimFeedsToSize(maxItems) — fans out to all feeds
- AppModules.trimLevelEvents: SharedFlow<Int> — broadcasts the OS level
- AccountFeedContentStates subscribes and calls trimFeedsToSize(50) at
  RUNNING_CRITICAL, letting the next scroll/refresh repopulate from cache

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 20:11:27 +00:00
Claude 4eb195fa5e fix: reorder pruning tiers in MemoryTrimmingService
Tier 2 (medium, >= RUNNING_LOW): pruneHiddenEvents + pruneHiddenMessages
  — muted/blocked content is known-safe to drop at low pressure.

Tier 3 (critical, >= RUNNING_CRITICAL): pruneOldMessages + pruneRepliesAndReactions
  — these are more aggressive since they remove content the user may
  still scroll back to, so reserve them for genuine memory emergencies.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 20:11:27 +00:00
Claude 281955fb17 fix: move cleanObservers to Tier 1 in MemoryTrimmingService
cleanObservers() only removes flows not currently held by the UI, so
it carries no visible side effects and is safe to run on every trim
regardless of pressure level. Move it from Tier 3 (RUNNING_CRITICAL)
to Tier 1 (always) so unused observer links are freed even on mild
memory signals.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 20:11:27 +00:00
Claude f038e54fa2 feat: scale LocalCache pruning aggressiveness to OS memory-pressure level
MemoryTrimmingService.doTrim() previously ran the full pruning suite
regardless of how severe the OS signal was. Now it tiers the work by
ComponentCallbacks2 level so low-pressure signals don't pay the cost of
aggressive observer teardown:

  Tier 1 (always): cleanMemory + pruneExpiredEvents + prunePastVersionsOfReplaceables
  Tier 2 (>= RUNNING_LOW): + pruneOldMessages + pruneRepliesAndReactions
  Tier 3 (>= RUNNING_CRITICAL): + cleanObservers + pruneHiddenEvents/Messages

The `level` is now threaded from Amethyst.onTrimMemory → AppModules.trim(level)
→ MemoryTrimmingService.run(…, level) → doTrim(…, level). The existing
scheduled-trim call site (AppModules.trim) defaults to RUNNING_CRITICAL so
its behaviour is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 20:11:27 +00:00
Claude 21e61e48ae feat: add debug-only memory usage chip to top nav bar
Shows JVM heap used/max as a color-coded label (green/amber/red) in the
top bar actions on debug builds. Tapping opens a dialog with a full
breakdown: native heap, Coil memory/disk cache sizes, and LocalCache
counts (notes, users, addressables, chatrooms). Polls every 2 seconds
via produceState. Adds MemorySnapshot data class and
collectMemorySnapshot() to DebugUtils for reuse by future tooling.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 20:11:01 +00:00
Vitor PamplonaandGitHub 6539841e5e Merge pull request #3350 from vitorpamplona/claude/jolly-mccarthy-jyeu87
Browser: add visit history, omnibox suggestions, and favicon capture
2026-06-24 14:46:58 -04:00
Vitor PamplonaandClaude Opus 4.8 cad987a99e fix(embed): napplet/nsite load overlay + draw it over the surface
Mirrors the web-app load-recovery fix to the napplet/nsite path and fixes a
layer bug that kept the overlay from ever showing.

The embedded surface (SandboxedSdkView) is drawn by EmbeddedTabLayer, which
sits *above* the nav screens in the shell. So a loading/error overlay placed in
the favorite screen was covered by the surface's opaque pre-first-frame
background — the black void persisted. Move the overlay into EmbeddedTabLayer,
drawn over the active tab's bounds (where the chrome sheet already lives), so it
actually covers the surface. Also fixes the overlay sizing (fillMaxSize, not
matchParentSize, which collapsed to zero inside the reserved Box).

- Promote load state to the EmbeddedSurfaceController interface (loadStatus /
  onLoadStatusChanged / retry), so EmbeddedTabLayer renders one overlay for both
  the browser and napplet controllers. Shared EmbeddedLoadStatus +
  EmbeddedLoadOverlay.
- NappletHostService now reports main-frame load state (start/finish/error) over
  a new MSG_LOAD_STATE; EmbeddedNappletController relays it and exposes retry()
  (= reload the verified content).
- The web-app path keeps its about:blank → canonical-URL self-heal; the napplet
  path has no client-supplied URL to drop, so retry = reload.

Verified on device: with the network cut, the brainstorm tab shows
"Couldn't load this app." + Retry; restoring the network and tapping Retry loads
the page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:35:44 -04:00
Vitor PamplonaandClaude Opus 4.8 fb4a2e0858 fix(browser): recover embedded web-app tab stuck on about:blank
A favorite web app pinned to the bottom bar at runtime could come up on a
blank surface (black, then white after a manual reload) and never recover.
Its warm browser session settled on about:blank — its real URL was dropped on
the way in — and the chrome Reload button calls WebView.reload(), which just
re-loads about:blank instead of the favorite's page.

Fixes:
- The provider now reports main-frame load state (start/finish/error) over a
  new MSG_LOAD_STATE. When a favorite session settles on about:blank while it
  has a real URL, the controller re-navigates to the canonical URL once.
  Gated on a real startUrl, so the generic browser's intentional about:blank
  new-tab page is left alone. Adds controller.retry() (navigate-to-canonical,
  not reload) for the chrome retry path.
- FavoriteWebAppScreen now draws a loading spinner until a real page paints,
  and an error + Retry overlay when the main frame fails or the load stalls
  (12s) — so a slow, blank, or failed load is no longer a silent black/white
  void.

Scoped to the browser/WebUrl path; the napplet/nsite path is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:03:33 -04:00
Claude 2df0c4d6bb feat: favicons in bottom nav + 3-dot menu on recent rows
- Pinned web favorites in the bottom navigation now show the captured favicon
  instead of the generic globe (falling back to the globe until one is
  captured). Resolved via BrowserIconRegistry, same as the launcher cards.
- Each Recent row in the browser home gains a 3-dot overflow menu to add the
  URL to favorites (or remove it if already favorited) and to remove it from
  history. Replaces the prior long-press-to-remove with a discoverable menu;
  the row icon shows a star once the site is favorited.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx
2026-06-24 16:04:46 +00:00
Vitor PamplonaandGitHub db99891c27 Merge pull request #3349 from vitorpamplona/claude/epic-carson-wjc0xm
Fix bottom bar fallback to use default entries instead of empty list
2026-06-24 11:43:36 -04:00
Claude 4f6a21c55d feat: favorites-first browser home + recents, with captured favicons
Builds on the omnibox work to modernize the launcher list now that favorites
and visit history both exist:

- Idle browser home (BrowserHome): pinned favorites on top under a "Favorites"
  header, then a "Recent" section from the visit history — all in one grid so
  they scroll together. Long-press a recent to drop it.
- Typed suggestions are grouped: a highlighted "Favorites" group first (subtle
  primary-container tint + medium weight), then "Recent". Favorites still rank
  first via the existing frecency boost.
- Real favicons: captured from the WebView that already loaded the page in the
  keyless :napplet browser host (so they ride the page's own Tor-routed network
  path — the main app never fetches host/favicon.ico itself), scaled and
  relayed as PNG bytes over a new MSG_RECORD_ICON IPC, and stored per-host by
  BrowserIconRegistry (main process, filesDir). Favorite cards, suggestion
  rows, and recent rows all show them, falling back to a glyph.

FavoriteAppIcon gains an optional iconModel; FavoriteAppCell is reusable via a
new LazyGridScope.favoriteAppItems extension so the browser home and the
Favorite Apps tab share one cell. Thumbnails deferred to a follow-up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx
2026-06-24 15:35:40 +00:00
Claude 851fb82735 fix: only reset bottom bar to defaults on parse errors, not intentional empty selection
The ifEmpty guard incorrectly reset a user-saved empty bar to defaults.
A successfully parsed empty list (user actively removed all items) is now
preserved. Only blank/unset keys and unrecognizable formats fall back to
DefaultBottomBarEntries.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 15:32:18 +00:00
Claude 4a907c750c fix: reset bottom bar to defaults when parsed config has 0 items
Blank stored strings and successfully-parsed empty JSON arrays both
left the bottom bar invisible. Now both cases fall back to
DefaultBottomBarEntries so navigation is never silently lost.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 15:14:22 +00:00
Claude 7a03a47d1d feat: omnibox autocomplete, visit history, and in-page address bar for the browser
Refines the browser URL-bar experience across the launcher and the in-page
browser chrome:

- Shared URL normalization (commons OmniboxInput): dedupes the logic that was
  copied between BrowserScreen and NappletBrowserService, recognizes bare
  domains/localhost/IPs, falls back to a (configurable) DuckDuckGo search, and
  flags .onion as Tor-only so the launcher forces Tor for it.
- Omnibox suggestions (commons OmniboxSuggestions): ranks favorites + visit
  history by prefix/substring match, favorite boost, and frecency; deduped by
  host. The launcher body turns into a suggestion list as you type.
- Inline ghost-text completion in the address field (TextFieldValue selection),
  completing a typed host fragment to the top-ranked host.
- Visit history (BrowserHistoryRegistry, main process): a device-local,
  bounded, DataStore-backed store. Pages are recorded ONLY on a clean
  main-frame load — relayed from the keyless :napplet browser host over a new
  MSG_RECORD_HISTORY IPC — so misspelled/unresolved addresses never enter it.
- In-page editable address bar (websites only) in NappletControlSheet, showing
  the live URL + a security glyph (Tor/https/plain) and loading what the user
  types. nsite/napplet hosts pass no navigate callback, so they never get one.

Pure logic is covered by unit tests in commons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx
2026-06-24 15:02:01 +00:00
Claude 4e6f6c104b fix: improve browser omnibar URL handling and keyboard behavior
- Disable autocorrect and autocapitalization in the URL bar so domain
  names are not mangled by the keyboard's spell-checker
- Align normalizeUrl with NappletBrowserService logic: bare domain
  names (no space, contains dot) get https:// prepended; everything
  else falls through to a DuckDuckGo search query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx
2026-06-24 14:25:45 +00:00
Vitor PamplonaandGitHub 048439201e Merge pull request #3347 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-24 10:05:25 -04:00
vitorpamplonaandgithub-actions[bot] 7b890a0eff chore: sync Crowdin translations and seed translator npub placeholders 2026-06-24 14:01:39 +00:00
Vitor PamplonaandGitHub f298750b79 Merge pull request #3348 from vitorpamplona/claude/webview-menu-custom-url-ikrgbz
Add full-screen direct-WebView browser and embedded napplet/nSite tabs
2026-06-24 09:58:59 -04:00
Claude 01ba2dc909 docs: document the final embedded-tab/browser/IME napplet architecture
This branch added a lot the existing napplet plans don't describe: embedded warm
bottom-bar tabs on a cross-process SurfaceControlViewHost, an arbitrary-URL
browser host, a soft-keyboard IME proxy, the :nappletHost module split, the
per-session service refactor, the two control-sheet twins, and per-site Tor
routing.

- Add amethyst/plans/2026-06-24-napplet-embedded-tabs.md capturing the final
  architecture and file map.
- Note on the 2026-06-19 sandbox-host plan that its rendering model is superseded
  (trust model unchanged), pointing at the new doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 13:48:41 +00:00
Claude 3460c58274 feat: show a back arrow in the Browser top bar when pushed from the drawer
The Browser is reachable both as a bottom-bar tab (top-level, no back arrow)
and from the navigation drawer (pushed onto the back stack). In the latter case
its omnibox now leads with a back arrow that pops, matching the convention the
other launcher/feed top bars already follow (NappletsTopBar et al.): show
ArrowBackIcon when nav.canPop(), nothing otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 13:33:21 +00:00
Claude 8b9fa10a1b style: standardize the two top-sheet twins (spacing, colors, Tor switch)
The embedded surfaces' Compose TopControlSheet and the full-screen activities'
native NappletControlSheet are deliberate twins in different processes/modules
(Compose in the main app vs hand-built Views in the Compose-free :napplet
sandbox host), so they can't share a composable — but they should render
identically. Bring them in line:

- Uniform row rhythm: every action/Tor row now uses the same 10dp vertical
  padding in both. The Compose switch row was 6dp while items were 12dp; both
  are now 10dp, matching the native rows.
- Native Tor row now uses a real framework Switch as the state indicator (like
  the embedded row) instead of an icon whose tint/alpha encoded on/off; the icon
  is a steady muted tint and the whole row is the toggle target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 13:20:25 +00:00
Vitor PamplonaandGitHub 3fa17e1cb7 Merge pull request #3346 from greenart7c3/claude/nip-2381-bunker-connect-psiq2t
NIP-46: Add optional client metadata to connect requests
2026-06-24 08:58:28 -04:00
Claude da4207319b fix(nip46): pin client metadata to 4th connect param, backfill empties
Per NIP-46 (nostr-protocol/nips#2381), client metadata is the 4th
positional connect param. When it is present but the optional secret or
permissions are not, those slots are now back-filled with empty strings
so the metadata always lands at index 3. Parsing maps empty placeholders
back to null. When no metadata is sent, the array stays as short as
possible for backward compatibility.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYpupiVAq4VyHDdjrYyPdi
2026-06-24 12:08:06 +00:00
Claude 4ae2debd6b refactor(cli): drop client-metadata log from bunker connect handler
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYpupiVAq4VyHDdjrYyPdi
2026-06-24 11:28:14 +00:00
Claude 22c8c45190 fix: harden embedded napplet/nsite/browser hosts against audit-found races and IME bugs
Sandbox host services (per-session correctness + leaks):
- NappletHostService/NappletBrowserService: guard broker-reply delivery against
  a stale tab (drop replies whose session was replaced) and wrap postMessage in
  runCatching so a torn-down WebView can't crash the relay; tear down each tab's
  content server + WebView on session close and in onDestroy; refuse to build an
  orphan WebView for an unknown session.
- NappletContentServer.close(): shut the OkHttp dispatcher + evict the pool off
  the hot path so a closed tab doesn't leak connections/threads.
- NappletBlobHttp: bound a blob fetch end-to-end with a callTimeout so a stalled
  Tor exit can't pin the WebView worker thread indefinitely.
- UiAdapter close(): hop to the main thread before destroying the WebView.

Embedded IME (shim.js + RemoteImeView):
- Surrogate-pair-safe diff so an edited astral char (emoji, CJK-supplement) is
  never split into a lone surrogate in the synthesized InputEvent data.
- Real contenteditable support: map char offsets through Ranges and replace in
  place instead of overwriting textContent (which destroyed structure + caret).
- Dedup selectionchange against the last applied selection so our own setSel
  doesn't echo back to the host as a fresh edit.
- RemoteImeView flushes synchronously at the outermost batch close, preserving
  the composing region across a compose+commit in the same frame.

Embedded layer + preloader:
- Resize the cross-process surface to the snapped imeAnimationTarget instead of
  the animated ime inset, so it doesn't reconfigure every keyboard-slide frame.
- yield() between favorites in the startup sweep so building WebViews doesn't
  monopolize the frame.
- Per-site Tor/open-web registries expose awaitReady(); the preloader awaits
  hydration before its first routing decision so a cold start can't route a
  site the user pinned to the open web through Tor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 04:27:50 +00:00
Claude 5eb39b141a refactor: session-scope NappletHostService so multiple napplet/nsite tabs are correct
Mirror the browser host's per-session refactor for the napplet/nsite embed
provider. A single NappletHostService instance is shared by every embedded tab
(same bound Intent), but it kept one set of fields (client messenger, config,
content server, WebView, broker bridge), so with >1 napplet tab open the
controls (reload/back/pause/resume), navigation state, "allow always" notices,
NIP-07 traffic, and IME all routed to whichever tab was created last.

Collect all per-surface state into a NappletTab keyed by a client-stamped
session id (KEY_SESSION_ID on MSG_CREATE_SESSION and every control message):

- Controls/pause/resume resolve the target tab by id and act on its own WebView.
- Content server, shell handshake (declaredDomains), launch token, and page
  state/notices are per tab; onShellMessage resolves the tab by its WebView.
- Each tab gets its OWN reply Messenger, so broker responses AND unsolicited
  relay pushes come back tagged to the right tab — no id rewriting, per-tab
  origin/fire-seq state.
- onSessionClosed drops the tab and destroys only its own WebView; the broker is
  bound once for the whole service.

EmbeddedNappletController generates a unique id and stamps it on all messages.
Both embed hosts (browser + napplet) are now fully per-tab correct, including the
keyboard for multiple simultaneous tabs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 04:00:54 +00:00
Claude 09cd69ba7c feat: soft keyboard in embedded napplet/nsite tabs (same proxy, via the shell)
Extend the embedded keyboard to napplet/nsite surfaces. The shim's IME agent now
installs on any embedded surface (gated by __nappletImeProxy), reaching native
over whichever transport it has — the direct bridge for the browser, the trusted
shell relay for napplets — both through send().

- NappletContentServer gains an imeProxy flag that injects __nappletImeProxy
  before the shim; NappletHostService sets it (embedded), the full-screen
  NappletHostActivity leaves it off (native keyboard).
- NappletHostService relays ime.* between the applet (via the shell bridge) and
  the client (MSG_IME_EVENT / MSG_IME_OP) — the shell already forwards all
  message types, so no change to the trusted shell page.
- EmbeddedNappletController implements EmbeddedImeBridge, so EmbeddedTabLayer's
  RemoteImeView drives it exactly like the browser.

Correct for a single nsite/napplet tab. Multiple simultaneous napplet tabs share
the host service's single client/bridge pointer (same limitation as reload/back/
NIP-07 there) — making that per-tab needs the session-scoping the browser host
already got; tracked as a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 03:50:47 +00:00
Claude e6bbecdea2 refactor: embedded keyboard to Flutter's editing-state model (batched, full coverage)
Rework the host-side input proxy to mirror Flutter's TextInputPlugin /
InputConnectionAdaptor instead of forwarding individual IME ops:

- RemoteImeView ships the whole editing STATE (text + selection + composing
  region) rather than per-op messages, coalesced across IME batch boundaries
  (beginBatchEdit/endBatchEdit nesting, like Flutter's batchEditNestDepth). A
  TextWatcher + onSelectionChanged flush captures every mutation, so the soft
  keyboard, HARDWARE keyboards, autofill, paste, and context-menu edits are all
  covered uniformly — they all mutate the same real Editable. Composing region is
  read from the platform via BaseInputConnection.getComposingSpan*.
- The shim adopts that state and synthesizes the matching DOM input/composition
  events (insertText / insertCompositionText / deleteContentBackward /
  insertReplacementText, with compositionstart/update/end) so web frameworks
  react as if typed natively — going beyond Flutter, whose consumer is a Dart
  widget. A common prefix/suffix diff classifies each change.
- Beyond Flutter: backed by a REAL EditText, so the platform answers
  getTextBeforeCursor/getExtractedText, suggestions, and spell-check for free
  rather than hand-rolling a ListenableEditingState.
- showSoftInput is posted after focus settles (avoids the show no-op race).

This collapses the op vocabulary to ime.set (state) + ime.action and removes the
commit/compose/delete/key/setSelection messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 03:44:15 +00:00
Claude f0d5e7a626 feat: soft keyboard in the embedded browser via a host-window input proxy
The embedded browser renders cross-process through SurfaceControlViewHost, which
forwards touch but not the soft keyboard (the embedded window can't be an IME
target, and androidx.privacysandbox.ui never wires IME). So focusing a field in
an embedded page did nothing.

Bridge the keyboard instead: host it in the main app window and relay editing to
the page.

- Shim IME agent (embedded browser only, gated by __nappletImeProxy): tracks the
  focused editable, reports focus/blur/external-change, and applies host ops
  (commit / compose / delete / key / editor-action) with real input & composition
  events. Scrolls the field into view on focus.
- NappletBrowserService relays ime.* envelopes between the page bridge and the
  client (MSG_IME_EVENT / MSG_IME_OP), per tab.
- EmbeddedBrowserController implements EmbeddedImeBridge (parses events, sends ops).
- RemoteImeView: an invisible EditText in the main window that takes the keyboard
  for the active tab. Keeps a real local Editable (so the platform handles
  composing/suggestions/selection) while an InputConnection wrapper forwards every
  op to the page. Maps web input types / enterKeyHint to inputType/IME action.
- EmbeddedTabLayer hosts the proxy bound to the active tab and shrinks the active
  surface by the IME height so the page can scroll the field clear of the keyboard.

Covers <input>/<textarea> fully and contenteditable best-effort (plain text).
Napplet/nsite embeds still need their own wiring (the shell path); this is the
browser surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 03:13:25 +00:00
Claude e3e2482954 feat: preload bottom-bar tabs at startup so the first tap is instant
Warm every pinned bottom-bar favorite at app startup instead of lazily on first
visit, so a browser favorite (ditto, amy-lm, …) has already downloaded and is
local by the time the user taps it.

- EmbeddedTabFactory: the single place that builds a warm controller, shared by
  the favorite screens and the new preloader so they produce the same session
  keyed by the same id (whoever runs first wins; the other reuses it).
- EmbeddedTabPreloader (mounted next to EmbeddedTabLayer): sweeps the bottom-bar
  favorites and acquires each. Retries for a bounded window so a napplet whose
  event hasn't synced, or a Tor proxy still connecting, gets a chance to settle.
- Privacy gate: a Tor-routed site is never preloaded over clearnet while Tor is
  merely still connecting — it waits for the proxy port. When Tor is off, or the
  user opted the site out, clearnet is the real route, so it preloads at once.
- Seed an approximate viewport (EmbeddedTabHost.seedBoundsIfUnset) so preloaded
  surfaces download as a full-size page rather than at the 1dp off-screen
  fallback; the first real visit corrects the bounds.

Browser favorites fully preload+download. Napplet/nsite favorites get a warm
session but stay JS-paused until opened (the existing background-gating security
rule for "allow always" apps), so they don't run in the background.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 02:24:26 +00:00
Claude 408eb3a948 refactor: scope embedded browser sessions per-tab in the shared service
A single NappletBrowserService instance is shared by every embedded browser tab
(they bind the same Intent), but it kept one set of fields (webView, client
messenger, NIP-07 bridge state, reply messenger), so with >1 browser tab open:
controls (reload/Tor/back/navigate) hit whichever tab opened last, URL updates
went to the wrong address bar, and NIP-07 responses/pushes could land in the
wrong page.

Collect all per-surface state into a BrowserTab keyed by a client-stamped
session id (KEY_SESSION_ID on MSG_CREATE_SESSION and every control message):

- Controls resolve the target tab by session id and act on its own WebView.
- pushUrl delivers to that tab's own client messenger.
- Each tab gets its OWN reply Messenger, so broker responses AND unsolicited
  relay pushes come back already tagged to the right tab (the broker just echoes
  replyTo) — no id rewriting, and per-tab originTokens/mint state.
- onSessionClosed drops the tab and destroys only its own WebView.

EmbeddedBrowserController generates a unique session id and stamps it on all
messages. Tor note: the WebView proxy override is process-global (Android has no
per-WebView proxy), so toggling Tor still affects every tab; only the toggled
tab is reloaded. The napplet host service has the analogous shared-instance
shape (black-out already fixed) but isn't session-scoped yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 02:02:37 +00:00
Claude b474869213 fix: second embedded browser/napplet tab no longer blacks out the first
A single NappletBrowserService / NappletHostService instance is shared by every
embedded tab (they bind the same Intent), but each tab opens its own session
with its own WebView; the service's `webView` field is only a "latest" pointer.

The audit-batch "destroy stale WebView before rebuild" in createXWebView was
therefore destroying a *sibling* tab's live WebView whenever another browser/
napplet tab opened a session — exactly the repro: open A, switch to B (B's
create destroys A's WebView), back to A → black, B (created last) stays fine.

- Drop the destroy-on-create entirely; each session's WebView lives until that
  session closes.
- onSessionClosed now destroys the closing session's OWN WebView (passed in)
  and clears the shared pointer only if it still referenced it, so evicting one
  tab can't tear down another either.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 01:31:15 +00:00
Claude 984c4e4e8c fix: embed surface no longer blacks out on double-tap / first open
Two black-surface reports traced to the active-tab bookkeeping:

- Double-tapping a bottom-bar tab pops and re-adds the SAME route, so the
  outgoing screen disposes AFTER the incoming one has already called
  setActive(id). clearActiveIfMatches(id) then nulled the active id the new
  instance just set (same id "matches"), leaving no active tab — every warm
  surface gets shoved off-screen and the embed goes black. setActive now returns
  a monotonic ownership token and the disposer clears only if it's still the
  latest claim (clearActiveIfOwner), so a re-nav can't null the new owner.
  clearActiveChrome got the same twin guard.
- Revert the reportBounds active-id guard added in the audit batch: it tied
  contentBounds to the same fragile activeId, so a first-ever embed whose bounds
  reported while the id was momentarily unset stayed at Rect.Zero (parked
  off-screen → black). Bounds are reported unconditionally again; the two
  screens cover the same content area, so there's nothing to clobber.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 01:10:18 +00:00
Claude a0ad9082e5 fix: don't stretch the full-screen pull-down grabber across the whole width
A vertical LinearLayout defaults its children to MATCH_PARENT width, so the
collapsed grabber chip's rounded background spanned the entire screen. Give the
grabber explicit WRAP_CONTENT layout params (centered) so only the chip shows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 01:02:27 +00:00
Claude 41843dd8e8 fix: stop bottom bar resetting on upgrade; make pull-tab a dismissable drawer
Two regressions from this session's audit batch:

1. Bottom nav bar reset. Adding stable @SerialName discriminators to
   BottomBarEntry changed the persisted polymorphic "type" value from the
   fully-qualified class name to "builtIn"/"favorite", so configs written by an
   earlier build no longer decoded — and the fallback returned an empty list,
   blanking the bar. decodeBottomBarItems now migrates the old fully-qualified
   discriminators to the short names (recovering the user's customized bar), and
   any unrecognizable value falls back to the defaults instead of empty. Locked
   with BottomBarEntrySerializationTest.

2. Pull-down sheet interfered with page taps. The expanded top sheet is a
   full-width drawer with no way to dismiss except the grabber, so it sat over
   the page. Hoist its expanded state into EmbeddedTabLayer (reset per tab) and
   draw a full-area dismiss scrim behind the open sheet; collapsed, only the
   small grabber is interactive and page taps pass through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:32:44 +00:00
Claude ba2af75b4c feat: replace full-screen sandbox corner chip with top pull-down sheet
The full-screen browser/napplet-host activities still showed the old corner
pill/globe — which sits exactly where a site puts its own login avatar. Add a
native-View NappletControlSheet (the twin of the embedded tabs' Compose
TopControlSheet): a small grabber at the top edge that pulls down to the page's
controls, and wire both :napplet full-screen activities to it.

- NappletControlSheet: title row (shield/globe), optional Tor row, reload, and
  optional "what it can access". Tor supports an inline toggle (browser) or a
  tap-through to a confirm dialog (nSite host, where switching rebuilds the
  session). Tap or vertical drag to expand/collapse.
- NappletBrowserActivity / NappletHostActivity: drop buildFloatingChip/chipGlyph
  for buildControlSheet(); attach at Gravity.TOP, full width.
- Add short napplet_net_tor_label / napplet_net_open_label strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:10:29 +00:00
Claude af9e8b885d refactor: apply embedded-tab audit fixes (durability, perf, leaks, cleanup)
Durability & correctness:
- FavoriteAppsRegistry: tombstone removals made before async hydration so a
  just-deleted favorite can't be resurrected by the disk merge.
- BottomBarEntry: stable @SerialName discriminators so persisted bottom-bar
  configs survive class renames/moves.
- EmbeddedTabHost: guard reportBounds/setActiveChrome by active id so a
  cross-fading outgoing screen can't clobber the incoming tab's bounds/chrome.
- EmbeddedNappletController: replay a parked-before-bound pause after session
  create so a never-shown applet doesn't come up running.
- NappletBrowserService: bind the broker once (no leaked binding on re-create),
  destroy a stale WebView before rebuilding, and reload only after the async
  proxy override actually applies. NappletHostService: same WebView-reuse guard.
- NappletBrokerService: cap concurrent foreground leases so a misbehaving
  sandbox can't pin Tor/relays with unbounded arbitrary keys.

Perf:
- Screens publish a remembered EmbeddedTabChrome; host short-circuits identical
  publishes so the tab layer isn't recomposed every frame.
- AppBottomBar resolves favorites via an id-indexed map, not a per-entry scan.
- TopControlSheet keyed on the active tab so its expand state resets per tab.

Cleanup:
- Delete dead BrowserHostActivity + EmbeddedBrowserSurface + AppControlPuck and
  their manifest entry; drop the unused `ready` surface state; null controller
  refs on unbind; refresh stale z-order/host docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-24 00:05:34 +00:00
Claude 89a32e078f fix: audit batch 1 — drop diagnostics, browser foreground heartbeat, Tor host key
- Remove the shipped scroll/zoom diagnostics from NappletBrowserService (per-touch
  MotionEvent log and per-page zoom log).
- NappletBrowserActivity now renews its foreground lease on a 30s heartbeat like
  NappletHostActivity, so the broker's 90s watchdog can't reap it (tearing down
  Tor/relays) while the browser is genuinely foreground.
- Persist the per-host Tor choice against the host actually displayed (webView.url),
  not the start URL, so an in-page navigation doesn't save the choice to the wrong site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:51:18 +00:00
Claude 7d5f68d115 Merge remote-tracking branch 'origin/main' into claude/webview-menu-custom-url-ikrgbz 2026-06-23 23:41:38 +00:00
Claude 9ebe16d7a1 feat: top pull-down control sheet for embedded tabs
Now that the surface is z-ordered below the Compose layer (alpha17), chrome can
finally draw over the page — so replace the slim top bar with a top pull-down
sheet, per request. Collapsed it's just a small grabber centered at the top edge
(out of the top-right corner, where sites put their own avatar/menu); pull it
down or tap to reveal the page's controls: route over Tor, reload, "what it can
access" (sandboxed napplets/nsites), and open full screen.

The active tab publishes its controls as EmbeddedTabChrome; EmbeddedTabLayer
draws the TopControlSheet over the active tab's bounds, after the surfaces so it
sits on top. Applies to both embedded web and napplet/nsite tabs.

The full-screen activities still carry the native corner chip — converting those
to the same pull-down is the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:38:10 +00:00
Claude 90150e72e4 fix: upgrade privacysandbox.ui to alpha17 (embedded drag/scroll input)
The embedded surface forwarding taps but cancelling drags was a known bug in
androidx.privacysandbox.ui alpha10: with the provider surface z-ordered above,
"the gesture is exclusively received by the provider window and not transferred
to the client window" (alpha13 release notes). alpha15 then "set the default
Z-ordering to below" and "added support for the UI provider to receive
MotionEvents in this mode after being received by the client window" — i.e. the
drag-input path we needed.

Bump alpha10 → alpha17 and adapt the changed API: openSession takes SessionData
instead of a windowInputToken IBinder, Session adds notifySessionRendered, and
the session-state listener became setEventListener(SandboxedSdkViewEventListener)
(ready now flips on onUiDisplayed). The direct-WebView browser is unaffected
(it doesn't use this library).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 23:16:02 +00:00
Claude 1531f3baad feat: direct-WebView browser activity (scroll, zoom, keyboard)
Confirmed on a real device: the streamed SurfaceControlViewHost surface forwards
taps but drops scroll/zoom/keyboard gestures — a hard limitation of
androidx.privacysandbox.ui on current Android. No tweak fixes it.

Add NappletBrowserActivity: a full-screen browser that hosts the WebView
*directly* in its own window in the keyless :napplet process, so scrolling,
pinch-zoom, and the soft keyboard (windowSoftInputMode=adjustResize) all work
natively. It carries over NappletBrowserService's per-origin NIP-07 bridge and
Tor proxy, plus NappletHostActivity's trusted chip, loading screen, and
foreground hold — so it stays just as keyless (page JS runs in :napplet, every
window.nostr call is brokered + consent-gated per origin in the main process).

Web favorites and URL launches now open this activity instead of the streamed
BrowserHostActivity. Per-host Tor choice persists via a new MSG_SET_WEB_TOR
broker message (the :napplet process relays it to WebUrlNetworkRegistry).

The embedded bottom-row web tab still uses the streamed surface (it must, to
live inside MainActivity) and so still can't scroll — that's a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 22:46:31 +00:00
Claude b8786091dd fix: no black flash when switching between embedded tabs
Parked (inactive) warm tabs were shrunk to 1dp off-screen, so activating one
resized its surface from 1dp to full size — forcing the SurfaceControlViewHost to
re-render at the new size, which flashed black for ~1s (page appears → black →
reappears) on every tab switch.

Keep parked tabs at the SAME size as the active tab and only shift them
off-screen, so bringing one back is a pure translation: no resize, no re-render,
no black flash. They stay full-size and warm while parked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 22:25:44 +00:00