Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions docs/tools/code_search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# code_search

Semantic code search using vector embeddings.

Finds code snippets by meaning, not just text matching. Use when you need to find code by concept (e.g., "authentication middleware", "database connection pool") rather than exact strings.

## Prerequisites

- Enable **Code Search (Zvec)** in `/settings` under Tools → Available Tools.
- Install `@zvec/zvec`: `bun add @zvec/zvec`
- Run `/code-index` to build the index before searching. The tool does **not** auto-index on first use — it only searches the existing index.

## Parameters

- `query` (string, required): Natural language search query.
- `pattern` (string, optional): File glob pattern to filter results (e.g. `*.ts`).
- `topK` (number, optional): Max results to return (default 20).

## Behavior

Returns matching code chunks with file paths, line numbers, and relevance scores. Supports hybrid (vector + FTS) search when embeddings are available, falling back to FTS-only mode otherwise.

If the index is empty, the tool returns a message telling you to run `/code-index`.

Prefer `grep` for exact string/regex matching. Use `code_search` when you need semantic understanding of what the code does.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@types/turndown": "5.0.6",
"@typescript/native-preview": "7.0.0-dev.20260609.1",
"@xterm/headless": "^6.0.0",
"@zvec/zvec": "0.5.0",
"arktype": "2.2.2",
"chalk": "^5.6.2",
"chart.js": "^4.5.1",
Expand Down Expand Up @@ -96,6 +97,9 @@
"overrides": {
"@ark/schema": "0.56.1"
},
"trustedDependencies": [
"@zvec/zvec"
],
"scripts": {
"setup": "bun install && bun run build:native && bun --cwd=packages/coding-agent link && sh scripts/link-omp.sh",
"dev": "bun --cwd=packages/coding-agent src/cli.ts",
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- Added model-keyed fallback management to the /models Roles view: model and `provider/*` chains render as a separate section below the roles (divider + "+ New fallback…" row for creating one by picking the protected model, then keying it by model or provider), with the same replace/remove/reorder editing as role chains; the model strip gains `fallbacks:<model>` and `fallbacks:<provider>/*` chips as shortcuts.
- Added `/queue <message>` plus `->` / `=>` composer shorthand for follow-up messages that wait until the agent yields. The shorthand opens a dim `Queueing` header and splits sequential numeric, Roman-numeral, or alphabetic lists into separately highlighted queue entries.
- Added per-model TPS/TTFT tracking: every completed assistant turn folds its timing into recency-weighted aggregates in `~/.omp/agent.db`, and the /models browser shows measured speed — a right-aligned `118t/s` column on wide terminals (plus TTFT, e.g. `0.9s 118t/s`, when wider) and `~118t/s · 0.9s ttft` facts in the selection detail line — with no dependency on the `omp stats` session scan.
- Added `code_search` tool for semantic code search using Zvec in-process vector database with HNSW indexing and BM25 full-text search; indexes the workspace on first use and supports hybrid (vector + FTS) search when embeddings are available, falling back to FTS-only mode otherwise.
- Added `tools.codeSearchEnabled` setting (default: off) to enable/disable semantic code search in the /settings panel.
- Added `/code-index` slash command to explicitly build or rebuild the Zvec code search index; the `code_search` tool no longer auto-indexes on first use.

### Changed

Expand Down
12 changes: 12 additions & 0 deletions packages/coding-agent/src/config/settings-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3644,6 +3644,18 @@ export const SETTINGS_SCHEMA = {
},
},

"tools.codeSearchEnabled": {
type: "boolean",
default: false,
ui: {
tab: "tools",
group: "Available Tools",
label: "Code Search (Zvec)",
description:
"Enable semantic code search using Zvec in-process vector database. Use /code-index to build the index before searching.",
},
},

"browser.enabled": {
type: "boolean",
default: true,
Expand Down
7 changes: 7 additions & 0 deletions packages/coding-agent/src/prompts/tools/code-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Semantic code search using vector embeddings.

Finds code snippets by meaning, not just text matching. Use when you need to find code by concept (e.g., "authentication middleware", "database connection pool") rather than exact strings.

Returns matching code chunks with file paths, line numbers, and relevance scores. The index must be built before searching — if the index is empty, the tool returns a message telling you to run `/code-index` first. Use `/code-index` to build or rebuild the index.

Prefer `grep` for exact string/regex matching. Use `code_search` when you need semantic understanding of what the code does.
60 changes: 60 additions & 0 deletions packages/coding-agent/src/slash-commands/builtin-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
MarketplaceManager,
} from "../extensibility/plugins/marketplace";
import { resolveMemoryBackend } from "../memory-backend";
import { loadMnemopi } from "../mnemopi/state";
import { runPauseScreen } from "../modes/components/pause-screen";
import { describeLoopLimitRuntime } from "../modes/loop-limit";
import { theme } from "../modes/theme/theme";
Expand All @@ -33,6 +34,7 @@ import type { AgentSession, FreshSessionResult } from "../session/agent-session"
import { COMPACT_MODES, parseCompactArgs } from "../session/compact-modes";
import { resolveResumableSession } from "../session/session-listing";
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
import { indexWorkspace } from "../tools/code-search-index";
import { expandTilde, resolveToCwd } from "../tools/path-utils";
import { urlHyperlinkAlways } from "../tui";
import {
Expand Down Expand Up @@ -1159,6 +1161,64 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
runtime.ctx.editor.setText("");
},
},
{
name: "code-index",
description: "Index the workspace for semantic code search (Zvec)",
inlineHint: "[rebuild]",
allowArgs: true,
getTuiAutocompleteDescription: runtime => {
if (!runtime.ctx.settings.get("tools.codeSearchEnabled")) {
return "Code Search: disabled in settings";
}
return "Code Search: index workspace";
},
handleTui: async (_command, runtime) => {
const enabled = runtime.ctx.settings.get("tools.codeSearchEnabled");
if (!enabled) {
runtime.ctx.showWarning("Code search is disabled. Enable it in /settings under Tools → Available Tools.");
runtime.ctx.editor.setText("");
return;
}
const { ZVecCodeStore } = await loadMnemopi();
if (!ZVecCodeStore.available()) {
runtime.ctx.showWarning("Code search requires @zvec/zvec. Install it with: bun add @zvec/zvec");
runtime.ctx.editor.setText("");
return;
}
runtime.ctx.showStatus("Indexing workspace for code search...");
runtime.ctx.editor.setText("");
try {
const result = await indexWorkspace(runtime.ctx.session.sessionManager.getCwd());
runtime.ctx.showStatus(
`Code search indexed: ${result.filesIndexed} file${result.filesIndexed === 1 ? "" : "s"}, ${result.chunksIndexed} chunk${result.chunksIndexed === 1 ? "" : "s"} in ${result.duration}ms`,
);
} catch (err) {
runtime.ctx.showError(`Code search indexing failed: ${String(err)}`);
}
},
handle: async (_command, runtime) => {
const enabled = runtime.settings.get("tools.codeSearchEnabled");
if (!enabled) {
await runtime.output("Code search is disabled. Enable it in /settings under Tools → Available Tools.");
return commandConsumed();
}
const { ZVecCodeStore } = await loadMnemopi();
if (!ZVecCodeStore.available()) {
await runtime.output("Code search requires @zvec/zvec. Install it with: bun add @zvec/zvec");
return commandConsumed();
}
await runtime.output("Indexing workspace for code search...");
try {
const result = await indexWorkspace(runtime.cwd);
await runtime.output(
`Code search indexed: ${result.filesIndexed} file${result.filesIndexed === 1 ? "" : "s"}, ${result.chunksIndexed} chunk${result.chunksIndexed === 1 ? "" : "s"} in ${result.duration}ms`,
);
} catch (err) {
await runtime.output(`Code search indexing failed: ${String(err)}`);
}
return commandConsumed();
},
},
{
name: "context",
description: "Show estimated context usage breakdown",
Expand Down
101 changes: 101 additions & 0 deletions packages/coding-agent/src/tools/__tests__/code-search-index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { afterEach, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import * as path from "node:path";
import { type CodeChunk, ZVecCodeStore } from "@oh-my-pi/pi-mnemopi";

// Zvec native FFI calls (upsert/optimize/searchFts) are slow on CI hardware.
setDefaultTimeout(15_000);

describe.skipIf(!ZVecCodeStore.available())("ZVecCodeStore edit-then-reindex", () => {
let tempDir: string;
let store: ZVecCodeStore;

beforeEach(async () => {
tempDir = await mkdtemp(path.join(tmpdir(), "zvec-code-search-"));
// ZVecCreateAndOpen requires a path that does not yet exist.
store = ZVecCodeStore.create(path.join(tempDir, "index"), 384);
});

afterEach(async () => {
store.close();
await rm(tempDir, { recursive: true, force: true });
});

function makeChunk(filePath: string, content: string, fileHash: string): CodeChunk {
return {
id: `${fileHash}-1`,
filePath,
language: "typescript",
lineStart: 1,
lineEnd: 1,
content,
chunkHash: Bun.hash(content).toString(16),
embedding: [],
};
}

test("upserting new chunks after removeFile does not leave stale chunks", () => {
const filePath = "src/test.ts";
const oldContent = "function foo() { return 1; }";
const newContent = "function bar() { return 2; }";

// Step 1: Upsert initial chunk.
const oldHash = Bun.hash(`${filePath}\0${oldContent}`).toString(16);
store.upsertChunks([makeChunk(filePath, oldContent, oldHash)]);
store.optimize();
expect(store.docCount).toBe(1);

// Step 2: Verify search finds the old content.
const fooResults = store.searchFts("foo", 10);
expect(fooResults.length).toBe(1);
expect(fooResults[0]!.content).toContain("foo");

// Step 3: Simulate an edit — remove old chunks, then upsert new ones.
store.removeFile(filePath);
const newHash = Bun.hash(`${filePath}\0${newContent}`).toString(16);
store.upsertChunks([makeChunk(filePath, newContent, newHash)]);
store.optimize();

// Step 4: docCount should be 1 (not 2 — no stale chunks).
expect(store.docCount).toBe(1);

// Step 5: Search for "foo" should return nothing (old chunk deleted).
const staleResults = store.searchFts("foo", 10);
expect(staleResults.length).toBe(0);

// Step 6: Search for "bar" should return the new chunk.
const barResults = store.searchFts("bar", 10);
expect(barResults.length).toBe(1);
expect(barResults[0]!.content).toContain("bar");
expect(barResults[0]!.filePath).toBe(filePath);
});

test("getFileHashes reflects current chunks after re-indexing", () => {
const filePath = "src/utils.ts";
const oldContent = "export function add(a, b) { return a + b; }";
const newContent = "export function multiply(a, b) { return a * b; }";

// Index initial content.
const oldHash = Bun.hash(`${filePath}\0${oldContent}`).toString(16);
store.upsertChunks([makeChunk(filePath, oldContent, oldHash)]);
store.optimize();

let hashes = store.getFileHashes();
expect(hashes.has(filePath)).toBe(true);
expect(hashes.get(filePath)!.size).toBe(1);

// Re-index with changed content.
store.removeFile(filePath);
const newHash = Bun.hash(`${filePath}\0${newContent}`).toString(16);
store.upsertChunks([makeChunk(filePath, newContent, newHash)]);
store.optimize();

// getFileHashes should reflect only the new chunk hash.
hashes = store.getFileHashes();
expect(hashes.get(filePath)!.size).toBe(1);
const currentHashes = hashes.get(filePath)!;
const newChunkHash = Bun.hash(newContent).toString(16);
expect(currentHashes.has(newChunkHash)).toBe(true);
});
});
1 change: 1 addition & 0 deletions packages/coding-agent/src/tools/builtin-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const BUILTIN_TOOL_NAMES = [
"web_search",
"search_tool_bm25",
"write",
"code_search",
"memory_edit",
"retain",
"recall",
Expand Down
Loading
Loading