@gmickel/gno 0.28.1 → 0.29.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 (52) hide show
  1. package/README.md +10 -2
  2. package/package.json +7 -7
  3. package/src/app/constants.ts +4 -2
  4. package/src/cli/commands/mcp/install.ts +4 -4
  5. package/src/cli/commands/mcp/status.ts +7 -7
  6. package/src/cli/commands/skill/install.ts +5 -5
  7. package/src/cli/program.ts +2 -2
  8. package/src/collection/add.ts +10 -0
  9. package/src/collection/types.ts +1 -0
  10. package/src/config/types.ts +12 -2
  11. package/src/core/depth-policy.ts +1 -1
  12. package/src/core/file-ops.ts +38 -0
  13. package/src/llm/registry.ts +20 -4
  14. package/src/serve/AGENTS.md +16 -16
  15. package/src/serve/CLAUDE.md +16 -16
  16. package/src/serve/config-sync.ts +32 -1
  17. package/src/serve/connectors.ts +243 -0
  18. package/src/serve/context.ts +9 -0
  19. package/src/serve/doc-events.ts +31 -1
  20. package/src/serve/embed-scheduler.ts +12 -0
  21. package/src/serve/import-preview.ts +173 -0
  22. package/src/serve/public/app.tsx +101 -7
  23. package/src/serve/public/components/AIModelSelector.tsx +383 -145
  24. package/src/serve/public/components/AddCollectionDialog.tsx +123 -7
  25. package/src/serve/public/components/BootstrapStatus.tsx +133 -0
  26. package/src/serve/public/components/CaptureModal.tsx +5 -2
  27. package/src/serve/public/components/CollectionsEmptyState.tsx +63 -0
  28. package/src/serve/public/components/FirstRunWizard.tsx +622 -0
  29. package/src/serve/public/components/HealthCenter.tsx +128 -0
  30. package/src/serve/public/components/IndexingProgress.tsx +21 -2
  31. package/src/serve/public/components/QuickSwitcher.tsx +62 -36
  32. package/src/serve/public/components/TagInput.tsx +5 -1
  33. package/src/serve/public/components/WikiLinkAutocomplete.tsx +15 -6
  34. package/src/serve/public/components/WorkspaceTabs.tsx +60 -0
  35. package/src/serve/public/hooks/use-doc-events.ts +48 -4
  36. package/src/serve/public/lib/local-history.ts +40 -7
  37. package/src/serve/public/lib/navigation-state.ts +156 -0
  38. package/src/serve/public/lib/workspace-tabs.ts +235 -0
  39. package/src/serve/public/pages/Ask.tsx +11 -1
  40. package/src/serve/public/pages/Browse.tsx +73 -0
  41. package/src/serve/public/pages/Collections.tsx +29 -13
  42. package/src/serve/public/pages/Connectors.tsx +178 -0
  43. package/src/serve/public/pages/Dashboard.tsx +493 -67
  44. package/src/serve/public/pages/DocView.tsx +192 -34
  45. package/src/serve/public/pages/DocumentEditor.tsx +127 -5
  46. package/src/serve/public/pages/Search.tsx +12 -1
  47. package/src/serve/routes/api.ts +532 -62
  48. package/src/serve/server.ts +79 -2
  49. package/src/serve/status-model.ts +149 -0
  50. package/src/serve/status.ts +706 -0
  51. package/src/serve/watch-service.ts +73 -8
  52. package/src/types/electrobun-shell.d.ts +43 -0
package/README.md CHANGED
@@ -34,7 +34,13 @@ GNO is a local knowledge engine that turns your documents into a searchable, con
34
34
 
35
35
  ---
36
36
 
37
- ## What's New in v0.24
37
+ ## What's New in v0.29
38
+
39
+ - **GNO Desktop Beta**: first mac-first desktop beta shell with deep-link routing, singleton handoff, and the same onboarding/search/edit flows as `gno serve`
40
+ - **Desktop Onboarding Polish**: guided setup now covers folders, presets, model readiness, indexing, connectors, import preview, app tabs, file actions, and recovery without drift between web and desktop
41
+ - **Default Preset Upgrade**: `slim-tuned` is now the built-in default, using the fine-tuned retrieval expansion model while keeping the same embed, rerank, and answer stack as `slim`
42
+
43
+ ### v0.24
38
44
 
39
45
  - **Structured Query Documents**: first-class multi-line query syntax using `term:`, `intent:`, and `hyde:`
40
46
  - **Cross-Surface Rollout**: works across CLI, API, MCP, SDK, and Web Search/Ask
@@ -65,7 +71,7 @@ models:
65
71
  embed: hf:gpustack/bge-m3-GGUF/bge-m3-Q4_K_M.gguf
66
72
  rerank: hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf
67
73
  expand: hf:guiltylemon/gno-expansion-slim-retrieval-v1/gno-expansion-auto-entity-lock-default-mix-lr95-f16.gguf
68
- gen: hf:unsloth/Qwen3-4B-Instruct-2507-GGUF/Qwen3-4B-Instruct-2507-Q4_K_M.gguf
74
+ gen: hf:unsloth/Qwen3-1.7B-GGUF/Qwen3-1.7B-Q4_K_M.gguf
69
75
  ```
70
76
 
71
77
  Then:
@@ -373,6 +379,8 @@ Open `http://localhost:3000` to:
373
379
  - **Edit**: Create, edit, and delete documents with live preview
374
380
  - **Ask**: AI-powered Q&A with citations
375
381
  - **Manage Collections**: Add, remove, and re-index collections
382
+ - **Connect agents**: Install core Skill/MCP integrations from the app
383
+ - **Manage files safely**: Rename, reveal, or move editable files to Trash with explicit index-vs-disk semantics
376
384
  - **Switch presets**: Change models live without restart
377
385
 
378
386
  ### Search
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -166,24 +166,24 @@
166
166
  },
167
167
  "devDependencies": {
168
168
  "@ai-sdk/openai": "^3.0.2",
169
- "@biomejs/biome": "2.3.13",
169
+ "@biomejs/biome": "2.3.14",
170
170
  "@tailwindcss/cli": "^4.1.18",
171
171
  "@types/bun": "latest",
172
- "@types/react": "^19.2.7",
172
+ "@types/react": "^19.2.13",
173
173
  "@types/react-dom": "^19.2.3",
174
174
  "ajv": "^8.17.1",
175
175
  "ajv-formats": "^3.0.1",
176
176
  "docx": "^9.5.1",
177
177
  "evalite": "^1.0.0-beta.15",
178
178
  "exceljs": "^4.4.0",
179
- "lefthook": "^2.0.13",
179
+ "lefthook": "^2.1.0",
180
180
  "oxfmt": "^0.28.0",
181
181
  "oxlint": "^1.42.0",
182
- "oxlint-tsgolint": "^0.11.4",
182
+ "oxlint-tsgolint": "^0.11.5",
183
183
  "pdf-lib": "^1.17.1",
184
- "playwright": "^1.52.0",
184
+ "playwright": "^1.58.2",
185
185
  "pptxgenjs": "^4.0.1",
186
- "ultracite": "7.1.3",
186
+ "ultracite": "7.1.5",
187
187
  "vitest": "^4.0.16"
188
188
  },
189
189
  "peerDependencies": {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { homedir, platform } from "node:os";
9
- import { join } from "node:path";
9
+ import { basename, join } from "node:path";
10
10
 
11
11
  // Bun supports JSON imports natively - version single source of truth
12
12
  import pkg from "../../package.json";
@@ -232,7 +232,9 @@ export function getConfigPath(dirs: ResolvedDirs = resolveDirs()): string {
232
232
  * Get path to models cache directory.
233
233
  */
234
234
  export function getModelsCachePath(dirs: ResolvedDirs = resolveDirs()): string {
235
- return join(dirs.cache, "models");
235
+ return basename(dirs.cache) === "models"
236
+ ? dirs.cache
237
+ : join(dirs.cache, "models");
236
238
  }
237
239
 
238
240
  // ─────────────────────────────────────────────────────────────────────────────
@@ -46,7 +46,7 @@ export interface InstallOptions {
46
46
  quiet?: boolean;
47
47
  }
48
48
 
49
- interface InstallResult {
49
+ export interface McpInstallResult {
50
50
  target: McpTarget;
51
51
  scope: McpScope;
52
52
  configPath: string;
@@ -61,7 +61,7 @@ interface InstallResult {
61
61
  /**
62
62
  * Install gno to a single target.
63
63
  */
64
- async function installToTarget(
64
+ export async function installMcpToTarget(
65
65
  target: McpTarget,
66
66
  scope: McpScope,
67
67
  serverEntry: StandardMcpEntry,
@@ -71,7 +71,7 @@ async function installToTarget(
71
71
  cwd?: string;
72
72
  homeDir?: string;
73
73
  }
74
- ): Promise<InstallResult> {
74
+ ): Promise<McpInstallResult> {
75
75
  const { force = false, dryRun = false, cwd, homeDir } = options;
76
76
 
77
77
  const { configPath, configFormat } = resolveMcpConfigPath({
@@ -165,7 +165,7 @@ export async function installMcp(opts: InstallOptions = {}): Promise<void> {
165
165
  const serverEntry = buildMcpServerEntry({ enableWrite });
166
166
 
167
167
  // Install
168
- const result = await installToTarget(target, scope, serverEntry, {
168
+ const result = await installMcpToTarget(target, scope, serverEntry, {
169
169
  force,
170
170
  dryRun,
171
171
  cwd: opts.cwd,
@@ -37,7 +37,7 @@ export interface StatusOptions {
37
37
  json?: boolean;
38
38
  }
39
39
 
40
- interface TargetStatus {
40
+ export interface McpTargetStatus {
41
41
  target: McpTarget;
42
42
  scope: McpScope;
43
43
  configPath: string;
@@ -47,7 +47,7 @@ interface TargetStatus {
47
47
  }
48
48
 
49
49
  interface StatusResult {
50
- targets: TargetStatus[];
50
+ targets: McpTargetStatus[];
51
51
  summary: {
52
52
  configured: number;
53
53
  total: number;
@@ -72,11 +72,11 @@ function normalizeEntry(
72
72
  return entry as StandardMcpEntry;
73
73
  }
74
74
 
75
- async function checkTargetStatus(
75
+ export async function checkMcpTargetStatus(
76
76
  target: McpTarget,
77
77
  scope: McpScope,
78
78
  options: { cwd?: string; homeDir?: string }
79
- ): Promise<TargetStatus> {
79
+ ): Promise<McpTargetStatus> {
80
80
  const { cwd, homeDir } = options;
81
81
 
82
82
  const { configPath, configFormat } = resolveMcpConfigPath({
@@ -151,7 +151,7 @@ export async function statusMcp(opts: StatusOptions = {}): Promise<void> {
151
151
  const targets: McpTarget[] =
152
152
  targetFilter === "all" ? MCP_TARGETS : [targetFilter];
153
153
 
154
- const results: TargetStatus[] = [];
154
+ const results: McpTargetStatus[] = [];
155
155
 
156
156
  for (const target of targets) {
157
157
  const supportsProject = TARGETS_WITH_PROJECT_SCOPE.includes(target);
@@ -163,7 +163,7 @@ export async function statusMcp(opts: StatusOptions = {}): Promise<void> {
163
163
 
164
164
  for (const scope of scopes) {
165
165
  results.push(
166
- await checkTargetStatus(target, scope, {
166
+ await checkMcpTargetStatus(target, scope, {
167
167
  cwd: opts.cwd,
168
168
  homeDir: opts.homeDir,
169
169
  })
@@ -172,7 +172,7 @@ export async function statusMcp(opts: StatusOptions = {}): Promise<void> {
172
172
  } else if (scopeFilter === "all" || scopeFilter === "user") {
173
173
  // User scope only - skip if filtering by project
174
174
  results.push(
175
- await checkTargetStatus(target, "user", {
175
+ await checkMcpTargetStatus(target, "user", {
176
176
  cwd: opts.cwd,
177
177
  homeDir: opts.homeDir,
178
178
  })
@@ -54,7 +54,7 @@ export interface InstallOptions {
54
54
  quiet?: boolean;
55
55
  }
56
56
 
57
- interface InstallResult {
57
+ export interface SkillInstallResult {
58
58
  target: SkillTarget;
59
59
  scope: SkillScope;
60
60
  path: string;
@@ -63,12 +63,12 @@ interface InstallResult {
63
63
  /**
64
64
  * Install skill to a single target.
65
65
  */
66
- async function installToTarget(
66
+ export async function installSkillToTarget(
67
67
  scope: SkillScope,
68
68
  target: SkillTarget,
69
69
  force: boolean,
70
70
  overrides?: { cwd?: string; homeDir?: string }
71
- ): Promise<InstallResult> {
71
+ ): Promise<SkillInstallResult> {
72
72
  const sourceDir = getSkillSourceDir();
73
73
  const paths = resolveSkillPaths({ scope, target, ...overrides });
74
74
 
@@ -174,10 +174,10 @@ export async function installSkill(opts: InstallOptions = {}): Promise<void> {
174
174
 
175
175
  const targets: SkillTarget[] = target === "all" ? SKILL_TARGETS : [target];
176
176
 
177
- const results: InstallResult[] = [];
177
+ const results: SkillInstallResult[] = [];
178
178
 
179
179
  for (const t of targets) {
180
- const result = await installToTarget(scope, t, force || yes, {
180
+ const result = await installSkillToTarget(scope, t, force || yes, {
181
181
  cwd: opts.cwd,
182
182
  homeDir: opts.homeDir,
183
183
  });
@@ -542,7 +542,7 @@ function wireSearchCommands(program: Command): void {
542
542
  const configResult = await loadConfig(globals.config);
543
543
  const activePresetId = configResult.ok
544
544
  ? getActivePreset(configResult.value).id
545
- : "slim";
545
+ : "slim-tuned";
546
546
  const candidateLimit = cmdOpts.candidateLimit
547
547
  ? parsePositiveInt("candidate-limit", cmdOpts.candidateLimit)
548
548
  : undefined;
@@ -655,7 +655,7 @@ function wireSearchCommands(program: Command): void {
655
655
  const configResult = await loadConfig(globals.config);
656
656
  const activePresetId = configResult.ok
657
657
  ? getActivePreset(configResult.value).id
658
- : "slim";
658
+ : "slim-tuned";
659
659
  const candidateLimit = cmdOpts.candidateLimit
660
660
  ? parsePositiveInt("candidate-limit", cmdOpts.candidateLimit)
661
661
  : undefined;
@@ -73,6 +73,16 @@ export async function addCollection(
73
73
  };
74
74
  }
75
75
 
76
+ // Check for duplicate path
77
+ const existingPath = config.collections.find((c) => c.path === absolutePath);
78
+ if (existingPath) {
79
+ return {
80
+ ok: false,
81
+ code: "DUPLICATE_PATH",
82
+ message: `Path is already indexed by collection "${existingPath.name}"`,
83
+ };
84
+ }
85
+
76
86
  // Parse include/exclude lists
77
87
  const includeList = parseList(input.include);
78
88
  const excludeList =
@@ -60,6 +60,7 @@ export interface CollectionError {
60
60
  | "VALIDATION"
61
61
  | "NOT_FOUND"
62
62
  | "DUPLICATE"
63
+ | "DUPLICATE_PATH"
63
64
  | "PATH_NOT_FOUND"
64
65
  | "HAS_REFERENCES";
65
66
  message: string;
@@ -176,9 +176,19 @@ export type ModelPreset = z.infer<typeof ModelPresetSchema>;
176
176
 
177
177
  /** Default model presets */
178
178
  export const DEFAULT_MODEL_PRESETS: ModelPreset[] = [
179
+ {
180
+ id: "slim-tuned",
181
+ name: "GNO Slim Tuned (Default, ~1GB)",
182
+ embed: "hf:gpustack/bge-m3-GGUF/bge-m3-Q4_K_M.gguf",
183
+ rerank:
184
+ "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",
185
+ expand:
186
+ "hf:guiltylemon/gno-expansion-slim-retrieval-v1/gno-expansion-auto-entity-lock-default-mix-lr95-f16.gguf",
187
+ gen: "hf:unsloth/Qwen3-1.7B-GGUF/Qwen3-1.7B-Q4_K_M.gguf",
188
+ },
179
189
  {
180
190
  id: "slim",
181
- name: "Slim (Default, ~1GB)",
191
+ name: "Slim (~1GB)",
182
192
  embed: "hf:gpustack/bge-m3-GGUF/bge-m3-Q4_K_M.gguf",
183
193
  rerank:
184
194
  "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf",
@@ -209,7 +219,7 @@ export const DEFAULT_MODEL_PRESETS: ModelPreset[] = [
209
219
 
210
220
  export const ModelConfigSchema = z.object({
211
221
  /** Active preset ID */
212
- activePreset: z.string().default("slim"),
222
+ activePreset: z.string().default("slim-tuned"),
213
223
  /** Model presets */
214
224
  presets: z.array(ModelPresetSchema).default(DEFAULT_MODEL_PRESETS),
215
225
  /** Model load timeout in ms */
@@ -21,7 +21,7 @@ export interface ResolvedDepthPolicy {
21
21
  export const DEFAULT_THOROUGH_CANDIDATE_LIMIT = 40;
22
22
 
23
23
  function normalizePresetId(presetId?: string): string {
24
- return presetId?.trim().toLowerCase() || "slim";
24
+ return presetId?.trim().toLowerCase() || "slim-tuned";
25
25
  }
26
26
 
27
27
  export function balancedUsesExpansion(presetId?: string): boolean {
@@ -6,6 +6,10 @@
6
6
 
7
7
  // node:fs/promises for rename/unlink (no Bun equivalent for structure ops)
8
8
  import { rename, unlink } from "node:fs/promises";
9
+ // node:os platform: no Bun equivalent
10
+ import { platform } from "node:os";
11
+ // node:path dirname: no Bun equivalent
12
+ import { dirname } from "node:path";
9
13
 
10
14
  export async function atomicWrite(
11
15
  path: string,
@@ -22,3 +26,37 @@ export async function atomicWrite(
22
26
  throw e;
23
27
  }
24
28
  }
29
+
30
+ async function runCommand(cmd: string[]): Promise<void> {
31
+ const proc = Bun.spawn({
32
+ cmd,
33
+ stdout: "pipe",
34
+ stderr: "pipe",
35
+ });
36
+ const exitCode = await proc.exited;
37
+ if (exitCode === 0) {
38
+ return;
39
+ }
40
+ const stderr = await new Response(proc.stderr).text();
41
+ throw new Error(stderr.trim() || `Command failed: ${cmd.join(" ")}`);
42
+ }
43
+
44
+ export async function renameFilePath(
45
+ currentPath: string,
46
+ nextPath: string
47
+ ): Promise<void> {
48
+ await rename(currentPath, nextPath);
49
+ }
50
+
51
+ export async function trashFilePath(path: string): Promise<void> {
52
+ await runCommand(["trash", path]);
53
+ }
54
+
55
+ export async function revealFilePath(path: string): Promise<void> {
56
+ if (platform() === "darwin") {
57
+ await runCommand(["open", "-R", path]);
58
+ return;
59
+ }
60
+
61
+ await runCommand(["xdg-open", dirname(path)]);
62
+ }
@@ -18,11 +18,27 @@ import { DEFAULT_MODEL_PRESETS } from "../config/types";
18
18
  * Get model config with defaults.
19
19
  */
20
20
  export function getModelConfig(config: Config): ModelConfig {
21
+ const customPresets = config.models?.presets ?? [];
22
+ const presetsById = new Map(
23
+ DEFAULT_MODEL_PRESETS.map((preset) => [preset.id, preset] as const)
24
+ );
25
+
26
+ for (const preset of customPresets) {
27
+ presetsById.set(preset.id, preset);
28
+ }
29
+
30
+ const mergedPresets = [
31
+ ...DEFAULT_MODEL_PRESETS.map(
32
+ (preset) => presetsById.get(preset.id) ?? preset
33
+ ),
34
+ ...customPresets.filter(
35
+ (preset) => !DEFAULT_MODEL_PRESETS.some((base) => base.id === preset.id)
36
+ ),
37
+ ];
38
+
21
39
  return {
22
- activePreset: config.models?.activePreset ?? "slim",
23
- presets: config.models?.presets?.length
24
- ? config.models.presets
25
- : DEFAULT_MODEL_PRESETS,
40
+ activePreset: config.models?.activePreset ?? "slim-tuned",
41
+ presets: mergedPresets,
26
42
  loadTimeout: config.models?.loadTimeout ?? 60_000,
27
43
  inferenceTimeout: config.models?.inferenceTimeout ?? 30_000,
28
44
  expandContextSize: config.models?.expandContextSize ?? 2_048,
@@ -65,22 +65,22 @@ Answer generation uses shared module to stay in sync with CLI:
65
65
 
66
66
  ## API Endpoints
67
67
 
68
- | Endpoint | Method | Description |
69
- | -------------------- | ------ | -------------------------- |
70
- | `/api/health` | GET | Health check |
71
- | `/api/status` | GET | Index stats, collections |
72
- | `/api/capabilities` | GET | Available features |
73
- | `/api/collections` | GET | List collections |
74
- | `/api/docs` | GET | List documents |
75
- | `/api/doc` | GET | Get document content |
76
- | `/api/search` | POST | BM25 search |
77
- | `/api/query` | POST | Hybrid search |
78
- | `/api/ask` | POST | AI answer with citations |
79
- | `/api/presets` | GET | List model presets |
80
- | `/api/presets` | POST | Switch preset (hot-reload) |
81
- | `/api/models/status` | GET | Download progress |
82
- | `/api/models/pull` | POST | Start model download |
83
- | `/api/tags` | GET | List tags (with counts) |
68
+ | Endpoint | Method | Description |
69
+ | -------------------- | ------ | ------------------------------------------ |
70
+ | `/api/health` | GET | Health check |
71
+ | `/api/status` | GET | Index stats, onboarding, health, bootstrap |
72
+ | `/api/capabilities` | GET | Available features |
73
+ | `/api/collections` | GET | List collections |
74
+ | `/api/docs` | GET | List documents |
75
+ | `/api/doc` | GET | Get document content |
76
+ | `/api/search` | POST | BM25 search |
77
+ | `/api/query` | POST | Hybrid search |
78
+ | `/api/ask` | POST | AI answer with citations |
79
+ | `/api/presets` | GET | List model presets |
80
+ | `/api/presets` | POST | Switch preset (hot-reload) |
81
+ | `/api/models/status` | GET | Download progress |
82
+ | `/api/models/pull` | POST | Start model download |
83
+ | `/api/tags` | GET | List tags (with counts) |
84
84
 
85
85
  ## Frontend
86
86
 
@@ -65,22 +65,22 @@ Answer generation uses shared module to stay in sync with CLI:
65
65
 
66
66
  ## API Endpoints
67
67
 
68
- | Endpoint | Method | Description |
69
- | -------------------- | ------ | -------------------------- |
70
- | `/api/health` | GET | Health check |
71
- | `/api/status` | GET | Index stats, collections |
72
- | `/api/capabilities` | GET | Available features |
73
- | `/api/collections` | GET | List collections |
74
- | `/api/docs` | GET | List documents |
75
- | `/api/doc` | GET | Get document content |
76
- | `/api/search` | POST | BM25 search |
77
- | `/api/query` | POST | Hybrid search |
78
- | `/api/ask` | POST | AI answer with citations |
79
- | `/api/presets` | GET | List model presets |
80
- | `/api/presets` | POST | Switch preset (hot-reload) |
81
- | `/api/models/status` | GET | Download progress |
82
- | `/api/models/pull` | POST | Start model download |
83
- | `/api/tags` | GET | List tags (with counts) |
68
+ | Endpoint | Method | Description |
69
+ | -------------------- | ------ | ------------------------------------------ |
70
+ | `/api/health` | GET | Health check |
71
+ | `/api/status` | GET | Index stats, onboarding, health, bootstrap |
72
+ | `/api/capabilities` | GET | Available features |
73
+ | `/api/collections` | GET | List collections |
74
+ | `/api/docs` | GET | List documents |
75
+ | `/api/doc` | GET | Get document content |
76
+ | `/api/search` | POST | BM25 search |
77
+ | `/api/query` | POST | Hybrid search |
78
+ | `/api/ask` | POST | AI answer with citations |
79
+ | `/api/presets` | GET | List model presets |
80
+ | `/api/presets` | POST | Switch preset (hot-reload) |
81
+ | `/api/models/status` | GET | Download progress |
82
+ | `/api/models/pull` | POST | Start model download |
83
+ | `/api/tags` | GET | List tags (with counts) |
84
84
 
85
85
  ## Frontend
86
86
 
@@ -40,7 +40,7 @@ export async function applyConfigChange(
40
40
  mutate: (config: Config) => Promise<MutationResult> | MutationResult,
41
41
  configPath?: string
42
42
  ): Promise<ApplyConfigResult> {
43
- return applyConfigChangeCore(
43
+ const result = await applyConfigChangeCore(
44
44
  {
45
45
  store,
46
46
  configPath,
@@ -51,4 +51,35 @@ export async function applyConfigChange(
51
51
  },
52
52
  mutate
53
53
  );
54
+
55
+ if (result.ok) {
56
+ ctxHolder.watchService?.updateCollections(ctxHolder.config.collections);
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ export async function applyConfigChangeTyped<T>(
63
+ ctxHolder: ContextHolder,
64
+ store: SqliteAdapter,
65
+ mutate: (config: Config) => Promise<MutationResult<T>> | MutationResult<T>,
66
+ configPath?: string
67
+ ): Promise<ApplyConfigResult<T>> {
68
+ const result = await applyConfigChangeCore(
69
+ {
70
+ store,
71
+ configPath,
72
+ onConfigUpdated: (config) => {
73
+ ctxHolder.config = config;
74
+ ctxHolder.current = { ...ctxHolder.current, config };
75
+ },
76
+ },
77
+ mutate
78
+ );
79
+
80
+ if (result.ok) {
81
+ ctxHolder.watchService?.updateCollections(ctxHolder.config.collections);
82
+ }
83
+
84
+ return result;
54
85
  }