Commit Graph
943 Commits
Author SHA1 Message Date
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 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 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 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
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 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 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 baf5c542a0 feat: favorite tabs use the app's own icon and drop the bottom-bar label
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
2026-06-23 19:34:50 +00:00
Claude 55bc75512a feat(nip46): support optional client metadata in connect request
Implements nostr-protocol/nips#2381: a client MAY attach an optional
4th positional parameter to the NIP-46 `connect` request carrying a
JSON-stringified `{name, url, image}` object, mirroring the fields
already present in `nostrconnect://` URIs. This lets a bunker:// paired
signer show who is asking to connect.

- quartz: add BunkerClientMetadata and a clientMetadata field on
  BunkerRequestConnect; serialize it as the 4th param (omitted when
  empty) and parse it back, degrading malformed/empty JSON to null.
- quartz: NostrSignerRemote carries and sends clientMetadata on
  connect() and threads it through fromBunkerUri().
- commons: BunkerLoginUseCase.execute() accepts optional clientMetadata.
- desktopApp: advertise Amethyst's metadata on bunker login.
- cli: the receiving bunker logs the connecting client's identity
  (display-only; never gates the ACK on it, since the client pubkey is
  unauthenticated in bunker:// pairing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYpupiVAq4VyHDdjrYyPdi
2026-06-23 16:17:19 +00:00
Claude c21b592cb1 feat: favorite web apps — launcher, full-screen host, and bottom-bar grid
Introduces a device-local "favorite apps" system that unifies nsites,
napplets, and arbitrary web clients behind one model and three
presentation shells, while removing the editable address bar from any
running app (it now lives only in the browser launcher).

Core spine:
- FavoriteApp (commons): a sealed model with two cases — NostrApp
  (nsite/napplet, keyed by addressable coordinate so it survives code
  updates) and WebUrl. napplet-vs-nsite is recomputed from the live
  event at launch, never stored.
- FavoriteAppsRegistry (amethyst): device-local, DataStore-backed,
  StateFlow source of truth; main process only, hydrated at app start.
- FavoriteAppLauncher: dispatches a favorite to its one launch path —
  full-screen BrowserHostActivity for a URL, sandboxed NappletLauncher
  (re-resolved from LocalCache) for an nsite/napplet.

Presentation:
- BrowserHostActivity: full-screen, single-app host in the main process
  that embeds the keyless :napplet browser surface. Its own task/recents
  entry (documentLaunchMode=intoExisting), no editable URL — locked to
  the app it opened with, keeping one NIP-07 trust context per instance.
- BrowserScreen is now a launcher: an omnibox that opens each URL in its
  own host activity, plus the shared favorites grid. The address bar is
  gone from the content surface.
- FavoriteAppsScreen + FAVORITE_APPS bottom-bar item: a grid of big
  launch buttons, reused inside the browser launcher.
- StaticWebsiteCard gains a header-actions slot; a star toggle on each
  nsite/napplet card pins it (strings/store stay in the app layer).

EmbeddedBrowserSurface extracts the chrome-free surface + controller
helper so the tab, the launcher, and the host activity share one piece
of cross-process glue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 14:30:49 +00:00
Claude d63bf87d2a feat: add in-app browser tab rendered from the keyless napplet sandbox
Adds a "Browser" navigation destination (drawer + pinnable bottom-nav item,
API 30+) that opens any URL. The page renders in the sandboxed, keyless
`:napplet` process and is streamed into the main activity as a cross-process
surface via androidx.privacysandbox.ui (SurfaceControlViewHost) — only pixels
and input cross the boundary, never the WebView's JS context or the NIP-07
bridge. The trusted address bar is drawn by the main process around the
embedded surface, so the sandbox can never spoof the URL.

NIP-07 `window.nostr` is injected the same way nSite website mode does it, but
scoped per visited origin: each origin gets its own broker-minted launch token
(keyed by the trusted source origin), so a grant to one site never leaks to
another.

- NappletBrowserService (`:napplet`): hosts the live-URL WebView, exposes it as
  a SandboxedUiAdapter, and relays the per-origin NIP-07 bridge to the broker.
- NappletBrowserUiAdapter: wraps the WebView session for privacysandbox.ui.
- NappletBrokerService: mints a per-origin synthetic identity so NIP-07 consent
  is scoped per host.
- EmbeddedBrowserController + BrowserScreen: bind the service, render the
  SandboxedSdkView, and drive the trusted address bar (navigate/reload/back/Tor).
- shim.js: a direct-bridge transport so the injected shim works in a top-level
  page that has no trusted shell parent.
- Browser nav item hidden below API 30 (SurfaceControlViewHost requirement).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
2026-06-23 03:39:06 +00:00
Claude 2b80bf49e3 feat(napplet): broker the NAP inc topic bus
Adds the `inc` pub/sub bus so napplets that declare it boot and can exchange
topic events: `inc.subscribe {topic}` / `inc.unsubscribe {topic}` register
interest, `inc.emit {topic, payload}` fans out an `inc.event {topic, payload,
sender}` to OTHER subscribed napplet sessions (never echoing the sender) — the
kehto runtime's inc contract.

- Router edge ops (gated on the INC declaration alone, like identity.watch —
  no per-call consent): SubscribeInc/UnsubscribeInc/EmitInc outcomes.
- Protocol: readTopic/readPayloadRaw + encodeIncEvent.
- NappletIncBus in the broker service routes across the live napplet sessions
  (the one service every sandbox binds), keyed by reply Messenger.
- Tests for inc routing + declaration gating; updated capability/router tests
  that asserted the old "inc/theme/notify are unknown" behavior.

NOTE: napplets run foreground-only/one-at-a-time, so cross-napplet delivery is
usually a no-op in practice; the bus is correct if sessions ever overlap. It is
app-wide (not author-scoped) — a future refinement could namespace topics by
author. Unblocks feed/profile-viewer/chat/bot. See the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-23 00:37:44 +00:00
Claude 1599f34308 feat(napplet): broker the NAP notify domain
Napplets can now create/list/dismiss user-facing notifications:
`notify.create { title, body }` → `notify.created { id }` (the bespoke
past-tense reply the client listens for, not the generic `.result`),
`notify.list` → `notify.listed { notifications }`, `notify.dismiss` fire-and-forget.

- NotifyCreate/NotifyList/NotifyDismiss requests, NotifyCreated/NotifyListed
  responses, NappletNotifyGateway + NappletNotification; broker executes them
  (consent-gated, ask-once).
- Protocol encodes the past-tense reply types.
- Android: NappletNotificationStore (per-coordinate, main-process, survives
  broker rebuilds — a napplet only ever sees/dismisses its own) + a best-effort
  system-tray notification.
- Consent summary + string for notify.

Unblocks the toaster demo. See the plan doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-23 00:30:42 +00:00
Claude 4b8b2ac513 feat(napplet): broker the NAP theme domain
Napplets can now read the host's current theme via `theme.get` →
`theme.get.result { theme: { colors: { background, text, primary } } }`,
mapping it to their CSS variables. This is the universal boot gate for
real-world napplets (e.g. kehto/web's demos all `requires: theme` and abort
if shell.supports('theme') is false).

- NappletCapability gains THEME (+ NOTIFY/INC, wired in following commits);
  adds requiresConsent (false for SHELL/THEME — cosmetic/negotiation never prompt).
- ThemeGet request, Theme response, NappletThemeGateway; broker executes it
  with no consent prompt.
- Android gateway returns Amethyst's brand purple with a dark/light bg+text pair.
- Capability label/description/icon + strings for theme/notify/inc.

See amethyst/plans/2026-06-23-napplet-nap-theme-notify-inc.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-23 00:23:26 +00:00
Claude c7d876096f feat: NIP-07 window.nostr provider for nSites
nSites now open in "website mode": a normal web app with normal network
access plus a NIP-07 window.nostr provider, so standard Nostr web apps can
"log in with Amethyst" and sign as the active user. Napplets are unchanged
(locked, declared-only sandbox).

- window.nostr (shim.js) installs only when the host sets __nappletNip07
  (website mode). getPublicKey/getRelays reuse the existing consent-gated
  identity reads; signEvent is a new sign-only op honoring the app-supplied
  created_at (no publish — the web app sends to relays itself).
- NappletRequest.SignEvent + nostr.signEvent decode; broker signs as the
  user and returns the signed event without publishing. pubkey is still
  fixed by the signer, so the app can never sign as another identity.
- Website mode: content server defers off-origin requests to the WebView
  and drops the app CSP (normal network); locked napplets keep connect-src
  'none' and 404 off-origin.
- Launcher grants IDENTITY + RELAY (consent-gated) for website mode,
  independent of the nSite's empty manifest requires.
- Consent dialog shows the kind + content preview for a sign request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:49:58 +00:00
Claude 5e4250bfbc fix(napplets): expand the "what it can access" section straight down, not from the left
The disclosure used AnimatedVisibility's default transition and the inner servers
Column didn't fill width, so the section faded/expanded while the server list also
grew in horizontally from the left — an inconsistent, weird effect. Make the
transition explicit (fade + expandVertically/shrinkVertically anchored at Top, so
it opens/closes straight down like the card) and fill width on the servers column
so nothing slides in sideways.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:26:47 +00:00
Vitor PamplonaandClaude Opus 4.8 53d6a2f8b4 fix(napplets/nsites): render real SPAs in the sandbox (blank-page + reload-loop)
Static-site / SPA nApplets & nSites (Vite/nsyte/CRA/webpack output) rendered as a
blank page, then — once that was fixed — as a fast reload-loop blink. Three layered
causes, each found on-device via logcat:

1. Sub-path serving. The applet loaded under https://napplet.local/app/, but bundlers
   emit absolute asset URLs (/assets/app.js, /fonts/…) that resolve against the origin
   ROOT, so every script/style/font 404'd. nSites are defined to be hosted at the domain
   root; serve there.

2. Opaque-origin storage. The applet ran in an `allow-scripts`-only iframe, so its origin
   was opaque ("null"): module scripts + asset fetches were CORS-blocked, and reading
   localStorage/IndexedDB/serviceWorker threw SecurityError — which crash-loops every SPA
   (gruuv: "cache version 0 < 23 → reset → reload", forever, because IndexedDB never
   worked so the version never persisted).

Fix: give each applet its OWN real, persistent, isolated origin — a per-applet subdomain
https://<id>.napplet.local (id = sha256(author:identifier)), framed by the shell with
`allow-scripts allow-same-origin`. A real origin restores localStorage/IndexedDB/SW and
makes the applet's own assets same-origin (no CORS). Isolation is preserved because the
origin is DISTINCT from the shell's: the native bridge stays origin-restricted to the
shell (napplet.local), so the cross-origin applet still can't reach it or read the shell
DOM, and per-applet subdomains keep applets' storage isolated from each other. The shell
HTML's iframe src + CSP frame-src are bound to the specific applet origin at serve time.

Also add an in-memory localStorage/sessionStorage polyfill to the injected shim as
belt-and-suspenders for any context where DOM storage is still unavailable.

By-design sandbox enforcement is unchanged and correctly blocks the rest (external CDN
scripts, direct relay WebSockets via connect-src 'none', external images) — apps must go
through the napplet SDK for those.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:18:56 -04:00
Claude 8c4eae6f5a chore(napplets): rebrand display text to "nApplet" / "nSite"
User-facing strings only — "Napplet(s)" → "nApplet(s)" and the static-site
label → "nSite". Code identifiers, resource keys, CLI verbs (amy napplet/nsite),
and unrelated profile "Website" labels are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 19:52:00 +00:00
Vitor PamplonaandClaude Opus 4.8 d01455f3c9 Merge remote napplet updates into local fix branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:29:24 -04:00
Vitor PamplonaandClaude Opus 4.8 07d25c02db fix(napplet): serialize consent prompts so concurrent requests don't drop
When a napplet issued several consent-gated calls at once (the common case: it
reads relays + storage + identity on load), each launched a NappletConsentActivity
concurrently. The host can only show one, so the rest were delivered to the
single-top activity and silently dropped — their broker calls hung forever
(storage stuck pending; a subscription's consent lost, yielding 0 events).

Gate the consent-prompt path behind a Mutex on the (per-account, reused) broker
so prompts queue one at a time. After taking the lock, re-read the ledger so a
sibling request for the same capability honors the just-recorded grant instead
of prompting again. Only the prompt is serialized — execute() and already-granted
paths stay parallel. Per-use capabilities (payments) still re-prompt every time.

Adds a regression test asserting 5 concurrent same-capability requests yield
exactly one prompt and never two dialogs at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:28:33 -04:00
Vitor PamplonaandClaude Opus 4.8 44deeb1ee0 fix(napplet): launch the sandbox host by reading shell/shim from assets
NappletHostActivity runs in the isolated `:napplet` process, which early-returns
from Amethyst.onCreate to stay key-free and so never initializes the
compose-resources Android context. `Res.readBytes` then threw
MissingResourceException, crashing the host 100% on launch (the napplet feature
could never open).

Read shell.html/shim.js straight from the APK assets (where compose-resources
packages them) via the Activity context instead of the suspending Res accessor.
NappletWebContract now exposes the relative paths + RESOURCE_ASSET_ROOT so the
paths stay single-sourced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:28:32 -04:00
Claude a2eae42c07 feat(napplets): app-store-style card + icon manifest tag; demote capabilities
Redesign the shared StaticWebsiteCard (used by the feed AND the napplets browse
screen) to look like an app entry instead of a manifest dump: square app icon
(with a colored monogram fallback), name, a NAPPLET/WEBSITE type label, a short
description, and an Open button. The technical details users don't care about —
declared capabilities, Blossom servers, source URL — move behind a tap-to-expand
"What it can access" disclosure; capabilities are still re-confirmed at the
consent prompt when actually used and remain fully manageable in the permissions
screen.

Add an `icon` tag (NIP-5A/5D) end-to-end:
- quartz: IconTag + siteIcon() accessor/builder, NappletManifest.icon(), and an
  icon param on all four site/napplet build() factories (+ round-trip test).
- amy: `--icon URL` on `nsite/napplet publish`, surfaced in the publish output.
- card: renders the icon via Coil, monogram fallback when absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 17:13:41 +00:00
Claude ece1f43975 feat(amy): publish napplets and nsites (ship a directory)
Add `amy nsite publish <dir>` and `amy napplet publish <dir>` so a static-site
or napplet directory can be shipped to Nostr in one command, building on the
new CLI/Blossom infrastructure.

- commons (jvmMain) StaticSitePublisher: the reusable "upload a tree" half —
  walks a directory (or single file), content-addresses each file, BUD-02
  signed-uploads it via BlossomClient, and maps it to an absolute web path
  (/index.html, /assets/app.js, …). Returns the NIP-5A path→sha256 tags.
- cli StaticSitePublish: thin shared flow — uploads via the commons publisher,
  hands the path tags to a kind-specific builder, signs with the account key,
  and broadcasts. nsite builds 15128/35128 (+ x aggregate); napplet builds
  15129/35129 (aggregate + requires already added by the quartz builder).
- nsite/napplet `publish` verbs wired into their routers.

Test harness README now recommends `amy napplet publish tools/napplet-test`,
keeping publish.sh as a no-amy fallback. Unit test covers the path mapping;
cli + commons build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 15:31:28 +00:00
Claude 08947510d9 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad 2026-06-22 15:04:44 +00:00
Claude a5eabbf1a2 Merge remote-tracking branch 'origin/main' into claude/loving-hopper-rn553t 2026-06-22 14:53:23 +00:00
Claude df15414424 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad
# Conflicts:
#	amethyst/src/main/res/values/strings.xml
2026-06-22 14:50:16 +00:00
ClaudeandVitor Pamplona 4e582d198d fix(nutzaps): receive nutzaps on inbox/dm/kind:10019 relays, not outbox
Inbound NIP-61 nutzaps (kind:9321) are messages other people send *to*
the user, so per the NIP-65 outbox model they must be read from the
user's inbox-side relays, not their outbox. The Cashu subscription used a
single relay set (outbox) for both the user's own NIP-60 events and
inbound nutzaps, so a sender following NIP-61 correctly (publishing to
the relays advertised in the recipient's kind:10019, or to the
recipient's NIP-65 inbox) could be missed.

Split the subscription relay sets per filter:
  - own NIP-60 wallet/token/history events keep reading from outbox,
    where the user published them (needed to restore on a fresh device);
  - inbound kind:9321 nutzaps now read from the union of the user's
    NIP-65 inbox + DM relays + the `relay` tags in the user's own
    kind:10019. The last one is NIP-61's source of truth for "where to
    send me nutzaps" and may be written by another client to a relay set
    unrelated to our NIP-65 lists, so we listen there too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHcZ2gv8ro9Q2bEiTSiHD8
2026-06-22 09:38:31 -04:00
Claude e883d99f53 feat(napplet): implement keys.onAction (key binding + keys.action push)
Bind a napplet's registered keyboard/command actions to real hardware-key
combos and fire them back as keys.action pushes, so napplet.keys.onAction
actually triggers (previously registration was acked but never fired):

- protocol: RegisterAction / ActionRegistered carry the key combo (binding,
  from the SDK's action.defaultKey); the codec decodes defaultKey and echoes
  binding in the result; encodeKeysAction push envelope added.
- broker: registerAction returns the honored binding (still no key access for
  the applet; KEYS stays a declared-only, no-prompt capability).
- NappletKeyActions (host): a registry that parses combos like "Ctrl+Shift+S"
  / "Cmd+Enter" / "F2" and matches them against KeyEvents.
- NappletHostActivity: binds an action only after the broker authorizes it
  (from the keys.registerAction.result), unbinds on keys.unregisterAction, and
  overrides dispatchKeyEvent to turn a matching combo into a keys.action push
  via the shell bridge. Unmatched keys fall through to the WebView, so the
  applet's own text inputs keep working. Touch-only devices simply never match.

shim already passed the full action (incl. defaultKey) and wired onAction to
the keys.action push, so no shim change was needed. Conformance test now
covers the defaultKey decode + binding round-trip; all napplet suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 01:37:39 +00:00
Claude 1a1fb9ff32 feat(napplet): implement identity.onChanged push
Wire napplet.identity.onChanged end to end so an applet is notified when the
active user's public key changes (account switch / connect / disconnect):

- shim: onChanged registers a handler and opens a watch (identity.watch) on the
  first handler; closing the last one stops it (identity.unwatch). identity.changed
  pushes are dispatched to the handlers with the new pubkey.
- router: identity.watch (gated on the IDENTITY declaration) / identity.unwatch
  become WatchIdentity / UnwatchIdentity outcomes — a push subscription, like
  relay.subscribe, that never reaches the broker.
- NappletIdentityWatch (host): collects the active account's pubkey from the
  session manager and pushes identity.changed on each subsequent change (the
  current value is dropped — the applet already has it via getPublicKey). Torn
  down on unwatch and on service destroy.
- codec: encodeIdentityChanged push envelope.

Router unit tests cover watch (declared/undeclared) and unwatch; commons tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 01:32:07 +00:00
Claude 443c35762a feat(napplet): implement identity.getList/getZaps/getBadges + resource nostr:
Fill in the identity reads that previously fell through to Unsupported, and
add nostr: resolution to resource.bytes — both reading from what Amethyst
already has locally, matching the @napplet/nap shapes.

identity (AccountIdentityReader):
- getList(listType): public tag values (e/a/p/t/word/r/emoji) of the user's
  NIP-51 replaceable list of that type (bookmarks 10003, pins 10001, mute
  10000, interests 10015, communities 10004, channels 10005, emojis 10030).
- getZaps: ZapReceipt[] {eventId, sender, amount, content?} from kind-9735
  receipts p-tagging the user in the cache.
- getBadges: Badge[] {id, name?, description?, image?, thumbs?, awardedBy}
  from kind-8 awards p-tagging the user, resolved against their kind-30009
  definitions in the cache.

resource.bytes (NappletResourceFetcher):
- nostr: URIs (NIP-19) resolve to the referenced event's JSON
  (application/json). nembed carries it inline; note/nevent/naddr resolve
  from the cache then a bounded relay fetch; npub/nprofile resolve the
  author's kind-0. Still no direct network for the applet.

The wire codec already routed these (generic IdentityRead + identityResultField,
ResourceBytes), so no protocol change was needed. Commons napplet tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 01:27:53 +00:00
Claude 47cc76d9f1 feat(cli): amy admin (NIP-86) + serve (embed geode relay)
Two new nak-parity commands:

- `amy admin RELAY METHOD [args]` — NIP-86 Relay Management API over NIP-98
  HTTP auth. Full method set: ban/unban + allow/unallow pubkey, ban/allow
  event, allow/disallow kind, block/unblock IP, change name/description/icon,
  and all list-* queries. Reuses quartz's Nip86Client (request build + NIP-98
  auth + parse) and the Nip86Retriever HTTP path — extracted from amethyst to
  commons/jvmAndroid so amy and the Android relay-management screen share it.

- `amy serve [--host --port --path --db --admin]` — runs a Nostr relay by
  embedding geode (the standalone Ktor relay on quartz's relay-server code).
  In-memory by default (ephemeral, like nak serve); --db FILE for SQLite. The
  account's own pubkey is always an admin, so `amy admin` works against it out
  of the box. cli gains a :geode dependency (geode depends only on :quartz) and
  kotlinx-serialization-json (to render NIP-86 JSON results).

Verified end-to-end: `amy serve` + `amy admin ws://127.0.0.1:PORT
supported-methods|change-name|ban-pubkey|list-banned-pubkeys` round-trip
cleanly over real HTTP + NIP-98 against the live geode relay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 00:19:29 +00:00
Claude 42a82c7e8b feat(cli): amy cashu receive/send/maintenance/mint-rec tiers
Complete the Cashu command surface, every verb a thin wrapper over the
shared commons CashuWalletOps the Android wallet runs:

  cashu receive ln SATS [--mint]      start mint, return bolt11 + kind:7374
  cashu receive complete QUOTE_ID     poll mint; on settle, mint proofs
  cashu receive resume QUOTE_ID       alias of complete
  cashu receive token TOKEN           redeem a cashuB token
  cashu receive nutzap-sweep [--mint] redeem inbound NIP-61 nutzaps
  cashu send ln INVOICE [--mint]      melt to a bolt11 (scrubs first)
  cashu send token SATS [--mint --memo]  export a cashuB token (scrubs first)
  cashu send nutzap USER SATS [--zapped --message]  P2PK-locked nutzap
  cashu maintenance scrub [--mint]    NUT-07 + NIP-09 prune spent proofs
  cashu maintenance restore MINT_URL  NUT-09 restore from seed
  cashu maintenance migrate-keysets [--mint]  consolidate onto active keyset
  cashu mint-rec show [--author] / add URL [--dtag --review] / remove ID

- scrubStaleProofs extracted into CashuWalletOps so Android's
  CashuWalletState.scrubLocallyStaleProofs and amy share one impl.
- Context.cashuRestore mirrors CashuWalletState.restoreFromMint (seed +
  NUT-13 counter bump); Context.cashuSeed warms the per-run seed.
- receive complete recovers the mint amount by decoding the quote's
  bolt11 (kind:7374 stores only the quote id), so it works statelessly.

Verified against mint.minibits.cash + live relays: receive ln returns a
real invoice, complete/resume poll a pending quote, mint-rec round-trips,
and every error path (insufficient_funds, no_mint, mint_quote_gone) is
clean. Happy-path mint/melt completions need a payable bolt11 (interop
harness, PR 9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 00:10:40 +00:00
Claude b6f9630883 refactor(napplet): single-source the web contract and feed card in commons
Two shared extractions so the future desktop host reuses the exact same
sandbox and feed UI as Android, with no chance of drift:

Shared web contract:
- Move shell.html + shim.js into commons composeResources
  (files/napplet/), read via Res.readBytes on any platform.
- New NappletWebContract (commons/commonMain) single-sources the whole
  web contract: the shell/shim loaders plus the internal origin/host/URLs
  and both Content-Security-Policies (SHELL_CSP, APP_CSP). The Android
  host preloads the bytes in onCreate and reads every origin/CSP constant
  from NappletWebContract instead of its own duplicated constants and
  assets.open() calls.

Shared feed card:
- New StaticWebsiteCard (commons/.../ui/note) renders the inert NIP-5A /
  NIP-5D preview card: self-contained with commons compose-resource
  strings, LocalUriHandler for links, and inlined card chrome. It takes
  an isNapplet flag and an onOpen launch slot, so it never executes applet
  code itself.
- amethyst's note/types/StaticWebsite.kt becomes thin event->card
  adapters that supply the sandboxed onOpen launch.

Card strings move to commons strings.xml. Both napplet test suites
(commons jvmTest + amethyst) stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 00:10:22 +00:00
Claude 0cff3bf8e2 refactor(napplet): extract host-agnostic NappletRequestRouter to commons
Move the decode → broker → encode orchestration out of Android's
NappletBrokerService.handleMessage and into a pure, transport-free
NappletRequestRouter in commons/jvmAndroid. It returns a small Outcome
(Ignore / Reply / OpenSubscription / CloseSubscription / Push) that each
host acts on, so the Android service and the future desktop host share
the routing brain and can't drift on wire behavior.

The service now resolves the broker and dispatches on the Outcome,
supplying only the Messenger transport and the live relay subscription.
openLiveSubscription takes the decoded filters from the router instead of
re-decoding the payload, and the now-redundant process() is removed.

Unit-tested in commons/jvmTest (NappletRequestRouterTest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 23:59:53 +00:00
Claude 8bb537438f feat(cli): amy cashu wallet/mint/balance on shared NIP-60/61 code
Add the offline Cashu command tier to amy, all driven by the shared
commons wallet code so amy exercises the same path as the Android app:

  amy cashu wallet create [--mint URL] [--mints a,b] [--privkey HEX] [--relay r1,r2]
  amy cashu wallet show
  amy cashu wallet export-key
  amy cashu wallet destroy
  amy cashu mint ping URL          (stateless)
  amy cashu mint info URL          (stateless)
  amy cashu balance [--mint URL]

- create/destroy reuse commons CashuWalletOps.publishWalletEvents /
  deleteWallet; show/balance reuse the CashuWalletReader projection over
  the local event store; mint ping/info hit quartz's MintHttpClient.
- Context gains cashuOps() (wired to the file NUT-13 counter store + a
  per-run seed cache) and cashuSnapshot(); DataDir gains cashu.json.
- Extraction D: CashuKeysetCounterStore contract in commons +
  FileCashuKeysetCounterStore (atomic ~/.amy/<account>/cashu.json).

PRs 4 of cli/plans/2026-05-28-cashu-cli.md (extractions A–D + offline
tier). receive/send/maintenance/mint-rec + interop harness still pending.
Verified end-to-end against mint.minibits.cash and live relays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 22:30:59 +00:00
Claude d111c2589d refactor(commons): extract CashuWalletReader projection for amy reuse
Add a pure, stateless CashuWalletReader in commons that projects a stream
of NIP-60/61/87 events into a WalletSnapshot (wallet/nutzap-info events,
decrypted mints, unspent token entries, history, pending quotes, nutzaps,
recommendations, plus balance + per-mint balances).

Android's CashuWalletState keeps its incremental dirty-tracking and
StateFlow plumbing but now delegates the two tricky computations —
del-rollover over decrypted tokens (computeUnspent) and the
destroyed/expired pending-quote filter (computePending) — to the shared
reader instead of carrying its own copies. amy will call project() once
per command over its event store.

Extraction C of cli/plans/2026-05-28-cashu-cli.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 22:20:25 +00:00
Claude fc7db088b2 refactor(commons): extract CashuWalletOps to commons for amy reuse
Move the NIP-60/61 wallet orchestration layer (CashuWalletOps + its
result types: TokenEntry, MintQuoteStarted, MintCompleted, MeltCompleted,
SendTokenCompleted, RedeemCompleted, NutzapSent, RestoreOutcome,
MigrationResult, CreatedWallet, describeMintError) out of amethyst into
commons/jvmAndroid/cashu/ops.

The class already had zero Android dependencies — it takes signer,
publish, okHttpClient, secretFactory, and the NUT-13 counter callbacks as
constructor params. It lands in the jvmAndroid source set (not commonMain)
because it composes quartz's jvmAndroid CashuMintOperations/MintHttpClient
and uses ConcurrentHashMap. Both Android (amethyst) and the JVM CLI (amy)
can now drive the exact same wallet code path.

This is Extraction B of cli/plans/2026-05-28-cashu-cli.md. Android
callers (CashuWalletState, the wallet ViewModels, AccountViewModel)
updated to the new package; no behavioral change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 22:17:04 +00:00
Claude 0b76518ef2 refactor(napplet): move wire codec to commons for desktop reuse + desktop host plan
Set the desktopApp up to host napplets/nsites by maximizing the shared
core and documenting the edge it must build.

- Moved NappletProtocolJson (the wire codec) from amethyst to
  commons/jvmAndroid (package ...commons.napplet.protocol), next to the
  NappletRequest/Response types it marshals. It depends only on quartz +
  kotlinx.serialization + java.util.Base64 (Android 26+/JVM), so a future
  desktop host marshals through the identical object — request/result/push
  shapes can't drift between platforms. amethyst host/service/tests updated
  to import it; tests stay in amethyst and still exercise it.
- Added desktopApp/plans/2026-06-21-napplet-desktop-host.md: what's already
  shared (broker, protocol, codec, resolver, the shell.html/shim.js web
  contract), what desktop must build (KCEF/JCEF engine, custom-scheme
  serving, isolation, transport, gateways, UI), the decisions to make, a
  security-parity checklist, and recommended further extractions
  (NappletRequestRouter, shared web assets, the inert feed card).

commons:jvmTest and the amethyst napplet suite pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 22:12:28 +00:00
Claude cd2bae081e feat(napplet): live subscription tail, multi-filters, resource.cancel
Implements the remaining tractable conformance follow-ups:

- Live subscription tail: relay.subscribe now opens a real
  client.subscribe whose SubscriptionListener streams relay.event
  (stored + live), relay.eose, and relay.closed pushes keyed by subId,
  instead of a one-shot snapshot. relay.close unsubscribes (tracked in
  liveSubs, torn down in onDestroy). The broker only authorizes the
  subscription (RELAY consent) and returns Subscribed; the host owns the
  live stream. Shim dispatches relay.closed too.
- Multi-filters: relay.query/subscribe honor every filter in filters[],
  not just the first (decodeFilterList; gateway query(List<Filter>);
  queryEvents unions across filters; max limit applied).
- resource.cancel: accepted at the host edge as a no-op Done.

Conformance tests extended (multi-filter decode, relay.closed push);
commons:jvmTest and the amethyst napplet suite pass.

Still open: identity getList/getZaps/getBadges + onChanged (shapes
underspecified), the keys.action push (needs a host trigger UI), the
resource nostr: scheme, inc + the niche domains, and on-device
verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 21:43:36 +00:00
Claude 0f23051d54 feat(napplet): implement the four SDK conformance breakers
Closes the 🔴 items from the conformance audit so stock @napplet/shim
napplets interop:

1. Shell handshake: the host answers shell.ready with shell.init
   {capabilities:{domains,protocols},services} built from the declared
   domains (NappletProtocolJson.encodeShellInit), so supports() works.
2. Id-less messages: onShellMessage no longer drops messages without an
   id — shell.ready is answered locally and fire-and-forget messages get
   a synthetic id so they reach the broker.
3. keys: keys.registerAction/unregisterAction decode and the broker acks
   them (declared-gated, no consent) so registerAction() resolves; the
   shim dispatches the keys.action push. (Global-key binding is a
   follow-up — keys.action isn't emitted yet.)
4. upload: realigned to upload.upload{request:{data,mimeType,filename}} →
   rich UploadResult{ok,uploadId,status,url,sha256,size,mimeType};
   shell.html inlines the request Blob as base64 so it survives the
   bridge; the gateway uploads via the app's BlossomUploader to the
   user's kind:10063 server with a signed auth event.

NappletSdkConformanceTest's gap guards flip to conformance assertions for
shell.init/keys/upload; inc stays the one documented gap. commons:jvmTest
and the amethyst napplet suite pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 21:02:55 +00:00
Vitor PamplonaandClaude Opus 4.8 df1982a7f6 feat(feeds): prefetch media + pre-parse text ahead of the viewport
Warms the next/previous few feed notes off the main thread so media and
link previews are ready before the user scrolls to them, and pre-parses
rich-text bodies into the shared cache so scroll-time composition is a
cache hit instead of a UI-thread parse.

Per upcoming note (off Dispatchers.Default, deduped via a per-feed set,
cancelled on a new visible range):
- Pre-parses TextNote/Comment bodies through CachedRichTextParser using the
  renderer's exact key, so the composition reads a cached parse. Other kinds
  still get their media/links discovered, just without the render-cache warm.
- Prefetches images into Coil — inline content images, NIP-92 imeta blobs,
  NIP-94 file-header url tags, and video poster frames — and records each
  decoded aspect ratio in MediaAspectRatioCache so the box is reserved on
  first layout (no jump). Video poster ratios seed the video URL's box too.
- Warms OpenGraph/link previews via UrlCachedPreviewer.
Gated on showImages()/showUrlPreview() so it honors data-saver/Wi-Fi-only.
Video bytes are deliberately not prefetched (large, HLS, player pool already
starts fast).

Wired centrally at the two feed dispatchers (RenderFeedContentState,
RenderFeedState) so every list feed routed through them is covered without
per-screen wiring, plus direct hooks for the custom-render feeds (hashtag,
profile notes) and a LazyGridState variant for the grid feeds (gallery,
products, discover).

CachedRichTextParser is now content-addressed (memoized contentHash on
ImmutableListOfLists) so an off-thread pre-parse maps to the same entry the
renderer looks up, and its cache grows 50 -> 500 to hold the prefetch +
multi-feed working set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:44:39 -04:00
Claude 24638cc50c feat(napplet): implement the identity read API
Wire the upstream identity.* read methods (beyond getPublicKey) to the
active Account, returning JSON gated by the IDENTITY consent:

- getProfile  -> kind-0 metadata content
- getRelays   -> NIP-65 { "<url>": { read, write } } map
- getFollows  -> kind-3 followed author pubkeys
- getMutes    -> NIP-51 mute-list user pubkeys (decrypted)
- getBlocked  -> NIP-51 block-list user pubkeys (decrypted)

getList/getZaps/getBadges route through but degrade to Unsupported for
now; onChanged stays a client-side no-op until the live push channel
lands. Reads are public data only — never key material — and remote/
external signers still self-gate the consent.

Adds NappletRequest.IdentityRead, NappletResponse.Json, a
NappletIdentityGateway collaborator, codec round-trip for identity.*,
the shim methods, and unit tests. commons:jvmTest and the amethyst codec
test pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 14:26:27 +00:00
Claude b43c1c9eda fix(napplet): align shell to verified @napplet/shim 0.15+ contract
Reverse-engineered the authoritative @napplet/shim (npm v0.16.0) and
corrected the napplet host to its real wire contract. The prior commit
carried guessed method names that would break real ecosystem napplets.

- Signing model: napplets never get a sign() (per upstream "signing and
  encryption are mediated by the shell"). Dropped keys.signEvent /
  keys.nip04* / keys.nip44*. relay.publish now takes an UNSIGNED template
  and the broker signs it as the user, returning the signed event;
  added relay.publishEncrypted (shell encrypts + signs + publishes).
- keys -> keyboard/command actions (registerAction/unregisterAction/
  onAction), client-side no-op stubs (not yet wired to the host keyboard).
- storage: get/set/remove -> getItem/setItem/removeItem; added storage.keys
  end-to-end (protocol, broker, DataStore, shim).
- resource.bytes returns a Blob (shim builds from {bytes, mime}).
- shell.supports gains optional protocol arg; added shell.ready/onReady/
  services stubs.
- relay.subscribe wired (initial matches; live tail still a follow-up).
- value.payInvoice and upload.blob kept as clearly-marked Amethyst-specific
  extensions (no upstream equivalent; real napplets never call them).

Broker now defers per-signature consent to remote/external signers via a
signsAsUser flag (publish/publishEncrypted) instead of an IDENTITY/KEYS
capability check. Tests updated; commons:jvmTest and the amethyst codec
round-trip test pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 19:39:36 +00:00
Claude 5ca44e277f feat(napplet): align with the upstream napplet SDK (envelope, namespaced API, domains)
Acts on the ecosystem audit so real napplets built against @napplet/web can run.

Wire: switch to the upstream envelope {type:"<domain>.<action>", id} →
{type:"…​.result", id, ok, …} across the JS shim, NappletProtocolJson, and the host
shuttle (host forwards the verbatim envelope, injects id on the reply).

API: rewrite the injected window.napplet.* to the namespaced SDK surface —
shell.supports, identity.getPublicKey (+onChanged stub), keys.{signEvent,nip04*,
nip44*}, relay.{publish,query,subscribe}, storage.{get,set,remove}, value.payInvoice,
resource.{bytes,bytesAsObjectURL}, upload.blob. subscribe currently returns the
initial matches via query (live tail is a follow-up).

Capabilities: split to the domain model — SHELL, IDENTITY, KEYS, RELAY, STORAGE,
VALUE, RESOURCE, UPLOAD (was IDENTITY/RELAY/WALLET/STORAGE/NET). shell.supports is
answered with no consent, reflecting declared+brokered domains; keys (signing) split
from identity (pubkey); signer-self-gating now covers both.

New ops: resource.bytes (https/data, broker-fetched, Tor-routed, consent-gated).
upload is wired end-to-end but its Android Blossom gateway is left unprovided
(Unsupported) pending the Uri + auth-event + server-selection integration.

Codec moved to java.util.Base64 (real on minSdk 26 and in JVM tests). Permissions
screen + capability labels updated to the 8 domains; new consent/label strings
localized. Broker + codec + capability tests updated/added.

:commons:jvmTest, :amethyst:testPlayDebugUnitTest (codec), and
:amethyst:compileFdroidDebugKotlin pass; spotless clean. See
plans/2026-06-20-napplet-ecosystem-audit.md (Update section) for the remaining gaps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 16:54:06 +00:00
Claude c999d2ac73 feat(napplet): permissions management screen
A modern Material3 screen to review and revoke the permissions napplets hold.

Data layer (commons, tested): NappletPermissionStore gains all() (enumerate
persisted grants by coordinate) and remove(coordinate, capability); the ledger
gains allPersistedGrants() and revoke(identity, capability). DataStore actual
implements both (capability is the final space-delimited token of each key).

UI (amethyst): NappletPermissionsScreen renders one ElevatedCard per napplet —
resolved title + author, and a row per capability with an icon, label, and a
control: a Switch (Allowed/Blocked) for normal capabilities, or a "Blocked"
indicator for per-use ones (payments only ever persist a DENY). Each row has a
revoke action; each card a "Forget this napplet" (revokeAll). Empty state with a
shield. Reads/writes the same DataStore the broker uses, so changes take effect
immediately. Reached via a "Manage permissions" action on the Napplets top bar
(Route.NappletPermissions).

commons ledger tests added for allPersistedGrants + single-capability revoke.
:commons:jvmTest and :amethyst:compileFdroidDebugKotlin pass; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 15:28:28 +00:00
Claude 916a624ddb feat(napplet): capability-aware consent — per-payment, signer-aware identity, foreground-only
Refines the uniform consent model now that wallet + identity carry different risk.

Payments (WALLET): NappletCapability.requiresPerUseConsent — every payInvoice
re-prompts (with the decoded sats amount); the dialog drops "Always allow" and the
broker downgrades any always/session grant to one-shot, so a payment grant is
never persisted. No silent spend.

Identity: gated by us only when Amethyst holds the key (NostrSignerInternal). For
remote (NIP-46) / external (NIP-55) signers the broker defers to the signer's own
per-request consent instead of double-prompting — while still honoring a standing
per-napplet DENY and the requires declaration. The sign prompt shows a kind +
content preview.

Foreground-only execution: NappletHostActivity pauses the WebView's JS/timers in
onPause and resumes in onResume, so a backgrounded applet can't fire a
sign/decrypt/pay request whose prompt would be confused with Amethyst's own UI.
This is the precondition that makes deferring identity to an external signer safe.

commons broker tests added: wallet prompts every time and is never persisted;
external signer defers identity without prompting but still honors a standing DENY.

:commons:jvmTest and :amethyst:compileFdroidDebugKotlin pass; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 15:15:10 +00:00
Claude b422274140 feat(napplet): capability enforcement, read/storage capabilities, nsite host wiring
Closes the highest-leverage gaps from the completeness report (items 2–5).

Capability enforcement (#2):
- The broker now refuses any request whose capability is not in the manifest's
  `requires`. The host resolves `requires` to a declared capability set and sends
  it with every IPC request; the broker denies undeclared capabilities before any
  consent prompt. Per-operation consent summaries added for the new ops.

Read capability (#3):
- New QueryEvents request (RELAY capability) → NappletRelayGateway.query, answered
  from LocalCache (account.cache.filter). window.napplet.queryEvents(filter) added.

nsite host wiring (#4):
- NappletLauncher generalized to launch any NIP-5A site from paths+servers, so
  nsites (kinds 15128/35128) open in the sandbox too. The nsite card
  (StaticWebsite) gets an "Open" button; nsites declare no capabilities, so the
  broker refuses everything and they render as inert static content.

Storage + wallet (#5):
- STORAGE fully implemented: StorageGet/Set/Remove + DataStoreNappletStorage,
  namespaced per applet coordinate. window.napplet.storage.{get,set,remove}.
- WALLET modeled with PayInvoice + NappletWalletGateway, but kept Unsupported (no
  gateway provided) — no money path ships until verified end-to-end.
- Inter-applet messaging and live (non-cache) relay query remain v2.

commons broker tests cover declaration enforcement, query, and storage round-trip.
:commons:jvmTest and :amethyst:compileFdroidDebugKotlin pass; spotless clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-20 13:50:19 +00:00
Claude da336897c2 fix: keep balanced closing delimiters and inline commas in detected URLs
The URL detector stripped a single trailing punctuation char unconditionally,
which dropped the closing ")" from legitimate URLs such as
https://en.wikipedia.org/wiki/Bitcoin_(disambiguation).

Make the trailing strip balance-aware: a trailing ")", "}" or "]" is kept when
the URL contains its matching opener (balanced), and only stripped when it is
unbalanced wrapping/sentence punctuation (e.g. "(see example.com)" or
"http://test.com)"). Commas without surrounding spaces were already kept inside
paths; this also adds "]" to the begin/end punctuation sets so an unbalanced
bracket is handled symmetrically with parens and braces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzZzVcMcuzjSdhD3xqCE87
2026-06-19 23:38:44 +00:00