Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b7c5bf1
feat(tool): codebase search tool
mikij Feb 11, 2026
7f51f80
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 12, 2026
d5de8b0
refactor(tool): extract codebase-search logic to Kilo-specific module
mikij Feb 12, 2026
e529bd6
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 12, 2026
127e747
refactor(tool): change codebase-search identifiers to snake_case
mikij Feb 12, 2026
bc2cc46
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 17, 2026
a8be7ea
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 17, 2026
708f008
feat(cli): add codebase search tool configuration
mikij Feb 17, 2026
3259a9f
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 18, 2026
03d2cd2
test(codebase-search): add unit tests for formatResults helper
mikij Feb 18, 2026
24cc129
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 18, 2026
481ace5
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 19, 2026
cc7e1be
perf(codebase-search): filter search results in-memory after database…
mikij Feb 19, 2026
9e2f5d4
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 19, 2026
5314333
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 19, 2026
ae56156
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 19, 2026
81a1372
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 19, 2026
4d564b9
fix: exit process when stdin closes in non-interactive mode
mikij Feb 19, 2026
af9000d
fix(config): improve config cache clearing and fix codebase-search to…
mikij Feb 19, 2026
d4ee793
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 19, 2026
0c88f63
refactor(embeddings): add defensive checks for embedding responses
mikij Feb 19, 2026
16e78af
feat(codebase-search): filter and limit search results before formatting
mikij Feb 19, 2026
2c692ab
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 19, 2026
fd623f2
refactor(codebase-search): simplify formatResults by removing filteri…
mikij Feb 19, 2026
06c5bd7
refactor: remove clearAll function and unused Config import
mikij Feb 19, 2026
7284825
Merge branch 'dev' into feat/codebase-search-tool
mikij Feb 20, 2026
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
1 change: 1 addition & 0 deletions bun.lock

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@kilocode/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@types/bun": "1.3.5",
"typescript": "catalog:"
},
"repository": {
Expand Down
32 changes: 32 additions & 0 deletions packages/opencode/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,35 @@ Hono-based HTTP server with OpenAPI spec generation. SSE for real-time events. W
## Providers and Models

Uses the **Vercel AI SDK** as the abstraction layer. Providers are loaded from a bundled map or dynamically installed at runtime. Models come from models.dev (external API), cached locally.

## Tools

### codebase_search

Requires configuration in `opencode.json`:

```jsonc
{
"provider": {
"kilo": {
"options": {
"codebase_search": {
"embedModel": "codestral-embed-2505", // or "text-embedding-3-small" for OpenAI, "nomic-embed-text" for Ollama
"vectorDb": {
"type": "qdrant",
"url": "http://localhost:6333"
},
"similarityThreshold": 0.4,
"maxResults": 50
}
}
}
}
}
```

- **Embedding providers supported**: OpenAI (text-embedding-3-small), Mistral (codestral-embed-2505), Ollama (nomic-embed-text)
- **Vector database supported**: Qdrant
- **API keys**: Configure in auth settings (`~/.local/share/kilo/auth.json`) for openai, mistral, qdrant
- **Collection name**: Automatically generated from workspace path using SHA-256 hash
- **Requirements**: Qdrant running and accessible, collection exists with indexed data
9 changes: 9 additions & 0 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { WebFetchTool } from "../../tool/webfetch"
import { EditTool } from "../../tool/edit"
import { WriteTool } from "../../tool/write"
import { CodeSearchTool } from "../../tool/codesearch"
import { CodebaseSearchTool } from "../../tool/codebase-search"
import { WebSearchTool } from "../../tool/websearch"
import { TaskTool } from "../../tool/task"
import { SkillTool } from "../../tool/skill"
Expand Down Expand Up @@ -159,6 +160,13 @@ function codesearch(info: ToolProps<typeof CodeSearchTool>) {
})
}

function codebasesearch(info: ToolProps<typeof CodebaseSearchTool>) {
inline({
icon: "◐",
title: `Codebase Search "${info.input.query}"`,
})
}

function websearch(info: ToolProps<typeof WebSearchTool>) {
inline({
icon: "◈",
Expand Down Expand Up @@ -408,6 +416,7 @@ export const RunCommand = cmd({
if (part.tool === "webfetch") return webfetch(props<typeof WebFetchTool>(part))
if (part.tool === "edit") return edit(props<typeof EditTool>(part))
if (part.tool === "codesearch") return codesearch(props<typeof CodeSearchTool>(part))
if (part.tool === "codebase_search") return codebasesearch(props<typeof CodebaseSearchTool>(part))
if (part.tool === "websearch") return websearch(props<typeof WebSearchTool>(part))
if (part.tool === "task") return task(props<typeof TaskTool>(part))
if (part.tool === "todowrite") return todo(props<typeof TodoWriteTool>(part))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
// kilocode_change - new file
import { TextAttributes, InputRenderable } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useDialog } from "@tui/ui/dialog"
import { createStore } from "solid-js/store"
import { createEffect, onMount, createSignal, Show } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { CODEBASE_SEARCH_DEFAULTS } from "@/kilocode/codebase-search/types"

export type CodebaseSearchConfig = {
embedModel: string
vectorDbType: "qdrant" | "lancedb"
qdrantUrl: string
lancedbPath: string
similarityThreshold: number
maxResults: number
}

export type DialogToolCodebaseSearchProps = {
initialConfig?: Partial<CodebaseSearchConfig>
onSave: (config: CodebaseSearchConfig) => void
onCancel?: () => void
}

type FieldKey = "embedModel" | "vectorDbType" | "qdrantUrl" | "lancedbPath" | "similarityThreshold" | "maxResults"

export function DialogToolCodebaseSearch(props: DialogToolCodebaseSearchProps) {
const dialog = useDialog()
const { theme } = useTheme()
const [store, setStore] = createStore<CodebaseSearchConfig>({
embedModel: props.initialConfig?.embedModel ?? CODEBASE_SEARCH_DEFAULTS.defaultEmbedModel,
vectorDbType: props.initialConfig?.vectorDbType ?? "qdrant",
qdrantUrl: props.initialConfig?.qdrantUrl ?? CODEBASE_SEARCH_DEFAULTS.defaultQdrantUrl,
lancedbPath: props.initialConfig?.lancedbPath ?? "",
similarityThreshold: props.initialConfig?.similarityThreshold ?? CODEBASE_SEARCH_DEFAULTS.similarityThreshold,
maxResults: props.initialConfig?.maxResults ?? CODEBASE_SEARCH_DEFAULTS.maxResults,
})

const [activeField, setActiveField] = createSignal<number>(0)

// Track raw string values for number fields during editing
// This prevents "0." from becoming "0" when user is still typing
const [rawNumberValues, setRawNumberValues] = createStore<Record<string, string>>({
similarityThreshold: String(store.similarityThreshold),
maxResults: String(store.maxResults),
})

let inputs: (InputRenderable | undefined)[] = []
let scrollboxRef: any

dialog.setSize("large")

const fields: { key: FieldKey; label: string; placeholder: string; type: "text" | "number" | "select" }[] = [
{ key: "embedModel", label: "Embed Model", placeholder: "e.g., codestral-embed-2505", type: "text" },
{ key: "vectorDbType", label: "Vector DB Type", placeholder: "qdrant or lancedb", type: "select" },
{ key: "qdrantUrl", label: "Qdrant URL", placeholder: "http://localhost:6333", type: "text" },
{
key: "lancedbPath",
label: "LanceDB Vector Store Path",
placeholder: "Custom vector store path (optional)",
type: "text",
},
{ key: "similarityThreshold", label: "Similarity Threshold", placeholder: "0.0 - 1.0", type: "number" },
{ key: "maxResults", label: "Max Results", placeholder: "1 - 100", type: "number" },
]

// Get visible fields based on current config
const visibleFields = () => {
const result: typeof fields = []
for (const field of fields) {
// Only show Qdrant URL if vectorDbType is qdrant
if (field.key === "qdrantUrl" && store.vectorDbType !== "qdrant") continue
// Only show LanceDB path if vectorDbType is lancedb
if (field.key === "lancedbPath" && store.vectorDbType !== "lancedb") continue
result.push(field)
}
return result
}

onMount(() => {
setTimeout(() => {
const visible = visibleFields()
const input = inputs[0]
if (input && !input.isDestroyed && visible.length > 0) {
input.focus()
}
}, 1)
})

createEffect(() => {
const idx = activeField()
const visible = visibleFields()
if (idx >= 0 && idx < visible.length) {
const field = visible[idx]
const actualIdx = fields.findIndex((f) => f.key === field.key)
const input = inputs[actualIdx]
if (input && !input.isDestroyed) {
input.focus()
}
}
})

// Commit all input values to store before saving
function commitAllValues() {
for (const field of fields) {
if (field.key === "vectorDbType") continue

if (field.type === "number") {
// Use raw number value for parsing
const rawValue = rawNumberValues[field.key]
if (rawValue !== undefined) {
const val = parseFloat(rawValue)
if (!isNaN(val)) {
setStore(field.key, val)
}
}
} else {
const actualIdx = fields.findIndex((f) => f.key === field.key)
const input = inputs[actualIdx]
if (input && !input.isDestroyed) {
setStore(field.key, input.value)
}
}
}
}

useKeyboard((evt) => {
const visible = visibleFields()
const currentField = visible[activeField()]

// Enter on select field toggles the value
if (evt.name === "return" && currentField?.key === "vectorDbType") {
evt.preventDefault()
toggleVectorDb()
return
}

// Enter on any other field saves the form
if (evt.name === "return") {
evt.preventDefault()
commitAllValues()
handleSave()
return
}

if (evt.name === "tab") {
evt.preventDefault()
const direction = evt.shift ? -1 : 1
let next = activeField() + direction
if (next < 0) next = visible.length - 1
if (next >= visible.length) next = 0
setActiveField(next)
}

if (evt.name === "up") {
evt.preventDefault()
const next = activeField() - 1
if (next >= 0) setActiveField(next)
}

if (evt.name === "down") {
evt.preventDefault()
const next = activeField() + 1
if (next < visible.length) setActiveField(next)
}

// Space on select field toggles the value
if (evt.name === "space" && currentField?.key === "vectorDbType") {
evt.preventDefault()
toggleVectorDb()
return
}

if (evt.name === "escape") {
props.onCancel?.()
dialog.clear()
}
})

function handleSave() {
props.onSave(store)
dialog.clear()
}

function toggleVectorDb() {
setStore("vectorDbType", store.vectorDbType === "qdrant" ? "lancedb" : "qdrant")
}

return (
<box paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Codebase Search Configuration
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.textMuted}>Configure semantic code search settings</text>
</box>

<scrollbox ref={scrollboxRef} maxHeight={12} scrollbarOptions={{ visible: false }}>
<box flexDirection="column" gap={1}>
{visibleFields().map((field, idx) => {
const actualIdx = fields.findIndex((f) => f.key === field.key)
const isActive = activeField() === idx
return (
<box flexDirection="row" gap={1} alignItems="flex-end">
<text fg={isActive ? theme.text : theme.textMuted} width={18} flexShrink={0}>
{field.label}:
</text>
<Show
when={field.type === "select"}
fallback={
<input
ref={(r) => {
inputs[actualIdx] = r
}}
focusedBackgroundColor={theme.backgroundPanel}
cursorColor={theme.primary}
focusedTextColor={theme.text}
textColor={theme.text}
onInput={(e) => {
if (field.type === "number") {
// Store raw string value to preserve "0." while typing
setRawNumberValues(field.key, e)
// Also parse and store numeric value if valid
const val = parseFloat(e)
if (!isNaN(val)) {
setStore(field.key, val)
}
} else {
setStore(field.key, e)
}
}}
value={
field.type === "number"
? (rawNumberValues[field.key] ?? String(store[field.key as keyof CodebaseSearchConfig]))
: (store[field.key as keyof CodebaseSearchConfig] as string)
}
placeholder={field.placeholder}
flexGrow={1}
maxWidth={70}
/>
}
>
<box backgroundColor={isActive ? theme.primary : undefined} onMouseUp={() => toggleVectorDb()}>
<text fg={isActive ? theme.selectedListItemText : theme.text}>
{store.vectorDbType.toUpperCase()}
</text>
</box>
<text fg={theme.textMuted}> (enter to toggle)</text>
</Show>
</box>
)
})}
</box>
</scrollbox>

<box flexDirection="row" justifyContent="flex-end" paddingBottom={1} paddingTop={1} gap={2}>
<text fg={theme.textMuted}>tab/↑↓</text>
<text fg={theme.textMuted}>navigate</text>
<text fg={theme.textMuted}>|</text>
<text fg={theme.textMuted}>space</text>
<text fg={theme.textMuted}>toggle</text>
<text fg={theme.textMuted}>|</text>
<text fg={theme.textMuted}>enter</text>
<text fg={theme.textMuted}>save</text>
</box>
</box>
)
}
Loading
Loading