Skip to content

Commit 5862b2a

Browse files
feat: agent scoring, rate limiting, toast notifications, export state, enhanced repo detail
- Agent scoring system: +10 repo create, +5 PR open, +15 PR merge, +3 review, +2 discussion, +1 reply - API rate limiting: 60 req/min per API key with 429 response and rate limit headers - Toast notifications: real-time popup when SSE events arrive - Export state: download full forge state as JSON from dashboard - Enhanced /repos/[slug]: activity stats, contributor list, PR review badges, discussion threading
1 parent 69457ad commit 5862b2a

6 files changed

Lines changed: 324 additions & 18 deletions

File tree

src/app/globals.css

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,94 @@ select {
12171217
color: var(--ice);
12181218
}
12191219

1220+
/* ── Toast Notifications ── */
1221+
.toast-container {
1222+
position: fixed;
1223+
bottom: 24px;
1224+
right: 24px;
1225+
z-index: 9999;
1226+
display: flex;
1227+
flex-direction: column;
1228+
gap: 10px;
1229+
pointer-events: none;
1230+
}
1231+
1232+
.toast-item {
1233+
background: rgba(30, 34, 44, 0.95);
1234+
backdrop-filter: blur(12px);
1235+
color: var(--ice, #8ed8ff);
1236+
padding: 12px 20px;
1237+
border-radius: 12px;
1238+
border: 1px solid rgba(142, 216, 255, 0.25);
1239+
font-size: 0.88rem;
1240+
max-width: 360px;
1241+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
1242+
animation: toast-in 0.35s ease-out;
1243+
pointer-events: auto;
1244+
}
1245+
1246+
.toast-item.toast-fade {
1247+
animation: toast-out 0.45s ease-in forwards;
1248+
}
1249+
1250+
@keyframes toast-in {
1251+
from { opacity: 0; transform: translateY(16px) scale(0.95); }
1252+
to { opacity: 1; transform: translateY(0) scale(1); }
1253+
}
1254+
1255+
@keyframes toast-out {
1256+
from { opacity: 1; transform: translateY(0) scale(1); }
1257+
to { opacity: 0; transform: translateY(-8px) scale(0.95); }
1258+
}
1259+
1260+
/* ── Export Button ── */
1261+
.export-btn {
1262+
font-size: 0.82rem;
1263+
padding: 6px 14px;
1264+
cursor: pointer;
1265+
}
1266+
1267+
/* ── Agent Leaderboard Highlight ── */
1268+
.score-pill {
1269+
font-variant-numeric: tabular-nums;
1270+
}
1271+
1272+
/* ── Repository Detail Enhancements ── */
1273+
.discussion-thread {
1274+
margin-top: 8px;
1275+
padding-left: 12px;
1276+
border-left: 2px solid rgba(142, 216, 255, 0.15);
1277+
display: flex;
1278+
flex-direction: column;
1279+
gap: 8px;
1280+
}
1281+
1282+
.discussion-msg {
1283+
font-size: 0.85rem;
1284+
}
1285+
1286+
.discussion-msg strong {
1287+
color: var(--ice, #8ed8ff);
1288+
margin-right: 6px;
1289+
}
1290+
1291+
.discussion-msg p {
1292+
margin: 2px 0 0;
1293+
color: rgba(255, 255, 255, 0.7);
1294+
}
1295+
1296+
.tone-approve {
1297+
background: rgba(80, 220, 120, 0.15) !important;
1298+
border-color: rgba(80, 220, 120, 0.3) !important;
1299+
color: #50dc78 !important;
1300+
}
1301+
1302+
.tone-reject {
1303+
background: rgba(255, 100, 100, 0.15) !important;
1304+
border-color: rgba(255, 100, 100, 0.3) !important;
1305+
color: #ff6464 !important;
1306+
}
1307+
12201308
.manual-info-box.warning strong {
12211309
color: var(--sun);
12221310
}

src/components/autonomous-forge-app.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ export function AutonomousForgeApp() {
130130
const [showKey, setShowKey] = useState<string | null>(null);
131131
const [isPending,] = useTransition();
132132
const deferredSearch = useDeferredValue(search);
133+
const [toasts, setToasts] = useState<{ id: string; message: string; fading: boolean }[]>([]);
134+
135+
function pushToast(message: string) {
136+
const id = crypto.randomUUID();
137+
setToasts((prev) => [...prev.slice(-4), { id, message, fading: false }]);
138+
setTimeout(() => setToasts((prev) => prev.map((t) => (t.id === id ? { ...t, fading: true } : t))), 3500);
139+
setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 4000);
140+
}
141+
142+
function handleExportState() {
143+
if (!state) return;
144+
const blob = new Blob([JSON.stringify(state, null, 2)], { type: "application/json" });
145+
const url = URL.createObjectURL(blob);
146+
const anchor = document.createElement("a");
147+
anchor.href = url;
148+
anchor.download = `forge-state-${new Date().toISOString().slice(0, 10)}.json`;
149+
anchor.click();
150+
URL.revokeObjectURL(url);
151+
pushToast("Forge state exported as JSON.");
152+
}
133153

134154
async function fetchAndApplyState() {
135155
const response = await fetch("/api/state", { cache: "no-store" });
@@ -213,10 +233,16 @@ export function AutonomousForgeApp() {
213233
}
214234

215235
const source = new EventSource("/api/events/stream");
216-
source.onmessage = () => {
236+
source.onmessage = (event) => {
217237
startTransition(() => {
218238
void refreshStateEvent();
219239
});
240+
try {
241+
const data = JSON.parse(event.data);
242+
if (data.type && data.type !== "stream.connected") {
243+
pushToast(data.payload?.summary ?? data.type);
244+
}
245+
} catch { /* ignore parse errors from heartbeats */ }
220246
};
221247
source.onerror = () => {
222248
setStatus("Live stream reconnecting...");
@@ -439,6 +465,7 @@ export function AutonomousForgeApp() {
439465
<div className="hero-status-row">
440466
<span className="status-pill">{status}</span>
441467
<span className="status-pill alt">Policy: {state.policy.minApprovals} approvals to merge</span>
468+
<button className="ghost-button export-btn" type="button" onClick={handleExportState}>Export State</button>
442469
</div>
443470
</div>
444471
<div className="hero-visual hero-stack-card">
@@ -727,6 +754,14 @@ export function AutonomousForgeApp() {
727754
</div>
728755
</div>
729756
</section>
757+
758+
<div className="toast-container">
759+
{toasts.map((toast) => (
760+
<div key={toast.id} className={`toast-item${toast.fading ? " toast-fade" : ""}`}>
761+
{toast.message}
762+
</div>
763+
))}
764+
</div>
730765
</main>
731766
);
732767
}

src/components/repository-detail-page.tsx

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,23 @@ type RepositoryDetail = {
3333
owner: { name: string };
3434
branches: BranchView[];
3535
commits: CommitView[];
36-
pullRequests: Array<{ id: string; title: string; status: string; sourceBranch: string; targetBranch: string }>;
37-
discussions: Array<{ id: string; title: string; channel: string; status: string; messages: Array<{ id: string; text: string; author: { name: string } }> }>;
36+
pullRequests: Array<{
37+
id: string;
38+
title: string;
39+
status: string;
40+
sourceBranch: string;
41+
targetBranch: string;
42+
author?: { name: string };
43+
reviews?: Array<{ decision: string; reviewer: { name: string } }>;
44+
}>;
45+
discussions: Array<{
46+
id: string;
47+
title: string;
48+
channel: string;
49+
status: string;
50+
author?: { name: string };
51+
messages: Array<{ id: string; text: string; author: { name: string } }>;
52+
}>;
3853
};
3954

4055
export function RepositoryDetailPage({ slug }: { slug: string }) {
@@ -76,13 +91,24 @@ export function RepositoryDetailPage({ slug }: { slug: string }) {
7691
);
7792
}
7893

94+
const mergedCount = repository.pullRequests.filter((pr) => pr.status === "MERGED").length;
95+
const openPrCount = repository.pullRequests.filter((pr) => pr.status === "OPEN").length;
96+
const contributors = [...new Set(repository.commits.map((c) => c.author.name))];
97+
7998
return (
8099
<main className="shell detail-shell">
81100
<section className="panel detail-header reveal-up">
82101
<div>
83102
<Link className="repo-link" href="/">Back to dashboard</Link>
84103
<h1>{repository.name}</h1>
85104
<p>{repository.description}</p>
105+
{repository.technologyStack.length > 0 && (
106+
<div className="stack-row" style={{ marginTop: 8 }}>
107+
{repository.technologyStack.map((tech) => (
108+
<span className="stack-pill" key={tech}>{tech}</span>
109+
))}
110+
</div>
111+
)}
86112
</div>
87113
<div className="detail-meta">
88114
<span className="language-pill">{repository.primaryLanguage}</span>
@@ -93,7 +119,25 @@ export function RepositoryDetailPage({ slug }: { slug: string }) {
93119
</div>
94120
</section>
95121

96-
<section className="detail-grid reveal-up delay-1">
122+
<section className="metrics-grid reveal-up delay-1">
123+
<div className="panel metric-card tone-mint"><span>Pull Requests</span><strong>{repository.pullRequests.length}</strong></div>
124+
<div className="panel metric-card tone-ice"><span>Merged</span><strong>{mergedCount}</strong></div>
125+
<div className="panel metric-card tone-sun"><span>Open PRs</span><strong>{openPrCount}</strong></div>
126+
<div className="panel metric-card tone-peach"><span>Discussions</span><strong>{repository.discussions.length}</strong></div>
127+
</section>
128+
129+
{contributors.length > 0 && (
130+
<section className="panel reveal-up delay-1" style={{ marginBottom: 16 }}>
131+
<h2>Contributors</h2>
132+
<div className="stack-row">
133+
{contributors.map((name) => (
134+
<span className="status-pill alt" key={name}>{name}</span>
135+
))}
136+
</div>
137+
</section>
138+
)}
139+
140+
<section className="detail-grid reveal-up delay-2">
97141
<div className="panel detail-panel">
98142
<h2>Branches</h2>
99143
<div className="branch-grid">
@@ -115,31 +159,59 @@ export function RepositoryDetailPage({ slug }: { slug: string }) {
115159
</div>
116160

117161
<div className="panel detail-panel">
118-
<h2>Pull Requests</h2>
162+
<h2>Pull Requests ({repository.pullRequests.length})</h2>
119163
<div className="mini-list">
120164
{repository.pullRequests.map((pullRequest) => (
121165
<div className="mini-card" key={pullRequest.id}>
122-
<strong>{pullRequest.title}</strong>
123-
<span>{pullRequest.sourceBranch} to {pullRequest.targetBranch} · {pullRequest.status}</span>
166+
<div className="repo-card-top">
167+
<strong>{pullRequest.title}</strong>
168+
<span className={`repo-status repo-status-${pullRequest.status.toLowerCase()}`}>{pullRequest.status}</span>
169+
</div>
170+
<span>{pullRequest.author?.name ? `${pullRequest.author.name} · ` : ""}{pullRequest.sourceBranch}{pullRequest.targetBranch}</span>
171+
{pullRequest.reviews && pullRequest.reviews.length > 0 && (
172+
<div className="stack-row" style={{ marginTop: 4 }}>
173+
{pullRequest.reviews.map((review, index) => (
174+
<span key={index} className={`stack-pill ${review.decision === "APPROVE" ? "tone-approve" : review.decision === "REJECT" ? "tone-reject" : ""}`}>
175+
{review.reviewer.name}: {review.decision}
176+
</span>
177+
))}
178+
</div>
179+
)}
124180
</div>
125181
))}
182+
{repository.pullRequests.length === 0 && <span className="muted-inline">No pull requests yet.</span>}
126183
</div>
127-
<h2>Discussions</h2>
128-
<div className="mini-list">
129-
{repository.discussions.map((discussion) => (
130-
<div className="mini-card" key={discussion.id}>
184+
</div>
185+
</section>
186+
187+
<section className="panel detail-panel reveal-up delay-3">
188+
<h2>Discussions ({repository.discussions.length})</h2>
189+
<div className="mini-list">
190+
{repository.discussions.map((discussion) => (
191+
<div className="mini-card" key={discussion.id}>
192+
<div className="repo-card-top">
131193
<strong>{discussion.title}</strong>
132-
<span>{discussion.channel} · {discussion.status}</span>
133-
{discussion.messages.slice(-2).map((message) => (
134-
<span key={message.id}>{message.author.name}: {message.text}</span>
194+
<div className="stack-row">
195+
<span className="stack-pill">{discussion.channel}</span>
196+
<span className={`repo-status repo-status-${(discussion.status ?? "OPEN").toLowerCase()}`}>{discussion.status ?? "OPEN"}</span>
197+
</div>
198+
</div>
199+
{discussion.author && <span className="muted-inline">by {discussion.author.name}</span>}
200+
<div className="discussion-thread">
201+
{discussion.messages.map((message) => (
202+
<div key={message.id} className="discussion-msg">
203+
<strong>{message.author.name}</strong>
204+
<p>{message.text}</p>
205+
</div>
135206
))}
136207
</div>
137-
))}
138-
</div>
208+
</div>
209+
))}
210+
{repository.discussions.length === 0 && <span className="muted-inline">No discussions yet.</span>}
139211
</div>
140212
</section>
141213

142-
<section className="panel detail-panel reveal-up delay-2">
214+
<section className="panel detail-panel reveal-up delay-4">
143215
<h2>Commit History and Diffs</h2>
144216
<div className="commit-list">
145217
{repository.commits.map((commit) => (

0 commit comments

Comments
 (0)