@gmickel/gno 1.12.2 → 1.12.3
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.
- package/README.md +1 -1
- package/assets/skill/SKILL.md +3 -1
- package/package.json +2 -1
- package/src/core/indexed-reference.ts +68 -0
- package/src/core/ref-parser.ts +6 -1
- package/src/index.ts +11 -2
- package/src/ingestion/sync.ts +182 -15
- package/src/ingestion/types.ts +2 -0
- package/src/mcp/resources/index.ts +71 -47
- package/src/mcp/tools/get.ts +108 -93
- package/src/mcp/tools/multi-get.ts +116 -99
- package/src/sdk/client.ts +56 -31
- package/src/serve/public/components/AIModelSelector.tsx +22 -7
- package/src/serve/public/pages/Dashboard.tsx +1 -1
- package/src/serve/routes/api.ts +26 -1
- package/src/serve/server.ts +11 -2
- package/src/serve/status.ts +24 -0
- package/src/serve/watch-service.ts +2 -1
- package/src/store/sqlite/adapter.ts +99 -49
- package/src/store/sqlite/scoped-index.ts +68 -0
- package/src/store/types.ts +3 -1
package/src/mcp/tools/get.ts
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
getDocumentCapabilities,
|
|
15
15
|
type DocumentCapabilities,
|
|
16
16
|
} from "../../core/document-capabilities";
|
|
17
|
+
import { resolveEffectiveIndex } from "../../core/indexed-reference";
|
|
17
18
|
import { parseRef } from "../../core/ref-parser";
|
|
19
|
+
import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
|
|
18
20
|
import { runTool, type ToolResult } from "./index";
|
|
19
21
|
|
|
20
22
|
interface GetInput {
|
|
@@ -128,109 +130,122 @@ export function handleGet(
|
|
|
128
130
|
throw new Error(parsed.error);
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
throw new Error(`Document not found: ${args.ref}`);
|
|
133
|
+
const resolution = resolveEffectiveIndex([args.ref], ctx.indexName);
|
|
134
|
+
if (!resolution.ok) {
|
|
135
|
+
throw new Error(resolution.error);
|
|
135
136
|
}
|
|
137
|
+
const scoped = await openScopedIndexStore({
|
|
138
|
+
activeStore: ctx.store,
|
|
139
|
+
activeIndexName: ctx.indexName,
|
|
140
|
+
requestedIndexName: resolution.value.indexName,
|
|
141
|
+
config: ctx.config,
|
|
142
|
+
configPath: ctx.actualConfigPath,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Lookup document
|
|
147
|
+
const doc = await lookupDocument(scoped.store, parsed);
|
|
148
|
+
if (!doc) {
|
|
149
|
+
throw new Error(`Document not found: ${args.ref}`);
|
|
150
|
+
}
|
|
136
151
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
// Get content
|
|
153
|
+
if (!doc.mirrorHash) {
|
|
154
|
+
throw new Error("Document has no indexed content");
|
|
155
|
+
}
|
|
141
156
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
const contentResult = await scoped.store.getContent(doc.mirrorHash);
|
|
158
|
+
if (!contentResult.ok) {
|
|
159
|
+
throw new Error(contentResult.error.message);
|
|
160
|
+
}
|
|
146
161
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} else {
|
|
166
|
-
const count = args.lineCount ?? totalLines - startLine + 1;
|
|
167
|
-
const endLine = Math.min(startLine + count - 1, totalLines);
|
|
168
|
-
|
|
169
|
-
const slicedLines = contentLines.slice(startLine - 1, endLine);
|
|
170
|
-
|
|
171
|
-
if (showLineNumbers) {
|
|
172
|
-
content = slicedLines
|
|
173
|
-
.map((line, i) => `${startLine + i}: ${line}`)
|
|
174
|
-
.join("\n");
|
|
162
|
+
const fullContent = contentResult.value ?? "";
|
|
163
|
+
const contentLines = fullContent.split("\n");
|
|
164
|
+
const totalLines = contentLines.length;
|
|
165
|
+
|
|
166
|
+
// Apply line range if specified
|
|
167
|
+
let content = fullContent;
|
|
168
|
+
let returnedLines: { start: number; end: number } | undefined;
|
|
169
|
+
|
|
170
|
+
// lineNumbers defaults to true per spec
|
|
171
|
+
const showLineNumbers = args.lineNumbers !== false;
|
|
172
|
+
|
|
173
|
+
if (args.fromLine || args.lineCount) {
|
|
174
|
+
const startLine = args.fromLine ?? 1;
|
|
175
|
+
// Clamp startLine to valid range
|
|
176
|
+
if (startLine > totalLines) {
|
|
177
|
+
// Return empty content for out-of-range request
|
|
178
|
+
content = "";
|
|
179
|
+
returnedLines = undefined;
|
|
175
180
|
} else {
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
const count = args.lineCount ?? totalLines - startLine + 1;
|
|
182
|
+
const endLine = Math.min(startLine + count - 1, totalLines);
|
|
178
183
|
|
|
179
|
-
|
|
184
|
+
const slicedLines = contentLines.slice(startLine - 1, endLine);
|
|
185
|
+
|
|
186
|
+
if (showLineNumbers) {
|
|
187
|
+
content = slicedLines
|
|
188
|
+
.map((line, i) => `${startLine + i}: ${line}`)
|
|
189
|
+
.join("\n");
|
|
190
|
+
} else {
|
|
191
|
+
content = slicedLines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
returnedLines = { start: startLine, end: endLine };
|
|
195
|
+
}
|
|
196
|
+
} else if (showLineNumbers) {
|
|
197
|
+
content = contentLines
|
|
198
|
+
.map((line, i) => `${i + 1}: ${line}`)
|
|
199
|
+
.join("\n");
|
|
180
200
|
}
|
|
181
|
-
} else if (showLineNumbers) {
|
|
182
|
-
content = contentLines.map((line, i) => `${i + 1}: ${line}`).join("\n");
|
|
183
|
-
}
|
|
184
201
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
// Build absPath
|
|
203
|
+
const uriParsed = parseUri(doc.uri);
|
|
204
|
+
let absPath: string | undefined;
|
|
205
|
+
if (uriParsed) {
|
|
206
|
+
const collection = ctx.collections.find(
|
|
207
|
+
(c) => c.name === uriParsed.collection
|
|
208
|
+
);
|
|
209
|
+
if (collection) {
|
|
210
|
+
absPath = pathJoin(collection.path, doc.relPath);
|
|
211
|
+
}
|
|
194
212
|
}
|
|
195
|
-
}
|
|
196
213
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
doc.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return response;
|
|
214
|
+
const response: GetResponse = {
|
|
215
|
+
docid: doc.docid,
|
|
216
|
+
uri: decorateUriForIndex(doc.uri, scoped.indexName),
|
|
217
|
+
title: doc.title ?? undefined,
|
|
218
|
+
content,
|
|
219
|
+
totalLines,
|
|
220
|
+
returnedLines,
|
|
221
|
+
language: doc.languageHint ?? undefined,
|
|
222
|
+
source: {
|
|
223
|
+
absPath,
|
|
224
|
+
relPath: doc.relPath,
|
|
225
|
+
mime: doc.sourceMime,
|
|
226
|
+
ext: doc.sourceExt,
|
|
227
|
+
modifiedAt: doc.sourceMtime,
|
|
228
|
+
sizeBytes: doc.sourceSize,
|
|
229
|
+
sourceHash: doc.sourceHash,
|
|
230
|
+
},
|
|
231
|
+
conversion: doc.mirrorHash
|
|
232
|
+
? {
|
|
233
|
+
converterId: doc.converterId ?? undefined,
|
|
234
|
+
converterVersion: doc.converterVersion ?? undefined,
|
|
235
|
+
mirrorHash: doc.mirrorHash,
|
|
236
|
+
}
|
|
237
|
+
: undefined,
|
|
238
|
+
capabilities: getDocumentCapabilities({
|
|
239
|
+
sourceExt: doc.sourceExt,
|
|
240
|
+
sourceMime: doc.sourceMime,
|
|
241
|
+
contentAvailable: doc.mirrorHash !== null,
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return response;
|
|
246
|
+
} finally {
|
|
247
|
+
await scoped.close();
|
|
248
|
+
}
|
|
234
249
|
},
|
|
235
250
|
formatGetResponse
|
|
236
251
|
);
|
|
@@ -10,7 +10,9 @@ import type { DocumentRow, StorePort } from "../../store/types";
|
|
|
10
10
|
import type { ToolContext } from "../server";
|
|
11
11
|
|
|
12
12
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
13
|
+
import { resolveEffectiveIndex } from "../../core/indexed-reference";
|
|
13
14
|
import { parseRef } from "../../core/ref-parser";
|
|
15
|
+
import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
|
|
14
16
|
import { runTool, type ToolResult } from "./index";
|
|
15
17
|
|
|
16
18
|
interface MultiGetInput {
|
|
@@ -144,121 +146,136 @@ export function handleMultiGet(
|
|
|
144
146
|
const skipped: Array<{ ref: string; reason: string }> = [];
|
|
145
147
|
|
|
146
148
|
let refs: string[] = args.refs ?? [];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// For pattern matching, list all documents and filter
|
|
151
|
-
const listResult = await ctx.store.listDocuments();
|
|
152
|
-
if (!listResult.ok) {
|
|
153
|
-
throw new Error(listResult.error.message);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Safe glob-like pattern matching: escape regex metacharacters first
|
|
157
|
-
const pattern = args.pattern;
|
|
158
|
-
// Escape all regex metacharacters except * and ?
|
|
159
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
160
|
-
// Then convert glob wildcards to regex
|
|
161
|
-
const regexPattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
162
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
163
|
-
|
|
164
|
-
refs = listResult.value
|
|
165
|
-
.filter((d) => regex.test(d.uri) || regex.test(d.relPath))
|
|
166
|
-
.map((d) => d.uri);
|
|
149
|
+
const resolution = resolveEffectiveIndex(refs, ctx.indexName);
|
|
150
|
+
if (!resolution.ok) {
|
|
151
|
+
throw new Error(resolution.error);
|
|
167
152
|
}
|
|
153
|
+
const scoped = await openScopedIndexStore({
|
|
154
|
+
activeStore: ctx.store,
|
|
155
|
+
activeIndexName: ctx.indexName,
|
|
156
|
+
requestedIndexName: resolution.value.indexName,
|
|
157
|
+
config: ctx.config,
|
|
158
|
+
configPath: ctx.actualConfigPath,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Pattern-based lookup
|
|
163
|
+
if (args.pattern) {
|
|
164
|
+
// For pattern matching, list all documents and filter
|
|
165
|
+
const listResult = await scoped.store.listDocuments();
|
|
166
|
+
if (!listResult.ok) {
|
|
167
|
+
throw new Error(listResult.error.message);
|
|
168
|
+
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
// Safe glob-like pattern matching: escape regex metacharacters first
|
|
171
|
+
const pattern = args.pattern;
|
|
172
|
+
// Escape all regex metacharacters except * and ?
|
|
173
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
174
|
+
// Then convert glob wildcards to regex
|
|
175
|
+
const regexPattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
176
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
177
|
+
|
|
178
|
+
refs = listResult.value
|
|
179
|
+
.filter((d) => regex.test(d.uri) || regex.test(d.relPath))
|
|
180
|
+
.map((d) => d.uri);
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
// Process each reference
|
|
184
|
+
for (const ref of refs) {
|
|
185
|
+
const parsed = parseRef(ref);
|
|
186
|
+
if ("error" in parsed) {
|
|
187
|
+
skipped.push({ ref, reason: parsed.error });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
const doc = await lookupDocument(scoped.store, parsed);
|
|
192
|
+
if (!doc) {
|
|
193
|
+
skipped.push({ ref, reason: "Not found" });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
187
196
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
197
|
+
if (!doc.mirrorHash) {
|
|
198
|
+
skipped.push({ ref, reason: "No indexed content" });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
194
201
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (contentBuffer.length > maxBytes) {
|
|
201
|
-
// Truncate by bytes, then decode safely (may cut mid-codepoint)
|
|
202
|
-
const truncatedBuffer = contentBuffer.subarray(0, maxBytes);
|
|
203
|
-
// Decode with replacement char for incomplete sequences
|
|
204
|
-
content = truncatedBuffer.toString("utf8");
|
|
205
|
-
// Remove potential trailing replacement char from cut codepoint
|
|
206
|
-
if (content.endsWith("\uFFFD")) {
|
|
207
|
-
content = content.slice(0, -1);
|
|
202
|
+
// Get content
|
|
203
|
+
const contentResult = await scoped.store.getContent(doc.mirrorHash);
|
|
204
|
+
if (!contentResult.ok) {
|
|
205
|
+
skipped.push({ ref, reason: contentResult.error.message });
|
|
206
|
+
continue;
|
|
208
207
|
}
|
|
209
|
-
truncated = true;
|
|
210
|
-
}
|
|
211
208
|
|
|
212
|
-
|
|
209
|
+
let content = contentResult.value ?? "";
|
|
210
|
+
let truncated = false;
|
|
211
|
+
|
|
212
|
+
// Apply maxBytes truncation (actual UTF-8 bytes, not characters)
|
|
213
|
+
const contentBuffer = Buffer.from(content, "utf8");
|
|
214
|
+
if (contentBuffer.length > maxBytes) {
|
|
215
|
+
// Truncate by bytes, then decode safely (may cut mid-codepoint)
|
|
216
|
+
const truncatedBuffer = contentBuffer.subarray(0, maxBytes);
|
|
217
|
+
// Decode with replacement char for incomplete sequences
|
|
218
|
+
content = truncatedBuffer.toString("utf8");
|
|
219
|
+
// Remove potential trailing replacement char from cut codepoint
|
|
220
|
+
if (content.endsWith("\uFFFD")) {
|
|
221
|
+
content = content.slice(0, -1);
|
|
222
|
+
}
|
|
223
|
+
truncated = true;
|
|
224
|
+
}
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
if (args.lineNumbers !== false) {
|
|
216
|
-
content = contentLines
|
|
217
|
-
.map((line, i) => `${i + 1}: ${line}`)
|
|
218
|
-
.join("\n");
|
|
219
|
-
}
|
|
226
|
+
const contentLines = content.split("\n");
|
|
220
227
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
(c) => c.name === uriParsed.collection
|
|
227
|
-
);
|
|
228
|
-
if (collection) {
|
|
229
|
-
absPath = pathJoin(collection.path, doc.relPath);
|
|
228
|
+
// Apply line numbers (defaults to true per spec)
|
|
229
|
+
if (args.lineNumbers !== false) {
|
|
230
|
+
content = contentLines
|
|
231
|
+
.map((line, i) => `${i + 1}: ${line}`)
|
|
232
|
+
.join("\n");
|
|
230
233
|
}
|
|
234
|
+
|
|
235
|
+
// Build absPath
|
|
236
|
+
const uriParsed = parseUri(doc.uri);
|
|
237
|
+
let absPath: string | undefined;
|
|
238
|
+
if (uriParsed) {
|
|
239
|
+
const collection = ctx.collections.find(
|
|
240
|
+
(c) => c.name === uriParsed.collection
|
|
241
|
+
);
|
|
242
|
+
if (collection) {
|
|
243
|
+
absPath = pathJoin(collection.path, doc.relPath);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
documents.push({
|
|
248
|
+
docid: doc.docid,
|
|
249
|
+
uri: decorateUriForIndex(doc.uri, scoped.indexName),
|
|
250
|
+
title: doc.title ?? undefined,
|
|
251
|
+
content,
|
|
252
|
+
totalLines: (contentResult.value ?? "").split("\n").length,
|
|
253
|
+
truncated,
|
|
254
|
+
source: {
|
|
255
|
+
absPath,
|
|
256
|
+
relPath: doc.relPath,
|
|
257
|
+
mime: doc.sourceMime,
|
|
258
|
+
ext: doc.sourceExt,
|
|
259
|
+
modifiedAt: doc.sourceMtime,
|
|
260
|
+
sizeBytes: doc.sourceSize,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
231
263
|
}
|
|
232
264
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
source: {
|
|
241
|
-
absPath,
|
|
242
|
-
relPath: doc.relPath,
|
|
243
|
-
mime: doc.sourceMime,
|
|
244
|
-
ext: doc.sourceExt,
|
|
245
|
-
modifiedAt: doc.sourceMtime,
|
|
246
|
-
sizeBytes: doc.sourceSize,
|
|
265
|
+
const response: MultiGetResponse = {
|
|
266
|
+
documents,
|
|
267
|
+
skipped,
|
|
268
|
+
meta: {
|
|
269
|
+
requested: refs.length,
|
|
270
|
+
returned: documents.length,
|
|
271
|
+
skipped: skipped.length,
|
|
247
272
|
},
|
|
248
|
-
}
|
|
249
|
-
}
|
|
273
|
+
};
|
|
250
274
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
requested: refs.length,
|
|
256
|
-
returned: documents.length,
|
|
257
|
-
skipped: skipped.length,
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
return response;
|
|
275
|
+
return response;
|
|
276
|
+
} finally {
|
|
277
|
+
await scoped.close();
|
|
278
|
+
}
|
|
262
279
|
},
|
|
263
280
|
formatMultiGetResponse
|
|
264
281
|
);
|
package/src/sdk/client.ts
CHANGED
|
@@ -39,11 +39,7 @@ import type {
|
|
|
39
39
|
GnoVectorSearchOptions,
|
|
40
40
|
} from "./types";
|
|
41
41
|
|
|
42
|
-
import {
|
|
43
|
-
decorateUriForIndex,
|
|
44
|
-
getIndexDbPath,
|
|
45
|
-
parseUri,
|
|
46
|
-
} from "../app/constants";
|
|
42
|
+
import { decorateUriForIndex, getIndexDbPath } from "../app/constants";
|
|
47
43
|
import {
|
|
48
44
|
ConfigSchema,
|
|
49
45
|
loadConfig,
|
|
@@ -69,6 +65,7 @@ import {
|
|
|
69
65
|
planMoveRefactor,
|
|
70
66
|
planRenameRefactor,
|
|
71
67
|
} from "../core/file-refactors";
|
|
68
|
+
import { resolveEffectiveIndex } from "../core/indexed-reference";
|
|
72
69
|
import { resolveNoteCreatePlan } from "../core/note-creation";
|
|
73
70
|
import { resolveNotePreset } from "../core/note-presets";
|
|
74
71
|
import { extractSections } from "../core/sections";
|
|
@@ -92,6 +89,7 @@ import { searchHybrid } from "../pipeline/hybrid";
|
|
|
92
89
|
import { searchBm25 } from "../pipeline/search";
|
|
93
90
|
import { searchVectorWithEmbedding } from "../pipeline/vsearch";
|
|
94
91
|
import { SqliteAdapter } from "../store/sqlite/adapter";
|
|
92
|
+
import { openScopedIndexStore } from "../store/sqlite/scoped-index";
|
|
95
93
|
import { createVectorIndexPort } from "../store/vector";
|
|
96
94
|
import {
|
|
97
95
|
getDocumentByRef,
|
|
@@ -647,36 +645,63 @@ class GnoClientImpl implements GnoClient {
|
|
|
647
645
|
|
|
648
646
|
async get(ref: string, options: GnoGetOptions = {}) {
|
|
649
647
|
this.assertOpen();
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
this.
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
648
|
+
const resolution = resolveEffectiveIndex([ref], this.indexName);
|
|
649
|
+
if (!resolution.ok) {
|
|
650
|
+
throw sdkError("VALIDATION", resolution.error);
|
|
651
|
+
}
|
|
652
|
+
const scoped = await openScopedIndexStore({
|
|
653
|
+
activeStore: this.store,
|
|
654
|
+
activeIndexName: this.indexName,
|
|
655
|
+
requestedIndexName: resolution.value.indexName,
|
|
656
|
+
config: this.config,
|
|
657
|
+
configPath: this.configPath,
|
|
658
|
+
});
|
|
659
|
+
try {
|
|
660
|
+
const result = await getDocumentByRef(
|
|
661
|
+
scoped.store,
|
|
662
|
+
this.config,
|
|
663
|
+
ref,
|
|
664
|
+
options
|
|
665
|
+
);
|
|
666
|
+
return {
|
|
667
|
+
...result,
|
|
668
|
+
uri: decorateUriForIndex(result.uri, scoped.indexName),
|
|
669
|
+
};
|
|
670
|
+
} finally {
|
|
671
|
+
await scoped.close();
|
|
672
|
+
}
|
|
663
673
|
}
|
|
664
674
|
|
|
665
675
|
async multiGet(refs: string[], options: GnoMultiGetOptions = {}) {
|
|
666
676
|
this.assertOpen();
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
677
|
+
const resolution = resolveEffectiveIndex(refs, this.indexName);
|
|
678
|
+
if (!resolution.ok) {
|
|
679
|
+
throw sdkError("VALIDATION", resolution.error);
|
|
680
|
+
}
|
|
681
|
+
const scoped = await openScopedIndexStore({
|
|
682
|
+
activeStore: this.store,
|
|
683
|
+
activeIndexName: this.indexName,
|
|
684
|
+
requestedIndexName: resolution.value.indexName,
|
|
685
|
+
config: this.config,
|
|
686
|
+
configPath: this.configPath,
|
|
687
|
+
});
|
|
688
|
+
try {
|
|
689
|
+
const result = await multiGetDocuments(
|
|
690
|
+
scoped.store,
|
|
691
|
+
this.config,
|
|
692
|
+
refs,
|
|
693
|
+
options
|
|
694
|
+
);
|
|
695
|
+
return {
|
|
696
|
+
...result,
|
|
697
|
+
documents: result.documents.map((doc) => ({
|
|
698
|
+
...doc,
|
|
699
|
+
uri: decorateUriForIndex(doc.uri, scoped.indexName),
|
|
700
|
+
})),
|
|
701
|
+
};
|
|
702
|
+
} finally {
|
|
703
|
+
await scoped.close();
|
|
704
|
+
}
|
|
680
705
|
}
|
|
681
706
|
|
|
682
707
|
async list(options: GnoListOptions = {}) {
|
|
@@ -128,6 +128,7 @@ function formatModelRole(uri: string | undefined): string {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
export interface AIModelSelectorProps {
|
|
131
|
+
appStatus?: AppStatusResponse | null;
|
|
131
132
|
onPresetChange?: (presetId: string) => void;
|
|
132
133
|
showDetails?: boolean;
|
|
133
134
|
showDownloadAction?: boolean;
|
|
@@ -139,6 +140,7 @@ function isCustomPreset(preset: Preset): boolean {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
export function AIModelSelector({
|
|
143
|
+
appStatus,
|
|
142
144
|
onPresetChange,
|
|
143
145
|
showDetails = false,
|
|
144
146
|
showDownloadAction = true,
|
|
@@ -164,6 +166,7 @@ export function AIModelSelector({
|
|
|
164
166
|
null
|
|
165
167
|
);
|
|
166
168
|
const pollInterval = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
169
|
+
const parentOwnsStatus = useRef(appStatus !== undefined);
|
|
167
170
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
168
171
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
169
172
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
@@ -274,13 +277,15 @@ export function AIModelSelector({
|
|
|
274
277
|
setLoading(false);
|
|
275
278
|
});
|
|
276
279
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
if (!parentOwnsStatus.current) {
|
|
281
|
+
void apiFetch<AppStatusResponse>("/api/status").then(({ data }) => {
|
|
282
|
+
if (data) {
|
|
283
|
+
setModelsNeeded(
|
|
284
|
+
data.bootstrap.models.cachedCount < data.bootstrap.models.totalCount
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
284
289
|
|
|
285
290
|
void apiFetch<DownloadStatus>("/api/models/status").then(({ data }) => {
|
|
286
291
|
if (data?.active) {
|
|
@@ -290,6 +295,16 @@ export function AIModelSelector({
|
|
|
290
295
|
});
|
|
291
296
|
}, [checkCapabilities]);
|
|
292
297
|
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (!appStatus) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
setModelsNeeded(
|
|
303
|
+
appStatus.bootstrap.models.cachedCount <
|
|
304
|
+
appStatus.bootstrap.models.totalCount
|
|
305
|
+
);
|
|
306
|
+
}, [appStatus]);
|
|
307
|
+
|
|
293
308
|
// Polling
|
|
294
309
|
useEffect(() => {
|
|
295
310
|
if (downloading && !pollInterval.current) {
|
|
@@ -396,7 +396,7 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
396
396
|
</div>
|
|
397
397
|
|
|
398
398
|
<div className="flex flex-wrap items-center gap-3 md:justify-end">
|
|
399
|
-
<AIModelSelector showLabel={false} />
|
|
399
|
+
<AIModelSelector appStatus={status} showLabel={false} />
|
|
400
400
|
<Button
|
|
401
401
|
disabled={syncing}
|
|
402
402
|
onClick={() => void handleSync()}
|