@gmickel/gno 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +36 -1
  2. package/package.json +4 -1
  3. package/src/cli/commands/ask.ts +9 -0
  4. package/src/cli/commands/query.ts +3 -2
  5. package/src/cli/pager.ts +1 -1
  6. package/src/cli/program.ts +89 -0
  7. package/src/core/links.ts +92 -20
  8. package/src/ingestion/sync.ts +267 -23
  9. package/src/ingestion/types.ts +2 -0
  10. package/src/ingestion/walker.ts +2 -1
  11. package/src/mcp/tools/index.ts +30 -1
  12. package/src/mcp/tools/query.ts +22 -2
  13. package/src/mcp/tools/search.ts +8 -0
  14. package/src/mcp/tools/vsearch.ts +8 -0
  15. package/src/pipeline/answer.ts +324 -7
  16. package/src/pipeline/expansion.ts +243 -7
  17. package/src/pipeline/explain.ts +93 -5
  18. package/src/pipeline/hybrid.ts +240 -57
  19. package/src/pipeline/query-modes.ts +125 -0
  20. package/src/pipeline/rerank.ts +34 -13
  21. package/src/pipeline/search.ts +41 -3
  22. package/src/pipeline/temporal.ts +257 -0
  23. package/src/pipeline/types.ts +58 -0
  24. package/src/pipeline/vsearch.ts +107 -9
  25. package/src/serve/public/app.tsx +1 -3
  26. package/src/serve/public/globals.built.css +2 -2
  27. package/src/serve/public/lib/retrieval-filters.ts +167 -0
  28. package/src/serve/public/pages/Ask.tsx +339 -109
  29. package/src/serve/public/pages/Browse.tsx +71 -5
  30. package/src/serve/public/pages/DocView.tsx +2 -21
  31. package/src/serve/public/pages/Search.tsx +507 -120
  32. package/src/serve/routes/api.ts +202 -2
  33. package/src/store/migrations/006-document-metadata.ts +104 -0
  34. package/src/store/migrations/007-document-date-fields.ts +24 -0
  35. package/src/store/migrations/index.ts +3 -1
  36. package/src/store/sqlite/adapter.ts +218 -5
  37. package/src/store/types.ts +46 -0
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  ArrowLeft,
3
3
  BookOpen,
4
+ ChevronDown,
4
5
  CornerDownLeft,
5
6
  FileText,
7
+ SlidersHorizontal,
6
8
  Sparkles,
7
9
  } from "lucide-react";
8
10
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -22,9 +24,24 @@ import {
22
24
  import { Badge } from "../components/ui/badge";
23
25
  import { Button } from "../components/ui/button";
24
26
  import { Card, CardContent } from "../components/ui/card";
27
+ import {
28
+ Collapsible,
29
+ CollapsibleContent,
30
+ CollapsibleTrigger,
31
+ } from "../components/ui/collapsible";
32
+ import { Input } from "../components/ui/input";
33
+ import {
34
+ Select,
35
+ SelectContent,
36
+ SelectItem,
37
+ SelectTrigger,
38
+ SelectValue,
39
+ } from "../components/ui/select";
25
40
  import { Textarea } from "../components/ui/textarea";
26
41
  import { apiFetch } from "../hooks/use-api";
27
42
  import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
43
+ import { parseTagsCsv, type TagMode } from "../lib/retrieval-filters";
44
+ import { cn } from "../lib/utils";
28
45
 
29
46
  interface PageProps {
30
47
  navigate: (to: string | number) => void;
@@ -80,6 +97,10 @@ interface ConversationEntry {
80
97
  error?: string;
81
98
  }
82
99
 
100
+ interface Collection {
101
+ name: string;
102
+ }
103
+
83
104
  const THOROUGHNESS_ORDER: Thoroughness[] = ["fast", "balanced", "thorough"];
84
105
 
85
106
  /**
@@ -100,7 +121,6 @@ function renderAnswer(
100
121
 
101
122
  // oxlint-disable-next-line no-cond-assign -- Standard regex match pattern
102
123
  while ((match = citationRegex.exec(answer)) !== null) {
103
- // Add text before citation
104
124
  if (match.index > lastIndex) {
105
125
  parts.push(answer.slice(lastIndex, match.index));
106
126
  }
@@ -128,7 +148,6 @@ function renderAnswer(
128
148
  lastIndex = match.index + match[0].length;
129
149
  }
130
150
 
131
- // Add remaining text
132
151
  if (lastIndex < answer.length) {
133
152
  parts.push(answer.slice(lastIndex));
134
153
  }
@@ -140,105 +159,152 @@ export default function Ask({ navigate }: PageProps) {
140
159
  const [query, setQuery] = useState("");
141
160
  const [conversation, setConversation] = useState<ConversationEntry[]>([]);
142
161
  const [capabilities, setCapabilities] = useState<Capabilities | null>(null);
162
+ const [collections, setCollections] = useState<Collection[]>([]);
143
163
  const [thoroughness, setThoroughness] = useState<Thoroughness>("balanced");
164
+
165
+ const [showAdvanced, setShowAdvanced] = useState(false);
166
+ const [selectedCollection, setSelectedCollection] = useState("");
167
+ const [since, setSince] = useState("");
168
+ const [until, setUntil] = useState("");
169
+ const [category, setCategory] = useState("");
170
+ const [author, setAuthor] = useState("");
171
+ const [tagMode, setTagMode] = useState<TagMode>("any");
172
+ const [tagsInput, setTagsInput] = useState("");
173
+
144
174
  const messagesEndRef = useRef<HTMLDivElement>(null);
145
175
  const textareaRef = useRef<HTMLTextAreaElement>(null);
146
176
 
147
- // Fetch capabilities on mount
177
+ const hybridAvailable = capabilities?.hybrid ?? false;
178
+
148
179
  useEffect(() => {
149
- async function fetchCapabilities() {
150
- const { data } = await apiFetch<Capabilities>("/api/capabilities");
151
- if (data) {
152
- setCapabilities(data);
153
- // Auto-select balanced if hybrid available, otherwise fast
154
- if (data.hybrid) {
155
- setThoroughness("balanced");
156
- } else {
157
- setThoroughness("fast");
158
- }
180
+ async function bootstrap(): Promise<void> {
181
+ const [capsResult, collectionsResult] = await Promise.all([
182
+ apiFetch<Capabilities>("/api/capabilities"),
183
+ apiFetch<Collection[]>("/api/collections"),
184
+ ]);
185
+
186
+ if (capsResult.data) {
187
+ const caps = capsResult.data;
188
+ setCapabilities(caps);
189
+ setThoroughness(caps.hybrid ? "balanced" : "fast");
190
+ }
191
+
192
+ if (collectionsResult.data) {
193
+ setCollections(collectionsResult.data);
159
194
  }
160
195
  }
161
- void fetchCapabilities();
196
+
197
+ void bootstrap();
162
198
  }, []);
163
199
 
164
- // Cycle thoroughness with 't' key
165
200
  const cycleThoroughness = useCallback(() => {
166
201
  setThoroughness((current) => {
202
+ if (!hybridAvailable) {
203
+ return "fast";
204
+ }
167
205
  const currentIdx = THOROUGHNESS_ORDER.indexOf(current);
168
206
  const nextIdx = (currentIdx + 1) % THOROUGHNESS_ORDER.length;
169
207
  return THOROUGHNESS_ORDER[nextIdx];
170
208
  });
171
- }, []);
209
+ }, [hybridAvailable]);
172
210
 
173
211
  const shortcuts = useMemo(
174
212
  () => [{ key: "t", action: cycleThoroughness }],
175
213
  [cycleThoroughness]
176
214
  );
177
-
178
215
  useKeyboardShortcuts(shortcuts);
179
216
 
180
- // Scroll to bottom when conversation updates
181
217
  useEffect(() => {
182
218
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
183
- }, []);
219
+ }, [conversation]);
184
220
 
185
- const handleSubmit = async (e: React.FormEvent) => {
186
- e.preventDefault();
187
- if (!query.trim()) {
188
- return;
189
- }
221
+ const handleSubmit = useCallback(
222
+ async (e: React.FormEvent) => {
223
+ e.preventDefault();
224
+ if (!query.trim()) {
225
+ return;
226
+ }
190
227
 
191
- const entryId = crypto.randomUUID();
192
- const currentQuery = query.trim();
193
-
194
- // Add entry to conversation
195
- setConversation((prev) => [
196
- ...prev,
197
- { id: entryId, query: currentQuery, response: null, loading: true },
198
- ]);
199
- setQuery("");
200
-
201
- // Build request body with thoroughness-mapped params
202
- // fast: BM25-only via noExpand + noRerank
203
- // balanced: with reranking, no expansion
204
- // thorough: full pipeline
205
- const requestBody: Record<string, unknown> = {
206
- query: currentQuery,
207
- limit: 5,
208
- };
209
-
210
- if (thoroughness === "fast") {
211
- requestBody.noExpand = true;
212
- requestBody.noRerank = true;
213
- } else if (thoroughness === "balanced") {
214
- requestBody.noExpand = true;
215
- requestBody.noRerank = false;
216
- } else {
217
- // thorough - full pipeline
218
- requestBody.noExpand = false;
219
- requestBody.noRerank = false;
220
- }
228
+ const entryId = crypto.randomUUID();
229
+ const currentQuery = query.trim();
221
230
 
222
- // Make API call
223
- const { data, error } = await apiFetch<AskResponse>("/api/ask", {
224
- method: "POST",
225
- body: JSON.stringify(requestBody),
226
- });
231
+ setConversation((prev) => [
232
+ ...prev,
233
+ { id: entryId, query: currentQuery, response: null, loading: true },
234
+ ]);
235
+ setQuery("");
227
236
 
228
- // Update conversation with response
229
- setConversation((prev) =>
230
- prev.map((entry) =>
231
- entry.id === entryId
232
- ? {
233
- ...entry,
234
- response: data ?? null,
235
- loading: false,
236
- error: error ?? undefined,
237
- }
238
- : entry
239
- )
240
- );
241
- };
237
+ const requestBody: Record<string, unknown> = {
238
+ query: currentQuery,
239
+ limit: 5,
240
+ };
241
+
242
+ if (selectedCollection) {
243
+ requestBody.collection = selectedCollection;
244
+ }
245
+ if (since) {
246
+ requestBody.since = since;
247
+ }
248
+ if (until) {
249
+ requestBody.until = until;
250
+ }
251
+ if (category.trim()) {
252
+ requestBody.category = category.trim();
253
+ }
254
+ if (author.trim()) {
255
+ requestBody.author = author.trim();
256
+ }
257
+
258
+ const normalizedTags = parseTagsCsv(tagsInput);
259
+ if (normalizedTags.length > 0) {
260
+ if (tagMode === "all") {
261
+ requestBody.tagsAll = normalizedTags.join(",");
262
+ } else {
263
+ requestBody.tagsAny = normalizedTags.join(",");
264
+ }
265
+ }
266
+
267
+ if (thoroughness === "fast") {
268
+ requestBody.noExpand = true;
269
+ requestBody.noRerank = true;
270
+ } else if (thoroughness === "balanced") {
271
+ requestBody.noExpand = true;
272
+ requestBody.noRerank = false;
273
+ } else {
274
+ requestBody.noExpand = false;
275
+ requestBody.noRerank = false;
276
+ }
277
+
278
+ const { data, error } = await apiFetch<AskResponse>("/api/ask", {
279
+ method: "POST",
280
+ body: JSON.stringify(requestBody),
281
+ });
282
+
283
+ setConversation((prev) =>
284
+ prev.map((entry) =>
285
+ entry.id === entryId
286
+ ? {
287
+ ...entry,
288
+ response: data ?? null,
289
+ loading: false,
290
+ error: error ?? undefined,
291
+ }
292
+ : entry
293
+ )
294
+ );
295
+ },
296
+ [
297
+ author,
298
+ category,
299
+ query,
300
+ selectedCollection,
301
+ since,
302
+ tagMode,
303
+ tagsInput,
304
+ thoroughness,
305
+ until,
306
+ ]
307
+ );
242
308
 
243
309
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
244
310
  if (e.key === "Enter" && !e.shiftKey) {
@@ -247,11 +313,31 @@ export default function Ask({ navigate }: PageProps) {
247
313
  }
248
314
  };
249
315
 
316
+ const clearFilters = () => {
317
+ setSelectedCollection("");
318
+ setSince("");
319
+ setUntil("");
320
+ setCategory("");
321
+ setAuthor("");
322
+ setTagsInput("");
323
+ setTagMode("any");
324
+ };
325
+
250
326
  const answerAvailable = capabilities?.answer ?? false;
251
327
 
328
+ const activeFilterPills = [
329
+ selectedCollection ? `collection:${selectedCollection}` : null,
330
+ since ? `since:${since}` : null,
331
+ until ? `until:${until}` : null,
332
+ category.trim() ? `category:${category.trim()}` : null,
333
+ author.trim() ? `author:${author.trim()}` : null,
334
+ parseTagsCsv(tagsInput).length > 0
335
+ ? `${tagMode}:${parseTagsCsv(tagsInput).join(",")}`
336
+ : null,
337
+ ].filter((pill): pill is string => Boolean(pill));
338
+
252
339
  return (
253
- <div className="flex min-h-screen flex-col">
254
- {/* Header */}
340
+ <div className="flex h-dvh min-h-0 flex-col overflow-hidden">
255
341
  <header className="glass sticky top-0 z-10 border-border/50 border-b">
256
342
  <div className="flex items-center gap-4 px-8 py-4">
257
343
  <Button
@@ -265,20 +351,16 @@ export default function Ask({ navigate }: PageProps) {
265
351
  </Button>
266
352
  <h1 className="font-semibold text-xl">Ask</h1>
267
353
  <div className="ml-auto flex items-center gap-4">
268
- {/* Search depth selector */}
269
354
  <ThoroughnessSelector
270
355
  disabled={!capabilities?.hybrid}
271
356
  onChange={setThoroughness}
272
357
  value={thoroughness}
273
358
  />
274
359
 
275
- {/* Divider */}
276
360
  <div className="h-6 w-px bg-border/40" />
277
361
 
278
- {/* AI model selector */}
279
362
  <AIModelSelector />
280
363
 
281
- {/* Capability badges */}
282
364
  {capabilities && (
283
365
  <div className="flex items-center gap-2">
284
366
  {capabilities.vector && (
@@ -297,12 +379,168 @@ export default function Ask({ navigate }: PageProps) {
297
379
  </div>
298
380
  </header>
299
381
 
300
- {/* Conversation area */}
301
- <main className="flex-1 overflow-y-auto p-8">
302
- <div className="mx-auto max-w-3xl space-y-8">
303
- {/* Empty state */}
382
+ <main className="min-h-0 flex-1 overflow-y-auto px-6 py-6 md:p-8">
383
+ <div className="mx-auto max-w-3xl space-y-5">
384
+ <Collapsible onOpenChange={setShowAdvanced} open={showAdvanced}>
385
+ <CollapsibleTrigger asChild>
386
+ <Button
387
+ className="gap-2"
388
+ size="sm"
389
+ type="button"
390
+ variant="outline"
391
+ >
392
+ <SlidersHorizontal className="size-4" />
393
+ Advanced Retrieval
394
+ <ChevronDown
395
+ className={cn(
396
+ "size-4 transition-transform",
397
+ showAdvanced && "rotate-180"
398
+ )}
399
+ />
400
+ </Button>
401
+ </CollapsibleTrigger>
402
+ <CollapsibleContent className="pt-3">
403
+ <Card className="border-border/50 bg-card/50">
404
+ <CardContent className="space-y-4 pt-4">
405
+ <div className="grid gap-3 md:grid-cols-2">
406
+ <div>
407
+ <p className="mb-1 text-muted-foreground text-xs">
408
+ Collection
409
+ </p>
410
+ <Select
411
+ onValueChange={(value) =>
412
+ setSelectedCollection(value === "all" ? "" : value)
413
+ }
414
+ value={selectedCollection || "all"}
415
+ >
416
+ <SelectTrigger className="w-full">
417
+ <SelectValue placeholder="All collections" />
418
+ </SelectTrigger>
419
+ <SelectContent>
420
+ <SelectItem value="all">All collections</SelectItem>
421
+ {collections.map((collection) => (
422
+ <SelectItem
423
+ key={collection.name}
424
+ value={collection.name}
425
+ >
426
+ {collection.name}
427
+ </SelectItem>
428
+ ))}
429
+ </SelectContent>
430
+ </Select>
431
+ </div>
432
+
433
+ <div>
434
+ <p className="mb-1 text-muted-foreground text-xs">
435
+ Author
436
+ </p>
437
+ <Input
438
+ onChange={(e) => setAuthor(e.target.value)}
439
+ placeholder="gordon"
440
+ value={author}
441
+ />
442
+ </div>
443
+
444
+ <div>
445
+ <p className="mb-1 text-muted-foreground text-xs">
446
+ Category
447
+ </p>
448
+ <Input
449
+ onChange={(e) => setCategory(e.target.value)}
450
+ placeholder="engineering, research"
451
+ value={category}
452
+ />
453
+ </div>
454
+
455
+ <div className="grid gap-3 sm:grid-cols-2">
456
+ <div>
457
+ <p className="mb-1 text-muted-foreground text-xs">
458
+ Since
459
+ </p>
460
+ <Input
461
+ onChange={(e) => setSince(e.target.value)}
462
+ type="date"
463
+ value={since}
464
+ />
465
+ </div>
466
+ <div>
467
+ <p className="mb-1 text-muted-foreground text-xs">
468
+ Until
469
+ </p>
470
+ <Input
471
+ onChange={(e) => setUntil(e.target.value)}
472
+ type="date"
473
+ value={until}
474
+ />
475
+ </div>
476
+ </div>
477
+
478
+ <div className="md:col-span-2">
479
+ <p className="mb-1 text-muted-foreground text-xs">
480
+ Tags (comma separated)
481
+ </p>
482
+ <Input
483
+ onChange={(e) => setTagsInput(e.target.value)}
484
+ placeholder="project/alpha, urgent"
485
+ value={tagsInput}
486
+ />
487
+ </div>
488
+ </div>
489
+
490
+ <div className="flex flex-wrap items-center gap-2">
491
+ <span className="text-muted-foreground text-xs">
492
+ Tag match:
493
+ </span>
494
+ <Button
495
+ onClick={() => setTagMode("any")}
496
+ size="sm"
497
+ type="button"
498
+ variant={tagMode === "any" ? "default" : "outline"}
499
+ >
500
+ Any
501
+ </Button>
502
+ <Button
503
+ onClick={() => setTagMode("all")}
504
+ size="sm"
505
+ type="button"
506
+ variant={tagMode === "all" ? "default" : "outline"}
507
+ >
508
+ All
509
+ </Button>
510
+ <Button
511
+ className="ml-auto"
512
+ onClick={clearFilters}
513
+ size="sm"
514
+ type="button"
515
+ variant="ghost"
516
+ >
517
+ Clear filters
518
+ </Button>
519
+ </div>
520
+ </CardContent>
521
+ </Card>
522
+ </CollapsibleContent>
523
+ </Collapsible>
524
+
525
+ {activeFilterPills.length > 0 && (
526
+ <div className="flex flex-wrap items-center gap-2">
527
+ <span className="font-mono text-muted-foreground text-xs">
528
+ Filters:
529
+ </span>
530
+ {activeFilterPills.map((pill) => (
531
+ <Badge
532
+ className="font-mono text-[10px]"
533
+ key={pill}
534
+ variant="outline"
535
+ >
536
+ {pill}
537
+ </Badge>
538
+ ))}
539
+ </div>
540
+ )}
541
+
304
542
  {conversation.length === 0 && (
305
- <div className="py-20 text-center">
543
+ <div className="py-10 text-center md:py-14">
306
544
  <Sparkles className="mx-auto mb-4 size-12 text-primary/60" />
307
545
  <h2 className="mb-2 font-medium text-lg">Ask anything</h2>
308
546
  <p className="text-muted-foreground">
@@ -313,17 +551,14 @@ export default function Ask({ navigate }: PageProps) {
313
551
  </div>
314
552
  )}
315
553
 
316
- {/* Conversation entries */}
317
554
  {conversation.map((entry) => (
318
555
  <div className="space-y-4" key={entry.id}>
319
- {/* User query */}
320
556
  <div className="flex justify-end">
321
557
  <div className="max-w-[80%] rounded-lg bg-secondary px-4 py-3">
322
558
  <p className="text-foreground">{entry.query}</p>
323
559
  </div>
324
560
  </div>
325
561
 
326
- {/* AI response */}
327
562
  <div className="space-y-3">
328
563
  {entry.loading && (
329
564
  <div className="flex items-center gap-3">
@@ -344,7 +579,6 @@ export default function Ask({ navigate }: PageProps) {
344
579
 
345
580
  {entry.response && (
346
581
  <>
347
- {/* Answer */}
348
582
  {entry.response.answer && (
349
583
  <div className="prose prose-sm prose-invert max-w-none rounded-lg bg-card/50 p-4">
350
584
  <p className="whitespace-pre-wrap leading-relaxed">
@@ -357,7 +591,6 @@ export default function Ask({ navigate }: PageProps) {
357
591
  </div>
358
592
  )}
359
593
 
360
- {/* Citations */}
361
594
  {entry.response.citations &&
362
595
  entry.response.citations.length > 0 && (
363
596
  <Sources defaultOpen>
@@ -365,26 +598,26 @@ export default function Ask({ navigate }: PageProps) {
365
598
  count={entry.response.citations.length}
366
599
  />
367
600
  <SourcesContent>
368
- {entry.response.citations.map((c, i) => (
601
+ {entry.response.citations.map((citation, i) => (
369
602
  <Source
370
603
  href="#"
371
- key={`${c.docid}-${i}`}
372
- onClick={(e) => {
373
- e.preventDefault();
604
+ key={`${citation.docid}-${i}`}
605
+ onClick={(event) => {
606
+ event.preventDefault();
374
607
  navigate(
375
- `/doc?uri=${encodeURIComponent(c.uri)}`
608
+ `/doc?uri=${encodeURIComponent(citation.uri)}`
376
609
  );
377
610
  }}
378
- title={`[${i + 1}] ${c.uri.split("/").pop()}`}
611
+ title={`[${i + 1}] ${citation.uri.split("/").pop()}`}
379
612
  >
380
613
  <BookOpen className="size-4 shrink-0" />
381
614
  <span className="truncate">
382
- [{i + 1}] {c.uri.split("/").pop()}
615
+ [{i + 1}] {citation.uri.split("/").pop()}
383
616
  </span>
384
- {c.startLine && (
617
+ {citation.startLine && (
385
618
  <span className="ml-1 font-mono text-[10px] text-muted-foreground/60">
386
- L{c.startLine}
387
- {c.endLine && `-${c.endLine}`}
619
+ L{citation.startLine}
620
+ {citation.endLine && `-${citation.endLine}`}
388
621
  </span>
389
622
  )}
390
623
  </Source>
@@ -393,7 +626,6 @@ export default function Ask({ navigate }: PageProps) {
393
626
  </Sources>
394
627
  )}
395
628
 
396
- {/* Meta info */}
397
629
  <div className="flex items-center gap-2 text-muted-foreground/60 text-xs">
398
630
  <span>{entry.response.results.length} results</span>
399
631
  {entry.response.meta.vectorsUsed && (
@@ -414,20 +646,19 @@ export default function Ask({ navigate }: PageProps) {
414
646
  )}
415
647
  </div>
416
648
 
417
- {/* Show retrieved sources if no answer */}
418
649
  {!entry.response.answer &&
419
650
  entry.response.results.length > 0 && (
420
651
  <div className="space-y-2">
421
652
  <p className="font-medium text-muted-foreground text-sm">
422
653
  Search results:
423
654
  </p>
424
- {entry.response.results.map((r, i) => (
655
+ {entry.response.results.map((result, i) => (
425
656
  <Card
426
657
  className="cursor-pointer transition-colors hover:border-primary/50"
427
- key={`${r.docid}-${i}`}
658
+ key={`${result.docid}-${i}`}
428
659
  onClick={() =>
429
660
  navigate(
430
- `/doc?uri=${encodeURIComponent(r.uri)}`
661
+ `/doc?uri=${encodeURIComponent(result.uri)}`
431
662
  )
432
663
  }
433
664
  >
@@ -435,17 +666,18 @@ export default function Ask({ navigate }: PageProps) {
435
666
  <div className="flex items-start justify-between gap-2">
436
667
  <div className="min-w-0">
437
668
  <p className="font-medium text-primary text-sm">
438
- {r.title || r.uri.split("/").pop()}
669
+ {result.title ||
670
+ result.uri.split("/").pop()}
439
671
  </p>
440
672
  <p className="line-clamp-2 text-muted-foreground text-xs">
441
- {r.snippet.slice(0, 200)}...
673
+ {result.snippet.slice(0, 200)}...
442
674
  </p>
443
675
  </div>
444
676
  <Badge
445
677
  className="shrink-0 font-mono text-[10px]"
446
678
  variant="secondary"
447
679
  >
448
- {(r.score * 100).toFixed(0)}%
680
+ {(result.score * 100).toFixed(0)}%
449
681
  </Badge>
450
682
  </div>
451
683
  </CardContent>
@@ -454,7 +686,6 @@ export default function Ask({ navigate }: PageProps) {
454
686
  </div>
455
687
  )}
456
688
 
457
- {/* No results */}
458
689
  {!entry.response.answer &&
459
690
  entry.response.results.length === 0 && (
460
691
  <div className="flex items-center gap-2 text-muted-foreground">
@@ -474,7 +705,6 @@ export default function Ask({ navigate }: PageProps) {
474
705
  </div>
475
706
  </main>
476
707
 
477
- {/* Input area */}
478
708
  <footer className="glass sticky bottom-0 border-border/50 border-t">
479
709
  <form
480
710
  className="mx-auto flex max-w-3xl items-end gap-3 p-4"