Commit Graph
15550 Commits
Author SHA1 Message Date
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 20b42d3443 fix: prefetch off main thread; use real Tor logo in sandbox bar
- StrictMode DiskReadViolation: PrefetchManifestBlobs read context.cacheDir
  (ensurePrivateCacheDirExists touches disk) on the composition dispatcher.
  Move it into withContext(Dispatchers.IO); the prefetch was already IO-bound.
- Sandbox top-bar network indicator now uses the app's real Tor logo
  (ic_tor, copied into :nappletHost since it can't depend on :amethyst) via an
  ImageView, instead of the onion/globe emoji. Lit when routing through Tor,
  dimmed when the site loads over the open web.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 23:30:30 +00:00
Claude 7eb0949e5c feat: route nSite WebView traffic through Tor by default, per-site opt-out
nSites now load over Tor by default when Tor is active — closing the gap where
only blob fetches were Tor-routed while the site's own web traffic (fetch/img/
script) went out the system network and could leak the user's IP. Users can opt
a specific site out to the open web (e.g. a site that breaks or is slow over
Tor); the choice is remembered per site and makes everything for that site
direct (web + blobs).

- NappletHostActivity sets a process-wide WebView SOCKS proxy override
  (socks5://127.0.0.1:<torPort>) for website-mode nSites, or clears it for open
  web. Best-effort + on-device-verifiable: SOCKS-over-WebView support varies by
  WebView version, so it's isolated to applyWebViewProxy() and never breaks the
  site if unsupported.
- Top-bar onion (🧅 Tor / 🌐 open web) shows the routing and toggles it; the
  dialog explains the IP-privacy trade. Shown only for nSites when Tor is active.
- NappletNetworkRegistry: main-process per-site preference (coordinate-keyed,
  DataStore-backed, Tor-default). The launcher reads it; the broker persists the
  sandbox's toggle resolved through the launch token. The key-free sandbox never
  touches it.
- Toggling persists via a new MSG_SET_NETWORK_MODE IPC, then relaunches the host
  so the proxy + content server rebuild cleanly for the new mode.
- "Open web" also routes blob fetches direct (effective proxy -1); locked
  napplets always keep Tor for blobs (no toggle exposed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 23:10:12 +00:00
Vitor PamplonaandClaude Opus 4.8 d0d1577bfe fix(napplets): remove the stray white band at the bottom of the applet WebView
Two edge-to-edge issues on the napplet/nsite host, both surfacing as a white strip below
the applet on a light page:

- Double bottom inset. The root inset listener turned the system-bar/cutout insets into
  padding but returned them un-consumed, so the child WebView (which on targetSdk 35+
  auto-applies any insets it receives to its web content) padded the bottom a SECOND time.
  Zero those types before they reach the WebView, keeping IME flowing so keyboard resize
  still works.

- Over-scroll reveal. Forcing a scroll past the content edge stretched the view and
  exposed the shell document's background behind the applet iframe. WebView.overScrollMode
  only governs the outer frame, so also inject `overscroll-behavior: none` into the applet
  document (alongside the shim) to kill the stretch at the source, theme-independently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:01:42 -04: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
Claude 42edc13be2 feat(napplets): back gesture pages back in the WebView, then exits to Amethyst
The sandbox host now routes the back gesture into the applet/site's own history
first: an OnBackPressedCallback calls webView.goBack() while there's history to
pop (in-page links, iframe navigations, and history.pushState all count), and
only disables itself — letting system back return to Amethyst — once the WebView
is at its first page. The callback's enabled state tracks canGoBack() via
doUpdateVisitedHistory / onPageFinished. Works with predictive back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:23:36 +00:00
Vitor PamplonaandClaude Opus 4.8 a66821d59e docs(napplets): record the per-applet storage-origin change in the security plan
Document the move from the opaque `allow-scripts`-only sandbox to a real per-applet
origin (https://<sha256(author:identifier)>.napplet.local, allow-same-origin) under
"Fixed in this pass", plus the residual risks it introduces: the load-bearing invariant
that the applet origin must stay distinct from the shell/bridge origin, persistent client
storage as a new persistence/exfil surface, the origin-id derivation, and service workers
now being reachable-but-unwired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:20:57 -04: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 176c96ef01 perf(napplets): faster opens — overlap WebView init, parallel prefetch, CAS fast-serve
Three load-time wins on the nApplet/nSite open path:

1. Overlap WebView/Chromium init with the index probe. The host now creates +
   warms the WebView (and binds the broker) in onCreate, so its slow first-in-
   process init runs concurrently with the IO availability probe instead of
   serially after it; the probe just attaches the ready WebView.
2. Prefetch in parallel, index-first. NappletBlobPrefetcher downloads a
   manifest's blobs with bounded concurrency (5) and fetches index.html first,
   instead of strictly sequentially — faster first paint on cold opens.
3. Serve CAS hits without re-hashing. The content server short-circuits a
   content-addressed cache hit (verified on write, addressed by sha256) straight
   to the response, skipping the resolver's per-serve sha256 over the whole blob
   — the dominant CPU cost for large bundles. Network (cache-miss) path still
   fully verifies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 22:07:09 +00:00
Claude 31bd34e838 feat(napplets): prefetch blobs to disk on render + loading/unavailable screen
Faster opens: while an nApplet/nSite card is on screen, eagerly download +
sha256-verify all of its blobs (Tor-routed) into a shared content-addressed
cache, so tapping Open serves from disk. Prefetch is de-duplicated (in-flight +
on-disk) and cancellation-aware (stops when the card scrolls away).

- New :nappletHost pieces: NappletBlobCache (content-addressed, multi-process-safe
  atomic store, replacing the single-process OkHttp DiskLruCache), NappletBlobHttp
  (shared Tor OkHttp + size-capped download), NappletBlobPrefetcher.
- NappletContentServer now reads the shared cache first, falling back to a Tor
  download that refills it; still re-verifies every blob on serve.
- Cards trigger prefetch via a LaunchedEffect in the StaticWebsite render fns.

Nicer launch: NappletHostActivity probes the index (resolving + caching it)
behind a loading screen (monogram + title + spinner), then shows the app, or a
clear "couldn't load — try again" screen if the publisher's servers are offline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 21:32:48 +00:00
Vitor PamplonaandClaude Opus 4.8 4b838fec72 feat(napplets/nsites): link browse feeds to the TopNav filter at the relay level
The nApplet (NIP-5D) and nSite (NIP-5A) browse feeds ignored the top-nav
follow-list selection at the relay layer: a single SingleSubEoseManager fired a
hardcoded all-authors REQ against the user's home relays, and the screen merely
re-filtered the dump client-side. Selecting Follows / a People-list never changed
what was fetched, so manifests on an author's own write relays were invisible.

Bring them to Pictures-style parity:

- Replace the SingleSubEoseManager with PerUserAndFollowListEoseManager, watching
  defaultX FollowList + the new liveX FollowListsPerRelay outbox flow and rebuilding
  the per-relay REQ on change.
- Add makeN{applets,sites}Filter dispatchers + subassemblies (Global, Authors,
  Follows, MutedAuthors). These manifests carry no topical tags, so the tag-based
  selections (hashtag/geohash/community) correctly fall through to no subscription.
- Add liveNappletsFollowListsPerRelay / liveNsitesFollowListsPerRelay (OutboxLoaderState).
- New authorOnlyRoutes spinner option set: author-based filters only (no geohash,
  hashtag, community, relay, interest-set or AroundMe — none can match these events).
- "Mine" is intercepted per-feed (SubAssembler + screen), querying the user's own
  pubkey against their outbox relays — the shared TopFilter.Mine flow resolves to
  all-follows, so it can't be used directly.

Fix a FATAL duplicate-key crash: these replaceable/addressable kinds were observed
through the versioned note store (observeEvents) and keyed by per-version event id.
Switch to observeNotes, which yields one AddressableNote per address (latest
version), keyed by the stable address (idHex) and auto-updating in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:06:43 -04:00
Claude 4218a7c1af refactor(napplets): extract sandbox runtime into :nappletHost module
The `:napplet` sandbox runtime (NappletHostActivity, NappletContentServer,
NappletIpc, NappletKeyActions) now lives in a new :nappletHost Android library
that depends only on :commons + :quartz — NEVER :amethyst. So the sandbox code
is compile-time incapable of importing Amethyst.instance / LocalCache / Account,
turning the "two-process, no secrets in the sandbox" rule from a convention into
a build-graph guarantee.

- New module + NappletHostContract (Intent-extra keys + broker service FQN), so
  the launcher (amethyst) and activity (module) share the launch contract with no
  dependency cycle. The activity binds the broker by class name.
- Capability labels for the "what it can access" sheet are resolved by the
  launcher (which has app resources) and passed in, so the module needs no
  capability string resources. Host-only strings moved into the module.
- amethyst depends on :nappletHost; the broker-side (NappletBrokerService,
  gateways, NappletLaunchRegistry) stays in :amethyst. Manifest declares the
  activity by FQN (keeps @style/Theme.Amethyst resolvable).
- Docs updated (CLAUDE.md + security plan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 20:45:19 +00:00
Claude 55cbb72a99 docs(napplets): document the two-process model for future contributors/AIs
Add a heads-up to the Amethyst Application KDoc and .claude/CLAUDE.md that the
app runs in two OS processes (main + :napplet), that Android reuses the single
Application class in both, and that statics/objects (Amethyst.instance,
LocalCache, NappletLaunchRegistry) are per-process — so code must not assume
`instance` exists off the main process or share singletons across the boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 20:18:34 +00:00
Claude cdd5e916c5 fix(napplets): don't crash the :napplet process on memory-trim
Android instantiates the single Amethyst Application in every process, so the
sandbox (`:napplet`) shares it and onCreate early-returns there, leaving the
lateinit `instance` (AppModules) unset. But onTrimMemory — delivered to every
process on real devices — and onTerminate called `instance` unconditionally,
crashing the sandbox with UninitializedPropertyAccessException on a trim.
Gate both with the cached sandbox-process check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 20:08:20 +00:00
Claude 6d367526a5 feat(nsites): dedicated nSites browse feed (mirrors nApplets)
Add a bottom-nav nSites feed for NIP-5A static sites (kinds 15128/35128),
mirroring the nApplets feed: a follow-list FeedFilterSpinner in the top bar
(persisted as defaultNsitesFollowList + liveNsitesFollowLists author-matcher)
and rows rendered through the shared NoteCompose path, so they reuse the author
header, StaticWebsiteCard (with Open), and reaction bar.

New: NsitesScreen, NsitesTopBar, the Nsites discovery datasource trio, Route.Nsites,
NavBarItem.NSITES. Wired into AppNavigation, the relay coordinator, and the
follow-list settings persistence.

Also includes the earlier display rebrand to nApplet/nSite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 20:03:44 +00: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
Claude ab2ca77998 fix(napplets): harden the sandbox trust boundary (identity, private lists, visibility)
Three security hardenings from the nsite/napplet review:

1. Launch-token identity binding. The broker no longer trusts the identity +
   declared capabilities sent on each IPC message from the (less-trusted)
   :napplet process. The main process now mints a random token at launch
   (NappletLaunchRegistry), hands only that to the sandbox, and resolves it back
   to the trusted identity/declared set. A compromised sandbox can act only as
   the napplet it was launched as — closing cross-napplet coordinate spoofing
   (storage + permission ledger).

2. Stop leaking private lists. identity.getMutes/getBlocked read the decrypted
   flow, which includes the user's PRIVATE mutes/blocks. Return only the events'
   public tags (MuteListEvent.publicMutes / PeopleListEvent.publicUsersIdSet).

3. Make grants visible + anti-phishing chrome. A persistent trusted sandbox bar
   (shield + name + tap for "what it can access") the applet can't draw over, and
   a live toast when a granted publish/upload/payment runs — so an allow-always
   grant can't act silently. Also keeps the host out from under the system bars.

Findings + residual risks (session-scoped grants, per-origin resource consent)
tracked in amethyst/plans/2026-06-22-napplet-nsite-security.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 18:47:19 +00:00
Vitor PamplonaandClaude Opus 4.8 6a07d21a23 Merge upstream/main into claude/awesome-pasteur-xwiwad
Brings in apps-feed-card 3-dot options menu (upstream #3336).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:37:06 -04: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 c2c00e7867 fix(napplets): respect system bar / cutout insets in the host WebView
NappletHostActivity is edge-to-edge by default on recent Android, so the
sandboxed applet/nsite content drew under the status and navigation bars.
Pad the WebView by the system-bar + display-cutout insets so the content
sits in the safe area.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 18:27:04 +00:00
Vitor PamplonaandGitHub f58cda166d Merge pull request #3336 from vitorpamplona/claude/modest-ritchie-lodbn3
Add more options button to software app note rendering
2026-06-22 14:22:17 -04:00
Vitor PamplonaandClaude Opus 4.8 a277f6a65f Merge upstream/main into claude/awesome-pasteur-xwiwad
Brings in nestsClient MoqLiteSession Ended-announce race fix (upstream #3334).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:15:30 -04:00
Claude 554fa16743 feat: add 3-dot options menu to apps feed card 2026-06-22 17:20:10 +00: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 4cd71b140c refactor(napplets): render browse rows via shared NoteCompose
Replace the bespoke NappletCard with NoteCompose — the same path the main feed
uses for kind 15129/35129 events. This reuses the author header, the shared
StaticWebsiteCard (title/description/source/servers/capability list + Open button
wired to NappletLauncher), and the standard reaction bar (reply/boost/like/zap),
instead of a second hand-rolled card that could drift from the feed and was
missing NoteCompose's timestamp, hidden-user handling, zap-amount menus, etc.

The new top bar + follow-list filter (defaultNappletsFollowList / matchAuthor)
are genuinely new and kept as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 16:47:05 +00:00
Claude 7aa5f10a72 feat(napplets): richer browse cards + follow-list filter bar
The Napplets browse screen now matches the other feed screens:

- Top bar gains a follow-list FeedFilterSpinner (drawer/back · filter · search +
  manage-permissions), persisted in account settings as defaultNappletsFollowList
  and applied via a new Account.liveNappletsFollowLists author-matcher so you can
  scope the list to a people set (All/Follows/custom).
- Each row is a rich card: author avatar + name, title, description, the declared
  capability chips, and the standard reaction bar (reply/boost/like/zap) wired to
  the canonical cache Note — so napplets get the same social actions as any event.

Follow-list plumbing mirrors the existing categories (AccountSettings field +
change fns, LocalPreferences persist/load, FollowListPrefs). LoggedInUserPictureDrawer
is now internal so the new NappletsTopBar can reuse the drawer opener.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 16:42:16 +00:00
Vitor PamplonaandGitHub 9dd2fb5ba9 Merge pull request #3334 from vitorpamplona/claude/modest-carson-lcgcsu
MoqLiteSession: send Ended announce on publisher close race
2026-06-22 12:25:14 -04:00
Claude 03aac0025d fix(nestsClient): write Ended announce when publisher.close() races registerAnnounceBidi
When publisher.close() acquires the gate lock before registerAnnounceBidi,
the announce bidi list is empty and no Ended message is sent. Fix by writing
Ended + finishing the bidi in registerAnnounceBidi when publisherClosed is
already true, mirroring what close() does for bidis it owns at close time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015t3bsJruGerz53yafZyp9n
2026-06-22 15:56:26 +00:00
Claude d60a618653 feat(amy): list an author's napplets / nsites
Add `amy napplet list <author>` and `amy nsite list <author>`: fetch the
author's root + named manifests (15129/35129 for napplets, 15128/35128 for
nsites), keep the latest per identifier, and emit a summary of each (kind, d,
title, description, path count, servers, requires/aggregate, event id,
created_at). Thin assembly over ctx.drain + the quartz manifest accessors.

Harness README notes `amy napplet list` for enumerating what you've published.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 15:38:21 +00:00
Claude 7bc95d836c feat(amy): nsite/napplet serve (local preview); drop standalone publish.sh
Add `amy nsite serve` and `amy napplet serve <author> [--d ID] [--port N]`:
fetch the manifest and serve its content over a local HTTP server, resolving
each request through quartz StaticSiteResolver (blob downloaded from Blossom
and sha256-verified per request, same as the device host) with SPA fallback to
index.html. Lets you open a published site/napplet in a browser to confirm it
loads and routes. (Static content only — a napplet's window.napplet.* runtime
still needs the Amethyst host; documented in the command + harness README.)

Implemented as thin cli glue (StaticSiteServe) over the resolver + commons
BlossomClient + the JDK HTTP server.

Also retire tools/napplet-test/publish.sh now that `amy napplet publish` is the
single source of truth; the harness README documents publish + serve via amy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 15:35:14 +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 b0332c9ff3 test(napplet): add on-device test harness (napplet + publish script)
A self-contained napplet (tools/napplet-test/index.html) that calls every
window.napplet.* API and renders each result on screen, for verifying the
NIP-5D host end to end on a real device — including the new
identity.getList/getZaps/getBadges, identity.onChanged, keys.onAction, and
resource.bytes nostr: paths.

publish.sh uploads it to a Blossom server (BUD-02) and publishes the NIP-5D
named-napplet event (kind 35129) via nak; README documents the flow and a
per-feature verification checklist. Tooling only — no app code or deps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 15:04:28 +00:00
Vitor PamplonaandGitHub 59361c378e Merge pull request #3333 from vitorpamplona/claude/loving-hopper-rn553t
Add comprehensive CLI command suite and Cashu wallet support
2026-06-22 11:03:43 -04:00
Claude 2c4fd48c96 fix(cli): realign amy nutzap relays with upstream NIP-65 inbox model
Upstream reworked kind:10019 to advertise the account's NIP-65 inbox (read)
relays as nutzap-receiving relays (was outbox). Realign amy:
`cashu wallet create` now defaults nutzapRelays to a new
Context.nip65ReadRelays() (kind:10002 read relays, falling back to outbox)
instead of outboxRelays(), so amy's kind:10019 matches the Android wallet's
again. The event's publish destination (anyRelays / sendLiterallyEverywhere)
is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 14:58:51 +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
Claude 188746485d docs(cli): correct nak parity tally (24 full / 3 partial / 7 missing)
Re-introspected the real nak binary: 34 functional commands. Fixes the
stale count — adds nsite to full (amy has NsiteCommands), corrects missing
to 7, and clarifies group/nip29 is an intentional MLS/Marmot divergence,
not a gap. Drops the stale "validate" cheap-win (key validate shipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 14:45:14 +00:00
ClaudeandVitor Pamplona cd14994859 fix(nutzaps): default kind:10019 relay tags to inbox list, drop dm
The advertised `relay` tags in our own kind:10019 now copy the NIP-65
inbox relay list only, rather than inbox + DM. Inbox relays are the
canonical "where others reach me" set, so they are the natural default
for "where to send me nutzaps". DM relays stay in the wallet's inbound
subscription as a safety net, but don't belong in the public kind:10019.

The publish destination of the kind:10019 event itself is unchanged — it
still broadcasts to our outbox via sendLiterallyEverywhere; only the relay
tags inside the event changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHcZ2gv8ro9Q2bEiTSiHD8
2026-06-22 09:38:31 -04:00
ClaudeandVitor Pamplona 33fdd34cf0 fix(nutzaps): advertise inbox+dm relays in kind:10019, not outbox
Complete the NIP-65 outbox-model alignment for NIP-61: the `relay` tags in
our kind:10019 are, per spec, the relays where the recipient *reads*
incoming token events — i.e. inbox-side relays others publish to. We were
advertising our outbox (write) relays there.

Both publish paths (initial wallet creation in CashuWalletViewModel and
P2PK key rotation in CashuWalletState.recreateNutzapKey) now advertise the
union of our NIP-65 inbox + DM relays. This mirrors the relay set the
wallet subscribes to for inbound kind:9321, so senders following our
kind:10019 publish exactly where we listen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHcZ2gv8ro9Q2bEiTSiHD8
2026-06-22 09:38:31 -04: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
vitorpamplonaandVitor Pamplona bd7c5c78cc chore: sync Crowdin translations and seed translator npub placeholders 2026-06-22 09:38:31 -04:00
ClaudeandVitor Pamplona 43565beb2b style: spotless formatting for CardFeedContentState 2026-06-22 09:38:31 -04:00
ClaudeandVitor Pamplona 2918dc5497 fix: replace LocalDate.ofInstant (API 34) with Instant.atZone chain (API 26+) 2026-06-22 09:38:31 -04:00
Claude 7a3c35c645 docs: document cashu, admin, serve, fetch code mode, key validate
Update the amy docs to reflect the new command surface:

- cli/README.md — add the Cashu (NIP-60/61), Relay management (NIP-86
  admin), and Run-a-relay (serve) sections; document fetch's nip19/nip05
  code mode and key validate.
- cli/DEVELOPMENT.md — add cashu.json to the on-disk layout and pin the
  command-family --json contracts (cashu keys/error codes + pointer to the
  cashu plan, admin {relay,method,result}, serve startup object).
- .claude/skills/amy-expert/SKILL.md — extend the "where things live" tree
  with AdminCommand/ServeCommand/cashu/, the commons/cashu + relayManagement
  shared modules, and the allowed :geode dependency.
- .claude/CLAUDE.md — note cli may depend on :geode (for serve), never on
  :amethyst/:desktopApp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-22 02:14:11 +00: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