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

semantic-search.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.

semantic-search.tsxBlame325 lines · 1 contributor
3cbe3d6Claude1/**
2 * Block D1 — Semantic code search UI + reindex trigger.
3 *
4 * GET /:owner/:repo/search/semantic?q=... — results page (ILIKE parity)
5 * POST /:owner/:repo/search/semantic/reindex — owner-only, fire-and-forget
6 *
7 * Intentionally tolerates a missing DB / missing repo / missing index so the
8 * page is always navigable. When there's no index yet, the page shows a
9 * "Build index" CTA pointing at the reindex endpoint.
10 */
11
12import { Hono } from "hono";
13import { eq, and, desc } from "drizzle-orm";
14import { db } from "../db";
15import { repositories, users, codeChunks } from "../db/schema";
16import { Layout } from "../views/layout";
17import { RepoHeader } from "../views/components";
18import { IssueNav } from "./issues";
19import { softAuth, requireAuth } from "../middleware/auth";
20import type { AuthEnv } from "../middleware/auth";
21import {
22 getDefaultBranch,
23 resolveRef,
24 repoExists,
25} from "../git/repository";
26import {
27 indexRepository,
28 searchRepository,
29 isEmbeddingsProviderAvailable,
30} from "../lib/semantic-search";
31
32const semanticSearch = new Hono<AuthEnv>();
33semanticSearch.use("*", softAuth);
34
35async function resolveRepo(ownerName: string, repoName: string) {
36 try {
37 const [owner] = await db
38 .select()
39 .from(users)
40 .where(eq(users.username, ownerName))
41 .limit(1);
42 if (!owner) return null;
43 const [repo] = await db
44 .select()
45 .from(repositories)
46 .where(
47 and(
48 eq(repositories.ownerId, owner.id),
49 eq(repositories.name, repoName)
50 )
51 )
52 .limit(1);
53 if (!repo) return null;
54 return { owner, repo };
55 } catch {
56 return null;
57 }
58}
59
60function NotFound({ user }: { user: any }) {
61 return (
62 <Layout title="Not Found" user={user}>
63 <div class="empty-state">
64 <h2>Repository not found</h2>
65 <p>No such repository, or you don't have access.</p>
66 </div>
67 </Layout>
68 );
69}
70
71semanticSearch.get("/:owner/:repo/search/semantic", async (c) => {
72 const { owner: ownerName, repo: repoName } = c.req.param();
73 const user = c.get("user");
74 const q = (c.req.query("q") || "").trim();
75 const flash = c.req.query("flash");
76
77 const resolved = await resolveRepo(ownerName, repoName);
78 if (!resolved) {
79 return c.html(<NotFound user={user} />, 404);
80 }
81 const { repo } = resolved;
82
83 // Figure out last-indexed state (chunk count + most recent createdAt).
84 let indexedCount = 0;
85 let lastIndexedAt: Date | null = null;
86 let indexedCommitSha: string | null = null;
87 try {
88 // Grab the newest chunk row for metadata (sha + createdAt).
89 const [newest] = await db
90 .select({
91 createdAt: codeChunks.createdAt,
92 commitSha: codeChunks.commitSha,
93 })
94 .from(codeChunks)
95 .where(eq(codeChunks.repositoryId, repo.id))
96 .orderBy(desc(codeChunks.createdAt))
97 .limit(1);
98 if (newest) {
99 lastIndexedAt = newest.createdAt as unknown as Date;
100 indexedCommitSha = newest.commitSha || null;
101 // Rough count for the UI blurb — order of magnitude is fine.
102 const rows = await db
103 .select({ id: codeChunks.id })
104 .from(codeChunks)
105 .where(eq(codeChunks.repositoryId, repo.id))
106 .limit(5000);
107 indexedCount = rows.length;
108 }
109 } catch {
110 // DB unavailable — show the page anyway so the URL always resolves.
111 }
112
113 let hits: Awaited<ReturnType<typeof searchRepository>> = [];
114 if (q && indexedCount > 0) {
115 try {
116 hits = await searchRepository({
117 repositoryId: repo.id,
118 query: q,
119 limit: 20,
120 });
121 } catch {
122 hits = [];
123 }
124 }
125
126 const providers = isEmbeddingsProviderAvailable();
127 const providerLabel = providers.voyage
128 ? "Voyage voyage-code-3"
129 : "lexical fallback (512-dim)";
130
131 const isOwner = !!user && user.id === repo.ownerId;
132 const refForLinks = indexedCommitSha || repo.defaultBranch || "HEAD";
133
134 return c.html(
135 <Layout title={`Semantic search — ${ownerName}/${repoName}`} user={user}>
136 <RepoHeader owner={ownerName} repo={repoName} />
137 <IssueNav owner={ownerName} repo={repoName} active="code" />
138
139 <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px">
140 <h2 style="margin: 0">Semantic search</h2>
141 <div class="meta" style="font-size: 12px">
142 Provider: <strong>{providerLabel}</strong>
143 </div>
144 </div>
145
146 {flash && (
147 <div class="auth-success" style="margin-bottom: 16px">
148 {decodeURIComponent(flash)}
149 </div>
150 )}
151
152 <form
001af43Claude153 method="get"
3cbe3d6Claude154 action={`/${ownerName}/${repoName}/search/semantic`}
155 style="margin-bottom: 16px"
156 >
157 <input
158 type="search"
159 name="q"
160 value={q}
161 placeholder="Ask a question or describe what you're looking for…"
162 style="width: 100%; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
163 autofocus
164 />
165 </form>
166
167 {indexedCount === 0 ? (
168 <div class="empty-state">
169 <h3>No index yet</h3>
170 <p>
171 This repository hasn't been indexed for semantic search. Build the
172 index to enable AI-powered code lookup.
173 </p>
174 {isOwner ? (
175 <form
001af43Claude176 method="post"
3cbe3d6Claude177 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
178 style="margin-top: 12px"
179 >
180 <button type="submit" class="btn btn-primary">
181 Build index
182 </button>
183 </form>
184 ) : (
185 <p class="meta" style="margin-top: 8px">
186 Only the repository owner can trigger indexing.
187 </p>
188 )}
189 </div>
190 ) : (
191 <>
192 <div
193 class="meta"
194 style="font-size: 12px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center"
195 >
196 <span>
197 {indexedCount} chunk{indexedCount === 1 ? "" : "s"} indexed
198 {lastIndexedAt && (
199 <>
200 {" · last indexed "}
201 {new Date(lastIndexedAt).toLocaleString()}
202 </>
203 )}
204 </span>
205 {isOwner && (
206 <form
001af43Claude207 method="post"
3cbe3d6Claude208 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
209 style="display: inline"
210 >
211 <button type="submit" class="btn btn-sm">
212 Reindex
213 </button>
214 </form>
215 )}
216 </div>
217
218 {!q ? (
219 <div class="empty-state">
220 <p>Type a query to search across this repo's code.</p>
221 </div>
222 ) : hits.length === 0 ? (
223 <div class="empty-state">
224 <p>No results for "{q}"</p>
225 </div>
226 ) : (
227 <div class="panel">
228 {hits.map((h) => {
229 const href = `/${ownerName}/${repoName}/blob/${refForLinks}/${h.path}#L${h.startLine}`;
230 const preview =
231 h.content.length > 600
232 ? h.content.slice(0, 600) + "\n…"
233 : h.content;
234 return (
235 <div class="panel-item" style="flex-direction: column; align-items: stretch">
236 <div
237 style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px"
238 >
239 <a href={href} style="font-weight: 600">
240 {h.path}
241 </a>
242 <span class="meta" style="font-size: 11px">
243 lines {h.startLine}–{h.endLine} · score{" "}
244 {h.score.toFixed(3)}
245 </span>
246 </div>
247 <pre
248 style="margin: 0; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; font-size: 12px; overflow-x: auto; white-space: pre-wrap"
249 >
250 {preview}
251 </pre>
252 </div>
253 );
254 })}
255 </div>
256 )}
257 </>
258 )}
259 </Layout>
260 );
261});
262
263// Owner-only: rebuild the index. Fire-and-forget so the UI doesn't block on
264// potentially multi-minute work. Always redirects.
265semanticSearch.post(
266 "/:owner/:repo/search/semantic/reindex",
267 requireAuth,
268 async (c) => {
269 const { owner: ownerName, repo: repoName } = c.req.param();
270 const user = c.get("user")!;
271
272 const resolved = await resolveRepo(ownerName, repoName);
273 if (!resolved) {
274 return c.redirect(`/${ownerName}/${repoName}`);
275 }
276 const { repo } = resolved;
277
278 if (repo.ownerId !== user.id) {
279 return c.redirect(
280 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
281 "Only the repository owner can trigger indexing."
282 )}`
283 );
284 }
285
286 // Resolve the commit sha at the default branch. If the repo has no
287 // commits yet we bail with a friendly flash.
288 let sha: string | null = null;
289 try {
290 if (await repoExists(ownerName, repoName)) {
291 const branch =
292 (await getDefaultBranch(ownerName, repoName)) ||
293 repo.defaultBranch ||
294 "main";
295 sha = await resolveRef(ownerName, repoName, branch);
296 }
297 } catch {
298 sha = null;
299 }
300
301 if (!sha) {
302 return c.redirect(
303 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
304 "Repository has no commits yet — nothing to index."
305 )}`
306 );
307 }
308
309 // Fire-and-forget. Errors are swallowed inside indexRepository.
310 void indexRepository({
311 owner: ownerName,
312 repo: repoName,
313 repositoryId: repo.id,
314 commitSha: sha,
315 });
316
317 return c.redirect(
318 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
319 "Indexing started — results will appear shortly."
320 )}`
321 );
322 }
323);
324
325export default semanticSearch;