feat(PILOT-50): add AI-aware output target selector - #51
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an AI-aware output target selector to the PromptPilot IntelliJ plugin, enabling users to route generated repository context files to different AI tool locations (GitHub Copilot, Cursor, or custom paths). The implementation introduces a dropdown UI component, persistence layer, and dynamic path resolution with automatic directory creation.
Key Changes:
- New dropdown UI component for selecting output targets (PromptPilot default, GitHub Copilot, Cursor, or Custom)
- Output target persistence using PropertiesComponent with backward compatibility for existing custom paths
- Live destination preview showing the resolved path and directory existence status
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/main/kotlin/com/github/egarcia/promptpilot/file/ContextFileManager.kt |
Refactored output path resolution to use ContextOutputTarget enum, added backward compatibility logic for custom paths |
src/main/kotlin/com/github/egarcia/promptpilot/toolWindow/PromptPilotToolWindowFactory.kt |
Added AI Output Target dropdown UI, custom field controls, live preview labels, and save functionality |
src/main/kotlin/com/github/egarcia/promptpilot/Constants.kt |
Added OUTPUT_TARGET_KEY and new file constants for GitHub Copilot and Cursor paths |
src/main/kotlin/com/github/egarcia/promptpilot/resources/StringKeys.kt |
Added string key constants for all new output target labels and messages |
src/main/resources/messages/MyBundle.properties |
Added localized strings for output target UI, fixed typo in files.label |
README.md |
Added "AI Output Targets" documentation section with feature table |
gradle.properties |
Bumped version from 0.1.0 to 0.1.2 |
.github/labeler.yml |
Added refactor label pattern |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
8edd0b6 to
5df859c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (2)
src/main/kotlin/com/github/egarcia/promptpilot/file/ContextFileManager.kt:81
- The new output target resolution logic in
ContextFileManagerlacks test coverage. Consider adding tests for:
hasCustomOverrides()detection logicresolveOutputLocation()for each target type- Path resolution with custom and preset targets
- Directory creation behavior via
ensureDirectoriesExist() - Backward compatibility with existing custom settings
private val selectedTarget: ContextOutputTarget
get() {
val valueSet = properties.isValueSet(SettingsKeys.OUTPUT_TARGET_KEY)
val storedTarget = properties.getValue(SettingsKeys.OUTPUT_TARGET_KEY)?.let { fromId(it) }
if (storedTarget != null) return storedTarget
if (!valueSet && hasCustomOverrides()) {
return ContextOutputTarget.CUSTOM
}
return ContextOutputTarget.PROMPT_PILOT
}
private fun hasCustomOverrides(): Boolean {
val customDir = properties.getValue(SettingsKeys.CUSTOM_OUTPUT_DIR)
val customFile = properties.getValue(SettingsKeys.CUSTOM_OUTPUT_FILENAME)
val dirDiffers = !customDir.isNullOrBlank() && customDir != FileConstants.OUTPUT_DIR
val fileDiffers = !customFile.isNullOrBlank() && customFile != FileConstants.REPO_CONTEXT_FILENAME
return dirDiffers || fileDiffers
}
private fun resolveOutputLocation(): OutputLocation {
val target = selectedTarget
val location = target.defaultLocation()
val relativeDir = when {
target.isCustom -> properties.getValue(SettingsKeys.CUSTOM_OUTPUT_DIR)?.takeUnless { it.isBlank() }
?: FileConstants.OUTPUT_DIR
location != null -> location.relativeDir
else -> FileConstants.OUTPUT_DIR
}
val filename = when {
target.isCustom -> properties.getValue(SettingsKeys.CUSTOM_OUTPUT_FILENAME)?.takeUnless { it.isBlank() }
?: FileConstants.REPO_CONTEXT_FILENAME
location != null -> location.filename
else -> FileConstants.REPO_CONTEXT_FILENAME
}
return OutputLocation(relativeDir, filename)
}
fun ensureDirectoriesExist() {
val location = resolveOutputLocation()
var lastAttempted = FileConstants.SOURCE_CONTEXT_DIR
runCatching {
ensureDirectoryExists(sourceDir)
lastAttempted = location.relativeDir
val outputDir = Paths.get(basePath, location.relativeDir).normalize().toFile()
ensureDirectoryExists(outputDir)
}.onFailure { e ->
throw IllegalStateException(
MyBundle.message(
Strings.ERROR_CREATING_OUTPUT_DIRECTORY,
lastAttempted,
e.message ?: MyBundle.message(Strings.ERROR_UNKNOWN)
)
)
}
}
src/main/kotlin/com/github/egarcia/promptpilot/toolWindow/PromptPilotToolWindowFactory.kt:457
- The save button action listener calls
fileManager.ensureDirectoriesExist()but does not handle potential exceptions. If directory creation fails (e.g., due to permissions issues), the user will see no feedback in the UI, and the success message will still be displayed. Consider wrapping this call in a try-catch block and showing an error message if directory creation fails.
saveButton.addActionListener {
val target = selectedTarget()
properties.setValue(SettingsKeys.OUTPUT_TARGET_KEY, target.id)
if (target.isCustom) {
properties.setValue(SettingsKeys.CUSTOM_OUTPUT_DIR, customOutputDirField.text.trim())
properties.setValue(
SettingsKeys.CUSTOM_OUTPUT_FILENAME,
customOutputFileField.text.trim()
)
}
fileManager.ensureDirectoriesExist()
updateDestinationLabels()
Messages.showInfoMessage(
project,
MyBundle.message(Strings.SAVE_OUTPUT_SETTINGS_SUCCESS),
MyBundle.message(Strings.TOOL_WINDOW_TITLE)
)
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Qodana Community for JVMIt seems all right 👌 No new problems were found according to the checks applied 💡 Qodana analysis was run in the pull request mode: only the changed files were checked View the detailed Qodana reportTo be able to view the detailed Qodana report, you can either:
To get - name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2024.2.5
with:
upload-result: trueContact Qodana teamContact us at qodana-support@jetbrains.com
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| properties.setValue( | ||
| SettingsKeys.CUSTOM_OUTPUT_FILENAME, | ||
| customOutputFileField.text.trim() | ||
| ) |
There was a problem hiding this comment.
When a non-CUSTOM target is selected and the save button is clicked, the custom output directory and filename properties are not cleared. This could lead to confusion if a user switches back to CUSTOM later, as they might see stale values. Consider clearing these properties when saving a non-CUSTOM target, or document this behavior if it's intentional.
| ) | |
| ) | |
| } else { | |
| properties.unsetValue(SettingsKeys.CUSTOM_OUTPUT_DIR) | |
| properties.unsetValue(SettingsKeys.CUSTOM_OUTPUT_FILENAME) |
| output.target.github.copilot.label=GitHub Copilot (.github/copilot-instructions.md) | ||
| output.target.cursor.label=Cursor (.cursor/rules) | ||
| output.target.custom.label=Custom (choose directory + file) | ||
| output.target.custom.hint=Custom paths are resolved relative to the project root unless absolute. |
There was a problem hiding this comment.
The documentation in MyBundle.properties and the hint label claim that custom paths are resolved relative to the project root "unless absolute", but there's no explicit handling of absolute paths in resolveOutputLocation() or getOutputFile(). Paths.get(basePath, location.relativeDir) will treat an absolute path as relative if it doesn't start with the filesystem root. Consider either:
- Adding explicit logic to detect and handle absolute paths correctly
- Updating the documentation to clarify that paths should always be relative
| output.target.custom.hint=Custom paths are resolved relative to the project root unless absolute. | |
| output.target.custom.hint=Custom paths must be relative to the project root. |
45a85e7 to
080fd56
Compare
080fd56 to
45505b7
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!fsOps.exists(outputDir)) fsOps.createDirectories(outputDir.toPath()) | ||
| ensureDirectoryExists(sourceDir) | ||
| lastAttempted = location.relativeDir | ||
| val outputDir = Paths.get(basePath, location.relativeDir).normalize().toFile() |
There was a problem hiding this comment.
The custom directory path from user input is used without validation for path traversal attempts. A malicious or accidental input like ../../etc could write files outside the project directory. Consider adding validation to ensure the resolved path after normalization is still within the project root, or at minimum warning the user when an absolute path or path traversal is detected.
| val outputDir = Paths.get(basePath, location.relativeDir).normalize().toFile() | |
| val outputDir = Paths.get(basePath, location.relativeDir).normalize().toFile() | |
| // Validate that outputDir is within basePath to prevent path traversal | |
| val baseCanonical = File(basePath).canonicalFile | |
| val outputCanonical = outputDir.canonicalFile | |
| if (!outputCanonical.path.startsWith(baseCanonical.path)) { | |
| throw IllegalArgumentException( | |
| MyBundle.message( | |
| Strings.ERROR_CREATING_OUTPUT_DIRECTORY, | |
| lastAttempted, | |
| "Output directory is outside the project root: ${outputCanonical.path}" | |
| ) | |
| ) | |
| } |
| # PromptPilot Changelog | ||
|
|
||
| ## [Unreleased] | ||
| ## 1.1.0 |
There was a problem hiding this comment.
The CHANGELOG.md indicates version "1.1.0" but gradle.properties sets the version to "0.1.1". These version numbers should be consistent - either both should use "0.1.1" (indicating a pre-1.0 release) or both should use "1.1.0" (indicating a post-1.0 release following the existing 1.0.0 entry in the changelog).
| ## 1.1.0 | |
| ## 0.1.1 |
| fileName | ||
| ) | ||
| val dirFile = Paths.get(basePath, relativeDir).normalize().toFile() | ||
| if (dirFile.exists()) { |
There was a problem hiding this comment.
Similar to the issue in ContextFileManager, the custom directory path from customOutputDirField.text is used directly without validation for path traversal. Consider validating that the resolved path stays within the project boundaries or displaying a warning when absolute paths or parent directory references are used.
| if (dirFile.exists()) { | |
| val baseDirFile = Paths.get(basePath).normalize().toFile() | |
| val isWithinProject = try { | |
| val dirCanonical = dirFile.canonicalPath | |
| val baseCanonical = baseDirFile.canonicalPath | |
| dirCanonical.startsWith(baseCanonical) | |
| } catch (e: Exception) { | |
| false | |
| } | |
| if (!isWithinProject) { | |
| destinationStatusLabel.text = | |
| MyBundle.message(Strings.OUTPUT_TARGET_STATUS_INVALID_PATH, relativeDir) | |
| destinationStatusLabel.foreground = JBColor.RED | |
| } else if (dirFile.exists()) { |
257bc5e to
0a063e7
Compare
Description: - add AI Output Target dropdown with Copilot, Cursor, default, and custom presets - persist selections, auto-create target directories, and update docs to cover the new workflow Closes #50
0a063e7 to
4a7d2c0
Compare
• Summary
Details
Testing