Skip to content

fix(amazonq): /doc add suppoort for uploading infrastructure diagrams #5357

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

Merged
merged 2 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "Amazon Q /doc: support making changes to architecture diagrams"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@

package software.aws.toolkits.jetbrains.services.amazonqDoc

import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUp
import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpStatusType
import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpTypes
import software.aws.toolkits.resources.message

const val FEATURE_EVALUATION_PRODUCT_NAME = "DocGeneration"

const val FEATURE_NAME = "Amazon Q Documentation Generation"
Expand All @@ -21,25 +16,8 @@ const val DEFAULT_RETRY_LIMIT = 0
// Max allowed size for a repository in bytes
const val MAX_PROJECT_SIZE_BYTES: Long = 200 * 1024 * 1024

enum class ModifySourceFolderErrorReason(
private val reasonText: String,
) {
ClosedBeforeSelection("ClosedBeforeSelection"),
NotInWorkspaceFolder("NotInWorkspaceFolder"),
;

override fun toString(): String = reasonText
}

val NEW_SESSION_FOLLOWUPS: List<FollowUp> = listOf(
FollowUp(
pillText = message("amazonqDoc.prompt.reject.new_task"),
type = FollowUpTypes.NEW_TASK,
status = FollowUpStatusType.Info
),
FollowUp(
pillText = message("amazonqDoc.prompt.reject.close_session"),
type = FollowUpTypes.CLOSE_SESSION,
status = FollowUpStatusType.Info
)
)
const val INFRA_DIAGRAM_PREFIX = "infra."
const val DIAGRAM_SVG_EXT = "svg"
const val DIAGRAM_DOT_EXT = "dot"
val SUPPORTED_DIAGRAM_EXT_SET: Set<String> = setOf(DIAGRAM_SVG_EXT, DIAGRAM_DOT_EXT)
val SUPPORTED_DIAGRAM_FILE_NAME_SET: Set<String> = SUPPORTED_DIAGRAM_EXT_SET.map { INFRA_DIAGRAM_PREFIX + it }.toSet()
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import com.intellij.diff.requests.SimpleDiffRequest
import com.intellij.diff.util.DiffUserDataKeys
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.testFramework.LightVirtualFile
import kotlinx.coroutines.withContext
import org.intellij.images.fileTypes.impl.SvgFileType
import software.amazon.awssdk.services.codewhispererruntime.model.DocFolderLevel
import software.amazon.awssdk.services.codewhispererruntime.model.DocInteractionType
import software.amazon.awssdk.services.codewhispererruntime.model.DocUserDecision
Expand All @@ -33,6 +36,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitConte
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
import software.aws.toolkits.jetbrains.services.amazonqDoc.DEFAULT_RETRY_LIMIT
import software.aws.toolkits.jetbrains.services.amazonqDoc.DIAGRAM_SVG_EXT
import software.aws.toolkits.jetbrains.services.amazonqDoc.DocException
import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_NAME
import software.aws.toolkits.jetbrains.services.amazonqDoc.InboundAppMessagesHandler
Expand Down Expand Up @@ -374,45 +378,51 @@ class DocController(

override suspend fun processOpenDiff(message: IncomingDocMessage.OpenDiff) {
val session = getSessionInfo(message.tabId)

val project = context.project
val sessionState = session.sessionState

when (sessionState) {
is PrepareDocGenerationState -> {
runInEdt {
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)

val leftDiffContent = if (existingFile == null) {
EmptyContent()
} else {
DiffContentFactory.getInstance().create(project, existingFile)
}

val newFileContent = sessionState.filePaths.find { it.zipFilePath == message.filePath }?.fileContent
if (sessionState !is PrepareDocGenerationState) {
logger.error { "$FEATURE_NAME: OpenDiff event is received for a conversation that has ${session.sessionState.phase} phase" }
messenger.sendError(
tabId = message.tabId,
errMessage = message("amazonqFeatureDev.exception.open_diff_failed"),
retries = 0,
conversationId = session.conversationIdUnsafe
)
return
}

val rightDiffContent = if (message.deleted || newFileContent == null) {
EmptyContent()
} else {
DiffContentFactory.getInstance().create(newFileContent)
}
runInEdt {
val newFileContent = sessionState.filePaths.find { it.zipFilePath == message.filePath }?.fileContent

val request = SimpleDiffRequest(message.filePath, leftDiffContent, rightDiffContent, null, null)
request.putUserData(DiffUserDataKeys.FORCE_READ_ONLY, true)
val isSvgFile = message.filePath.lowercase().endsWith(".".plus(DIAGRAM_SVG_EXT))
if (isSvgFile && newFileContent != null) {
// instead of diff display generated svg in edit/preview window
val inMemoryFile = LightVirtualFile(
message.filePath,
SvgFileType.INSTANCE,
newFileContent
)
inMemoryFile.isWritable = false
FileEditorManager.getInstance(context.project).openFile(inMemoryFile, true)
} else {
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
val leftDiffContent = if (existingFile == null) {
EmptyContent()
} else {
DiffContentFactory.getInstance().create(context.project, existingFile)
}

val newDiff = ChainDiffVirtualFile(SimpleDiffRequestChain(request), message.filePath)
DiffEditorTabFilesManager.getInstance(context.project).showDiffFile(newDiff, true)
val rightDiffContent = if (message.deleted || newFileContent == null) {
EmptyContent()
} else {
DiffContentFactory.getInstance().create(newFileContent)
}
}

else -> {
logger.error { "$FEATURE_NAME: OpenDiff event is received for a conversation that has ${session.sessionState.phase} phase" }
messenger.sendError(
tabId = message.tabId,
errMessage = message("amazonqFeatureDev.exception.open_diff_failed"),
retries = 0,
conversationId = session.conversationIdUnsafe
)
val request = SimpleDiffRequest(message.filePath, leftDiffContent, rightDiffContent, null, null)
request.putUserData(DiffUserDataKeys.FORCE_READ_ONLY, true)

val newDiff = ChainDiffVirtualFile(SimpleDiffRequestChain(request), message.filePath)
DiffEditorTabFilesManager.getInstance(context.project).showDiffFile(newDiff, true)
}
}
}
Expand Down Expand Up @@ -738,6 +748,7 @@ class DocController(
SessionStatePhase.CODEGEN -> {
onCodeGeneration(session, message, tabId, mode)
}

else -> null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthNeededState
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.ProgressField
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.messages.PromptProgressMessage
import software.aws.toolkits.jetbrains.services.amazonqDoc.NEW_SESSION_FOLLOWUPS
import software.aws.toolkits.jetbrains.services.amazonqDoc.ui.NEW_SESSION_FOLLOWUPS
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import software.aws.toolkits.jetbrains.common.util.AmazonQCodeGenService
import software.aws.toolkits.jetbrains.common.util.resolveAndCreateOrUpdateFile
import software.aws.toolkits.jetbrains.common.util.resolveAndDeleteFile
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonqDoc.CODE_GENERATION_RETRY_LIMIT
import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_NAME
Expand All @@ -40,7 +39,7 @@
private val logger = getLogger<AmazonQCodeGenerateClient>()

class DocSession(val tabID: String, val project: Project) {
var context: FeatureDevSessionContext
var context: DocSessionContext = DocSessionContext(project, MAX_PROJECT_SIZE_BYTES)
val sessionStartTime = System.currentTimeMillis()

var state: SessionState?
Expand All @@ -48,7 +47,7 @@
var localConversationId: String? = null
var localLatestMessage: String = ""
var task: String = ""
val proxyClient: AmazonQCodeGenerateClient

Check notice on line 50 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/DocSession.kt

View workflow job for this annotation

GitHub Actions / qodana

Join declaration and assignment

Can be joined with assignment
val amazonQCodeGenService: AmazonQCodeGenService
private val _reportedChanges = mutableMapOf<String, String>()

Expand All @@ -59,7 +58,6 @@
var isAuthenticating: Boolean

init {
context = FeatureDevSessionContext(project, MAX_PROJECT_SIZE_BYTES)
proxyClient = AmazonQCodeGenerateClient.getInstance(project)
amazonQCodeGenService = AmazonQCodeGenService(proxyClient, project)
state = ConversationNotStartedState("", tabID, token = null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonqDoc.session

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonqDoc.SUPPORTED_DIAGRAM_EXT_SET
import software.aws.toolkits.jetbrains.services.amazonqDoc.SUPPORTED_DIAGRAM_FILE_NAME_SET

class DocSessionContext(project: Project, maxProjectSizeBytes: Long? = null) : FeatureDevSessionContext(project, maxProjectSizeBytes) {

/**
* Ensure diagram files are not ignored
*/
override fun getAdditionalGitIgnoreBinaryFilesRules(): Set<String> {
val ignoreRules = super.getAdditionalGitIgnoreBinaryFilesRules()
val diagramExtRulesInGitIgnoreFormatSet = SUPPORTED_DIAGRAM_EXT_SET.map { "*.$it" }.toSet()
return ignoreRules - diagramExtRulesInGitIgnoreFormatSet
}

/**
* Ensure diagram files are not filtered
*/
override fun isFileExtensionAllowed(file: VirtualFile): Boolean {
if (super.isFileExtensionAllowed(file)) {
return true
}

return file.extension != null && SUPPORTED_DIAGRAM_FILE_NAME_SET.contains(file.name)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonqDoc.ui

import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUp
import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpStatusType
import software.aws.toolkits.jetbrains.services.amazonqDoc.messages.FollowUpTypes
import software.aws.toolkits.resources.message

Check warning on line 9 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/ui/UiContants.kt

View workflow job for this annotation

GitHub Actions / qodana

Usage of redundant or deprecated syntax or deprecated symbols

Remove deprecated symbol import

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols Warning

Remove deprecated symbol import
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please address

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AmazonQ bundle doesn't have those messages yet

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols

Remove deprecated symbol import

val NEW_SESSION_FOLLOWUPS: List<FollowUp> = listOf(
FollowUp(
pillText = message("amazonqDoc.prompt.reject.new_task"),

Check warning on line 13 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/ui/UiContants.kt

View workflow job for this annotation

GitHub Actions / qodana

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols Warning

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead
type = FollowUpTypes.NEW_TASK,
status = FollowUpStatusType.Info
),
FollowUp(
pillText = message("amazonqDoc.prompt.reject.close_session"),

Check warning on line 18 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/ui/UiContants.kt

View workflow job for this annotation

GitHub Actions / qodana

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols Warning

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead

Check warning

Code scanning / QDJVMC

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead
type = FollowUpTypes.CLOSE_SESSION,
status = FollowUpStatusType.Info
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface RepoSizeError {
}
class RepoSizeLimitError(override val message: String) : RuntimeException(), RepoSizeError

class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Long? = null) {
open class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Long? = null) {
// TODO: Need to correct this class location in the modules going further to support both amazonq and codescan.

private val additionalGitIgnoreFolderRules = setOf(
Expand All @@ -61,7 +61,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
"dist",
)

private val additionalGitIgnoreBinaryFilesRules = setOf(
private val defaultAdditionalGitIgnoreBinaryFilesRules = setOf(
"*.zip",
"*.bin",
"*.png",
Expand Down Expand Up @@ -91,17 +91,17 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
// selectedSourceFolder: is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
private var _selectedSourceFolder = projectRoot
private var ignorePatternsWithGitIgnore = emptyList<Regex>()
private var ignorePatternsForBinaryFiles = additionalGitIgnoreBinaryFilesRules
.map { convertGitIgnorePatternToRegex(it) }
.mapNotNull { pattern ->
runCatching { Regex(pattern) }.getOrNull()
}
private var ignorePatternsForBinaryFiles = buildIgnorePatternsForBinaryFiles()

private val gitIgnoreFile = File(selectedSourceFolder.path, ".gitignore")

init {
ignorePatternsWithGitIgnore = try {
buildList {
addAll(additionalGitIgnoreFolderRules.map { convertGitIgnorePatternToRegex(it) })
addAll(
additionalGitIgnoreFolderRules
.map { convertGitIgnorePatternToRegex(it) }
)
addAll(parseGitIgnore())
}.mapNotNull { pattern ->
runCatching { Regex(pattern) }.getOrNull()
Expand All @@ -111,6 +111,15 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
}
}

private fun buildIgnorePatternsForBinaryFiles(): List<Regex> =
getAdditionalGitIgnoreBinaryFilesRules()
.map { convertGitIgnorePatternToRegex(it) }
.mapNotNull { pattern ->
runCatching { Regex(pattern) }.getOrNull()
}

open fun getAdditionalGitIgnoreBinaryFilesRules(): Set<String> = defaultAdditionalGitIgnoreBinaryFilesRules

// This function checks for existence of `devfile.yaml` in customer's repository, currently only `devfile.yaml` is supported for this feature.
fun checkForDevFile(): Boolean {
val devFile = File(projectRoot.path, "/devfile.yaml")
Expand All @@ -129,7 +138,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
return ZipCreationResult(zippedProject, checkSum256, zippedProject.length())
}

fun isFileExtensionAllowed(file: VirtualFile): Boolean {
open fun isFileExtensionAllowed(file: VirtualFile): Boolean {
// if it is a directory, it is allowed
if (file.isDirectory) return true
val extension = file.extension ?: return false
Expand Down
Loading