Commit Graph
934 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub ceca5d52ae Merge pull request #3394 from vitorpamplona/claude/composable-memory-ci-build-jxpwkv
Move UI components to commons module for better code sharing
2026-06-26 20:12:09 -04:00
Claude 94677d1677 refactor: extract self-contained layout/preview leaves to :commons
Continues moving genuinely platform-agnostic leaves out of the :amethyst app
module so they compile once in :commons instead of across all six app variants.

Moved (no Android coupling, no foundation deps):
  - ui/layouts/DisappearingBarState, DisappearingBarNestedScroll, PaddingMerge
    -> commons commonMain (com.vitorpamplona.amethyst.commons.ui.layouts)
  - ui/components/UrlPreviewState
    -> commons jvmAndroid (it references commons.preview.UrlInfoItem, which
       lives in the jvmAndroid source set)

Consumers (incl. the existing DisappearingBar*Test unit tests, which stay in
:amethyst and now import from commons) updated to the new packages. No behavior
change.

Verified: :commons, :amethyst compilePlayDebugKotlin + compilePlayDebugUnitTest,
and :desktopApp:compileKotlin build clean; spotless applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
2026-06-27 00:06:52 +00:00
Claude 8b74a3e24e refactor: extract 7 pure leaf composables from :amethyst to :commons
First Tier A slice of the UI/components extraction. Moves the genuinely
platform-agnostic leaf composables — those with zero :amethyst
dependencies and no Android coupling — from the app module into the
shared :commons KMP module (commonMain):

  ClickableTexts, ForwardingPainter, GenericLoadable, GlowingCard,
  LoadingAnimation, TranslationConfig, ZonedSwipeModifier  (~616 LOC)

These now live under com.vitorpamplona.amethyst.commons.ui.components and
compile once in :commons (cacheable, incremental) instead of being part
of every one of the six :amethyst variant compilations (play/fdroid ×
debug/release/benchmark). That shrinks the app-module Kotlin compilation
unit — the root cause of the CI Kotlin-daemon OOM — rather than renting
headroom with heap flags.

Consumers updated to import from the new package; no behavior change.
Verified: :commons, :amethyst compile{Play,Fdroid}DebugKotlin, and
:desktopApp:compileKotlin all build clean; spotless applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
2026-06-26 22:29:37 +00:00
Vitor PamplonaandClaude Opus 4.8 1dbfd78b44 fix(napplet): route brokered resource fetches by the applet's own Tor mode
The consolidation passed `useProxy = true` for every brokered `resource.bytes`
fetch, forcing them through Tor whenever Tor was active — regardless of the
napplet/nSite's actual network mode. That overrides the user's explicit choice:
an nSite running in "open web" mode would still have its blob fetches tunneled,
inconsistent with how its own WebView page loads.

The authoritative per-applet preference already exists main-side in
NappletNetworkRegistry.useTor(coordinate) (locked napplets pinned to Tor;
nSites follow the persisted per-site toggle, which relaunches on change) — the
same source NappletLauncher reads to set the WebView proxy. Thread the calling
applet's coordinate through NappletResourceGateway.fetch so the broker can
resolve it, and pick the shared client with
getHttpClient(useProxy = NappletNetworkRegistry.useTor(coordinate)). This
mirrors the host's own `effectiveProxy = if (useTor) proxyPort else -1` exactly,
so a brokered fetch now routes like the applet's page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:32:57 -04:00
Claude 5a297f17a1 fix: move orphaned browser/napplet translations to :commons
The browser/napplet strings (browser_address_hint, browser_console_title,
browser_console_title_short, browser_console_clear, napplet_untitled) were
moved to :commons, but their per-locale translations were left behind in
amethyst's values-*/strings.xml. With the default keys gone from amethyst,
lint flagged them as ExtraTranslation (80 errors across 16 locales).

Move the translations into commons/src/androidMain/res/values-*/strings.xml
so the default key and its translations live in the same module, preserving
the existing translation work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uza7sGxYPZtY43Ln2yH8FQ
2026-06-26 18:58:08 +00:00
Vitor PamplonaandGitHub 356e1431d8 Merge pull request #3378 from vitorpamplona/fix/embed-ime-selection
Embedded text selection: native-parity IME + selection UI + magnifier, and platform-bug fixes
2026-06-26 11:08:42 -04:00
Claude 3c041d2915 feat: modern git cards for pull requests, plus issue/patch polish
NIP-34 pull requests (kind 1618) and pull-request updates (kind 1619)
previously had no renderer and fell through to the plain text-note
branch, so a PR notification opened onto an unstyled markdown blob with
none of its structured data shown. Add dedicated cards that reuse the
existing patch/issue card vocabulary (bordered container, type chip,
status pill, embedded repository header) and surface the PR-specific
metadata the event carries:

- Pull Request card: "Pull Request" chip with a merge glyph, status
  pill, subject title, branch name, current commit, merge base, and
  clone-URL download rows.
- PR Update card: "PR Update" chip, repository header, new commit /
  merge base, clone URLs, and an explanatory line (updates carry no
  body content).

While here, modernize the existing cards consistently:

- Factor the shared markdown body, metadata row, and subject title into
  reusable composables (GitMarkdownBody / GitMetaRow / GitSubjectTitle).
- Show the issue subject as a proper title. The old code cast the event
  to TextNoteEvent to read the subject, which always returned null
  (GitIssueEvent is not a TextNoteEvent), so issue subjects were never
  displayed; read it from GitIssueEvent.subject() instead.
- Render the patch commit as an iconed metadata row.

Wire the two new kinds into NoteCompose and the thread detail view, add
the CallMerge / Commit / AltRoute Material symbols (font subset
regenerated), and add the new string resources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCXL6btUeZZmSP1TCYkKh
2026-06-26 13:39:05 +00:00
Vitor PamplonaandClaude Opus 4.8 aa520c361f feat(embed): hybrid word+char granularity for in-field selection handles
Completes parity #5. `fieldExtend` kept a pure word-snap, so the in-field
start/end handles couldn't be fine-tuned to a single character. Now it keeps
per-drag state (reset on a >250ms gap or edge switch) and matches native
`Editor` word-selection drags: the gesture baselines at the current selection
edge, sweeping past that word's far boundary snaps to the next whole word
(never stopping mid-gap), and moving within / back from the furthest-reached
word gives character precision. Symmetric for both handles. Page-text extend
stays character-granular.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:25:55 -04:00
Vitor PamplonaandClaude Opus 4.8 dec1e0bec1 feat(embed): selection loupe + native-parity fixes for embedded text selection
Builds out host-drawn text selection for embedded napplet/nsite/browser
surfaces toward native parity, and fixes the bugs found while exercising it.

- Magnifier loupe (#4): EmbeddedMagnifier + provider-side pixel capture
  (EmbeddedMagnifierProbe) shipped over IPC for both embed paths; the
  caret/selection handles drive it via OnMagnify.
- SelectionUiState: single source of truth for the overlay show/hide rules
  (insertion caret / in-field range / page-text range + dragging/scrolling).
- EmbeddedSelectionDrag: suspends the nav drawer's edge swipe while a handle
  is dragged (auto-scroll #9).

Bug fixes:
- No more overlay blink on word-select: the shim's selection-reveal scrolls
  (a textarea auto-scrolling to show a forming/re-asserted range) no longer
  trip the hide-on-scroll path, and the hide self-heals instead of being
  re-armed indefinitely.
- RemoteImeView debounces the range-lost signal so a transient collapse that
  gets re-asserted doesn't flicker the handles/toolbar.
- Focusing a field clears any page-text selection (shim + host), so the stale
  page handles/Copy bar no longer linger above — and stop stealing drags from —
  the field overlays; also cancels any in-flight scroll-hide on focus.
- Caret insertion-handle drag now actually moves the caret: read the pointer
  delta with positionChangeIgnoreConsumed() before consuming, so the value
  isn't zeroed by our own consume (or the sandbox surface consuming the move).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:25:55 -04:00
Vitor PamplonaandClaude Opus 4.8 baddd5b3bd fix(embed): IME typing + host-drawn text selection for embedded surfaces
Embedded WebView surfaces (:napplet process, SurfaceControlViewHost) can't
host the soft keyboard or present Chrome's own selection UI, so editing and
selection are relayed to the main process. This lands the working set of that
relay:

- shim.js: fix React-controlled input erase by writing through the native
  HTMLInputElement/HTMLTextAreaElement value setter (so React's value tracker
  stays in sync); make the host authoritative for selection re-assert (the
  editable selectionchange handler only mirrors); report field/caret geometry
  and page-text selection geometry; add pageExtend + caret coords (border-width
  corrected) for drag-to-extend and the insertion handle.
- RemoteImeView: land caret where tapped on focus (requestFocus before applying
  remote state); host-authoritative selection re-assert within a time window;
  setText only when text actually changed; wire copy/cut/paste/select-all and
  edit callbacks.
- EmbeddedTabLayer: stop resizing the surface on IME show (removes the ~1s
  first-letter freeze); draw the selection overlay — toolbar, teardrop
  selection handles, and the insertion (cursor) handle.
- EmbeddedImeBridge / Embedded{Napplet,Browser}Controller: carry selection +
  caret geometry and the page-selection event across the Messenger channel.

Known limitation (not fixed here): after a field's page is opened in its own
full-screen activity and the user returns, the selection-highlight paint stays
off across all embedded surfaces. DOM selection, the toolbar, and copy still
work — only the native highlight is gone. This is a WebView/Chromium behavior
in off-window surfaces and is not reachable from the app layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:25:55 -04:00
Claude f2a7548434 fix: move shared browser/napplet strings to :commons to eliminate cross-module duplicate
AGP 9.2.1 treats resources defined in both an app module and a library module
with the same key (qualifiers="") as an error in non-debug builds.

`browser_address_hint`, `browser_console_title_short`, `browser_console_title`,
`browser_console_clear`, and `napplet_untitled` were defined in both
`:amethyst/values/strings.xml` and `:nappletHost/values/strings.xml`.
Both `:amethyst` and `:nappletHost` depend on `:commons`, so the canonical
home for these shared strings is `commons/src/androidMain/res/values/strings.xml`.

Update callers in both modules to use `com.vitorpamplona.amethyst.commons.R as
CommonsR`. Locale translations in amethyst's `values-*/` directories remain as
Android resource overlays (app module overrides library module at merge time).

Fixes: Found item String/browser_console_clear more than one time (packageFdroidBenchmarkResources)
2026-06-26 00:11:31 +00:00
Claude c3a57f94a6 fix(icons): nudge Cashu icon another 0.3 left
Shift the mark a further 0.3 units left (net -1.6 from the original).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEFq9cUPECuB8F56sTtWXu
2026-06-25 21:16:56 +00:00
Claude 1ea1b369ea fix(icons): nudge Cashu icon another 0.1 left
Shift the mark a further 0.1 units left (net -1.3 from the original).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEFq9cUPECuB8F56sTtWXu
2026-06-25 21:13:35 +00:00
Claude 7b3c5325ff fix(icons): ease Cashu icon shift back to 5% left
10% felt over-shifted; move the mark right by 1.2 units so the net offset
from the original is -1.2 (5% of the 24x24 viewport) instead of -2.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEFq9cUPECuB8F56sTtWXu
2026-06-25 21:09:30 +00:00
Claude 782935320e fix(icons): recenter Cashu outline icon by shifting it 10% left
The cashew + pixel-shades mark was visually weighted to the right of the
24x24 viewport. Shift every x-coordinate in both paths (the stroked cashew
body outline and the filled pixel sunglasses) by -2.4 units so the drawing
sits balanced within the icon bounds. Shape is unchanged; only position.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEFq9cUPECuB8F56sTtWXu
2026-06-25 21:07:04 +00:00
Claude 9a064984a5 refactor: rename web-app surfaces to distinguish them from the native App Store
The in-app "app" surfaces had colliding, sometimes inaccurate names. NIP-89
native apps showed as "Apps" while the in-app favorites showed as "Favorite
apps", and the single-app host screens were named `FavoriteWebAppScreen` /
`FavoriteNappletScreen` even though they open any url/coordinate (favorited or
not) and the "Napplet" one also renders nSites.

Establish one taxonomy:
- Native app store -> "App Store" (NIP-89), in-app Nostr web client -> "Web app",
  plus the existing nApplet / nSite. "Favorite" is now only the star toggle and
  the pinned grid, not a screen.
- Code axis: `WebApp` (url-based) and `NostrApp` (coordinate-based nSite/nApplet).
  The cross-process sandbox infra (`napplet/`, `nappletHost/`) keeps "Napplet".

Renames:
- Routes `FavoriteWebApp`/`FavoriteNostrApp` -> `WebApp`/`NostrApp`
- Screens `FavoriteWebAppScreen`/`FavoriteNappletScreen` -> `WebAppScreen`/`NostrAppScreen`
- Controllers `EmbeddedBrowserController`/`EmbeddedNappletController`
  -> `EmbeddedWebAppController`/`EmbeddedNostrAppController`
- Factory `acquireBrowser`/`acquireNapplet` -> `acquireWebApp`/`acquireNostrApp`
- Model `FavoriteApp.WebUrl` -> `FavoriteApp.WebApp`; `WebUrlNetworkRegistry`
  -> `WebAppNetworkRegistry`
- Strings: `software_apps`/`route_software_apps` "Apps" -> "App Store";
  `favorite_apps_empty` reworded to name web app / nApplet / nSite

Persistence is untouched: favorite id prefixes ("url:"/"nostr:"), DataStore
names ("favorite_apps"/"weburl_network"), and serialized type tags ("url"/
"nostr") are all kept stable. Taxonomy documented in
amethyst/plans/2026-06-25-web-app-naming.md.

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h2c44rAwexuUEP3kky2F3
2026-06-24 20:11:28 +00:00
Claude 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