Browse Source

Internal codebase indexing

John Fawcett 3 months ago
parent
commit
499bf1a52d
38 changed files with 4193 additions and 53 deletions
  1. 5 0
      .changeset/dull-bars-lead.md
  2. 8 5
      src/core/prompts/sections/capabilities.ts
  3. 7 4
      src/core/prompts/sections/objective.ts
  4. 7 4
      src/core/prompts/sections/rules.ts
  5. 7 4
      src/core/prompts/sections/tool-use-guidelines.ts
  6. 5 1
      src/core/prompts/tools/index.ts
  7. 8 1
      src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts
  8. 10 7
      src/core/tools/codebaseSearchTool.ts
  9. 4 4
      src/core/webview/ClineProvider.ts
  10. 3 0
      src/extension.ts
  11. 42 0
      src/services/code-index/config-manager.ts
  12. 11 0
      src/services/code-index/constants/index.ts
  13. 165 0
      src/services/code-index/managed/__tests__/error-handling.spec.ts
  14. 194 0
      src/services/code-index/managed/__tests__/get-base-branch.spec.ts
  15. 122 0
      src/services/code-index/managed/__tests__/git-tracked-files.spec.ts
  16. 140 0
      src/services/code-index/managed/__tests__/is-base-branch.spec.ts
  17. 92 0
      src/services/code-index/managed/__tests__/scanner-git-paths.spec.ts
  18. 216 0
      src/services/code-index/managed/api-client.ts
  19. 250 0
      src/services/code-index/managed/chunker.ts
  20. 353 0
      src/services/code-index/managed/git-utils.ts
  21. 391 0
      src/services/code-index/managed/git-watcher.ts
  22. 85 0
      src/services/code-index/managed/index.ts
  23. 378 0
      src/services/code-index/managed/indexer.ts
  24. 429 0
      src/services/code-index/managed/scanner.ts
  25. 222 0
      src/services/code-index/managed/types.ts
  26. 131 0
      src/services/code-index/managed/webview.ts
  27. 222 16
      src/services/code-index/manager.ts
  28. 29 1
      src/services/code-index/state-manager.ts
  29. 85 0
      src/services/kilocode/OrganizationService.ts
  30. 6 0
      src/shared/ExtensionMessage.ts
  31. 43 0
      src/shared/kilocode/organization.ts
  32. 153 0
      src/shared/utils/exec.ts
  33. 66 0
      src/shared/utils/iterable.ts
  34. 4 0
      src/utils/__tests__/project-config.spec.ts
  35. 4 3
      src/utils/kilo-config-file.ts
  36. 10 3
      webview-ui/src/components/chat/IndexingStatusBadge.tsx
  37. 121 0
      webview-ui/src/components/chat/kilocode/ManagedCodeIndexPopover.tsx
  38. 165 0
      webview-ui/src/components/chat/kilocode/OrganizationIndexingTab.tsx

+ 5 - 0
.changeset/dull-bars-lead.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": patch
+---
+
+Introduces the managed codebase indexing feature for Kilo Code Teams and Enterprise organizations. This feature is currently gated to internal customers only. Managed codebase indexing is a branch-aware indexing and search product that does not require any configuration (as opposed to the current codebase indexing feature which relies on a local qdrant instance and configurating an embedding provider).

+ 8 - 5
src/core/prompts/sections/capabilities.ts

@@ -24,11 +24,14 @@ CAPABILITIES
 		supportsComputerUse ? ", use the browser" : ""
 	}, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
 - When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('${cwd}') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.${
-		codeIndexManager &&
-		codeIndexManager.isFeatureEnabled &&
-		codeIndexManager.isFeatureConfigured &&
-		codeIndexManager.isInitialized
-			? `
+		// kilocode_change start
+		codeIndexManager?.isManagedIndexingAvailable ||
+		(codeIndexManager &&
+			codeIndexManager.isFeatureEnabled &&
+			codeIndexManager.isFeatureConfigured &&
+			codeIndexManager.isInitialized)
+			? // kilocode_change end
+				`
 - You can use the \`codebase_search\` tool to perform semantic searches across your entire codebase. This tool is powerful for finding functionally relevant code, even if you don't know the exact keywords or file names. It's particularly useful for understanding how features are implemented across multiple files, discovering usages of a particular API, or finding code examples related to a concept. This capability relies on a pre-built index of your code.`
 			: ""
 	}

+ 7 - 4
src/core/prompts/sections/objective.ts

@@ -5,10 +5,13 @@ export function getObjectiveSection(
 	experimentsConfig?: Record<string, boolean>,
 ): string {
 	const isCodebaseSearchAvailable =
-		codeIndexManager &&
-		codeIndexManager.isFeatureEnabled &&
-		codeIndexManager.isFeatureConfigured &&
-		codeIndexManager.isInitialized
+		// kilocode_change start
+		codeIndexManager?.isManagedIndexingAvailable ||
+		(codeIndexManager &&
+			codeIndexManager.isFeatureEnabled &&
+			codeIndexManager.isFeatureConfigured &&
+			codeIndexManager.isInitialized)
+	// kilocode_change end
 
 	const codebaseSearchInstruction = isCodebaseSearchAvailable
 		? "First, for ANY exploration of code you haven't examined yet in this conversation, you MUST use the `codebase_search` tool to search for relevant code based on the task's intent BEFORE using any other search or file exploration tools. This applies throughout the entire task, not just at the beginning - whenever you need to explore a new area of code, codebase_search must come first. Then, "

+ 7 - 4
src/core/prompts/sections/rules.ts

@@ -56,10 +56,13 @@ export function getRulesSection(
 	toolUseStyle?: ToolUseStyle, // kilocode_change
 ): string {
 	const isCodebaseSearchAvailable =
-		codeIndexManager &&
-		codeIndexManager.isFeatureEnabled &&
-		codeIndexManager.isFeatureConfigured &&
-		codeIndexManager.isInitialized
+		// kilocode_change start
+		codeIndexManager?.isManagedIndexingAvailable ||
+		(codeIndexManager &&
+			codeIndexManager.isFeatureEnabled &&
+			codeIndexManager.isFeatureConfigured &&
+			codeIndexManager.isInitialized)
+	// kilocode_change end
 
 	const codebaseSearchRule = isCodebaseSearchAvailable
 		? "- **CRITICAL: For ANY exploration of code you haven't examined yet in this conversation, you MUST use the `codebase_search` tool FIRST before using search_files or other file exploration tools.** This requirement applies throughout the entire conversation, not just when starting a task. The codebase_search tool uses semantic search to find relevant code based on meaning, not just keywords, making it much more effective for understanding how features are implemented. Even if you've already explored some parts of the codebase, any new area or functionality you need to understand requires using codebase_search first.\n"

+ 7 - 4
src/core/prompts/sections/tool-use-guidelines.ts

@@ -6,10 +6,13 @@ export function getToolUseGuidelinesSection(
 	toolUseStyle?: ToolUseStyle, // kilocode_change
 ): string {
 	const isCodebaseSearchAvailable =
-		codeIndexManager &&
-		codeIndexManager.isFeatureEnabled &&
-		codeIndexManager.isFeatureConfigured &&
-		codeIndexManager.isInitialized
+		// kilocode_change start
+		codeIndexManager?.isManagedIndexingAvailable ||
+		(codeIndexManager &&
+			codeIndexManager.isFeatureEnabled &&
+			codeIndexManager.isFeatureConfigured &&
+			codeIndexManager.isInitialized)
+	// kilocode_change end
 
 	// Build guidelines array with automatic numbering
 	let itemNumber = 1

+ 5 - 1
src/core/prompts/tools/index.ts

@@ -135,7 +135,11 @@ export function getToolDescriptionsForMode(
 		!codeIndexManager ||
 		!(codeIndexManager.isFeatureEnabled && codeIndexManager.isFeatureConfigured && codeIndexManager.isInitialized)
 	) {
-		tools.delete("codebase_search")
+		// kilocode_change start
+		if (!codeIndexManager?.isManagedIndexingAvailable) {
+			tools.delete("codebase_search")
+		}
+		// kilocode_change end
 	}
 
 	// kilocode_change start: Morph fast apply

+ 8 - 1
src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts

@@ -88,7 +88,14 @@ export async function getAllowedJSONToolsForMode(
 	// Conditionally exclude codebase_search if feature is disabled or not configured
 	if (
 		!codeIndexManager ||
-		!(codeIndexManager.isFeatureEnabled && codeIndexManager.isFeatureConfigured && codeIndexManager.isInitialized)
+		// kilcode_change start
+		!(
+			codeIndexManager.isFeatureEnabled &&
+			codeIndexManager.isFeatureConfigured &&
+			codeIndexManager.isInitialized &&
+			codeIndexManager.isManagedIndexingAvailable
+		)
+		// kilcode_change end
 	) {
 		tools.delete("codebase_search")
 	}

+ 10 - 7
src/core/tools/codebaseSearchTool.ts

@@ -75,12 +75,16 @@ export async function codebaseSearchTool(
 			throw new Error("CodeIndexManager is not available.")
 		}
 
-		if (!manager.isFeatureEnabled) {
-			throw new Error("Code Indexing is disabled in the settings.")
-		}
-		if (!manager.isFeatureConfigured) {
-			throw new Error("Code Indexing is not configured (Missing OpenAI Key or Qdrant URL).")
+		// kilcode_change start
+		if (!manager.isManagedIndexingAvailable) {
+			if (!manager.isFeatureEnabled) {
+				throw new Error("Code Indexing is disabled in the settings.")
+			}
+			if (!manager.isFeatureConfigured) {
+				throw new Error("Code Indexing is not configured (Missing OpenAI Key or Qdrant URL).")
+			}
 		}
+		// kilcode_change end
 
 		// kilocode_change start
 		const status = manager.getCurrentStatus()
@@ -185,8 +189,7 @@ ${jsonResult.results
 		(result) => `File path: ${result.filePath}
 Score: ${result.score}
 Lines: ${result.startLine}-${result.endLine}
-Code Chunk: ${result.codeChunk}
-`,
+${result.codeChunk ? `Code Chunk: ${result.codeChunk}\n` : ""}`, // kilocode_change - don't include code chunk managed indexing
 	)
 	.join("\n")}`
 

+ 4 - 4
src/core/webview/ClineProvider.ts

@@ -93,8 +93,6 @@ import { Task } from "../task/Task"
 import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
 
 import { webviewMessageHandler } from "./webviewMessageHandler"
-import type { ClineMessage } from "@roo-code/types"
-import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
 import { getNonce } from "./getNonce"
 import { getUri } from "./getUri"
 import { REQUESTY_BASE_URL } from "../../shared/utils/requesty"
@@ -107,8 +105,8 @@ import { stringifyError } from "../../shared/kilocode/errorUtils"
 import isWsl from "is-wsl"
 import { getKilocodeDefaultModel } from "../../api/providers/kilocode/getKilocodeDefaultModel"
 import { getKiloCodeWrapperProperties } from "../../core/kilocode/wrapper"
-import { getKiloUrlFromToken } from "@roo-code/types" // kilocode_change
-import { getKilocodeConfig, getWorkspaceProjectId, KilocodeConfig } from "../../utils/kilo-config-file" // kilocode_change
+import { getKilocodeConfig, KilocodeConfig } from "../../utils/kilo-config-file" // kilocode_change
+import { updateCodeIndexWithKiloProps } from "../../services/code-index/managed/webview" // kilocode_change
 
 export type ClineProviderState = Awaited<ReturnType<ClineProvider["getState"]>>
 // kilocode_change end
@@ -1427,6 +1425,7 @@ ${prompt}
 				}
 
 				await TelemetryService.instance.updateIdentity(providerSettings.kilocodeToken ?? "") // kilocode_change
+				await updateCodeIndexWithKiloProps(this) // kilocode_change
 			} else {
 				await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
 			}
@@ -1491,6 +1490,7 @@ ${prompt}
 
 		await this.postStateToWebview()
 		await TelemetryService.instance.updateIdentity(providerSettings.kilocodeToken ?? "") // kilocode_change
+		await updateCodeIndexWithKiloProps(this) // kilocode_change
 
 		if (providerSettings.apiProvider) {
 			this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider })

+ 3 - 0
src/extension.ts

@@ -46,6 +46,7 @@ import { registerGhostProvider } from "./services/ghost" // kilocode_change
 import { registerMainThreadForwardingLogger } from "./utils/fowardingLogger" // kilocode_change
 import { getKiloCodeWrapperProperties } from "./core/kilocode/wrapper" // kilocode_change
 import { flushModels, getModels } from "./api/providers/fetchers/modelCache"
+import { updateCodeIndexWithKiloProps } from "./services/code-index/managed/webview" // kilocode_change
 
 /**
  * Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -156,6 +157,7 @@ export async function activate(context: vscode.ExtensionContext) {
 
 	// Initialize the provider *before* the Roo Code Cloud service.
 	const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, mdmService)
+	const initManagedCodeIndexing = updateCodeIndexWithKiloProps(provider) // kilocode_change
 
 	// Initialize Roo Code Cloud service.
 	const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebview()
@@ -429,6 +431,7 @@ export async function activate(context: vscode.ExtensionContext) {
 	}
 
 	await checkAndRunAutoLaunchingTask(context) // kilocode_change
+	await initManagedCodeIndexing // kilocode_change
 
 	return new API(outputChannel, provider, socketPath, enableLogging)
 }

+ 42 - 0
src/services/code-index/config-manager.ts

@@ -26,11 +26,46 @@ export class CodeIndexConfigManager {
 	private searchMinScore?: number
 	private searchMaxResults?: number
 
+	// kilocode_change start: Kilo org indexing props
+	private _kiloOrgProps: {
+		organizationId: string
+		kilocodeToken: string
+		projectId: string
+	} | null = null
+	// kilocode_change end
+
 	constructor(private readonly contextProxy: ContextProxy) {
 		// Initialize with current configuration to avoid false restart triggers
 		this._loadAndSetConfiguration()
 	}
 
+	// kilocode_change start: Kilo org indexing methods
+	/**
+	 * Sets Kilo organization properties for cloud-based indexing
+	 */
+	public setKiloOrgProps(props: { organizationId: string; kilocodeToken: string; projectId: string }) {
+		this._kiloOrgProps = props
+	}
+
+	/**
+	 * Gets Kilo organization properties
+	 */
+	public getKiloOrgProps() {
+		return this._kiloOrgProps
+	}
+
+	/**
+	 * Checks if Kilo org mode is available (has valid credentials)
+	 */
+	public get isKiloOrgMode(): boolean {
+		return !!(
+			this._kiloOrgProps?.organizationId &&
+			this._kiloOrgProps?.kilocodeToken &&
+			this._kiloOrgProps?.projectId
+		)
+	}
+	// kilocode_change end
+
 	/**
 	 * Gets the context proxy instance
 	 */
@@ -210,8 +245,15 @@ export class CodeIndexConfigManager {
 
 	/**
 	 * Checks if the service is properly configured based on the embedder type.
+	 * kilocode_change: Also returns true if Kilo org mode is available
 	 */
 	public isConfigured(): boolean {
+		// kilocode_change start: Allow Kilo org mode as configured
+		if (this.isKiloOrgMode) {
+			return true
+		}
+		// kilocode_change end
+
 		if (this.embedderProvider === "openai") {
 			const openAiKey = this.openAiOptions?.openAiNativeApiKey
 			const qdrantUrl = this.qdrantUrl

+ 11 - 0
src/services/code-index/constants/index.ts

@@ -29,3 +29,14 @@ export const BATCH_PROCESSING_CONCURRENCY = 10
 
 /**Gemini Embedder */
 export const GEMINI_MAX_ITEM_TOKENS = 2048
+
+// kilocode_change start
+/**Managed Indexing */
+export const MANAGED_MAX_CHUNK_CHARS = 1000
+export const MANAGED_MIN_CHUNK_CHARS = 50
+export const MANAGED_OVERLAP_LINES = 5
+export const MANAGED_BATCH_SIZE = 60
+export const MANAGED_FILE_WATCH_DEBOUNCE_MS = 500
+export const MANAGED_MAX_CONCURRENT_FILES = 10
+export const MANAGED_MAX_CONCURRENT_BATCHES = 50
+// kilocode_change end

+ 165 - 0
src/services/code-index/managed/__tests__/error-handling.spec.ts

@@ -0,0 +1,165 @@
+// kilocode_change - new file
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import * as vscode from "vscode"
+import { startIndexing } from "../indexer"
+import { scanDirectory } from "../scanner"
+import { ManagedIndexingConfig } from "../types"
+
+// Mock dependencies
+vi.mock("../scanner")
+vi.mock("../watcher", () => ({
+	createFileWatcher: vi.fn(() => ({
+		dispose: vi.fn(),
+	})),
+}))
+vi.mock("../git-watcher", () => ({
+	createGitWatcher: vi.fn(() => ({
+		dispose: vi.fn(),
+	})),
+}))
+vi.mock("../git-utils", () => ({
+	isGitRepository: vi.fn(() => Promise.resolve(true)),
+	getCurrentBranch: vi.fn(() => Promise.resolve("main")),
+	isDetachedHead: vi.fn(() => Promise.resolve(false)),
+}))
+vi.mock("@roo-code/telemetry", () => ({
+	TelemetryService: {
+		instance: {
+			captureEvent: vi.fn(),
+		},
+	},
+}))
+vi.mock("../../../utils/logging", () => ({
+	logger: {
+		info: vi.fn(),
+		error: vi.fn(),
+		warn: vi.fn(),
+		debug: vi.fn(),
+	},
+}))
+
+describe("Managed Indexing Error Handling", () => {
+	let mockContext: vscode.ExtensionContext
+	let config: ManagedIndexingConfig
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+
+		mockContext = {
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn(),
+			},
+		} as any
+
+		config = {
+			organizationId: "test-org",
+			projectId: "test-project",
+			kilocodeToken: "test-token",
+			workspacePath: "/test/workspace",
+			chunker: {
+				maxChunkChars: 1000,
+				minChunkChars: 200,
+				overlapLines: 5,
+			},
+			batchSize: 60,
+			autoSync: true,
+		}
+	})
+
+	it("should provide detailed error messages when scan fails", async () => {
+		// Mock scanDirectory to return errors
+		const mockErrors = [
+			new Error("Failed to process file1.ts: Network error"),
+			new Error("Failed to process file2.ts: Permission denied"),
+			new Error("Failed to process file3.ts: Invalid syntax"),
+		]
+
+		vi.mocked(scanDirectory).mockResolvedValue({
+			success: false,
+			filesProcessed: 0,
+			filesSkipped: 0,
+			chunksIndexed: 0,
+			errors: mockErrors,
+		})
+
+		// Attempt to start indexing
+		await expect(startIndexing(config, mockContext)).rejects.toThrow(/Scan failed with 3 errors/)
+	})
+
+	it("should include error details in thrown error message", async () => {
+		const mockErrors = [new Error("Error 1"), new Error("Error 2"), new Error("Error 3")]
+
+		vi.mocked(scanDirectory).mockResolvedValue({
+			success: false,
+			filesProcessed: 0,
+			filesSkipped: 0,
+			chunksIndexed: 0,
+			errors: mockErrors,
+		})
+
+		try {
+			await startIndexing(config, mockContext)
+			expect.fail("Should have thrown an error")
+		} catch (error) {
+			expect(error).toBeInstanceOf(Error)
+			const err = error as Error
+			expect(err.message).toContain("Error 1")
+			expect(err.message).toContain("Error 2")
+			expect(err.message).toContain("Error 3")
+		}
+	})
+
+	it("should truncate error list when there are many errors", async () => {
+		const mockErrors = Array.from({ length: 25 }, (_, i) => new Error(`Error ${i + 1}`))
+
+		vi.mocked(scanDirectory).mockResolvedValue({
+			success: false,
+			filesProcessed: 0,
+			filesSkipped: 0,
+			chunksIndexed: 0,
+			errors: mockErrors,
+		})
+
+		try {
+			await startIndexing(config, mockContext)
+			expect.fail("Should have thrown an error")
+		} catch (error) {
+			expect(error).toBeInstanceOf(Error)
+			const err = error as Error
+			expect(err.message).toContain("Scan failed with 25 errors")
+			expect(err.message).toContain("and 20 more")
+			// Should only include first 5 errors in message
+			expect(err.message).toContain("Error 1")
+			expect(err.message).toContain("Error 5")
+			expect(err.message).not.toContain("Error 6")
+		}
+	})
+
+	it("should call state change callback with error state", async () => {
+		const mockErrors = [new Error("Test error")]
+		const onStateChange = vi.fn()
+
+		vi.mocked(scanDirectory).mockResolvedValue({
+			success: false,
+			filesProcessed: 0,
+			filesSkipped: 0,
+			chunksIndexed: 0,
+			errors: mockErrors,
+		})
+
+		try {
+			await startIndexing(config, mockContext, onStateChange)
+		} catch {
+			// Expected to throw
+		}
+
+		// Should have called with error state
+		expect(onStateChange).toHaveBeenCalledWith(
+			expect.objectContaining({
+				status: "error",
+				message: expect.stringContaining("Failed to start indexing"),
+			}),
+		)
+	})
+})

+ 194 - 0
src/services/code-index/managed/__tests__/get-base-branch.spec.ts

@@ -0,0 +1,194 @@
+// kilocode_change - new file
+/**
+ * Tests for getBaseBranch and getDefaultBranchFromRemote functionality
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import { getBaseBranch, getDefaultBranchFromRemote } from "../git-utils"
+
+// Mock exec utils
+vi.mock("../../../../shared/utils/exec", () => ({
+	execGetLines: vi.fn(),
+}))
+
+import { execGetLines } from "../../../../shared/utils/exec"
+import type { ExecOptions } from "../../../../shared/utils/exec"
+
+describe("Git Base Branch Detection", () => {
+	const workspacePath = "/Users/test/project"
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("getDefaultBranchFromRemote", () => {
+		it("should return default branch from remote symbolic ref", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "refs/remotes/origin/main"
+			})
+
+			const result = await getDefaultBranchFromRemote(workspacePath)
+
+			expect(result).toBe("main")
+		})
+
+		it("should return canary when remote default is canary", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "refs/remotes/origin/canary"
+			})
+
+			const result = await getDefaultBranchFromRemote(workspacePath)
+
+			expect(result).toBe("canary")
+		})
+
+		it("should return develop when remote default is develop", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "refs/remotes/origin/develop"
+			})
+
+			const result = await getDefaultBranchFromRemote(workspacePath)
+
+			expect(result).toBe("develop")
+		})
+
+		it("should try to set remote HEAD if symbolic-ref fails initially", async () => {
+			let callCount = 0
+			vi.mocked(execGetLines).mockImplementation(async function* ({ cmd }: ExecOptions) {
+				callCount++
+				if (callCount === 1) {
+					// First call to symbolic-ref fails
+					throw new Error("No symbolic ref")
+				} else if (callCount === 2) {
+					// Second call to set-head succeeds
+					return
+				} else if (callCount === 3) {
+					// Third call to symbolic-ref succeeds
+					yield "refs/remotes/origin/main"
+				}
+			})
+
+			const result = await getDefaultBranchFromRemote(workspacePath)
+
+			expect(result).toBe("main")
+			expect(execGetLines).toHaveBeenCalledTimes(3)
+		})
+
+		it("should return null if unable to determine remote default", async () => {
+			// eslint-disable-next-line require-yield
+			vi.mocked(execGetLines).mockImplementation(async function* (): AsyncGenerator<string> {
+				throw new Error("Failed")
+			})
+
+			const result = await getDefaultBranchFromRemote(workspacePath)
+
+			expect(result).toBeNull()
+		})
+
+		it("should return null if symbolic-ref output is malformed", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "invalid-format"
+			})
+
+			const result = await getDefaultBranchFromRemote(workspacePath)
+
+			expect(result).toBeNull()
+		})
+	})
+
+	describe("getBaseBranch", () => {
+		it("should return default branch from remote when available", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* ({ cmd }: ExecOptions) {
+				if (cmd.includes("symbolic-ref")) {
+					yield "refs/remotes/origin/canary"
+				} else if (cmd.includes("rev-parse --verify canary")) {
+					yield "abc123"
+				}
+			})
+
+			const result = await getBaseBranch(workspacePath)
+
+			expect(result).toBe("canary")
+		})
+
+		it("should fallback to main if remote default doesn't exist locally", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* ({ cmd }: ExecOptions) {
+				if (cmd.includes("symbolic-ref")) {
+					yield "refs/remotes/origin/canary"
+				} else if (cmd.includes("rev-parse --verify canary")) {
+					throw new Error("Branch doesn't exist locally")
+				} else if (cmd.includes("rev-parse --verify main")) {
+					yield "abc123"
+				}
+			})
+
+			const result = await getBaseBranch(workspacePath)
+
+			expect(result).toBe("main")
+		})
+
+		it("should check common branches when remote default is unavailable", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* ({ cmd }: ExecOptions) {
+				if (cmd.includes("symbolic-ref")) {
+					throw new Error("No remote HEAD")
+				} else if (cmd.includes("set-head")) {
+					throw new Error("Cannot set HEAD")
+				} else if (cmd.includes("rev-parse --verify main")) {
+					throw new Error("main doesn't exist")
+				} else if (cmd.includes("rev-parse --verify develop")) {
+					yield "abc123"
+				}
+			})
+
+			const result = await getBaseBranch(workspacePath)
+
+			expect(result).toBe("develop")
+		})
+
+		it("should return master if main and develop don't exist", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* ({ cmd }: ExecOptions) {
+				if (cmd.includes("symbolic-ref") || cmd.includes("set-head")) {
+					throw new Error("No remote")
+				} else if (cmd.includes("rev-parse --verify main")) {
+					throw new Error("main doesn't exist")
+				} else if (cmd.includes("rev-parse --verify develop")) {
+					throw new Error("develop doesn't exist")
+				} else if (cmd.includes("rev-parse --verify master")) {
+					yield "abc123"
+				}
+			})
+
+			const result = await getBaseBranch(workspacePath)
+
+			expect(result).toBe("master")
+		})
+
+		it("should fallback to main if no branches exist", async () => {
+			// eslint-disable-next-line require-yield
+			vi.mocked(execGetLines).mockImplementation(async function* (): AsyncGenerator<string> {
+				throw new Error("No branches")
+			})
+
+			const result = await getBaseBranch(workspacePath)
+
+			expect(result).toBe("main")
+		})
+
+		it("should prioritize remote default over common branch names", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* ({ cmd }: ExecOptions) {
+				if (cmd.includes("symbolic-ref")) {
+					yield "refs/remotes/origin/production"
+				} else if (cmd.includes("rev-parse --verify production")) {
+					yield "abc123"
+				} else if (cmd.includes("rev-parse --verify main")) {
+					yield "def456"
+				}
+			})
+
+			const result = await getBaseBranch(workspacePath)
+
+			// Should return production (from remote) even though main exists
+			expect(result).toBe("production")
+		})
+	})
+})

+ 122 - 0
src/services/code-index/managed/__tests__/git-tracked-files.spec.ts

@@ -0,0 +1,122 @@
+// kilocode_change - new file
+/**
+ * Tests for git-tracked files functionality
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import { getGitTrackedFiles } from "../git-utils"
+
+// Mock exec utils
+vi.mock("../../../../shared/utils/exec", () => ({
+	execGetLines: vi.fn(),
+}))
+
+import { execGetLines } from "../../../../shared/utils/exec"
+
+describe("Git Tracked Files", () => {
+	const workspacePath = "/Users/test/project"
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("getGitTrackedFiles", () => {
+		it("should yield list of git-tracked files", async () => {
+			const mockFiles = ["src/app.ts", "src/utils/helper.ts", "src/index.ts", "README.md", "package.json"]
+
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				for (const file of mockFiles) {
+					yield file
+				}
+			})
+
+			const files: string[] = []
+			for await (const file of getGitTrackedFiles(workspacePath)) {
+				files.push(file)
+			}
+
+			expect(files).toEqual(mockFiles)
+		})
+
+		it("should filter out empty lines", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "src/app.ts"
+				yield ""
+				yield "src/utils/helper.ts"
+				yield ""
+			})
+
+			const files: string[] = []
+			for await (const file of getGitTrackedFiles(workspacePath)) {
+				files.push(file)
+			}
+
+			expect(files).toEqual(["src/app.ts", "src/utils/helper.ts"])
+		})
+
+		it("should handle files with special characters", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "src/app/(app)/page.tsx"
+				yield "src/components/[id]/view.tsx"
+				yield "src/utils/file with spaces.ts"
+			})
+
+			const files: string[] = []
+			for await (const file of getGitTrackedFiles(workspacePath)) {
+				files.push(file)
+			}
+
+			expect(files).toEqual([
+				"src/app/(app)/page.tsx",
+				"src/components/[id]/view.tsx",
+				"src/utils/file with spaces.ts",
+			])
+		})
+
+		it("should throw error if git command fails", async () => {
+			// eslint-disable-next-line require-yield
+			vi.mocked(execGetLines).mockImplementation(async function* (): AsyncGenerator<string> {
+				throw new Error("Not a git repository")
+			})
+
+			await expect(async () => {
+				for await (const file of getGitTrackedFiles(workspacePath)) {
+					// Should throw before yielding anything
+				}
+			}).rejects.toThrow("Failed to get git tracked files")
+		})
+
+		it("should handle empty repository", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				// Yield nothing
+			})
+
+			const files: string[] = []
+			for await (const file of getGitTrackedFiles(workspacePath)) {
+				files.push(file)
+			}
+
+			expect(files).toEqual([])
+		})
+
+		it("should handle large number of files", async () => {
+			// Generate 10000 file paths
+			const mockFiles = Array.from({ length: 10000 }, (_, i) => `src/file${i}.ts`)
+
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				for (const file of mockFiles) {
+					yield file
+				}
+			})
+
+			const files: string[] = []
+			for await (const file of getGitTrackedFiles(workspacePath)) {
+				files.push(file)
+			}
+
+			expect(files).toHaveLength(10000)
+			expect(files[0]).toBe("src/file0.ts")
+			expect(files[9999]).toBe("src/file9999.ts")
+		})
+	})
+})

+ 140 - 0
src/services/code-index/managed/__tests__/is-base-branch.spec.ts

@@ -0,0 +1,140 @@
+// kilocode_change - new file
+/**
+ * Tests for isBaseBranch functionality
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import { isBaseBranch } from "../git-utils"
+
+// Mock exec utils
+vi.mock("../../../../shared/utils/exec", () => ({
+	execGetLines: vi.fn(),
+}))
+
+import { execGetLines } from "../../../../shared/utils/exec"
+import type { ExecOptions } from "../../../../shared/utils/exec"
+
+describe("isBaseBranch", () => {
+	const workspacePath = "/Users/test/project"
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("without workspace path", () => {
+		it("should return true for main", async () => {
+			expect(await isBaseBranch("main")).toBe(true)
+		})
+
+		it("should return true for master", async () => {
+			expect(await isBaseBranch("master")).toBe(true)
+		})
+
+		it("should return true for develop", async () => {
+			expect(await isBaseBranch("develop")).toBe(true)
+		})
+
+		it("should return true for development", async () => {
+			expect(await isBaseBranch("development")).toBe(true)
+		})
+
+		it("should be case insensitive for common branches", async () => {
+			expect(await isBaseBranch("MAIN")).toBe(true)
+			expect(await isBaseBranch("Master")).toBe(true)
+			expect(await isBaseBranch("DEVELOP")).toBe(true)
+		})
+
+		it("should return false for feature branches", async () => {
+			expect(await isBaseBranch("feature/new-api")).toBe(false)
+			expect(await isBaseBranch("bugfix/issue-123")).toBe(false)
+			expect(await isBaseBranch("canary")).toBe(false)
+		})
+	})
+
+	describe("with workspace path", () => {
+		it("should return true for common base branches even without checking remote", async () => {
+			expect(await isBaseBranch("main", workspacePath)).toBe(true)
+			expect(await isBaseBranch("master", workspacePath)).toBe(true)
+			expect(await isBaseBranch("develop", workspacePath)).toBe(true)
+		})
+
+		it("should return true when branch matches remote default", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "refs/remotes/origin/canary"
+			})
+
+			const result = await isBaseBranch("canary", workspacePath)
+
+			expect(result).toBe(true)
+		})
+
+		it("should be case insensitive when comparing with remote default", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "refs/remotes/origin/Canary"
+			})
+
+			expect(await isBaseBranch("canary", workspacePath)).toBe(true)
+			expect(await isBaseBranch("CANARY", workspacePath)).toBe(true)
+			expect(await isBaseBranch("Canary", workspacePath)).toBe(true)
+		})
+
+		it("should return true for production when it's the remote default", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "refs/remotes/origin/production"
+			})
+
+			expect(await isBaseBranch("production", workspacePath)).toBe(true)
+		})
+
+		it("should return false when branch doesn't match remote default", async () => {
+			vi.mocked(execGetLines).mockImplementation(async function* () {
+				yield "refs/remotes/origin/main"
+			})
+
+			expect(await isBaseBranch("feature/test", workspacePath)).toBe(false)
+		})
+
+		it("should return false when remote default cannot be determined", async () => {
+			// eslint-disable-next-line require-yield
+			vi.mocked(execGetLines).mockImplementation(async function* (): AsyncGenerator<string> {
+				throw new Error("No remote")
+			})
+
+			expect(await isBaseBranch("canary", workspacePath)).toBe(false)
+		})
+
+		it("should handle remote default check failure gracefully", async () => {
+			// eslint-disable-next-line require-yield
+			vi.mocked(execGetLines).mockImplementation(async function* (): AsyncGenerator<string> {
+				throw new Error("Git error")
+			})
+
+			// Should still work for common branches
+			expect(await isBaseBranch("main", workspacePath)).toBe(true)
+			// But return false for non-common branches
+			expect(await isBaseBranch("canary", workspacePath)).toBe(false)
+		})
+
+		it("should try to set remote HEAD if symbolic-ref fails initially", async () => {
+			let callCount = 0
+			vi.mocked(execGetLines).mockImplementation(async function* ({ cmd }: ExecOptions) {
+				callCount++
+				if (callCount === 1) {
+					// First call to symbolic-ref fails
+					throw new Error("No symbolic ref")
+				} else if (callCount === 2) {
+					// Second call to set-head succeeds
+					return
+				} else if (callCount === 3) {
+					// Third call to symbolic-ref succeeds
+					yield "refs/remotes/origin/canary"
+				}
+			})
+
+			const result = await isBaseBranch("canary", workspacePath)
+
+			expect(result).toBe(true)
+			expect(execGetLines).toHaveBeenCalledTimes(3)
+		})
+	})
+})

+ 92 - 0
src/services/code-index/managed/__tests__/scanner-git-paths.spec.ts

@@ -0,0 +1,92 @@
+// kilocode_change - new file
+/**
+ * Tests for scanner git path handling
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import * as path from "path"
+import * as scanner from "../scanner"
+import * as gitUtils from "../git-utils"
+
+// Mock dependencies
+vi.mock("../git-utils")
+vi.mock("../../glob/list-files")
+vi.mock("../../../core/ignore/RooIgnoreController")
+vi.mock("vscode", () => ({
+	workspace: {
+		fs: {
+			readFile: vi.fn(),
+		},
+	},
+	Uri: {
+		file: vi.fn((p) => ({ fsPath: p })),
+	},
+}))
+
+describe("Scanner Git Path Handling", () => {
+	// Use platform-appropriate path for testing
+	const workspacePath = process.platform === "win32" ? "C:\\Users\\test\\project" : "/Users/test/project"
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+	})
+
+	it("should convert relative git paths to absolute paths for feature branches", async () => {
+		// Mock git utilities
+		vi.mocked(gitUtils.isGitRepository).mockResolvedValue(true)
+		vi.mocked(gitUtils.getCurrentBranch).mockResolvedValue("feature/test")
+		vi.mocked(gitUtils.getGitDiff).mockResolvedValue({
+			added: ["src/app.ts", "src/utils/helper.ts"],
+			modified: ["src/index.ts"],
+			deleted: ["src/old.ts"],
+		})
+
+		// The getFilesToScan function is not exported, but we can test it indirectly
+		// by checking that scanDirectory doesn't throw ENOENT errors
+
+		// For this test, we'll verify the git diff returns relative paths
+		const diff = await gitUtils.getGitDiff("feature/test", "main", workspacePath)
+
+		// Verify git returns relative paths
+		expect(diff.added).toEqual(["src/app.ts", "src/utils/helper.ts"])
+		expect(diff.modified).toEqual(["src/index.ts"])
+
+		// The scanner should convert these to absolute paths internally
+		// Expected absolute paths would be:
+		const expectedPaths = [
+			path.join(workspacePath, "src/app.ts"),
+			path.join(workspacePath, "src/utils/helper.ts"),
+			path.join(workspacePath, "src/index.ts"),
+		]
+
+		// Verify the paths are absolute
+		expectedPaths.forEach((p) => {
+			expect(path.isAbsolute(p)).toBe(true)
+		})
+	})
+
+	it("should handle git paths with special characters", () => {
+		const relativePaths = [
+			"src/app/(app)/page.tsx",
+			"src/components/[id]/view.tsx",
+			"src/utils/file with spaces.ts",
+		]
+
+		// Convert to absolute paths
+		const absolutePaths = relativePaths.map((p) => path.join(workspacePath, p))
+
+		// Verify all are absolute
+		absolutePaths.forEach((p) => {
+			expect(path.isAbsolute(p)).toBe(true)
+			expect(p.startsWith(workspacePath)).toBe(true)
+		})
+	})
+
+	it("should handle nested directory paths correctly", () => {
+		const relativePath = "src/deeply/nested/directory/file.ts"
+		const absolutePath = path.join(workspacePath, relativePath)
+
+		expect(absolutePath).toBe(path.join(workspacePath, "src/deeply/nested/directory/file.ts"))
+		expect(path.isAbsolute(absolutePath)).toBe(true)
+	})
+})

+ 216 - 0
src/services/code-index/managed/api-client.ts

@@ -0,0 +1,216 @@
+// kilocode_change - new file
+/**
+ * API client for managed codebase indexing
+ *
+ * This module provides pure functions for communicating with the Kilo Code
+ * backend API for managed indexing operations (upsert, search, delete, manifest).
+ */
+
+import axios from "axios"
+import { ManagedCodeChunk, SearchRequest, SearchResult, ServerManifest } from "./types"
+import { logger } from "../../../utils/logging"
+import { getKiloBaseUriFromToken } from "../../../../packages/types/src/kilocode/kilocode"
+
+/**
+ * Upserts code chunks to the server using the new envelope format
+ *
+ * @param chunks Array of chunks to upsert (must all be from same org/project/branch)
+ * @param kilocodeToken Authentication token
+ * @throws Error if the request fails or chunks are from different contexts
+ */
+export async function upsertChunks(chunks: ManagedCodeChunk[], kilocodeToken: string): Promise<void> {
+	if (chunks.length === 0) {
+		return
+	}
+
+	// Validate all chunks are from same context
+	const firstChunk = chunks[0]
+	const allSameContext = chunks.every(
+		(c) =>
+			c.organizationId === firstChunk.organizationId &&
+			c.projectId === firstChunk.projectId &&
+			c.gitBranch === firstChunk.gitBranch &&
+			c.isBaseBranch === firstChunk.isBaseBranch,
+	)
+
+	if (!allSameContext) {
+		throw new Error("All chunks must be from the same organization, project, and branch")
+	}
+
+	const baseUrl = getKiloBaseUriFromToken(kilocodeToken)
+
+	// Transform to new envelope format
+	const requestBody = {
+		organizationId: firstChunk.organizationId,
+		projectId: firstChunk.projectId,
+		gitBranch: firstChunk.gitBranch,
+		isBaseBranch: firstChunk.isBaseBranch,
+		chunks: chunks.map((chunk) => ({
+			id: chunk.id,
+			codeChunk: chunk.codeChunk,
+			filePath: chunk.filePath,
+			startLine: chunk.startLine,
+			endLine: chunk.endLine,
+			chunkHash: chunk.chunkHash,
+		})),
+	}
+
+	try {
+		const response = await axios({
+			method: "PUT",
+			url: `${baseUrl}/api/code-indexing/upsert`,
+			data: requestBody,
+			headers: {
+				Authorization: `Bearer ${kilocodeToken}`,
+				"Content-Type": "application/json",
+			},
+		})
+
+		if (response.status !== 200) {
+			throw new Error(`Failed to upsert chunks: ${response.statusText}`)
+		}
+
+		logger.info(`Successfully upserted ${chunks.length} chunks`)
+	} catch (error) {
+		const errorMessage = error instanceof Error ? error.message : String(error)
+		logger.error(`Failed to upsert chunks: ${errorMessage}`)
+		throw error
+	}
+}
+
+/**
+ * Searches code in the managed index with branch preferences
+ *
+ * @param request Search request with preferences
+ * @param kilocodeToken Authentication token
+ * @returns Array of search results sorted by relevance
+ * @throws Error if the request fails
+ */
+export async function searchCode(request: SearchRequest, kilocodeToken: string): Promise<SearchResult[]> {
+	const baseUrl = getKiloBaseUriFromToken(kilocodeToken)
+
+	try {
+		const response = await axios({
+			method: "POST",
+			url: `${baseUrl}/api/code-indexing/search`,
+			data: request,
+			headers: {
+				Authorization: `Bearer ${kilocodeToken}`,
+				"Content-Type": "application/json",
+			},
+		})
+
+		if (response.status !== 200) {
+			throw new Error(`Search failed: ${response.statusText}`)
+		}
+
+		const results: SearchResult[] = response.data || []
+		logger.info(`Search returned ${results.length} results`)
+		return results
+	} catch (error) {
+		const errorMessage = error instanceof Error ? error.message : String(error)
+		logger.error(`Search failed: ${errorMessage}`)
+		throw error
+	}
+}
+
+/**
+ * Deletes chunks for specific files on a specific branch
+ *
+ * @param filePaths Array of file paths to delete
+ * @param gitBranch Git branch to delete from
+ * @param organizationId Organization ID
+ * @param projectId Project ID
+ * @param kilocodeToken Authentication token
+ * @throws Error if the request fails
+ */
+export async function deleteFiles(
+	filePaths: string[],
+	gitBranch: string,
+	organizationId: string,
+	projectId: string,
+	kilocodeToken: string,
+): Promise<void> {
+	if (filePaths.length === 0) {
+		return
+	}
+
+	const baseUrl = getKiloBaseUriFromToken(kilocodeToken)
+
+	try {
+		const response = await axios({
+			method: "PUT",
+			url: `${baseUrl}/api/code-indexing/delete`,
+			data: {
+				organizationId,
+				projectId,
+				gitBranch,
+				filePaths,
+			},
+			headers: {
+				Authorization: `Bearer ${kilocodeToken}`,
+				"Content-Type": "application/json",
+			},
+		})
+
+		if (response.status !== 200) {
+			throw new Error(`Failed to delete files: ${response.statusText}`)
+		}
+
+		logger.info(`Successfully deleted ${filePaths.length} files from branch ${gitBranch}`)
+	} catch (error) {
+		const errorMessage = error instanceof Error ? error.message : String(error)
+		logger.error(`Failed to delete files: ${errorMessage}`)
+		throw error
+	}
+}
+
+/**
+ * Gets the server manifest for a specific branch
+ *
+ * The manifest contains metadata about all indexed files on the branch,
+ * allowing clients to determine what needs to be indexed.
+ *
+ * @param organizationId Organization ID
+ * @param projectId Project ID
+ * @param gitBranch Git branch name
+ * @param kilocodeToken Authentication token
+ * @returns Server manifest with file metadata
+ * @throws Error if the request fails
+ */
+export async function getServerManifest(
+	organizationId: string,
+	projectId: string,
+	gitBranch: string,
+	kilocodeToken: string,
+): Promise<ServerManifest> {
+	const baseUrl = getKiloBaseUriFromToken(kilocodeToken)
+
+	try {
+		const response = await axios({
+			method: "GET",
+			url: `${baseUrl}/api/code-indexing/manifest`,
+			params: {
+				organizationId,
+				projectId,
+				gitBranch,
+			},
+			headers: {
+				Authorization: `Bearer ${kilocodeToken}`,
+				"Content-Type": "application/json",
+			},
+		})
+
+		if (response.status !== 200) {
+			throw new Error(`Failed to get manifest: ${response.statusText}`)
+		}
+
+		const manifest: ServerManifest = response.data
+		logger.info(`Retrieved manifest for ${gitBranch}: ${manifest.totalFiles} files, ${manifest.totalChunks} chunks`)
+		return manifest
+	} catch (error) {
+		const errorMessage = error instanceof Error ? error.message : String(error)
+		logger.error(`Failed to get manifest: ${errorMessage}`)
+		throw error
+	}
+}

+ 250 - 0
src/services/code-index/managed/chunker.ts

@@ -0,0 +1,250 @@
+// kilocode_change - new file
+/**
+ * Line-based file chunking for managed codebase indexing
+ *
+ * This module provides a simple, fast alternative to tree-sitter parsing.
+ * It chunks files based on line boundaries with configurable overlap,
+ * making it language-agnostic and 3-5x faster than AST-based approaches.
+ */
+
+import { createHash } from "crypto"
+import { v5 as uuidv5 } from "uuid"
+import { ManagedCodeChunk, ChunkerConfig } from "./types"
+import { MANAGED_MAX_CHUNK_CHARS, MANAGED_MIN_CHUNK_CHARS, MANAGED_OVERLAP_LINES } from "../constants"
+
+interface ChunkFileOptions {
+	/** Relative file path from workspace root */
+	filePath: string
+	/** File content to chunk */
+	content: string
+	/** SHA-256 hash of the file content */
+	fileHash: string
+	/** Organization ID */
+	organizationId: string
+	/** Project ID */
+	projectId: string
+	/** Git branch name */
+	gitBranch: string
+	/** Whether this is a base branch (main/develop) */
+	isBaseBranch: boolean
+	/** Chunker configuration (optional, uses defaults if not provided) */
+	config?: Partial<ChunkerConfig>
+}
+
+/**
+ * Chunks a file's content into overlapping segments based on line boundaries
+ *
+ * Algorithm:
+ * 1. Split content into lines
+ * 2. Accumulate lines until maxChunkChars is reached
+ * 3. Create chunk (always includes complete lines, never splits mid-line)
+ * 4. Start next chunk with overlapLines from previous chunk
+ * 5. Continue until all lines are processed
+ *
+ * @returns Array of code chunks with metadata
+ */
+export function chunkFile({
+	filePath,
+	content,
+	fileHash,
+	organizationId,
+	projectId,
+	gitBranch,
+	isBaseBranch,
+	config,
+}: ChunkFileOptions): ManagedCodeChunk[] {
+	const chunkerConfig: ChunkerConfig = {
+		maxChunkChars: config?.maxChunkChars ?? MANAGED_MAX_CHUNK_CHARS,
+		minChunkChars: config?.minChunkChars ?? MANAGED_MIN_CHUNK_CHARS,
+		overlapLines: config?.overlapLines ?? MANAGED_OVERLAP_LINES,
+	}
+
+	const lines = content.split("\n")
+	const chunks: ManagedCodeChunk[] = []
+
+	let currentChunk: string[] = []
+	let currentChunkChars = 0
+	let startLine = 1
+
+	for (let i = 0; i < lines.length; i++) {
+		const line = lines[i]
+		const lineLength = line.length + 1 // +1 for newline character
+
+		// Check if adding this line would exceed max chunk size
+		if (currentChunkChars + lineLength > chunkerConfig.maxChunkChars && currentChunk.length > 0) {
+			// Finalize current chunk if it meets minimum size
+			if (currentChunkChars >= chunkerConfig.minChunkChars) {
+				chunks.push(
+					createChunk({
+						lines: currentChunk,
+						startLine,
+						endLine: i,
+						filePath,
+						fileHash,
+						organizationId,
+						projectId,
+						gitBranch,
+						isBaseBranch,
+					}),
+				)
+
+				// Start next chunk with overlap
+				const overlapStart = Math.max(0, currentChunk.length - chunkerConfig.overlapLines)
+				currentChunk = currentChunk.slice(overlapStart)
+				currentChunkChars = currentChunk.reduce((sum, l) => sum + l.length + 1, 0)
+				startLine = i - (currentChunk.length - 1)
+			}
+		}
+
+		currentChunk.push(line)
+		currentChunkChars += lineLength
+	}
+
+	// Finalize last chunk if it meets minimum size
+	if (currentChunk.length > 0 && currentChunkChars >= chunkerConfig.minChunkChars) {
+		chunks.push(
+			createChunk({
+				lines: currentChunk,
+				startLine,
+				endLine: lines.length,
+				filePath,
+				fileHash,
+				organizationId,
+				projectId,
+				gitBranch,
+				isBaseBranch,
+			}),
+		)
+	}
+
+	return chunks
+}
+
+interface CreateChunkOptions {
+	/** Array of lines that make up this chunk */
+	lines: string[]
+	/** Starting line number (1-based) */
+	startLine: number
+	/** Ending line number (1-based, inclusive) */
+	endLine: number
+	/** Relative file path */
+	filePath: string
+	/** SHA-256 hash of the file */
+	fileHash: string
+	/** Organization ID */
+	organizationId: string
+	/** Project ID */
+	projectId: string
+	/** Git branch name */
+	gitBranch: string
+	/** Whether this is a base branch */
+	isBaseBranch: boolean
+}
+
+/**
+ * Creates a single chunk with all required metadata
+ *
+ * @returns ManagedCodeChunk with all metadata
+ */
+function createChunk({
+	lines,
+	startLine,
+	endLine,
+	filePath,
+	fileHash,
+	organizationId,
+	projectId,
+	gitBranch,
+	isBaseBranch,
+}: CreateChunkOptions): ManagedCodeChunk {
+	const content = lines.join("\n")
+	const chunkHash = generateChunkHash({ filePath, startLine, endLine })
+	const id = generateChunkId({ chunkHash, organizationId, gitBranch })
+
+	return {
+		id,
+		organizationId,
+		projectId,
+		filePath,
+		codeChunk: content,
+		startLine,
+		endLine,
+		chunkHash,
+		gitBranch,
+		isBaseBranch,
+	}
+}
+
+interface GenerateChunkHashOptions {
+	/** Relative file path */
+	filePath: string
+	/** Starting line number */
+	startLine: number
+	/** Ending line number */
+	endLine: number
+}
+
+/**
+ * Generates a unique hash for a chunk based on its content and location
+ *
+ * The hash includes:
+ * - File path (to distinguish same content in different files)
+ * - Line range (to distinguish same content at different locations)
+ * - Content length (quick differentiator)
+ * - Content preview (first 100 chars for uniqueness)
+ *
+ * @returns SHA-256 hash string
+ */
+function generateChunkHash({ filePath, startLine, endLine }: GenerateChunkHashOptions): string {
+	return createHash("sha256").update(`${filePath}-${startLine}-${endLine}`).digest("hex")
+}
+
+interface GenerateChunkIdOptions {
+	/** Hash of the chunk content and location */
+	chunkHash: string
+	/** Organization ID (used as UUID namespace) */
+	organizationId: string
+	/** Git branch name (included in hash for branch isolation) */
+	gitBranch: string
+}
+
+/**
+ * Generates a unique ID for a chunk
+ *
+ * The ID is a UUIDv5 based on the chunk hash and organization ID.
+ * This ensures:
+ * - Same content in same location = same ID (idempotent upserts)
+ * - Different organizations = different IDs (isolation)
+ * - Different branches = different IDs (branch isolation via chunk hash)
+ *
+ * @returns UUID string
+ */
+function generateChunkId({ chunkHash, organizationId, gitBranch }: GenerateChunkIdOptions): string {
+	// Include branch in the hash to ensure different IDs across branches
+	const branchAwareHash = createHash("sha256").update(`${chunkHash}-${gitBranch}`).digest("hex")
+
+	return uuidv5(branchAwareHash, organizationId)
+}
+
+/**
+ * Calculates the SHA-256 hash of file content
+ *
+ * @param content File content
+ * @returns SHA-256 hash string
+ */
+export function calculateFileHash(content: string): string {
+	return createHash("sha256").update(content).digest("hex")
+}
+
+/**
+ * Gets the default chunker configuration
+ *
+ * @returns Default ChunkerConfig
+ */
+export function getDefaultChunkerConfig(): ChunkerConfig {
+	return {
+		maxChunkChars: MANAGED_MAX_CHUNK_CHARS,
+		minChunkChars: MANAGED_MIN_CHUNK_CHARS,
+		overlapLines: MANAGED_OVERLAP_LINES,
+	}
+}

+ 353 - 0
src/services/code-index/managed/git-utils.ts

@@ -0,0 +1,353 @@
+// kilocode_change - new file
+/**
+ * Git utility functions for managed codebase indexing
+ *
+ * This module provides pure functions for interacting with git to determine
+ * branch state and file changes. Used to implement delta-based indexing.
+ */
+
+import { execGetLines } from "../../../shared/utils/exec"
+import { GitDiff } from "./types"
+
+/**
+ * Helper function to collect all lines from execGetLines into a single string
+ */
+async function collectOutput(cmd: string, cwd: string, context: string): Promise<string> {
+	const lines: string[] = []
+	for await (const line of execGetLines({ cmd, cwd, context })) {
+		lines.push(line)
+	}
+	return lines.join("\n").trim()
+}
+
+/**
+ * Gets the current git branch name
+ * @param workspacePath Path to the workspace
+ * @returns Current branch name (e.g., "main", "feature/new-api")
+ * @throws Error if not in a git repository
+ */
+export async function getCurrentBranch(workspacePath: string): Promise<string> {
+	try {
+		return await collectOutput("git rev-parse --abbrev-ref HEAD", workspacePath, "getting current git branch")
+	} catch (error) {
+		throw new Error(`Failed to get current git branch: ${error instanceof Error ? error.message : String(error)}`)
+	}
+}
+
+/**
+ * Gets the current git commit SHA
+ * @param workspacePath Path to the workspace
+ * @returns Current commit SHA (full 40-character hash)
+ * @throws Error if not in a git repository
+ */
+export async function getCurrentCommitSha(workspacePath: string): Promise<string> {
+	try {
+		return await collectOutput("git rev-parse HEAD", workspacePath, "getting current commit SHA")
+	} catch (error) {
+		throw new Error(`Failed to get current commit SHA: ${error instanceof Error ? error.message : String(error)}`)
+	}
+}
+
+/**
+ * Gets the remote URL for the repository
+ * @param workspacePath Path to the workspace
+ * @returns Remote URL (e.g., "https://github.com/org/repo.git")
+ * @throws Error if no remote is configured
+ */
+export async function getRemoteUrl(workspacePath: string): Promise<string> {
+	try {
+		return await collectOutput("git config --get remote.origin.url", workspacePath, "getting remote URL")
+	} catch (error) {
+		throw new Error(`Failed to get remote URL: ${error instanceof Error ? error.message : String(error)}`)
+	}
+}
+
+/**
+ * Checks if the workspace is a git repository
+ * @param workspacePath Path to the workspace
+ * @returns true if workspace is a git repository
+ */
+export async function isGitRepository(workspacePath: string): Promise<boolean> {
+	try {
+		await collectOutput("git rev-parse --git-dir", workspacePath, "checking if git repository")
+		return true
+	} catch {
+		return false
+	}
+}
+
+/**
+ * Gets the diff between a feature branch and base branch
+ * @param featureBranch The feature branch name
+ * @param baseBranch The base branch name (usually 'main' or 'develop')
+ * @param workspacePath Path to the workspace
+ * @returns GitDiff object with added, modified, and deleted files
+ * @throws Error if git command fails
+ */
+export async function getGitDiff(featureBranch: string, baseBranch: string, workspacePath: string): Promise<GitDiff> {
+	try {
+		// Get the merge base (commit where branches diverged)
+		const mergeBase = await collectOutput(
+			`git merge-base ${baseBranch} ${featureBranch}`,
+			workspacePath,
+			"getting merge base",
+		)
+
+		// Get diff between merge base and feature branch
+		const diffOutput = await collectOutput(
+			`git diff --name-status ${mergeBase}..${featureBranch}`,
+			workspacePath,
+			"getting git diff",
+		)
+
+		return parseDiffOutput(diffOutput)
+	} catch (error) {
+		throw new Error(
+			`Failed to get git diff between ${featureBranch} and ${baseBranch}: ${error instanceof Error ? error.message : String(error)}`,
+		)
+	}
+}
+
+/**
+ * Parses git diff --name-status output into structured format
+ * @param diffOutput Raw output from git diff --name-status
+ * @returns GitDiff object with categorized file changes
+ */
+function parseDiffOutput(diffOutput: string): GitDiff {
+	const added: string[] = []
+	const modified: string[] = []
+	const deleted: string[] = []
+
+	const lines = diffOutput.split("\n").filter((line) => line.trim())
+
+	for (const line of lines) {
+		const parts = line.split("\t")
+		if (parts.length < 2) continue
+
+		const status = parts[0]
+		const filePath = parts.slice(1).join("\t") // Handle file paths with tabs
+
+		switch (status[0]) {
+			case "A":
+				added.push(filePath)
+				break
+			case "M":
+				modified.push(filePath)
+				break
+			case "D":
+				deleted.push(filePath)
+				break
+			case "R": // Renamed - treat as delete + add
+				if (parts.length >= 3) {
+					deleted.push(parts[1])
+					added.push(parts[2])
+				}
+				break
+			case "C": // Copied - treat as add
+				if (parts.length >= 3) {
+					added.push(parts[2])
+				}
+				break
+			// Ignore other statuses (T=type change, U=unmerged, X=unknown)
+		}
+	}
+
+	return { added, modified, deleted }
+}
+
+/**
+ * Determines if a branch is a base branch (main or develop)
+ * @param branchName The branch name to check
+ * @param workspacePath Optional workspace path to check against remote default branch
+ * @returns true if this is a base branch
+ */
+export async function isBaseBranch(branchName: string, workspacePath?: string): Promise<boolean> {
+	const baseBranches = ["main", "master", "develop", "development"]
+	const isCommonBaseBranch = baseBranches.includes(branchName.toLowerCase())
+
+	// If it's a common base branch, return true
+	if (isCommonBaseBranch) {
+		return true
+	}
+
+	// If workspace path is provided, check if this branch is the remote's default branch
+	if (workspacePath) {
+		const defaultBranch = await getDefaultBranchFromRemote(workspacePath)
+		if (defaultBranch && defaultBranch.toLowerCase() === branchName.toLowerCase()) {
+			return true
+		}
+	}
+
+	return false
+}
+
+/**
+ * Gets the default branch name from the remote repository
+ * @param workspacePath Path to the workspace
+ * @returns The default branch name or null if it cannot be determined
+ */
+export async function getDefaultBranchFromRemote(workspacePath: string): Promise<string | null> {
+	try {
+		// Try to get the default branch from the remote's symbolic ref
+		const output = await collectOutput(
+			"git symbolic-ref refs/remotes/origin/HEAD",
+			workspacePath,
+			"getting default branch from remote",
+		)
+
+		// Output format: refs/remotes/origin/main
+		// Extract the branch name after the last /
+		const match = output.match(/refs\/remotes\/origin\/(.+)$/)
+		if (match && match[1]) {
+			return match[1]
+		}
+	} catch {
+		// If symbolic-ref fails, try to set it first
+		try {
+			await collectOutput("git remote set-head origin --auto", workspacePath, "setting remote head")
+
+			// Try again after setting
+			const output = await collectOutput(
+				"git symbolic-ref refs/remotes/origin/HEAD",
+				workspacePath,
+				"getting default branch from remote",
+			)
+
+			const match = output.match(/refs\/remotes\/origin\/(.+)$/)
+			if (match && match[1]) {
+				return match[1]
+			}
+		} catch {
+			// Failed to determine from remote
+		}
+	}
+
+	return null
+}
+
+/**
+ * Gets the base branch for a given feature branch
+ * First tries to get the default branch from the remote repository,
+ * then checks if common base branches exist, defaults to 'main'
+ * @param workspacePath Path to the workspace
+ * @returns The base branch name (e.g., 'main', 'canary', 'develop')
+ */
+export async function getBaseBranch(workspacePath: string): Promise<string> {
+	// First, try to get the default branch from the remote
+	const defaultBranch = await getDefaultBranchFromRemote(workspacePath)
+	if (defaultBranch) {
+		// Verify the branch exists locally
+		try {
+			await collectOutput(`git rev-parse --verify ${defaultBranch}`, workspacePath, "verifying branch exists")
+			return defaultBranch
+		} catch {
+			// Default branch from remote doesn't exist locally, continue to fallback
+		}
+	}
+
+	// Fallback: Check common base branch names
+	const commonBranches = ["main", "develop", "master"]
+	for (const branch of commonBranches) {
+		try {
+			await collectOutput(`git rev-parse --verify ${branch}`, workspacePath, "verifying branch exists")
+			return branch
+		} catch {
+			// Branch doesn't exist, try next
+		}
+	}
+
+	// Ultimate fallback
+	return "main"
+}
+
+/**
+ * Checks if there are uncommitted changes in the workspace
+ * @param workspacePath Path to the workspace
+ * @returns true if there are uncommitted changes
+ */
+export async function hasUncommittedChanges(workspacePath: string): Promise<boolean> {
+	try {
+		const status = await collectOutput("git status --porcelain", workspacePath, "checking for uncommitted changes")
+		return status.length > 0
+	} catch {
+		return false
+	}
+}
+
+/**
+ * Gets all files tracked by git using async generator for memory efficiency
+ * @param workspacePath Path to the workspace
+ * @yields File paths relative to workspace root
+ */
+export async function* getGitTrackedFiles(workspacePath: string): AsyncGenerator<string, void, unknown> {
+	try {
+		for await (const line of execGetLines({
+			cmd: "git ls-files",
+			cwd: workspacePath,
+			context: "getting git tracked files",
+		})) {
+			const trimmed = line.trim()
+			if (trimmed) {
+				yield trimmed
+			}
+		}
+	} catch (error) {
+		throw new Error(`Failed to get git tracked files: ${error instanceof Error ? error.message : String(error)}`)
+	}
+}
+
+/**
+ * Checks if the repository is in a detached HEAD state
+ * @param workspacePath Path to the workspace
+ * @returns true if in detached HEAD state
+ */
+export async function isDetachedHead(workspacePath: string): Promise<boolean> {
+	try {
+		const branch = await collectOutput(
+			"git rev-parse --abbrev-ref HEAD",
+			workspacePath,
+			"checking for detached HEAD",
+		)
+		return branch === "HEAD"
+	} catch {
+		return false
+	}
+}
+
+/**
+ * Gets the path to the .git/HEAD file
+ * @param workspacePath Path to the workspace
+ * @returns Path to .git/HEAD file
+ */
+export async function getGitHeadPath(workspacePath: string): Promise<string> {
+	try {
+		const gitDir = await collectOutput("git rev-parse --git-dir", workspacePath, "getting git directory")
+		return `${gitDir}/HEAD`
+	} catch (error) {
+		throw new Error(`Failed to get git HEAD path: ${error instanceof Error ? error.message : String(error)}`)
+	}
+}
+
+/**
+ * Gets the current git state (branch and commit)
+ * @param workspacePath Path to the workspace
+ * @returns Object with branch name and commit SHA, or null if detached
+ */
+export async function getGitState(
+	workspacePath: string,
+): Promise<{ branch: string; commit: string; isDetached: boolean } | null> {
+	try {
+		const isDetached = await isDetachedHead(workspacePath)
+
+		if (isDetached) {
+			return null
+		}
+
+		const branch = await getCurrentBranch(workspacePath)
+		const commit = await getCurrentCommitSha(workspacePath)
+
+		return { branch, commit, isDetached: false }
+	} catch {
+		return null
+	}
+}

+ 391 - 0
src/services/code-index/managed/git-watcher.ts

@@ -0,0 +1,391 @@
+// kilocode_change - new file
+/**
+ * Git-based watcher for managed codebase indexing
+ *
+ * This module provides a watcher that monitors git state changes (commits and branch switches)
+ * instead of file system changes. This avoids infinite loops with .gitignored files and
+ * ensures we only index committed changes.
+ *
+ * The watcher monitors:
+ * - Git commits (by watching .git/HEAD and branch refs)
+ * - Branch switches (by watching .git/HEAD)
+ * - Detached HEAD state (disables indexing)
+ */
+
+import * as vscode from "vscode"
+import * as path from "path"
+import * as fs from "fs"
+import { scanDirectory } from "./scanner"
+import { ManagedIndexingConfig, IndexerState } from "./types"
+import { getGitHeadPath, getGitState, isDetachedHead, getCurrentBranch } from "./git-utils"
+import { getServerManifest } from "./api-client"
+import { logger } from "../../../utils/logging"
+import { TelemetryService } from "@roo-code/telemetry"
+import { TelemetryEventName } from "@roo-code/types"
+
+/**
+ * Git state snapshot for change detection
+ */
+interface GitStateSnapshot {
+	branch: string
+	commit: string
+	isDetached: boolean
+}
+
+/**
+ * Creates a git-based watcher that monitors git state changes
+ *
+ * The watcher:
+ * - Monitors .git/HEAD for branch switches and commits
+ * - Triggers re-indexing after commits (files are naturally committed)
+ * - Triggers manifest refresh after branch switches
+ * - Disables indexing in detached HEAD state
+ *
+ * @param config Managed indexing configuration
+ * @param context VSCode extension context
+ * @param onStateChange Optional callback when git state changes
+ * @returns Disposable watcher instance
+ */
+export async function createGitWatcher(
+	config: ManagedIndexingConfig,
+	context: vscode.ExtensionContext,
+	onStateChange?: (state: IndexerState) => void,
+): Promise<vscode.Disposable> {
+	const disposables: vscode.Disposable[] = []
+	let currentState: GitStateSnapshot | null = null
+	let isProcessing = false
+
+	// Get initial git state - use async initialization
+	const initPromise = (async () => {
+		try {
+			const gitState = await getGitState(config.workspacePath)
+			console.log("[GitWatcher] Git state:", gitState)
+
+			if (gitState) {
+				currentState = gitState
+			} else {
+				onStateChange?.({
+					status: "idle",
+					message: "Detached HEAD state - indexing disabled",
+					gitBranch: undefined,
+				})
+			}
+		} catch (error) {
+			logger.error(`[GitWatcher] Failed to get initial git state:`, error)
+		}
+	})()
+
+	/**
+	 * Handles git state changes
+	 */
+	const handleGitChange = async () => {
+		if (isProcessing) {
+			return
+		}
+
+		try {
+			isProcessing = true
+
+			// Check for detached HEAD
+			if (await isDetachedHead(config.workspacePath)) {
+				currentState = null
+				onStateChange?.({
+					status: "idle",
+					message: "Detached HEAD state - indexing disabled",
+					gitBranch: undefined,
+				})
+				return
+			}
+
+			// Get new git state
+			const newState = await getGitState(config.workspacePath)
+			if (!newState) {
+				logger.warn("[GitWatcher] Could not determine git state")
+				return
+			}
+
+			// Check if state actually changed
+			if (currentState) {
+				const branchChanged = currentState.branch !== newState.branch
+				const commitChanged = currentState.commit !== newState.commit
+
+				if (!branchChanged && !commitChanged) {
+					return
+				}
+
+				if (branchChanged) {
+					await handleBranchChange(newState.branch, config, context, onStateChange)
+				} else if (commitChanged) {
+					await handleCommit(newState.branch, config, context, onStateChange)
+				}
+			} else {
+				// First time seeing a valid state (recovered from detached HEAD)
+				await handleBranchChange(newState.branch, config, context, onStateChange)
+			}
+
+			currentState = newState
+		} catch (error) {
+			TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+				error: error instanceof Error ? error.message : String(error),
+				stack: error instanceof Error ? error.stack : undefined,
+				location: "handleGitChange",
+			})
+		} finally {
+			isProcessing = false
+		}
+	}
+
+	/**
+	 * Handles branch changes - fetches new manifest and re-indexes
+	 */
+	const handleBranchChange = async (
+		newBranch: string,
+		config: ManagedIndexingConfig,
+		context: vscode.ExtensionContext,
+		onStateChange?: (state: IndexerState) => void,
+	) => {
+		try {
+			onStateChange?.({
+				status: "scanning",
+				message: `Branch changed to ${newBranch}, fetching manifest...`,
+				gitBranch: newBranch,
+			})
+
+			// Fetch manifest for new branch
+			let manifest
+			try {
+				manifest = await getServerManifest(
+					config.organizationId,
+					config.projectId,
+					newBranch,
+					config.kilocodeToken,
+				)
+			} catch (error) {
+				logger.warn(`[GitWatcher] No manifest found for ${newBranch}, will perform full scan`)
+			}
+
+			// Trigger re-scan with manifest
+			onStateChange?.({
+				status: "scanning",
+				message: `Scanning branch ${newBranch}...`,
+				gitBranch: newBranch,
+			})
+
+			const result = await scanDirectory(config, context, manifest, (progress) => {
+				onStateChange?.({
+					status: "scanning",
+					message: `Scanning: ${progress.filesProcessed}/${progress.filesTotal} files (${progress.chunksIndexed} chunks)`,
+					gitBranch: newBranch,
+				})
+			})
+
+			if (result.success) {
+				// Fetch updated manifest
+				let updatedManifest
+				try {
+					updatedManifest = await getServerManifest(
+						config.organizationId,
+						config.projectId,
+						newBranch,
+						config.kilocodeToken,
+					)
+				} catch (error) {
+					logger.warn("[GitWatcher] Failed to fetch updated manifest after scan")
+				}
+
+				onStateChange?.({
+					status: "watching",
+					message: `Branch ${newBranch} indexed successfully`,
+					gitBranch: newBranch,
+					lastSyncTime: Date.now(),
+					totalFiles: result.filesProcessed,
+					totalChunks: result.chunksIndexed,
+					manifest: updatedManifest
+						? {
+								totalFiles: updatedManifest.totalFiles,
+								totalChunks: updatedManifest.totalChunks,
+								lastUpdated: updatedManifest.lastUpdated,
+							}
+						: undefined,
+				})
+			} else {
+				throw new Error(`Scan failed with ${result.errors.length} errors`)
+			}
+		} catch (error) {
+			logger.error(`[GitWatcher] Failed to handle branch change:`, error)
+			onStateChange?.({
+				status: "error",
+				message: `Failed to index branch ${newBranch}: ${error instanceof Error ? error.message : String(error)}`,
+				error: error instanceof Error ? error.message : String(error),
+				gitBranch: newBranch,
+			})
+		}
+	}
+
+	/**
+	 * Handles commits - re-indexes changed files
+	 */
+	const handleCommit = async (
+		branch: string,
+		config: ManagedIndexingConfig,
+		context: vscode.ExtensionContext,
+		onStateChange?: (state: IndexerState) => void,
+	) => {
+		try {
+			onStateChange?.({
+				status: "scanning",
+				message: `New commit detected, updating index...`,
+				gitBranch: branch,
+			})
+
+			// Fetch current manifest
+			let manifest
+			try {
+				manifest = await getServerManifest(
+					config.organizationId,
+					config.projectId,
+					branch,
+					config.kilocodeToken,
+				)
+			} catch (error) {
+				logger.warn(`[GitWatcher] No manifest found for ${branch}`)
+			}
+
+			// Re-scan to pick up committed changes
+			const result = await scanDirectory(config, context, manifest, (progress) => {
+				onStateChange?.({
+					status: "scanning",
+					message: `Updating: ${progress.filesProcessed}/${progress.filesTotal} files (${progress.chunksIndexed} chunks)`,
+					gitBranch: branch,
+				})
+			})
+
+			if (result.success) {
+				// Fetch updated manifest
+				let updatedManifest
+				try {
+					updatedManifest = await getServerManifest(
+						config.organizationId,
+						config.projectId,
+						branch,
+						config.kilocodeToken,
+					)
+				} catch (error) {
+					logger.warn("[GitWatcher] Failed to fetch updated manifest after commit")
+				}
+
+				onStateChange?.({
+					status: "watching",
+					message: `Index updated after commit`,
+					gitBranch: branch,
+					lastSyncTime: Date.now(),
+					totalFiles: result.filesProcessed,
+					totalChunks: result.chunksIndexed,
+					manifest: updatedManifest
+						? {
+								totalFiles: updatedManifest.totalFiles,
+								totalChunks: updatedManifest.totalChunks,
+								lastUpdated: updatedManifest.lastUpdated,
+							}
+						: undefined,
+				})
+			} else {
+				throw new Error(`Scan failed with ${result.errors.length} errors`)
+			}
+		} catch (error) {
+			logger.error(`[GitWatcher] Failed to handle commit:`, error)
+			onStateChange?.({
+				status: "error",
+				message: `Failed to update index after commit: ${error instanceof Error ? error.message : String(error)}`,
+				error: error instanceof Error ? error.message : String(error),
+				gitBranch: branch,
+			})
+		}
+	}
+
+	// Watch .git/HEAD file for changes (branch switches and commits)
+	;(async () => {
+		try {
+			const gitHeadPath = await getGitHeadPath(config.workspacePath)
+			const absoluteGitHeadPath = path.isAbsolute(gitHeadPath)
+				? gitHeadPath
+				: path.join(config.workspacePath, gitHeadPath)
+
+			// Use VSCode's file watcher for .git/HEAD
+			const headWatcher = vscode.workspace.createFileSystemWatcher(absoluteGitHeadPath)
+
+			disposables.push(
+				headWatcher.onDidChange(() => {
+					console.log("[GitWatcher] ✓✓✓ .git/HEAD CHANGED - branch switch or commit detected")
+					logger.info("[GitWatcher] ✓ .git/HEAD changed - branch switch or commit detected")
+					handleGitChange()
+				}),
+			)
+
+			disposables.push(headWatcher)
+
+			// Watch all branch refs for commits (more reliable than watching individual branch)
+			try {
+				const gitDir = path.dirname(absoluteGitHeadPath)
+				const refsHeadsPattern = path.join(gitDir, "refs", "heads", "**")
+				const refsWatcher = vscode.workspace.createFileSystemWatcher(refsHeadsPattern)
+
+				disposables.push(
+					refsWatcher.onDidChange((uri) => {
+						console.log(`[GitWatcher] ✓✓✓ BRANCH REF CHANGED: ${uri.fsPath}`)
+						logger.info(`[GitWatcher] ✓ Branch ref changed: ${uri.fsPath}`)
+						handleGitChange()
+					}),
+				)
+
+				disposables.push(refsWatcher)
+			} catch (error) {
+				logger.warn(`[GitWatcher] Could not watch branch refs:`, error)
+			}
+
+			// Also watch packed-refs for repositories that use packed refs
+			try {
+				const gitDir = path.dirname(absoluteGitHeadPath)
+				const packedRefsPath = path.join(gitDir, "packed-refs")
+
+				if (fs.existsSync(packedRefsPath)) {
+					const packedRefsWatcher = vscode.workspace.createFileSystemWatcher(packedRefsPath)
+
+					disposables.push(
+						packedRefsWatcher.onDidChange(() => {
+							logger.info("[GitWatcher] ✓ packed-refs changed")
+							handleGitChange()
+						}),
+					)
+
+					disposables.push(packedRefsWatcher)
+				}
+			} catch (error) {
+				logger.warn(`[GitWatcher] Could not watch packed-refs:`, error)
+			}
+		} catch (error) {
+			logger.error(`[GitWatcher] Failed to create git watcher:`, error)
+			TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+				error: error instanceof Error ? error.message : String(error),
+				stack: error instanceof Error ? error.stack : undefined,
+				location: "createGitWatcher",
+			})
+		}
+	})()
+
+	// Add polling as a fallback (VSCode file watchers may not work reliably with .git files)
+	const pollingInterval = setInterval(() => {
+		handleGitChange()
+	}, 3000) // Poll every 3 seconds
+
+	disposables.push({
+		dispose: () => {
+			clearInterval(pollingInterval)
+		},
+	})
+
+	await initPromise
+
+	// Return composite disposable
+	return vscode.Disposable.from(...disposables)
+}

+ 85 - 0
src/services/code-index/managed/index.ts

@@ -0,0 +1,85 @@
+// kilocode_change - new file
+/**
+ * Managed Codebase Indexing
+ *
+ * This module provides a complete, standalone indexing system for Kilo Code
+ * organization users. It is completely separate from the local indexing system
+ * and uses a simpler, more efficient approach:
+ *
+ * - Line-based chunking (no tree-sitter)
+ * - Delta indexing (only changed files on feature branches)
+ * - Server-side embeddings (no client computation)
+ * - Client-driven search (client sends deleted files)
+ * - Functional architecture (stateless, composable functions)
+ *
+ * @example
+ * ```typescript
+ * import { startIndexing, search, createManagedIndexingConfig } from './managed'
+ *
+ * // Create configuration
+ * const config = createManagedIndexingConfig(
+ *   organizationId,
+ *   projectId,
+ *   kilocodeToken,
+ *   workspacePath
+ * )
+ *
+ * // Start indexing
+ * const disposable = await startIndexing(config, context, (state) => {
+ *   console.log('State:', state)
+ * })
+ *
+ * // Search
+ * const results = await search('my query', config)
+ *
+ * // Stop indexing
+ * disposable.dispose()
+ * ```
+ */
+
+// Main API
+export { startIndexing, search, getIndexerState, createManagedIndexingConfig } from "./indexer"
+
+// Scanner functions (for advanced usage)
+export { scanDirectory, indexFile, handleFileDeleted } from "./scanner"
+
+// Watcher functions
+export { createGitWatcher } from "./git-watcher"
+
+// Chunker functions
+export { chunkFile, calculateFileHash, getDefaultChunkerConfig } from "./chunker"
+
+// API client functions
+export { upsertChunks, searchCode, deleteFiles, getServerManifest } from "./api-client"
+
+// Git utilities
+export {
+	getCurrentBranch,
+	getCurrentCommitSha,
+	getRemoteUrl,
+	getGitDiff,
+	isBaseBranch,
+	getBaseBranch,
+	isGitRepository,
+	hasUncommittedChanges,
+	isDetachedHead,
+	getGitHeadPath,
+	getGitState,
+	getGitTrackedFiles,
+} from "./git-utils"
+
+// Types
+export type {
+	ManagedCodeChunk,
+	ChunkerConfig,
+	GitDiff,
+	ManagedIndexingConfig,
+	ScanProgress,
+	ScanResult,
+	ManifestFileEntry,
+	ServerManifest,
+	SearchRequest,
+	SearchResult,
+	FileChangeEvent,
+	IndexerState,
+} from "./types"

+ 378 - 0
src/services/code-index/managed/indexer.ts

@@ -0,0 +1,378 @@
+// kilocode_change - new file
+/**
+ * Main orchestration module for managed codebase indexing
+ *
+ * This module provides the high-level API for managed indexing operations:
+ * - Starting/stopping indexing
+ * - Searching the index
+ * - Managing state
+ */
+
+import * as vscode from "vscode"
+import { scanDirectory } from "./scanner"
+import { createGitWatcher } from "./git-watcher"
+import { searchCode as apiSearchCode, getServerManifest } from "./api-client"
+import { getCurrentBranch, getGitDiff, isGitRepository, isDetachedHead } from "./git-utils"
+import { ManagedIndexingConfig, IndexerState, SearchResult, ServerManifest } from "./types"
+import { getDefaultChunkerConfig } from "./chunker"
+import { logger } from "../../../utils/logging"
+import { TelemetryService } from "@roo-code/telemetry"
+import { TelemetryEventName } from "@roo-code/types"
+
+/**
+ * Starts the managed indexing process
+ *
+ * This function:
+ * 1. Validates the workspace is a git repository
+ * 2. Performs initial scan (full for main, delta for feature branches)
+ * 3. Starts file watcher for incremental updates
+ * 4. Reports progress via state callback
+ *
+ * @param config Managed indexing configuration
+ * @param context VSCode extension context
+ * @param onStateChange Optional state change callback
+ * @returns Disposable that stops the indexer when disposed
+ */
+export async function startIndexing(
+	config: ManagedIndexingConfig,
+	context: vscode.ExtensionContext,
+	onStateChange?: (state: IndexerState) => void,
+): Promise<vscode.Disposable> {
+	try {
+		// Validate git repository
+		if (!(await isGitRepository(config.workspacePath))) {
+			const error = new Error("Workspace is not a git repository")
+			onStateChange?.({
+				status: "error",
+				message: "Not a git repository",
+				error: error.message,
+			})
+			throw error
+		}
+
+		// Check for detached HEAD state
+		if (await isDetachedHead(config.workspacePath)) {
+			const error = new Error("Repository is in detached HEAD state")
+			onStateChange?.({
+				status: "idle",
+				message: "Detached HEAD state - indexing disabled",
+			})
+			logger.warn("[Managed Indexing] Detached HEAD state detected - indexing disabled")
+			// Return a no-op disposable
+			return vscode.Disposable.from({
+				dispose: () => {
+					logger.info("[Managed Indexing] Disposable called (detached HEAD)")
+				},
+			})
+		}
+
+		// Get current branch
+		const gitBranch = await getCurrentBranch(config.workspacePath)
+
+		// Fetch server manifest to determine what's already indexed
+		let manifest: ServerManifest | undefined
+		let serverHasNoData = false
+		try {
+			manifest = await getServerManifest(config.organizationId, config.projectId, gitBranch, config.kilocodeToken)
+			logger.info(
+				`[Managed Indexing] Server manifest: ${manifest.totalFiles} files, ${manifest.totalChunks} chunks`,
+			)
+		} catch (error) {
+			// Check if this is a 404 (no data on server)
+			const is404 =
+				error && typeof error === "object" && "response" in error && (error as any).response?.status === 404
+
+			if (is404) {
+				logger.info("[Managed Indexing] No data on server (404), will perform full scan")
+				serverHasNoData = true
+			} else {
+				// Safely extract error message to avoid circular reference issues
+				const errorMsg = error instanceof Error ? error.message : String(error)
+				logger.warn(`[Managed Indexing] Failed to fetch manifest, will perform full scan: ${errorMsg}`)
+			}
+			// Continue without manifest - scanner will index everything
+		}
+
+		// Update state: scanning
+		onStateChange?.({
+			status: "scanning",
+			message: `Starting scan on branch ${gitBranch}...`,
+			gitBranch,
+		})
+
+		// Perform initial scan with manifest for intelligent delta indexing
+		const result = await scanDirectory(config, context, manifest, (progress) => {
+			onStateChange?.({
+				status: "scanning",
+				message: `Scanning: ${progress.filesProcessed}/${progress.filesTotal} files (${progress.chunksIndexed} chunks)`,
+				gitBranch,
+			})
+		})
+
+		if (!result.success) {
+			// Log all errors for debugging - safely extract error messages
+			logger.error(`Scan failed with ${result.errors.length} errors:`)
+			result.errors.forEach((err, index) => {
+				// Safely extract error message and stack to avoid circular reference issues
+				try {
+					const message = err.message || String(err)
+					logger.error(`  Error ${index + 1}: ${message}`)
+					if (err.stack) {
+						logger.error(`    Stack: ${err.stack}`)
+					}
+				} catch (e) {
+					logger.error(`  Error ${index + 1}: [Unable to extract error message]`)
+				}
+			})
+
+			// Create a detailed error message - safely extract error messages
+			const errorMessages = result.errors.slice(0, 5).map((e) => {
+				// Safely extract message, handling potential circular references
+				try {
+					return e.message || String(e)
+				} catch {
+					return "Unknown error"
+				}
+			})
+			const errorSummary = errorMessages.join("; ")
+			const remainingCount = result.errors.length > 5 ? ` and ${result.errors.length - 5} more` : ""
+			throw new Error(`Scan failed with ${result.errors.length} errors: ${errorSummary}${remainingCount}`)
+		}
+
+		console.log(
+			`[Managed Indexing] Initial scan complete: ${result.filesProcessed} files processed, ${result.filesSkipped} skipped, ${result.chunksIndexed} chunks indexed`,
+		)
+		logger.info(
+			`Initial scan complete: ${result.filesProcessed} files processed, ${result.chunksIndexed} chunks indexed`,
+		)
+
+		// Fetch updated manifest after indexing to get accurate server state
+		// This is important for feature branches where we might have skipped all files
+		// but there's still data on the server from the base branch
+		let updatedManifest: ServerManifest | undefined
+		try {
+			updatedManifest = await getServerManifest(
+				config.organizationId,
+				config.projectId,
+				gitBranch,
+				config.kilocodeToken,
+			)
+			console.log(
+				`[Managed Indexing] Server manifest: ${updatedManifest.totalFiles} files, ${updatedManifest.totalChunks} chunks`,
+			)
+		} catch (error) {
+			console.log("[Managed Indexing] No manifest found on server (404 or error)")
+			// Safely extract error message to avoid circular reference issues
+			const errorMsg = error instanceof Error ? error.message : String(error)
+			logger.warn(`[Managed Indexing] Failed to fetch updated manifest after indexing: ${errorMsg}`)
+		}
+
+		// Check if we have indexed data - either from this scan OR from the server
+		// For feature branches, we might skip all files but still have data from base branch
+		const hasIndexedData =
+			result.chunksIndexed > 0 ||
+			result.filesProcessed > 0 ||
+			(updatedManifest && updatedManifest.totalChunks > 0)
+
+		console.log(`[Managed Indexing] Has indexed data: ${hasIndexedData}`)
+		console.log(`  - Chunks indexed this scan: ${result.chunksIndexed}`)
+		console.log(`  - Files processed this scan: ${result.filesProcessed}`)
+		console.log(`  - Server manifest chunks: ${updatedManifest?.totalChunks ?? 0}`)
+
+		// Start git-based watcher to monitor commits and branch changes
+		console.log("[Managed Indexing] ========== STARTING GIT WATCHER ==========")
+		let gitWatcher: vscode.Disposable | undefined
+		try {
+			console.log("[Managed Indexing] Calling createGitWatcher...")
+			gitWatcher = await createGitWatcher(config, context, onStateChange)
+			console.log("[Managed Indexing] ✓ Git watcher created successfully")
+			logger.info("[Managed Indexing] Git watcher started successfully")
+		} catch (error) {
+			// Safely extract error message to avoid circular reference issues
+			const errorMsg = error instanceof Error ? error.message : String(error)
+			console.error(`[Managed Indexing] ✗ Failed to start git watcher: ${errorMsg}`)
+			logger.error(`[Managed Indexing] Failed to start git watcher: ${errorMsg}`)
+			// Continue without watcher - manual refresh will still work
+		}
+
+		// Update state based on whether we have data
+		if (hasIndexedData) {
+			onStateChange?.({
+				status: "watching",
+				message: "Index up-to-date. Watching for git commits and branch changes.",
+				gitBranch,
+				lastSyncTime: Date.now(),
+				totalFiles: result.filesProcessed,
+				totalChunks: result.chunksIndexed,
+				manifest: updatedManifest
+					? {
+							totalFiles: updatedManifest.totalFiles,
+							totalChunks: updatedManifest.totalChunks,
+							lastUpdated: updatedManifest.lastUpdated,
+						}
+					: undefined,
+			})
+		} else {
+			// No data indexed - set to idle state to indicate re-scan is needed
+			onStateChange?.({
+				status: "idle",
+				message: "No files indexed. Click 'Start Indexing' to begin.",
+				gitBranch,
+			})
+		}
+
+		// Return disposable that cleans up watcher and state
+		return vscode.Disposable.from({
+			dispose: () => {
+				if (gitWatcher) {
+					gitWatcher.dispose()
+				}
+				onStateChange?.({
+					status: "idle",
+					message: "Indexing stopped",
+					gitBranch,
+				})
+			},
+		})
+	} catch (error) {
+		const err = error instanceof Error ? error : new Error(String(error))
+		logger.error(`Failed to start indexing: ${err.message}`)
+
+		TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+			error: err.message,
+			stack: err.stack,
+			location: "startIndexing",
+		})
+
+		onStateChange?.({
+			status: "error",
+			message: `Failed to start indexing: ${err.message}`,
+			error: err.message,
+		})
+
+		throw err
+	}
+}
+
+/**
+ * Searches the managed index with branch-aware preferences
+ *
+ * This function:
+ * 1. Gets deleted files from git diff (for feature branches)
+ * 2. Sends search request with branch preferences
+ * 3. Returns results with feature branch files preferred over main
+ *
+ * @param query Search query
+ * @param config Managed indexing configuration
+ * @param path Optional directory path filter
+ * @returns Array of search results sorted by relevance
+ */
+export async function search(query: string, config: ManagedIndexingConfig, path?: string): Promise<SearchResult[]> {
+	try {
+		const gitBranch = await getCurrentBranch(config.workspacePath)
+
+		// Get deleted files for feature branches
+		let excludeFiles: string[] = []
+		if (gitBranch !== "main" && gitBranch !== "master" && gitBranch !== "develop") {
+			try {
+				const diff = await getGitDiff(gitBranch, "main", config.workspacePath)
+				excludeFiles = diff.deleted
+			} catch (error) {
+				// If git diff fails, continue without exclusions
+				logger.warn(`Failed to get git diff for search: ${error}`)
+			}
+		}
+
+		// Perform search
+		const results = await apiSearchCode(
+			{
+				query,
+				organizationId: config.organizationId,
+				projectId: config.projectId,
+				preferBranch: gitBranch,
+				fallbackBranch: "main",
+				excludeFiles,
+				path,
+			},
+			config.kilocodeToken,
+		)
+
+		logger.info(`Search for "${query}" returned ${results.length} results`)
+
+		return results
+	} catch (error) {
+		const err = error instanceof Error ? error : new Error(String(error))
+		logger.error(`Search failed: ${err.message}`)
+
+		TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+			error: err.message,
+			stack: err.stack,
+			location: "search",
+			query,
+		})
+
+		throw err
+	}
+}
+
+/**
+ * Gets the current indexer state
+ *
+ * @param config Managed indexing configuration
+ * @param context VSCode extension context
+ * @returns Current indexer state
+ */
+export async function getIndexerState(
+	config: ManagedIndexingConfig,
+	context: vscode.ExtensionContext,
+): Promise<IndexerState> {
+	try {
+		if (!(await isGitRepository(config.workspacePath))) {
+			return {
+				status: "error",
+				message: "Not a git repository",
+				error: "Workspace is not a git repository",
+			}
+		}
+
+		const gitBranch = await getCurrentBranch(config.workspacePath)
+
+		return {
+			status: "idle",
+			message: "Ready",
+			gitBranch,
+		}
+	} catch (error) {
+		return {
+			status: "error",
+			message: "Failed to get state",
+			error: error instanceof Error ? error.message : String(error),
+		}
+	}
+}
+
+/**
+ * Creates a managed indexing configuration from organization credentials
+ *
+ * @param organizationId Organization ID
+ * @param projectId Project ID
+ * @param kilocodeToken Authentication token
+ * @param workspacePath Workspace root path
+ * @returns Managed indexing configuration with defaults
+ */
+export function createManagedIndexingConfig(
+	organizationId: string,
+	projectId: string,
+	kilocodeToken: string,
+	workspacePath: string,
+): ManagedIndexingConfig {
+	return {
+		organizationId,
+		projectId,
+		kilocodeToken,
+		workspacePath,
+		chunker: getDefaultChunkerConfig(),
+		batchSize: 60,
+		autoSync: true,
+	}
+}

+ 429 - 0
src/services/code-index/managed/scanner.ts

@@ -0,0 +1,429 @@
+// kilocode_change - new file
+/**
+ * File scanner for managed codebase indexing
+ *
+ * This module provides functions for scanning directories and indexing files.
+ * It implements delta-based indexing where feature branches only index changed files.
+ */
+
+import * as vscode from "vscode"
+import * as path from "path"
+import { stat } from "fs/promises"
+import pLimit from "p-limit"
+import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController"
+import { isPathInIgnoredDirectory } from "../../glob/ignore-utils"
+import { scannerExtensions } from "../shared/supported-extensions"
+import { generateRelativeFilePath } from "../shared/get-relative-path"
+import { chunkFile, calculateFileHash } from "./chunker"
+import { upsertChunks, deleteFiles } from "./api-client"
+import {
+	getCurrentBranch,
+	getGitDiff,
+	isBaseBranch as checkIsBaseBranch,
+	isGitRepository,
+	getGitTrackedFiles,
+	getBaseBranch,
+} from "./git-utils"
+import { ManagedIndexingConfig, ScanProgress, ScanResult, ServerManifest } from "./types"
+import { MAX_FILE_SIZE_BYTES, MANAGED_MAX_CONCURRENT_FILES, MANAGED_BATCH_SIZE } from "../constants"
+import { TelemetryService } from "@roo-code/telemetry"
+import { TelemetryEventName } from "@roo-code/types"
+import { logger } from "../../../utils/logging"
+
+/**
+ * Helper function to compare two arrays for equality
+ */
+function arraysEqual<T>(a: T[], b: T[]): boolean {
+	if (a.length !== b.length) return false
+	for (let i = 0; i < a.length; i++) {
+		if (a[i] !== b[i]) return false
+	}
+	return true
+}
+
+/**
+ * Scans a directory and indexes files based on branch strategy
+ *
+ * - Main branch: Scans all files
+ * - Feature branch: Only scans files changed from main (delta)
+ *
+ * @param config Managed indexing configuration
+ * @param context VSCode extension context
+ * @param manifest Optional server manifest for intelligent delta indexing
+ * @param onProgress Optional progress callback
+ * @param forceFullScan Force a full scan even on feature branches (used when server has no data)
+ * @returns Scan result with statistics
+ */
+export async function scanDirectory(
+	config: ManagedIndexingConfig,
+	context: vscode.ExtensionContext,
+	manifest?: ServerManifest,
+	onProgress?: (progress: ScanProgress) => void,
+	forceFullScan: boolean = false,
+): Promise<ScanResult> {
+	const errors: Error[] = []
+
+	try {
+		// Check if workspace is a git repository
+		if (!(await isGitRepository(config.workspacePath))) {
+			throw new Error("Workspace is not a git repository")
+		}
+
+		// Get current branch
+		const currentBranch = await getCurrentBranch(config.workspacePath)
+		const isBase = await checkIsBaseBranch(currentBranch, config.workspacePath)
+
+		// Determine which files to scan
+		const filesToScan = await getFilesToScan(config.workspacePath, currentBranch, isBase)
+
+		console.info(`Scanning ${filesToScan.length} files on branch ${currentBranch} (isBase: ${isBase})`)
+
+		// Process files with manifest for intelligent skipping
+		const result = await processFiles(filesToScan, config, context, currentBranch, isBase, manifest, onProgress)
+
+		return {
+			success: result.errors.length === 0,
+			filesProcessed: result.filesProcessed,
+			filesSkipped: result.filesSkipped,
+			chunksIndexed: result.chunksIndexed,
+			errors: result.errors,
+		}
+	} catch (error) {
+		const err = error instanceof Error ? error : new Error(String(error))
+		errors.push(err)
+		console.error(`Scan directory failed: ${err.message}`)
+		TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+			error: err.message,
+			stack: err.stack,
+			location: "scanDirectory",
+		})
+
+		return {
+			success: false,
+			filesProcessed: 0,
+			filesSkipped: 0,
+			chunksIndexed: 0,
+			errors,
+		}
+	}
+}
+
+/**
+ * Determines which files to scan based on branch strategy
+ *
+ * @param workspacePath Workspace root path
+ * @param currentBranch Current git branch
+ * @param isBase Whether current branch is a base branch
+ * @returns Array of file paths to scan
+ */
+async function getFilesToScan(workspacePath: string, currentBranch: string, isBase: boolean): Promise<string[]> {
+	if (isBase) {
+		// Base branch: scan all files
+		return await getAllSupportedFiles(workspacePath)
+	} else {
+		// Feature branch: only scan changed files
+		const baseBranch = await getBaseBranch(workspacePath)
+		const diff = await getGitDiff(currentBranch, baseBranch, workspacePath)
+		const changedFiles = [...diff.added, ...diff.modified]
+
+		// Convert relative paths from git to absolute paths and filter to only supported files
+		return changedFiles
+			.filter((file) => {
+				const ext = path.extname(file).toLowerCase()
+				return scannerExtensions.includes(ext)
+			})
+			.map((file) => path.join(workspacePath, file))
+	}
+}
+
+/**
+ * Gets all supported files in the workspace that are tracked by git
+ *
+ * @param workspacePath Workspace root path
+ * @returns Array of supported file paths (absolute paths)
+ */
+async function getAllSupportedFiles(workspacePath: string): Promise<string[]> {
+	// Get all git-tracked files (relative paths) using async generator
+	const gitTrackedFiles: string[] = []
+	for await (const file of getGitTrackedFiles(workspacePath)) {
+		gitTrackedFiles.push(file)
+	}
+
+	logger.info(`Found ${gitTrackedFiles.length} git-tracked files`)
+
+	// Initialize ignore controller for .rooignore
+	const ignoreController = new RooIgnoreController(workspacePath)
+	await ignoreController.initialize()
+
+	// Filter by .rooignore
+	const allowedPaths = ignoreController.filterPaths(gitTrackedFiles)
+
+	logger.info(`After .rooignore filter: ${allowedPaths.length} files`)
+
+	// Filter by supported extensions and convert to absolute paths
+	const supportedFiles = allowedPaths
+		.filter((filePath) => {
+			const ext = path.extname(filePath).toLowerCase()
+
+			// Check if file is in an ignored directory
+			if (isPathInIgnoredDirectory(filePath)) {
+				return false
+			}
+
+			return scannerExtensions.includes(ext)
+		})
+		.map((filePath) => path.join(workspacePath, filePath))
+
+	logger.info(`After extension filter: ${supportedFiles.length} files`)
+
+	return supportedFiles
+}
+
+/**
+ * Processes files in parallel with batching
+ *
+ * @param filePaths Files to process
+ * @param config Indexing configuration
+ * @param context VSCode extension context
+ * @param gitBranch Current git branch
+ * @param isBase Whether this is a base branch
+ * @param manifest Optional server manifest for intelligent skipping
+ * @param onProgress Progress callback
+ * @returns Processing result
+ */
+async function processFiles(
+	filePaths: string[],
+	config: ManagedIndexingConfig,
+	context: vscode.ExtensionContext,
+	gitBranch: string,
+	isBase: boolean,
+	manifest?: ServerManifest,
+	onProgress?: (progress: ScanProgress) => void,
+): Promise<{
+	filesProcessed: number
+	filesSkipped: number
+	chunksIndexed: number
+	errors: Error[]
+}> {
+	const limit = pLimit(MANAGED_MAX_CONCURRENT_FILES)
+	const errors: Error[] = []
+	let filesProcessed = 0
+	let filesSkipped = 0
+	let chunksIndexed = 0
+
+	// Batch accumulator
+	let currentBatch: any[] = []
+
+	const processBatch = async () => {
+		if (currentBatch.length === 0) return
+
+		try {
+			await upsertChunks(currentBatch, config.kilocodeToken)
+			chunksIndexed += currentBatch.length
+		} catch (error) {
+			errors.push(error instanceof Error ? error : new Error(String(error)))
+		}
+
+		currentBatch = []
+	}
+
+	const promises = filePaths.map((filePath) =>
+		limit(async () => {
+			try {
+				// Check file size
+				const stats = await stat(filePath)
+				if (stats.size > MAX_FILE_SIZE_BYTES) {
+					filesSkipped++
+					console.warn(`Skipping large file: ${filePath} (${stats.size} bytes)`)
+					return
+				}
+
+				// Read file content
+				const content = await vscode.workspace.fs
+					.readFile(vscode.Uri.file(filePath))
+					.then((buffer) => Buffer.from(buffer).toString("utf-8"))
+
+				// Calculate file hash
+				const fileHash = calculateFileHash(content)
+
+				// Get relative path for manifest comparison
+				const relativeFilePath = generateRelativeFilePath(filePath, config.workspacePath)
+
+				// Chunk the file
+				const chunks = chunkFile({
+					filePath: relativeFilePath,
+					content,
+					fileHash,
+					organizationId: config.organizationId,
+					projectId: config.projectId,
+					gitBranch,
+					isBaseBranch: isBase,
+					config: config.chunker,
+				})
+
+				// Extract chunk hashes for comparison
+				const currentChunkHashes = chunks.map((c) => c.chunkHash)
+
+				// Check if file is already indexed on server with matching chunk hashes
+				if (manifest) {
+					const manifestEntry = manifest.files.find((f) => f.filePath === relativeFilePath)
+					if (manifestEntry && arraysEqual(currentChunkHashes, manifestEntry.chunkHashes)) {
+						// File already indexed on server with same chunks - skip
+						filesSkipped++
+						logger.info(`[Scanner] Skipping ${relativeFilePath} - already indexed on server`)
+						return
+					}
+				}
+
+				// Add to batch
+				currentBatch.push(...chunks)
+
+				// Process batch if threshold reached
+				if (currentBatch.length >= MANAGED_BATCH_SIZE) {
+					await processBatch()
+				}
+
+				filesProcessed++
+
+				// Report progress
+				onProgress?.({
+					filesProcessed,
+					filesTotal: filePaths.length,
+					chunksIndexed,
+					currentFile: relativeFilePath,
+				})
+			} catch (error) {
+				const err = error instanceof Error ? error : new Error(String(error))
+				// Create a more descriptive error with file context
+				const contextualError = new Error(`Failed to process ${filePath}: ${err.message}`)
+				contextualError.stack = err.stack
+				errors.push(contextualError)
+				console.error(`Error processing file ${filePath}: ${err.message}`)
+				if (err.stack) {
+					console.error(`Stack trace: ${err.stack}`)
+				}
+				TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+					error: err.message,
+					stack: err.stack,
+					location: "processFiles",
+					filePath,
+				})
+			}
+		}),
+	)
+
+	// Wait for all files to be processed
+	await Promise.all(promises)
+
+	// Process remaining batch
+	await processBatch()
+
+	return {
+		filesProcessed,
+		filesSkipped,
+		chunksIndexed,
+		errors,
+	}
+}
+
+/**
+ * Indexes a single file
+ *
+ * This is used by the file watcher for incremental updates.
+ *
+ * @param filePath Absolute file path
+ * @param config Indexing configuration
+ * @param context VSCode extension context
+ */
+export async function indexFile(
+	filePath: string,
+	config: ManagedIndexingConfig,
+	context: vscode.ExtensionContext,
+): Promise<void> {
+	try {
+		// Get current branch
+		const gitBranch = await getCurrentBranch(config.workspacePath)
+		const isBase = await checkIsBaseBranch(gitBranch, config.workspacePath)
+
+		// Check file size
+		const stats = await stat(filePath)
+		if (stats.size > MAX_FILE_SIZE_BYTES) {
+			console.warn(`Skipping large file: ${filePath} (${stats.size} bytes)`)
+			return
+		}
+
+		// Read file content
+		const content = await vscode.workspace.fs
+			.readFile(vscode.Uri.file(filePath))
+			.then((buffer) => Buffer.from(buffer).toString("utf-8"))
+
+		// Calculate file hash
+		const fileHash = calculateFileHash(content)
+
+		// Get relative path
+		const relativeFilePath = generateRelativeFilePath(filePath, config.workspacePath)
+
+		// Delete old chunks for this file on this branch
+		await deleteFiles([relativeFilePath], gitBranch, config.organizationId, config.projectId, config.kilocodeToken)
+
+		// Chunk the file
+		const chunks = chunkFile({
+			filePath: relativeFilePath,
+			content,
+			fileHash,
+			organizationId: config.organizationId,
+			projectId: config.projectId,
+			gitBranch,
+			isBaseBranch: isBase,
+			config: config.chunker,
+		})
+
+		// Upsert new chunks
+		await upsertChunks(chunks, config.kilocodeToken)
+
+		console.info(`Indexed file: ${relativeFilePath} (${chunks.length} chunks)`)
+	} catch (error) {
+		const err = error instanceof Error ? error : new Error(String(error))
+		console.error(`Failed to index file ${filePath}: ${err.message}`)
+		TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+			error: err.message,
+			stack: err.stack,
+			location: "indexFile",
+			filePath,
+		})
+		throw err
+	}
+}
+
+/**
+ * Handles file deletion
+ *
+ * @param filePath Absolute file path
+ * @param config Indexing configuration
+ * @param context VSCode extension context
+ */
+export async function handleFileDeleted(
+	filePath: string,
+	config: ManagedIndexingConfig,
+	context: vscode.ExtensionContext,
+): Promise<void> {
+	try {
+		const gitBranch = await getCurrentBranch(config.workspacePath)
+		const relativeFilePath = generateRelativeFilePath(filePath, config.workspacePath)
+
+		// Delete chunks from server
+		await deleteFiles([relativeFilePath], gitBranch, config.organizationId, config.projectId, config.kilocodeToken)
+
+		console.info(`Deleted file from index: ${relativeFilePath}`)
+	} catch (error) {
+		const err = error instanceof Error ? error : new Error(String(error))
+		console.error(`Failed to handle file deletion ${filePath}: ${err.message}`)
+		TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+			error: err.message,
+			stack: err.stack,
+			location: "handleFileDeleted",
+			filePath,
+		})
+		throw err
+	}
+}

+ 222 - 0
src/services/code-index/managed/types.ts

@@ -0,0 +1,222 @@
+// kilocode_change - new file
+/**
+ * Type definitions for Managed Codebase Indexing
+ *
+ * This module defines the core types used throughout the managed indexing system.
+ * The system uses a delta-based approach where only the main branch has a full index,
+ * and feature branches only index their changes (added/modified files).
+ */
+
+/**
+ * A code chunk with git metadata for managed indexing
+ */
+export interface ManagedCodeChunk {
+	/** Unique identifier for this chunk (uuidv5 based on chunk hash + org ID) */
+	id: string
+	/** Organization ID */
+	organizationId: string
+	/** Project ID */
+	projectId: string
+	/** Relative file path from workspace root */
+	filePath: string
+	/** The actual code content of this chunk */
+	codeChunk: string
+	/** Starting line number (1-based) */
+	startLine: number
+	/** Ending line number (1-based, inclusive) */
+	endLine: number
+	/** Hash of the chunk content for deduplication */
+	chunkHash: string
+	/** Git branch this chunk belongs to */
+	gitBranch: string
+	/** Whether this is from a base branch (main/develop) */
+	isBaseBranch: boolean
+}
+
+/**
+ * Configuration for the line-based chunker
+ */
+export interface ChunkerConfig {
+	/** Maximum characters per chunk (default: 1000) */
+	maxChunkChars: number
+	/** Minimum characters per chunk (default: 200) */
+	minChunkChars: number
+	/** Number of lines to overlap between chunks (default: 5) */
+	overlapLines: number
+}
+
+/**
+ * Git diff result showing changes between branches
+ */
+export interface GitDiff {
+	/** Files added on the feature branch */
+	added: string[]
+	/** Files modified on the feature branch */
+	modified: string[]
+	/** Files deleted on the feature branch */
+	deleted: string[]
+}
+
+/**
+ * Configuration for managed indexing
+ */
+export interface ManagedIndexingConfig {
+	/** Organization ID */
+	organizationId: string
+	/** Project ID */
+	projectId: string
+	/** Kilo Code authentication token */
+	kilocodeToken: string
+	/** Workspace root path */
+	workspacePath: string
+	/** Chunker configuration */
+	chunker: ChunkerConfig
+	/** Batch size for API calls (default: 60) */
+	batchSize: number
+	/** Whether to auto-sync on file changes (default: true) */
+	autoSync: boolean
+}
+
+/**
+ * Progress information during scanning
+ */
+export interface ScanProgress {
+	/** Number of files processed so far */
+	filesProcessed: number
+	/** Total number of files to process */
+	filesTotal: number
+	/** Number of chunks indexed so far */
+	chunksIndexed: number
+	/** Current file being processed (optional) */
+	currentFile?: string
+}
+
+/**
+ * Result of a directory scan operation
+ */
+export interface ScanResult {
+	/** Whether the scan completed successfully */
+	success: boolean
+	/** Number of files processed */
+	filesProcessed: number
+	/** Number of files skipped (unchanged) */
+	filesSkipped: number
+	/** Number of chunks indexed */
+	chunksIndexed: number
+	/** Any errors encountered during scanning */
+	errors: Error[]
+}
+
+/**
+ * Server manifest entry for a single file
+ */
+export interface ManifestFileEntry {
+	/** Relative file path */
+	filePath: string
+	/** Array of chunk hashes for this file (for accurate change detection) */
+	chunkHashes: string[]
+	/** Number of chunks for this file */
+	chunkCount: number
+	/** When this file was last indexed */
+	lastIndexed: string
+	/** Optional: which user/client indexed it */
+	indexedBy?: string
+}
+
+/**
+ * Server manifest response
+ */
+export interface ServerManifest {
+	/** Organization ID */
+	organizationId: string
+	/** Project ID */
+	projectId: string
+	/** Git branch */
+	gitBranch: string
+	/** List of indexed files */
+	files: ManifestFileEntry[]
+	/** Total number of files in manifest */
+	totalFiles: number
+	/** Total number of chunks across all files */
+	totalChunks: number
+	/** When manifest was last updated */
+	lastUpdated: string
+}
+
+/**
+ * Search request with branch preferences
+ */
+export interface SearchRequest {
+	/** Search query */
+	query: string
+	/** Organization ID */
+	organizationId: string
+	/** Project ID */
+	projectId: string
+	/** Preferred branch to search first */
+	preferBranch: string
+	/** Fallback branch to search (usually 'main') */
+	fallbackBranch: string
+	/** Files to exclude from results (deleted on preferred branch) */
+	excludeFiles: string[]
+	/** Optional directory path filter */
+	path?: string
+}
+
+/**
+ * Search result from the server
+ */
+export interface SearchResult {
+	/** Chunk ID */
+	id: string
+	/** File path */
+	filePath: string
+	/** Starting line number */
+	startLine: number
+	/** Ending line number */
+	endLine: number
+	/** Relevance score */
+	score: number
+	/** Which branch this result came from */
+	gitBranch: string
+	/** Whether this result came from the preferred branch */
+	fromPreferredBranch: boolean
+}
+
+/**
+ * File change event
+ */
+export interface FileChangeEvent {
+	/** Type of change */
+	type: "created" | "changed" | "deleted"
+	/** File path */
+	filePath: string
+	/** Timestamp of change */
+	timestamp: number
+}
+
+/**
+ * Indexer state for UI updates
+ */
+export interface IndexerState {
+	/** Current status */
+	status: "idle" | "scanning" | "watching" | "error"
+	/** Status message */
+	message: string
+	/** Current git branch */
+	gitBranch?: string
+	/** Last sync timestamp */
+	lastSyncTime?: number
+	/** Total files indexed */
+	totalFiles?: number
+	/** Total chunks indexed */
+	totalChunks?: number
+	/** Error message if status is 'error' */
+	error?: string
+	/** Server manifest data (when available) */
+	manifest?: {
+		totalFiles: number
+		totalChunks: number
+		lastUpdated: string
+	}
+}

+ 131 - 0
src/services/code-index/managed/webview.ts

@@ -0,0 +1,131 @@
+// kilocode_change - new file
+/**
+ * webview module exports functions which interfact with the webview provider
+ * (i.e. ClineProvider)
+ */
+
+import { ClineProvider } from "../../../core/webview/ClineProvider"
+import { t } from "../../../i18n"
+import { OrganizationService } from "../../kilocode/OrganizationService"
+import { CodeIndexManager } from "../manager"
+
+export async function tryStartManagedIndexing(provider: ClineProvider): Promise<boolean> {
+	try {
+		const manager = provider.getCurrentWorkspaceCodeIndexManager()
+		if (!manager) {
+			// No workspace open - send error status
+			provider.postMessageToWebview({
+				type: "indexingStatusUpdate",
+				values: {
+					systemStatus: "Error",
+					message: t("embeddings:orchestrator.indexingRequiresWorkspace"),
+					processedItems: 0,
+					totalItems: 0,
+					currentItemUnit: "items",
+				},
+			})
+			provider.log("Cannot start indexing: No workspace folder open")
+			return false
+		}
+
+		// kilocode_change start: Support managed indexing
+		const [{ apiConfiguration }, kiloConfig] = await Promise.all([provider.getState(), provider.getKiloConfig()])
+		const projectId = kiloConfig?.project?.id
+		if (apiConfiguration.kilocodeToken && apiConfiguration.kilocodeOrganizationId && projectId) {
+			provider.log(
+				`[startIndexing] Setting Kilo org props: orgId=${apiConfiguration.kilocodeOrganizationId} projectId=${projectId}`,
+			)
+			manager.setKiloOrgCodeIndexProps({
+				kilocodeToken: apiConfiguration.kilocodeToken,
+				organizationId: apiConfiguration.kilocodeOrganizationId,
+				projectId,
+			})
+
+			return true
+		}
+
+		provider.log(
+			`[startIndexing] No Kilo org props available: token=${!!apiConfiguration.kilocodeToken}, orgId=${!!apiConfiguration.kilocodeOrganizationId}`,
+		)
+	} catch (error) {
+		provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)
+		provider.log(`Stack: ${error instanceof Error ? error.stack : "N/A"}`)
+	}
+
+	return false
+}
+
+/**
+ * Updates the code index manager with current Kilo org credentials
+ * This should be called whenever the API configuration changes
+ */
+export async function updateCodeIndexWithKiloProps(provider: ClineProvider): Promise<void> {
+	console.log("updateCodeIndexWithKiloProps", provider)
+
+	try {
+		const { apiConfiguration } = await provider.getState()
+
+		// Only proceed if we have both required credentials
+		if (!apiConfiguration.kilocodeToken || !apiConfiguration.kilocodeOrganizationId) {
+			return
+		}
+
+		// Get kilocodeTesterWarningsDisabledUntil from context
+		const kilocodeTesterWarningsDisabledUntil = provider.contextProxy.getValue(
+			"kilocodeTesterWarningsDisabledUntil",
+		)
+
+		// Fetch organization settings to check if code indexing is enabled
+		const organization = await OrganizationService.fetchOrganization(
+			apiConfiguration.kilocodeToken,
+			apiConfiguration.kilocodeOrganizationId,
+			kilocodeTesterWarningsDisabledUntil,
+		)
+
+		// Check if code indexing is enabled for this organization
+		const codeIndexingEnabled = OrganizationService.isCodeIndexingEnabled(organization)
+
+		if (!codeIndexingEnabled) {
+			provider.log("[updateCodeIndexWithKiloProps] Code indexing is disabled for provider organization")
+			return
+		}
+
+		// Get project ID from Kilo config
+		const kiloConfig = await provider.getKiloConfig()
+		const projectId = kiloConfig?.project?.id
+
+		if (!projectId) {
+			provider.log("[updateCodeIndexWithKiloProps] No projectId found in Kilo config, skipping code index update")
+			return
+		}
+
+		// Get or create the code index manager for the current workspace
+		let codeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()
+
+		// If manager doesn't exist yet, it will be created on first access
+		// We need to ensure it's initialized with the context proxy
+		if (!codeIndexManager) {
+			// Try to get the manager again, which will create it if workspace exists
+			const workspacePath = provider.cwd
+			if (workspacePath) {
+				codeIndexManager = CodeIndexManager.getInstance(provider.context, workspacePath)
+			}
+		}
+
+		if (codeIndexManager) {
+			// Set the Kilo org props - code indexing is enabled
+			codeIndexManager.setKiloOrgCodeIndexProps({
+				kilocodeToken: apiConfiguration.kilocodeToken,
+				organizationId: apiConfiguration.kilocodeOrganizationId,
+				projectId,
+			})
+
+			// Initialize the manager with context proxy if not already initialized
+			if (!codeIndexManager.isInitialized) {
+				await codeIndexManager.initialize(provider.contextProxy)
+			}
+		}
+	} catch (error) {
+		provider.log(`Failed to update code index with Kilo props: ${error}`)
+	}
+}

+ 222 - 16
src/services/code-index/manager.ts

@@ -12,9 +12,12 @@ import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
 import fs from "fs/promises"
 import ignore from "ignore"
 import path from "path"
-import { t } from "../../i18n"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryEventName } from "@roo-code/types"
+// kilocode_change start: Managed indexing (new standalone system)
+import { startIndexing as startManagedIndexing, search as searchManaged, createManagedIndexingConfig } from "./managed"
+import type { IndexerState as ManagedIndexerState } from "./managed"
+// kilocode_change end
 
 export class CodeIndexManager {
 	// --- Singleton Implementation ---
@@ -28,6 +31,11 @@ export class CodeIndexManager {
 	private _searchService: CodeIndexSearchService | undefined
 	private _cacheManager: CacheManager | undefined
 
+	// kilocode_change start: Managed indexing (new standalone system)
+	private _managedIndexerDisposable: vscode.Disposable | undefined
+	private _managedIndexerState: ManagedIndexerState | undefined
+	// kilocode_change end
+
 	// Flag to prevent race conditions during error recovery
 	private _isRecoveringFromError = false
 
@@ -63,7 +71,7 @@ export class CodeIndexManager {
 		CodeIndexManager.instances.clear()
 	}
 
-	private readonly workspacePath: string
+	public readonly workspacePath: string // kilocode_change
 	private readonly context: vscode.ExtensionContext
 
 	// Private constructor for singleton pattern
@@ -120,6 +128,12 @@ export class CodeIndexManager {
 		if (!this._configManager) {
 			this._configManager = new CodeIndexConfigManager(contextProxy)
 		}
+
+		// Pass Kilo org props to config manager if available
+		if (this._kiloOrgCodeIndexProps) {
+			this._configManager.setKiloOrgProps(this._kiloOrgCodeIndexProps)
+		}
+
 		// Load configuration once to get current state and restart requirements
 		const { requiresRestart } = await this._configManager.loadConfiguration()
 
@@ -148,7 +162,20 @@ export class CodeIndexManager {
 		const needsServiceRecreation = !this._serviceFactory || requiresRestart
 
 		if (needsServiceRecreation) {
-			await this._recreateServices()
+			// kilocode_change start: add additional logging
+			try {
+				await this._recreateServices()
+			} catch (error) {
+				// Log the error and set error state
+				console.error("[CodeIndexManager] Failed to recreate services:", error)
+				this._stateManager.setSystemState(
+					"Error",
+					`Failed to initialize: ${error instanceof Error ? error.message : String(error)}`,
+				)
+				// Re-throw to prevent further initialization
+				throw error
+			}
+			// kilocode_change end
 		}
 
 		// 5. Handle Indexing Start/Restart
@@ -264,6 +291,12 @@ export class CodeIndexManager {
 		if (this._orchestrator) {
 			this.stopWatcher()
 		}
+		// kilocode_change start
+		if (this._managedIndexerDisposable) {
+			this._managedIndexerDisposable.dispose()
+			this._managedIndexerDisposable = undefined
+		}
+		// kilocode_change end
 		this._stateManager.dispose()
 	}
 
@@ -291,6 +324,12 @@ export class CodeIndexManager {
 	}
 
 	public async searchIndex(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
+		// kilocode_change start: Route to managed indexing if available
+		if (this.isManagedIndexingAvailable) {
+			return this.searchManagedIndex(query, directoryPrefix)
+		}
+
+		// kilocode_change end: Fall back to local indexing
 		if (!this.isFeatureEnabled) {
 			return []
 		}
@@ -354,17 +393,21 @@ export class CodeIndexManager {
 			rooIgnoreController,
 		)
 
-		// kilocode_change start
-		// Only validate the embedder if it matches the currently configured provider
-		const config = this._configManager!.getConfig()
-		const shouldValidate = embedder.embedderInfo.name === config.embedderProvider
-
-		if (shouldValidate) {
-			const validationResult = await this._serviceFactory.validateEmbedder(embedder)
-			if (!validationResult.valid) {
-				const errorMessage = validationResult.error || "Embedder configuration validation failed"
-				this._stateManager.setSystemState("Error", errorMessage)
-				throw new Error(errorMessage)
+		// kilocode_change start: Handle Kilo org mode (no embedder/vector store validation needed)
+		const isKiloOrgMode = this._configManager!.isKiloOrgMode
+
+		if (!isKiloOrgMode) {
+			// Only validate the embedder if it matches the currently configured provider
+			const config = this._configManager!.getConfig()
+			const shouldValidate = embedder && embedder.embedderInfo.name === config.embedderProvider
+
+			if (shouldValidate) {
+				const validationResult = await this._serviceFactory.validateEmbedder(embedder)
+				if (!validationResult.valid) {
+					const errorMessage = validationResult.error || "Embedder configuration validation failed"
+					this._stateManager.setSystemState("Error", errorMessage)
+					throw new Error(errorMessage)
+				}
 			}
 		}
 		// kilocode_change end
@@ -380,15 +423,17 @@ export class CodeIndexManager {
 			fileWatcher,
 		)
 
-		// (Re)Initialize search service
+		// kilocode_change start: Always create search service (it handles both local and Kilo org mode)
+		// In Kilo org mode, embedder and vectorStore ill be null, but search service handles this
 		this._searchService = new CodeIndexSearchService(
 			this._configManager!,
 			this._stateManager,
 			embedder,
 			vectorStore,
 		)
+		// kilocode_change end
 
-		// Clear any error state after successful recreation
+		// Clear any error state after successful recwreation
 		this._stateManager.setSystemState("Standby", "")
 	}
 
@@ -440,4 +485,165 @@ export class CodeIndexManager {
 			}
 		}
 	}
+
+	// kilocode_change start Add ability to set kilo specific props
+	private _kiloOrgCodeIndexProps: {
+		organizationId: string
+		kilocodeToken: string
+		projectId: string
+	} | null = null
+
+	public setKiloOrgCodeIndexProps(props: NonNullable<typeof this._kiloOrgCodeIndexProps>) {
+		console.log("setKiloOrgCodeIndexProps", props)
+
+		this._kiloOrgCodeIndexProps = props
+
+		// Pass props to config manager if it exists
+		if (this._configManager) {
+			this._configManager.setKiloOrgProps(props)
+		}
+
+		// Start managed indexing automatically
+		this.startManagedIndexing().catch((error) => {
+			const err = error instanceof Error ? error : new Error(String(error))
+			console.error("[CodeIndexManager] Failed to start managed indexing:", err.message)
+			if (err.stack) {
+				console.error("[CodeIndexManager] Stack trace:", err.stack)
+			}
+			// Don't throw - allow the manager to continue functioning
+			// Set error state so UI can show the issue
+			this._stateManager.setSystemState("Error", `Failed to start indexing: ${err.message}`)
+		})
+	}
+
+	public getKiloOrgCodeIndexProps() {
+		return this._kiloOrgCodeIndexProps
+	}
+
+	// --- Managed Indexing Methods ---
+
+	/**
+	 * Starts the managed indexer (for organization users)
+	 * This is the new standalone indexing system that uses delta-based indexing
+	 */
+	public async startManagedIndexing(): Promise<void> {
+		if (!this._kiloOrgCodeIndexProps) {
+			throw new Error("Managed indexing requires organization credentials")
+		}
+
+		try {
+			// Stop any existing managed indexer
+			if (this._managedIndexerDisposable) {
+				this._managedIndexerDisposable.dispose()
+				this._managedIndexerDisposable = undefined
+			}
+
+			// Create configuration
+			const config = createManagedIndexingConfig(
+				this._kiloOrgCodeIndexProps.organizationId,
+				this._kiloOrgCodeIndexProps.projectId,
+				this._kiloOrgCodeIndexProps.kilocodeToken,
+				this.workspacePath,
+			)
+
+			// Start indexing
+			this._managedIndexerDisposable = await startManagedIndexing(config, this.context, (state) => {
+				this._managedIndexerState = state
+				// Emit state change event through state manager
+				// Map managed indexer states to system states:
+				// - "error" → "Error"
+				// - "scanning" → "Indexing"
+				// - "watching" → "Indexed" (has data and watching for changes)
+				// - "idle" → "Standby" (no data or needs re-scan)
+				let systemState: "Standby" | "Indexing" | "Indexed" | "Error"
+				if (state.status === "error") {
+					systemState = "Error"
+				} else if (state.status === "scanning") {
+					systemState = "Indexing"
+				} else if (state.status === "watching") {
+					systemState = "Indexed"
+				} else {
+					// "idle" or any other status
+					systemState = "Standby"
+				}
+
+				this._stateManager.setSystemState(systemState, state.message, state.manifest, state.gitBranch)
+			})
+		} catch (error) {
+			const errorMessage = error instanceof Error ? error.message : String(error)
+			console.error("[CodeIndexManager] Failed to start managed indexing:", error)
+			TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+				error: errorMessage,
+				stack: error instanceof Error ? error.stack : undefined,
+				location: "startManagedIndexing",
+			})
+			throw error
+		}
+	}
+
+	/**
+	 * Stops the managed indexer
+	 */
+	public stopManagedIndexing(): void {
+		if (this._managedIndexerDisposable) {
+			this._managedIndexerDisposable.dispose()
+			this._managedIndexerDisposable = undefined
+			this._managedIndexerState = undefined
+		}
+	}
+
+	/**
+	 * Searches using the managed indexer
+	 */
+	public async searchManagedIndex(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
+		if (!this._kiloOrgCodeIndexProps) {
+			return []
+		}
+
+		try {
+			const config = createManagedIndexingConfig(
+				this._kiloOrgCodeIndexProps.organizationId,
+				this._kiloOrgCodeIndexProps.projectId,
+				this._kiloOrgCodeIndexProps.kilocodeToken,
+				this.workspacePath,
+			)
+
+			const results = await searchManaged(query, config, directoryPrefix)
+
+			// Convert to VectorStoreSearchResult format
+			return results.map((result) => ({
+				id: result.id,
+				score: result.score,
+				payload: {
+					filePath: result.filePath,
+					codeChunk: "", // Managed indexing doesn't return code chunks
+					startLine: result.startLine,
+					endLine: result.endLine,
+				},
+			}))
+		} catch (error) {
+			const errorMessage = error instanceof Error ? error.message : String(error)
+			console.error("[CodeIndexManager] Managed search failed:", error)
+			TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
+				error: errorMessage,
+				stack: error instanceof Error ? error.stack : undefined,
+				location: "searchManagedIndex",
+			})
+			return []
+		}
+	}
+
+	/**
+	 * Gets the managed indexer state
+	 */
+	public getManagedIndexerState(): ManagedIndexerState | undefined {
+		return this._managedIndexerState
+	}
+
+	/**
+	 * Checks if managed indexing is available (has org credentials)
+	 */
+	public get isManagedIndexingAvailable(): boolean {
+		return !!this._kiloOrgCodeIndexProps
+	}
 }

+ 29 - 1
src/services/code-index/state-manager.ts

@@ -8,6 +8,12 @@ export class CodeIndexStateManager {
 	private _processedItems: number = 0
 	private _totalItems: number = 0
 	private _currentItemUnit: string = "blocks"
+	private _gitBranch?: string
+	private _manifest?: {
+		totalFiles: number
+		totalChunks: number
+		lastUpdated: string
+	}
 	private _progressEmitter = new vscode.EventEmitter<ReturnType<typeof this.getCurrentStatus>>()
 
 	// --- Public API ---
@@ -25,12 +31,23 @@ export class CodeIndexStateManager {
 			processedItems: this._processedItems,
 			totalItems: this._totalItems,
 			currentItemUnit: this._currentItemUnit,
+			gitBranch: this._gitBranch,
+			manifest: this._manifest,
 		}
 	}
 
 	// --- State Management ---
 
-	public setSystemState(newState: IndexingState, message?: string): void {
+	public setSystemState(
+		newState: IndexingState,
+		message?: string,
+		manifest?: {
+			totalFiles: number
+			totalChunks: number
+			lastUpdated: string
+		},
+		gitBranch?: string,
+	): void {
 		const stateChanged =
 			newState !== this._systemStatus || (message !== undefined && message !== this._statusMessage)
 
@@ -39,6 +56,12 @@ export class CodeIndexStateManager {
 			if (message !== undefined) {
 				this._statusMessage = message
 			}
+			if (manifest !== undefined) {
+				this._manifest = manifest
+			}
+			if (gitBranch !== undefined) {
+				this._gitBranch = gitBranch
+			}
 
 			// Reset progress counters if moving to a non-indexing state or starting fresh
 			if (newState !== "Indexing") {
@@ -51,6 +74,11 @@ export class CodeIndexStateManager {
 				if (newState === "Error" && message === undefined) this._statusMessage = "An error occurred."
 			}
 
+			// Clear manifest if not in Indexed state
+			if (newState !== "Indexed") {
+				this._manifest = undefined
+			}
+
 			this._progressEmitter.fire(this.getCurrentStatus())
 		}
 	}

+ 85 - 0
src/services/kilocode/OrganizationService.ts

@@ -0,0 +1,85 @@
+// kilocode_change - new file
+import axios from "axios"
+import { getKiloUrlFromToken } from "@roo-code/types"
+import { X_KILOCODE_ORGANIZATIONID, X_KILOCODE_TESTER } from "../../shared/kilocode/headers"
+import { KiloOrganization, KiloOrganizationSchema } from "../../shared/kilocode/organization"
+import { logger } from "../../utils/logging"
+
+/**
+ * Service for fetching and managing Kilo Code organization settings
+ */
+export class OrganizationService {
+	/**
+	 * Fetches organization details from the Kilo Code API
+	 * @param kilocodeToken - The authentication token
+	 * @param organizationId - The organization ID
+	 * @param kilocodeTesterWarningsDisabledUntil - Timestamp for suppressing tester warnings
+	 * @returns The organization object with settings
+	 */
+	public static async fetchOrganization(
+		kilocodeToken: string,
+		organizationId: string,
+		kilocodeTesterWarningsDisabledUntil?: number,
+	): Promise<KiloOrganization | null> {
+		try {
+			if (!organizationId || !kilocodeToken) {
+				logger.warn("[OrganizationService] Missing required parameters for fetching organization")
+				return null
+			}
+
+			const headers: Record<string, string> = {
+				Authorization: `Bearer ${kilocodeToken}`,
+				"Content-Type": "application/json",
+			}
+
+			headers[X_KILOCODE_ORGANIZATIONID] = organizationId
+
+			// Add X-KILOCODE-TESTER: SUPPRESS header if the setting is enabled
+			if (kilocodeTesterWarningsDisabledUntil && kilocodeTesterWarningsDisabledUntil > Date.now()) {
+				headers[X_KILOCODE_TESTER] = "SUPPRESS"
+			}
+
+			const url = getKiloUrlFromToken(
+				`https://api.kilocode.ai/api/organizations/${organizationId}`,
+				kilocodeToken,
+			)
+
+			const response = await axios.get(url, { headers })
+
+			// Validate the response against the schema
+			const validationResult = KiloOrganizationSchema.safeParse(response.data)
+
+			if (!validationResult.success) {
+				logger.error("[OrganizationService] Invalid organization response format", {
+					organizationId,
+					errors: validationResult.error.errors,
+				})
+				return null
+			}
+
+			logger.info("[OrganizationService] Successfully fetched organization", {
+				organizationId,
+				codeIndexingEnabled: validationResult.data.settings.code_indexing_enabled,
+			})
+
+			return validationResult.data
+		} catch (error) {
+			// Log error but don't throw - gracefully degrade
+			logger.error("[OrganizationService] Failed to fetch organization", {
+				organizationId,
+				error: error instanceof Error ? error.message : String(error),
+			})
+			return null
+		}
+	}
+
+	/**
+	 * Checks if code indexing is enabled for an organization
+	 * @param organization - The organization object
+	 * @returns true if code indexing is enabled (defaults to true if not specified)
+	 */
+	public static isCodeIndexingEnabled(organization: KiloOrganization | null): boolean {
+		// Default to true if organization is null or setting is not specified
+		return organization?.settings?.code_indexing_enabled ?? true
+	}
+}

+ 6 - 0
src/shared/ExtensionMessage.ts

@@ -57,6 +57,12 @@ export interface IndexingStatus {
 	totalItems: number
 	currentItemUnit?: string
 	workspacePath?: string
+	gitBranch?: string // Current git branch being indexed
+	manifest?: {
+		totalFiles: number
+		totalChunks: number
+		lastUpdated: string
+	}
 }
 
 export interface IndexingStatusUpdateMessage {

+ 43 - 0
src/shared/kilocode/organization.ts

@@ -0,0 +1,43 @@
+// kilocode_change - new file
+import { z } from "zod"
+
+/**
+ * Kilo Code Organization Settings Schema
+ * These settings control organization-level features and configurations
+ */
+export const KiloOrganizationSettingsSchema = z.object({
+	model_allow_list: z.array(z.string()).optional(),
+	provider_allow_list: z.array(z.string()).optional(),
+	default_model: z.string().optional(),
+	data_collection: z.enum(["allow", "deny"]).nullable().optional(),
+	// null means they were grandfathered in and so they have usage limits enabled
+	enable_usage_limits: z.boolean().optional(),
+	code_indexing_enabled: z.boolean().optional(),
+	projects_ui_enabled: z.boolean().optional(),
+})
+
+export type KiloOrganizationSettings = z.infer<typeof KiloOrganizationSettingsSchema>
+
+/**
+ * Kilo Code Organization Schema
+ * Represents the full organization object returned from the API
+ */
+export const KiloOrganizationSchema = z.object({
+	id: z.string(),
+	name: z.string(),
+	created_at: z.string(),
+	updated_at: z.string(),
+	microdollars_balance: z.number(),
+	microdollars_used: z.number(),
+	stripe_customer_id: z.string().nullable(),
+	auto_top_up_enabled: z.boolean(),
+	settings: KiloOrganizationSettingsSchema,
+	seat_count: z.number().min(0).default(0),
+	require_seats: z.boolean().default(false),
+	created_by_kilo_user_id: z.string().nullable(),
+	deleted_at: z.string().nullable(),
+	sso_domain: z.string().nullable(),
+	plan: z.enum(["teams", "enterprise"]),
+})
+
+export type KiloOrganization = z.infer<typeof KiloOrganizationSchema>

+ 153 - 0
src/shared/utils/exec.ts

@@ -0,0 +1,153 @@
+// kilocode_change - new file
+import { spawn } from "child_process"
+import { Readable } from "stream"
+
+import { chunksToLinesAsync, combine } from "./iterable"
+
+import type { ChildProcess } from "child_process"
+
+export interface ExecOptions {
+	/**
+	 * command to execute
+	 */
+	cmd: string
+
+	/**
+	 * where to execute the command
+	 */
+	cwd?: string
+
+	/**
+	 * what is being executed that should be logged for
+	 * the user when it fails.eg. "running user command"
+	 */
+	context?: string
+
+	/**
+	 * the user that should run the command
+	 */
+	uid?: number
+
+	/**
+	 * environment variables
+	 */
+	env?: Record<string, string | undefined>
+}
+
+/**
+ * Returns a childProcess, piping stdin to the process
+ */
+export async function execWithStdin({ cmd, cwd, uid, env }: ExecOptions): Promise<ChildProcess> {
+	const proc = spawn(cmd, {
+		cwd,
+		uid,
+		shell: true,
+		stdio: ["pipe", 1, 2],
+		env,
+	})
+	return proc
+}
+
+/**
+ * Executes a shell command, piping all stdio to the current process
+ */
+export async function exec({ cmd, cwd, context, uid, env }: ExecOptions): Promise<void> {
+	const proc = spawn(cmd, {
+		cwd,
+		uid,
+		shell: true,
+		stdio: ["inherit", "inherit", "inherit"],
+		env,
+	})
+
+	await onChildProcessExit(proc, context)
+}
+
+/**
+ * Just like exec, except it is a generator which yields stdout lines
+ */
+export async function* execGetLines({
+	cmd,
+	cwd,
+	context,
+	uid,
+	env,
+}: ExecOptions): AsyncGenerator<string, void, unknown> {
+	const proc = spawn(cmd, {
+		cwd,
+		uid,
+		shell: true,
+		stdio: ["ignore", "pipe", "inherit"],
+		env,
+	})
+
+	const exit = onChildProcessExit(proc, context)
+
+	for await (const line of chunksToLinesAsync(proc.stdout)) {
+		yield line
+	}
+
+	await exit
+}
+
+/**
+ * Just like exec, except it is a generator which yields stdout lines
+ */
+export async function* execGetLinesStdoutStderr({
+	cmd,
+	cwd,
+	context,
+	uid,
+	env,
+}: ExecOptions): AsyncGenerator<WithIOType, void, unknown> {
+	const proc = spawn(cmd, {
+		cwd,
+		uid,
+		shell: true,
+		stdio: ["ignore", "pipe", "pipe"],
+		env,
+	})
+
+	// Transform stdout/stderr into a stream of lines, then wrap the lines in
+	// objects that say where the lines came from (either stdout | stderr)
+	const stdout = withIOType("stdout", chunksToLinesAsync(proc.stdout))
+	const stderr = withIOType("stderr", chunksToLinesAsync(proc.stderr))
+	const exit = onChildProcessExit(proc, context)
+
+	// Yield each line object from the combined iterable from stdout/stderr
+	for await (const line of combine(stdout, stderr)) {
+		yield line
+	}
+
+	await exit
+}
+
+export type WithIOType = {
+	type: "stdout" | "stderr"
+	line: string
+}
+
+function withIOType(type: WithIOType["type"], source: AsyncIterable<string>): AsyncIterable<WithIOType> {
+	return Readable.from(source).map((line): WithIOType => ({ type, line }))
+}
+
+export function onChildProcessExit(childProcess: ChildProcess, context?: string): Promise<void> {
+	return new Promise((resolve, reject) => {
+		childProcess.once("exit", (code: number, _signal: string) => {
+			if (code === 0) {
+				resolve(undefined)
+			} else {
+				if (context) {
+					reject(new ExecError(`Error while ${context}. Exited with error code: ${code}`))
+				} else {
+					reject(new Error(`Exit with error code: ${code}`))
+				}
+			}
+		})
+		childProcess.once("error", (err: Error) => {
+			reject(err)
+		})
+	})
+}
+
+export class ExecError extends Error {}

+ 66 - 0
src/shared/utils/iterable.ts

@@ -0,0 +1,66 @@
+// kilocode_change - new file
+/**
+ * Given an async iterable, re-yield the chunks into line chunks
+ */
+export async function* chunksToLinesAsync(chunks: AsyncIterable<string>): AsyncIterable<string> {
+	if (!(Symbol.asyncIterator in chunks)) {
+		throw new Error("Parameter is not an asynchronous iterable")
+	}
+	let previous = ""
+	for await (const chunk of chunks) {
+		previous += chunk
+		let eolIndex: number
+		while ((eolIndex = previous.indexOf("\n")) >= 0) {
+			const line = previous.slice(0, eolIndex)
+			yield line
+			previous = previous.slice(eolIndex + 1)
+		}
+	}
+	if (previous.length > 0) {
+		yield previous
+	}
+}
+
+/**
+ * Combines multiple async iterables into a single async iterable, interleaving
+ * the values over time. Adapted from: https://stackoverflow.com/a/50586391
+ */
+export async function* combine<T>(...iterable: Array<AsyncIterable<T>>): AsyncIterable<T> {
+	const asyncIterators = Array.from(iterable, (o) => o[Symbol.asyncIterator]())
+	const results = []
+	let count = asyncIterators.length
+	const never = new Promise<never>(() => {})
+	type PromiseResult = {
+		index: number
+		result: IteratorResult<T, any>
+	}
+
+	function getNext(asyncIterator: AsyncIterator<T>, index: number): Promise<PromiseResult> {
+		return asyncIterator.next().then((result) => ({
+			index,
+			result,
+		}))
+	}
+	const nextPromises: Array<Promise<PromiseResult | never>> = asyncIterators.map(getNext)
+	try {
+		while (count) {
+			const { index, result } = await Promise.race(nextPromises)
+			if (result.done === true) {
+				nextPromises[index] = never
+				results[index] = result.value
+				count--
+			} else {
+				nextPromises[index] = getNext(asyncIterators[index], index)
+				yield result.value
+			}
+		}
+	} finally {
+		for (const [index, iterator] of asyncIterators.entries()) {
+			if (nextPromises[index] !== never && iterator.return !== undefined) {
+				// no await here - see https://github.com/tc39/proposal-async-iteration/issues/126
+				void iterator.return()
+			}
+		}
+	}
+	return results
+}

+ 4 - 0
src/utils/__tests__/project-config.spec.ts

@@ -33,6 +33,10 @@ describe("project-config", () => {
 			expect(normalizeProjectId("[email protected]:Kilo-Org/handbook.git")).toBe("handbook")
 		})
 
+		it("extracts repository name from SSH git URL without .git extension", () => {
+			expect(normalizeProjectId("[email protected]:brianc/node-postgres")).toBe("node-postgres")
+		})
+
 		it("returns plain project ID as-is", () => {
 			expect(normalizeProjectId("my-project")).toBe("my-project")
 		})

+ 4 - 3
src/utils/kilo-config-file.ts

@@ -33,11 +33,12 @@ export function normalizeProjectId(projectId?: KilocodeConfigProject["id"]): str
 	}
 
 	// Check if it looks like a git URL (https or ssh)
-	const httpsGitPattern = /^https?:\/\/.+\.git$/i
-	const sshGitPattern = /^git@.+\.git$/i
+	// Patterns match with or without .git extension
+	const httpsGitPattern = /^https?:\/\/.+\/.+/i
+	const sshGitPattern = /^git@.+:.+/i
 
 	if (httpsGitPattern.test(projectId) || sshGitPattern.test(projectId)) {
-		// Extract the last path component and remove .git extension
+		// Extract the last path component and remove .git extension if present
 		const parts = projectId.split("/")
 		const lastPart = parts[parts.length - 1]
 		return lastPart.replace(/\.git$/i, "")

+ 10 - 3
webview-ui/src/components/chat/IndexingStatusBadge.tsx

@@ -11,6 +11,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { PopoverTrigger, StandardTooltip, Button } from "@src/components/ui"
 
 import { CodeIndexPopover } from "./CodeIndexPopover"
+import { ManagedCodeIndexPopover } from "./kilocode/ManagedCodeIndexPopover"
 
 interface IndexingStatusBadgeProps {
 	className?: string
@@ -18,7 +19,10 @@ interface IndexingStatusBadgeProps {
 
 export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ className }) => {
 	const { t } = useAppTranslation()
-	const { cwd } = useExtensionState()
+	const { cwd, apiConfiguration } = useExtensionState()
+
+	// Check if organization indexing is available
+	const hasOrganization = !!apiConfiguration?.kilocodeOrganizationId
 
 	const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>({
 		systemStatus: "Standby",
@@ -82,8 +86,11 @@ export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ classN
 		return statusColors[indexingStatus.systemStatus as keyof typeof statusColors] || statusColors.Standby
 	}, [indexingStatus.systemStatus])
 
+	// Use ManagedCodeIndexPopover when organization is available, otherwise use regular CodeIndexPopover
+	const PopoverComponent = hasOrganization ? ManagedCodeIndexPopover : CodeIndexPopover
+
 	return (
-		<CodeIndexPopover indexingStatus={indexingStatus}>
+		<PopoverComponent indexingStatus={indexingStatus}>
 			<StandardTooltip content={tooltipText}>
 				<PopoverTrigger asChild>
 					<Button
@@ -107,6 +114,6 @@ export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ classN
 					</Button>
 				</PopoverTrigger>
 			</StandardTooltip>
-		</CodeIndexPopover>
+		</PopoverComponent>
 	)
 }

+ 121 - 0
webview-ui/src/components/chat/kilocode/ManagedCodeIndexPopover.tsx

@@ -0,0 +1,121 @@
+// kilocode_change - new file
+import React, { useState, useEffect, useCallback } from "react"
+import { Trans } from "react-i18next"
+import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+
+import type { IndexingStatus } from "@roo/ExtensionMessage"
+
+import { vscode } from "@src/utils/vscode"
+import { useExtensionState } from "@src/context/ExtensionStateContext"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { buildDocLink } from "@src/utils/docLinks"
+import { Popover, PopoverContent } from "@src/components/ui"
+import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
+import { useEscapeKey } from "@src/hooks/useEscapeKey"
+import { OrganizationIndexingTab } from "./OrganizationIndexingTab"
+
+interface CodeIndexPopoverProps {
+	children: React.ReactNode
+	indexingStatus: IndexingStatus
+}
+
+export const ManagedCodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
+	children,
+	indexingStatus: externalIndexingStatus,
+}) => {
+	const { t } = useAppTranslation()
+	const { cwd } = useExtensionState()
+	const [open, setOpen] = useState(false)
+
+	const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>(externalIndexingStatus)
+
+	// Update indexing status from parent
+	useEffect(() => {
+		setIndexingStatus(externalIndexingStatus)
+	}, [externalIndexingStatus])
+
+	// Request initial indexing status
+	useEffect(() => {
+		if (open) {
+			vscode.postMessage({ type: "requestIndexingStatus" })
+		}
+		const handleMessage = (event: MessageEvent) => {
+			if (event.data.type === "workspaceUpdated") {
+				// When workspace changes, request updated indexing status
+				if (open) {
+					vscode.postMessage({ type: "requestIndexingStatus" })
+				}
+			}
+		}
+
+		window.addEventListener("message", handleMessage)
+		return () => window.removeEventListener("message", handleMessage)
+	}, [open])
+
+	// Listen for indexing status updates
+	useEffect(() => {
+		const handleMessage = (event: MessageEvent<any>) => {
+			if (event.data.type === "indexingStatusUpdate") {
+				if (!event.data.values.workspacePath || event.data.values.workspacePath === cwd) {
+					setIndexingStatus({
+						systemStatus: event.data.values.systemStatus,
+						message: event.data.values.message || "",
+						processedItems: event.data.values.processedItems,
+						totalItems: event.data.values.totalItems,
+						currentItemUnit: event.data.values.currentItemUnit || "items",
+					})
+				}
+			}
+		}
+
+		window.addEventListener("message", handleMessage)
+		return () => window.removeEventListener("message", handleMessage)
+	}, [cwd])
+
+	// Use the shared ESC key handler hook
+	useEscapeKey(open, () => setOpen(false))
+
+	const handleCancelIndexing = useCallback(() => {
+		// Optimistically update UI while backend cancels
+		setIndexingStatus((prev) => ({
+			...prev,
+			message: t("settings:codeIndex.cancelling"),
+		}))
+		vscode.postMessage({ type: "cancelIndexing" })
+	}, [t])
+
+	const portalContainer = useRooPortal("roo-portal")
+
+	return (
+		<Popover open={open} onOpenChange={setOpen}>
+			{children}
+			<PopoverContent
+				className="w-[calc(100vw-32px)] max-w-[450px] max-h-[80vh] overflow-y-auto p-0"
+				align="end"
+				alignOffset={0}
+				side="bottom"
+				sideOffset={5}
+				collisionPadding={16}
+				avoidCollisions={true}
+				container={portalContainer}>
+				<div className="p-3 border-b border-vscode-dropdown-border cursor-default">
+					<div className="flex flex-row items-center gap-1 p-0 mt-0 mb-1 w-full">
+						<h4 className="m-0 pb-2 flex-1">{t("settings:codeIndex.title")}</h4>
+					</div>
+					<p className="my-0 pr-4 text-sm w-full">
+						<Trans i18nKey="settings:codeIndex.description">
+							<VSCodeLink
+								href={buildDocLink("features/codebase-indexing", "settings")}
+								style={{ display: "inline" }}
+							/>
+						</Trans>
+					</p>
+				</div>
+
+				<div className="p-4">
+					<OrganizationIndexingTab indexingStatus={indexingStatus} onCancelIndexing={handleCancelIndexing} />
+				</div>
+			</PopoverContent>
+		</Popover>
+	)
+}

+ 165 - 0
webview-ui/src/components/chat/kilocode/OrganizationIndexingTab.tsx

@@ -0,0 +1,165 @@
+// kilocode_change - new file
+import React, { useMemo } from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+
+import type { IndexingStatus } from "@roo/ExtensionMessage"
+
+import { vscode } from "@src/utils/vscode"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useExtensionState } from "@src/context/ExtensionStateContext"
+import { cn } from "@src/lib/utils"
+
+interface OrganizationIndexingTabProps {
+	indexingStatus: IndexingStatus & {
+		manifest?: {
+			totalFiles: number
+			totalChunks: number
+			lastUpdated: string
+		}
+	}
+	onCancelIndexing: () => void
+}
+
+export const OrganizationIndexingTab: React.FC<OrganizationIndexingTabProps> = ({
+	indexingStatus,
+	onCancelIndexing,
+}) => {
+	const { t } = useAppTranslation()
+	const { cloudUserInfo } = useExtensionState()
+	const organizationId = cloudUserInfo?.organizationId
+
+	const progressPercentage = useMemo(
+		() =>
+			indexingStatus.totalItems > 0
+				? Math.round((indexingStatus.processedItems / indexingStatus.totalItems) * 100)
+				: 0,
+		[indexingStatus.processedItems, indexingStatus.totalItems],
+	)
+
+	const transformStyleString = `translateX(-${100 - progressPercentage}%)`
+
+	return (
+		<div className="space-y-4">
+			{/* Status Section */}
+			<div className="space-y-2">
+				<h4 className="text-sm font-medium">Status</h4>
+				<div className="text-sm text-vscode-descriptionForeground">
+					<span
+						className={cn("inline-block w-3 h-3 rounded-full mr-2", {
+							"bg-gray-400": indexingStatus.systemStatus === "Standby",
+							"bg-yellow-500 animate-pulse": indexingStatus.systemStatus === "Indexing",
+							"bg-green-500": indexingStatus.systemStatus === "Indexed",
+							"bg-red-500": indexingStatus.systemStatus === "Error",
+						})}
+					/>
+					{t(`settings:codeIndex.indexingStatuses.${indexingStatus.systemStatus.toLowerCase()}`)}
+					{indexingStatus.message ? ` - ${indexingStatus.message}` : ""}
+				</div>
+
+				{indexingStatus.systemStatus === "Indexing" && (
+					<div className="mt-2">
+						<ProgressPrimitive.Root
+							className="relative h-2 w-full overflow-hidden rounded-full bg-secondary"
+							value={progressPercentage}>
+							<ProgressPrimitive.Indicator
+								className="h-full w-full flex-1 bg-primary transition-transform duration-300 ease-in-out"
+								style={{
+									transform: transformStyleString,
+								}}
+							/>
+						</ProgressPrimitive.Root>
+					</div>
+				)}
+			</div>
+
+			{/* Info Section - Show manifest data when indexed, otherwise show how it works */}
+			{indexingStatus.systemStatus === "Indexed" && indexingStatus.manifest ? (
+				<div className="space-y-2 p-3 bg-vscode-input-background rounded-md">
+					<h4 className="text-sm font-medium">Index Information</h4>
+					<div className="text-xs text-vscode-descriptionForeground space-y-1">
+						{indexingStatus.gitBranch && (
+							<div className="flex justify-between pb-1 mb-1 border-b border-vscode-dropdown-border">
+								<span>Current branch:</span>
+								<code className="font-mono font-medium">{indexingStatus.gitBranch}</code>
+							</div>
+						)}
+						<div className="flex justify-between">
+							<span>Files indexed:</span>
+							<span className="font-medium">{indexingStatus.manifest.totalFiles.toLocaleString()}</span>
+						</div>
+						<div className="flex justify-between">
+							<span>Total chunks:</span>
+							<span className="font-medium">{indexingStatus.manifest.totalChunks.toLocaleString()}</span>
+						</div>
+						<div className="flex justify-between">
+							<span>Last indexed:</span>
+							<span className="font-medium">
+								{new Date(indexingStatus.manifest.lastUpdated).toLocaleString()}
+							</span>
+						</div>
+					</div>
+				</div>
+			) : (
+				<div className="space-y-2 p-3 bg-vscode-input-background rounded-md">
+					<h4 className="text-sm font-medium">How it works</h4>
+					<ul className="text-xs text-vscode-descriptionForeground space-y-1 list-disc list-inside">
+						<li>Main branch: Full index (shared across organization)</li>
+						<li>Feature branches: Only changed files indexed (99% storage savings)</li>
+						<li>Automatic updates after git commits and branch switches</li>
+						<li>Branch-aware search with deleted file handling</li>
+						<li>Detached HEAD state: Indexing automatically disabled</li>
+					</ul>
+				</div>
+			)}
+
+			{/* Action Buttons */}
+			<div className="space-y-3">
+				<div className="flex gap-2">
+					{indexingStatus.systemStatus === "Indexing" && (
+						<VSCodeButton appearance="secondary" onClick={onCancelIndexing}>
+							{t("settings:codeIndex.cancelIndexingButton")}
+						</VSCodeButton>
+					)}
+					{(indexingStatus.systemStatus === "Error" || indexingStatus.systemStatus === "Standby") && (
+						<VSCodeButton onClick={() => vscode.postMessage({ type: "startIndexing" })}>
+							Start Organization Indexing
+						</VSCodeButton>
+					)}
+				</div>
+
+				{/* Management Link */}
+				{organizationId && (
+					<div className="pt-2 border-t border-vscode-dropdown-border">
+						<div className="text-xs text-vscode-descriptionForeground">
+							<VSCodeLink
+								href={`https://app.kilocode.ai/organizations/${organizationId}/code-indexing`}
+								className="inline-flex items-center gap-1 hover:underline">
+								<svg
+									className="w-3 h-3"
+									fill="none"
+									stroke="currentColor"
+									viewBox="0 0 24 24"
+									xmlns="http://www.w3.org/2000/svg">
+									<path
+										strokeLinecap="round"
+										strokeLinejoin="round"
+										strokeWidth={2}
+										d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
+									/>
+									<path
+										strokeLinecap="round"
+										strokeLinejoin="round"
+										strokeWidth={2}
+										d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
+									/>
+								</svg>
+								Manage indexing in admin dashboard
+							</VSCodeLink>
+						</div>
+					</div>
+				)}
+			</div>
+		</div>
+	)
+}