@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,5 +1,5 @@
1
1
  import { ArrowLeft, ChevronRight, FileText, FolderOpen } from "lucide-react";
2
- import { useEffect, useState } from "react";
2
+ import { Fragment, useEffect, useState } from "react";
3
3
 
4
4
  import { Loader } from "../components/ai-elements/loader";
5
5
  import { Badge } from "../components/ui/badge";
@@ -44,6 +44,9 @@ interface DocsResponse {
44
44
  total: number;
45
45
  limit: number;
46
46
  offset: number;
47
+ availableDateFields: string[];
48
+ sortField: string;
49
+ sortOrder: "asc" | "desc";
47
50
  }
48
51
 
49
52
  export default function Browse({ navigate }: PageProps) {
@@ -54,6 +57,9 @@ export default function Browse({ navigate }: PageProps) {
54
57
  const [offset, setOffset] = useState(0);
55
58
  const [loading, setLoading] = useState(false);
56
59
  const [initialLoad, setInitialLoad] = useState(true);
60
+ const [availableDateFields, setAvailableDateFields] = useState<string[]>([]);
61
+ const [sortField, setSortField] = useState("modified");
62
+ const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
57
63
  const limit = 25;
58
64
 
59
65
  // Parse collection from URL on mount
@@ -75,21 +81,41 @@ export default function Browse({ navigate }: PageProps) {
75
81
 
76
82
  useEffect(() => {
77
83
  setLoading(true);
78
- const url = selected
79
- ? `/api/docs?collection=${encodeURIComponent(selected)}&limit=${limit}&offset=${offset}`
80
- : `/api/docs?limit=${limit}&offset=${offset}`;
84
+ const params = new URLSearchParams({
85
+ limit: String(limit),
86
+ offset: String(offset),
87
+ sortField,
88
+ sortOrder,
89
+ });
90
+ if (selected) {
91
+ params.set("collection", selected);
92
+ }
93
+ const url = `/api/docs?${params.toString()}`;
81
94
 
82
95
  void apiFetch<DocsResponse>(url).then(({ data }) => {
83
96
  setLoading(false);
84
97
  setInitialLoad(false);
85
98
  if (data) {
99
+ setAvailableDateFields(data.availableDateFields ?? []);
100
+ setSortField(data.sortField);
101
+ setSortOrder(data.sortOrder);
86
102
  setDocs((prev) =>
87
103
  offset === 0 ? data.documents : [...prev, ...data.documents]
88
104
  );
89
105
  setTotal(data.total);
90
106
  }
91
107
  });
92
- }, [selected, offset]);
108
+ }, [selected, offset, sortField, sortOrder]);
109
+
110
+ useEffect(() => {
111
+ if (sortField === "modified" || availableDateFields.includes(sortField)) {
112
+ return;
113
+ }
114
+ setSortField("modified");
115
+ setSortOrder("desc");
116
+ setOffset(0);
117
+ setDocs([]);
118
+ }, [availableDateFields, sortField]);
93
119
 
94
120
  const handleCollectionChange = (value: string) => {
95
121
  const newSelected = value === "all" ? "" : value;
@@ -107,6 +133,24 @@ export default function Browse({ navigate }: PageProps) {
107
133
  setOffset((prev) => prev + limit);
108
134
  };
109
135
 
136
+ const handleSortChange = (value: string) => {
137
+ const [nextField, nextOrder] = value.split(":");
138
+ if (!nextField || (nextOrder !== "asc" && nextOrder !== "desc")) {
139
+ return;
140
+ }
141
+ setSortField(nextField);
142
+ setSortOrder(nextOrder);
143
+ setOffset(0);
144
+ setDocs([]);
145
+ };
146
+
147
+ const formatDateFieldLabel = (field: string) =>
148
+ field
149
+ .split("_")
150
+ .filter((token) => token.length > 0)
151
+ .map((token) => token.charAt(0).toUpperCase() + token.slice(1))
152
+ .join(" ");
153
+
110
154
  const getExtBadgeVariant = (ext: string) => {
111
155
  switch (ext.toLowerCase()) {
112
156
  case ".md":
@@ -157,6 +201,28 @@ export default function Browse({ navigate }: PageProps) {
157
201
  ))}
158
202
  </SelectContent>
159
203
  </Select>
204
+ <Select
205
+ onValueChange={handleSortChange}
206
+ value={`${sortField}:${sortOrder}`}
207
+ >
208
+ <SelectTrigger className="w-[230px]">
209
+ <SelectValue placeholder="Sort" />
210
+ </SelectTrigger>
211
+ <SelectContent>
212
+ <SelectItem value="modified:desc">Newest Modified</SelectItem>
213
+ <SelectItem value="modified:asc">Oldest Modified</SelectItem>
214
+ {availableDateFields.map((field) => (
215
+ <Fragment key={field}>
216
+ <SelectItem value={`${field}:desc`}>
217
+ {`Newest by ${formatDateFieldLabel(field)}`}
218
+ </SelectItem>
219
+ <SelectItem value={`${field}:asc`}>
220
+ {`Oldest by ${formatDateFieldLabel(field)}`}
221
+ </SelectItem>
222
+ </Fragment>
223
+ ))}
224
+ </SelectContent>
225
+ </Select>
160
226
  <Badge className="font-mono" variant="outline">
161
227
  {total.toLocaleString()} docs
162
228
  </Badge>
@@ -192,29 +192,10 @@ export default function DocView({ navigate }: PageProps) {
192
192
  // Request sequencing - ignore stale responses on rapid navigation
193
193
  const requestIdRef = useRef(0);
194
194
 
195
- // Track URL for reactivity when navigating between docs
196
- const [currentUri, setCurrentUri] = useState(() => {
195
+ // App remounts page on route/query changes, so URI is stable per render.
196
+ const currentUri = useMemo(() => {
197
197
  const params = new URLSearchParams(window.location.search);
198
198
  return params.get("uri") ?? "";
199
- });
200
-
201
- // Listen for URL changes (popstate for back/forward, locationchange for navigate())
202
- useEffect(() => {
203
- const updateUri = () => {
204
- const params = new URLSearchParams(window.location.search);
205
- const newUri = params.get("uri") ?? "";
206
- setCurrentUri(newUri);
207
- };
208
-
209
- // Listen for browser back/forward
210
- window.addEventListener("popstate", updateUri);
211
- // Listen for programmatic navigation via navigate()
212
- window.addEventListener("locationchange", updateUri);
213
-
214
- return () => {
215
- window.removeEventListener("popstate", updateUri);
216
- window.removeEventListener("locationchange", updateUri);
217
- };
218
199
  }, []);
219
200
 
220
201
  // Fetch document when URI changes