mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #12869 from Kilo-Org/glitter-organization
fix(jetbrains): add dropped files as references
This commit is contained in:
@@ -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.
|
||||
+33
-13
@@ -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) {
|
||||
|
||||
+13
-3
@@ -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
|
||||
|
||||
+83
@@ -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())
|
||||
}
|
||||
}
|
||||
+17
-5
@@ -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 = { _, _ -> })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user