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.tsxBlame728 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.
e3eb5ffClaude10 *
11 * 2026 polish:
12 * - Scoped `.ss-*` CSS — sits below RepoHeader + IssueNav.
13 * - Eyebrow + display headline + 1-line subtitle.
14 * - Prominent search input w/ focus ring + gradient submit button.
15 * - Result cards show file:line in mono, snippet, and match score chip.
16 * - Dashed empty state w/ orb + Build/Reindex CTA when no index exists.
17 *
18 * All query strings + POST handlers preserved verbatim.
3cbe3d6Claude19 */
20
21import { Hono } from "hono";
22import { eq, and, desc } from "drizzle-orm";
23import { db } from "../db";
24import { repositories, users, codeChunks } from "../db/schema";
25import { Layout } from "../views/layout";
26import { RepoHeader } from "../views/components";
27import { IssueNav } from "./issues";
28import { softAuth, requireAuth } from "../middleware/auth";
29import type { AuthEnv } from "../middleware/auth";
30import {
31 getDefaultBranch,
32 resolveRef,
33 repoExists,
34} from "../git/repository";
35import {
36 indexRepository,
37 searchRepository,
38 isEmbeddingsProviderAvailable,
39} from "../lib/semantic-search";
a686079Claude40import { searchSemantic } from "../lib/semantic-index";
3cbe3d6Claude41
42const semanticSearch = new Hono<AuthEnv>();
43semanticSearch.use("*", softAuth);
44
e3eb5ffClaude45// ─── Scoped CSS (.ss-*) ──────────────────────────────────────────────────
46const ssStyles = `
eed4684Claude47 .ss-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
e3eb5ffClaude48
49 .ss-head {
50 margin-bottom: var(--space-5);
51 display: flex;
52 align-items: flex-end;
53 justify-content: space-between;
54 gap: var(--space-4);
55 flex-wrap: wrap;
56 }
57 .ss-head-text { flex: 1; min-width: 280px; }
58 .ss-eyebrow {
59 display: inline-flex;
60 align-items: center;
61 gap: 8px;
62 text-transform: uppercase;
63 font-family: var(--font-mono);
64 font-size: 11px;
65 letter-spacing: 0.16em;
66 color: var(--text-muted);
67 font-weight: 600;
68 margin-bottom: 10px;
69 }
70 .ss-eyebrow-dot {
71 width: 8px; height: 8px;
72 border-radius: 9999px;
73 background: linear-gradient(135deg, #8c6dff, #36c5d6);
74 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
75 }
76 .ss-title {
77 font-family: var(--font-display);
78 font-size: clamp(24px, 3.4vw, 36px);
79 font-weight: 800;
80 letter-spacing: -0.028em;
81 line-height: 1.1;
82 margin: 0 0 6px;
83 color: var(--text-strong);
84 }
85 .ss-title-grad {
86 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
87 -webkit-background-clip: text;
88 background-clip: text;
89 -webkit-text-fill-color: transparent;
90 color: transparent;
91 }
92 .ss-sub {
93 margin: 0;
94 font-size: 14px;
95 color: var(--text-muted);
96 line-height: 1.5;
97 max-width: 720px;
98 }
99
100 .ss-provider {
101 display: inline-flex;
102 align-items: center;
103 gap: 6px;
104 padding: 5px 10px;
105 border-radius: 9999px;
106 font-family: var(--font-mono);
107 font-size: 11px;
108 color: var(--text-muted);
109 background: rgba(255,255,255,0.03);
110 border: 1px solid var(--border);
111 }
112 .ss-provider .dot {
113 width: 6px; height: 6px;
114 border-radius: 9999px;
115 background: linear-gradient(135deg, #8c6dff, #36c5d6);
116 }
117
118 .ss-banner {
119 margin-bottom: var(--space-4);
120 padding: 10px 14px;
121 border-radius: 10px;
122 font-size: 13.5px;
123 border: 1px solid var(--border);
124 background: rgba(255,255,255,0.025);
125 color: var(--text);
126 display: flex;
127 align-items: center;
128 gap: 10px;
129 }
130 .ss-banner.is-ok { border-color: rgba(52,211,153,0.40); background: rgba(52,211,153,0.08); color: #bbf7d0; }
131 .ss-banner-dot { width: 8px; height: 8px; border-radius: 9999px; background: currentColor; flex-shrink: 0; }
132
133 /* ─── Search bar ─── */
134 .ss-search {
135 display: flex;
136 gap: 10px;
137 align-items: stretch;
138 margin-bottom: var(--space-4);
139 }
140 .ss-search-input-wrap { position: relative; flex: 1; }
141 .ss-search-icon {
142 position: absolute;
143 top: 50%;
144 left: 14px;
145 transform: translateY(-50%);
146 color: var(--text-muted);
147 pointer-events: none;
148 }
149 .ss-search-input {
150 width: 100%;
151 box-sizing: border-box;
152 padding: 12px 14px 12px 40px;
153 font: inherit;
154 font-size: 14.5px;
155 color: var(--text);
156 background: rgba(255,255,255,0.03);
157 border: 1px solid var(--border-strong);
158 border-radius: 12px;
159 outline: none;
160 transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
161 }
162 .ss-search-input:focus {
163 border-color: rgba(140,109,255,0.55);
164 background: rgba(255,255,255,0.05);
165 box-shadow: 0 0 0 3px rgba(140,109,255,0.20);
166 }
167 .ss-btn {
168 display: inline-flex;
169 align-items: center;
170 justify-content: center;
171 gap: 6px;
172 padding: 0 18px;
173 border-radius: 12px;
174 font-size: 14px;
175 font-weight: 600;
176 text-decoration: none;
177 border: 1px solid transparent;
178 cursor: pointer;
179 font: inherit;
180 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease;
181 line-height: 1;
182 white-space: nowrap;
183 }
184 .ss-btn-primary {
185 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
186 color: #ffffff;
187 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.55), inset 0 1px 0 rgba(255,255,255,0.16);
188 }
189 .ss-btn-primary:hover {
190 transform: translateY(-1px);
191 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.65), inset 0 1px 0 rgba(255,255,255,0.20);
192 text-decoration: none;
193 color: #ffffff;
194 }
195 .ss-btn-ghost {
196 background: transparent;
197 color: var(--text);
198 border-color: var(--border-strong);
199 padding: 7px 12px;
200 font-size: 12.5px;
201 border-radius: 9px;
202 }
203 .ss-btn-ghost:hover {
204 background: rgba(140,109,255,0.06);
205 border-color: rgba(140,109,255,0.45);
206 color: var(--text-strong);
207 text-decoration: none;
208 }
209
210 /* ─── Index status bar ─── */
211 .ss-status {
212 display: flex;
213 align-items: center;
214 justify-content: space-between;
215 gap: var(--space-3);
216 flex-wrap: wrap;
217 margin-bottom: var(--space-3);
218 padding: 9px 14px;
219 border-radius: 10px;
220 background: rgba(255,255,255,0.025);
221 border: 1px solid var(--border);
222 font-size: 12px;
223 color: var(--text-muted);
224 font-variant-numeric: tabular-nums;
225 }
226 .ss-status .num { color: var(--text-strong); font-weight: 600; }
227
228 /* ─── Result cards ─── */
229 .ss-results { display: flex; flex-direction: column; gap: 10px; }
230 .ss-result {
231 padding: 14px;
232 background: var(--bg-elevated);
233 border: 1px solid var(--border);
234 border-radius: 12px;
235 transition: border-color 120ms ease, background 120ms ease;
236 }
237 .ss-result:hover {
238 border-color: var(--border-strong);
239 background: rgba(255,255,255,0.025);
240 }
241 .ss-result-head {
242 display: flex;
243 align-items: center;
244 justify-content: space-between;
245 gap: 10px;
246 flex-wrap: wrap;
247 margin-bottom: 10px;
248 }
249 .ss-result-path {
250 font-family: var(--font-mono);
251 font-size: 13px;
252 font-weight: 600;
253 color: var(--text-strong);
254 text-decoration: none;
255 word-break: break-all;
256 letter-spacing: -0.005em;
257 }
258 .ss-result-path .lines { color: var(--text-muted); font-weight: 500; }
259 .ss-result-path:hover { color: #c4b5fd; text-decoration: none; }
260 .ss-score {
261 display: inline-flex;
262 align-items: center;
263 gap: 5px;
264 padding: 2px 9px;
265 border-radius: 9999px;
266 background: rgba(54,197,214,0.12);
267 color: #67e8f9;
268 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.30);
269 font-family: var(--font-mono);
270 font-size: 11px;
271 font-weight: 600;
272 font-variant-numeric: tabular-nums;
273 }
274 .ss-snippet {
275 margin: 0;
276 padding: 10px 12px;
277 background: rgba(0,0,0,0.25);
278 border: 1px solid var(--border);
279 border-radius: 8px;
280 font-family: var(--font-mono);
281 font-size: 12px;
282 line-height: 1.55;
283 color: var(--text);
284 overflow-x: auto;
285 white-space: pre-wrap;
286 word-break: break-word;
287 }
288
289 /* ─── Empty state ─── */
290 .ss-empty {
291 position: relative;
292 overflow: hidden;
293 padding: clamp(28px, 5vw, 52px) clamp(20px, 4vw, 40px);
294 text-align: center;
295 background: var(--bg-elevated);
296 border: 1px dashed var(--border-strong);
297 border-radius: 16px;
298 }
299 .ss-empty-orb {
300 position: absolute;
301 inset: -40% 25% auto 25%;
302 height: 300px;
303 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
304 filter: blur(72px);
305 opacity: 0.7;
306 pointer-events: none;
307 z-index: 0;
308 }
309 .ss-empty-inner { position: relative; z-index: 1; }
310 .ss-empty-icon {
311 width: 56px; height: 56px;
312 margin: 0 auto 14px;
313 border-radius: 9999px;
314 background: linear-gradient(135deg, rgba(140,109,255,0.25), rgba(54,197,214,0.20));
315 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.40);
316 display: inline-flex;
317 align-items: center;
318 justify-content: center;
319 color: #c4b5fd;
320 }
321 .ss-empty-title {
322 font-family: var(--font-display);
323 font-size: 18px;
324 font-weight: 700;
325 margin: 0 0 6px;
326 color: var(--text-strong);
327 }
328 .ss-empty-sub {
329 margin: 0 auto 16px;
330 font-size: 13.5px;
331 color: var(--text-muted);
332 max-width: 480px;
333 line-height: 1.5;
334 }
335`;
336
337function IconSearch() {
338 return (
339 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
340 <circle cx="11" cy="11" r="7" />
341 <line x1="21" y1="21" x2="16.65" y2="16.65" />
342 </svg>
343 );
344}
345function IconSparkles() {
346 return (
347 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
348 <path d="M12 3l1.7 4.6L18 9.3l-4.3 1.7L12 15l-1.7-4-4.3-1.7L10 7.6 12 3z" />
349 <path d="M19 13l.9 2.4L22 16l-2.1.6L19 19l-.9-2.4L16 16l2.1-.6L19 13z" />
350 </svg>
351 );
352}
353
3cbe3d6Claude354async function resolveRepo(ownerName: string, repoName: string) {
355 try {
356 const [owner] = await db
357 .select()
358 .from(users)
359 .where(eq(users.username, ownerName))
360 .limit(1);
361 if (!owner) return null;
362 const [repo] = await db
363 .select()
364 .from(repositories)
365 .where(
366 and(
367 eq(repositories.ownerId, owner.id),
368 eq(repositories.name, repoName)
369 )
370 )
371 .limit(1);
372 if (!repo) return null;
373 return { owner, repo };
374 } catch {
375 return null;
376 }
377}
378
379function NotFound({ user }: { user: any }) {
380 return (
381 <Layout title="Not Found" user={user}>
382 <div class="empty-state">
383 <h2>Repository not found</h2>
384 <p>No such repository, or you don't have access.</p>
385 </div>
386 </Layout>
387 );
388}
389
390semanticSearch.get("/:owner/:repo/search/semantic", async (c) => {
391 const { owner: ownerName, repo: repoName } = c.req.param();
392 const user = c.get("user");
393 const q = (c.req.query("q") || "").trim();
394 const flash = c.req.query("flash");
395
396 const resolved = await resolveRepo(ownerName, repoName);
397 if (!resolved) {
398 return c.html(<NotFound user={user} />, 404);
399 }
400 const { repo } = resolved;
401
402 // Figure out last-indexed state (chunk count + most recent createdAt).
403 let indexedCount = 0;
404 let lastIndexedAt: Date | null = null;
405 let indexedCommitSha: string | null = null;
406 try {
407 const [newest] = await db
408 .select({
409 createdAt: codeChunks.createdAt,
410 commitSha: codeChunks.commitSha,
411 })
412 .from(codeChunks)
413 .where(eq(codeChunks.repositoryId, repo.id))
414 .orderBy(desc(codeChunks.createdAt))
415 .limit(1);
416 if (newest) {
417 lastIndexedAt = newest.createdAt as unknown as Date;
418 indexedCommitSha = newest.commitSha || null;
419 const rows = await db
420 .select({ id: codeChunks.id })
421 .from(codeChunks)
422 .where(eq(codeChunks.repositoryId, repo.id))
423 .limit(5000);
424 indexedCount = rows.length;
425 }
426 } catch {
e3eb5ffClaude427 // DB unavailable — show the page anyway.
3cbe3d6Claude428 }
429
430 let hits: Awaited<ReturnType<typeof searchRepository>> = [];
a686079Claude431 if (q) {
432 // Prefer the continuous per-push index (pgvector-backed). It's kept
433 // fresh by `src/hooks/post-receive.ts` → `indexChangedFiles`, so it
434 // typically beats the chunked full-repo index on staleness. Returns
435 // `[]` when pgvector isn't available; fall back to the chunked index
436 // in that case.
3cbe3d6Claude437 try {
a686079Claude438 const live = await searchSemantic({
3cbe3d6Claude439 repositoryId: repo.id,
440 query: q,
441 limit: 20,
442 });
a686079Claude443 if (live.length > 0) {
444 // Adapt to the chunked search-hit shape the UI expects. The
445 // continuous index stores whole-file snippets (no line ranges),
446 // so we surface line `1` and the snippet length as a best-effort
447 // line span.
448 hits = live.map((h) => {
449 const lineCount = h.snippet
450 ? Math.max(1, h.snippet.split("\n").length)
451 : 1;
452 return {
453 path: h.filePath,
454 startLine: 1,
455 endLine: lineCount,
456 content: h.snippet,
457 score: h.score,
458 };
459 });
460 }
3cbe3d6Claude461 } catch {
462 hits = [];
463 }
a686079Claude464 // Fall back to the chunked index if the continuous one had nothing.
465 if (hits.length === 0 && indexedCount > 0) {
466 try {
467 hits = await searchRepository({
468 repositoryId: repo.id,
469 query: q,
470 limit: 20,
471 });
472 } catch {
473 hits = [];
474 }
475 }
3cbe3d6Claude476 }
477
478 const providers = isEmbeddingsProviderAvailable();
479 const providerLabel = providers.voyage
480 ? "Voyage voyage-code-3"
481 : "lexical fallback (512-dim)";
482
483 const isOwner = !!user && user.id === repo.ownerId;
484 const refForLinks = indexedCommitSha || repo.defaultBranch || "HEAD";
485
486 return c.html(
487 <Layout title={`Semantic search — ${ownerName}/${repoName}`} user={user}>
488 <RepoHeader owner={ownerName} repo={repoName} />
489 <IssueNav owner={ownerName} repo={repoName} active="code" />
490
e3eb5ffClaude491 <div class="ss-wrap">
492 <header class="ss-head">
493 <div class="ss-head-text">
494 <div class="ss-eyebrow">
495 <span class="ss-eyebrow-dot" aria-hidden="true" />
496 Repository · Semantic search
497 </div>
498 <h1 class="ss-title">
499 <span class="ss-title-grad">Ask the codebase anything.</span>
500 </h1>
501 <p class="ss-sub">
502 Embeddings-powered code search across every indexed chunk —
503 describe what you're looking for in natural language.
504 </p>
505 </div>
506 <div class="ss-provider" title="Active embeddings provider">
507 <span class="dot" aria-hidden="true" />
508 {providerLabel}
509 </div>
510 </header>
3cbe3d6Claude511
e3eb5ffClaude512 {flash && (
513 <div class="ss-banner is-ok" role="status">
514 <span class="ss-banner-dot" aria-hidden="true" />
515 {decodeURIComponent(flash)}
516 </div>
517 )}
3cbe3d6Claude518
e3eb5ffClaude519 <form
520 method="get"
521 action={`/${ownerName}/${repoName}/search/semantic`}
522 class="ss-search"
523 >
524 <div class="ss-search-input-wrap">
525 <span class="ss-search-icon" aria-hidden="true">
526 <IconSearch />
3cbe3d6Claude527 </span>
e3eb5ffClaude528 <input
529 type="search"
530 name="q"
531 value={q}
532 placeholder="Ask a question or describe what you're looking for…"
533 aria-label="Search"
534 class="ss-search-input"
535 autofocus
536 />
3cbe3d6Claude537 </div>
e3eb5ffClaude538 <button type="submit" class="ss-btn ss-btn-primary">
539 <IconSparkles />
540 Search
541 </button>
542 </form>
3cbe3d6Claude543
e3eb5ffClaude544 {indexedCount === 0 ? (
545 <div class="ss-empty">
546 <div class="ss-empty-orb" aria-hidden="true" />
547 <div class="ss-empty-inner">
548 <div class="ss-empty-icon" aria-hidden="true">
549 <IconSparkles />
550 </div>
551 <h3 class="ss-empty-title">No index yet</h3>
552 <p class="ss-empty-sub">
553 This repository hasn't been indexed for semantic search. Build
554 the index to enable AI-powered code lookup.
555 </p>
556 {isOwner ? (
557 <form
558 method="post"
559 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
560 >
561 <button type="submit" class="ss-btn ss-btn-primary">
562 <IconSparkles />
563 Build index
564 </button>
565 </form>
566 ) : (
567 <p
568 style="margin:0;color:var(--text-muted);font-size:13px"
569 >
570 Only the repository owner can trigger indexing.
571 </p>
572 )}
3cbe3d6Claude573 </div>
e3eb5ffClaude574 </div>
575 ) : (
576 <>
577 <div class="ss-status">
578 <span>
579 <span class="num">{indexedCount}</span> chunk
580 {indexedCount === 1 ? "" : "s"} indexed
581 {lastIndexedAt && (
582 <>
583 {" · last indexed "}
584 <span class="num">
585 {new Date(lastIndexedAt).toLocaleString()}
586 </span>
587 </>
588 )}
589 </span>
590 {isOwner && (
591 <form
592 method="post"
593 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
594 style="display:inline"
595 >
596 <button type="submit" class="ss-btn ss-btn-ghost">
597 Reindex
598 </button>
599 </form>
600 )}
3cbe3d6Claude601 </div>
e3eb5ffClaude602
603 {!q ? (
604 <div class="ss-empty">
605 <div class="ss-empty-orb" aria-hidden="true" />
606 <div class="ss-empty-inner">
607 <div class="ss-empty-icon" aria-hidden="true">
608 <IconSearch />
3cbe3d6Claude609 </div>
e3eb5ffClaude610 <h3 class="ss-empty-title">Type a query to begin</h3>
611 <p class="ss-empty-sub">
612 Search across this repo's code — try a function name, a
613 file path, or a plain-English description of what you're
614 looking for.
615 </p>
616 </div>
617 </div>
618 ) : hits.length === 0 ? (
619 <div class="ss-empty">
620 <div class="ss-empty-orb" aria-hidden="true" />
621 <div class="ss-empty-inner">
622 <div class="ss-empty-icon" aria-hidden="true">
623 <IconSearch />
624 </div>
625 <h3 class="ss-empty-title">No results for "{q}"</h3>
626 <p class="ss-empty-sub">
627 Try a different phrasing or a related symbol name.
628 </p>
629 </div>
630 </div>
631 ) : (
632 <div class="ss-results">
633 {hits.map((h) => {
634 const href = `/${ownerName}/${repoName}/blob/${refForLinks}/${h.path}#L${h.startLine}`;
635 const preview =
636 h.content.length > 600
637 ? h.content.slice(0, 600) + "\n…"
638 : h.content;
639 return (
640 <div class="ss-result">
641 <div class="ss-result-head">
642 <a href={href} class="ss-result-path">
643 {h.path}
644 <span class="lines">
645 :{h.startLine}–{h.endLine}
646 </span>
647 </a>
648 <span class="ss-score" title="Cosine similarity score">
649 score {h.score.toFixed(3)}
650 </span>
651 </div>
652 <pre class="ss-snippet">{preview}</pre>
653 </div>
654 );
655 })}
656 </div>
657 )}
658 </>
659 )}
660 </div>
661 <style dangerouslySetInnerHTML={{ __html: ssStyles }} />
3cbe3d6Claude662 </Layout>
663 );
664});
665
666// Owner-only: rebuild the index. Fire-and-forget so the UI doesn't block on
667// potentially multi-minute work. Always redirects.
668semanticSearch.post(
669 "/:owner/:repo/search/semantic/reindex",
670 requireAuth,
671 async (c) => {
672 const { owner: ownerName, repo: repoName } = c.req.param();
673 const user = c.get("user")!;
674
675 const resolved = await resolveRepo(ownerName, repoName);
676 if (!resolved) {
677 return c.redirect(`/${ownerName}/${repoName}`);
678 }
679 const { repo } = resolved;
680
681 if (repo.ownerId !== user.id) {
682 return c.redirect(
683 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
684 "Only the repository owner can trigger indexing."
685 )}`
686 );
687 }
688
689 // Resolve the commit sha at the default branch. If the repo has no
690 // commits yet we bail with a friendly flash.
691 let sha: string | null = null;
692 try {
693 if (await repoExists(ownerName, repoName)) {
694 const branch =
695 (await getDefaultBranch(ownerName, repoName)) ||
696 repo.defaultBranch ||
697 "main";
698 sha = await resolveRef(ownerName, repoName, branch);
699 }
700 } catch {
701 sha = null;
702 }
703
704 if (!sha) {
705 return c.redirect(
706 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
707 "Repository has no commits yet — nothing to index."
708 )}`
709 );
710 }
711
712 // Fire-and-forget. Errors are swallowed inside indexRepository.
713 void indexRepository({
714 owner: ownerName,
715 repo: repoName,
716 repositoryId: repo.id,
717 commitSha: sha,
718 });
719
720 return c.redirect(
721 `/${ownerName}/${repoName}/search/semantic?flash=${encodeURIComponent(
722 "Indexing started — results will appear shortly."
723 )}`
724 );
725 }
726);
727
728export default semanticSearch;