mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge pull request #11813 from Kilo-Org/luck-family
fix(jetbrains): resolve workspace by project id
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep JetBrains sessions scoped to the correct worktree when multiple IntelliJ windows are open.
|
||||
+24
-8
@@ -40,6 +40,8 @@ import com.intellij.openapi.roots.ProjectFileIndex
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import com.intellij.platform.project.findProjectOrNull
|
||||
import com.intellij.navigation.NavigationItem
|
||||
import com.intellij.psi.PsiFileSystemItem
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
@@ -68,8 +70,8 @@ import kotlin.coroutines.resume
|
||||
* Backend implementation of [KiloWorkspaceRpcApi].
|
||||
*
|
||||
* Routes through the [KiloBackendWorkspaceManager] to get a workspace
|
||||
* for the given directory. No [ProjectManager] dependency — any
|
||||
* directory (including worktrees) can get a workspace.
|
||||
* for the given directory. Project lookup is only used to resolve the
|
||||
* calling frontend project to the correct backend directory.
|
||||
*/
|
||||
class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
||||
companion object {
|
||||
@@ -94,12 +96,15 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi {
|
||||
private val manager: KiloBackendWorkspaceManager
|
||||
get() = app.workspaces
|
||||
|
||||
override suspend fun resolveProjectDirectory(hint: String): String {
|
||||
// In monolith mode, find the open project whose basePath matches the hint.
|
||||
// In split mode, the backend's project.basePath is the real directory.
|
||||
val projects = ProjectManager.getInstance().openProjects
|
||||
val match = projects.firstOrNull { !it.isDefault }
|
||||
return match?.basePath ?: hint
|
||||
override suspend fun resolveProjectDirectory(projectId: ProjectId?, hint: String): String {
|
||||
// Experimental IntelliJ ProjectId API: maps the calling frontend project
|
||||
// to the matching backend project across monolith windows and split mode.
|
||||
val base = projectId?.findProjectOrNull()?.takeIf { !it.isDefault }?.basePath
|
||||
if (base != null) return base
|
||||
val bases = ProjectManager.getInstance().openProjects
|
||||
.filter { !it.isDefault }
|
||||
.mapNotNull { it.basePath }
|
||||
return resolveProjectDirectoryHint(hint, bases)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,6 +450,17 @@ internal fun normalizeWorkspacePath(path: String): String? {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun resolveProjectDirectoryHint(hint: String, bases: List<String>): String {
|
||||
val clean = normalizeWorkspacePath(hint)
|
||||
val match = bases.firstOrNull { base ->
|
||||
val path = normalizeWorkspacePath(base)
|
||||
path != null && clean != null && path == clean
|
||||
}
|
||||
if (match != null) return match
|
||||
if (hint.isNotBlank()) return hint
|
||||
return bases.firstOrNull() ?: hint
|
||||
}
|
||||
|
||||
internal fun workspaceGitAvailable(base: Path, cache: ConcurrentHashMap<String, Boolean> = ConcurrentHashMap()): Boolean {
|
||||
if (Files.exists(base.resolve(".git"))) return true
|
||||
return cache.getOrPut(base.toString()) {
|
||||
|
||||
+34
@@ -77,6 +77,40 @@ class WorkspacePathScopingTest {
|
||||
assertNull(normalizeWorkspacePath("file://%"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `project directory hint matches second open project`() {
|
||||
assertEquals(
|
||||
"/repo/wt-b",
|
||||
resolveProjectDirectoryHint("/repo/wt-b", listOf("/repo/wt-a", "/repo/wt-b")),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unmatched project directory hint is preserved`() {
|
||||
assertEquals(
|
||||
"/repo/wt-c",
|
||||
resolveProjectDirectoryHint("/repo/wt-c", listOf("/repo/wt-a", "/repo/wt-b")),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blank project directory hint falls back to first project`() {
|
||||
assertEquals("/repo/wt-a", resolveProjectDirectoryHint("", listOf("/repo/wt-a", "/repo/wt-b")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blank project directory hint without projects stays blank`() {
|
||||
assertEquals("", resolveProjectDirectoryHint("", emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `project directory hint comparison normalizes paths`() {
|
||||
assertEquals(
|
||||
"/repo/wt-b",
|
||||
resolveProjectDirectoryHint("/repo/wt-b/./", listOf("/repo/wt-a", "/repo/wt-b")),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `git availability detects temp repository`() {
|
||||
val dir = repo() ?: return
|
||||
|
||||
+4
-1
@@ -12,6 +12,7 @@ import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.wm.ToolWindow
|
||||
import com.intellij.openapi.wm.ToolWindowFactory
|
||||
import com.intellij.platform.project.projectIdOrNull
|
||||
import com.intellij.ui.content.ContentFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,9 +45,11 @@ internal class KiloToolWindowSetupService(
|
||||
try {
|
||||
val workspaces = service<KiloWorkspaceService>()
|
||||
val hint = project.basePath ?: ""
|
||||
// Experimental IntelliJ ProjectId API keeps multi-window and split-mode routing exact.
|
||||
val pid = project.projectIdOrNull()
|
||||
|
||||
cs.launch {
|
||||
val dir = workspaces.resolveProjectDirectory(hint)
|
||||
val dir = workspaces.resolveProjectDirectory(pid, hint)
|
||||
val workspace = workspaces.workspace(dir)
|
||||
withContext(Dispatchers.Main) {
|
||||
setup(project, toolWindow, workspace)
|
||||
|
||||
+4
-3
@@ -12,6 +12,7 @@ import ai.kilocode.rpc.dto.ModelsWorkspaceDto
|
||||
import ai.kilocode.rpc.dto.WorkspaceFileDto
|
||||
import com.intellij.openapi.components.Service
|
||||
import ai.kilocode.log.KiloLog
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import fleet.rpc.client.durable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -88,10 +89,10 @@ class KiloWorkspaceService internal constructor(
|
||||
* `/home/.cache/JetBrains/RemoteDev/...`). The backend resolves
|
||||
* it to the actual project root on the host.
|
||||
*/
|
||||
suspend fun resolveProjectDirectory(hint: String): String {
|
||||
suspend fun resolveProjectDirectory(projectId: ProjectId?, hint: String): String {
|
||||
return try {
|
||||
val resolved = call { resolveProjectDirectory(hint) }
|
||||
LOG.info("Resolved project directory: hint=$hint → $resolved")
|
||||
val resolved = call { resolveProjectDirectory(projectId, hint) }
|
||||
LOG.info("Resolved project directory: projectId=$projectId hint=$hint -> $resolved")
|
||||
resolved
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("Failed to resolve directory, falling back to hint=$hint", e)
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ internal abstract class BaseSettingsUi<C : BaseContentPanel, D, P, R, W>(
|
||||
jobs += scope.launch { app.connect() }
|
||||
val path = hint ?: return
|
||||
jobs += scope.launch {
|
||||
val dir = workspaces.resolveProjectDirectory(path)
|
||||
val dir = workspaces.resolveProjectDirectory(null, path)
|
||||
withContext(edt) {
|
||||
projectDirectory = dir
|
||||
workspaceLoaded = false
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
|
||||
import ai.kilocode.rpc.dto.ModelsWorkspaceDto
|
||||
import ai.kilocode.rpc.dto.WorkspaceFileDto
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -50,7 +51,7 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi {
|
||||
var globalConfigPathCalls = 0
|
||||
private set
|
||||
|
||||
override suspend fun resolveProjectDirectory(hint: String): String {
|
||||
override suspend fun resolveProjectDirectory(projectId: ProjectId?, hint: String): String {
|
||||
assertNotEdt("resolveProjectDirectory")
|
||||
return directory
|
||||
}
|
||||
|
||||
+5
-4
@@ -5,6 +5,7 @@ import ai.kilocode.rpc.dto.FileSearchResultDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
|
||||
import ai.kilocode.rpc.dto.ModelsWorkspaceDto
|
||||
import ai.kilocode.rpc.dto.WorkspaceFileDto
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
@@ -29,11 +30,11 @@ interface KiloWorkspaceRpcApi : RemoteApi<Unit> {
|
||||
/**
|
||||
* Resolve the real project directory as seen by the backend.
|
||||
*
|
||||
* In split mode, the frontend's [Project.getBasePath] returns a
|
||||
* synthetic sandbox path. This method returns the backend's actual
|
||||
* project directory so the frontend can use it for CLI server calls.
|
||||
* [projectId] identifies the exact calling frontend project across the
|
||||
* frontend/backend boundary. [hint] is the frontend's project path and is
|
||||
* used as a fallback if the project cannot be resolved on the backend.
|
||||
*/
|
||||
suspend fun resolveProjectDirectory(hint: String): String
|
||||
suspend fun resolveProjectDirectory(projectId: ProjectId?, hint: String): String
|
||||
|
||||
/** Observe workspace state loading progress. */
|
||||
suspend fun state(directory: String): Flow<KiloWorkspaceStateDto>
|
||||
|
||||
Reference in New Issue
Block a user