The "locked nApplet vs open nSite" distinction was a `websiteMode: Boolean`
threaded through an Intent extra and re-branched at eight independent sites
across the launcher, the content server, both host surfaces, and the chrome.
That is one coupled security posture (capabilities, CSP, NIP-07 injection,
off-origin policy, network UI) expressed as scattered, drift-prone flags — and
boolean-blind, since the "website" is actually the *more* capable mode.
Introduce `HostProfile` (NAPPLET | WEBSITE), resolved once in the trusted main
process and carried over the Intent/Messenger boundary as its name. Every
coupled consequence now reads from one place:
- `declaredCapabilities(requires)` — THE broker grant, minted into the token
- `appCsp` / `injectsNip07` / `allowsOffOrigin` / `exposesNetwork`
Pure mechanical mapping (every branch 1:1, no behavior change). The wire extra
`EXTRA_WEBSITE_MODE` boolean becomes `EXTRA_HOST_PROFILE` string — safe, as it
is in-app process-to-process IPC, never persisted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrxLCMcQADUPnm8Sj9ZJ63
The in-app "app" surfaces had colliding, sometimes inaccurate names. NIP-89
native apps showed as "Apps" while the in-app favorites showed as "Favorite
apps", and the single-app host screens were named `FavoriteWebAppScreen` /
`FavoriteNappletScreen` even though they open any url/coordinate (favorited or
not) and the "Napplet" one also renders nSites.
Establish one taxonomy:
- Native app store -> "App Store" (NIP-89), in-app Nostr web client -> "Web app",
plus the existing nApplet / nSite. "Favorite" is now only the star toggle and
the pinned grid, not a screen.
- Code axis: `WebApp` (url-based) and `NostrApp` (coordinate-based nSite/nApplet).
The cross-process sandbox infra (`napplet/`, `nappletHost/`) keeps "Napplet".
Renames:
- Routes `FavoriteWebApp`/`FavoriteNostrApp` -> `WebApp`/`NostrApp`
- Screens `FavoriteWebAppScreen`/`FavoriteNappletScreen` -> `WebAppScreen`/`NostrAppScreen`
- Controllers `EmbeddedBrowserController`/`EmbeddedNappletController`
-> `EmbeddedWebAppController`/`EmbeddedNostrAppController`
- Factory `acquireBrowser`/`acquireNapplet` -> `acquireWebApp`/`acquireNostrApp`
- Model `FavoriteApp.WebUrl` -> `FavoriteApp.WebApp`; `WebUrlNetworkRegistry`
-> `WebAppNetworkRegistry`
- Strings: `software_apps`/`route_software_apps` "Apps" -> "App Store";
`favorite_apps_empty` reworded to name web app / nApplet / nSite
Persistence is untouched: favorite id prefixes ("url:"/"nostr:"), DataStore
names ("favorite_apps"/"weburl_network"), and serialized type tags ("url"/
"nostr") are all kept stable. Taxonomy documented in
amethyst/plans/2026-06-25-web-app-naming.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrxLCMcQADUPnm8Sj9ZJ63
- TopControlSheet: replace SheetItem with SheetSwitchItem for the
console row, adding a Switch that reflects the current
consoleShowing state; add consoleShowing param and wire it from
EmbeddedTabLayer
- BottomConsoleSheet: move the grabber Column before AnimatedVisibility
so it sits at the top of the expanded panel (standard bottom-sheet
handle behaviour) and animates upward as the console opens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GxQdArv6h6SezciUWeoR38
Reorder Account Settings: group network (relays/sync/follows, media/nests),
interactions (reactions/zaps), media playback, identity (DVMs/badges/payments),
then safety/i18n.
Reorder App Settings: group UI (privacy/prefs), composition, personalization
(home/reactions/bottom-bar/profile), then advanced (calendar/OTS/namecoin).
Also drop the now-redundant Build.VERSION_CODES.R gate on BROWSER in
DrawerNavigateItems, matching the fix landed in main (PR #3361).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdbTQuhuk2g5XGWEqmKLRz
Move BROWSER to the Navigate section (between VIDEO and DISCOVER).
Reorganise Feeds into logical clusters: content types first, then
live/social, communities/chats, calendar, apps/tools, packs, misc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdbTQuhuk2g5XGWEqmKLRz
The Browser tab is a launcher: each opened site loads full-screen in its
own direct-WebView NappletBrowserActivity, which has no API-30 dependency
(it never uses SurfaceControlViewHost and only feature-detects WebView
capabilities). The API-30 gate genuinely belongs to the *embedded*
favorite-app tabs (NappletBrowserService → privacy-sandbox surface), so
keep FAVORITE_APPS gated and remove the gate from BROWSER:
- BROWSER is now unconditional in the default bottom bar and the drawer.
- BrowserScreen no longer shows the "unsupported" fallback below API 30.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRdmDri69mLJiHpJQpJU73
Resolved conflicts from main adding isFavorite to NappletBrowserActivity.intent()
and FavoriteAppLauncher.launchUrl() while our branch added the theme parameter.
Both params are now present together.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198rKcuv32DEoUPpLqsBYbx
Move java.* imports after kotlinx.* imports in MainActivity and
BouncingIntentNav per ktlint ordering rules. Replace fully-qualified
Route reference in PreviewUrl with an import.
Adds WebSettingsCompat.setAlgorithmicDarkeningAllowed(true) to all four
WebView setup functions (NappletHostActivity, NappletHostService,
NappletBrowserActivity, NappletBrowserService) so web pages that do not
implement prefers-color-scheme are algorithmically darkened when the
process is in night mode, matching the user's chosen theme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198rKcuv32DEoUPpLqsBYbx
Instead of passing the raw ThemeType name ("SYSTEM") to the :napplet process,
resolve it to the actual dark/light value in the main process by reading
context.resources.configuration.uiMode before the launch. The napplet process
now always receives "DARK" or "LIGHT" and sets UiModeManager.nightMode
unconditionally, making prefers-color-scheme reliable on all ThemeType choices.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198rKcuv32DEoUPpLqsBYbx
Animated GIF/AVIF media in the multi-image gallery rendered with its own
intrinsic aspect ratio via fillMaxWidth(), ignoring the ContentScale.Crop
the grid asks for
Notification small icons are rendered by the system as flat, alpha-only
silhouettes, so the always-on relay-connection service showed the exact
same solid gem as real notifications (DMs, zaps, mentions). The persistent
service therefore kept reading as a fresh notification.
Add an outlined (hollow) gem variant, `amethyst_service`, and point the
foreground service notification at it so it's clearly distinguishable at a
glance while staying on-brand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141R1Km7YsA2miFoxLkXLQk
The Text() calls inside the logs.forEach loop were 8 lambdas deep
(Box > Column > AnimatedVisibility > Surface > Column > Column > forEach > Row),
which caused java.lang.OutOfMemoryError in the Kotlin bytecode optimizer
(ConstantConditionEliminationMethodTransformer). This broke CI when
PR #3353 merged. Extract the Row content into a top-level ConsoleLogRow
composable to reduce nesting depth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmiRNcMEync2rVVbrPzWt
Change the default bottom navigation for new users from
Home/Messages/Shorts/Discover/Favorite Algo Feeds/Notifications to
Home/Messages/Wallet/Browser/Notifications. The Browser entry is gated
to API 30+ since it renders a cross-process surface, matching the
drawer's existing gating.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRdmDri69mLJiHpJQpJU73
The :napplet sandboxed process does not share memory with the main process,
so UiModeManager night mode set in the main app does not propagate there.
Pass the user's ThemeType name ("DARK"/"LIGHT"/"SYSTEM") via Intent extras
and Messenger IPC bundle keys, then apply UiModeManager.nightMode in each
receiving surface (NappletHostActivity, NappletBrowserActivity,
NappletHostService, NappletBrowserService) before any views or WebViews are
created. This makes prefers-color-scheme and the activity chrome match the
user's chosen theme in all napplet/nsite/web-app surfaces.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198rKcuv32DEoUPpLqsBYbx
Remove the star icon from the browser URL bar (OmniBar) and add a
favorite toggle row to both pull-down sheet surfaces instead:
- Compose TopControlSheet (embedded web tabs and napplets): shows
filled/outline star with "Add to favorites" / "Remove from favorites"
sourced from FavoriteAppsRegistry; isFavorite state flows reactively
through EmbeddedTabChrome so the label updates without reopening.
- Native NappletControlSheet (full-screen NappletBrowserActivity): same
toggle backed by a new MSG_TOGGLE_WEB_FAVORITE IPC message handled in
NappletBrokerService; initial state is passed via intent so the star
opens in the correct filled/outline state for the launch URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBpBE7bQJ2JRni6sYG8UDo
The deeply-nested Text() call inside DropdownMenu > forEach > DropdownMenuItem
generated bytecode with MAXSTACK=26, which caused java.lang.OutOfMemoryError in
the JVM bytecode frame analyzer (ConstantConditionEliminationMethodTransformer).
Extracting DropdownMenuItem into a top-level private composable breaks the lambda
nesting depth and lets the compiler succeed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmiRNcMEync2rVVbrPzWt
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
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
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
- Show source filename and line number as dimmed secondary text on each
log entry (matches native NappletConsolePanel behaviour)
- Replace hardcoded #FF9800 orange with a dark-mode-aware amber pair:
#E65100 in light mode, #FFB74D in dark mode
- Cap panel height at 40% of screen height (was fixed 240dp) so large
phones display more log lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QXpdfwj2cujnEa7HPzQXj
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
Captures JavaScript console output (log/warn/error/debug) from embedded
WebViews and surfaces it via a bottom pull-up sheet, triggered by a new
"Console (N)" row in the existing top pull-down control sheets.
Embedded browser (Compose):
- NappletBrowserService: attaches WebChromeClient to intercept console
messages and forwards them to the client process via new MSG_CONSOLE_LOG
IPC message in NappletBrowserContract
- EmbeddedBrowserController: implements new ConsoleBridge interface,
stores up to 200 entries in a SnapshotStateList observable by Compose,
handles the incoming IPC message
- TopControlSheet: adds optional consoleCount/onConsole params; shows a
"Console (N)" row when onConsole is provided
- BottomConsoleSheet: new Compose pull-up panel anchored at the bottom,
with level-coloured monospace log entries and a Clear button
- EmbeddedTabLayer: wires ConsoleBridge → TopControlSheet → BottomConsoleSheet
Full-screen activity browser (native Views):
- NappletBrowserActivity: attaches WebChromeClient, wires NappletConsolePanel
and updates the control sheet count label on each new entry
- NappletControlSheet: adds optional onConsole callback and updateConsoleCount()
- NappletConsolePanel: new native-View bottom pull-up panel with scrollable
log entries, grab-to-open gesture, and Clear button; capped at 200 entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QXpdfwj2cujnEa7HPzQXj
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
- 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
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>