Pressing Back on the "Connect to Nostr" first-connect dialog resolves to
AppConnectResult.Cancelled, which added the app's coordinate to an in-memory
`sessionCancelled` set. That set suppressed every future connect prompt for the
entire broker lifetime, so a later request — e.g. the user re-clicking "login"
in the in-app browser — was silently denied and the dialog never reappeared.
Because a Cancel persists nothing, the app also never showed up in Connected
Apps, leaving the user with nothing to clear to recover.
Replace the permanent suppression with a short, self-clearing cooldown
(`cancelledUntil` map): a Cancel suppresses re-prompts only briefly so the
burst of requests a page/napplet fires on load doesn't relaunch the dialog per
request, while a deliberate retry seconds later prompts again. The clock is
injectable for deterministic tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDcts4HzwSff4fy6oSVA6D
- Top-bar repo bookmark toggle now switches between BookmarkAdd (with +)
and Bookmark glyphs instead of two identical glyphs, so the icon
visibly changes shape (not just tint) when starred/unstarred.
- Add a thin HorizontalDivider after the ReactionsRow on the repo home.
- Code browser: opening/closing a file or changing folders swaps the
scrollable in place, landing the new view at the top with no scroll
delta, which left the disappearing top bar stranded at its hidden
offset over a blank band. Expose the scaffold bar state via
LocalDisappearingBarState and reset it to visible on each in-place view
change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The commonMain GitRepositoryListState used Dispatchers.IO without importing
the multiplatform kotlinx.coroutines.IO extension, so it resolved to the
JVM-only member and broke the iOS native compile
(:commons:compileKotlinIosSimulatorArm64). Matches BookmarkListState.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Snapshot cache: a process-wide GitRepoSnapshotCache keyed by repo address.
The browser ViewModel serves an already-fetched default-branch snapshot
synchronously, so the stats render in share-to-image and don't re-fetch when
switching screens.
- Issues/PR screens: filter chips now live inside the disappearing top bar
(via the scaffold's belowBar slot) so they hide with it instead of leaving a
static black band; the feed uses normal content padding.
- New issue is now an extended FAB on the Issues screen.
- Top bar shows the repo description as a single-line subtitle under the name;
removed the duplicate description from the home body.
- Tighter spacing: home sections, code header rows (branch row / search /
breadcrumb).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The KMP lifecycle-viewmodel artifact used by commons doesn't expose the
create(Class<T>) ViewModelProvider.Factory override (only the desktop/JVM
target hit this), so the factory now lives in amethyst alongside the
viewModel() call, mirroring the NestViewModel pattern. The commons
ViewModel keeps only platform-agnostic state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Relocates the two app-agnostic pieces of the NIP-34 code browser into commons
so the desktop front end can reuse them verbatim:
- GitRepositoryBrowserViewModel (+ GitBrowseState) → commons jvmAndroid
nip34Git package. Pure StateFlow ViewModel over quartz's GitHttpClient;
no Android/AccountViewModel/INav dependency.
- CodeHighlighter → commons commonMain nip34Git/ui. Pulls the Apache-2.0
dev.snipme:highlights dependency into commons commonMain.
Amethyst composables now import both from commons. The screen-level
composables stay app-side, matching the commons convention that shared UI
never takes AccountViewModel/INav.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Adds NIP-51 (kind 10018) repository bookmarking with a star toggle in the
git repository top bar, backed by a new GitRepositoryListState in commons.
Removal rebuilds the public tag set and re-signs so encrypted private
bookmarks are preserved without decryption.
Adds a label-filter chip row and open/closed item counts to the Issues and
Patches & PRs status feeds. The active feed's distinct labels drive the
chips; selecting one filters the rendered list, and a stale selection is
dropped when switching status.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Render the repository README in the first tab and add a Code tab that
browses the repo's file tree and renders source files (syntax-highlighted),
reading directly from the NIP-34 clone URL over the git smart-HTTP v2
protocol (works with GRASP/ngit bare servers as well as GitHub/GitLab).
quartz (jvmAndroid): a from-scratch git smart-HTTP v2 client — pkt-line
codec, packfile parser with OFS/REF delta resolution and SHA-1 oids,
tree/commit parsers, and a high-level browser that fetches a shallow
filter=blob:none snapshot (one request for the whole tree) and lazily
pulls file blobs on demand. Offline tests run against real captured
GitHub wire bytes plus a git-generated OFS-delta pack.
amethyst: README tab (rich markdown), Code tab (folders-first browser
with breadcrumb navigation + a file viewer that renders markdown or
syntax-highlighted source), a browser ViewModel, new UI strings, and a
Folder material symbol (font subset regenerated). Syntax highlighting
uses dev.snipme:highlights (Apache-2.0, permissive).
Tabs are now: README, Code, Overview, Issues, Patches & PRs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Extract resolveNappletMeta() to NappletManifestLookup.kt, replacing
three private copies of the same manifest lookup across
ConnectedAppsScreen, ConnectedAppDetailScreen, NappletPermissionsScreen,
and NappletSignerConsentActivity.
- Extract PolicyCard composable to PolicyCard.kt, shared between
ConnectedAppDetailScreen and RelayAuthSettingsScreen (was duplicated).
- Extract NappletCapability.symbol() to NappletCapabilityExt.kt, shared
between ConnectedAppDetailScreen and NappletPermissionsScreen.
- Drop what-comments on kind 1/6/7 lines in NostrSignerPermissionLedger.
- Reword TrustedRelayListState stateIn comment to note private-tag absence
on first boot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
- Cancel on first-connect dialog now suppresses re-prompting for the
rest of the session (sessionCancelled set under signerConsentLock)
instead of showing the dialog on every subsequent request.
- PARANOID signer policy no longer silently bulk-grants ALLOW_ALWAYS
for all capabilities; the capability ledger is left empty so each
capability prompts individually, matching user intent.
- NostrSignerOp.Decrypt default changed from ALLOW → ASK in
reasonableDecision(); the branch is currently unreachable
(toSignerOp() never produces Decrypt) but ASK is the safer default
if a decrypt request type is added in future.
- TrustedRelayListState seeds its StateFlow from the synchronously
available cached relay set, eliminating a startup window where
IF_IN_MY_LIST incorrectly denied auth to relays in the user's list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Remove the signer self-gating bypass that allowed external (Amber/NIP-55)
and remote (NIP-46) signers to skip Amethyst's per-napplet consent UI.
All signer types now go through Amethyst's consent dialogs first; the
external signer then adds its own approval on top (double-prompting).
This lets users differentiate signing requests by app inside the external
signer, since Amethyst itself is the requesting app.
Also expand the REASONABLE policy to auto-approve Encrypt and Decrypt
operations, matching the intent that common/private-key operations that
apps routinely need are pre-approved at the "reasonable" trust level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
- Add AllowForSession and AllowUntil(expiresAt) signer grant types so users
can grant temporary access (session, 24h, 30d) from the consent dialog
- Track per-app lastUsed timestamp in NostrSignerPermissionStore and update
it on every granted signing operation
- Auto-expire timed grants: decide() clears expired op decisions before
returning, so no background sweep is needed
- Add NappletBroker.sessionAllows in-memory set for session grants (cleared
on broker destroy, never persisted)
- Implement full NIP-42 relay auth policy system:
- RelayAuthPolicy enum (ALWAYS / NEVER / IF_IN_MY_LIST) stored in
AccountSettings and persisted in LocalPreferences
- RelayAuthDecision (ALLOW / DENY) per-relay overrides in DataStore
- RelayAuthPermissionLedger combining global policy + per-relay overrides
- DataStoreRelayAuthPermissionStore writing to relay_auth.preferences_pb
- Wire relay auth into AuthCoordinator: subscribeLedger/unsubscribeLedger
lets each logged-in account contribute its own policy; signWithAllLoggedInUsers
now receives the relay URL so it can check the ledger before signing
- Update RelayAuthenticator (quartz) to pass relay URL in the signing lambda
- Add RelayAuthSubscription composable that subscribes both the account and
its ledger when a screen is active
- Add RelayAuthSettingsScreen: global policy radio picker + per-relay
override list with toggle and remove; reachable from Settings
- Add Route.RelayAuthSettings, AppNavigation wiring, and SettingsCatalog entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Implements per-app permission management for the internal nsec signer when
webapps/napplets/nsites connect via Amethyst's built-in key:
- Three trust levels on first connect (FULL_TRUST, REASONABLE, PARANOID)
with a UI dialog (NappletConnectActivity) matching the design spec
- Per-operation consent dialogs (NappletSignerConsentActivity) for
sign-kind/encrypt/decrypt with Allow once, Don't ask again, Deny options
- Per-app DataStore storage (DataStoreNostrSignerPermissionStore) using
SHA-256-hashed filenames so 1000s of apps don't bloat a single file
- NostrSignerPermissionLedger applies policy decisions: REASONABLE
auto-allows kinds 1/6/7; FULL_TRUST auto-allows all non-payment ops
- NappletBroker extended with first-connect gate and per-op signer gate,
serialized by a dedicated signerConsentLock mutex
- Permission management screen (NappletSignerPermissionsScreen) to review
and revoke stored per-app signer permissions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
Render the browser "Discover" section as full-width rows (icon + name +
one-line description) instead of bare icon cells, matching the Recent
row layout which already carries a subtitle. Each suggestion now has a
short curated description (trimmed from the app's own meta description)
so unfamiliar apps explain themselves; tapping a row opens the app, and
a trailing star pins it to favorites.
Auto-pulling page <title>/description was rejected: many of these apps
are client-rendered SPAs that serve an empty <title>, and several titles
are long marketing strings — curated short names read better in the list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Grow the browser "Discover web apps" list to the full set of
browser-openable Nostr web apps from the nostrapps.com directory plus
several requested additions, and give each entry its own logo.
- Each suggestion now carries iconUrl set to the app's own declared
apple-touch-icon / icon (PNG or SVG, individually verified to return an
image), so the grid matches the favicon look of Favorites/Recent
without any third-party favicon service. Apps whose only icon is an ICO
(Coil has no ICO decoder) or that couldn't be resolved stay icon-less
and fall back to the globe glyph until their favicon is captured on
first visit.
- Added: nymchat, nostr.build, nostrcheck, zap.cooking, x21, divine.video,
brainstorm, zappix, plektos, zaptrax, zaplytics, podstr, ghostr, mutable,
metadata, plebsvszombies, blobbi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Add a "Discover web apps" section to the browser launcher home, shown
after the Recent block, with a hardcoded list of popular Nostr web apps
drawn from the nostrapps.com directory. Gives new users (whose Favorites
and Recent are empty) somewhere to start instead of a bare empty screen.
- New DefaultWebClients in commons (URL + label entries), grouped by
category; extensions/signer-only tools are excluded and every URL is a
confirmed canonical domain. No remote icons are loaded on the idle
screen — favicons are captured the normal way once a site is opened.
- Render the list via a new suggestedAppItems grid (long-press offers
"Add to favorites"); already-favorited apps are filtered out.
- FavoriteAppCell now takes a menu slot so the favorites grid and the
suggestions grid can offer different long-press actions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151Uczec41LhTogxkgoAhKa
Continues moving genuinely platform-agnostic leaves out of the :amethyst app
module so they compile once in :commons instead of across all six app variants.
Moved (no Android coupling, no foundation deps):
- ui/layouts/DisappearingBarState, DisappearingBarNestedScroll, PaddingMerge
-> commons commonMain (com.vitorpamplona.amethyst.commons.ui.layouts)
- ui/components/UrlPreviewState
-> commons jvmAndroid (it references commons.preview.UrlInfoItem, which
lives in the jvmAndroid source set)
Consumers (incl. the existing DisappearingBar*Test unit tests, which stay in
:amethyst and now import from commons) updated to the new packages. No behavior
change.
Verified: :commons, :amethyst compilePlayDebugKotlin + compilePlayDebugUnitTest,
and :desktopApp:compileKotlin build clean; spotless applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
First Tier A slice of the UI/components extraction. Moves the genuinely
platform-agnostic leaf composables — those with zero :amethyst
dependencies and no Android coupling — from the app module into the
shared :commons KMP module (commonMain):
ClickableTexts, ForwardingPainter, GenericLoadable, GlowingCard,
LoadingAnimation, TranslationConfig, ZonedSwipeModifier (~616 LOC)
These now live under com.vitorpamplona.amethyst.commons.ui.components and
compile once in :commons (cacheable, incremental) instead of being part
of every one of the six :amethyst variant compilations (play/fdroid ×
debug/release/benchmark). That shrinks the app-module Kotlin compilation
unit — the root cause of the CI Kotlin-daemon OOM — rather than renting
headroom with heap flags.
Consumers updated to import from the new package; no behavior change.
Verified: :commons, :amethyst compile{Play,Fdroid}DebugKotlin, and
:desktopApp:compileKotlin all build clean; spotless applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
The consolidation passed `useProxy = true` for every brokered `resource.bytes`
fetch, forcing them through Tor whenever Tor was active — regardless of the
napplet/nSite's actual network mode. That overrides the user's explicit choice:
an nSite running in "open web" mode would still have its blob fetches tunneled,
inconsistent with how its own WebView page loads.
The authoritative per-applet preference already exists main-side in
NappletNetworkRegistry.useTor(coordinate) (locked napplets pinned to Tor;
nSites follow the persisted per-site toggle, which relaunches on change) — the
same source NappletLauncher reads to set the WebView proxy. Thread the calling
applet's coordinate through NappletResourceGateway.fetch so the broker can
resolve it, and pick the shared client with
getHttpClient(useProxy = NappletNetworkRegistry.useTor(coordinate)). This
mirrors the host's own `effectiveProxy = if (useTor) proxyPort else -1` exactly,
so a brokered fetch now routes like the applet's page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The browser/napplet strings (browser_address_hint, browser_console_title,
browser_console_title_short, browser_console_clear, napplet_untitled) were
moved to :commons, but their per-locale translations were left behind in
amethyst's values-*/strings.xml. With the default keys gone from amethyst,
lint flagged them as ExtraTranslation (80 errors across 16 locales).
Move the translations into commons/src/androidMain/res/values-*/strings.xml
so the default key and its translations live in the same module, preserving
the existing translation work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uza7sGxYPZtY43Ln2yH8FQ
The Cashu "Add wallet" screen had become a mint manager, and its Create
path published a fresh kind:17375 that could clobber a portable NIP-60
wallet the user already owned in another client (kind:17375 is
replaceable).
When no wallet is loaded, drive the user into a new find-or-create wizard
that crawls every relay (modeled on the Event Sync tool) for the user's
existing kind:17375 wallets and branches on the result:
- 0 wallets: offer to create a new one.
- 1 wallet: verify + balance-probe it, adopt as the main wallet and
rebroadcast to outbox so it's easy to find next time.
- >1 wallets: the newest becomes the main wallet; older/duplicate wallets
are verified, their recoverable balances probed via NUT-09/NUT-07, and
the user gets one-tap "Recover funds to main wallet" per old wallet.
The mint manager (AddCashuWalletScreen) is now reachable only from Cashu
Wallet Settings, once a wallet exists; the no-wallet and "add wallet"
picker paths route through the wizard instead.
Implementation:
- Split CashuWalletOps.restoreFromMint into scanRecoverableProofs (no
publish, for the balance probe) + publishRecoveredProofs; foreign-seed
recovery never bumps the main wallet's NUT-13 counter.
- New CashuWalletDiscovery crawler reuses fetchAllPages + a fresh
NostrClient so crawled events don't pollute LocalCache.
- CashuWalletState gains decrypt/probe/recover/adopt helpers.
- Extract AccountViewModel's relay-crawl closures (crawlRelayDb,
buildCrawlClient) so Event Sync and the wizard share them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
NIP-34 pull requests (kind 1618) and pull-request updates (kind 1619)
previously had no renderer and fell through to the plain text-note
branch, so a PR notification opened onto an unstyled markdown blob with
none of its structured data shown. Add dedicated cards that reuse the
existing patch/issue card vocabulary (bordered container, type chip,
status pill, embedded repository header) and surface the PR-specific
metadata the event carries:
- Pull Request card: "Pull Request" chip with a merge glyph, status
pill, subject title, branch name, current commit, merge base, and
clone-URL download rows.
- PR Update card: "PR Update" chip, repository header, new commit /
merge base, clone URLs, and an explanatory line (updates carry no
body content).
While here, modernize the existing cards consistently:
- Factor the shared markdown body, metadata row, and subject title into
reusable composables (GitMarkdownBody / GitMetaRow / GitSubjectTitle).
- Show the issue subject as a proper title. The old code cast the event
to TextNoteEvent to read the subject, which always returned null
(GitIssueEvent is not a TextNoteEvent), so issue subjects were never
displayed; read it from GitIssueEvent.subject() instead.
- Render the patch commit as an iconed metadata row.
Wire the two new kinds into NoteCompose and the thread detail view, add
the CallMerge / Commit / AltRoute Material symbols (font subset
regenerated), and add the new string resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
Completes parity #5. `fieldExtend` kept a pure word-snap, so the in-field
start/end handles couldn't be fine-tuned to a single character. Now it keeps
per-drag state (reset on a >250ms gap or edge switch) and matches native
`Editor` word-selection drags: the gesture baselines at the current selection
edge, sweeping past that word's far boundary snaps to the next whole word
(never stopping mid-gap), and moving within / back from the furthest-reached
word gives character precision. Symmetric for both handles. Page-text extend
stays character-granular.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds out host-drawn text selection for embedded napplet/nsite/browser
surfaces toward native parity, and fixes the bugs found while exercising it.
- Magnifier loupe (#4): EmbeddedMagnifier + provider-side pixel capture
(EmbeddedMagnifierProbe) shipped over IPC for both embed paths; the
caret/selection handles drive it via OnMagnify.
- SelectionUiState: single source of truth for the overlay show/hide rules
(insertion caret / in-field range / page-text range + dragging/scrolling).
- EmbeddedSelectionDrag: suspends the nav drawer's edge swipe while a handle
is dragged (auto-scroll #9).
Bug fixes:
- No more overlay blink on word-select: the shim's selection-reveal scrolls
(a textarea auto-scrolling to show a forming/re-asserted range) no longer
trip the hide-on-scroll path, and the hide self-heals instead of being
re-armed indefinitely.
- RemoteImeView debounces the range-lost signal so a transient collapse that
gets re-asserted doesn't flicker the handles/toolbar.
- Focusing a field clears any page-text selection (shim + host), so the stale
page handles/Copy bar no longer linger above — and stop stealing drags from —
the field overlays; also cancels any in-flight scroll-hide on focus.
- Caret insertion-handle drag now actually moves the caret: read the pointer
delta with positionChangeIgnoreConsumed() before consuming, so the value
isn't zeroed by our own consume (or the sandbox surface consuming the move).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Embedded WebView surfaces (:napplet process, SurfaceControlViewHost) can't
host the soft keyboard or present Chrome's own selection UI, so editing and
selection are relayed to the main process. This lands the working set of that
relay:
- shim.js: fix React-controlled input erase by writing through the native
HTMLInputElement/HTMLTextAreaElement value setter (so React's value tracker
stays in sync); make the host authoritative for selection re-assert (the
editable selectionchange handler only mirrors); report field/caret geometry
and page-text selection geometry; add pageExtend + caret coords (border-width
corrected) for drag-to-extend and the insertion handle.
- RemoteImeView: land caret where tapped on focus (requestFocus before applying
remote state); host-authoritative selection re-assert within a time window;
setText only when text actually changed; wire copy/cut/paste/select-all and
edit callbacks.
- EmbeddedTabLayer: stop resizing the surface on IME show (removes the ~1s
first-letter freeze); draw the selection overlay — toolbar, teardrop
selection handles, and the insertion (cursor) handle.
- EmbeddedImeBridge / Embedded{Napplet,Browser}Controller: carry selection +
caret geometry and the page-selection event across the Messenger channel.
Known limitation (not fixed here): after a field's page is opened in its own
full-screen activity and the user returns, the selection-highlight paint stays
off across all embedded surfaces. DOM selection, the toolbar, and copy still
work — only the native highlight is gone. This is a WebView/Chromium behavior
in off-window surfaces and is not reachable from the app layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AGP 9.2.1 treats resources defined in both an app module and a library module
with the same key (qualifiers="") as an error in non-debug builds.
`browser_address_hint`, `browser_console_title_short`, `browser_console_title`,
`browser_console_clear`, and `napplet_untitled` were defined in both
`:amethyst/values/strings.xml` and `:nappletHost/values/strings.xml`.
Both `:amethyst` and `:nappletHost` depend on `:commons`, so the canonical
home for these shared strings is `commons/src/androidMain/res/values/strings.xml`.
Update callers in both modules to use `com.vitorpamplona.amethyst.commons.R as
CommonsR`. Locale translations in amethyst's `values-*/` directories remain as
Android resource overlays (app module overrides library module at merge time).
Fixes: Found item String/browser_console_clear more than one time (packageFdroidBenchmarkResources)
The cashew + pixel-shades mark was visually weighted to the right of the
24x24 viewport. Shift every x-coordinate in both paths (the stroked cashew
body outline and the filled pixel sunglasses) by -2.4 units so the drawing
sits balanced within the icon bounds. Shape is unchanged; only position.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEFq9cUPECuB8F56sTtWXu
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
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
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>
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
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
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
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
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
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
Two bottom-nav polish items:
- No label on favorite tabs — they now match the built-in items, which are
icon-only.
- Use the app's own icon. FavoriteApp gains an optional iconUrl (the
nsite/napplet manifest icon, captured when you favorite from its card and
persisted). A shared FavoriteAppIcon renders that icon, falling back to a
type glyph (the napplet/nsite mark, or the globe for a plain URL) when
there's no icon or it fails to load. Used in both the bottom bar and the
Favorite Apps grid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN