From 6718760b46fe6c861e43ca1e6f3a6f2f92db67b3 Mon Sep 17 00:00:00 2001 From: Ownistic Date: Fri, 21 Nov 2025 02:46:50 -0700 Subject: [PATCH] fix(jetbrains): use selected files for commit message generation Previously, the orchestrator would query for all staged/unstaged changes and then filter them, which was inefficient and failed if no general changes existed. This commit inverts the logic to construct changes directly from the list of selected files provided by the JetBrains plugin, ensuring it correctly uses the user's selection. --- .../jetbrains/git/FileDiscoveryService.kt | 122 +++++++++++++----- .../CommitMessageOrchestrator.ts | 33 +++-- 2 files changed, 114 insertions(+), 41 deletions(-) diff --git a/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/git/FileDiscoveryService.kt b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/git/FileDiscoveryService.kt index e5de215a9cb..c5a81ab05b6 100644 --- a/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/git/FileDiscoveryService.kt +++ b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/git/FileDiscoveryService.kt @@ -2,18 +2,14 @@ package ai.kilocode.jetbrains.git import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.progress.util.BackgroundTaskUtil import com.intellij.openapi.project.Project import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.vcs.VcsDataKeys import com.intellij.openapi.vcs.CheckinProjectPanel import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.ChangeListManager -import com.intellij.vcs.commit.CommitMessageUi import com.intellij.openapi.vcs.ui.Refreshable import com.intellij.openapi.wm.ToolWindowManager -import java.util.concurrent.CompletableFuture -import java.util.concurrent.TimeUnit /** * Service for discovering files to include in commit messages @@ -25,53 +21,114 @@ class FileDiscoveryService { * Discover files from the given data context using multiple strategies */ fun discoverFiles(project: Project, dataContext: DataContext): List { - logger.debug("Attempting to discover files from DataContext") + logger.info("Starting file discovery for commit message generation") // Try different strategies in order of preference - return tryCommitMessageControl(project, dataContext) - ?: tryCommitToolWindow(project) + // 1. VcsDataKeys (contextual selection) - most specific + // 2. CheckinProjectPanel (from commit dialog) + // 3. ChangeListManager (fallback to all uncommitted changes) + + val result = tryVcsDataKeys(dataContext) + ?: tryCheckinProjectPanel(dataContext) ?: tryChangeListManager(project) ?: emptyList() + + logger.info("File discovery completed: found ${result.size} files") + return result } - private fun tryCommitMessageControl(project: Project, dataContext: DataContext): List? { + private fun tryVcsDataKeys(dataContext: DataContext): List? { return try { - val commitControl = VcsDataKeys.COMMIT_MESSAGE_CONTROL.getData(dataContext) - if (commitControl is CommitMessageUi) { - // Fallback to change list manager when we have a commit control - val changeListManager = ChangeListManager.getInstance(project) - val changes = changeListManager.defaultChangeList.changes - changes.mapNotNull { it.virtualFile?.path } - } else null + logger.debug("[DIAGNOSTIC] Trying VcsDataKeys discovery...") + + // Try SELECTED_CHANGES first (user selection) + val selectedChanges = VcsDataKeys.SELECTED_CHANGES.getData(dataContext) + logger.debug("VcsDataKeys.SELECTED_CHANGES.getData() returned: ${selectedChanges?.size ?: "null"} changes") + if (!selectedChanges.isNullOrEmpty()) { + val files = selectedChanges.mapNotNull { it.virtualFile?.path } + logger.debug("Mapped SELECTED_CHANGES to ${files.size} files") + if (files.isNotEmpty()) { + return files + } + } + + // Try CHANGES (context changes) + val changes = VcsDataKeys.CHANGES.getData(dataContext) + logger.debug("VcsDataKeys.CHANGES.getData() returned: ${changes?.size ?: "null"} changes") + if (!changes.isNullOrEmpty()) { + val files = changes.mapNotNull { it.virtualFile?.path } + logger.debug("Mapped CHANGES to ${files.size} files") + if (files.isNotEmpty()) { + return files + } + } + + logger.debug("[DIAGNOSTIC] VcsDataKeys: no changes found from either SELECTED_CHANGES or CHANGES") + null } catch (e: Exception) { - logger.debug("CommitMessage control context failed: ${e.message}") + logger.warn("[DIAGNOSTIC] VcsDataKeys discovery failed with exception: ${e.message}", e) null } } - private fun tryCommitToolWindow(project: Project): List? { + private fun tryCheckinProjectPanel(dataContext: DataContext): List? { return try { - val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Commit") - ?: ToolWindowManager.getInstance(project).getToolWindow("Version Control") - ?: return null - - // Fallback to change list manager - val changeListManager = ChangeListManager.getInstance(project) - val changes = changeListManager.defaultChangeList.changes - changes.mapNotNull { it.virtualFile?.path } + logger.debug("[DIAGNOSTIC] Trying CheckinProjectPanel discovery...") + + // Try to get the panel from DataContext + val panel = Refreshable.PANEL_KEY.getData(dataContext) as? CheckinProjectPanel + logger.debug("Refreshable.PANEL_KEY.getData() returned: ${panel?.let { it::class.java.simpleName } ?: "null"}") + if (panel != null) { + logger.debug("[DIAGNOSTIC] Found CheckinProjectPanel") + + // Try to get selected changes + val selectedChanges = try { + panel.selectedChanges + } catch (e: Exception) { + logger.warn("[DIAGNOSTIC] Failed to get selectedChanges from CheckinProjectPanel with exception: ${e.message}", e) + null + } + logger.debug("CheckinProjectPanel.selectedChanges returned: ${selectedChanges?.size ?: "null"} changes") + + if (!selectedChanges.isNullOrEmpty()) { + val files = selectedChanges.mapNotNull { it.virtualFile?.path } + logger.debug("Mapped CheckinProjectPanel.selectedChanges to ${files.size} files") + if (files.isNotEmpty()) { + return files + } + } + + logger.debug("[DIAGNOSTIC] CheckinProjectPanel exists but no selected changes found") + } else { + logger.debug("[DIAGNOSTIC] No CheckinProjectPanel in DataContext") + } + null } catch (e: Exception) { - logger.debug("CommitToolWindow failed: ${e.message}") + logger.warn("[DIAGNOSTIC] CheckinProjectPanel discovery failed with exception: ${e.message}", e) null } } private fun tryChangeListManager(project: Project): List? { return try { + logger.debug("[DIAGNOSTIC] Trying ChangeListManager discovery (fallback)...") + val changeListManager = ChangeListManager.getInstance(project) - val changes = changeListManager.defaultChangeList.changes - changes.mapNotNull { it.virtualFile?.path } + logger.debug("Retrieved ChangeListManager instance") + + // Get all changes from all changelists + val allChanges = changeListManager.allChanges + logger.debug("ChangeListManager.allChanges returned: ${allChanges.size} changes") + if (allChanges.isNotEmpty()) { + val files = allChanges.mapNotNull { it.virtualFile?.path } + logger.debug("Mapped ChangeListManager.allChanges to ${files.size} files") + return files + } + + logger.warn("[DIAGNOSTIC] ChangeListManager: no changes found in any changelist") + null } catch (e: Exception) { - logger.debug("ChangeListManager failed: ${e.message}") + logger.error("[DIAGNOSTIC] ChangeListManager discovery failed with exception: ${e.message}", e) null } } @@ -93,10 +150,13 @@ class FileDiscoveryService { val files = discoverFiles(project, dataContext) when { files.isNotEmpty() -> FileDiscoveryResult.Success(files) - else -> FileDiscoveryResult.NoFiles + else -> { + logger.warn("No files discovered from any source") + FileDiscoveryResult.NoFiles + } } } catch (e: Exception) { - logger.warn("File discovery failed", e) + logger.error("File discovery failed with exception", e) FileDiscoveryResult.Error("Failed to discover files: ${e.message}") } } diff --git a/src/services/commit-message/CommitMessageOrchestrator.ts b/src/services/commit-message/CommitMessageOrchestrator.ts index 5d5f762bc6b..eec350aa8ba 100644 --- a/src/services/commit-message/CommitMessageOrchestrator.ts +++ b/src/services/commit-message/CommitMessageOrchestrator.ts @@ -5,6 +5,7 @@ import { GitExtensionService, GitChange } from "./GitExtensionService" import { CommitMessageGenerator } from "./CommitMessageGenerator" import { ICommitMessageIntegration } from "./adapters/ICommitMessageIntegration" import { t } from "../../i18n" +import { GitStatus } from "./types" export interface ChangeResolution { changes: GitChange[] @@ -99,6 +100,28 @@ export class CommitMessageOrchestrator { selectedFiles?: string[], integration?: ICommitMessageIntegration, ): Promise { + // If specific files are selected, we don't need to query for all changes + // We can construct the change objects directly from the selected files. + if (selectedFiles && selectedFiles.length > 0) { + const changes: GitChange[] = selectedFiles.map((filePath) => { + // A simple way to get status is to check git status for each file + // For now, we assume 'Modified' and 'unstaged' as a safe default for commit dialog selections. + // A more robust implementation might run `git status --porcelain -- ` for each. + const status: GitStatus = "M" // Default to Modified + const staged = false // Default to unstaged + return { + filePath, + status, + staged, + } + }) + return { + changes, + files: selectedFiles, + usedStaged: false, // Assume unstaged as commit dialog selections are typically unstaged + } + } + // Try staged changes first let changes = await gitService.gatherChanges({ staged: true }) let usedStaged = true @@ -109,16 +132,6 @@ export class CommitMessageOrchestrator { usedStaged = false } - // Apply file filtering if specific files were selected - if (selectedFiles && selectedFiles.length > 0) { - // Use simple exact path matching - both VSCode and JetBrains should provide full paths - const workspaceRoot = gitService["workspaceRoot"] // Access private property - changes = changes.filter((change) => { - const relativePath = path.relative(workspaceRoot, change.filePath) - return selectedFiles.includes(change.filePath) || selectedFiles.includes(relativePath) - }) - } - return { changes, files: changes.map((change) => change.filePath),