account-state
Account, LocalCache, User/Note and feeds in commons/model + commons/feeds: per-user XState objects (.flow),…
Compose-scoped relay subscriptions in commons/relayClient: observeUser*/EventFinder, FilterAssemblers, EOSE managers, ComposeSubscriptionManager. Use when a screen must load events from relays while visible.
Claude Code
$ npx -y skills add vitorpamplona/amethyst --skill relay-client --agent claude-code
Codex
$ npx skills add vitorpamplona/amethyst --skill relay-client -a codex -y
Cursor
$ npx skills add vitorpamplona/amethyst --skill relay-client -a cursor -y
Gemini CLI
$ npx skills add vitorpamplona/amethyst --skill relay-client -a gemini-cli -y
GitHub Copilot
$ npx skills add vitorpamplona/amethyst --skill relay-client -a github-copilot -y
OpenCode
$ npx skills add vitorpamplona/amethyst --skill relay-client -a opencode -y
Windsurf
$ npx skills add vitorpamplona/amethyst --skill relay-client -a windsurf -y
name: relay-client description: Compose-scoped relay subscriptions in commons/relayClient: observeUser*/EventFinder, FilterAssemblers, EOSE managers, ComposeSubscriptionManager. Use when a screen must load events from relays while visible.
How a composable gets events from relays only while it is on screen. Headless code (amy, geode, tests, one-shot fetches) does not use this layer: it uses the `INostrClient` extensions — see AGENTS.md › Feature Workflow ("Relay client ops already exist").
headless half: one package per feature (`chess/`, `communities/`, `home/`, `thread/`, `user/`, `event/`, …) holding query states, filter assemblers and sub-assemblers; `composeSubscriptionManagers/`, `eoseManagers/`, `subscriptions/` (`ExplainedFilter`, `SubPurpose`), `assemblers/` (shared ones: `MetadataFilterAssembler`, `ReactionsFilterAssembler`, `ContactCardFilters.kt`), `oneshot/`, `paging/`, `preload/`.
(`account`, `home`, `userFinder`, `eventFinder`, `chess`, `community`, …). Screens reach it as `accountViewModel.dataSources()` (`BaseAccountViewModel.dataSources`).
`@Composable` entry points, same package names: `observeUser*`, `EventFinderFilterAssemblerSubscription`, `KeyDataSourceSubscription`, `LifecycleAwareKeyDataSourceSubscription`, and per-feature `*FilterAssemblerSubscription`s.
Before writing any assembler, check whether these already load what you need:
`observeUserInfo`, `observeUserPicture`, `observeUserBanner`, `observeUserAboutMe`, `observeUserName(user)` each open `UserFinderFilterAssemblerSubscription(user)` and return a `State`. Metadata, relay lists, reports and contact cards load for on-screen users only, batched into one REQ per relay for the whole visible set. A `LazyColumn` composes only the visible window, so this is "load what's on screen" for free.
`EventFinderFilterAssemblerSubscription(note)` loads the note's reactions, zaps, reposts and replies while composed; the observers in `event/EventObservers.kt` / `AccountEventObservers.kt` sit on top of it.
once in `commonsUI/.../ui/app/AmethystApp.kt` from `dataSources()`. Their defaults are `error(...)`, so they must never be reached from a composition with no relay client (e.g. the Android `:napplet` process).
Don't hand-roll "load metadata for this list". `FeedMetadataCoordinator` and `MetadataPreloader` (`assemblers/`, `preload/`) still exist but have no production caller; prefer the observers.
// composeSubscriptionManagers/ComposeSubscriptionManager.kt
interface Subscribable<T> {
fun subscribe(query: T?) // called on main: keep it fast
fun unsubscribe(query: T?)
fun subscribe(query: List<T>)
fun unsubscribe(query: List<T>)
}
abstract class ComposeSubscriptionManager<T> : ComposeSubscriptionManagerControls, Subscribable<T>
// keeps the live query states; allKeys() snapshots them; each change calls invalidateKeys()
interface ComposeSubscriptionManagerControls { fun invalidateKeys(); fun invalidateFilters(); fun destroy() }`MutableComposeSubscriptionManager<T : MutableQueryState>` is the variant for query states that change over time (it collects their flow).
A **filter assembler** is a `ComposeSubscriptionManager` that groups **sub-assemblers**; each sub-assembler is an EOSE manager that turns the current keys into relay filters. They are not pure: they own subscriptions on the `INostrClient` and per-relay EOSE cursors. Real shape (`chess/ChessGameFilterAssembler.kt`):
class ChessGameFilterAssembler(cache: ICacheProvider, client: INostrClient) : ComposeSubscriptionManager<ChessGameQueryState>() {
val group = listOf(ChessGameSubAssembler(cache, client, ::allKeys))
override fun invalidateKeys() = invalidateFilters()
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
override fun destroy() = group.forEach { it.destroy() }
}EOSE managers (`eoseManagers/`, all on `BaseEoseManager(client, allKeys, sampleTime = 500)`, which batches invalidations): `SingleSubEoseManager` (one REQ for all keys, one shared EOSE cursor per relay — `MetadataFilterAssembler` uses it), `PerUserEoseManager`, `PerKeyEoseManager`, `PerUniqueIdEoseManager`, `PerUserAndFollowListEoseManager`, `SingleSubNoEoseCacheEoseManager`, `AccountScopedSingleSubNoEoseCacheEoseManager`. Pick by how the EOSE cursor must be shared. Details and the new-screen recipe: `references/filter-assemblers.md`.
// commonsUI/.../relayClient/communities/CommunityFilterAssemblerSubscription.kt
@Composable
fun CommunityFilterAssemblerSubscription(channel: AddressableNote, filterAssembler: CommunityFilterAssembler) {
val state = remember(channel) { CommunityQueryState(channel) } // different screens get different states
LifecycleAwareKeyDataSourceSubscription(state, filterAssembler)
}
// call site: CommunityFilterAssemblerSubscription(note, accountViewModel.dataSources().community)unsubscribe on dispose. For light, always-needed subscriptions.
the lifecycle stops (cancelled if it restarts), and immediately on dispose. Use it for feeds and anything he
Account, LocalCache, User/Note and feeds in commons/model + commons/feeds: per-user XState objects (.flow),…
amy CLI in cli/: adding an `amy <verb>` command, Main.kt dispatch, Output.emit/--json contract, exit codes 0/1/2/124, ~/.amy…
Android shim in `amethyst/`: MainActivity, AndroidManifest/intent filters, :napplet process, play/fdroid flavors, R8 +…
NostrSigner in Amethyst: NostrSignerInternal (local key), NostrSignerRemote (NIP-46 bunker), NostrSignerExternal (NIP-55 Android…
Shared Compose UI in `commonsUI/`: screens, components, the nav shell (`ScreenLayoutSpec`, `AppShellLayout`, Navigation 3 back…
Desktop JVM shim in desktopApp/: AmethystDesktop window, DesktopMenuBar, platform ports, OS keyring; :desktopApp:run, packaging.…
Decompile Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineer Android apps, extract HTTP…