Skip to content

v2.1.4 - feat: embed model swap + semantic search + UMAP canvas - #56

Merged
ddutchie merged 3 commits into
mainfrom
ddutchie/betterembeddings
Jun 20, 2026
Merged

ddutchie merged 3 commits into
mainfrom
ddutchie/betterembeddings

Conversation

@ddutchie

@ddutchie ddutchie commented Jun 20, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Swaps the local embedding model from Nomic (137 MB, 768-dim) to bge-small-en-v1.5 (33 MB, 384-dim) via the new official @huggingface/transformers v4, and exposes embeddings search/projections to users and agents for the first time: semantic search in the global ⌘K panel, a new semantic_search_notes MCP tool, a new "Semantic Map" Insights canvas with inline UMAP recompute, an embeddings onboarding step, and a reindex-on-model-change modal.

Type of change

  • Bug fix
  • New feature
  • Refactor / code quality
  • Docs / changelog
  • Tests

Screenshots / recording

TODO — add before merge:

  • ⌘K search panel with semantic toggle on, showing a hit with the Sparkles 87% badge
  • Insights view → Semantic Map layout with dots coloured by project, zoom controls, stale banner
  • Onboarding "Embeddings" step with toggle on + model download progress bar
  • ReindexModal on startup after model swap
  • Empty-state Semantic Map with "Compute projections" button

Checklist

  • npm run type-check:all passes
  • npm run lint passes (not run — please run before merge)
  • npm test passes (519/519)
  • npm run test:e2e passes (run before merging — UI changes present)
  • No hardcoded colours — CSS variables only (var(--accent), var(--text-primary), etc.)
  • No text-[Npx] pixel font classes — rem equivalents only (text-[0.714rem], text-xs, etc.)
  • New IPC handlers wrapped in handle() and return IpcResult<T> — embeddings:projections, embeddings:needsReindex
  • New DB migrations appended (not edited) in schema.ts (no schema change — dim_x/dim_y/proj_stale columns already existed; new query only)
  • New SQL goes in electron/db/queries.ts — single source of truth (imported by both Electron main process and MCP server); never construct a Database instance outside db/client.ts (Electron) or mcp-server.ts (MCP runtime) — getNoteProjections() added there
  • New dependencies or devDependencies added to ROLE_MAP in scripts/generate-licenses.js and licenses.json regenerated (needs check: @huggingface/transformers replaced @xenova/transformers; onnxruntime-node bumped to 1.24.3)
  • New MCP tools registered in electron/mcp/tools/index.ts dispatch + electron/lib/tool-schemas.ts Zod schema — semantic_search_notes

Notes for reviewer

  • Breaking: drops Intel macOS (darwin-x64) build target. onnxruntime-node 1.24.3 (the version @huggingface/transformers v4 pins) no longer ships a darwin-x64 prebuilt. electron-builder.yml macOS arch is now [arm64] only. Intel Mac users should stay on v2.1.3. Flagging since this is the first platform drop.
  • DB dimension change is auto-handled. vector column is TEXT (JSON-serialized), so 768→384 needs no migration; model ID mismatch in reindexNotes triggers automatic re-embed on next reindex. ReindexModal prompts the user on startup when a mismatch is detected.
  • Dynamic imports in graph.ts — semantic_search_notes uses await import("../../embeddings/service") rather than a top-level import. This is deliberate: service.ts → manifest.ts calls app.getPath("userData") at module load, which isn't available in the MCP test environment. Static import broke 6 test suites (mcp-server, chat-executor, handlers, context-audit, pi-agent-loop, payload-baseline). Dynamic import defers evaluation to call time.
  • get_semantic_neighbors routing fix is incidental — chat-executor was missing this case in its dispatch group, so AI chat calls to this pre-existing MCP tool fell through to "Unknown tool". Folded into the same case group as get_knowledge_graph/get_neighbors. Not strictly part of this feature but tests would have caught it once the suite was green again.
  • Licenses need regen before merge — @huggingface/transformers is new, @xenova/transformers is removed. I didn't run scripts/generate-licenses.js because I'm not sure if it needs ROLE_MAP updates first. Please advise or run locally.
  • E2E not run — UI changes (search panel toggle, onboarding step, new canvas, modal) all need npm run test:e2e before merge.

Summary by CodeRabbit

  • New Features

    • Added a semantic search toggle to the global search panel with similarity scoring
    • Introduced a “Semantic Map” view in Insights to visualize embedded notes (with recompute/projection progress)
    • Added an embeddings onboarding step and a startup reindex prompt when the embedding model changes
    • Added a semantic note search tool for the AI assistant
  • Improvements

    • Upgraded local embeddings to a smaller model with automatic cleanup of unused model files
    • Semantic results merge with keyword results (deduped) with cosine-score badges; automatic onboarding prepares embeddings before indexing
  • Bug Fixes

    • Prevented embeddings worker crashes and ensured semantic search tool routing works reliably in chat

@coderabbitai

coderabbitai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8efbbaa9-d956-4aa6-b771-7715d6c01747

📥 Commits

Reviewing files that changed from the base of the PR and between 013f2c7 and 22c7eb3.

📒 Files selected for processing (2)
  • src/components/onboarding/StepEmbeddings.tsx
  • src/components/search/search-panel.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/onboarding/StepEmbeddings.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/search/search-panel.tsx

📝 Walkthrough

Walkthrough

This PR migrates the local embedding stack from nomic-ai/nomic-embed-text-v1.5 (768-dim, @xenova/transformers) to Xenova/bge-small-en-v1.5 (384-dim, @huggingface/transformers v4), adds a search_notes_semantic MCP tool wired through the agent, introduces a Semantic Map canvas in Insights via UMAP projections, adds semantic mode to the ⌘K search panel, adds a startup reindex modal, and inserts an embeddings configuration step into the onboarding wizard.

Changes

Local Embeddings Upgrade: bge-small model, semantic features, onboarding

Layer / File(s) Summary
Embedding types, prefix helper, and library migration
electron/embeddings/types.ts, electron/embeddings/prefix.ts, electron/embeddings/pipeline.ts, electron/embeddings/service.ts, electron/embeddings/client.ts, electron/embeddings/server.ts, electron/embeddings/service.bench.test.ts, electron/embeddings/query-cache.test.ts, src/components/settings/EmbeddingsSettings.tsx, package.json, scripts/build-embeddings-binary.js, scripts/generate-licenses.js, electron/native-deps-guard.test.ts
Replaces NomicTask/NOMIC_MODEL_ID/NOMIC_DIM with EmbedTask/EMBED_MODEL_ID/EMBED_DIM; adds withTaskPrefix helper (search_query prefixed, others pass-through); swaps @xenova/transformers for @huggingface/transformers v4 in dependencies, build scripts, and license map; updates bench tests to 384-dim.
Manifest: supported models update and orphan pruning
electron/embeddings/manifest.ts
Switches SUPPORTED_EMBEDDING_MODELS to the bge-small entry; adds pruneOrphanedModels() called at manifest read time to delete unsupported model directories, remove stale manifest keys, and persist the cleaned manifest.
DB projection query
electron/db/queries.ts
Adds exported NoteProjectionRow interface and getNoteProjections(db, workspaceId) that selects section_idx=0 rows, computes anyStale from proj_stale, skips null-dimension rows, and returns { rows, anyStale }.
New IPC channels: needsReindex and projections
electron/ipc/embeddings-handlers.ts, electron/preload.ts
Registers embeddings:needsReindex (cached settings + model-mismatch check) and embeddings:projections (wraps getNoteProjections + effective model) IPC handlers; updates all existing handler fallbacks to EMBED_MODEL_ID; exposes both as window.electron.embeddings.needsReindex() / .projections() in preload.
search_notes_semantic MCP tool and agent wiring
electron/lib/tool-schemas.ts, electron/lib/tools.ts, electron/mcp/tools/graph.ts, electron/mcp/tools/index.ts, electron/ipc/chat-executor.ts, electron/lib/pi-agent-loop.ts, electron/lib/pi-agent-prompt.ts
Adds search_notes_semantic Zod schema to TOOL_SCHEMAS/MCP_TOOLS; implements the tool in graph.ts (validates args, gates on embeddings enabled, calls searchAdjacent, returns { query, model, results }); routes it in executeTool dispatcher and chat-executor; adds to allowlists and both Plan/Execute system prompts.
Semantic Map canvas in Insights
src/components/graph/SemanticMapCanvas.tsx, src/components/insights/InsightsView.tsx
New SemanticMapCanvas component fetches 2D UMAP projection rows via IPC, renders a D3-scaled SVG with pan/zoom, per-project color coding, hover tooltip, recompute button/progress overlay, and stale banner; InsightsView adds "semantic-map" to layout options and renders the canvas when selected.
Semantic mode in the ⌘K search panel
src/components/search/search-panel.tsx
Adds semanticMode toggle (persisted to localStorage), embedding readiness probe, async debounced semantic search merged with keyword results, per-result cosine score badge on ResultRow, and a toggle button disabled until embeddings are ready.
ReindexModal: startup reindex prompt
src/components/layout/ReindexModal.tsx, src/app/page.tsx
New ReindexModal checks needsReindex() on mount, subscribes to progress events, and transitions through pending/running/done/error phases with "Reindex Now"/"Later"/"Continue" actions; inserted after MigrationModal in the page tree.
Embeddings onboarding step
src/components/onboarding/StepEmbeddings.tsx, src/components/onboarding/index.tsx, src/components/onboarding/shared.tsx
New StepEmbeddings component manages model catalog fetch, download/progress, and enable toggle; adds "embeddings" to OnboardingStep union and PROGRESS_STEPS; wizard routes mcp → embeddings → views, loads/saves settings via electron API.
Build config, sharp stub, and changelog
electron-builder.yml, electron/sharp-stub/index.d.ts, electron/sharp-stub/index.js, changelogs/v2.1.4.md
Restricts macOS zip build target to arm64 only; changes sharp stub default export from null to a throwing function to satisfy @huggingface/transformers compat check; documents all v2.1.4 new features and fixes.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(99, 102, 241, 0.5)
    Note over Renderer,DB: Startup reindex check
  end
  participant Renderer
  participant preload
  participant embeddings_handlers
  participant DB
  participant EmbeddingsWorker

  Renderer->>preload: embeddings.needsReindex()
  preload->>embeddings_handlers: IPC embeddings:needsReindex
  embeddings_handlers->>DB: getEmbeddingsSettingsCached() + stored model query
  DB-->>embeddings_handlers: { currentModel }
  embeddings_handlers-->>Renderer: { needed: true, reason: "model_changed" }
  Renderer->>Renderer: show ReindexModal (pending phase)
  Renderer->>preload: embeddings.reindex(workspaceId)
  preload->>embeddings_handlers: IPC db:embeddings:reindex
  embeddings_handlers->>EmbeddingsWorker: reindexNotes(model: EMBED_MODEL_ID)
  EmbeddingsWorker-->>Renderer: onProgress events (done/total)
  EmbeddingsWorker-->>Renderer: onProgress { done: true }
  Renderer->>Renderer: transition to "done" phase

  rect rgba(16, 185, 129, 0.5)
    Note over Agent,service_ts: search_notes_semantic MCP tool
  end
  participant Agent
  participant chat_executor
  participant graph_ts
  participant service_ts

  Agent->>chat_executor: executeTool("search_notes_semantic", args)
  chat_executor->>graph_ts: search_notes_semantic(db, args)
  graph_ts->>graph_ts: validate workspaceId + query
  graph_ts->>service_ts: searchAdjacent(db, workspaceId, query, k, EMBED_MODEL_ID)
  service_ts-->>graph_ts: results[]
  graph_ts-->>Agent: { query, model, results }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ddutchie/cairn#54: Introduces the get_semantic_neighbors MCP tool in electron/mcp/tools/graph.ts, which this PR now correctly routes through chat-executor.ts alongside the new search_notes_semantic tool.

Poem

🐇 Hopped from Nomic lands to bge-small terrain,
With half the dimensions but search just the same!
A Semantic Map blooms like clover on screen,
The ⌘K panel now sparkles and gleams.
New onboarding steps show the way to the den —
This rabbit's embeddings are tiny but keen! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main changes: embedding model upgrade (from Nomic to bge-small), semantic search feature, and UMAP visualization canvas.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ddutchie/betterembeddings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (1)
electron/lib/tool-schemas.ts (1)

351-351: ⚡ Quick win

Constrain k in the schema to prevent oversized semantic queries.

k currently accepts any number. Adding integer/positive/max bounds at schema level keeps the tool contract tight and avoids expensive accidental requests.

Proposed fix
-      k:          z.number().optional().describe("Max results (default: 5)"),
+      k:          z.number().int().positive().max(50).optional().describe("Max results (default: 5, max: 50)"),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/lib/tool-schemas.ts` at line 351, The `k` parameter in the schema
currently accepts any number without constraints, which could lead to expensive
semantic queries if large values are passed. Modify the `k` field definition to
add bounds by chaining constraints to the z.number() call: ensure it accepts
only integers using .int(), ensure it is positive using .positive(), and set a
reasonable maximum value using .max() to prevent oversized requests and keep the
tool contract tight.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/embeddings/manifest.ts`:
- Around line 199-208: After the manifest cleanup loop that removes unsupported
keys from the manifest object, add logic to also reset the default model ID if
it is no longer supported. After calling writeManifestFile(manifest), read the
current default model ID using readDefaultModelId() and check if it exists in
SUPPORTED_EMBEDDING_MODELS. If the default model ID is not in the supported
models, reset it to a valid default value by calling the appropriate method to
write a new default model ID to the default-model file, ensuring consistency
between the cleaned manifest and the persisted default model configuration.
- Around line 173-188: The orphan pruning logic has a symlink vulnerability
where fs.statSync follows symlinks and fs.rmSync could delete directories
outside the cache root. Replace fs.statSync with fs.lstatSync in the directory
validation check to prevent following symlinks, and add path validation using
path.resolve to ensure the resolved modelDir path stays within MODELS_DIR before
attempting recursive deletion with fs.rmSync, preventing accidental deletion of
unintended directories.

In `@electron/ipc/embeddings-handlers.ts`:
- Around line 92-97: The query in the note_embeddings check is selecting only
one distinct model with LIMIT 1, which misses model mismatches when multiple
different models exist in the table. Instead of selecting a model and comparing
it, change the query to check for the existence of any row where the model does
NOT match the current model parameter. Replace the SELECT DISTINCT approach with
a query that returns a row only if a mismatch is found (for example, using a
WHERE clause to filter for rows with a different model value), so that the
condition correctly identifies when reindexing is needed regardless of how many
different models are in the database.

In `@electron/lib/tool-schemas.ts`:
- Around line 346-354: The tool semantic_search_notes is being misclassified as
a write operation because its name does not start with one of the recognized
read operation prefixes (get_, list_, search_) that the toolCategory() function
checks for. Rename the tool from semantic_search_notes to search_notes_semantic
(or another name that begins with search_) to ensure it matches the naming
pattern that toolCategory() uses to correctly classify it as a read operation.

In `@electron/mcp/tools/graph.ts`:
- Around line 273-280: In the searchAdjacent function call, the k parameter is
currently checked for being a positive number but lacks upper bounds clamping,
which allows excessively large values to propagate and cause SQL placeholder
list overflow. Replace the current k parameter logic that checks typeof k ===
"number" && k > 0 ? k : 5 with a clamping approach that not only validates k as
a positive number but also caps it to a reasonable maximum value (such as 100 or
1000) and ensures it is converted to an integer using Math.floor or Math.trunc
to prevent floating-point issues.

In `@src/components/graph/SemanticMapCanvas.tsx`:
- Line 254: In the SemanticMapCanvas.tsx file, the className attribute on line
254 contains the raw Tailwind color token text-white, which violates the
repository's color-token rule requiring all colors to use CSS variables. Replace
text-white with text-[var(--text-primary)] or the appropriate CSS variable token
that matches your repository's theme variables (following the same pattern
already used for bg-[var(--accent)] in the same className), ensuring consistency
with the codebase's CSS variable naming conventions.
- Line 296: The cursor style on line 296 reads isDragging.current from a ref,
which doesn't trigger re-renders when the ref changes, preventing the cursor
from updating during drag operations. Convert isDragging from a ref to React
state using useState, ensure the state is updated whenever drag operations begin
and end (replace all isDragging.current assignments with setState calls), and
update the style prop to use the state value instead of the ref. Additionally,
replace the hardcoded text-white class on line 254 with
text-[var(--text-primary)] to comply with the color token guidelines.

In `@src/components/layout/ReindexModal.tsx`:
- Around line 180-191: Replace the raw Tailwind color name `text-white` in the
className attributes of both buttons (the "Reindex Now" button around line 180
and the dismiss button around line 190) with a semantic CSS variable that
represents white text color, following the color-token contract established in
your design system. This ensures theme consistency and prevents color drift from
the theming system.
- Around line 48-56: The onProgress callback for electron.embeddings.models is
listening to a global channel, so any model download error will trigger the
error state in this modal even if unrelated to the current reindex. Scope the
error handling by adding a check before setting the error state in the else if
(ev.status === "error") condition to verify that the error event is actually
related to the current reindex operation being tracked in this modal, such as by
checking if a reindex is currently active or by verifying the event corresponds
to the model being downloaded in this context.

In `@src/components/onboarding/StepEmbeddings.tsx`:
- Around line 131-132: In the StepEmbeddings component, replace all Tailwind
opacity modifiers (like the `/40` alpha syntax on the accent color) and raw
color utilities (like raw `white` utilities) with semantic CSS variable-based
colors using `color-mix(in srgb, var(--token) X%, transparent)` syntax for alpha
variants. Convert the border and background color declarations throughout the
component to reference CSS variables (--accent, --border, --surface-2, etc.) and
where transparency is needed, use the color-mix function to define the alpha
value instead of Tailwind's opacity syntax, ensuring all color usage follows the
coding guidelines of using only CSS variables and the specified color-mix
pattern for alpha variants.

In `@src/components/search/search-panel.tsx`:
- Around line 247-248: The focused index calculation is based on the grouped UI
order of noteResults and taskResults (shown in lines 343-368), but the Enter key
handler at line 234 selects from the original filtered array using
filtered[focused], causing a mismatch between visual focus and what gets
selected. Fix this by determining which group (noteResults or taskResults) the
currently focused item belongs to based on the focus index, then select from the
appropriate grouped array instead of the original filtered array when handling
the Enter key press.
- Around line 280-295: The Tooltip component directly wraps a disabled button,
which prevents the tooltip from displaying since disabled buttons don't emit
trigger events. To fix this, move the disabled button inside a wrapper element
(such as a div) that remains interactive and can trigger the tooltip. The
wrapper should contain the button with the toggleSemantic onClick handler and
the Sparkles icon, while the Tooltip still wraps the outer container. This
allows the tooltip with the "enable embeddings" help text to display even when
embeddingsReady is false and the button is disabled.
- Around line 170-205: The semantic search in semanticTimer.current fires an
asynchronous request but merges results without verifying the query is still
fresh, allowing stale responses from older queries to contaminate newer search
results. Capture the trimmed query value at the beginning of the semantic search
request, then before merging results in the setResults call around line 194,
check that the current trimmed value still matches the query that was originally
searched. Only proceed with the merge if the queries match, ensuring stale
responses from outdated queries are discarded.

---

Nitpick comments:
In `@electron/lib/tool-schemas.ts`:
- Line 351: The `k` parameter in the schema currently accepts any number without
constraints, which could lead to expensive semantic queries if large values are
passed. Modify the `k` field definition to add bounds by chaining constraints to
the z.number() call: ensure it accepts only integers using .int(), ensure it is
positive using .positive(), and set a reasonable maximum value using .max() to
prevent oversized requests and keep the tool contract tight.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6414e55-ef98-4288-827f-ef3985cd7e42

📥 Commits

Reviewing files that changed from the base of the PR and between 677872e and 0004992.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (35)
  • changelogs/v2.1.4.md
  • electron-builder.yml
  • electron/db/queries.ts
  • electron/embeddings/client.ts
  • electron/embeddings/manifest.ts
  • electron/embeddings/nomic.ts
  • electron/embeddings/pipeline.ts
  • electron/embeddings/prefix.ts
  • electron/embeddings/query-cache.test.ts
  • electron/embeddings/server.ts
  • electron/embeddings/service.bench.test.ts
  • electron/embeddings/service.ts
  • electron/embeddings/types.ts
  • electron/ipc/chat-executor.ts
  • electron/ipc/embeddings-handlers.ts
  • electron/lib/pi-agent-loop.ts
  • electron/lib/pi-agent-prompt.ts
  • electron/lib/tool-schemas.ts
  • electron/lib/tools.ts
  • electron/mcp/tools/graph.ts
  • electron/mcp/tools/index.ts
  • electron/native-deps-guard.test.ts
  • electron/preload.ts
  • package.json
  • scripts/build-embeddings-binary.js
  • scripts/generate-licenses.js
  • src/app/page.tsx
  • src/components/graph/SemanticMapCanvas.tsx
  • src/components/insights/InsightsView.tsx
  • src/components/layout/ReindexModal.tsx
  • src/components/onboarding/StepEmbeddings.tsx
  • src/components/onboarding/index.tsx
  • src/components/onboarding/shared.tsx
  • src/components/search/search-panel.tsx
  • src/components/settings/EmbeddingsSettings.tsx
💤 Files with no reviewable changes (1)
  • electron/embeddings/nomic.ts

Comment thread electron/embeddings/manifest.ts
Comment thread electron/embeddings/manifest.ts
Comment thread electron/ipc/embeddings-handlers.ts
Comment thread electron/lib/tool-schemas.ts Outdated
Comment on lines +346 to +354
semantic_search_notes: {
description: "Semantic search over notes using local embeddings. Sends a natural-language query, embeds it, and returns matching notes ranked by cosine similarity. Use this when keyword search (search_notes) would miss conceptually related notes — e.g. querying 'auth flow' should find notes titled 'Login pipeline'. Requires embeddings to be enabled and indexed. Returns noteId, title, score (0–1), sectionTitle for each match.",
schema: z.object({
workspaceId: sId,
query: sStr.describe("Natural-language query"),
k: z.number().optional().describe("Max results (default: 5)"),
}),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

semantic_search_notes is misclassified as a write tool.

Because the tool name doesn’t match get_/list_/search_, toolCategory() (Line 470+) will classify it as "write". This is a read-only operation and should be categorized as "read" to avoid incorrect behavior in any read/write policy paths.

Proposed fix
 function toolCategory(name: string): ToolCategory {
   if (name.startsWith("delete_")) return "delete";
   if (
     name.startsWith("get_") ||
     name.startsWith("list_") ||
-    name.startsWith("search_")
+    name.startsWith("search_") ||
+    name.startsWith("semantic_search_")
   ) return "read";
   return "write";
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
semantic_search_notes: {
description: "Semantic search over notes using local embeddings. Sends a natural-language query, embeds it, and returns matching notes ranked by cosine similarity. Use this when keyword search (search_notes) would miss conceptually related notes — e.g. querying 'auth flow' should find notes titled 'Login pipeline'. Requires embeddings to be enabled and indexed. Returns noteId, title, score (0–1), sectionTitle for each match.",
schema: z.object({
workspaceId: sId,
query: sStr.describe("Natural-language query"),
k: z.number().optional().describe("Max results (default: 5)"),
}),
},
function toolCategory(name: string): ToolCategory {
if (name.startsWith("delete_")) return "delete";
if (
name.startsWith("get_") ||
name.startsWith("list_") ||
name.startsWith("search_") ||
name.startsWith("semantic_search_")
) return "read";
return "write";
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/lib/tool-schemas.ts` around lines 346 - 354, The tool
semantic_search_notes is being misclassified as a write operation because its
name does not start with one of the recognized read operation prefixes (get_,
list_, search_) that the toolCategory() function checks for. Rename the tool
from semantic_search_notes to search_notes_semantic (or another name that begins
with search_) to ensure it matches the naming pattern that toolCategory() uses
to correctly classify it as a read operation.

Comment thread electron/mcp/tools/graph.ts
Comment thread src/components/layout/ReindexModal.tsx Outdated
Comment on lines +131 to +132
? "border-[var(--accent)]/40 bg-[var(--accent-dim)]"
: "border-[var(--border)] bg-[var(--surface-2)]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use semantic color tokens + color-mix for alpha variants in this step.

Line 131 uses /40 alpha syntax, and Line 146 / Line 177 use raw white utilities. Please switch these to CSS variables and color-mix(in srgb, var(--token) X%, transparent) where alpha is needed.

As per coding guidelines, **/*.{css,scss,ts,tsx}: “All colours must use CSS variables … never raw Tailwind colour names,” and “Use color-mix(in srgb, var(--token) X%, transparent) for alpha variants.”

Also applies to: 146-147, 177-177

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/onboarding/StepEmbeddings.tsx` around lines 131 - 132, In the
StepEmbeddings component, replace all Tailwind opacity modifiers (like the `/40`
alpha syntax on the accent color) and raw color utilities (like raw `white`
utilities) with semantic CSS variable-based colors using `color-mix(in srgb,
var(--token) X%, transparent)` syntax for alpha variants. Convert the border and
background color declarations throughout the component to reference CSS
variables (--accent, --border, --surface-2, etc.) and where transparency is
needed, use the color-mix function to define the alpha value instead of
Tailwind's opacity syntax, ensuring all color usage follows the coding
guidelines of using only CSS variables and the specified color-mix pattern for
alpha variants.

Source: Coding guidelines

Comment thread src/components/search/search-panel.tsx
Comment thread src/components/search/search-panel.tsx
Comment thread src/components/search/search-panel.tsx
- Swap embedding model Nomic → bge-small-en-v1.5 (33MB, 384-dim, 4× smaller)
- Migrate @xenova/transformers v2 → @huggingface/transformers v4.2.0
- Drop Intel macOS (darwin-x64) build target (onnxruntime-node 1.24.3)
- New ReindexModal prompts on startup when model changes detected
- New embeddings onboarding step (toggle + model download with progress)
- Semantic search in global ⌘K panel (Sparkles toggle, merges with keyword hits)
- New semantic_search_notes MCP tool for agents and external MCP clients
- New Semantic Map Insights canvas (D3 scatter of UMAP projections)
- Surface recomputeProjections inline on canvas empty/stale states
- New IPC: embeddings:projections, embeddings:needsReindex
- Auto-delete orphaned models on settings panel open
- Fix sharp stub to export truthy function (was null, crashed Node 25 worker)
- Fix get_semantic_neighbors routing in chat-executor dispatch
@ddutchie
ddutchie force-pushed the ddutchie/betterembeddings branch from 1809a61 to 13443b3 Compare June 20, 2026 01:01
- manifest.ts: reset default model ID if unsupported after orphan pruning;
  use lstatSync to skip symlinked dirs; validate resolved path stays within
  MODELS_DIR before recursive delete (path traversal protection)
- embeddings-handlers.ts: needsReindex query now checks for ANY row with
  mismatched model (was SELECT DISTINCT LIMIT 1, missed partial states)
- tool-schemas.ts: rename semantic_search_notes → search_notes_semantic so
  toolCategory() classifies it as a read op; add k bounds (int, positive, max 100)
- graph.ts: clamp k parameter (Math.floor, max 100)
- SemanticMapCanvas: text-white → text-[var(--surface)]
- ReindexModal: text-white → text-[var(--surface)]; scope error handler to
  only fire during active reindex (was triggering on unrelated model downloads)
- StepEmbeddings: text-white → text-[var(--surface)]; accent /40 opacity
  → color-mix(in srgb, var(--accent) 40%, transparent)
- search-panel: stale query guard (discard async results if query changed
  during debounce); fix Enter key index to match grouped UI order (notes
  then tasks, not interleaved filtered array order)
@ddutchie

Copy link
Copy Markdown
Owner Author

@coderabbitai

Skipped findings (with reasons):

SemanticMapCanvas isDragging ref→state — Already fixed in the prior lint cleanup commit. Line 300 already uses the dragging state variable, not the ref.
search-panel Tooltip wrapping disabled button — False positive. Radix Tooltip with asChild does support disabled buttons (verified in EmbeddingsSettings.tsx:288-296 where the same pattern works in production). No change needed.
StepEmbeddings bg-white on toggle knob — Established codebase convention (ViewVisibilitySettings.tsx:29, StepAISetup.tsx). Toggle knobs are intentionally white in both themes for visual contrast against the accent track.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@ddutchie Thanks for the thorough explanations — all three make sense:

  1. isDragging ref→state — Noted; the fix was already applied in the prior lint cleanup commit.
  2. Tooltip wrapping disabled button — Withdrawn. Good call; asChild on Radix <Tooltip.Trigger> correctly forwards the disabled state, and the same pattern is used in production in EmbeddingsSettings.tsx.
  3. bg-white on toggle knob — Withdrawn. Intentional convention for visual contrast against the accent-coloured track, consistent with ViewVisibilitySettings.tsx and StepAISetup.tsx.

✏️ Learnings added
Learnt from: ddutchie
URL: https://github.com/ddutchie/cairn/pull/56

Timestamp: 2026-06-20T01:05:57.040Z
Learning: In the Cairn codebase (TypeScript/React, Electron), wrapping a disabled button with a Radix UI Tooltip using `asChild` is valid and intentional — the Tooltip.Trigger with asChild correctly forwards the disabled state. This pattern is used in production at EmbeddingsSettings.tsx lines 288-296 and should not be flagged as a bug.

Learnt from: ddutchie
URL: https://github.com/ddutchie/cairn/pull/56

Timestamp: 2026-06-20T01:05:57.040Z
Learning: In the Cairn codebase (TypeScript/React), toggle knob elements use `bg-white` intentionally in both light and dark themes. This is an established codebase convention for visual contrast against the accent-coloured track, consistent with ViewVisibilitySettings.tsx line 29 and StepAISetup.tsx. Do not flag bg-white on toggle knobs as a theming issue.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelogs/v2.1.4.md`:
- Line 19: Remove the trailing spaces inside the inline code spans on the
changelog entry for bge-small query prefix. The backtick-enclosed terms
`search_query: ` and `search_document: ` currently have spaces before the
closing backticks, which violates markdown formatting rules (MD038). Trim these
spaces so the code spans end immediately after the colons without trailing
whitespace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 861924d0-2d0f-459f-87f2-be5f7417e1d7

📥 Commits

Reviewing files that changed from the base of the PR and between 0004992 and 013f2c7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (37)
  • changelogs/v2.1.4.md
  • electron-builder.yml
  • electron/db/queries.ts
  • electron/embeddings/client.ts
  • electron/embeddings/manifest.ts
  • electron/embeddings/nomic.ts
  • electron/embeddings/pipeline.ts
  • electron/embeddings/prefix.ts
  • electron/embeddings/query-cache.test.ts
  • electron/embeddings/server.ts
  • electron/embeddings/service.bench.test.ts
  • electron/embeddings/service.ts
  • electron/embeddings/types.ts
  • electron/ipc/chat-executor.ts
  • electron/ipc/embeddings-handlers.ts
  • electron/lib/pi-agent-loop.ts
  • electron/lib/pi-agent-prompt.ts
  • electron/lib/tool-schemas.ts
  • electron/lib/tools.ts
  • electron/mcp/tools/graph.ts
  • electron/mcp/tools/index.ts
  • electron/native-deps-guard.test.ts
  • electron/preload.ts
  • electron/sharp-stub/index.d.ts
  • electron/sharp-stub/index.js
  • package.json
  • scripts/build-embeddings-binary.js
  • scripts/generate-licenses.js
  • src/app/page.tsx
  • src/components/graph/SemanticMapCanvas.tsx
  • src/components/insights/InsightsView.tsx
  • src/components/layout/ReindexModal.tsx
  • src/components/onboarding/StepEmbeddings.tsx
  • src/components/onboarding/index.tsx
  • src/components/onboarding/shared.tsx
  • src/components/search/search-panel.tsx
  • src/components/settings/EmbeddingsSettings.tsx
💤 Files with no reviewable changes (1)
  • electron/embeddings/nomic.ts
✅ Files skipped from review due to trivial changes (7)
  • electron/sharp-stub/index.d.ts
  • electron-builder.yml
  • scripts/generate-licenses.js
  • src/components/settings/EmbeddingsSettings.tsx
  • electron/lib/pi-agent-prompt.ts
  • electron/lib/pi-agent-loop.ts
  • scripts/build-embeddings-binary.js
🚧 Files skipped from review as they are similar to previous changes (22)
  • electron/embeddings/query-cache.test.ts
  • electron/ipc/chat-executor.ts
  • electron/embeddings/prefix.ts
  • src/app/page.tsx
  • electron/preload.ts
  • electron/embeddings/server.ts
  • electron/embeddings/types.ts
  • src/components/insights/InsightsView.tsx
  • src/components/onboarding/shared.tsx
  • src/components/onboarding/index.tsx
  • electron/embeddings/manifest.ts
  • electron/ipc/embeddings-handlers.ts
  • package.json
  • electron/embeddings/client.ts
  • electron/embeddings/service.bench.test.ts
  • electron/embeddings/pipeline.ts
  • src/components/onboarding/StepEmbeddings.tsx
  • electron/db/queries.ts
  • electron/embeddings/service.ts
  • src/components/layout/ReindexModal.tsx
  • src/components/search/search-panel.tsx
  • src/components/graph/SemanticMapCanvas.tsx

Comment thread changelogs/v2.1.4.md
@ddutchie

Copy link
Copy Markdown
Owner Author

@coderabbitai whats left to address. please provide final prompt for agent and double check if things have been adressed

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check open review comments and PR status
gh pr view 56 --repo ddutchie/cairn --json state,reviewDecision,reviews,comments | jq '{state, reviewDecision}'

echo "--- Open review threads ---"
gh api graphql -F owner=ddutchie -F name=cairn -F pr=56 -f query='
query($owner: String!, $name: String!, $pr: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $pr) {
      reviewThreads(first: 50) {
        nodes {
          isResolved
          comments(first: 1) {
            nodes {
              author { login }
              body
            }
          }
        }
      }
    }
  }
}' | jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {isResolved, firstComment: .comments.nodes[0].body[:120]}'

Length of output: 1408


🏁 Script executed:

#!/bin/bash
gh api graphql -F owner=ddutchie -F name=cairn -F pr=56 -f query='
query($owner: String!, $name: String!, $pr: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $pr) {
      reviewThreads(first: 50) {
        nodes {
          isResolved
          path
          line
          comments(first: 10) {
            nodes {
              author { login }
              body
            }
          }
        }
      }
    }
  }
}' | jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)'

Length of output: 8974



@ddutchie Here's the complete status:


✅ Already addressed

# Item Status
1 isDragging ref→state (SemanticMapCanvas) Fixed in prior lint commit
2 Tooltip+asChild on disabled button (EmbeddingsSettings pattern) Withdrawn — valid pattern
3 bg-white on toggle knob (StepEmbeddings) Withdrawn — established convention
4 search_notes_semantic tool category Likely already fixed — the tool is named search_notes_semantic which starts with search_, so toolCategory() already returns "read". The open thread's embedded prompt says "rename from semantic_search_notes → search_notes_semantic" — that rename appears to have already happened. You can resolve this thread.

🔴 Still open (2 code issues)

Thread A — src/components/onboarding/StepEmbeddings.tsx (coding guideline violation)
Lines 131, 146, 177 use raw Tailwind opacity (/40) and raw white utilities. Per coding guidelines, all colours must use CSS variables and color-mix(in srgb, var(--token) X%, transparent) for alpha.

Thread B — src/components/search/search-panel.tsx line 305
A plain <Tooltip> (no asChild) directly wraps a disabled button. Unlike the asChild pattern discussed earlier, this case means the tooltip will not show on hover when the button is disabled (disabled buttons don't emit pointer events). Wrapping the button in a <span> pointer container fixes it. (This is a distinct issue from the withdrawn comment.)


📋 Pre-merge checklist (from PR description)

  • Run npm run lint
  • Regenerate licenses (node scripts/generate-licenses.js — maps @huggingface/transformers replacing @xenova/transformers)
  • Run npm run test:e2e

🤖 Final agent prompt

You are fixing open review issues in PR `#56` of the Cairn codebase.
Verify each item against the current code before changing anything.

──────────────────────────────────────────────────
ISSUE 1 — src/components/onboarding/StepEmbeddings.tsx
File: src/components/onboarding/StepEmbeddings.tsx
Lines: ~131, ~146, ~177

The component uses Tailwind opacity-modifier syntax (e.g. `accent/40`) and raw
`white` colour utilities. Per project coding guidelines, all colours must be
expressed as CSS variables and alpha variants must use:
  color-mix(in srgb, var(--token) X%, transparent)

Tasks:
1. Replace any `/40` (or similar fraction) opacity modifier on an accent/colour
   class with the color-mix equivalent referencing the matching CSS variable.
2. Replace raw `white` utility classes (bg-white, text-white, border-white, etc.)
   with the correct semantic CSS variable (e.g. `bg-[var(--surface)]`,
   `text-[var(--text-primary)]`). Check surrounding components in the codebase
   (ViewVisibilitySettings.tsx, StepAISetup.tsx) to confirm the right token.
3. Keep all other styling unchanged.

──────────────────────────────────────────────────
ISSUE 2 — src/components/search/search-panel.tsx
File: src/components/search/search-panel.tsx
Lines: ~280–295 (Sparkles semantic toggle button)

A plain <Tooltip> directly wraps a <button disabled={!embeddingsReady}>.
Disabled buttons swallow pointer events, so the tooltip never appears when
embeddings are not ready — exactly when the help text ("Enable embeddings in
Settings to use semantic search") is most needed.

Fix: wrap the disabled button in an interactive container so pointer events
reach the Tooltip trigger:

  <Tooltip content={...}>
    <span className={cn(!embeddingsReady && "cursor-not-allowed")}>
      <button
        type="button"
        onClick={toggleSemantic}
        disabled={!embeddingsReady}
        className={cn(...)}
      >
        <Sparkles size={13} />
      </button>
    </span>
  </Tooltip>

The Tooltip itself does NOT use asChild here, so do not add it.
Keep all existing class names and logic unchanged.

──────────────────────────────────────────────────
VERIFICATION ONLY (no code change expected)
File: electron/lib/tool-schemas.ts

Confirm that the search_notes_semantic tool name already starts with "search_"
so that toolCategory() returns "read". If so, no code change is needed — just
report it as already correct.
──────────────────────────────────────────────────

- StepEmbeddings: bg-white → bg-[var(--accent-fg,#fff)] on toggle knob
  (matches button.tsx pattern, respects --accent-fg if defined)
- search-panel: wrap disabled Sparkles button in <span> so Tooltip trigger
  lands on interactive element, not the disabled button (fixes tooltip
  not showing when embeddings are disabled)
@ddutchie

Copy link
Copy Markdown
Owner Author

@coderabbitai
VERIFICATION (tool-schemas.ts) — Already correct. search_notes_semantic starts with search_, so toolCategory() returns "read". No change needed.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@ddutchie Confirmed — that thread is resolved. search_notes_semantic already has the correct "read" category, no change needed.

The two remaining open code issues from the agent prompt are still outstanding:

  • Issue 1 — StepEmbeddings.tsx colour token cleanup (/40 opacity modifiers → color-mix, raw white → CSS variables)
  • Issue 2 — search-panel.tsx tooltip <span> wrapper around the disabled Sparkles button

Plus the pre-merge checklist items: npm run lint, license regeneration, and npm run test:e2e.

@ddutchie

Copy link
Copy Markdown
Owner Author

@coderabbitai everything should be addressed. Give approval and your thoughts on this PR.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check open review threads
gh api graphql -F owner=ddutchie -F name=cairn -F pr=56 -f query='
query($owner: String!, $name: String!, $pr: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $pr) {
      reviewThreads(first: 50) {
        nodes {
          isResolved
          path
          line
          comments(first: 5) {
            nodes {
              author { login }
              body
            }
          }
        }
      }
    }
  }
}' | jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {path, line, firstComment: .comments.nodes[0].body[:150]}'

echo "--- PR state ---"
gh pr view 56 --repo ddutchie/cairn --json state,title,headRefName

Length of output: 1418


🏁 Script executed:

#!/bin/bash
# Check current state of the two files with open threads
echo "=== StepEmbeddings.tsx lines around 125-185 ==="
fd "StepEmbeddings.tsx" --exec cat {} | head -200

echo ""
echo "=== search-panel.tsx lines around 280-320 ==="
fd "search-panel.tsx" --exec grep -n "Sparkles\|semantic\|Tooltip\|disabled\|span\|cursor-not-allowed\|embeddingsReady\|color-mix\|white\|/40" {} | head -60

echo ""
echo "=== tool-schemas.ts - search_notes_semantic entry ==="
fd "tool-schemas.ts" --exec grep -n "semantic\|search_notes\|toolCategory\|CHAT_ONLY" {} | head -20

Length of output: 12039



@ddutchie All code issues are confirmed fixed in the current branch:

Issue Status
StepEmbeddings.tsx — /40 opacity → color-mix(in srgb, var(--accent) 40%, transparent) via inline style ✅ Fixed
StepEmbeddings.tsx — toggle knob → bg-[var(--accent-fg,#fff)] ✅ Fixed
search-panel.tsx — <span> wrapper around disabled Sparkles button ✅ Fixed (lines 291–306)
tool-schemas.ts — search_notes_semantic category = "read" ✅ Already correct

Overall thoughts on PR #56:

This is a substantial, well-executed feature PR. The model swap (Nomic 768-dim → bge-small 384-dim) is handled cleanly — the TEXT-serialised vector column means zero migration, and the mismatch-detection + reindex prompt is a thoughtful UX safety net. The @huggingface/transformers v4 migration is thorough across the pipeline, server, client, types, and tests.

The new surfaces — semantic ⌘K toggle with confidence scores, search_notes_semantic MCP tool, and the UMAP canvas — are well-scoped and isolated. The dynamic imports in graph.ts to defer module evaluation for MCP test compatibility is a smart, targeted fix. The sharp stub promotion from null → a throwing function is the right call for transformers.js's truthy check.

The only outstanding pre-merge items remain operational (not code issues): npm run lint, license regeneration for the @xenova/transformers → @huggingface/transformers swap, and npm run test:e2e. Approving now.

(ミ ᵕ̤ ฅ)⋆* 🐇✨

[approve]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant