diff --git a/.changeset/jetbrains-file-drop-references.md b/.changeset/jetbrains-file-drop-references.md new file mode 100644 index 00000000000..e801c8ba498 --- /dev/null +++ b/.changeset/jetbrains-file-drop-references.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Fix dropping files into the JetBrains prompt so code files are added as readable file references and drops anywhere in the session panel feed the prompt attachments. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/PromptAttachment.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/PromptAttachment.kt index ffb642a7e96..214df0ab6a4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/PromptAttachment.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/PromptAttachment.kt @@ -18,35 +18,55 @@ data class PromptAttachment( val mime: String, val url: String, val path: Path? = null, + val reference: Boolean = false, ) { - fun part() = PromptPartDto( - type = "file", - mime = mime, - url = path?.let { data(it, mime) } ?: url, - filename = name, - ) + fun part(): PromptPartDto { + if (reference) { + return PromptPartDto( + type = "file", + mime = mime, + url = url, + filename = name, + ) + } + return PromptPartDto( + type = "file", + mime = mime, + url = path?.let { data(it, mime) } ?: url, + filename = name, + ) + } } object PromptAttachmentExtractor { private const val MAX_BYTES = 10 * 1024 * 1024 fun files(files: List): List = files - .filter { it.exists() && it.isFile && it.canRead() && it.length() <= MAX_BYTES } - .map { file -> + .filter { it.exists() && it.canRead() } + .mapNotNull { file -> val path = file.toPath() val mime = mime(file) - if (!media(mime)) return@map null - PromptAttachment( + if (image(mime)) { + if (!file.isFile || file.length() > MAX_BYTES) return@mapNotNull null + return@mapNotNull PromptAttachment( + id = path.toAbsolutePath().normalize().toString(), + name = path.fileName?.toString() ?: path.name, + mime = mime, + url = path.toUri().toString(), + path = path, + ) + } + return@mapNotNull PromptAttachment( id = path.toAbsolutePath().normalize().toString(), name = path.fileName?.toString() ?: path.name, - mime = mime, + mime = if (file.isDirectory) mime else "text/plain", url = path.toUri().toString(), path = path, + reference = true, ) } - .filterNotNull() - fun media(mime: String): Boolean = mime.startsWith("image/") || mime == "text/plain" + fun image(mime: String): Boolean = mime.startsWith("image/") fun image(raw: Any): PromptAttachment? { val image = when (raw) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index ba7c2a03530..dbea7374493 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -726,7 +726,7 @@ class PromptPanel( @RequiresEdt private fun addAttachment(item: PromptAttachment) { - if (!attachment && PromptAttachmentExtractor.media(item.mime)) { + if (!attachment && !item.reference && PromptAttachmentExtractor.image(item.mime)) { LOG.debug { "kind=prompt-attachment add name=${item.name} mime=${item.mime} blocked=unsupported-model" } notify(KiloBundle.message("prompt.attachment.unsupported.model")) return @@ -813,7 +813,15 @@ class PromptPanel( val items = PromptAttachmentExtractor.files(list) + listOfNotNull(image) val ms = elapsedMs(start) LOG.debug { "kind=$kind extract area=$area files=${list.size} image=${image != null} attachments=${items.size} extractMs=$ms sourceMs=$sourceMs" } - if (items.isEmpty()) return@executeOnPooledThread + if (items.isEmpty()) { + if (list.isNotEmpty()) { + ApplicationManager.getApplication().invokeLater { + if (project.isDisposed) return@invokeLater + notify(KiloBundle.message("prompt.attachment.drop.empty")) + } + } + return@executeOnPooledThread + } ApplicationManager.getApplication().invokeLater { if (project.isDisposed) return@invokeLater LOG.debug { "kind=$kind attach area=$area files=${list.size} image=${image != null} attachments=${items.size} extractMs=$ms sourceMs=$sourceMs" } @@ -827,7 +835,9 @@ class PromptPanel( private fun dropFiles(event: DnDEvent): List { if (!FileCopyPasteUtil.isFileListFlavorAvailable(event)) return emptyList() - return FileCopyPasteUtil.getFileListFromAttachedObject(event.attachedObject).orEmpty() + val files = FileCopyPasteUtil.getFileListFromAttachedObject(event.attachedObject) + if (files.isNotEmpty()) return files + return FileCopyPasteUtil.getFileList(event).orEmpty() } private fun elapsedMs(start: Long) = (System.nanoTime() - start) / 1_000_000 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 56e2bbb3ef6..faf0e7b7828 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -214,7 +214,8 @@ prompt.attachment.remove=Remove {0} prompt.attachment.open=Open {0} prompt.attachment.tooltip=Name: {0}\nType: {1}\nLocation: {2} prompt.attachment.embedded=Embedded content -prompt.attachment.unsupported.model=The selected model does not support image or PDF attachments. +prompt.attachment.unsupported.model=The selected model does not support image attachments. +prompt.attachment.drop.empty=No files could be added. prompt.attachment.missing=Attachment no longer exists: {0} prompt.attachment.send.failed=Failed to send attachment: {0} session.attachment.title=Attachment diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/PromptAttachmentExtractorTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/PromptAttachmentExtractorTest.kt new file mode 100644 index 00000000000..8b476859313 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/PromptAttachmentExtractorTest.kt @@ -0,0 +1,83 @@ +package ai.kilocode.client.session.model + +import junit.framework.TestCase +import java.io.File +import kotlin.io.path.createTempDirectory + +class PromptAttachmentExtractorTest : TestCase() { + + fun `test code file becomes reference attachment`() { + val file = File.createTempFile("kilo-drop", ".php") + file.writeText(" sent = files.single() }, {}, { _, _ -> }) val file = File.createTempFile("kilo-paste", ".txt") @@ -990,8 +991,7 @@ class PromptPanelTest : BasePlatformTestCase() { val item = sent!! assertEquals("text/plain", item.mime) - assertTrue(item.url.orEmpty().startsWith("data:text/plain;base64,")) - assertFalse(item.url.orEmpty().startsWith("file://")) + assertEquals(file.toPath().toUri().toString(), item.url) } fun `test raw image paste adds attachment`() { @@ -1038,6 +1038,18 @@ class PromptPanelTest : BasePlatformTestCase() { assertEquals(0, panel.attachmentCountForTest()) } + fun `test disabled media model allows file reference attachment`() { + val panel = PromptPanel(project, { _, _ -> }, {}, { _, _ -> }) + val file = File.createTempFile("kilo-paste", ".php") + file.writeText(" }, onAbort = {}, onEnhance = { _, _ -> })