diff --git a/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx
new file mode 100644
index 0000000..a3e17dc
--- /dev/null
+++ b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx
@@ -0,0 +1,143 @@
+import { useMemo } from "react";
+import { GitBranch, GitCommit, Tag, Copy, CopyCheck } from "lucide-react";
+import { useCopy } from "@/hooks/useCopy";
+import type { NostrEvent } from "@/types/nostr";
+import {
+ getRepositoryIdentifier,
+ getRepositoryStateHead,
+ parseHeadBranch,
+ getRepositoryStateHeadCommit,
+ getRepositoryStateBranches,
+ getRepositoryStateTags,
+} from "@/lib/nip34-helpers";
+
+/**
+ * Detail renderer for Kind 30618 - Repository State
+ * Displays full repository state with all refs, branches, and tags
+ */
+export function RepositoryStateDetailRenderer({ event }: { event: NostrEvent }) {
+ const repoId = useMemo(() => getRepositoryIdentifier(event), [event]);
+ const headRef = useMemo(() => getRepositoryStateHead(event), [event]);
+ const branch = useMemo(() => parseHeadBranch(headRef), [event, headRef]);
+ const commitHash = useMemo(() => getRepositoryStateHeadCommit(event), [event]);
+ const branches = useMemo(() => getRepositoryStateBranches(event), [event]);
+ const tags = useMemo(() => getRepositoryStateTags(event), [event]);
+
+ const displayName = repoId || "Repository";
+
+ return (
+
+ {/* Repository Header */}
+
+ {/* Name */}
+ {displayName}
+
+ {/* HEAD Info */}
+ {branch && commitHash && (
+
+
+
+ HEAD
+
+
+
+ Branch:
+ {branch}
+
+
+ Commit:
+
+
+
+
+ )}
+
+
+ {/* Branches Section */}
+ {branches.length > 0 && (
+
+
+
+ Branches
+
+
+ {branches.map(({ name, hash }) => (
+ -
+
+ {name}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Tags Section */}
+ {tags.length > 0 && (
+
+
+
+ Tags
+
+
+ {tags.map(({ name, hash }) => (
+ -
+
+ {name}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Raw HEAD Reference */}
+ {headRef && (
+
+
+ HEAD Reference
+
+
+ {headRef}
+
+
+ )}
+
+ );
+}
+
+/**
+ * Component to display a commit hash with copy button
+ */
+function CommitHashItem({ hash }: { hash: string }) {
+ const { copy, copied } = useCopy();
+ const shortHash = hash.substring(0, 8);
+
+ return (
+
+
+ {shortHash}
+
+
+
+ );
+}
diff --git a/src/components/nostr/kinds/RepositoryStateRenderer.tsx b/src/components/nostr/kinds/RepositoryStateRenderer.tsx
new file mode 100644
index 0000000..71cf5f1
--- /dev/null
+++ b/src/components/nostr/kinds/RepositoryStateRenderer.tsx
@@ -0,0 +1,51 @@
+import {
+ BaseEventContainer,
+ type BaseEventProps,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import { GitCommit } from "lucide-react";
+import {
+ getRepositoryIdentifier,
+ getRepositoryStateHeadCommit,
+ parseHeadBranch,
+ getRepositoryStateHead,
+} from "@/lib/nip34-helpers";
+
+/**
+ * Renderer for Kind 30618 - Repository State
+ * Displays as a compact git push notification in feed view
+ */
+export function RepositoryStateRenderer({ event }: BaseEventProps) {
+ const repoId = getRepositoryIdentifier(event);
+ const headRef = getRepositoryStateHead(event);
+ const branch = parseHeadBranch(headRef);
+ const commitHash = getRepositoryStateHeadCommit(event);
+
+ // Format: "pushed <8 chars of HEAD commit> to in "
+ const shortHash = commitHash?.substring(0, 8) || "unknown";
+ const branchName = branch || "unknown";
+ const repoName = repoId || "repository";
+
+ return (
+
+
+ {/* Push notification */}
+
+
+
+ pushed{" "}
+
+ {shortHash}
+ {" "}
+ to {branchName} in{" "}
+ {repoName}
+
+
+
+
+ );
+}
diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx
index a10da0b..fce03f9 100644
--- a/src/components/nostr/kinds/index.tsx
+++ b/src/components/nostr/kinds/index.tsx
@@ -30,6 +30,8 @@ import { CommunityNIPRenderer } from "./CommunityNIPRenderer";
import { CommunityNIPDetailRenderer } from "./CommunityNIPDetailRenderer";
import { RepositoryRenderer } from "./RepositoryRenderer";
import { RepositoryDetailRenderer } from "./RepositoryDetailRenderer";
+import { RepositoryStateRenderer } from "./RepositoryStateRenderer";
+import { RepositoryStateDetailRenderer } from "./RepositoryStateDetailRenderer";
import { Kind39701Renderer } from "./BookmarkRenderer";
import { GenericRelayListRenderer } from "./GenericRelayListRenderer";
import { LiveActivityRenderer } from "./LiveActivityRenderer";
@@ -73,6 +75,7 @@ const kindRenderers: Record> = {
30023: Kind30023Renderer, // Long-form Article
30311: LiveActivityRenderer, // Live Streaming Event (NIP-53)
30617: RepositoryRenderer, // Repository (NIP-34)
+ 30618: RepositoryStateRenderer, // Repository State (NIP-34)
30817: CommunityNIPRenderer, // Community NIP
39701: Kind39701Renderer, // Web Bookmarks (NIP-B0)
};
@@ -128,6 +131,7 @@ const detailRenderers: Record<
30023: Kind30023DetailRenderer, // Long-form Article Detail
30311: LiveActivityDetailRenderer, // Live Streaming Event Detail (NIP-53)
30617: RepositoryDetailRenderer, // Repository Detail (NIP-34)
+ 30618: RepositoryStateDetailRenderer, // Repository State Detail (NIP-34)
30817: CommunityNIPDetailRenderer, // Community NIP Detail
};
diff --git a/src/lib/nip34-helpers.ts b/src/lib/nip34-helpers.ts
index 97aa0e7..c921686 100644
--- a/src/lib/nip34-helpers.ts
+++ b/src/lib/nip34-helpers.ts
@@ -281,3 +281,90 @@ export function getPullRequestRepositoryAddress(
): string | undefined {
return getTagValue(event, "a");
}
+
+// ============================================================================
+// Repository State Event Helpers (Kind 30618)
+// ============================================================================
+
+/**
+ * Get the HEAD reference from a repository state event
+ * @param event Repository state event (kind 30618)
+ * @returns HEAD reference (e.g., "ref: refs/heads/main") or undefined
+ */
+export function getRepositoryStateHead(event: NostrEvent): string | undefined {
+ return getTagValue(event, "HEAD");
+}
+
+/**
+ * Parse HEAD reference to extract branch name
+ * @param headRef HEAD reference string (e.g., "ref: refs/heads/main")
+ * @returns Branch name (e.g., "main") or undefined
+ */
+export function parseHeadBranch(headRef: string | undefined): string | undefined {
+ if (!headRef) return undefined;
+ const match = headRef.match(/^ref:\s*refs\/heads\/(.+)$/);
+ return match ? match[1] : undefined;
+}
+
+/**
+ * Get all git refs from a repository state event
+ * @param event Repository state event (kind 30618)
+ * @returns Array of { ref: string, hash: string } objects
+ */
+export function getRepositoryStateRefs(
+ event: NostrEvent,
+): Array<{ ref: string; hash: string }> {
+ return event.tags
+ .filter((t) => t[0].startsWith("refs/"))
+ .map((t) => ({ ref: t[0], hash: t[1] }));
+}
+
+/**
+ * Get the commit hash that HEAD points to
+ * @param event Repository state event (kind 30618)
+ * @returns Commit hash or undefined
+ */
+export function getRepositoryStateHeadCommit(
+ event: NostrEvent,
+): string | undefined {
+ const headRef = getRepositoryStateHead(event);
+ const branch = parseHeadBranch(headRef);
+ if (!branch) return undefined;
+
+ // Find the refs/heads/{branch} tag
+ const branchRef = `refs/heads/${branch}`;
+ const branchTag = event.tags.find((t) => t[0] === branchRef);
+ return branchTag ? branchTag[1] : undefined;
+}
+
+/**
+ * Get branches from repository state refs
+ * @param event Repository state event (kind 30618)
+ * @returns Array of { name: string, hash: string } objects
+ */
+export function getRepositoryStateBranches(
+ event: NostrEvent,
+): Array<{ name: string; hash: string }> {
+ return event.tags
+ .filter((t) => t[0].startsWith("refs/heads/"))
+ .map((t) => ({
+ name: t[0].replace("refs/heads/", ""),
+ hash: t[1],
+ }));
+}
+
+/**
+ * Get tags from repository state refs
+ * @param event Repository state event (kind 30618)
+ * @returns Array of { name: string, hash: string } objects
+ */
+export function getRepositoryStateTags(
+ event: NostrEvent,
+): Array<{ name: string; hash: string }> {
+ return event.tags
+ .filter((t) => t[0].startsWith("refs/tags/"))
+ .map((t) => ({
+ name: t[0].replace("refs/tags/", ""),
+ hash: t[1],
+ }));
+}