@gmickel/gno 1.12.3 → 1.13.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 (69) hide show
  1. package/README.md +57 -30
  2. package/assets/skill/SKILL.md +6 -1
  3. package/assets/skill/cli-reference.md +16 -6
  4. package/assets/skill/mcp-reference.md +22 -3
  5. package/package.json +2 -1
  6. package/src/app/constants.ts +43 -10
  7. package/src/app/index-name.ts +127 -0
  8. package/src/cli/commands/doctor-activation.ts +151 -0
  9. package/src/cli/commands/doctor.ts +41 -16
  10. package/src/cli/commands/get.ts +18 -0
  11. package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
  12. package/src/cli/commands/mcp/config-discovery.ts +42 -0
  13. package/src/cli/commands/mcp/config-editors.ts +432 -0
  14. package/src/cli/commands/mcp/config.ts +63 -160
  15. package/src/cli/commands/mcp/install.ts +75 -37
  16. package/src/cli/commands/mcp/paths.ts +141 -136
  17. package/src/cli/commands/mcp/server-entry.ts +66 -0
  18. package/src/cli/commands/mcp/status.ts +189 -57
  19. package/src/cli/commands/mcp/target-display.ts +30 -0
  20. package/src/cli/commands/mcp/uninstall.ts +29 -31
  21. package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
  22. package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
  23. package/src/cli/commands/multi-get.ts +31 -6
  24. package/src/cli/commands/status.ts +107 -11
  25. package/src/cli/program.ts +66 -20
  26. package/src/core/activation-connector-health.ts +19 -0
  27. package/src/core/activation-probe-plan.ts +321 -0
  28. package/src/core/activation-probe.ts +138 -0
  29. package/src/core/activation-receipt-store.ts +39 -0
  30. package/src/core/activation-status.ts +513 -0
  31. package/src/core/activation-verifier.ts +416 -0
  32. package/src/core/connector-environment.ts +68 -0
  33. package/src/core/connector-policy.ts +233 -0
  34. package/src/core/connector-verification-target.ts +150 -0
  35. package/src/core/connector-verifier.ts +497 -0
  36. package/src/core/context-resolver.ts +285 -0
  37. package/src/core/indexed-reference.ts +33 -8
  38. package/src/core/runtime-entrypoint.ts +24 -0
  39. package/src/mcp/activation-verification-mode.ts +4 -0
  40. package/src/mcp/server.ts +9 -2
  41. package/src/mcp/tools/index.ts +3 -3
  42. package/src/pipeline/answer-prompt.ts +80 -0
  43. package/src/pipeline/answer.ts +12 -26
  44. package/src/pipeline/hybrid.ts +2 -0
  45. package/src/pipeline/result-context.ts +51 -0
  46. package/src/pipeline/search.ts +5 -1
  47. package/src/pipeline/vsearch.ts +2 -0
  48. package/src/sdk/client.ts +7 -0
  49. package/src/sdk/types.ts +1 -0
  50. package/src/serve/activation-health.ts +91 -0
  51. package/src/serve/background-runtime.ts +11 -1
  52. package/src/serve/connectors.ts +164 -19
  53. package/src/serve/public/components/BootstrapStatus.tsx +94 -1
  54. package/src/serve/public/components/FirstRunWizard.tsx +13 -51
  55. package/src/serve/public/components/HealthCenter.tsx +8 -2
  56. package/src/serve/public/globals.built.css +1 -1
  57. package/src/serve/public/pages/Connectors.tsx +216 -55
  58. package/src/serve/public/pages/Dashboard.tsx +1 -0
  59. package/src/serve/routes/api.ts +152 -8
  60. package/src/serve/server.ts +44 -9
  61. package/src/serve/status-model.ts +4 -0
  62. package/src/serve/status.ts +79 -35
  63. package/src/store/activation-receipts.ts +390 -0
  64. package/src/store/index.ts +8 -0
  65. package/src/store/migrations/012-activation-receipts.ts +38 -0
  66. package/src/store/migrations/013-fts-sync-marker.ts +39 -0
  67. package/src/store/migrations/index.ts +4 -0
  68. package/src/store/sqlite/adapter.ts +320 -53
  69. package/src/store/types.ts +124 -0
@@ -18,6 +18,7 @@ import { formatQueryForEmbedding } from "./contextual";
18
18
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
19
19
  import { selectBestChunkForSteering } from "./intent";
20
20
  import { detectQueryLanguage } from "./query-language";
21
+ import { attachSearchResultContexts } from "./result-context";
21
22
  import {
22
23
  resolveRecencyTimestamp,
23
24
  isWithinTemporalRange,
@@ -333,6 +334,7 @@ export async function searchVectorWithEmbedding(
333
334
  }
334
335
 
335
336
  const finalResults = results.slice(0, limit);
337
+ await attachSearchResultContexts(store, finalResults);
336
338
 
337
339
  return ok({
338
340
  results: finalResults,
package/src/sdk/client.ts CHANGED
@@ -40,6 +40,7 @@ import type {
40
40
  } from "./types";
41
41
 
42
42
  import { decorateUriForIndex, getIndexDbPath } from "../app/constants";
43
+ import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
43
44
  import {
44
45
  ConfigSchema,
45
46
  loadConfig,
@@ -1208,6 +1209,12 @@ class GnoClientImpl implements GnoClient {
1208
1209
  export async function createGnoClient(
1209
1210
  options: GnoClientInitOptions = {}
1210
1211
  ): Promise<GnoClient> {
1212
+ if (options.indexName !== undefined && !isValidIndexName(options.indexName)) {
1213
+ throw sdkError(
1214
+ "VALIDATION",
1215
+ `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`
1216
+ );
1217
+ }
1211
1218
  const state = await resolveClientState(options);
1212
1219
  return new GnoClientImpl(state);
1213
1220
  }
package/src/sdk/types.ts CHANGED
@@ -46,6 +46,7 @@ export interface GnoClientInitOptions {
46
46
  config?: Config;
47
47
  configPath?: string;
48
48
  dbPath?: string;
49
+ /** Filesystem-safe index name: 1-64 Unicode letters/marks/numbers plus ` ._-`. */
49
50
  indexName?: string;
50
51
  cacheDir?: string;
51
52
  downloadPolicy?: DownloadPolicy;
@@ -0,0 +1,91 @@
1
+ /** UI-neutral health checks derived from the shared activation contract. */
2
+
3
+ import type { ActivationStatus } from "../core/activation-status";
4
+ import type { HealthCheck } from "./status-model";
5
+
6
+ function countLabel(count: number, singular: string): string {
7
+ return `${count} ${count === 1 ? singular : `${singular}s`}`;
8
+ }
9
+
10
+ export function buildActivationCheck(
11
+ activation: ActivationStatus
12
+ ): HealthCheck {
13
+ if (activation.healthy) {
14
+ const semanticReasons = [
15
+ ...new Set(
16
+ activation.collections.map(
17
+ ({ semanticAvailability }) => semanticAvailability.code
18
+ )
19
+ ),
20
+ ];
21
+ return {
22
+ id: "retrieval-activation",
23
+ title: "Retrieval proof",
24
+ status: "ok",
25
+ summary: `${countLabel(activation.collections.length, "folder")} passed lexical retrieval`,
26
+ detail: `Lexical search is proven. Semantic availability is separate (${semanticReasons.join(", ")}).`,
27
+ actionLabel: "Run update",
28
+ actionKind: "sync",
29
+ };
30
+ }
31
+
32
+ const failed = activation.collections.filter(({ ready }) => !ready);
33
+ const first = failed[0];
34
+ const detail = first?.remediation
35
+ ? `${first.collection}: ${first.remediation.stage}/${first.remediation.code}. Run: ${first.remediation.command}`
36
+ : "Add and index a supported text collection, then check retrieval again.";
37
+ return {
38
+ id: "retrieval-activation",
39
+ title: "Retrieval proof",
40
+ status: activation.usable ? "warn" : "error",
41
+ summary: activation.usable
42
+ ? `${countLabel(failed.length, "folder")} failed lexical retrieval`
43
+ : "No folder passed lexical retrieval",
44
+ detail,
45
+ actionLabel: "Run update",
46
+ actionKind: "sync",
47
+ };
48
+ }
49
+
50
+ export function buildConnectorActivationCheck(
51
+ activation: ActivationStatus
52
+ ): HealthCheck | null {
53
+ const { projected, total, truncated } = activation.connectorProjection;
54
+ const omitted = total - projected;
55
+ const observed = activation.connectors.filter(
56
+ ({ code }) =>
57
+ code !== "connector_not_configured" &&
58
+ code !== "target_runtime_unverifiable"
59
+ );
60
+ if (observed.length === 0 && !truncated) {
61
+ return null;
62
+ }
63
+ const failed = observed.filter(({ status }) => status === "failed");
64
+ const incomplete = observed.filter(({ status }) => status !== "passed");
65
+ const first = failed[0] ?? incomplete[0] ?? observed[0];
66
+ const firstDetail = first
67
+ ? `${first.target} / ${first.collection}: ${first.status}${first.code ? `/${first.code}` : ""}${first.remediation ? `. ${first.remediation}` : ""}`
68
+ : null;
69
+ const projectionDetail = truncated
70
+ ? `${omitted} target/collection checks were omitted by the bounded status projection; no result is claimed for them.`
71
+ : null;
72
+ return {
73
+ id: "connector-activation",
74
+ title: "Connector proof",
75
+ status:
76
+ failed.length > 0
77
+ ? "error"
78
+ : incomplete.length > 0 || truncated
79
+ ? "warn"
80
+ : "ok",
81
+ summary:
82
+ failed.length > 0
83
+ ? `${countLabel(failed.length, "connector proof")} failed`
84
+ : incomplete.length > 0
85
+ ? `${countLabel(incomplete.length, "connector proof")} incomplete`
86
+ : truncated
87
+ ? `${projected} of ${total} connector target/collection checks projected`
88
+ : `${countLabel(observed.length, "connector proof")} passed`,
89
+ detail: [projectionDetail, firstDetail].filter(Boolean).join(" "),
90
+ };
91
+ }
@@ -1,3 +1,6 @@
1
+ // node:path resolve has no Bun equivalent for canonical process-relative paths.
2
+ import { resolve } from "node:path";
3
+
1
4
  import type { Config } from "../config/types";
2
5
  import type { SyncResult } from "../ingestion";
3
6
  import type { DocumentEventBus } from "./doc-events";
@@ -9,6 +12,7 @@ import type {
9
12
  } from "./watch-service";
10
13
 
11
14
  import { getIndexDbPath } from "../app/constants";
15
+ import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
12
16
  import {
13
17
  ensureDirectories,
14
18
  formatConfigWarnings,
@@ -89,6 +93,12 @@ export async function startBackgroundRuntime(
89
93
  options: BackgroundRuntimeOptions = {},
90
94
  deps: BackgroundRuntimeDeps = {}
91
95
  ): Promise<BackgroundRuntimeResult> {
96
+ if (options.index !== undefined && !isValidIndexName(options.index)) {
97
+ return {
98
+ success: false,
99
+ error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`,
100
+ };
101
+ }
92
102
  const syncAllService = deps.syncAllService
93
103
  ? (...args: Parameters<typeof defaultSyncService.syncAll>) =>
94
104
  deps.syncAllService!(...args)
@@ -123,7 +133,7 @@ export async function startBackgroundRuntime(
123
133
  const store = deps.storeFactory ? deps.storeFactory() : new SqliteAdapter();
124
134
  const dbPath = getIndexDbPath(options.index);
125
135
  const paths = (deps.getConfigPaths ?? getConfigPaths)();
126
- const actualConfigPath = options.configPath ?? paths.configFile;
136
+ const actualConfigPath = resolve(options.configPath ?? paths.configFile);
127
137
  store.setConfigPath(actualConfigPath);
128
138
 
129
139
  const openResult = await store.open(dbPath, config.ftsTokenizer);
@@ -1,14 +1,27 @@
1
1
  import type { McpScope, McpTarget } from "../cli/commands/mcp/paths";
2
2
  import type { SkillScope, SkillTarget } from "../cli/commands/skill/paths";
3
+ import type {
4
+ ConnectorVerifierOptions,
5
+ ConnectorVerificationTarget,
6
+ } from "../core/connector-verifier";
7
+ import type {
8
+ ActivationVerificationReceipt,
9
+ StorePort,
10
+ StoreResult,
11
+ } from "../store/types";
3
12
 
4
13
  import { installMcpToTarget } from "../cli/commands/mcp/install";
5
14
  import {
6
15
  buildMcpServerEntry,
7
16
  getTargetDisplayName,
8
17
  } from "../cli/commands/mcp/paths";
9
- import { checkMcpTargetStatus } from "../cli/commands/mcp/status";
18
+ import {
19
+ checkMcpTargetStatus,
20
+ toMcpConnectorVerificationTarget,
21
+ } from "../cli/commands/mcp/status";
10
22
  import { installSkillToTarget } from "../cli/commands/skill/install";
11
23
  import { resolveSkillPaths } from "../cli/commands/skill/paths";
24
+ import { verifyConnectorActivation } from "../core/connector-verifier";
12
25
 
13
26
  export interface ConnectorStatus {
14
27
  id: string;
@@ -47,6 +60,75 @@ interface McpConnectorDefinition {
47
60
 
48
61
  type ConnectorDefinition = SkillConnectorDefinition | McpConnectorDefinition;
49
62
 
63
+ interface SkillInspection {
64
+ installed: boolean;
65
+ path: string;
66
+ unavailable: boolean;
67
+ }
68
+
69
+ interface ConnectorInstallContext {
70
+ cwd?: string;
71
+ homeDir?: string;
72
+ indexName?: string;
73
+ configPath?: string;
74
+ }
75
+
76
+ const SKILL_PATH_UNAVAILABLE_ERROR =
77
+ "Skill path configuration is invalid or unavailable.";
78
+
79
+ function unresolvedSkillPath(target: SkillTarget): string {
80
+ return `unresolved-skill-path/${target}`;
81
+ }
82
+
83
+ async function inspectSkillConnector(
84
+ definition: SkillConnectorDefinition,
85
+ overrides?: { cwd?: string; homeDir?: string }
86
+ ): Promise<SkillInspection> {
87
+ try {
88
+ const paths = resolveSkillPaths({
89
+ scope: definition.scope,
90
+ target: definition.target,
91
+ ...overrides,
92
+ });
93
+ return {
94
+ installed: await Bun.file(`${paths.gnoDir}/SKILL.md`).exists(),
95
+ path: paths.gnoDir,
96
+ unavailable: false,
97
+ };
98
+ } catch {
99
+ return {
100
+ installed: false,
101
+ path: unresolvedSkillPath(definition.target),
102
+ unavailable: true,
103
+ };
104
+ }
105
+ }
106
+
107
+ function toSkillVerificationTarget(
108
+ definition: SkillConnectorDefinition,
109
+ inspection: SkillInspection
110
+ ): ConnectorVerificationTarget {
111
+ if (inspection.unavailable) {
112
+ return {
113
+ kind: "skill",
114
+ id: definition.id,
115
+ target: definition.target,
116
+ scope: definition.scope,
117
+ configPath: inspection.path,
118
+ installed: false,
119
+ configError: true,
120
+ };
121
+ }
122
+ return {
123
+ kind: "skill",
124
+ id: definition.id,
125
+ target: definition.target,
126
+ scope: definition.scope,
127
+ configPath: inspection.path,
128
+ installed: inspection.installed,
129
+ };
130
+ }
131
+
50
132
  const CONNECTOR_DEFINITIONS: ConnectorDefinition[] = [
51
133
  {
52
134
  id: "claude-code-skill",
@@ -139,28 +221,29 @@ export async function getConnectorStatuses(overrides?: {
139
221
  const statuses = await Promise.all(
140
222
  CONNECTOR_DEFINITIONS.map(async (definition) => {
141
223
  if (definition.installKind === "skill") {
142
- const paths = resolveSkillPaths({
143
- scope: definition.scope,
144
- target: definition.target,
145
- ...overrides,
146
- });
147
- const skillMdPath = `${paths.gnoDir}/SKILL.md`;
148
- const installed = await Bun.file(skillMdPath).exists();
224
+ const inspection = await inspectSkillConnector(definition, overrides);
149
225
  return {
150
226
  id: definition.id,
151
227
  appName: definition.appName,
152
228
  installKind: definition.installKind,
153
229
  target: definition.target,
154
230
  scope: definition.scope,
155
- installed,
156
- path: paths.gnoDir,
157
- summary: installed
158
- ? `${definition.appName} skill is installed.`
159
- : `${definition.appName} skill is not installed yet.`,
160
- nextAction: installed
161
- ? "Restart the agent to reload the skill."
162
- : "Install the skill from the app.",
231
+ installed: inspection.installed,
232
+ path: inspection.path,
233
+ summary: inspection.unavailable
234
+ ? `${definition.appName} skill path is unavailable.`
235
+ : inspection.installed
236
+ ? `${definition.appName} skill is installed.`
237
+ : `${definition.appName} skill is not installed yet.`,
238
+ nextAction: inspection.unavailable
239
+ ? "Fix the skill path configuration, then reload status."
240
+ : inspection.installed
241
+ ? "Restart the agent to reload the skill."
242
+ : "Install the skill from the app.",
163
243
  mode: definition.mode,
244
+ ...(inspection.unavailable
245
+ ? { error: SKILL_PATH_UNAVAILABLE_ERROR }
246
+ : {}),
164
247
  } satisfies ConnectorStatus;
165
248
  }
166
249
 
@@ -192,12 +275,34 @@ export async function getConnectorStatuses(overrides?: {
192
275
  return statuses;
193
276
  }
194
277
 
278
+ /** Inspect current connector configs without starting any connector runtime. */
279
+ export async function getConnectorVerificationTargets(overrides?: {
280
+ cwd?: string;
281
+ homeDir?: string;
282
+ }): Promise<ConnectorVerificationTarget[]> {
283
+ return Promise.all(
284
+ CONNECTOR_DEFINITIONS.map(async (definition) => {
285
+ if (definition.installKind === "skill") {
286
+ const inspection = await inspectSkillConnector(definition, overrides);
287
+ return toSkillVerificationTarget(definition, inspection);
288
+ }
289
+
290
+ const status = await checkMcpTargetStatus(
291
+ definition.target,
292
+ definition.scope,
293
+ overrides ?? {}
294
+ );
295
+ return toMcpConnectorVerificationTarget(definition.id, status);
296
+ })
297
+ );
298
+ }
299
+
195
300
  export async function installConnector(
196
301
  id: string,
197
302
  options?: {
198
303
  reinstall?: boolean;
199
304
  },
200
- overrides?: { cwd?: string; homeDir?: string }
305
+ overrides?: ConnectorInstallContext
201
306
  ): Promise<ConnectorStatus> {
202
307
  const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id);
203
308
  if (!definition) {
@@ -220,14 +325,21 @@ export async function installConnector(
220
325
  overrides
221
326
  );
222
327
  } else {
328
+ const targetOverrides = overrides
329
+ ? { cwd: overrides.cwd, homeDir: overrides.homeDir }
330
+ : undefined;
223
331
  await installMcpToTarget(
224
332
  definition.target,
225
333
  definition.scope,
226
- buildMcpServerEntry({ enableWrite: false }),
334
+ buildMcpServerEntry({
335
+ enableWrite: false,
336
+ indexName: overrides?.indexName,
337
+ configPath: overrides?.configPath,
338
+ }),
227
339
  {
228
340
  force: options?.reinstall ?? false,
229
341
  dryRun: false,
230
- ...overrides,
342
+ ...targetOverrides,
231
343
  }
232
344
  );
233
345
  }
@@ -253,3 +365,36 @@ export function getConnectorDisplayName(id: string): string {
253
365
 
254
366
  return definition.appName;
255
367
  }
368
+
369
+ /**
370
+ * Resolve and verify one connector without editing its client configuration.
371
+ * Kept separate from passive status listing because this starts a local MCP
372
+ * child and performs a real, collection-scoped retrieval smoke.
373
+ */
374
+ export async function verifyInstalledConnector(
375
+ id: string,
376
+ store: StorePort,
377
+ collection: string,
378
+ options?: ConnectorVerifierOptions,
379
+ overrides?: { cwd?: string; homeDir?: string }
380
+ ): Promise<StoreResult<ActivationVerificationReceipt>> {
381
+ const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id);
382
+ if (!definition) {
383
+ throw new Error(`Unknown connector: ${id}`);
384
+ }
385
+
386
+ let target: ConnectorVerificationTarget;
387
+ if (definition.installKind === "skill") {
388
+ const inspection = await inspectSkillConnector(definition, overrides);
389
+ target = toSkillVerificationTarget(definition, inspection);
390
+ } else {
391
+ const status = await checkMcpTargetStatus(
392
+ definition.target,
393
+ definition.scope,
394
+ overrides ?? {}
395
+ );
396
+ target = toMcpConnectorVerificationTarget(definition.id, status);
397
+ }
398
+
399
+ return verifyConnectorActivation(store, collection, target, options);
400
+ }
@@ -1,4 +1,6 @@
1
1
  import {
2
+ AlertCircleIcon,
3
+ CheckCircle2Icon,
2
4
  DownloadIcon,
3
5
  HardDriveIcon,
4
6
  PackageIcon,
@@ -7,6 +9,7 @@ import {
7
9
 
8
10
  import type { AppStatusResponse } from "../../status-model";
9
11
 
12
+ import { buildConnectorActivationCheck } from "../../activation-health";
10
13
  import { Button } from "./ui/button";
11
14
  import {
12
15
  Card,
@@ -18,6 +21,7 @@ import {
18
21
 
19
22
  interface BootstrapStatusProps {
20
23
  bootstrap: AppStatusResponse["bootstrap"];
24
+ activation: AppStatusResponse["activation"];
21
25
  onDownloadModels: () => void;
22
26
  }
23
27
 
@@ -37,11 +41,21 @@ function formatRole(
37
41
  }
38
42
 
39
43
  export function BootstrapStatus({
44
+ activation,
40
45
  bootstrap,
41
46
  onDownloadModels,
42
47
  }: BootstrapStatusProps) {
43
48
  const missingModels =
44
49
  bootstrap.models.totalCount - bootstrap.models.cachedCount;
50
+ const displayedConnectorCount = Math.min(activation.connectors.length, 8);
51
+ const hiddenProjectedConnectorCount =
52
+ activation.connectorProjection.projected - displayedConnectorCount;
53
+ const omittedConnectorCount =
54
+ activation.connectorProjection.total -
55
+ activation.connectorProjection.projected;
56
+ const connectorHealth = buildConnectorActivationCheck(activation);
57
+ const connectorsHealthy =
58
+ connectorHealth === null || connectorHealth.status === "ok";
45
59
 
46
60
  return (
47
61
  <section className="space-y-4">
@@ -64,7 +78,7 @@ export function BootstrapStatus({
64
78
  )}
65
79
  </div>
66
80
 
67
- <div className="grid gap-4 xl:grid-cols-3">
81
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
68
82
  <Card className="border-border/60 bg-card/70">
69
83
  <CardHeader className="pb-3">
70
84
  <div className="flex items-center gap-2">
@@ -127,6 +141,85 @@ export function BootstrapStatus({
127
141
  </div>
128
142
  </CardContent>
129
143
  </Card>
144
+
145
+ <Card className="border-border/60 bg-card/70">
146
+ <CardHeader className="pb-3">
147
+ <div className="flex items-center gap-2">
148
+ {activation.healthy && connectorsHealthy ? (
149
+ <CheckCircle2Icon className="size-4 text-emerald-500" />
150
+ ) : activation.usable ? (
151
+ <ServerCogIcon className="size-4 text-amber-500" />
152
+ ) : (
153
+ <AlertCircleIcon className="size-4 text-destructive" />
154
+ )}
155
+ <CardTitle className="text-base">Retrieval proof</CardTitle>
156
+ </div>
157
+ <CardDescription>
158
+ {activation.healthy
159
+ ? "Lexical retrieval proven"
160
+ : activation.usable
161
+ ? `Search usable in ${activation.collections.filter(({ ready }) => ready).length}/${activation.collections.length} folders`
162
+ : "Retrieval proof failed"}
163
+ </CardDescription>
164
+ </CardHeader>
165
+ <CardContent className="space-y-3 text-sm">
166
+ {activation.collections.length === 0 ? (
167
+ <p className="text-muted-foreground">
168
+ Add and index a text folder to prove retrieval.
169
+ </p>
170
+ ) : (
171
+ activation.collections.map((collection) => (
172
+ <div
173
+ className="rounded-lg border border-border/50 px-3 py-2"
174
+ key={collection.collection}
175
+ >
176
+ <p className="font-medium">{collection.collection}</p>
177
+ <p className="text-muted-foreground text-xs">
178
+ {collection.ready
179
+ ? `Lexical passed; semantic ${collection.semanticAvailability.code}`
180
+ : `${collection.remediation?.stage ?? "index"}/${collection.remediation?.code ?? "index_query_failed"}`}
181
+ </p>
182
+ {collection.remediation && (
183
+ <p className="mt-1 font-mono text-muted-foreground text-xs">
184
+ {collection.remediation.command}
185
+ </p>
186
+ )}
187
+ </div>
188
+ ))
189
+ )}
190
+ {(activation.connectors.length > 0 ||
191
+ activation.connectorProjection.truncated) && (
192
+ <div className="space-y-2 border-border/50 border-t pt-3">
193
+ <p className="font-medium text-xs uppercase tracking-wide">
194
+ Connector proof
195
+ </p>
196
+ {activation.connectors.slice(0, 8).map((connector) => (
197
+ <div
198
+ className="text-muted-foreground text-xs"
199
+ key={`${connector.collection}-${connector.target}`}
200
+ >
201
+ <span className="font-medium text-foreground">
202
+ {connector.target}
203
+ </span>{" "}
204
+ · {connector.collection} · {connector.status}
205
+ {connector.code ? `/${connector.code}` : ""}
206
+ </div>
207
+ ))}
208
+ {hiddenProjectedConnectorCount > 0 && (
209
+ <p className="text-muted-foreground text-xs">
210
+ +{hiddenProjectedConnectorCount} more projected checks
211
+ </p>
212
+ )}
213
+ {activation.connectorProjection.truncated && (
214
+ <p className="text-muted-foreground text-xs">
215
+ {omittedConnectorCount} additional target/collection checks
216
+ omitted from this bounded status view
217
+ </p>
218
+ )}
219
+ </div>
220
+ )}
221
+ </CardContent>
222
+ </Card>
130
223
  </div>
131
224
  </section>
132
225
  );
@@ -204,10 +204,8 @@ export function FirstRunWizard({
204
204
  onboarding,
205
205
  onAddCollection,
206
206
  onDownloadModels,
207
- onEmbed,
208
207
  onSync,
209
208
  onSyncComplete,
210
- embedding = false,
211
209
  syncJobId = null,
212
210
  syncing = false,
213
211
  }: FirstRunWizardProps) {
@@ -238,27 +236,19 @@ export function FirstRunWizard({
238
236
  : null;
239
237
  const progressValue = getStepProgress(onboarding);
240
238
  const isShowingRecommended = activeStep?.id === recommendedStepId;
241
- const indexingNeedsEmbeddings =
242
- onboarding.stage === "indexing" &&
243
- onboarding.detail.toLowerCase().includes("embedding");
244
-
245
239
  const runRecommendedAction = () => {
246
240
  if (!activeStep) {
247
241
  return;
248
242
  }
249
- if (activeStep.id === "folders") {
243
+ if (activeStep.action === "add-collection") {
250
244
  onAddCollection();
251
245
  return;
252
246
  }
253
- if (activeStep.id === "models") {
247
+ if (activeStep.action === "download-models") {
254
248
  onDownloadModels();
255
249
  return;
256
250
  }
257
- if (activeStep.id === "indexing" && indexingNeedsEmbeddings && !embedding) {
258
- onEmbed();
259
- return;
260
- }
261
- if (activeStep.id === "indexing" && !syncing) {
251
+ if (activeStep.action === "sync" && !syncing) {
262
252
  onSync();
263
253
  }
264
254
  };
@@ -403,7 +393,7 @@ export function FirstRunWizard({
403
393
  <WizardPanel>
404
394
  <WizardPanelHeader
405
395
  badge={<Badge variant="outline">Safe to rerun</Badge>}
406
- title="Finish first indexing"
396
+ title="Prove lexical retrieval"
407
397
  >
408
398
  <p className="max-w-2xl text-muted-foreground text-sm leading-6">
409
399
  {activeStep.detail}
@@ -419,31 +409,13 @@ export function FirstRunWizard({
419
409
 
420
410
  <WizardActionPanel
421
411
  action={
422
- <Button
423
- disabled={indexingNeedsEmbeddings ? embedding : syncing}
424
- onClick={indexingNeedsEmbeddings ? onEmbed : onSync}
425
- size="lg"
426
- >
412
+ <Button disabled={syncing} onClick={onSync} size="lg">
427
413
  <RefreshCwIcon className="mr-2 size-4" />
428
- {indexingNeedsEmbeddings
429
- ? embedding
430
- ? "Embedding..."
431
- : "Finish embeddings"
432
- : syncing
433
- ? "Syncing..."
434
- : "Run first sync"}
414
+ {syncing ? "Syncing..." : "Run first sync"}
435
415
  </Button>
436
416
  }
437
- body={
438
- indexingNeedsEmbeddings
439
- ? "Your files are indexed. One more embedding pass will unlock semantic search and local answers."
440
- : "Good last step before you move into normal use."
441
- }
442
- title={
443
- indexingNeedsEmbeddings
444
- ? "Finish semantic indexing"
445
- : "Index the current workspace"
446
- }
417
+ body="Builds the local lexical index and proves that this exact folder can return corpus-derived evidence."
418
+ title="Index and verify the current workspace"
447
419
  />
448
420
 
449
421
  {syncJobId && (
@@ -451,7 +423,7 @@ export function FirstRunWizard({
451
423
  <WizardPanelHeader title="Sync progress">
452
424
  <p className="max-w-2xl text-muted-foreground text-sm leading-6">
453
425
  Your first indexing run is in progress. The wizard will advance
454
- once the job finishes and embeddings catch up.
426
+ once the job finishes and lexical retrieval is proven.
455
427
  </p>
456
428
  </WizardPanelHeader>
457
429
  <div style={{ padding: "24px 28px" }}>
@@ -568,24 +540,14 @@ export function FirstRunWizard({
568
540
  </div>
569
541
  {activeStep && activeStep.id !== "preset" && (
570
542
  <Button
571
- disabled={
572
- activeStep.id === "indexing"
573
- ? indexingNeedsEmbeddings
574
- ? embedding
575
- : syncing
576
- : false
577
- }
543
+ disabled={activeStep.action === "sync" ? syncing : false}
578
544
  onClick={runRecommendedAction}
579
545
  size="sm"
580
546
  variant={isShowingRecommended ? "default" : "outline"}
581
547
  >
582
- {activeStep.id === "indexing" && indexingNeedsEmbeddings
583
- ? embedding
584
- ? "Embedding..."
585
- : "Finish embeddings"
586
- : activeStep.id === "indexing" && syncing
587
- ? "Syncing..."
588
- : getStepActionLabel(activeStep.id)}
548
+ {activeStep.action === "sync" && syncing
549
+ ? "Syncing..."
550
+ : getStepActionLabel(activeStep.id)}
589
551
  </Button>
590
552
  )}
591
553
  </div>