@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.
@@ -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
- // Lookup document
132
- const doc = await lookupDocument(ctx.store, parsed);
133
- if (!doc) {
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
- // Get content
138
- if (!doc.mirrorHash) {
139
- throw new Error("Document has no indexed content");
140
- }
152
+ // Get content
153
+ if (!doc.mirrorHash) {
154
+ throw new Error("Document has no indexed content");
155
+ }
141
156
 
142
- const contentResult = await ctx.store.getContent(doc.mirrorHash);
143
- if (!contentResult.ok) {
144
- throw new Error(contentResult.error.message);
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
- const fullContent = contentResult.value ?? "";
148
- const contentLines = fullContent.split("\n");
149
- const totalLines = contentLines.length;
150
-
151
- // Apply line range if specified
152
- let content = fullContent;
153
- let returnedLines: { start: number; end: number } | undefined;
154
-
155
- // lineNumbers defaults to true per spec
156
- const showLineNumbers = args.lineNumbers !== false;
157
-
158
- if (args.fromLine || args.lineCount) {
159
- const startLine = args.fromLine ?? 1;
160
- // Clamp startLine to valid range
161
- if (startLine > totalLines) {
162
- // Return empty content for out-of-range request
163
- content = "";
164
- returnedLines = undefined;
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
- content = slicedLines.join("\n");
177
- }
181
+ const count = args.lineCount ?? totalLines - startLine + 1;
182
+ const endLine = Math.min(startLine + count - 1, totalLines);
178
183
 
179
- returnedLines = { start: startLine, end: endLine };
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
- // Build absPath
186
- const uriParsed = parseUri(doc.uri);
187
- let absPath: string | undefined;
188
- if (uriParsed) {
189
- const collection = ctx.collections.find(
190
- (c) => c.name === uriParsed.collection
191
- );
192
- if (collection) {
193
- absPath = pathJoin(collection.path, doc.relPath);
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
- const response: GetResponse = {
198
- docid: doc.docid,
199
- uri: decorateUriForIndex(
200
- doc.uri,
201
- parsed.type === "uri"
202
- ? (parseUri(parsed.value)?.indexName ?? ctx.indexName)
203
- : ctx.indexName
204
- ),
205
- title: doc.title ?? undefined,
206
- content,
207
- totalLines,
208
- returnedLines,
209
- language: doc.languageHint ?? undefined,
210
- source: {
211
- absPath,
212
- relPath: doc.relPath,
213
- mime: doc.sourceMime,
214
- ext: doc.sourceExt,
215
- modifiedAt: doc.sourceMtime,
216
- sizeBytes: doc.sourceSize,
217
- sourceHash: doc.sourceHash,
218
- },
219
- conversion: doc.mirrorHash
220
- ? {
221
- converterId: doc.converterId ?? undefined,
222
- converterVersion: doc.converterVersion ?? undefined,
223
- mirrorHash: doc.mirrorHash,
224
- }
225
- : undefined,
226
- capabilities: getDocumentCapabilities({
227
- sourceExt: doc.sourceExt,
228
- sourceMime: doc.sourceMime,
229
- contentAvailable: doc.mirrorHash !== null,
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
- // Pattern-based lookup
149
- if (args.pattern) {
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
- // Process each reference
170
- for (const ref of refs) {
171
- const parsed = parseRef(ref);
172
- if ("error" in parsed) {
173
- skipped.push({ ref, reason: parsed.error });
174
- continue;
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
- const doc = await lookupDocument(ctx.store, parsed);
178
- if (!doc) {
179
- skipped.push({ ref, reason: "Not found" });
180
- continue;
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
- if (!doc.mirrorHash) {
184
- skipped.push({ ref, reason: "No indexed content" });
185
- continue;
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
- // Get content
189
- const contentResult = await ctx.store.getContent(doc.mirrorHash);
190
- if (!contentResult.ok) {
191
- skipped.push({ ref, reason: contentResult.error.message });
192
- continue;
193
- }
197
+ if (!doc.mirrorHash) {
198
+ skipped.push({ ref, reason: "No indexed content" });
199
+ continue;
200
+ }
194
201
 
195
- let content = contentResult.value ?? "";
196
- let truncated = false;
197
-
198
- // Apply maxBytes truncation (actual UTF-8 bytes, not characters)
199
- const contentBuffer = Buffer.from(content, "utf8");
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
- const contentLines = content.split("\n");
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
- // Apply line numbers (defaults to true per spec)
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
- // Build absPath
222
- const uriParsed = parseUri(doc.uri);
223
- let absPath: string | undefined;
224
- if (uriParsed) {
225
- const collection = ctx.collections.find(
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
- documents.push({
234
- docid: doc.docid,
235
- uri: decorateUriForIndex(doc.uri, ctx.indexName),
236
- title: doc.title ?? undefined,
237
- content,
238
- totalLines: (contentResult.value ?? "").split("\n").length,
239
- truncated,
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
- const response: MultiGetResponse = {
252
- documents,
253
- skipped,
254
- meta: {
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 explicitIndex = ref.startsWith("gno://")
651
- ? parseUri(ref)?.indexName
652
- : undefined;
653
- const result = await getDocumentByRef(
654
- this.store,
655
- this.config,
656
- ref,
657
- options
658
- );
659
- return {
660
- ...result,
661
- uri: decorateUriForIndex(result.uri, explicitIndex ?? this.indexName),
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 result = await multiGetDocuments(
668
- this.store,
669
- this.config,
670
- refs,
671
- options
672
- );
673
- return {
674
- ...result,
675
- documents: result.documents.map((doc) => ({
676
- ...doc,
677
- uri: decorateUriForIndex(doc.uri, this.indexName),
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
- void apiFetch<AppStatusResponse>("/api/status").then(({ data }) => {
278
- if (data) {
279
- setModelsNeeded(
280
- data.bootstrap.models.cachedCount < data.bootstrap.models.totalCount
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()}