From 4a4ed9bc2de913d2e2774f5cbfb9beff0c370176 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 5 May 2026 17:33:11 -0500 Subject: [PATCH] Split nostr-kinds skill into design and rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The combined skill conflated two unrelated jobs: (1) design-time decisions when authoring a new kind (NIP-vs-custom, ranges, tag design, NIP.md) and (2) implementation-time checklist for wiring rendering into Ditto's many UI touchpoints. The single description sentence was unwieldy, and its trigger ('introducing a new kind... or registering a kind in the UI') was phrased around the author's perspective — it didn't match user phrasing like 'support displaying kind X' or 'render NIP-Y', so I skipped loading it when implementing NIP-84 and missed half the registration points. Split into: - nostr-kind-design — NIP-vs-custom decision, kind ranges, tag design, content-vs-tags, NIP.md. Loads when minting or extending a schema. - nostr-kind-rendering — the multi-location UI registration checklist. Loads when rendering a kind Ditto doesn't yet display, or when asked to 'support / display / render' a NIP or kind number. Expanded the rendering checklist with the points I missed during the NIP-84 pass: the six-file notification stack, the four-file AppConfig triple for feed-toggle keys, sidebar icon registration, AppRouter route wiring, shouldHideFeedEvent spam guards, and a 'bugs that signal a missed step' section so the checklist reads as diagnostic too. Also flagged the embedded-previews trap where skipping the dispatcher branch silently feeds quoted prose through the kind-1 tokenizer. Updated both AGENTS.md references to point at the two new skills. --- .agents/skills/nostr-kind-design/SKILL.md | 89 +++++++++ .agents/skills/nostr-kind-rendering/SKILL.md | 185 +++++++++++++++++++ .agents/skills/nostr-kinds/SKILL.md | 119 ------------ AGENTS.md | 7 +- 4 files changed, 279 insertions(+), 121 deletions(-) create mode 100644 .agents/skills/nostr-kind-design/SKILL.md create mode 100644 .agents/skills/nostr-kind-rendering/SKILL.md delete mode 100644 .agents/skills/nostr-kinds/SKILL.md diff --git a/.agents/skills/nostr-kind-design/SKILL.md b/.agents/skills/nostr-kind-design/SKILL.md new file mode 100644 index 00000000..9bcc854d --- /dev/null +++ b/.agents/skills/nostr-kind-design/SKILL.md @@ -0,0 +1,89 @@ +--- +name: nostr-kind-design +description: Decide whether to reuse an existing NIP or mint a new kind, design tag structures that relays can index, choose what goes in content vs. tags, and document new kinds or extensions in NIP.md. Load when authoring a new schema — not when wiring up rendering for a kind that already exists (use nostr-kind-rendering for that). +--- + +# Nostr Kinds — Design and Schema + +Load this skill when: + +- Minting a new event kind for a Ditto feature. +- Extending an existing NIP with new tags. +- Deciding whether an existing NIP covers a use case or whether a custom kind is warranted. +- Documenting a custom kind or extension in `NIP.md`. + +**Not this skill** — if an existing NIP/kind covers your use case and you only need to render it in Ditto's UI, use the **`nostr-kind-rendering`** skill instead. + +## Choosing Between Existing NIPs and Custom Kinds + +1. **Thorough NIP review first.** Browse the NIP index, then read candidate NIPs in detail. The goal is to find the closest existing solution. +2. **Prefer extending existing NIPs** over creating custom kinds, even at the cost of minor schema compromises. Custom kinds fragment the ecosystem. +3. **When an existing NIP is close but not perfect**, use its kind as the base and add domain-specific tags. Document the extension in `NIP.md`. +4. **Only mint a new kind** when no existing NIP covers the core functionality, the data structure is fundamentally different, or the use case requires different storage characteristics (regular vs. replaceable vs. addressable). +5. **If a tool to generate a new kind number is available, you MUST call it.** Never pick an arbitrary number. +6. **Custom kinds MUST include a NIP-31 `alt` tag** with a human-readable description of the event's purpose. + +**Example decision:** + +``` +Need: Equipment marketplace for farmers +Options: + 1. NIP-15 (Marketplace) — too structured for peer-to-peer sales + 2. NIP-99 (Classifieds) — good fit, extensible with farming tags + 3. Custom kind — perfect fit, no interoperability + +Decision: NIP-99 + farming-specific tags. +``` + +## Kind Ranges + +An event's kind number determines storage semantics: + +- **Regular** (1000 ≤ kind < 10000) — stored permanently by relays. Notes, articles, etc. +- **Replaceable** (10000 ≤ kind < 20000) — only the latest event per `pubkey+kind` is kept. Profile metadata, contact lists, mute lists. +- **Addressable** (30000 ≤ kind < 40000) — identified by `pubkey+kind+d-tag`; only the latest per combo is kept. Long-form content, products, definitions. + +Kinds below 1000 are "legacy"; storage is per-kind (e.g. kind 1 is regular, kind 3 is replaceable). + +## Tag Design Principles + +- **Kind = schema, tags = semantics.** Don't mint a new kind just to represent a different category of the same data. +- **Relays only index single-letter tags.** Use `t` for categories so filters like `'#t': ['electronics']` work at the relay level. Multi-letter tags (`product_type`, etc.) force inefficient client-side filtering. +- **Filter at the relay**, not in JavaScript: + + ```ts + // ❌ Fetch everything, filter locally + const events = await nostr.query([{ kinds: [30402] }]); + const filtered = events.filter((e) => hasTag(e, 'product_type', 'electronics')); + + // ✅ Filter at the relay + const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]); + ``` + +- **For Ditto-specific niches** (community apps, regional variants), tag events with a `t` value and query on it. Don't do this for generic platforms — it would silo content. + +## Content vs. Tags + +- **`content`** — large freeform text or existing industry-standard JSON (GeoJSON, FHIR, Tiled maps). Kind 0 is the one exception where structured JSON goes in content. +- **Tags** — queryable metadata, structured data, anything you might filter on. +- **Empty content is fine.** `content: ""` is idiomatic for tag-only events. +- **If you need to filter by a field, it must be a tag** — relays don't index content. + +```json +// ✅ Queryable +{ "kind": 30402, "content": "", + "tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]] } + +// ❌ Structured data buried in content +{ "kind": 30402, "content": "{\"title\":\"Camera\",\"price\":250}", "tags": [["d", "product-123"]] } +``` + +## `NIP.md` + +`NIP.md` documents Ditto's custom kinds and any extensions to existing NIPs. Whenever you mint a new kind or change a custom schema, **create or update `NIP.md`** with the tag list, content format, and intended usage. If a kind you add is effectively the same shape as an existing NIP, note the NIP reference rather than duplicating the spec. + +Standard NIPs (like NIP-84 Highlights, NIP-23 Articles) do **not** go in `NIP.md` — only Ditto-custom kinds and Ditto-specific extensions. + +## After Designing — What's Next? + +Once you've settled on a kind number and tag shape, you still need to render it in Ditto's UI. Load the **`nostr-kind-rendering`** skill for the full multi-location registration checklist (feed cards, detail pages, embedded previews, kind-label maps, notifications, feed-toggle registration). diff --git a/.agents/skills/nostr-kind-rendering/SKILL.md b/.agents/skills/nostr-kind-rendering/SKILL.md new file mode 100644 index 00000000..23b1d9e3 --- /dev/null +++ b/.agents/skills/nostr-kind-rendering/SKILL.md @@ -0,0 +1,185 @@ +--- +name: nostr-kind-rendering +description: Add UI rendering for an event kind Ditto doesn't yet display — feed cards, detail pages, embedded previews, notifications, routes, feed-toggle registration, and the several kind-label maps (KIND_LABELS, KIND_HEADER_MAP, NOTIFICATION_KIND_NOUNS, CommentContext) that must stay in sync. Load when asked to "support / display / render" a NIP or kind number, when a kind renders blank or as "Kind 12345", or when quote embeds of a kind show "This event kind is not supported". +--- + +# Nostr Kinds — UI Rendering Checklist + +Ditto's kind dispatch is **spread across many files** by design — feed cards, detail pages, embedded previews, notifications, and several kind-label maps each have their own rendering requirements. The central `KIND_LABELS` registry covers the easy cases, but most context-specific maps (grammar, icons, verbs) cannot be derived mechanically and must be updated manually. + +**Missing any location causes visible bugs**: a kind might render blank in quote posts, show "Kind 12345" as a label, skip its action header, tombstone as "This event kind is not supported" in embeds, or — worst of all — have its content fed through the kind-1 tokenizer and auto-linkify URLs/hashtags that weren't authored by the event creator. + +**When in doubt, grep for an existing kind number like `30617` or `9802`** — you'll find every registration point you need to mirror. + +## Decision: Feed-toggle + dedicated page, or just rendering? + +Before touching code, pick one: + +- **Just render it everywhere Nostr content appears** (no feed toggle, no dedicated page). Use when the kind is niche or only reached via direct links / quote embeds. Minimal surface — steps 1–6 below. +- **Add a feed toggle + optional dedicated page.** Use when users should be able to browse events of this kind or opt them in/out of their home feed. Requires the feed registration (step 7) and `AppConfig` triple (step 8). + +When the user asks generally to "support" a kind, ask which direction they want if it's not obvious from context. + +## Checklist + +### 1. Content card component (`src/components/`) + +Create `` that renders the event's tags/content appropriately. + +- **Never run event content through the kind-1 tokenizer** (`` / ``) unless the kind's content is actually free-form user prose. Quote-type content (highlights, snippets, citations) contains URLs and hashtags from the *source*, not the event author — tokenizing them is misleading. +- Render plaintext with `whitespace-pre-wrap break-words` inside a `

` instead. +- Route any event-sourced URLs (`r` tags, media URLs, source links) through `sanitizeUrl()` from `@/lib/sanitizeUrl` before using them in `href`/`src`. +- Support an `expanded` prop if the card looks different on the detail page than in the feed. + +### 2. Feed card dispatch (`src/components/NoteCard.tsx`) + +Three edits in this file: + +1. **Flag block** (around lines 384–435): add `const isMyKind = event.kind === XXXX;`. +2. **`isTextNote` negation list** (around lines 440–475): add `&& !isMyKind`. Without this, unknown kinds fall through to `UnknownKindContent` (showing only the `alt` tag). +3. **Content dispatch ternary** (around lines 578–692): add `) : isMyKind ? (`. +4. **`KIND_HEADER_MAP`** (around lines 1710+): add an entry so the feed shows "Alice shared a *noun*" or similar. Pattern: + ```ts + 9802: { + icon: Highlighter, + action: "shared a", + noun: "highlight", + nounRoute: "/highlights", // omit if no dedicated page + }, + ``` +5. Import the card component and any new lucide icons. + +### 3. Detail page dispatch (`src/pages/PostDetailPage.tsx`) + +Mirror the three NoteCard edits: + +1. **Flag block** (around lines 1021–1098): `const isMyKind = event.kind === XXXX;`. +2. **`isTextNote` negation list**: add `&& !isMyKind`. +3. **Content dispatch ternary** (around lines 2147–2251): add `) : isMyKind ? (`. + +The loading-state title uses `shellTitleForKind()`, which falls through to `KIND_LABELS` — no override needed unless the kind belongs to a group ("Music Details") or needs composite grammar. + +### 4. Central kind label (`src/lib/kindLabels.ts`) + +Add a **capitalized noun phrase, no articles** to the `KIND_LABELS` map: + +```ts +9802: 'Highlight', +``` + +This is consumed by the detail-page loading title, nsite permission prompt, signer nudge toasts, and addressable-event preview headers. Ignoring this gives "Kind 9802" everywhere it appears. + +### 5. Context-specific label and icon maps + +Each of these maps exists because the surrounding UI needs a different grammatical form. They are **not derived** from `KIND_LABELS` and must be updated manually. + +- **`src/components/CommentContext.tsx`** — `KIND_LABELS` (uses articles: `'a highlight'`, `'an article'`) and `KIND_ICONS` (lucide component reference). Rendered as "Commenting on {label}". Without an entry you get "an unsupported event". +- **`src/pages/NotificationsPage.tsx`** — `NOTIFICATION_KIND_NOUNS` (bare lowercase nouns: `'highlight'`, `'article'`). Rendered as "reacted to your {noun}". Without an entry you get "post" as a fallback. +- **`src/components/NoteCard.tsx`** — `KIND_HEADER_MAP` (already covered in step 2). + +### 6. Embedded previews (`src/components/EmbeddedNote.tsx`) + +The quote-embed dispatcher in `EmbeddedNote` (around lines 65–110) routes kinds to dedicated compact cards. **Without a branch here, non-content kinds fall through to `EmbeddedNoteCard`, which either:** + +- Shows only the NIP-31 `alt` tag (if present), or +- Tombstones as "This event kind is not supported", or +- **Feeds the event's `content` through the kind-1 tokenizer** if the kind is mistakenly treated as a content-kind — auto-linkifying URLs and hashtags that weren't authored by the event creator. This is a security/UX bug. + +For any kind whose `content` isn't freeform user prose, add an explicit dispatch branch even if it just renders a minimal compact card. Pattern: + +```tsx +if (event.kind === 9802) { + return ; +} +``` + +Then define the compact card using `EmbeddedCardShell` for the author row + navigation, and render the kind-specific body inside. See `EmbeddedHighlightCard` and `EmbeddedBadgeAwardCard` for reference. + +`src/components/EmbeddedNaddr.tsx` works similarly for addressable kinds — add a branch there if your kind is addressable. + +### 7. Feed/sidebar registration (`src/lib/extraKinds.ts`) + +Only needed if you decided on "feed-toggle + dedicated page" above. Add an `ExtraKindDef`: + +```ts +{ + kind: 9802, + id: 'highlights', + showKey: 'showHighlights', + feedKey: 'feedIncludeHighlights', + label: 'Highlights', + description: 'Noteworthy excerpts from articles, posts, and the web (NIP-84)', + route: 'highlights', // omit for feed-only registration + addressable: false, + section: 'social', // feed | media | social | development | whimsy + blurb: 'Longer marketing copy shown in the info modal.', +}, +``` + +Then: + +- **Sidebar icon** (`src/lib/sidebarItems.tsx`) — add `{ id: "highlights", label: "Highlights", path: "/highlights", icon: Highlighter }` to `SIDEBAR_ITEMS`, and import the icon at the top. `CONTENT_KIND_ICONS` picks up the icon automatically from the sidebar definition. +- **Route** (`src/AppRouter.tsx`) — add `const highlightsDef = getExtraKindDef("highlights")!;` at the top of the file and a `} />` above the catch-all `*` route. + +### 8. `AppConfig` triple (required if you added feed/sidebar toggle keys in step 7) + +Three files must stay in sync, or the build fails or the setting silently no-ops: + +1. **`src/contexts/AppContext.ts`** — add the fields to the `FeedSettings` interface with JSDoc comments. +2. **`src/lib/schemas.ts`** — add the same fields to `FeedSettingsSchema` as `z.boolean().optional()`. `DittoConfigSchema` is derived from `AppConfigSchema` with `.strict()` mode, so any `ditto.json` field missing from Zod is a build error. +3. **`src/App.tsx`** — add the default value in the initial `feedSettings` block. +4. **`src/test/TestApp.tsx`** — mirror the default in test config so component tests work. + +Convention: `show*` toggles default to `true` (sidebar entries visible), `feedInclude*` toggles default to `false` for niche content, `true` for core feed content. + +### 9. Notification integration (if applicable) + +Load this step when the kind represents an interaction with the user's content (reactions, reposts, highlights, awards, etc.) — i.e. when an event author "does something with" another user's content via an `e`/`a`/`p` tag. + +**Six files** to update: + +1. **`src/hooks/useEncryptedSettings.ts`** — add `highlights?: boolean` (or equivalent) to the `notificationPreferences` object. +2. **`src/lib/notificationKinds.ts`** — add the kind to `ALL_NOTIFICATION_KINDS` and add a `if (p.X !== false) kinds.push(XXXX);` line in `getEnabledNotificationKinds`. +3. **`src/lib/notificationTemplates.ts`** — add a `NOTIFICATION_TEMPLATES` entry with a title and body for nostr-push server-side notifications. +4. **`src/pages/NotificationSettings.tsx`** — extend `NotificationPrefKey` union, add a row to `NOTIFICATION_TYPES` with icon/label/description/kinds. +5. **`src/hooks/useNotifications.ts`** — extend `groupKey` (decide if events of this kind group by referenced event or stand alone), and if it's a "did something to your content" kind, add it to the author-ownership filter so users only get notified for interactions with their own content. +6. **`src/pages/NotificationsPage.tsx`** — add a case to `GroupedNotificationView`'s switch; write `MyKindNotification` + `MyKindNotificationGroup` components modeled on `RepostNotification` / `LikeNotification`. + +### 10. Spam guards (`src/lib/feedUtils.ts`) + +If the kind has required tags (NIP-spec-mandated references, minimum content, etc.), add a check in `shouldHideFeedEvent` to hide events that don't meet the minimum bar. This pre-filters events before `NoteCard` mounts them, avoiding layout shifts from components that would return `null`. + +Example: + +```ts +// NIP-84 highlights with no excerpt AND no source reference. +if (event.kind === 9802) { + const hasContent = event.content.trim().length > 0; + const hasSource = event.tags.some(([n]) => n === 'a' || n === 'e' || n === 'r'); + if (!hasContent && !hasSource) return true; +} +``` + +### 11. `NIP.md` (custom kinds only) + +If the kind is a Ditto-custom kind or a Ditto-specific extension of an existing NIP, document it in `NIP.md` — see the **`nostr-kind-design`** skill for the format. Standard NIPs (like NIP-84, NIP-23) do not go in `NIP.md`. + +## Validation + +After making changes, run `npm run test` — it runs `tsc --noEmit`, `eslint`, `vitest`, and `vite build` in sequence. All must pass. Additions to the `AppConfig` triple in particular frequently break the build if one of the four files is missed. + +## Why so many locations? + +These are genuinely different UI contexts (feed cards, detail pages, embedded previews, comment-context labels, notifications, sidebar routes) with different rendering requirements and grammar needs. The central `KIND_LABELS` in `src/lib/kindLabels.ts` handles the common "what to call this kind" case, but feed headers, comment-context text, and notification verbs each need their own grammar, and notification integration involves a whole independent subsystem. + +## Bugs that signal a missed step + +- **"Kind 12345" shown as a label** → step 4 (`KIND_LABELS`). +- **"an unsupported event" in CommentContext** → step 5 (`CommentContext` maps). +- **"reacted to your **post**"** when it should say "highlight" → step 5 (`NOTIFICATION_KIND_NOUNS`). +- **No action header above a feed card** → step 2.4 (`KIND_HEADER_MAP`). +- **Blank / `alt`-only card in quote embeds** → step 6 (`EmbeddedNote` dispatcher). +- **URLs/hashtags in quoted text auto-linkified** → step 6 (embedded dispatcher forgot to bypass the kind-1 tokenizer). +- **Kind doesn't appear in the home feed even with the toggle on** → step 7 (`ExtraKindDef` missing `feedKey`). +- **Build error mentioning a missing `FeedSettings` field** → step 8 (one of the three files out of sync). +- **Users not notified when their content is interacted with** → step 9 (notification stack). diff --git a/.agents/skills/nostr-kinds/SKILL.md b/.agents/skills/nostr-kinds/SKILL.md deleted file mode 100644 index f76509dd..00000000 --- a/.agents/skills/nostr-kinds/SKILL.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: nostr-kinds -description: Decide whether to reuse an existing NIP or mint a new kind, design tag structures that relays can index, choose what goes in content vs. tags, and register a new kind in Ditto's many UI touchpoints (feed cards, detail pages, embedded previews, kind-label maps). ---- - -# Nostr Kinds — Design and Registration - -Use this skill when introducing a new kind to Ditto, extending an existing NIP with new tags, or deciding whether an existing NIP covers a feature. It covers the decision framework, schema rules, and — critically — the full list of places a new kind must be registered in Ditto's UI. - -## Choosing Between Existing NIPs and Custom Kinds - -1. **Thorough NIP review first.** Browse the NIP index, then read candidate NIPs in detail. The goal is to find the closest existing solution. -2. **Prefer extending existing NIPs** over creating custom kinds, even at the cost of minor schema compromises. Custom kinds fragment the ecosystem. -3. **When an existing NIP is close but not perfect**, use its kind as the base and add domain-specific tags. Document the extension in `NIP.md`. -4. **Only mint a new kind** when no existing NIP covers the core functionality, the data structure is fundamentally different, or the use case requires different storage characteristics (regular vs. replaceable vs. addressable). -5. **If a tool to generate a new kind number is available, you MUST call it.** Never pick an arbitrary number. -6. **Custom kinds MUST include a NIP-31 `alt` tag** with a human-readable description of the event's purpose. - -**Example decision:** - -``` -Need: Equipment marketplace for farmers -Options: - 1. NIP-15 (Marketplace) — too structured for peer-to-peer sales - 2. NIP-99 (Classifieds) — good fit, extensible with farming tags - 3. Custom kind — perfect fit, no interoperability - -Decision: NIP-99 + farming-specific tags. -``` - -## Kind Ranges - -An event's kind number determines storage semantics: - -- **Regular** (1000 ≤ kind < 10000) — stored permanently by relays. Notes, articles, etc. -- **Replaceable** (10000 ≤ kind < 20000) — only the latest event per `pubkey+kind` is kept. Profile metadata, contact lists, mute lists. -- **Addressable** (30000 ≤ kind < 40000) — identified by `pubkey+kind+d-tag`; only the latest per combo is kept. Long-form content, products, definitions. - -Kinds below 1000 are "legacy"; storage is per-kind (e.g. kind 1 is regular, kind 3 is replaceable). - -## Tag Design Principles - -- **Kind = schema, tags = semantics.** Don't mint a new kind just to represent a different category of the same data. -- **Relays only index single-letter tags.** Use `t` for categories so filters like `'#t': ['electronics']` work at the relay level. Multi-letter tags (`product_type`, etc.) force inefficient client-side filtering. -- **Filter at the relay**, not in JavaScript: - - ```ts - // ❌ Fetch everything, filter locally - const events = await nostr.query([{ kinds: [30402] }]); - const filtered = events.filter((e) => hasTag(e, 'product_type', 'electronics')); - - // ✅ Filter at the relay - const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]); - ``` - -- **For Ditto-specific niches** (community apps, regional variants), tag events with a `t` value and query on it. Don't do this for generic platforms — it would silo content. - -## Content vs. Tags - -- **`content`** — large freeform text or existing industry-standard JSON (GeoJSON, FHIR, Tiled maps). Kind 0 is the one exception where structured JSON goes in content. -- **Tags** — queryable metadata, structured data, anything you might filter on. -- **Empty content is fine.** `content: ""` is idiomatic for tag-only events. -- **If you need to filter by a field, it must be a tag** — relays don't index content. - -```json -// ✅ Queryable -{ "kind": 30402, "content": "", - "tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]] } - -// ❌ Structured data buried in content -{ "kind": 30402, "content": "{\"title\":\"Camera\",\"price\":250}", "tags": [["d", "product-123"]] } -``` - -## `NIP.md` - -`NIP.md` documents Ditto's custom kinds and any extensions to existing NIPs. Whenever you mint a new kind or change a custom schema, **create or update `NIP.md`** with the tag list, content format, and intended usage. If a kind you add is effectively the same shape as an existing NIP, note the NIP reference rather than duplicating the spec. - -## Registering a New Kind in the Ditto UI - -When adding support for a new kind, the kind must be registered in **multiple locations** or it will render incorrectly in certain views (blank content in quote posts, "Kind 12345" as a label, missing action headers, etc.). - -### Checklist - -1. **Content card component** (`src/components/`) — create `` that renders the event's tags/content appropriately. - -2. **Feed rendering** (`src/components/NoteCard.tsx`): - - Add `const isMyKind = event.kind === XXXX;`. - - Include it in the appropriate group flag (e.g. `isDevKind`) or the `isTextNote` exclusion list. - - Add the content dispatch: `isMyKind ? : …`. - - Add an entry to `KIND_HEADER_MAP` for the action header (e.g. "deployed an nsite"). - - Import the new component and any new icons (e.g. `Globe` from `lucide-react`). - -3. **Detail page** (`src/pages/PostDetailPage.tsx`): - - Mirror the `isMyKind` detection and group/exclusion flags from `NoteCard`. - - Add the content dispatch for the detail view. - - `shellTitleForKind()` falls through to the central `KIND_LABELS` registry, so adding a label there is sufficient for the loading-state title. Only add a manual override in `shellTitleForKind()` if the kind belongs to a group (e.g. music kinds → "Track Details") or needs a composite label (e.g. "Badge Collection"). - - Import the new component. - -4. **Feed registration** (`src/lib/extraKinds.ts`): - - Add the kind number to an existing feed definition's `extraFeedKinds` array, or create a new `ExtraKindDef` entry. - -5. **Central kind label registry** (`src/lib/kindLabels.ts`): - - Add an entry to the `KIND_LABELS` map with a short, user-facing label (capitalized noun phrase, no articles). - - This registry is the single source of truth for kind→label mappings and is consumed by the nsite permission prompt, signer nudge toasts, detail page loading titles, and addressable event preview headers. - - Some UI contexts maintain **context-specific** label maps that cannot use the central registry directly (they need different grammar): - - `KIND_LABELS` and `KIND_ICONS` in `src/components/CommentContext.tsx` — uses articles ("a post", "an article") for "Commenting on {label}" text. - - `NOTIFICATION_KIND_NOUNS` in `src/pages/NotificationsPage.tsx` — uses bare lowercase nouns for notification action text. - - `KIND_HEADER_MAP` in `src/components/NoteCard.tsx` — uses action verbs + nouns for feed headers. - - These context-specific maps must also be updated when adding a new kind. - -6. **Embedded note cards** (`src/components/EmbeddedNote.tsx`, `src/components/EmbeddedNaddr.tsx`) — small preview cards shown inside quote posts, reply-context indicators, and CommentContext hover cards. They are **separate components** from `NoteCard` and render a minimal preview (author + title/content + attachment indicators). Basic rendering works for all kinds automatically, but kinds whose media lives in tags (e.g. kind 20 photos via `imeta` tags) may need attachment-indicator logic added to `EmbeddedNoteCard`. - - > Do not confuse these with the `compact` prop on `NoteCard` — that just hides action buttons on the full `NoteCard`. `EmbeddedNote`/`EmbeddedNaddr` are entirely different components. - -7. **Reply composer** (`src/components/ReplyComposeModal.tsx`) — `EmbeddedPost` delegates to the shared `EmbeddedNote`/`EmbeddedNaddr` components, so no per-kind registration is needed here. - -### Why so many places? - -These are genuinely different UI contexts (feed cards, detail pages, embedded previews, reply previews, comment-context labels) with different rendering requirements. The central `KIND_LABELS` in `src/lib/kindLabels.ts` handles the common case, but several contexts need grammar-specific maps (articles, verbs, lowercase nouns) that can't be derived mechanically. **When in doubt, grep the codebase for an existing kind number like `30617`** — you'll find every registration point you need to mirror. diff --git a/AGENTS.md b/AGENTS.md index 4431b3b2..79fc5596 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ Ditto is a Nostr client built with React 19.x, TailwindCSS 3.x, Vite, shadcn/ui, - `/public/` — static assets. - `App.tsx` — **already configured** with `QueryClientProvider`, `NostrProvider`, `UnheadProvider`, `AppProvider`, `NostrLoginProvider`, `NWCContext`. Read before editing; changes are rarely needed. - `AppRouter.tsx` — React Router configuration. -- `NIP.md` — custom kinds documented by this project (see the `nostr-kinds` skill). +- `NIP.md` — custom kinds documented by this project (see the `nostr-kind-design` skill). **Always read an existing file before modifying it.** Never overwrite `App.tsx`, `AppRouter.tsx`, or `NostrProvider` without first reading their contents. @@ -54,7 +54,10 @@ By default `nostr` uses the app's connection pool (reads from one relay, publish ### Kinds, Tags, and NIP.md -When introducing a new kind, extending an existing NIP with new tags, or registering a kind in the UI (feed cards, detail pages, embedded previews, kind-label maps), load the **`nostr-kinds`** skill. It covers the NIP-vs-custom-kind decision framework, kind ranges, tag design (single-letter indexed tags, content vs. tags), the `NIP.md` documentation requirement, and Ditto's multi-location UI registration checklist. +Two skills split the work of working with kinds: + +- **`nostr-kind-design`** — load when minting a new kind, extending an existing NIP with new tags, or deciding whether an existing NIP covers a use case. Covers the NIP-vs-custom decision framework, kind ranges, tag design (single-letter indexed tags, content vs. tags), and the `NIP.md` documentation requirement. +- **`nostr-kind-rendering`** — load when adding UI for an event kind Ditto doesn't yet display, when asked to "support" / "display" / "render" a specific NIP or kind number, or when a kind renders blank / as "Kind 12345" / as "This event kind is not supported". Covers Ditto's multi-location UI registration checklist — feed cards, detail pages, embedded previews, kind-label maps (`KIND_LABELS`, `KIND_HEADER_MAP`, `NOTIFICATION_KIND_NOUNS`, `CommentContext`), notifications, routes, and the `AppConfig` triple that must stay in sync. Summary rules: