Files
Vitor PamplonaandClaude Opus 5.5 395af4dc9c chore(skills): consolidate 33 skills into 14, verified against the tree
The 33 descriptions (~16k chars, ~19k with paths) overran Codex's skill-list
budget (2% of the context window, or 8,000 chars when unknown), and a review
found most bodies stale: paths from before the commons/commonsUI moves, the
deleted legacy desktop app, a callback signer API that no longer exists, and
example APIs that were never in the tree.

Kept, rewritten and checked against the code (every path, class and command):
account-state (+feeds), amy-expert, android-expert, auth-signers,
compose-expert (+API style, Navigation 3 back stacks), desktop-expert
(+run/package), event-store-semantics (+NIP-50 search), find-missing-
translations, kotlin-coroutines (+structured concurrency, flow modeling),
kotlin-multiplatform (+Gradle, composable extraction), ngit-pr,
nip85-trusted-assertions, nostr-expert (+NIP lookup, generated NIP catalog),
relay-client.

Dropped: textbook Compose/Kotlin technique skills, kotlin-expert,
kotlin-types-value-class, gradle-expert, quartz-integration (external-facing;
see the Quartz docs commit), find-non-lambda-logs (audit finished; the rule is
now in AGENTS.md Kotlin Style), and the /nip, /desktop-run, /extract commands.
The skill list is now ~4k chars. Also drops .claude/core-skills-plan.md
(stale history) and repoints SearchableEvent's KDoc to event-store-semantics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:21:01 -04:00

3.6 KiB

Rich Text Parsing

Event content (text with URLs, mentions, hashtags, media links, nostr references, invoices, markdown) is parsed headlessly into segments, then rendered by shared composables.

Parser (headless, commons)

All under commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/:

  • RichTextParser.kt: class RichTextParser, parseText(content, tags, callbackUri, authorPubKey) returns a RichTextViewerState.
  • CachedRichTextParser.kt: object CachedRichTextParser, an LRU in front of the parser: parseText(...) (parse or hit), cachedText(...) (hit only, may return null), isMarkdown(content), trimToSize(maxItems). Always parse through this.
  • RichTextParserSegments.kt: RichTextViewerState (urlSet, mediaForPager, mediaList, customEmoji, paragraphs, tags, all Immutable*), ParagraphState / ImageGalleryParagraph, and the Segment subclasses (ImageSegment, VideoSegment, LinkSegment, HashTagSegment, BechSegment, HashIndexUserSegment, InvoiceSegment, CashuSegment, RelayUrlSegment, MathSegment, RegularTextSegment, ...).
  • Patterns.kt: the regex bank (URL, hashtag, mention, email, invoice, cashu, nostr URI). Add a case here instead of writing a regex at a call site.
  • UrlParser.kt: UrlParser / Urls, URL extraction before classification.
  • GalleryParser.kt: groups consecutive media into an ImageGalleryParagraph.
  • MediaContentModels.kt: MediaUrlImage, MediaUrlVideo, MediaUrlPdf and their encrypted siblings EncryptedMediaUrlImage / EncryptedMediaUrlVideo. MediaLocalContent.kt: MediaLocalImage / MediaLocalVideo for not-yet-uploaded drafts.
  • ExpandableTextCutOffCalculator.kt: where to fold long content for "Show more".

Renderer (commonsUI)

  • commonsUI/.../commons/ui/components/RichTextViewer.kt: RichTextViewer(content, ...), the entry point screens call. It checks CachedRichTextParser.isMarkdown, parses through CachedRichTextParser.parseText inside remember(content, tags), and hands the state to the core below.
  • commonsUI/.../commons/ui/richtext/RichTextViewer.kt: the shared core that takes a parsed RichTextViewerState: paragraphs, RTL, word flow, plain text, custom emoji, hashtags.
  • commonsUI/.../commons/ui/richtext/RichTextSegmentRenderer.kt: the seam for segments whose look and action differ per platform (media, gallery, equation, nostr entity, quoted event, user mention, payment, ...), installed through LocalRichTextSegmentRenderer; DefaultRichTextSegmentRenderer.kt is the default. See the README.md in that folder.

To render a new segment type: add the Segment subclass and its detection in the parser, then either draw it in the shared core (if it looks the same everywhere) or add a method to RichTextSegmentRenderer.

NIP-92 imeta enrichment

imeta tags carry per-URL metadata (url, m, dim, blurhash, x, size). The parser maps them onto MediaUrlImage / MediaUrlVideo so the renderer can reserve the aspect ratio and show a blurhash before the image loads. The data comes from the event's tags, not the URL.

Gotchas

  • Never parse per recomposition. Go through CachedRichTextParser; it is keyed by content, tags, callback URI and author.
  • Encrypted media is a subclass. EncryptedMediaUrlImage extends MediaUrlImage (and the video likewise), so an is MediaUrlImage branch also catches encrypted NIP-17 / Blossom media. Test for the encrypted class first wherever it needs decryption.
  • Changing GalleryParser grouping changes the layout of every note with several images.