Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/diagram-viewer-window-jetbrains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---

Click a diagram in chat to open it in a resizable viewer window with zoom controls, trackpad pinch zoom, drag to pan, double click to fit and scrollbars. The diagram editor tab uses the same viewer. Copying a rendered diagram, from the viewer or from chat, now puts the picture on the clipboard.
5 changes: 5 additions & 0 deletions .changeset/mermaid-diagrams-jetbrains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": minor
---

Render Mermaid code fences as inline diagrams in JetBrains chat markdown, and open any diagram in its own editor tab with Diagram and Source views.
1 change: 1 addition & 0 deletions packages/kilo-jetbrains/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ For the full release process (resolve version, pin verification, prepare, change
- **Via Turbo**: `bun turbo build --filter=@kilocode/kilo-jetbrains` from repo root.
- **Run split mode**: `./gradlew --no-configuration-cache runIdeSplitMode` or the checked-in `Run IDE (Split Mode)` configuration — launches backend and frontend locally. Emulate latency via the Split Mode widget (requires internal mode: `-Didea.is.internal=true`).
- **Run split backend**: `./gradlew --no-configuration-cache runIdeBackend` — if it exits shortly after startup, check for an orphaned Java process from a previous backend run and kill it before restarting.
- **Corrupt IDE extraction**: if `runIdeBackend` or `runIdeSplitMode` fails before startup with `coroutinesJavaAgentFile` / `Collection contains no element matching the predicate`, the extracted IDE under `.intellijPlatform/ides/` is likely incomplete. Health check: `ls .intellijPlatform/ides/*/lib/*.jar | wc -l` should be in the hundreds. Repair by removing `.intellijPlatform/ides`, `.intellijPlatform/localPlatformArtifacts`, `.intellijPlatform/layoutIndex`, and `.intellijPlatform/coroutines-javaagent.jar`, then rerun the Gradle task.
- **Run in monolithic sandbox**: `./gradlew runIde` — launches sandboxed IntelliJ with the plugin. Does not build or bundle CLI binaries; the backend downloads the pinned release at connect time.

### CLI/SDK Change Awareness
Expand Down
23 changes: 23 additions & 0 deletions packages/kilo-jetbrains/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,29 @@ val worktreeRoot = providers.gradleProperty("kilo.dev.worktree.root").orElse(
providers.provider { rootProject.layout.projectDirectory.asFile.parentFile.parentFile.canonicalPath }
)

val ides = file(".intellijPlatform/ides")
val corrupt = ides.listFiles()
?.filter { ide ->
ide.isDirectory && (
ide.walkTopDown().none { it.name == "product-info.json" } ||
ide.resolve("lib").listFiles()?.any { jar -> jar.isFile && jar.extension == "jar" } != true
)
}
.orEmpty()

if (corrupt.isNotEmpty()) {
val paths = corrupt.joinToString("\n") { ide -> "- ${ide.absolutePath}" }
error(
"""
Incomplete IntelliJ Platform extraction detected:
$paths

Remove .intellijPlatform/ides, .intellijPlatform/localPlatformArtifacts, .intellijPlatform/layoutIndex,
and .intellijPlatform/coroutines-javaagent.jar, then rerun the Gradle task.
""".trimIndent(),
)
}

version = ver

plugins {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ai.kilocode.client.plugin
import ai.kilocode.KiloPlugin
import ai.kilocode.client.agentManager.worktree.unregisterWorktreeSessionEditorKind
import ai.kilocode.client.session.ui.attachment.unregisterAttachmentEditorKind
import ai.kilocode.client.ui.diagram.ui.DiagramWindows
import ai.kilocode.client.ui.diagram.ui.unregisterDiagramEditorKind
import ai.kilocode.client.vfs.KiloEditorKindRegistry
import ai.kilocode.client.vfs.KiloVirtualFileSystem
import ai.kilocode.log.KiloLog
Expand All @@ -29,6 +31,7 @@ object KiloFrontendUnloadCleanup {
runEdt {
ProjectManager.getInstance().openProjects.forEach { project ->
if (project.isDisposed) return@forEach
project.getServiceIfCreated(DiagramWindows::class.java)?.closeAll()
ToolWindowManager.getInstance(project).getToolWindow("Kilo Code")
?.contentManager
?.removeAllContents(true)
Expand All @@ -39,6 +42,7 @@ object KiloFrontendUnloadCleanup {
}
unregisterAttachmentEditorKind()
unregisterWorktreeSessionEditorKind()
unregisterDiagramEditorKind()
service<KiloEditorKindRegistry>().clear()
KiloVirtualFileSystem.getInstance().clear()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ internal object SessionSurface {
}

/** Runs [paint] with the graphics clipped to the rounded block, keeping opaque content rounded. */
inline fun clipped(g: Graphics, width: Int, height: Int, paint: (Graphics) -> Unit) {
inline fun <T> clipped(g: Graphics, width: Int, height: Int, paint: (Graphics) -> T): T {
val g2 = g.create() as Graphics2D
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val arc = arc().toFloat()
g2.clip(RoundRectangle2D.Float(0f, 0f, width.toFloat(), height.toFloat(), arc, arc))
paint(g2)
return paint(g2)
} finally {
g2.dispose()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ai.kilocode.client.session.ui.selection

import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.ui.ToolbarButtonAction
import ai.kilocode.client.ui.copyImage
import ai.kilocode.client.ui.toolbarButton
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.util.IconLoader
Expand All @@ -13,17 +14,20 @@ import java.awt.Point
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage
import javax.swing.Icon

internal class SessionCopyButton(
fill: Boolean = false,
tooltip: String = KiloBundle.message("session.copy.hover"),
icon: Icon = COPY_ICON,
private val image: () -> BufferedImage? = { null },
private val text: () -> String?,
) {
private var balloon: Balloon? = null
val button = toolbarButton(
ToolbarButtonAction(
COPY_ICON,
icon,
tooltip,
) { copy() },
fill,
Expand All @@ -45,8 +49,7 @@ internal class SessionCopyButton(

@RequiresEdt
fun copy() {
val value = text()?.takeIf { it.isNotEmpty() } ?: return
CopyPasteManager.getInstance().setContents(StringSelection(value))
if (!put()) return
dismiss()
balloon = JBPopupFactory.getInstance()
.createHtmlTextBalloonBuilder(KiloBundle.message("session.copy.copied"), null, null, null)
Expand All @@ -57,6 +60,24 @@ internal class SessionCopyButton(
}
}

/**
* Writes the clipboard and reports whether anything was put there.
*
* A picture wins over text, so a rendered diagram is pasted as an image while everything else (and
* a diagram that is still streaming or failed to render) keeps copying its text.
*/
@RequiresEdt
private fun put(): Boolean {
val picture = image()
if (picture != null) {
copyImage(picture)
return true
}
val value = text()?.takeIf { it.isNotEmpty() } ?: return false
CopyPasteManager.getInstance().setContents(StringSelection(value))
return true
}

companion object {
private val COPY_ICON: Icon = IconLoader.getIcon("/icons/copy.svg", SessionCopyButton::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ internal interface SessionCopyTarget {

val copyToolbar: JComponent? get() = null

val copyCorner: Boolean get() = false

@RequiresEdt
fun copyText(): String?
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ internal class SessionHoverCopyOverlay(
val gap = JBUI.scale(4)
val limit = limit(pane)
if (limit.isEmpty) return Rectangle()
if (item.copyToolbar != null) {
if (item.copyToolbar != null && !item.copyCorner) {
val pt = SwingUtilities.convertPoint(anchor, Point(visible.x, visible.y), pane)
// A zero-height anchor is an inline header placeholder (edit/modified open-diff): center
// the floating button on the header row so it lines up with the change badge. A real-height
// anchor is a footer row (message/text copy): keep the button bottom-aligned inside it.
// Targets that opt into corner placement fall through to the code-block positioning below.
val inline = anchor.preferredSize.height == 0
val offset = if (inline) (visible.height - size.height) / 2 else visible.height - size.height
val x = clamp(pt.x + visible.width - size.width, limit.x, limit.x + limit.width - size.width)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,12 @@ internal object SessionTargetResolver {
if (current is SessionCopyTarget && current.copyEligible) targets.add(current)
current = current.parent
}
val toolbar = targets.indexOfFirst { it.copyToolbar != null }
if (toolbar > 0) return targets.take(toolbar).firstOrNull { it.copyToolbar == null }
val own = targets.indexOfFirst { it.copyToolbar != null }
// The deepest target under the pointer wins when it brings its own toolbar (rendered
// diagram); a toolbar-owning ancestor instead yields to a plain inner target (code block),
// and plain targets anchor on the outermost one for a stable hover position.
if (own == 0) return targets.first()
if (own > 0) return targets.take(own).firstOrNull { it.copyToolbar == null }
return targets.lastOrNull()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ object SessionUiStyle {
fun topPadding(): Int = VIEWPORT_TOP_PADDING + UiStyle.Gap.lg()
}

object Diagram {
const val MAX_HEIGHT = 480
const val PADDING = 16
const val EMPTY_HEIGHT = 96
}

/** Permission session-view command preview limits. */
object Permission {
const val COMMAND_LINES = 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ import ai.kilocode.client.plugin.KiloBundle
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.image.BufferedImage
import javax.swing.JComponent
import javax.swing.JPanel

internal class MessageToolbar(
text: () -> String?,
image: () -> BufferedImage? = { null },
actions: List<ToolbarButtonAction> = emptyList(),
tooltip: String = KiloBundle.message("session.copy.hover"),
) : JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)) {
constructor(text: () -> String?, revert: (() -> Unit)?) : this(
text,
revert?.let {
actions = revert?.let {
listOf(ToolbarButtonAction(AllIcons.Actions.Rollback, KiloBundle.message("revert.message.rollback"), it))
}.orEmpty(),
KiloBundle.message("session.copy.prompt"),
tooltip = KiloBundle.message("session.copy.prompt"),
)

private val copy = SessionCopyButton(text = text, tooltip = tooltip)
private val copy = SessionCopyButton(text = text, image = image, tooltip = tooltip)
private val button = copy.button
private val buttons = actions.map(::toolbarButton)
private val row = Stack.horizontal(UiStyle.Gap.xs()).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ai.kilocode.client.app.KiloAgentBehaviorService
import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.SettingsContentField
import ai.kilocode.client.settings.base.SettingsDraftPage
import ai.kilocode.client.settings.base.SettingsDraftState
import ai.kilocode.client.settings.base.SettingsListPanel
Expand All @@ -15,6 +14,7 @@ import ai.kilocode.client.settings.base.SettingsPathDialogHandle
import ai.kilocode.client.settings.base.settingsChoosePath
import ai.kilocode.client.settings.base.settingsContentScroll
import ai.kilocode.client.settings.base.settingsEditorFileType
import ai.kilocode.client.ui.CodeViewField
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import ai.kilocode.client.ui.list.ActiveListBadge
Expand Down Expand Up @@ -320,7 +320,7 @@ private fun saved(base: SkillsDraft, draft: SkillsDraft): Boolean = base == draf

internal class SkillEditDialog(private val skill: SkillDto, private val savable: Boolean) : DialogWrapper(true), SkillEditDialogHandle {
private val base = initial()
private val editor = SettingsContentField(base, skillFileType(skill.location, base), savable)
private val editor = CodeViewField(base, skillFileType(skill.location, base), savable)

init {
title = skill.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import ai.kilocode.client.KiloNotifications
import ai.kilocode.client.app.KiloAgentBehaviorService
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.SettingsContentField
import ai.kilocode.client.settings.base.SettingsDraftPage
import ai.kilocode.client.settings.base.SettingsDraftState
import ai.kilocode.client.settings.base.SettingsListPanel
import ai.kilocode.client.settings.base.SettingsMessageException
import ai.kilocode.client.settings.base.settingsContentScroll
import ai.kilocode.client.settings.base.settingsEditorFileType
import ai.kilocode.client.ui.CodeViewField
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.list.ActiveListBadge
import ai.kilocode.client.ui.list.ActiveListCell
Expand Down Expand Up @@ -283,7 +283,7 @@ private fun saved(base: WorkflowsDraft, draft: WorkflowsDraft): Boolean = base =

internal class WorkflowEditDialog(private val flow: CommandFileDto, private val savable: Boolean) : DialogWrapper(true), WorkflowEditDialogHandle {
private val base = initial()
private val editor = SettingsContentField(base, workflowFileType(flow.location, base), savable)
private val editor = CodeViewField(base, workflowFileType(flow.location, base), savable)

init {
title = "/${flow.name}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,14 @@
package ai.kilocode.client.settings.base

import ai.kilocode.client.session.ui.style.SessionUiStyle
import com.intellij.openapi.editor.EditorFactory
import ai.kilocode.client.ui.CodeViewField
import ai.kilocode.client.ui.codeViewScroll
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.fileTypes.UnknownFileType
import com.intellij.openapi.project.ProjectManager
import com.intellij.ui.EditorTextField
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.JBUI
import javax.swing.ScrollPaneConstants

/**
* Shared code-editor primitives for settings dialogs (skill content, instruction files).
*
* Keeps the tuned [EditorTextField] configuration, scroll chrome, and content-aware file-type
* detection in one place so pages don't each hand-roll their own editor.
*/
internal class SettingsContentField(
content: String,
fileType: FileType,
editable: Boolean,
) : EditorTextField(
EditorFactory.getInstance().createDocument(content),
ProjectManager.getInstance().defaultProject,
fileType,
!editable,
false,
) {
init {
border = JBUI.Borders.empty()
setOneLineMode(false)
addSettingsProvider { ed ->
ed.setBorder(JBUI.Borders.empty())
ed.scrollPane.border = JBUI.Borders.empty()
ed.scrollPane.viewportBorder = JBUI.Borders.empty()
ed.settings.isUseSoftWraps = true
ed.settings.isPaintSoftWraps = false
ed.settings.isAdditionalPageAtBottom = false
ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
}
}
}

internal fun settingsContentScroll(field: SettingsContentField) = JBScrollPane(field).apply {
viewportBorder = JBUI.Borders.empty(
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING),
JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING),
)
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
internal fun settingsContentScroll(field: CodeViewField) = codeViewScroll(field).apply {
preferredSize = JBUI.size(720, 520)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ai.kilocode.client.app.KiloAgentBehaviorService
import ai.kilocode.client.app.KiloAppService
import ai.kilocode.client.app.KiloWorkspaceService
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.settings.base.SettingsContentField
import ai.kilocode.client.settings.base.SettingsDraftPage
import ai.kilocode.client.settings.base.SettingsDraftState
import ai.kilocode.client.settings.base.SettingsListPanel
Expand All @@ -16,6 +15,7 @@ import ai.kilocode.client.settings.base.SettingsToolbarAction
import ai.kilocode.client.settings.base.settingsChoosePath
import ai.kilocode.client.settings.base.settingsContentScroll
import ai.kilocode.client.settings.base.settingsEditorFileType
import ai.kilocode.client.ui.CodeViewField
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.layout.Stack
import ai.kilocode.client.ui.layout.StackAxis
Expand Down Expand Up @@ -294,7 +294,7 @@ internal class InstructionEditDialog(
content: String,
) : DialogWrapper(true), RuleContentDialogHandle {
private val base = content
private val field = SettingsContentField(base, settingsEditorFileType(heading, base), true)
private val field = CodeViewField(base, settingsEditorFileType(heading, base), true)

init {
title = heading
Expand Down Expand Up @@ -363,4 +363,3 @@ private fun writeInstruction(root: String?, path: String, text: String): Boolean
}
return ok
}

Loading
Loading