Repository navigation
Expand file tree
/
Copy pathprovider.ts
More file actions
288 lines (249 loc) · 9.4 KB
/
Copy pathprovider.ts
File metadata and controls
288 lines (249 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import * as path from "path"
import * as vscode from "vscode"
import { IndexingState } from "../interfaces/manager"
import { VectorStoreSearchResult } from "../interfaces/vector-store"
import { CodeIndexStateManager } from "../state-manager"
import { SembleCLI } from "./semble-cli"
import { downloadSemble, isSembleSupportedPlatform, SEMBLE_VERSION } from "./semble-downloader"
import { ISembleProvider, SembleConfig, SembleContentType, SembleSearchResult, SEMBLE_DEFAULTS } from "./types"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName } from "@roo-code/types"
import { t } from "../../../i18n"
/**
* Orchestrates code search via the semble CLI.
*
* Semble indexes on-the-fly with each search call — there is no separate
* "indexing" step. The provider automatically downloads the semble binary
* on first use, then delegates search queries to `semble search`.
*
* When `embedderProvider === "semble"`, the CodeIndexManager delegates
* to this provider instead of the ServiceFactory → orchestrator pipeline.
*/
export class SembleProvider implements ISembleProvider {
private cli!: SembleCLI
private readonly workspacePath: string
private readonly config: SembleConfig
private readonly stateManager: CodeIndexStateManager
private readonly context: vscode.ExtensionContext
private _state: IndexingState = "Standby"
private _isInitialized = false
private _initPromise: Promise<void> | undefined
constructor(
workspacePath: string,
context: vscode.ExtensionContext,
stateManager: CodeIndexStateManager,
options?: { topK?: number; content?: SembleContentType },
) {
this.workspacePath = workspacePath
this.context = context
this.stateManager = stateManager
this.config = {
topK: options?.topK ?? SEMBLE_DEFAULTS.DEFAULT_TOP_K,
content: options?.content ?? SEMBLE_DEFAULTS.DEFAULT_CONTENT,
}
}
get state(): IndexingState {
return this._state
}
/**
* Initializes the provider: downloads semble, then validates it works.
* Uses an _initPromise to prevent concurrent initialization races.
*/
async initialize(): Promise<void> {
if (this._isInitialized) {
return
}
// If initialization is already in progress, wait for it
if (this._initPromise) {
return this._initPromise
}
this._initPromise = this._doInitialize()
try {
await this._initPromise
} finally {
this._initPromise = undefined
}
}
/**
* Internal initialization logic, called only once via _initPromise guard.
*/
private async _doInitialize(): Promise<void> {
// Check platform support
if (!isSembleSupportedPlatform()) {
this._state = "Error"
this.stateManager.setSystemState(
"Error",
t("embeddings:semble.unsupportedPlatform", { platform: process.platform, arch: process.arch }),
)
console.error(`[SembleProvider] Unsupported platform: ${process.platform}-${process.arch}`)
return
}
// Download semble binary
try {
this.stateManager.setSystemState("Indexing", t("embeddings:semble.downloadingBinary"))
const storageDir = this.context.globalStorageUri.fsPath
const binaryPath = await downloadSemble(storageDir)
if (!binaryPath) {
throw new Error("Download returned no path")
}
this.cli = new SembleCLI(binaryPath)
} catch (error: any) {
this._state = "Error"
this.stateManager.setSystemState(
"Error",
t("embeddings:semble.downloadFailed", { errorMessage: error?.message || error }),
)
console.error("[SembleProvider] Download failed:", error?.message || error)
return
}
// Verify the binary works
const checkResult = await this.cli.checkInstalled()
if (!checkResult.installed) {
const errorMsg = checkResult.error || "Semble binary is not functional"
this._state = "Error"
this.stateManager.setSystemState("Error", t("embeddings:semble.checkFailed", { errorMessage: errorMsg }))
console.error("[SembleProvider] Semble check failed:", errorMsg)
return
}
console.log("[SembleProvider] Semble found and ready.")
// Semble indexes on-the-fly, so we mark as "Indexed" (ready for search).
// The version is included in the status message so the UI (CodeIndexPopover)
// surfaces which semble release is active.
this._state = "Indexed"
this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready", { version: SEMBLE_VERSION }))
this._isInitialized = true
}
/**
* Starts indexing. Since semble indexes on-the-fly with each search,
* this just validates the installation and marks as ready.
*/
async startIndexing(): Promise<void> {
if (!this._isInitialized) {
await this.initialize()
}
if (this._state === "Error") {
return
}
// Semble indexes on-the-fly — no separate indexing step needed.
// Mark as indexed/ready.
this._state = "Indexed"
this.stateManager.setSystemState("Indexed", t("embeddings:semble.ready", { version: SEMBLE_VERSION }))
}
/**
* Stops indexing (no-op — semble has no background indexing process).
*/
stopIndexing(): void {
// No-op: semble indexes on-the-fly per search call
}
/**
* Searches the codebase using `semble search`.
*
* Always searches the full workspace root to avoid creating separate
* Semble cache directories for each subdirectory. When directoryPrefix
* is provided, results are filtered post-search to only include files
* within that directory.
*/
async searchIndex(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
if (!this._isInitialized) {
console.warn("[SembleProvider] searchIndex called before initialization")
return []
}
if (this._state === "Error") {
return []
}
try {
// Always search the full workspace to maintain a single Semble cache.
// Semble creates a separate cache directory per path (SHA-256 of the
// resolved absolute path), so passing subdirectories would create
// redundant indexes and waste disk space.
console.log(`[SembleProvider] Searching in ${this.workspacePath}`)
const results = await this.cli.search(query, this.workspacePath, {
topK: this.config.topK,
content: this.config.content,
})
// Semble returns file paths relative to the search path (workspace root).
// We join against workspacePath to produce correct absolute paths.
let converted = this._convertResults(results, this.workspacePath)
// Filter results to the requested directory prefix, if any.
if (directoryPrefix) {
const normalizedPrefix = path.join(this.workspacePath, directoryPrefix).replace(/\\/g, "/")
converted = converted.filter((r) => {
const filePath = (r.payload?.filePath ?? "").replace(/\\/g, "/")
return filePath.startsWith(normalizedPrefix + "/") || filePath === normalizedPrefix
})
console.log(
`[SembleProvider] Filtered to "${directoryPrefix}": ${converted.length} of ${results.length} results`,
)
}
console.log(
`[SembleProvider] Search returned ${converted.length} results (raw: ${results.length}). Sample path: ${converted[0]?.payload?.filePath ?? "none"}`,
)
return converted
} catch (error: any) {
const errorMessage = error?.message || String(error)
console.error("[SembleProvider] Search failed:", errorMessage)
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
location: "SembleProvider.searchIndex",
})
return []
}
}
/**
* Clears index data. Semble manages its own cache at ~/Library/Caches/semble/
* (or equivalent per-platform). This resets the provider state but does not
* delete semble's on-disk cache — use `semble clear-cache` for that.
*/
async clearIndexData(): Promise<void> {
this._state = "Standby"
this.stateManager.setSystemState("Standby", t("embeddings:semble.providerReset"))
}
/**
* Disposes resources.
*/
dispose(): void {
this._isInitialized = false
}
// --- Private Helpers ---
/**
* Converts Semble CLI results to Zoo's VectorStoreSearchResult format.
*
* Semble v0.4.0+ returns results in a flat format (no `chunk` wrapper):
* { file_path, start_line, end_line, score, content }
*
* Note: semble returns file paths relative to the path it was invoked with.
* We join against `basePath` (the actual path passed to semble) to produce
* correct absolute paths for the rest of the pipeline.
* Results with missing file paths or paths that escape the workspace are excluded.
*/
private _convertResults(results: SembleSearchResult[], basePath: string): VectorStoreSearchResult[] {
// Resolve basePath to an absolute canonical form for the traversal check.
const resolvedBase = path.resolve(basePath).replace(/\\/g, "/")
const converted: VectorStoreSearchResult[] = []
for (const [index, r] of results.entries()) {
if (!r.file_path) {
continue
}
// Use path.join for the displayed path (preserves basePath format).
const filePath = path.join(basePath, r.file_path).replace(/\\/g, "/")
// Use path.resolve to normalize any ../ for the security check.
const resolvedFilePath = path.resolve(basePath, r.file_path).replace(/\\/g, "/")
// Guard against path traversal: reject file paths that resolve outside the workspace
if (!resolvedFilePath.startsWith(resolvedBase + "/") && resolvedFilePath !== resolvedBase) {
continue
}
converted.push({
id: `semble-${index}`,
score: r.score,
payload: {
filePath,
codeChunk: r.content ?? "",
startLine: r.start_line ?? 0,
endLine: r.end_line ?? 0,
},
})
}
return converted
}
}