Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.

Commit 4b7e20b

Browse files
feat(vscode): add git worktree support for agent manager sessions (#418)
* feat(vscode): add git worktree support for agent manager sessions * test(vscode): add WorktreeManager unit and integration tests * refactor(vscode): address review — extract branch-name, shared SessionMode type, remove premature delete handling * refactor(vscode): pass directory through handler chain, fix listener leak, add cleanup regression tests - Remove messageDirectory class field; pass dir as parameter to all handler methods - Only trust directory override when onBeforeMessage interceptor is attached - Add proper type for CreateWorktreeSessionRequest, remove `as any` cast - Fix onMessage listener leak with onCleanup in AgentManagerApp - Use removeWorktree() for pre-creation cleanup instead of inline fs.rm - Add regression tests for orphaned directory removal and cleanup before re-creation * refactor(vscode): bind directory to session not message, fix duplicate import - KiloProvider: add sessionDirectories map and setSessionDirectory() - getWorkspaceDirectory() resolves by sessionId lookup instead of per-message override - Remove dir param from all 18 handler methods — they resolve directory from their sessionId - AgentManagerProvider: call setSessionDirectory() at session creation and recovery - Remove directory injection from message interceptor (interceptor only routes custom messages) - Fix duplicate ExtensionMessage import in AgentManagerApp.tsx - Forward agent param in worktree session creation * fix: update stale docstring in AgentManagerProvider * fix: path guard on rm, clean up sessionDirectories on delete/dispose, track recovered sessions for SSE, log metadata write failures * fix: recover worktree sessions on restart by merging worktree directories into loadSessions - handleLoadSessions fetches sessions from all registered worktree directories and merges them into the response (deduped by session ID) - recoverWorktrees result is awaited by the interceptor before loadSessions passes through, ensuring sessionDirectories are populated first - Fixes: worktree sessions disappearing after VS Code restart * fix: non-blocking worktree recovery — refresh session list after discovery instead of blocking loadSessions * fix: don't retry with new branch when existingBranch was requested
1 parent b3a470a commit 4b7e20b

14 files changed

Lines changed: 1414 additions & 46 deletions

File tree

bun.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/kilo-vscode/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,9 @@
325325
"ignore": "^7.0.3",
326326
"js-tiktoken": "^1.0.18",
327327
"lru-cache": "^11.0.2",
328-
"quick-lru": "^7.0.0",
329328
"openai": "^4.85.4",
329+
"quick-lru": "^7.0.0",
330+
"simple-git": "3.31.1",
330331
"solid-js": "^1.9.11",
331332
"uri-js": "^4.4.1",
332333
"web-tree-sitter": "^0.24.7",

packages/kilo-vscode/src/KiloProvider.ts

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@ export class KiloProvider implements vscode.WebviewViewProvider {
2323
private cachedConfigMessage: unknown = null
2424

2525
private trackedSessionIds: Set<string> = new Set()
26+
/** Per-session directory overrides (e.g., worktree paths registered by AgentManagerProvider). */
27+
private sessionDirectories = new Map<string, string>()
2628
private unsubscribeEvent: (() => void) | null = null
2729
private unsubscribeState: (() => void) | null = null
2830
private webviewMessageDisposable: vscode.Disposable | null = null
2931

32+
/** Optional interceptor called before the standard message handler.
33+
* Return null to consume the message, or return a (possibly transformed) message. */
34+
private onBeforeMessage: ((msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>) | null = null
35+
3036
constructor(
3137
private readonly extensionUri: vscode.Uri,
3238
private readonly connectionService: KiloConnectionService,
@@ -144,13 +150,58 @@ export class KiloProvider implements vscode.WebviewViewProvider {
144150
this.initializeConnection()
145151
}
146152

153+
/**
154+
* Register a session created externally (e.g., worktree sessions from AgentManagerProvider).
155+
* Sets currentSession, adds to trackedSessionIds, and notifies the webview.
156+
*/
157+
public registerSession(session: SessionInfo): void {
158+
this.currentSession = session
159+
this.trackedSessionIds.add(session.id)
160+
this.postMessage({
161+
type: "sessionCreated",
162+
session: this.sessionToWebview(session),
163+
})
164+
}
165+
166+
/**
167+
* Add a session ID to the tracked set without changing currentSession.
168+
* Used to re-register worktree sessions after clearSession wipes the set.
169+
*/
170+
public trackSession(sessionId: string): void {
171+
this.trackedSessionIds.add(sessionId)
172+
}
173+
174+
/**
175+
* Register a directory override for a session (e.g., worktree path).
176+
* When set, all operations for this session use this directory instead of the workspace root.
177+
*/
178+
public setSessionDirectory(sessionId: string, directory: string): void {
179+
this.sessionDirectories.set(sessionId, directory)
180+
}
181+
182+
/**
183+
* Re-fetch and send the full session list to the webview.
184+
* Called by AgentManagerProvider after worktree recovery completes.
185+
*/
186+
public refreshSessions(): void {
187+
void this.handleLoadSessions()
188+
}
189+
147190
/**
148191
* Attach to a webview that already has its own HTML set.
149192
* Sets up message handling and connection without overriding HTML content.
193+
*
194+
* @param options.onBeforeMessage - Optional interceptor called before the standard handler.
195+
* Return null to consume the message (stop propagation), or return the message
196+
* (possibly transformed) to continue with standard handling.
150197
*/
151-
public attachToWebview(webview: vscode.Webview): void {
198+
public attachToWebview(
199+
webview: vscode.Webview,
200+
options?: { onBeforeMessage?: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null> },
201+
): void {
152202
this.isWebviewReady = false
153203
this.webview = webview
204+
this.onBeforeMessage = options?.onBeforeMessage ?? null
154205
this.setupWebviewMessageHandler(webview)
155206
this.initializeConnection()
156207
}
@@ -162,6 +213,18 @@ export class KiloProvider implements vscode.WebviewViewProvider {
162213
private setupWebviewMessageHandler(webview: vscode.Webview): void {
163214
this.webviewMessageDisposable?.dispose()
164215
this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => {
216+
// Run interceptor if attached (e.g., AgentManagerProvider worktree logic)
217+
if (this.onBeforeMessage) {
218+
try {
219+
const result = await this.onBeforeMessage(message)
220+
if (result === null) return // consumed by interceptor
221+
message = result
222+
} catch (error) {
223+
console.error("[Kilo New] KiloProvider: interceptor error:", error)
224+
return
225+
}
226+
}
227+
165228
switch (message.type) {
166229
case "webviewReady":
167230
console.log("[Kilo New] KiloProvider: ✅ webviewReady received")
@@ -472,7 +535,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
472535
}
473536

474537
try {
475-
const workspaceDir = this.getWorkspaceDirectory()
538+
const workspaceDir = this.getWorkspaceDirectory(sessionID)
476539
const messagesData = await this.httpClient.getMessages(sessionID, workspaceDir)
477540

478541
// Update currentSession so fallback logic in handleSendMessage/handleAbort
@@ -532,6 +595,26 @@ export class KiloProvider implements vscode.WebviewViewProvider {
532595
const workspaceDir = this.getWorkspaceDirectory()
533596
const sessions = await this.httpClient.listSessions(workspaceDir)
534597

598+
// Also fetch sessions from worktree directories so they appear in the list
599+
const worktreeDirs = new Set(this.sessionDirectories.values())
600+
const extra = await Promise.all(
601+
[...worktreeDirs].map((dir) =>
602+
this.httpClient!.listSessions(dir).catch((err) => {
603+
console.error(`[Kilo New] KiloProvider: Failed to list sessions for ${dir}:`, err)
604+
return [] as SessionInfo[]
605+
}),
606+
),
607+
)
608+
const seen = new Set(sessions.map((s) => s.id))
609+
for (const batch of extra) {
610+
for (const s of batch) {
611+
if (!seen.has(s.id)) {
612+
sessions.push(s)
613+
seen.add(s.id)
614+
}
615+
}
616+
}
617+
535618
this.postMessage({
536619
type: "sessionsLoaded",
537620
sessions: sessions.map((s) => this.sessionToWebview(s)),
@@ -555,9 +638,10 @@ export class KiloProvider implements vscode.WebviewViewProvider {
555638
}
556639

557640
try {
558-
const workspaceDir = this.getWorkspaceDirectory()
641+
const workspaceDir = this.getWorkspaceDirectory(sessionID)
559642
await this.httpClient.deleteSession(sessionID, workspaceDir)
560643
this.trackedSessionIds.delete(sessionID)
644+
this.sessionDirectories.delete(sessionID)
561645
if (this.currentSession?.id === sessionID) {
562646
this.currentSession = null
563647
}
@@ -581,7 +665,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
581665
}
582666

583667
try {
584-
const workspaceDir = this.getWorkspaceDirectory()
668+
const workspaceDir = this.getWorkspaceDirectory(sessionID)
585669
const updated = await this.httpClient.updateSession(sessionID, { title }, workspaceDir)
586670
if (this.currentSession?.id === sessionID) {
587671
this.currentSession = updated
@@ -774,7 +858,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
774858
}
775859

776860
try {
777-
const workspaceDir = this.getWorkspaceDirectory()
861+
const workspaceDir = this.getWorkspaceDirectory(sessionID || this.currentSession?.id)
778862

779863
// Create session if needed
780864
if (!sessionID && !this.currentSession) {
@@ -842,7 +926,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
842926
}
843927

844928
try {
845-
const workspaceDir = this.getWorkspaceDirectory()
929+
const workspaceDir = this.getWorkspaceDirectory(targetSessionID)
846930
await this.httpClient.abortSession(targetSessionID, workspaceDir)
847931
} catch (error) {
848932
console.error("[Kilo New] KiloProvider: Failed to abort session:", error)
@@ -877,7 +961,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
877961
}
878962

879963
try {
880-
const workspaceDir = this.getWorkspaceDirectory()
964+
const workspaceDir = this.getWorkspaceDirectory(target)
881965
await this.httpClient.summarize(target, providerID, modelID, workspaceDir)
882966
} catch (error) {
883967
console.error("[Kilo New] KiloProvider: Failed to compact session:", error)
@@ -907,7 +991,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
907991
}
908992

909993
try {
910-
const workspaceDir = this.getWorkspaceDirectory()
994+
const workspaceDir = this.getWorkspaceDirectory(targetSessionID)
911995
await this.httpClient.respondToPermission(targetSessionID, permissionId, response, workspaceDir)
912996
} catch (error) {
913997
console.error("[Kilo New] KiloProvider: Failed to respond to permission:", error)
@@ -924,7 +1008,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
9241008
}
9251009

9261010
try {
927-
await this.httpClient.replyToQuestion(requestID, answers, this.getWorkspaceDirectory())
1011+
await this.httpClient.replyToQuestion(requestID, answers, this.getWorkspaceDirectory(this.currentSession?.id))
9281012
} catch (error) {
9291013
console.error("[Kilo New] KiloProvider: Failed to reply to question:", error)
9301014
this.postMessage({ type: "questionError", requestID })
@@ -941,7 +1025,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
9411025
}
9421026

9431027
try {
944-
await this.httpClient.rejectQuestion(requestID, this.getWorkspaceDirectory())
1028+
await this.httpClient.rejectQuestion(requestID, this.getWorkspaceDirectory(this.currentSession?.id))
9451029
} catch (error) {
9461030
console.error("[Kilo New] KiloProvider: Failed to reject question:", error)
9471031
this.postMessage({ type: "questionError", requestID })
@@ -1329,9 +1413,14 @@ export class KiloProvider implements vscode.WebviewViewProvider {
13291413
}
13301414

13311415
/**
1332-
* Get the workspace directory.
1416+
* Get the workspace directory for a session.
1417+
* Checks session directory overrides first (e.g., worktree paths), then falls back to workspace root.
13331418
*/
1334-
private getWorkspaceDirectory(): string {
1419+
private getWorkspaceDirectory(sessionId?: string): string {
1420+
if (sessionId) {
1421+
const dir = this.sessionDirectories.get(sessionId)
1422+
if (dir) return dir
1423+
}
13351424
const workspaceFolders = vscode.workspace.workspaceFolders
13361425
if (workspaceFolders && workspaceFolders.length > 0) {
13371426
return workspaceFolders[0].uri.fsPath
@@ -1359,5 +1448,6 @@ export class KiloProvider implements vscode.WebviewViewProvider {
13591448
this.unsubscribeState?.()
13601449
this.webviewMessageDisposable?.dispose()
13611450
this.trackedSessionIds.clear()
1451+
this.sessionDirectories.clear()
13621452
}
13631453
}

0 commit comments

Comments
 (0)