diff --git a/src/components/repo/RepoPulls.tsx b/src/components/repo/RepoPulls.tsx
new file mode 100644
index 0000000..ff8accf
--- /dev/null
+++ b/src/components/repo/RepoPulls.tsx
@@ -0,0 +1,57 @@
+import type { Did } from '@atcute/lexicons'
+import { IconGitPullRequest, IconMessageCircle } from '@tabler/icons-react'
+import { useRepoPulls } from '../../hooks/useRepoPulls'
+import { SurfaceCard } from '../shared/SurfaceCard'
+
+type RepoPullsProps = {
+ repoDid: Did
+}
+
+export function RepoPulls({ repoDid }: RepoPullsProps) {
+ const { pulls, error } = useRepoPulls(repoDid)
+
+ if (error) {
+ return
Could not load pulls: {error.message}
+ }
+
+ if (pulls === null) {
+ return Loading pulls...
+ }
+
+ if (pulls.items.length === 0) {
+ return No pulls found.
+ }
+
+ return (
+
+ {pulls.items.map((pull) => {
+ const stateClassName =
+ pull.state === 'open'
+ ? 'text-ctp-teal'
+ : pull.state === 'merged'
+ ? 'text-ctp-mauve'
+ : 'text-ctp-overlay-1'
+
+ return (
+ -
+
+
+
+
+ {pull.state}
+
+
+
+
+ {pull.commentCount}
+
+
+
+ {pull.value.title}
+
+
+ )
+ })}
+
+ )
+}
diff --git a/src/hooks/useRepoPulls.ts b/src/hooks/useRepoPulls.ts
new file mode 100644
index 0000000..2d3f631
--- /dev/null
+++ b/src/hooks/useRepoPulls.ts
@@ -0,0 +1,26 @@
+import type { Did } from '@atcute/lexicons'
+import { useEffect, useState } from 'react'
+import { listPulls, type PullList } from '../lib/tangled/repo'
+
+const INITIAL_PULL_LIMIT = 20
+
+export function useRepoPulls(repoDid: Did) {
+ const [pulls, setPulls] = useState(null)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ async function loadPulls() {
+ setError(null)
+ try {
+ const result = await listPulls(repoDid, { limit: INITIAL_PULL_LIMIT })
+ setPulls(result)
+ } catch (caught) {
+ setError(caught instanceof Error ? caught : new Error('Unable to load pulls.'))
+ }
+ }
+
+ void loadPulls()
+ }, [repoDid])
+
+ return { pulls, error }
+}
diff --git a/src/lib/tangled/repo/index.ts b/src/lib/tangled/repo/index.ts
index 470db78..4df9535 100644
--- a/src/lib/tangled/repo/index.ts
+++ b/src/lib/tangled/repo/index.ts
@@ -9,6 +9,8 @@ export type { RepoLogOptions } from './getRepoLog'
export { getRepoTree } from './getRepoTree'
export { listIssues } from './listIssues'
export type { Issue, IssueList, ListIssuesOptions } from './listIssues'
+export { listPulls } from './listPulls'
+export type { ListPullsOptions, Pull, PullList } from './listPulls'
export { listRepos } from './listRepos'
export { getRepoName, getRepoRkey } from './types'
export type { Repo, RepoList } from './types'
diff --git a/src/lib/tangled/repo/listPulls.ts b/src/lib/tangled/repo/listPulls.ts
new file mode 100644
index 0000000..f65d1f0
--- /dev/null
+++ b/src/lib/tangled/repo/listPulls.ts
@@ -0,0 +1,57 @@
+import { ok } from '@atcute/client'
+import type { Did } from '@atcute/lexicons'
+import { safeParse } from '@atcute/lexicons'
+import type { Main as TangledPull } from '@atcute/tangled/types/repo/pull'
+import { mainSchema as pullSchema } from '@atcute/tangled/types/repo/pull'
+import type { $output as PullListResponse } from '@atcute/tangled/types/repo/listPulls'
+import { mainSchema as pullListSchema } from '@atcute/tangled/types/repo/listPulls'
+import { rpc } from '../client'
+import { removeNullCursor } from '../utils'
+
+export type Pull = Omit & {
+ value: TangledPull
+}
+
+export type PullList = {
+ items: Pull[]
+ cursor?: string
+}
+
+export type ListPullsOptions = {
+ cursor?: string
+ limit?: number
+ order?: 'asc' | 'desc'
+ status?: 'open' | 'closed' | 'merged'
+}
+
+export async function listPulls(repoDid: Did, options: ListPullsOptions = {}): Promise {
+ const response = await ok(
+ rpc.get('sh.tangled.repo.listPulls', {
+ params: {
+ subject: repoDid,
+ cursor: options.cursor,
+ limit: options.limit,
+ order: options.order,
+ status: options.status,
+ },
+ }),
+ )
+
+ const listValidation = safeParse(pullListSchema.output.schema, removeNullCursor(response))
+ if (!listValidation.ok) {
+ throw new Error(`Bobbin returned an invalid pull list: ${listValidation.message}`)
+ }
+
+ const items = listValidation.value.items.map((item, index) => {
+ const valueValidation = safeParse(pullSchema, item.value)
+ if (!valueValidation.ok) {
+ throw new Error(
+ `Bobbin returned an invalid pull record at index ${index}: ${valueValidation.message}`,
+ )
+ }
+
+ return { ...item, value: valueValidation.value }
+ })
+
+ return { items, cursor: listValidation.value.cursor }
+}
diff --git a/src/pages/RepoPage.tsx b/src/pages/RepoPage.tsx
index 0299dfd..e505516 100644
--- a/src/pages/RepoPage.tsx
+++ b/src/pages/RepoPage.tsx
@@ -3,6 +3,7 @@ import { useParams, useSearchParams } from 'react-router-dom'
import { PageContainer } from '../components/layout/PageContainer'
import { ProfileByline } from '../components/profile/ProfileByline'
import { RepoIssues } from '../components/repo/RepoIssues'
+import { RepoPulls } from '../components/repo/RepoPulls'
import { RepoReadme } from '../components/repo/RepoReadme'
import { parseRepoSection } from '../components/repo/repoSections'
import { RepoTabs } from '../components/repo/RepoTabs'
@@ -77,7 +78,9 @@ export function RepoPage() {
{activeSection === 'issues' && repo.value.repoDid !== undefined && (
)}
- {activeSection === 'pulls' && }
+ {activeSection === 'pulls' && repo.value.repoDid !== undefined && (
+
+ )}
{activeSection === 'pipelines' && }