Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions .changeset/jetbrains-auto-editor-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": minor
---

Include the active editor file, open files, visible files, and selected text in JetBrains chat context by default, with a Context settings toggle to disable it. Files matched by `.kilocodeignore` (or `.gitignore` plus `.env` files) are excluded, and the default shell is reported to the agent.
5 changes: 5 additions & 0 deletions .changeset/jetbrains-prompt-attachments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---

Render prompt attachments inside the sent message bubble with file chips, image previews, and selection-aware file opening.
2 changes: 1 addition & 1 deletion packages/kilo-jetbrains/.run/Run IDE (Backend).run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--no-configuration-cache --purge-old-log-directories -Pkilo.dev.log.level=debug -Pkilo.splitModeServerPort=0 -Pkilo.dev.storage.isolated=true" />
<option name="scriptParameters" value="--no-configuration-cache --purge-old-log-directories -Pkilo.dev.log.level=debug -Pkilo.dev.log.chat.content=off -Pkilo.dev.log.chat.preview.max=160 -Pkilo.splitModeServerPort=0 -Pkilo.dev.storage.isolated=true" />
<option name="taskDescriptions">
<list />
</option>
Expand Down
2 changes: 1 addition & 1 deletion packages/kilo-jetbrains/.run/Run IDE (Frontend).run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--no-configuration-cache --purge-old-log-directories -Pkilo.dev.log.level=debug -Pkilo.splitModeServerPort=0 -Pkilo.dev.storage.isolated=true" />
<option name="scriptParameters" value="--no-configuration-cache --purge-old-log-directories -Pkilo.dev.log.level=debug -Pkilo.dev.log.chat.content=off -Pkilo.dev.log.chat.preview.max=160 -Pkilo.splitModeServerPort=0 -Pkilo.dev.storage.isolated=true" />
<option name="taskDescriptions">
<list />
</option>
Expand Down
2 changes: 1 addition & 1 deletion packages/kilo-jetbrains/.run/runIdeSplitMode.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--no-configuration-cache --purge-old-log-directories -Pkilo.dev.log.level=debug -Pkilo.splitModeServerPort=0 -Pkilo.dev.storage.isolated=true" />
<option name="scriptParameters" value="--no-configuration-cache --purge-old-log-directories -Pkilo.dev.log.level=debug -Pkilo.dev.log.chat.content=off -Pkilo.dev.log.chat.preview.max=160 -Pkilo.splitModeServerPort=0 -Pkilo.dev.storage.isolated=true" />
<option name="taskDescriptions">
<list />
</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import ai.kilocode.rpc.dto.CustomModelDto
import ai.kilocode.rpc.dto.CustomProviderConfigDto
import ai.kilocode.rpc.dto.CustomProviderSaveDto
import ai.kilocode.rpc.dto.DiffFileDto
import ai.kilocode.rpc.dto.EditorContextDto
import ai.kilocode.rpc.dto.MessageDto
import ai.kilocode.rpc.dto.MessageErrorDto
import ai.kilocode.rpc.dto.MessageSummaryDto
Expand Down Expand Up @@ -822,10 +823,27 @@ object KiloCliDataParser {
if (variant != null) {
sb.append(""","variant":${escape(variant)}""")
}
val editor = prompt.editorContext
if (editor != null) {
sb.append(""","editorContext":${editorContextJson(editor)}""")
}
sb.append("}")
return sb.toString()
}

private fun editorContextJson(ctx: EditorContextDto): String {
val fields = mutableListOf<String>()
ctx.directory?.let { fields += "\"directory\":${escape(it)}" }
ctx.worktree?.let { fields += "\"worktree\":${escape(it)}" }
ctx.visibleFiles?.takeIf { it.isNotEmpty() }?.let { fields += "\"visibleFiles\":${array(it)}" }
ctx.openTabs?.takeIf { it.isNotEmpty() }?.let { fields += "\"openTabs\":${array(it)}" }
ctx.activeFile?.let { fields += "\"activeFile\":${escape(it)}" }
ctx.shell?.let { fields += "\"shell\":${escape(it)}" }
return "{${fields.joinToString(",")}}"
}

private fun array(values: List<String>): String = values.joinToString(",", "[", "]") { escape(it) }

private fun buildPromptPartJson(part: PromptPartDto): String {
val fields = mutableListOf("\"type\":${escape(part.type)}")
if (part.type == "file") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
Expand Down Expand Up @@ -288,15 +290,15 @@ class KiloWorkspaceRpcApiImpl internal constructor(
}.ifBlank { null }
}

override suspend fun openFile(path: String, line: Int?, column: Int?): Boolean {
override suspend fun openFile(path: String, line: Int?, column: Int?, endLine: Int?): Boolean {
val item = clean(path) ?: return false
val target = file(item)?.takeIf { it.isAbsolute } ?: return false
val vf = LocalFileSystem.getInstance().refreshAndFindFileByPath(target.toString()) ?: return false
val project = project(target) ?: run {
LOG.warn("No project available to open file: $path")
return false
}
navigate(project, vf, line, column)
navigate(project, vf, line, column, endLine)
return true
}

Expand Down Expand Up @@ -374,8 +376,26 @@ class KiloWorkspaceRpcApiImpl internal constructor(
null
}

private suspend fun navigate(project: Project, file: VirtualFile, line: Int? = null, column: Int? = null) = suspendCancellableCoroutine { cont ->
private suspend fun navigate(project: Project, file: VirtualFile, line: Int? = null, column: Int? = null, endLine: Int? = null) = suspendCancellableCoroutine { cont ->
ApplicationManager.getApplication().invokeLater({
if (line != null && endLine != null) {
val editor = FileEditorManager.getInstance(project).openTextEditor(
OpenFileDescriptor(project, file, (line - 1).coerceAtLeast(0), 0),
true,
)
val doc = editor?.document
if (editor != null && doc != null && doc.lineCount > 0) {
val start = (line - 1).coerceIn(0, doc.lineCount - 1)
val end = (endLine - 1).coerceIn(start, doc.lineCount - 1)
val from = doc.getLineStartOffset(start)
val to = doc.getLineEndOffset(end)
editor.selectionModel.setSelection(from, to)
editor.caretModel.moveToOffset(from)
editor.scrollingModel.scrollToCaret(ScrollType.CENTER)
}
if (cont.isActive) cont.resume(Unit)
return@invokeLater
}
val descriptor = if (line == null) {
OpenFileDescriptor(project, file)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ai.kilocode.backend.cli

import ai.kilocode.log.ChatLogSummary
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.EditorContextDto
import ai.kilocode.rpc.dto.MessageDto
import ai.kilocode.rpc.dto.MessageErrorDto
import ai.kilocode.rpc.dto.MessageTimeDto
Expand Down Expand Up @@ -156,6 +157,27 @@ class ChatLogSummaryTest {
assertTrue(out.contains("variant=medium"), out)
}

@Test
fun `prompt dto summary includes editor context`() {
System.setProperty("kilo.dev.log.chat.content", "preview")

val out = ChatLogSummary.prompt(
PromptDto(
parts = listOf(PromptPartDto(type = "text", text = "hello")),
editorContext = EditorContextDto(
activeFile = "settings.gradle",
openTabs = listOf("settings.gradle", "src/App.kt"),
visibleFiles = listOf("settings.gradle"),
),
)
)

assertTrue(out.contains("editorContext=true"), out)
assertTrue(out.contains("activeFile=\"settings.gradle\""), out)
assertTrue(out.contains("openTabs=2"), out)
assertTrue(out.contains("visibleFiles=1"), out)
}

@Test
fun `prompt dto summary redacts file attachment urls`() {
System.setProperty("kilo.dev.log.chat.content", "preview")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ai.kilocode.rpc.dto.CompactionPatchDto
import ai.kilocode.rpc.dto.ConfigDto
import ai.kilocode.rpc.dto.ConfigPatchDto
import ai.kilocode.rpc.dto.ConfigUpdateDto
import ai.kilocode.rpc.dto.EditorContextDto
import ai.kilocode.rpc.dto.McpConfigDto
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
import ai.kilocode.rpc.dto.PermissionReplyDto
Expand Down Expand Up @@ -1958,6 +1959,25 @@ class KiloCliDataParserTest {
assertEquals("""{"parts":[{"type":"text","text":"Hi"}],"noReply":true}""", result)
}

@Test
fun `buildPromptJson - with editor context`() {
val prompt = PromptDto(
parts = listOf(PromptPartDto("text", "Hi")),
editorContext = EditorContextDto(
activeFile = "src/App.kt",
visibleFiles = listOf("src/App.kt"),
openTabs = listOf("src/App.kt", "src/Other.kt"),
),
)

val result = KiloCliDataParser.buildPromptJson(prompt)

assertEquals(
"""{"parts":[{"type":"text","text":"Hi"}],"editorContext":{"visibleFiles":["src/App.kt"],"openTabs":["src/App.kt","src/Other.kt"],"activeFile":"src/App.kt"}}""",
result,
)
}

@Test
fun `buildProviderOAuthJson - numeric method index`() {
val result = KiloCliDataParser.buildProviderOAuthJson("0", mapOf("deploymentType" to "github.com"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,19 @@ class KiloWorkspaceService internal constructor(
}
}

suspend fun openPath(directory: String, path: String, line: Int? = null, column: Int? = null): Boolean {
suspend fun openPath(directory: String, path: String, line: Int? = null, column: Int? = null, endLine: Int? = null): Boolean {
val match = files(directory, path).firstOrNull() ?: return false
return try {
call { openFile(match.path, line, column) }
call { openFile(match.path, line, column, endLine) }
} catch (e: Exception) {
LOG.warn("workspace file open failed for path=${match.path}", e)
false
}
}

suspend fun openFile(path: String, line: Int? = null, column: Int? = null): Boolean {
suspend fun openFile(path: String, line: Int? = null, column: Int? = null, endLine: Int? = null): Boolean {
return try {
call { openFile(path, line, column) }
call { openFile(path, line, column, endLine) }
} catch (e: Exception) {
LOG.warn("workspace file open failed for path=$path", e)
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.intellij.ide.util.PropertiesComponent

object KiloPluginSettings {
private const val AUTO_APPROVE_KEY = "kilo.session.autoApprove"
private const val AUTO_EDITOR_CONTEXT_KEY = "kilo.session.autoEditorContext"
private const val PERMISSION_RULES_EXPANDED_KEY = "kilo.session.permissionRulesExpanded"

fun getAutoApprove(): Boolean = PropertiesComponent.getInstance().getBoolean(AUTO_APPROVE_KEY, false)
Expand All @@ -16,6 +17,16 @@ object KiloPluginSettings {
PropertiesComponent.getInstance().unsetValue(AUTO_APPROVE_KEY)
}

fun getAutoEditorContext(): Boolean = PropertiesComponent.getInstance().getBoolean(AUTO_EDITOR_CONTEXT_KEY, true)

fun setAutoEditorContext(value: Boolean) {
PropertiesComponent.getInstance().setValue(AUTO_EDITOR_CONTEXT_KEY, value.toString())
}

internal fun unsetAutoEditorContext() {
PropertiesComponent.getInstance().unsetValue(AUTO_EDITOR_CONTEXT_KEY)
}

fun getPermissionRulesExpanded(): Boolean = PropertiesComponent.getInstance().getBoolean(PERMISSION_RULES_EXPANDED_KEY, false)

fun setPermissionRulesExpanded(value: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class SessionFileLinks(
}
val target = parse(href)
scope.launch {
val ok = service.openPath(dir, target.path, target.line, target.column)
val ok = service.openPath(dir, target.path, target.line, target.column, target.endLine)
if (ok) {
track(target, "direct")
return@launch
Expand All @@ -72,7 +72,7 @@ class SessionFileLinks(
when (val result = decide(false, found)) {
Resolution.Opened -> Unit
is Resolution.OpenDirect -> {
val opened = service.openPath(dir, result.file.path, target.line, target.column)
val opened = service.openPath(dir, result.file.path, target.line, target.column, target.endLine)
track(target, if (opened) "search_direct" else "missing")
}
is Resolution.Choose -> {
Expand Down Expand Up @@ -104,7 +104,7 @@ class SessionFileLinks(
.createPopupChooserBuilder(files)
.setRenderer(FileRenderer())
.setItemChosenCallback { file ->
scope.launch { service.openPath(dir, file.path, target.line, target.column) }
scope.launch { service.openPath(dir, file.path, target.line, target.column, target.endLine) }
}
.createPopup()
popup.show(anchor ?: RelativePoint.getCenterOf(root))
Expand Down Expand Up @@ -143,18 +143,19 @@ class SessionFileLinks(
data object Missing : Resolution
}

data class Target(val path: String, val line: Int? = null, val column: Int? = null)
data class Target(val path: String, val line: Int? = null, val column: Int? = null, val endLine: Int? = null)

companion object {
private const val FILE_SEARCH_LIMIT = 50
private val LINE = Regex(":(\\d+)(?:-\\d+)?(?::(\\d+))?$")
private val LINE = Regex(":(\\d+)(?:-(\\d+))?(?::(\\d+))?$")
private val SCHEME = Regex("^([A-Za-z][A-Za-z0-9+.-]*):")

fun parse(href: String): Target {
val match = LINE.find(href) ?: return Target(href)
return Target(
href.substring(0, match.range.first),
match.groupValues[1].toIntOrNull(),
match.groupValues.getOrNull(3)?.takeIf { it.isNotBlank() }?.toIntOrNull(),
match.groupValues.getOrNull(2)?.takeIf { it.isNotBlank() }?.toIntOrNull(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.controller.EVENT_FLUSH_MS
import ai.kilocode.client.session.controller.SessionController
import ai.kilocode.client.session.controller.SessionControllerEvent
import ai.kilocode.client.session.context.EditorContextGatherer
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.views.LoginRequiredView
import ai.kilocode.client.session.views.permission.PermissionView
Expand Down Expand Up @@ -682,15 +683,6 @@ class SessionUi(

private fun sendPrompt(text: String, files: List<PromptPartDto>) {
if (text.isBlank() && files.isEmpty()) return
val parts = buildList {
text.takeIf { it.isNotBlank() }?.let { add(PromptPartDto(type = "text", text = it)) }
addAll(files)
}
LOG.debug {
val agent = controller.model.agent ?: "none"
val model = controller.model.model ?: "none"
"${ChatLogSummary.prompt(PromptDto(parts = parts))} agent=$agent model=$model ready=${controller.ready}"
}
prompt.clear()
val follow = scroll.atBottom()
val action = completion.clientAction(text)
Expand All @@ -705,7 +697,20 @@ class SessionUi(
scroll.followBottom(follow)
return
}
controller.prompt(text, files)
// Only the prompt path uses editor context; gather after the command branches so slash
// commands and client actions don't pay the editor-context cost or hit its failure modes.
val editor = EditorContextGatherer.gather(project, workspace.directory)
val allFiles = files + listOfNotNull(editor.selection)
LOG.debug {
val parts = buildList {
text.takeIf { it.isNotBlank() }?.let { add(PromptPartDto(type = "text", text = it)) }
addAll(allFiles)
}
val agent = controller.model.agent ?: "none"
val model = controller.model.model ?: "none"
"${ChatLogSummary.prompt(PromptDto(parts = parts, editorContext = editor.context))} agent=$agent model=$model ready=${controller.ready}"
}
controller.prompt(text, allFiles, editor.context)
scroll.followBottom(follow)
}

Expand Down Expand Up @@ -918,12 +923,13 @@ class SessionUi(
return
}
if (uri.scheme == "file") {
val path = runCatching { Path.of(uri).toString() }.getOrNull() ?: run {
val path = runCatching { Path.of(cleanAttachmentUri(uri)).toString() }.getOrNull() ?: run {
LOG.info("kind=attachment-open skipped=true reason=invalid-file-uri message=$messageId part=${item.id} url=${attachmentUrl(url)}")
return
}
LOG.info("kind=attachment-open route=file session=${controller.id ?: "none"} message=$messageId part=${item.id} path=$path")
fileLinks.open(path, null)
val target = attachmentHref(path, item)
LOG.info("kind=attachment-open route=file session=${controller.id ?: "none"} message=$messageId part=${item.id} path=$target")
fileLinks.open(target, null)
return
}
LOG.info("kind=attachment-open route=browser session=${controller.id ?: "none"} message=$messageId part=${item.id} url=${attachmentUrl(url)}")
Expand All @@ -934,6 +940,14 @@ class SessionUi(
?: item.url.substringBefore(',').substringAfterLast('/').takeIf { it.isNotBlank() }
?: "attachment"

private fun attachmentHref(path: String, item: FileAttachment): String {
val start = item.startLine ?: return path
val end = item.endLine ?: start
return "$path:$start-$end"
}

private fun cleanAttachmentUri(uri: URI): URI = URI(uri.scheme, uri.authority, uri.path, null, null)

private fun attachmentUrl(url: String): String {
val scheme = url.substringBefore(':', missingDelimiterValue = "none")
return "scheme=$scheme chars=${url.length} embedded=${isEmbeddedAttachment(url)}"
Expand Down
Loading
Loading