Skip to content

Commit 7939492

Browse files
sweetmantechRecoup Agentclaude
authored
agent: @U0AJM7X8FBR Admin + Docs + API - we want to update the /coding page in (#22)
* feat: show merged PR status on /coding page Updates the coding agent Slack tags page to surface which pull requests have been merged, using the new GET /api/admins/coding/pr endpoint. - types/coding-agent.ts — CodingPrStatus, CodingPrStatusResponse types - lib/recoup/fetchCodingPrStatus.ts — HTTP client for the new endpoint - hooks/useCodingPrStatus.ts — React Query hook, returns Set of merged URLs - lib/coding-agent/getTagsByDate.ts — adds merged_pr_count per date - components/Admin/AdminLineChart.tsx — adds thirdLine prop support - components/CodingAgentSlackTags/SlackTagsColumns.tsx — ship 🚢 emoji for merged PRs - components/CodingAgentSlackTags/SlackTagsTable.tsx — accepts mergedPrUrls prop - components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx — wires it all together: merged PRs top stat, "PRs Merged" chart line, ship emoji in table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: update CodingPrStatus to use status enum instead of merged boolean Aligns with API change from { merged: boolean } to { status: "open" | "closed" | "merged" }. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: rename SlackTagsColumns to createSlackTagsColumns, remove deprecated export Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add tag-to-merged-PR conversion rate metric Shows percentage of Slack tags that resulted in a merged PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Recoup Agent <agent@recoupable.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0e1af72 commit 7939492

File tree

9 files changed

+220
-94
lines changed

9 files changed

+220
-94
lines changed

components/Admin/AdminLineChart.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
type ChartConfig,
1111
} from "@/components/ui/chart";
1212

13-
interface SecondLine {
13+
interface ExtraLine {
1414
data: Array<{ date: string; count: number }>;
1515
label: string;
1616
color?: string;
@@ -20,14 +20,16 @@ interface AdminLineChartProps {
2020
title: string;
2121
data: Array<{ date: string; count: number }>;
2222
label?: string;
23-
secondLine?: SecondLine;
23+
secondLine?: ExtraLine;
24+
thirdLine?: ExtraLine;
2425
}
2526

2627
export default function AdminLineChart({
2728
title,
2829
data,
2930
label = "Count",
3031
secondLine,
32+
thirdLine,
3133
}: AdminLineChartProps) {
3234
if (data.length === 0) return null;
3335

@@ -36,14 +38,19 @@ export default function AdminLineChart({
3638
...(secondLine
3739
? { count2: { label: secondLine.label, color: secondLine.color ?? "#6B8E93" } }
3840
: {}),
41+
...(thirdLine
42+
? { count3: { label: thirdLine.label, color: thirdLine.color ?? "#4A90A4" } }
43+
: {}),
3944
} satisfies ChartConfig;
4045

41-
// Merge primary and secondary data by date
46+
// Merge primary, secondary, and tertiary data by date
4247
const secondMap = new Map(secondLine?.data.map((d) => [d.date, d.count]) ?? []);
48+
const thirdMap = new Map(thirdLine?.data.map((d) => [d.date, d.count]) ?? []);
4349
const mergedData = data.map((d) => ({
4450
date: d.date,
4551
count: d.count,
4652
...(secondLine ? { count2: secondMap.get(d.date) ?? 0 } : {}),
53+
...(thirdLine ? { count3: thirdMap.get(d.date) ?? 0 } : {}),
4754
}));
4855

4956
return (
@@ -77,7 +84,7 @@ export default function AdminLineChart({
7784
/>
7885
}
7986
/>
80-
{secondLine && <ChartLegend content={<ChartLegendContent />} />}
87+
{(secondLine || thirdLine) && <ChartLegend content={<ChartLegendContent />} />}
8188
<Line
8289
dataKey="count"
8390
type="monotone"
@@ -94,6 +101,15 @@ export default function AdminLineChart({
94101
dot={{ fill: "var(--color-count2)", r: 4 }}
95102
/>
96103
)}
104+
{thirdLine && (
105+
<Line
106+
dataKey="count3"
107+
type="monotone"
108+
stroke="var(--color-count3)"
109+
strokeWidth={2}
110+
dot={{ fill: "var(--color-count3)", r: 4 }}
111+
/>
112+
)}
97113
</LineChart>
98114
</ChartContainer>
99115
</div>

components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from "react";
44
import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb";
55
import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink";
66
import { useSlackTags } from "@/hooks/useSlackTags";
7+
import { useCodingPrStatus } from "@/hooks/useCodingPrStatus";
78
import SlackTagsTable from "./SlackTagsTable";
89
import AdminLineChart from "@/components/Admin/AdminLineChart";
910
import { getTagsByDate } from "@/lib/coding-agent/getTagsByDate";
@@ -15,6 +16,10 @@ import type { AdminPeriod } from "@/types/admin";
1516
export default function CodingAgentSlackTagsPage() {
1617
const [period, setPeriod] = useState<AdminPeriod>("all");
1718
const { data, isLoading, error } = useSlackTags(period);
19+
const { data: mergedPrUrls } = useCodingPrStatus(data?.tags);
20+
21+
const tagsByDate = data ? getTagsByDate(data.tags, mergedPrUrls) : [];
22+
const totalMergedPrs = mergedPrUrls?.size ?? 0;
1823

1924
return (
2025
<main className="mx-auto max-w-6xl px-4 py-10">
@@ -47,6 +52,16 @@ export default function CodingAgentSlackTagsPage() {
4752
<span className="font-semibold text-gray-900 dark:text-gray-100">{data.total_pull_requests}</span>{" "}
4853
total PRs
4954
</span>
55+
<span>
56+
<span className="font-semibold text-gray-900 dark:text-gray-100">{totalMergedPrs}</span>{" "}
57+
merged PRs
58+
</span>
59+
<span>
60+
<span className="font-semibold text-gray-900 dark:text-gray-100">
61+
{data.total > 0 ? Math.round((totalMergedPrs / data.total) * 100) : 0}%
62+
</span>{" "}
63+
conversion
64+
</span>
5065
</div>
5166
)}
5267
</div>
@@ -74,17 +89,23 @@ export default function CodingAgentSlackTagsPage() {
7489
<>
7590
<AdminLineChart
7691
title="Tags & Pull Requests Over Time"
77-
data={getTagsByDate(data.tags).map((d) => ({ date: d.date, count: d.count }))}
92+
data={tagsByDate.map((d) => ({ date: d.date, count: d.count }))}
7893
label="Tags"
7994
secondLine={{
80-
data: getTagsByDate(data.tags).map((d) => ({
81-
date: d.date,
82-
count: d.pull_request_count,
83-
})),
95+
data: tagsByDate.map((d) => ({ date: d.date, count: d.pull_request_count })),
8496
label: "Tags with PRs",
8597
}}
98+
thirdLine={
99+
mergedPrUrls
100+
? {
101+
data: tagsByDate.map((d) => ({ date: d.date, count: d.merged_pr_count })),
102+
label: "PRs Merged",
103+
color: "#22863a",
104+
}
105+
: undefined
106+
}
86107
/>
87-
<SlackTagsTable tags={data.tags} />
108+
<SlackTagsTable tags={data.tags} mergedPrUrls={mergedPrUrls} />
88109
</>
89110
)}
90111
</main>

components/CodingAgentSlackTags/SlackTagsColumns.tsx

Lines changed: 0 additions & 75 deletions
This file was deleted.

components/CodingAgentSlackTags/SlackTagsTable.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,24 @@ import {
1616
TableHeader,
1717
TableRow,
1818
} from "@/components/ui/table";
19-
import { slackTagsColumns } from "./SlackTagsColumns";
19+
import { createSlackTagsColumns } from "./createSlackTagsColumns";
2020
import type { SlackTag } from "@/types/coding-agent";
2121

2222
interface SlackTagsTableProps {
2323
tags: SlackTag[];
24+
mergedPrUrls?: Set<string>;
2425
}
2526

26-
export default function SlackTagsTable({ tags }: SlackTagsTableProps) {
27+
export default function SlackTagsTable({ tags, mergedPrUrls }: SlackTagsTableProps) {
2728
const [sorting, setSorting] = useState<SortingState>([
2829
{ id: "timestamp", desc: true },
2930
]);
3031

32+
const columns = createSlackTagsColumns(mergedPrUrls);
33+
3134
const table = useReactTable({
3235
data: tags,
33-
columns: slackTagsColumns,
36+
columns,
3437
state: { sorting },
3538
onSortingChange: setSorting,
3639
getCoreRowModel: getCoreRowModel(),
@@ -66,7 +69,7 @@ export default function SlackTagsTable({ tags }: SlackTagsTableProps) {
6669
))
6770
) : (
6871
<TableRow>
69-
<TableCell colSpan={slackTagsColumns.length} className="h-24 text-center">
72+
<TableCell colSpan={columns.length} className="h-24 text-center">
7073
No results.
7174
</TableCell>
7275
</TableRow>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { type ColumnDef } from "@tanstack/react-table";
2+
import { SortableHeader } from "@/components/SandboxOrgs/SortableHeader";
3+
import type { SlackTag } from "@/types/coding-agent";
4+
5+
export function createSlackTagsColumns(mergedPrUrls?: Set<string>): ColumnDef<SlackTag>[] {
6+
return [
7+
{
8+
id: "user_name",
9+
accessorKey: "user_name",
10+
header: "Tagged By",
11+
cell: ({ row }) => {
12+
const tag = row.original;
13+
return (
14+
<div className="flex items-center gap-2">
15+
{tag.user_avatar && (
16+
<img
17+
src={tag.user_avatar}
18+
alt={tag.user_name}
19+
className="h-6 w-6 rounded-full"
20+
/>
21+
)}
22+
<span className="font-medium">{tag.user_name}</span>
23+
</div>
24+
);
25+
},
26+
},
27+
{
28+
id: "prompt",
29+
accessorKey: "prompt",
30+
header: "Prompt",
31+
cell: ({ getValue }) => (
32+
<span className="max-w-md truncate block text-sm text-gray-700 dark:text-gray-300">
33+
{getValue<string>()}
34+
</span>
35+
),
36+
},
37+
{
38+
id: "channel_name",
39+
accessorKey: "channel_name",
40+
header: "Channel",
41+
cell: ({ getValue }) => (
42+
<span className="text-sm text-gray-500 dark:text-gray-400">#{getValue<string>()}</span>
43+
),
44+
},
45+
{
46+
id: "pull_requests",
47+
accessorKey: "pull_requests",
48+
header: "Pull Requests",
49+
cell: ({ getValue }) => {
50+
const prs = getValue<string[]>();
51+
if (!prs?.length) return <span className="text-sm text-gray-400"></span>;
52+
return (
53+
<div className="flex flex-col gap-1">
54+
{prs.map((url) => {
55+
const isMerged = mergedPrUrls?.has(url);
56+
return (
57+
<a
58+
key={url}
59+
href={url}
60+
target="_blank"
61+
rel="noopener noreferrer"
62+
className="text-sm text-blue-600 hover:underline dark:text-blue-400"
63+
>
64+
{isMerged ? "🚢 " : ""}
65+
{url.match(/github\.com\/[^/]+\/([^/]+)\/pull\/(\d+)/)?.slice(1).join("#")}
66+
</a>
67+
);
68+
})}
69+
</div>
70+
);
71+
},
72+
},
73+
{
74+
id: "timestamp",
75+
accessorKey: "timestamp",
76+
header: ({ column }) => <SortableHeader column={column} label="Timestamp" />,
77+
cell: ({ getValue }) => new Date(getValue<string>()).toLocaleString(),
78+
sortingFn: "basic",
79+
},
80+
];
81+
}

hooks/useCodingPrStatus.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
import { usePrivy } from "@privy-io/react-auth";
5+
import { fetchCodingPrStatus } from "@/lib/recoup/fetchCodingPrStatus";
6+
import type { SlackTag } from "@/types/coding-agent";
7+
8+
/**
9+
* Fetches the merged status for all PR URLs found in the given Slack tags.
10+
* Returns a Set of merged PR URLs for fast lookup.
11+
*/
12+
export function useCodingPrStatus(tags: SlackTag[] | undefined) {
13+
const { ready, authenticated, getAccessToken } = usePrivy();
14+
15+
const allPrUrls = [...new Set(tags?.flatMap((tag) => tag.pull_requests ?? []) ?? [])];
16+
17+
return useQuery({
18+
queryKey: ["admin", "coding-agent", "pr-status", allPrUrls],
19+
queryFn: async () => {
20+
const token = await getAccessToken();
21+
if (!token) throw new Error("Not authenticated");
22+
const res = await fetchCodingPrStatus(token, allPrUrls);
23+
return new Set(res.pull_requests.filter((pr) => pr.status === "merged").map((pr) => pr.url));
24+
},
25+
enabled: ready && authenticated && allPrUrls.length > 0,
26+
});
27+
}

0 commit comments

Comments
 (0)