Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

ai-explain.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

ai-explain.tsxBlame196 lines · 1 contributor
3cbe3d6Claude1/**
2 * Block D6 — "Explain this codebase" route.
3 *
4 * GET /:owner/:repo/explain — render cached (or freshly
5 * generated on first visit)
6 * Markdown explanation
7 * POST /:owner/:repo/explain/regenerate — owner-only; force-regenerate
8 * and redirect back
9 *
10 * Heavy lifting lives in `lib/ai-explain.ts`; this file is just HTTP glue.
11 */
12
13import { Hono } from "hono";
14import { html } from "hono/html";
15import { and, eq } from "drizzle-orm";
16import { db } from "../db";
17import { repositories, users } from "../db/schema";
18import { Layout } from "../views/layout";
19import { RepoHeader } from "../views/components";
20import { IssueNav } from "./issues";
21import { renderMarkdown } from "../lib/markdown";
22import { softAuth, requireAuth } from "../middleware/auth";
23import type { AuthEnv } from "../middleware/auth";
24import { getDefaultBranch, resolveRef } from "../git/repository";
25import {
26 explainCodebase,
27 getCachedExplanation,
28} from "../lib/ai-explain";
29
30const aiExplainRoutes = new Hono<AuthEnv>();
31
32interface ResolvedRepo {
33 ownerId: string;
34 ownerUsername: string;
35 repoId: string;
36 repoName: string;
37}
38
39async function resolveRepo(
40 ownerName: string,
41 repoName: string
42): Promise<ResolvedRepo | null> {
43 try {
44 const [ownerRow] = await db
45 .select()
46 .from(users)
47 .where(eq(users.username, ownerName))
48 .limit(1);
49 if (!ownerRow) return null;
50 const [repoRow] = await db
51 .select()
52 .from(repositories)
53 .where(
54 and(
55 eq(repositories.ownerId, ownerRow.id),
56 eq(repositories.name, repoName)
57 )
58 )
59 .limit(1);
60 if (!repoRow) return null;
61 return {
62 ownerId: ownerRow.id,
63 ownerUsername: ownerRow.username,
64 repoId: repoRow.id,
65 repoName: repoRow.name,
66 };
67 } catch {
68 return null;
69 }
70}
71
72async function resolveHeadSha(
73 owner: string,
74 repo: string
75): Promise<string | null> {
76 const branch = await getDefaultBranch(owner, repo);
77 if (!branch) return null;
78 return resolveRef(owner, repo, branch);
79}
80
81aiExplainRoutes.get(
82 "/:owner/:repo/explain",
83 softAuth,
84 async (c) => {
85 const { owner, repo } = c.req.param();
86 const user = c.get("user");
87
88 const resolved = await resolveRepo(owner, repo);
89 if (!resolved) {
90 return c.html(
91 <Layout title="Not Found" user={user}>
92 <div class="empty-state">
93 <h2>Repository not found</h2>
94 </div>
95 </Layout>,
96 404
97 );
98 }
99
100 const sha = await resolveHeadSha(owner, repo);
101 if (!sha) {
102 return c.html(
103 <Layout title={`Explain — ${owner}/${repo}`} user={user}>
104 <RepoHeader owner={owner} repo={repo} />
105 <IssueNav owner={owner} repo={repo} active="code" />
106 <div class="empty-state">
107 <h2>No commits yet</h2>
108 <p>
109 Push some code to <code>{repo}</code> and check back — the
110 explanation is generated from the default branch.
111 </p>
112 </div>
113 </Layout>
114 );
115 }
116
117 // Prefer cache first to avoid calling the AI on every page load.
118 let result = await getCachedExplanation(resolved.repoId, sha);
119 if (!result) {
120 result = await explainCodebase({
121 owner,
122 repo,
123 repositoryId: resolved.repoId,
124 commitSha: sha,
125 });
126 }
127
128 const canRegenerate = !!user && user.id === resolved.ownerId;
129
130 return c.html(
131 <Layout title={`Explain — ${owner}/${repo}`} user={user}>
132 <RepoHeader owner={owner} repo={repo} />
133 <IssueNav owner={owner} repo={repo} active="code" />
134 <div style="display: flex; justify-content: space-between; align-items: center; margin: 16px 0;">
135 <h2 style="margin: 0;">Codebase explanation</h2>
136 {canRegenerate && (
137 <form
b9968e3Claude138 method="post"
3cbe3d6Claude139 action={`/${owner}/${repo}/explain/regenerate`}
140 style="display: inline"
141 >
142 <button type="submit" class="star-btn">
143 Regenerate
144 </button>
145 </form>
146 )}
147 </div>
148 <div style="font-size: 12px; color: var(--text-muted); margin-bottom: 12px;">
149 Generated from commit <code>{sha.slice(0, 7)}</code> · model{" "}
150 <code>{result.model}</code>
151 {result.cached ? " · cached" : ""}
152 </div>
153 <div class="markdown-body">
154 {html(
155 [renderMarkdown(result.markdown)] as unknown as TemplateStringsArray
156 )}
157 </div>
158 </Layout>
159 );
160 }
161);
162
163aiExplainRoutes.post(
164 "/:owner/:repo/explain/regenerate",
165 requireAuth,
166 async (c) => {
167 const { owner, repo } = c.req.param();
168 const user = c.get("user")!;
169
170 const resolved = await resolveRepo(owner, repo);
171 if (!resolved) return c.notFound();
172
173 if (resolved.ownerId !== user.id) {
174 return c.redirect(`/${owner}/${repo}/explain`);
175 }
176
177 const sha = await resolveHeadSha(owner, repo);
178 if (!sha) {
179 return c.redirect(`/${owner}/${repo}/explain`);
180 }
181
182 // Run synchronously so the redirect lands on a fresh result. The helper
183 // itself never throws; worst case the user sees the fallback copy.
184 await explainCodebase({
185 owner,
186 repo,
187 repositoryId: resolved.repoId,
188 commitSha: sha,
189 force: true,
190 });
191
192 return c.redirect(`/${owner}/${repo}/explain`);
193 }
194);
195
196export default aiExplainRoutes;