Merge pull request #12869 from Kilo-Org/glitter-organization

fix(jetbrains): add dropped files as references
This commit is contained in:
Kirill Kalishev
2026-08-04 12:33:56 -04:00
committed by GitHub
6 changed files with 153 additions and 22 deletions
@@ -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.
@@ -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<java.io.File>): List<PromptAttachment> = 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) {
@@ -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<java.io.File> {
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
@@ -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
@@ -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("<?php echo 'hello';")
val item = PromptAttachmentExtractor.files(listOf(file)).single()
val part = item.part()
assertTrue(item.reference)
assertEquals("text/plain", item.mime)
assertEquals(file.toPath().toUri().toString(), item.url)
assertEquals(file.toPath(), item.path)
assertEquals(file.name, item.name)
assertEquals("file", part.type)
assertEquals("text/plain", part.mime)
assertEquals(file.toPath().toUri().toString(), part.url)
assertFalse(part.url.orEmpty().startsWith("data:"))
}
fun `test text file becomes reference attachment`() {
val file = File.createTempFile("kilo-drop", ".txt")
file.writeText("hello")
val item = PromptAttachmentExtractor.files(listOf(file)).single()
assertTrue(item.reference)
assertEquals("text/plain", item.mime)
assertEquals(file.toPath().toUri().toString(), item.part().url)
}
fun `test directory becomes reference attachment`() {
val dir = createTempDirectory(prefix = "kilo-drop").toFile()
val item = PromptAttachmentExtractor.files(listOf(dir)).single()
val part = item.part()
assertTrue(item.reference)
assertEquals("application/x-directory", item.mime)
assertEquals(dir.toPath().toUri().toString(), item.url)
assertEquals(dir.toPath(), item.path)
assertEquals(dir.name, item.name)
assertEquals("file", part.type)
assertEquals("application/x-directory", part.mime)
assertEquals(dir.toPath().toUri().toString(), part.url)
}
fun `test image file remains embedded attachment`() {
val file = File.createTempFile("kilo-drop", ".png")
file.writeBytes(byteArrayOf(1, 2, 3))
val item = PromptAttachmentExtractor.files(listOf(file)).single()
assertFalse(item.reference)
assertEquals("image/png", item.mime)
assertEquals(file.toPath().toUri().toString(), item.url)
assertTrue(item.part().url.orEmpty().startsWith("data:image/png;base64,"))
}
fun `test oversized image is skipped but oversized code is referenced`() {
val image = File.createTempFile("kilo-drop", ".png")
image.writeBytes(ByteArray(10 * 1024 * 1024 + 1))
val code = File.createTempFile("kilo-drop", ".php")
code.writeBytes(ByteArray(10 * 1024 * 1024 + 1))
val items = PromptAttachmentExtractor.files(listOf(image, code))
assertEquals(listOf(code.name), items.map { it.name })
assertTrue(items.single().reference)
}
fun `test nonexistent file is ignored`() {
val file = File(File(System.getProperty("java.io.tmpdir")), "kilo-missing-${System.nanoTime()}.php")
assertTrue(PromptAttachmentExtractor.files(listOf(file)).isEmpty())
}
}
@@ -966,17 +966,18 @@ class PromptPanelTest : BasePlatformTestCase() {
assertEquals(1, panel.attachmentCountForTest())
}
fun `test frontend file attachment defers data url encoding until send`() {
fun `test frontend text file attachment stays file reference`() {
val file = File.createTempFile("kilo-paste", ".txt")
file.writeText("hello")
val item = ai.kilocode.client.session.model.PromptAttachmentExtractor.files(listOf(file)).single()
assertTrue(item.reference)
assertTrue(item.url.startsWith("file://"))
assertTrue(item.part().url.orEmpty().startsWith("data:text/plain;base64,"))
assertEquals(item.url, item.part().url)
}
fun `test pasted frontend file sends data url payload`() {
fun `test pasted frontend file sends reference payload`() {
var sent: ai.kilocode.rpc.dto.PromptPartDto? = null
val panel = PromptPanel(project, { _, files -> 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("<?php echo 'hello';")
val item = ai.kilocode.client.session.model.PromptAttachmentExtractor.files(listOf(file)).single()
panel.setAttachmentEnabled(false)
panel.addAttachmentForTest(item)
assertEquals(1, panel.attachmentCountForTest())
}
fun `test prompt button switches between send and stop state`() {
val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> })