diff --git a/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx
new file mode 100644
index 0000000..8e70ffb
--- /dev/null
+++ b/src/components/nostr/kinds/RepositoryStateDetailRenderer.tsx
@@ -0,0 +1,198 @@
+import { useMemo } from "react";
+import {
+ GitBranch,
+ GitCommit,
+ Tag,
+ Copy,
+ CopyCheck,
+ FolderGit2,
+} from "lucide-react";
+import { useCopy } from "@/hooks/useCopy";
+import { useGrimoire } from "@/core/state";
+import { useNostrEvent } from "@/hooks/useNostrEvent";
+import type { NostrEvent } from "@/types/nostr";
+import {
+ getRepositoryIdentifier,
+ getRepositoryStateHead,
+ parseHeadBranch,
+ getRepositoryStateHeadCommit,
+ getRepositoryStateBranches,
+ getRepositoryStateTags,
+ getRepositoryName,
+} 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 { addWindow } = useGrimoire();
+ const repoId = useMemo(() => getRepositoryIdentifier(event), [event]);
+ const headRef = useMemo(() => getRepositoryStateHead(event), [event]);
+ const branch = useMemo(() => parseHeadBranch(headRef), [headRef]);
+ const commitHash = useMemo(
+ () => getRepositoryStateHeadCommit(event),
+ [event],
+ );
+ const branches = useMemo(() => getRepositoryStateBranches(event), [event]);
+ const tags = useMemo(() => getRepositoryStateTags(event), [event]);
+
+ // Create repository pointer (kind 30617)
+ const repoPointer = useMemo(
+ () =>
+ repoId
+ ? {
+ kind: 30617,
+ pubkey: event.pubkey,
+ identifier: repoId,
+ }
+ : null,
+ [repoId, event.pubkey],
+ );
+
+ // Fetch the repository event to get its name
+ const repoEvent = useNostrEvent(repoPointer || undefined);
+
+ // Get repository display name
+ const displayName = repoEvent
+ ? getRepositoryName(repoEvent) || repoId || "Repository"
+ : repoId || "Repository";
+
+ const handleRepoClick = () => {
+ if (repoPointer) {
+ addWindow("open", { pointer: repoPointer });
+ }
+ };
+
+ return (
+
+ {/* Repository Header */}
+
+ {/* Name with link to repository */}
+ {repoPointer ? (
+
+
+ {displayName}
+
+ ) : (
+ {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..1cc21d6
--- /dev/null
+++ b/src/components/nostr/kinds/RepositoryStateRenderer.tsx
@@ -0,0 +1,84 @@
+import {
+ BaseEventContainer,
+ type BaseEventProps,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import { GitCommit, FolderGit2 } from "lucide-react";
+import { useGrimoire } from "@/core/state";
+import { useNostrEvent } from "@/hooks/useNostrEvent";
+import {
+ getRepositoryIdentifier,
+ getRepositoryStateHeadCommit,
+ parseHeadBranch,
+ getRepositoryStateHead,
+ getRepositoryName,
+} 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 { addWindow } = useGrimoire();
+ const repoId = getRepositoryIdentifier(event);
+ const headRef = getRepositoryStateHead(event);
+ const branch = parseHeadBranch(headRef);
+ const commitHash = getRepositoryStateHeadCommit(event);
+
+ // Create repository pointer (kind 30617)
+ const repoPointer = repoId
+ ? {
+ kind: 30617,
+ pubkey: event.pubkey,
+ identifier: repoId,
+ }
+ : null;
+
+ // Fetch the repository event to get its name
+ const repoEvent = useNostrEvent(repoPointer || undefined);
+
+ // Get repository display name
+ const repoName = repoEvent
+ ? getRepositoryName(repoEvent) || repoId || "Repository"
+ : repoId || "repository";
+
+ const shortHash = commitHash?.substring(0, 8) || "unknown";
+ const branchName = branch || "unknown";
+
+ const handleRepoClick = () => {
+ if (repoPointer) {
+ addWindow("open", { pointer: repoPointer });
+ }
+ };
+
+ return (
+
+
+ {/* Push notification */}
+
+
+
+
+ pushed{" "}
+
+ {shortHash}
+
+ {" "}
+ to {branchName} in{" "}
+ {repoPointer ? (
+
+
+ {repoName}
+
+ ) : (
+ {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..cac5ec7 100644
--- a/src/lib/nip34-helpers.ts
+++ b/src/lib/nip34-helpers.ts
@@ -281,3 +281,92 @@ 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],
+ }));
+}