-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix: VS Code extension ignores non-primary folders in multi-root workspaces #3229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "kimi-code": patch | ||
| --- | ||
|
|
||
| Fix the extension ignoring the non-primary folders of a multi-root workspace: they are now passed to the session as additional directories, edits made in them are tracked in File Changes (and can be kept or undone), and a folder added to the workspace mid-conversation reaches the open session instead of waiting for a reload. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,6 +95,11 @@ export class BridgeHandler { | |
| return vscode.workspace.workspaceFolders?.[0]?.uri ?? null; | ||
| } | ||
|
|
||
| /** Other multi-root workspace folders, passed to the session as ephemeral additionalDirs. */ | ||
| private get additionalWorkspaceDirs(): readonly string[] { | ||
| return vscode.workspace.workspaceFolders?.slice(1).map((folder) => folder.uri.fsPath) ?? []; | ||
| } | ||
|
|
||
| private getWorkDir(webviewId: string): string | null { | ||
| return this.customWorkDirs.get(webviewId) ?? this.workspaceRoot; | ||
| } | ||
|
|
@@ -172,6 +177,7 @@ export class BridgeHandler { | |
| model, | ||
| effort, | ||
| yoloMode: VSCodeSettings.yoloMode, | ||
| additionalDirs: this.additionalWorkspaceDirs, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a user adds another folder to the VS Code workspace after starting a conversation, subsequent prompts compute the updated Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 8f75815 — confirmed exactly as described:
I went this route rather than adding an Covered by two tests: a folder added mid-conversation reaching the open session, and no re-add for one it already has. |
||
| ...(sessionId === undefined ? {} : { sessionId }), | ||
| }); | ||
| this.fileManager.setSession(webviewId, baselineSession(runtime)); | ||
|
|
@@ -181,7 +187,11 @@ export class BridgeHandler { | |
| const current = this.runtime.getSession(sessionId); | ||
| const session = | ||
| current?.session ?? | ||
| (await this.runtime.harness.resumeSession({ id: sessionId, includeSubagents: true })); | ||
| (await this.runtime.harness.resumeSession({ | ||
| id: sessionId, | ||
| includeSubagents: true, | ||
| additionalDirs: this.additionalWorkspaceDirs, | ||
| })); | ||
| if (!areSameFsPath(session.workDir, this.requireWorkDir(webviewId))) { | ||
| if (current === undefined) { | ||
| await session.close().catch((error: unknown) => { | ||
|
|
@@ -241,6 +251,33 @@ export class BridgeHandler { | |
| : `@${mentionTarget}:${selection.start.line + 1}-${selection.end.line + 1}`; | ||
| } | ||
|
|
||
| /** | ||
| * The file's URI if the session may write to it: under its working | ||
| * directory, or under one of its additionalDirs (other multi-root workspace | ||
| * folders, /add-dir). Additional roots sit outside workDir, where the | ||
| * workDir-relative resolver returns undefined, so those are resolved from | ||
| * the absolute path instead. Edits there used to be dropped, never reaching | ||
| * File Changes and impossible to keep or undo. | ||
| */ | ||
| private resolveTrackablePath( | ||
| session: BaselineSession, | ||
| workDirUri: vscode.Uri, | ||
| filePath: string, | ||
| ): vscode.Uri | undefined { | ||
| const resolved = resolveSessionFilePath(workDirUri, session.workDir, filePath); | ||
| if (resolved !== undefined) { | ||
| return isWorkspacePathContainedSync(workDirUri, resolved.uri, { allowMissing: true }) | ||
| ? resolved.uri | ||
| : undefined; | ||
| } | ||
| if (!path.isAbsolute(filePath) && !path.win32.isAbsolute(filePath)) return undefined; | ||
| const candidate = vscode.Uri.file(filePath); | ||
| const underAdditionalDir = (session.additionalDirs ?? []).some((dir) => | ||
| isWorkspacePathContainedSync(vscode.Uri.file(dir), candidate, { allowMissing: true }), | ||
| ); | ||
| return underAdditionalDir ? candidate : undefined; | ||
| } | ||
|
|
||
| captureFileBaseline( | ||
| session: BaselineSession, | ||
| filePath: string, | ||
|
|
@@ -262,24 +299,22 @@ export class BridgeHandler { | |
| return; | ||
| } | ||
|
|
||
| const resolved = resolveSessionFilePath(workDirUri, session.workDir, filePath); | ||
| if ( | ||
| resolved === undefined || | ||
| !isWorkspacePathContainedSync(workDirUri, resolved.uri, { allowMissing: true }) | ||
| ) { | ||
| const resolvedUri = this.resolveTrackablePath(session, workDirUri, filePath); | ||
| if (resolvedUri === undefined) { | ||
| this.logRuntimeError( | ||
| "Unable to capture a file baseline", | ||
| new Error("File is outside the session working directory"), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const capture = this.baselineManager.capture(session, resolved.uri.fsPath); | ||
| const capture = this.baselineManager.capture(session, resolvedUri.fsPath); | ||
|
|
||
| void capture | ||
| .then(async () => { | ||
| await Promise.all( | ||
| webviewIds.map(async (webviewId) => { | ||
| this.fileManager.trackFile(webviewId, resolved.uri.fsPath); | ||
| this.fileManager.trackFile(webviewId, resolvedUri.fsPath); | ||
| await this.fileManager.refreshChanges(webviewId); | ||
| }), | ||
| ); | ||
|
|
@@ -331,14 +366,18 @@ function baselineSession(runtime: SessionRuntime): BaselineSession { | |
| id: runtime.id, | ||
| workDir: runtime.session.workDir, | ||
| metadata: runtime.summary?.metadata, | ||
| additionalDirs: runtime.summary?.additionalDirs, | ||
| }); | ||
| } | ||
|
|
||
| function baselineSummary(summary: Pick<BaselineSession, "id" | "workDir" | "metadata">): BaselineSession { | ||
| function baselineSummary( | ||
| summary: Pick<BaselineSession, "id" | "workDir" | "metadata" | "additionalDirs">, | ||
| ): BaselineSession { | ||
| return { | ||
| id: summary.id, | ||
| workDir: summary.workDir, | ||
| ...(summary.metadata === undefined ? {} : { metadata: summary.metadata }), | ||
| ...(summary.additionalDirs === undefined ? {} : { additionalDirs: summary.additionalDirs }), | ||
| }; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the agent edits a file in one of these newly permitted secondary folders,
captureFileBaselinerejects the absolute path because it requires every captured file to be contained bysession.workDir. The edit therefore succeeds but never appears in the extension's File Changes list and cannot be restored through its keep/undo or fork-baseline flows. Extend baseline and file tracking to understand the additional workspace roots before exposing them as writable session directories.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 8f75815 — good catch, and it turned out to be three containment gates rather than one:
captureFileBaselinedropped these beforeBaselineManagerever saw them. Its resolver returnsundefinedoutsideworkDir, so additional-root files are now resolved from the absolute path and checked against each root.BaselineManager.resolveSessionFilerejected them too. Files under an additional root are keyed in the manifest by absolute path — aworkDir-relative key would escape the root (../other/a.ts) and could collide with a same-named file in another root.paths.resolve(root, <absolute>)returns an absolute path unchanged, so the key round-trips on read and no manifest version bump is needed.requireContainedRestorePath, the realpath-based symlink guard on undo, only knewworkDir. It now takes the session's full root list and accepts a path contained by any of them, still resolving both sides throughrealpathso a symlink can't point a restore outside them. This one surfaced from the new test's undo assertion rather than from reading the code.Added coverage for capture → File Changes → undo under a secondary root, plus a regression test that a path outside both the workspace and its additional roots is still rejected.