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.tsxBlame326 lines · 2 contributors
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…"
e1c0fbecopilot-swe-agent[bot]162 aria-label="Search"
3cbe3d6Claude163 style="width: 100%; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
164 autofocus
165 />
166 </form>
167
168 {indexedCount === 0 ? (
169 <div class="empty-state">
170 <h3>No index yet</h3>
171 <p>
172 This repository hasn't been indexed for semantic search. Build the
173 index to enable AI-powered code lookup.
174 </p>
175 {isOwner ? (
176 <form
001af43Claude177 method="post"
3cbe3d6Claude178 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
179 style="margin-top: 12px"
180 >
181 <button type="submit" class="btn btn-primary">
182 Build index
183 </button>
184 </form>
185 ) : (
186 <p class="meta" style="margin-top: 8px">
187 Only the repository owner can trigger indexing.
188 </p>
189 )}
190 </div>
191 ) : (
192 <>
193 <div
194 class="meta"
195 style="font-size: 12px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center"
196 >
197 <span>
198 {indexedCount} chunk{indexedCount === 1 ? "" : "s"} indexed
199 {lastIndexedAt && (
200 <>
201 {" · last indexed "}
202 {new Date(lastIndexedAt).toLocaleString()}
203 </>
204 )}
205 </span>
206 {isOwner && (
207 <form
001af43Claude208 method="post"
3cbe3d6Claude209 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
210 style="display: inline"
211 >
212 <button type="submit" class="btn btn-sm">
213 Reindex
214 </button>
215 </form>
216 )}
217 </div>
218
219 {!q ? (
220 <div class="empty-state">
221 <p>Type a query to search across this repo's code.</p>
222 </div>
223 ) : hits.length === 0 ? (
224 <div class="empty-state">
225 <p>No results for "{q}"</p>
226 </div>
227 ) : (
228 <div class="panel">
229 {hits.map((h) => {
230 const href = `/${ownerName}/${repoName}/blob/${refForLinks}/${h.path}#L${h.startLine}`;
231 const preview =
232 h.content.length > 600
233 ? h.content.slice(0, 600) + "\n…"
234 : h.content;
235 return (
236 <div class="panel-item" style="flex-direction: column; align-items: stretch">
237 <div
238 style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px"
239 >
240 <a href={href} style="font-weight: 600">
241 {h.path}
242 </a>
243 <span class="meta" style="font-size: 11px">
244 lines {h.startLine}–{h.endLine} · score{" "}
245 {h.score.toFixed(3)}
246 </span>
247 </div>
248 <pre
249 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"
250 >
251 {preview}
252 </pre>
253 </div>
254 );
255 })}
256 </div>
257 )}
258 </>
259 )}
260 </Layout>
261 );
262});
263
264// Owner-only: rebuild the index. Fire-and-forget so the UI doesn't block on
265// potentially multi-minute work. Always redirects.
266semanticSearch.post(
267 "/:owner/:repo/search/semantic/reindex",
268 requireAuth,
269 async (c) => {
270 const { owner: ownerName, repo: repoName } = c.req.param();
271 const user = c.get("user")!;
272
273 const resolved = await resolveRepo(ownerName, repoName);
274 if (!resolved) {
275 return c.redirect(`/${ownerName}/${repoName}`);
276 }
277 const { repo } = resolved;
278
279 if (repo.ownerId !== user.id) {
280 return c.redirect(
281 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
282 "Only the repository owner can trigger indexing."
283 )}`
284 );
285 }
286
287 // Resolve the commit sha at the default branch. If the repo has no
288 // commits yet we bail with a friendly flash.
289 let sha: string | null = null;
290 try {
291 if (await repoExists(ownerName, repoName)) {
292 const branch =
293 (await getDefaultBranch(ownerName, repoName)) ||
294 repo.defaultBranch ||
295 "main";
296 sha = await resolveRef(ownerName, repoName, branch);
297 }
298 } catch {
299 sha = null;
300 }
301
302 if (!sha) {
303 return c.redirect(
304 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
305 "Repository has no commits yet — nothing to index."
306 )}`
307 );
308 }
309
310 // Fire-and-forget. Errors are swallowed inside indexRepository.
311 void indexRepository({
312 owner: ownerName,
313 repo: repoName,
314 repositoryId: repo.id,
315 commitSha: sha,
316 });
317
318 return c.redirect(
319 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
320 "Indexing started — results will appear shortly."
321 )}`
322 );
323 }
324);
325
326export default semanticSearch;