Skip to content

Commit d90bab7

Browse files
authored
feat: code indexing support multiple folder similar with task history (RooCodeInc#6204)
* feat: Implement code indexing support multi-folder workspaces similar to task history * fix: add missing mock for onDidChangeActiveTextEditor in tests
1 parent 8a35b64 commit d90bab7

File tree

13 files changed

+189
-63
lines changed

13 files changed

+189
-63
lines changed

src/activate/registerCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit<Registe
237237
mdmService = undefined
238238
}
239239

240-
const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, codeIndexManager, mdmService)
240+
const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, mdmService)
241241
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
242242

243243
// Check if there are any visible text editors, otherwise open a new group

src/core/prompts/__tests__/custom-system-prompt.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
// Mocks must come first, before imports
2+
vi.mock("vscode", () => ({
3+
env: {
4+
language: "en",
5+
},
6+
workspace: {
7+
workspaceFolders: [{ uri: { fsPath: "/test/path" } }],
8+
getWorkspaceFolder: vi.fn().mockReturnValue({ uri: { fsPath: "/test/path" } }),
9+
},
10+
window: {
11+
activeTextEditor: undefined,
12+
},
13+
EventEmitter: vi.fn().mockImplementation(() => ({
14+
event: vi.fn(),
15+
fire: vi.fn(),
16+
dispose: vi.fn(),
17+
})),
18+
}))
19+
220
vi.mock("fs/promises", () => {
321
const mockReadFile = vi.fn()
422
const mockMkdir = vi.fn().mockResolvedValue(undefined)

src/core/prompts/system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ async function generatePrompt(
8585
: Promise.resolve(""),
8686
])
8787

88-
const codeIndexManager = CodeIndexManager.getInstance(context)
88+
const codeIndexManager = CodeIndexManager.getInstance(context, cwd)
8989

9090
const basePrompt = `${roleDefinition}
9191

src/core/webview/ClineProvider.ts

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export class ClineProvider
104104
private view?: vscode.WebviewView | vscode.WebviewPanel
105105
private clineStack: Task[] = []
106106
private codeIndexStatusSubscription?: vscode.Disposable
107+
private currentWorkspaceManager?: CodeIndexManager
107108
private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
108109
public get workspaceTracker(): WorkspaceTracker | undefined {
109110
return this._workspaceTracker
@@ -125,15 +126,13 @@ export class ClineProvider
125126
private readonly outputChannel: vscode.OutputChannel,
126127
private readonly renderContext: "sidebar" | "editor" = "sidebar",
127128
public readonly contextProxy: ContextProxy,
128-
public readonly codeIndexManager?: CodeIndexManager,
129129
mdmService?: MdmService,
130130
) {
131131
super()
132132

133133
this.log("ClineProvider instantiated")
134134
ClineProvider.activeInstances.add(this)
135135

136-
this.codeIndexManager = codeIndexManager
137136
this.mdmService = mdmService
138137
this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES)
139138

@@ -602,16 +601,15 @@ export class ClineProvider
602601
// and executes code based on the message that is received
603602
this.setWebviewMessageListener(webviewView.webview)
604603

605-
// Subscribe to code index status updates if the manager exists
606-
if (this.codeIndexManager) {
607-
this.codeIndexStatusSubscription = this.codeIndexManager.onProgressUpdate((update: IndexProgressUpdate) => {
608-
this.postMessageToWebview({
609-
type: "indexingStatusUpdate",
610-
values: update,
611-
})
612-
})
613-
this.webviewDisposables.push(this.codeIndexStatusSubscription)
614-
}
604+
// Initialize code index status subscription for the current workspace
605+
this.updateCodeIndexStatusSubscription()
606+
607+
// Listen for active editor changes to update code index status for the current workspace
608+
const activeEditorSubscription = vscode.window.onDidChangeActiveTextEditor(() => {
609+
// Update subscription when workspace might have changed
610+
this.updateCodeIndexStatusSubscription()
611+
})
612+
this.webviewDisposables.push(activeEditorSubscription)
615613

616614
// Logs show up in bottom panel > Debug Console
617615
//console.log("registering listener")
@@ -647,8 +645,8 @@ export class ClineProvider
647645
} else {
648646
this.log("Clearing webview resources for sidebar view")
649647
this.clearWebviewResources()
650-
this.codeIndexStatusSubscription?.dispose()
651-
this.codeIndexStatusSubscription = undefined
648+
// Reset current workspace manager reference when view is disposed
649+
this.currentWorkspaceManager = undefined
652650
}
653651
},
654652
null,
@@ -2223,6 +2221,61 @@ export class ClineProvider
22232221
...gitInfo,
22242222
}
22252223
}
2224+
2225+
/**
2226+
* Gets the CodeIndexManager for the current active workspace
2227+
* @returns CodeIndexManager instance for the current workspace or the default one
2228+
*/
2229+
public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined {
2230+
return CodeIndexManager.getInstance(this.context)
2231+
}
2232+
2233+
/**
2234+
* Updates the code index status subscription to listen to the current workspace manager
2235+
*/
2236+
private updateCodeIndexStatusSubscription(): void {
2237+
// Get the current workspace manager
2238+
const currentManager = this.getCurrentWorkspaceCodeIndexManager()
2239+
2240+
// If the manager hasn't changed, no need to update subscription
2241+
if (currentManager === this.currentWorkspaceManager) {
2242+
return
2243+
}
2244+
2245+
// Dispose the old subscription if it exists
2246+
if (this.codeIndexStatusSubscription) {
2247+
this.codeIndexStatusSubscription.dispose()
2248+
this.codeIndexStatusSubscription = undefined
2249+
}
2250+
2251+
// Update the current workspace manager reference
2252+
this.currentWorkspaceManager = currentManager
2253+
2254+
// Subscribe to the new manager's progress updates if it exists
2255+
if (currentManager) {
2256+
this.codeIndexStatusSubscription = currentManager.onProgressUpdate((update: IndexProgressUpdate) => {
2257+
// Only send updates if this manager is still the current one
2258+
if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) {
2259+
// Get the full status from the manager to ensure we have all fields correctly formatted
2260+
const fullStatus = currentManager.getCurrentStatus()
2261+
this.postMessageToWebview({
2262+
type: "indexingStatusUpdate",
2263+
values: fullStatus,
2264+
})
2265+
}
2266+
})
2267+
2268+
if (this.view) {
2269+
this.webviewDisposables.push(this.codeIndexStatusSubscription)
2270+
}
2271+
2272+
// Send initial status for the current workspace
2273+
this.postMessageToWebview({
2274+
type: "indexingStatusUpdate",
2275+
values: currentManager.getCurrentStatus(),
2276+
})
2277+
}
2278+
}
22262279
}
22272280

22282281
class OrganizationAllowListViolationError extends Error {

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ vi.mock("vscode", () => ({
147147
showInformationMessage: vi.fn(),
148148
showWarningMessage: vi.fn(),
149149
showErrorMessage: vi.fn(),
150+
onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })),
150151
},
151152
workspace: {
152153
getConfiguration: vi.fn().mockReturnValue({

src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ vi.mock("vscode", () => ({
2727
showInformationMessage: vi.fn(),
2828
showWarningMessage: vi.fn(),
2929
showErrorMessage: vi.fn(),
30+
onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })),
3031
},
3132
workspace: {
3233
getConfiguration: vi.fn().mockReturnValue({

src/core/webview/webviewMessageHandler.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { checkExistKey } from "../../shared/checkExistApiConfig"
2929
import { experimentDefault } from "../../shared/experiments"
3030
import { Terminal } from "../../integrations/terminal/Terminal"
3131
import { openFile } from "../../integrations/misc/open-file"
32+
import { CodeIndexManager } from "../../services/code-index/manager"
3233
import { openImage, saveImage } from "../../integrations/misc/image-handler"
3334
import { selectImages } from "../../integrations/misc/process-images"
3435
import { getTheme } from "../../integrations/theme/getTheme"
@@ -2062,13 +2063,14 @@ export const webviewMessageHandler = async (
20622063
// Update webview state
20632064
await provider.postStateToWebview()
20642065

2065-
// Then handle validation and initialization
2066-
if (provider.codeIndexManager) {
2066+
// Then handle validation and initialization for the current workspace
2067+
const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()
2068+
if (currentCodeIndexManager) {
20672069
// If embedder provider changed, perform proactive validation
20682070
if (embedderProviderChanged) {
20692071
try {
20702072
// Force handleSettingsChange which will trigger validation
2071-
await provider.codeIndexManager.handleSettingsChange()
2073+
await currentCodeIndexManager.handleSettingsChange()
20722074
} catch (error) {
20732075
// Validation failed - the error state is already set by handleSettingsChange
20742076
provider.log(
@@ -2077,15 +2079,15 @@ export const webviewMessageHandler = async (
20772079
// Send validation error to webview
20782080
await provider.postMessageToWebview({
20792081
type: "indexingStatusUpdate",
2080-
values: provider.codeIndexManager.getCurrentStatus(),
2082+
values: currentCodeIndexManager.getCurrentStatus(),
20812083
})
20822084
// Exit early - don't try to start indexing with invalid configuration
20832085
break
20842086
}
20852087
} else {
20862088
// No provider change, just handle settings normally
20872089
try {
2088-
await provider.codeIndexManager.handleSettingsChange()
2090+
await currentCodeIndexManager.handleSettingsChange()
20892091
} catch (error) {
20902092
// Log but don't fail - settings are saved
20912093
provider.log(
@@ -2098,10 +2100,10 @@ export const webviewMessageHandler = async (
20982100
await new Promise((resolve) => setTimeout(resolve, 200))
20992101

21002102
// Auto-start indexing if now enabled and configured
2101-
if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) {
2102-
if (!provider.codeIndexManager.isInitialized) {
2103+
if (currentCodeIndexManager.isFeatureEnabled && currentCodeIndexManager.isFeatureConfigured) {
2104+
if (!currentCodeIndexManager.isInitialized) {
21032105
try {
2104-
await provider.codeIndexManager.initialize(provider.contextProxy)
2106+
await currentCodeIndexManager.initialize(provider.contextProxy)
21052107
provider.log(`Code index manager initialized after settings save`)
21062108
} catch (error) {
21072109
provider.log(
@@ -2110,7 +2112,7 @@ export const webviewMessageHandler = async (
21102112
// Send error status to webview
21112113
await provider.postMessageToWebview({
21122114
type: "indexingStatusUpdate",
2113-
values: provider.codeIndexManager.getCurrentStatus(),
2115+
values: currentCodeIndexManager.getCurrentStatus(),
21142116
})
21152117
}
21162118
}
@@ -2141,7 +2143,7 @@ export const webviewMessageHandler = async (
21412143
}
21422144

21432145
case "requestIndexingStatus": {
2144-
const manager = provider.codeIndexManager
2146+
const manager = provider.getCurrentWorkspaceCodeIndexManager()
21452147
if (!manager) {
21462148
// No workspace open - send error status
21472149
provider.postMessageToWebview({
@@ -2152,11 +2154,23 @@ export const webviewMessageHandler = async (
21522154
processedItems: 0,
21532155
totalItems: 0,
21542156
currentItemUnit: "items",
2157+
workerspacePath: undefined,
21552158
},
21562159
})
21572160
return
21582161
}
2159-
const status = manager.getCurrentStatus()
2162+
2163+
const status = manager
2164+
? manager.getCurrentStatus()
2165+
: {
2166+
systemStatus: "Standby",
2167+
message: "No workspace folder open",
2168+
processedItems: 0,
2169+
totalItems: 0,
2170+
currentItemUnit: "items",
2171+
workspacePath: undefined,
2172+
}
2173+
21602174
provider.postMessageToWebview({
21612175
type: "indexingStatusUpdate",
21622176
values: status,
@@ -2187,7 +2201,7 @@ export const webviewMessageHandler = async (
21872201
}
21882202
case "startIndexing": {
21892203
try {
2190-
const manager = provider.codeIndexManager
2204+
const manager = provider.getCurrentWorkspaceCodeIndexManager()
21912205
if (!manager) {
21922206
// No workspace open - send error status
21932207
provider.postMessageToWebview({
@@ -2217,7 +2231,7 @@ export const webviewMessageHandler = async (
22172231
}
22182232
case "clearIndexData": {
22192233
try {
2220-
const manager = provider.codeIndexManager
2234+
const manager = provider.getCurrentWorkspaceCodeIndexManager()
22212235
if (!manager) {
22222236
provider.log("Cannot clear index data: No workspace folder open")
22232237
provider.postMessageToWebview({

src/extension.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,24 @@ export async function activate(context: vscode.ExtensionContext) {
9393
}
9494

9595
const contextProxy = await ContextProxy.getInstance(context)
96-
const codeIndexManager = CodeIndexManager.getInstance(context)
9796

98-
try {
99-
await codeIndexManager?.initialize(contextProxy)
100-
} catch (error) {
101-
outputChannel.appendLine(
102-
`[CodeIndexManager] Error during background CodeIndexManager configuration/indexing: ${error.message || error}`,
103-
)
97+
// Initialize code index managers for all workspace folders
98+
const codeIndexManagers: CodeIndexManager[] = []
99+
if (vscode.workspace.workspaceFolders) {
100+
for (const folder of vscode.workspace.workspaceFolders) {
101+
const manager = CodeIndexManager.getInstance(context, folder.uri.fsPath)
102+
if (manager) {
103+
codeIndexManagers.push(manager)
104+
try {
105+
await manager.initialize(contextProxy)
106+
} catch (error) {
107+
outputChannel.appendLine(
108+
`[CodeIndexManager] Error during background CodeIndexManager configuration/indexing for ${folder.uri.fsPath}: ${error.message || error}`,
109+
)
110+
}
111+
context.subscriptions.push(manager)
112+
}
113+
}
104114
}
105115

106116
// Initialize Roo Code Cloud service.
@@ -126,13 +136,9 @@ export async function activate(context: vscode.ExtensionContext) {
126136
// Add to subscriptions for proper cleanup on deactivate.
127137
context.subscriptions.push(cloudService)
128138

129-
const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, codeIndexManager, mdmService)
139+
const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, mdmService)
130140
TelemetryService.instance.setProvider(provider)
131141

132-
if (codeIndexManager) {
133-
context.subscriptions.push(codeIndexManager)
134-
}
135-
136142
context.subscriptions.push(
137143
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, provider, {
138144
webviewOptions: { retainContextWhenHidden: true },

src/services/code-index/__tests__/manager.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type { MockedClass } from "vitest"
44

55
// Mock vscode module
66
vi.mock("vscode", () => ({
7+
window: {
8+
activeTextEditor: null,
9+
},
710
workspace: {
811
workspaceFolders: [
912
{

0 commit comments

Comments
 (0)