mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
fix(jetbrains): use semantic session timeline colors
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains session timeline colors to follow semantic theme keys.
|
||||
+6
-6
@@ -121,13 +121,13 @@ object SessionUiStyle {
|
||||
|
||||
/** Colors for timeline/activity indicators in the session header. */
|
||||
object Timeline {
|
||||
val READ: Color = JBColor(Color(0x37, 0x94, 0xff), Color(0x37, 0x94, 0xff))
|
||||
val WRITE: Color = JBColor(Color(0x00, 0x7f, 0xd4), Color(0x00, 0x7f, 0xd4))
|
||||
val TOOL: Color = JBColor(Color(0x00, 0x7a, 0xcc), Color(0x00, 0x7a, 0xcc))
|
||||
val READ: Color = JBColor.namedColor("Kilo.Session.Timeline.Read", Color(0x37, 0x94, 0xff))
|
||||
val WRITE: Color = JBColor.namedColor("Kilo.Session.Timeline.Write", Color(0x00, 0x7f, 0xd4))
|
||||
val TOOL: Color = JBColor.namedColor("Kilo.Session.Timeline.Tool", Color(0x00, 0x7a, 0xcc))
|
||||
val SUCCESS: Color = JBColor.namedColor("Label.successForeground", UIUtil.getLabelSuccessForeground())
|
||||
val ERROR: Color = JBColor(Color(0xf4, 0x87, 0x71), Color(0xf4, 0x87, 0x71))
|
||||
val TEXT: Color = JBColor(Color(0x9d, 0x9d, 0x9d), Color(0x9d, 0x9d, 0x9d))
|
||||
val STEP: Color = JBColor(Color(0x4d, 0x4d, 0x4d), Color(0x4d, 0x4d, 0x4d))
|
||||
val ERROR: Color = JBColor.namedColor("Kilo.Session.Timeline.Error", UIUtil.getErrorForeground())
|
||||
val TEXT: Color = JBColor.namedColor("Kilo.Session.Timeline.Text", UIUtil.getContextHelpForeground())
|
||||
val STEP: Color = JBColor.namedColor("Kilo.Session.Timeline.Step", JBColor.border())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
@@ -23,6 +23,8 @@ import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStateDto
|
||||
import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
|
||||
import ai.kilocode.rpc.dto.MessageDto
|
||||
import ai.kilocode.rpc.dto.MessageTimeDto
|
||||
import ai.kilocode.rpc.dto.QuestionInfoDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
@@ -36,6 +38,7 @@ import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
@@ -238,6 +241,29 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf("/test" to "ses_1", "/test" to "ses_2", "/test" to "ses_1"), created)
|
||||
}
|
||||
|
||||
fun `test queued hidden transcript events are discarded after timeout disposal`() {
|
||||
useShortInactiveDisposeTimeout()
|
||||
val flow = MutableSharedFlow<ChatEventDto>(extraBufferCapacity = 16)
|
||||
rpc.eventFlow = { _, _ -> flow }
|
||||
val manager = manager()
|
||||
|
||||
manager.openSession(session("ses_1"))
|
||||
val first = active(manager)
|
||||
settle()
|
||||
manager.openSession(session("ses_2"))
|
||||
kotlinx.coroutines.runBlocking {
|
||||
flow.emit(ChatEventDto.MessageUpdated("ses_1", msg("msg_hidden", "ses_1", "assistant")))
|
||||
flow.emit(ChatEventDto.PartDelta("ses_1", "msg_hidden", "txt_hidden", "text", "stale"))
|
||||
}
|
||||
settle()
|
||||
manager.openSession(session("ses_1"))
|
||||
val second = active(manager)
|
||||
settle()
|
||||
|
||||
assertNotSame(first, second)
|
||||
assertTrue(empty(second))
|
||||
}
|
||||
|
||||
fun `test pending session is retained for history overlays before timeout`() {
|
||||
useLongInactiveDisposeTimeout()
|
||||
val history = JLabel("History")
|
||||
@@ -623,6 +649,14 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
|
||||
private fun active(manager: SessionSidePanelManager) = manager.component.getComponent(0) as JPanel
|
||||
|
||||
private fun empty(panel: JPanel): Boolean {
|
||||
var empty = false
|
||||
com.intellij.openapi.application.ApplicationManager.getApplication().invokeAndWait {
|
||||
empty = panel.controller().model.isEmpty()
|
||||
}
|
||||
return empty
|
||||
}
|
||||
|
||||
private fun useShortInactiveDisposeTimeout() = setInactiveDisposeTimeout(10)
|
||||
|
||||
private fun useLongInactiveDisposeTimeout() = setInactiveDisposeTimeout(60_000)
|
||||
@@ -682,6 +716,13 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
time = SessionTimeDto(created = 1.0, updated = 2.0),
|
||||
)
|
||||
|
||||
private fun msg(id: String, session: String, role: String) = MessageDto(
|
||||
id = id,
|
||||
sessionID = session,
|
||||
role = role,
|
||||
time = MessageTimeDto(created = 1.0),
|
||||
)
|
||||
|
||||
private fun cloud(id: String) = CloudSessionDto(
|
||||
id = id,
|
||||
title = "Cloud $id",
|
||||
|
||||
+20
-2
@@ -16,10 +16,12 @@ import ai.kilocode.rpc.dto.TodoDto
|
||||
import ai.kilocode.rpc.dto.TodoViewDto
|
||||
import ai.kilocode.rpc.dto.TokensDto
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.UsefulTestCase
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
|
||||
class SessionModelTest : UsefulTestCase() {
|
||||
@Suppress("UnstableApiUsage")
|
||||
class SessionModelTest : BasePlatformTestCase() {
|
||||
|
||||
private lateinit var model: SessionModel
|
||||
private lateinit var parent: Disposable
|
||||
@@ -50,6 +52,15 @@ class SessionModelTest : UsefulTestCase() {
|
||||
assertEquals(SessionState.Idle, model.state)
|
||||
}
|
||||
|
||||
fun `test model mutation works through EDT`() {
|
||||
// The test fixture does not consistently throw for @RequiresEdt when called
|
||||
// from a pooled thread, so keep this as a behavioral EDT contract check.
|
||||
edt { model.addMessage(msg("on_edt", "assistant")) }
|
||||
|
||||
assertNotNull(edt { model.message("on_edt") })
|
||||
assertTrue(events.any { it is SessionModelEvent.MessageAdded && it.info.info.id == "on_edt" })
|
||||
}
|
||||
|
||||
fun `test isReady requires app and workspace readiness`() {
|
||||
model.app = KiloAppStateDto(KiloAppStatusDto.READY)
|
||||
assertFalse(model.isReady())
|
||||
@@ -883,4 +894,11 @@ class SessionModelTest : UsefulTestCase() {
|
||||
private fun assertModel(expected: String) {
|
||||
assertEquals(expected.trimIndent().trim(), model.toString().trim())
|
||||
}
|
||||
|
||||
private fun <T> edt(block: () -> T): T {
|
||||
var result: T? = null
|
||||
ApplicationManager.getApplication().invokeAndWait { result = block() }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result as T
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -6,6 +6,7 @@ import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.model.ToolKind
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.controller.SessionControllerTestBase
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.MessageDto
|
||||
@@ -21,6 +22,7 @@ import java.awt.Color
|
||||
import java.awt.Point
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseWheelEvent
|
||||
import javax.swing.UIManager
|
||||
|
||||
class SessionHeaderPanelTest : SessionControllerTestBase() {
|
||||
|
||||
@@ -120,7 +122,20 @@ class SessionHeaderPanelTest : SessionControllerTestBase() {
|
||||
emit(ChatEventDto.TodoUpdated("ses_test", listOf(TodoDto("Done", "completed", "high"))))
|
||||
|
||||
assertEquals("All 1 todos complete", panel.todoText())
|
||||
assertEquals(ai.kilocode.client.session.ui.style.SessionUiStyle.Timeline.SUCCESS, panel.foregrounds()[3])
|
||||
assertEquals(SessionUiStyle.Timeline.SUCCESS, panel.foregrounds()[3])
|
||||
}
|
||||
|
||||
fun `test timeline colors honor semantic named color keys`() {
|
||||
val old = UIManager.getColor("Kilo.Session.Timeline.Read")
|
||||
val color = Color(12, 34, 56)
|
||||
|
||||
try {
|
||||
UIManager.put("Kilo.Session.Timeline.Read", color)
|
||||
|
||||
assertEquals(color.rgb, SessionUiStyle.Timeline.READ.rgb)
|
||||
} finally {
|
||||
UIManager.put("Kilo.Session.Timeline.Read", old)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test retained labels update on later header event`() {
|
||||
|
||||
+12
@@ -706,6 +706,18 @@ class QuestionViewTest : BasePlatformTestCase() {
|
||||
assertNull("Empty custom editor should be removed after selecting a normal option", findAll<EditorTextField>(view).firstOrNull { it.parent != null })
|
||||
}
|
||||
|
||||
fun `test empty custom editor is detached after forcing underlying editor creation`() {
|
||||
view.show(customSingleQuestion("q_custom_editor_release"))
|
||||
findAll<JBRadioButton>(view).first { it.actionCommand == "" }.doClick()
|
||||
val field = findAll<EditorTextField>(view).first()
|
||||
val editor = field.getEditor(true)
|
||||
assertSame(editor, field.getEditor(false))
|
||||
|
||||
option<JBRadioButton>(view, "Minimal").doClick()
|
||||
|
||||
assertNull(SwingUtilities.getAncestorOfClass(QuestionView::class.java, field))
|
||||
}
|
||||
|
||||
fun `test focusing retained custom editor reselects custom response`() {
|
||||
view.show(customSingleQuestion("q_custom_focus"))
|
||||
|
||||
|
||||
+35
@@ -3,6 +3,8 @@ package ai.kilocode.client.session.views
|
||||
import ai.kilocode.client.session.model.Text
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.RepaintManager
|
||||
|
||||
/**
|
||||
* Tests for [TextView].
|
||||
@@ -63,6 +65,24 @@ class TextViewTest : BasePlatformTestCase() {
|
||||
assertEquals("first second", view.markdown())
|
||||
}
|
||||
|
||||
fun `test appendDelta empty string does not repaint or change markdown`() {
|
||||
val view = TextView(Text("p1").also { it.content.append("keep") })
|
||||
val repaint = TrackingRepaintManager(view)
|
||||
val old = RepaintManager.currentManager(view)
|
||||
|
||||
try {
|
||||
RepaintManager.setCurrentManager(repaint)
|
||||
|
||||
view.appendDelta("")
|
||||
|
||||
assertEquals("keep", view.markdown())
|
||||
assertEquals(0, repaint.dirty)
|
||||
assertEquals(0, repaint.invalid)
|
||||
} finally {
|
||||
RepaintManager.setCurrentManager(old)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- contentId ------
|
||||
|
||||
fun `test contentId matches Text id`() {
|
||||
@@ -123,4 +143,19 @@ class TextViewTest : BasePlatformTestCase() {
|
||||
|
||||
assertEquals(listOf("https://kilocode.ai/docs"), urls)
|
||||
}
|
||||
|
||||
private class TrackingRepaintManager(private val watched: JComponent) : RepaintManager() {
|
||||
var dirty = 0
|
||||
var invalid = 0
|
||||
|
||||
override fun addDirtyRegion(c: JComponent, x: Int, y: Int, w: Int, h: Int) {
|
||||
if (c === watched) dirty++
|
||||
super.addDirtyRegion(c, x, y, w, h)
|
||||
}
|
||||
|
||||
override fun addInvalidComponent(invalidComponent: JComponent) {
|
||||
if (invalidComponent === watched) invalid++
|
||||
super.addInvalidComponent(invalidComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -11,6 +11,9 @@ import ai.kilocode.rpc.dto.MessageDto
|
||||
import ai.kilocode.rpc.dto.MessageTimeDto
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.JBUI
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.RepaintManager
|
||||
|
||||
/**
|
||||
* Tests for [TurnView] and [MessageView].
|
||||
@@ -191,6 +194,25 @@ class TurnViewTest : BasePlatformTestCase() {
|
||||
mv.appendDelta("unknown", "delta")
|
||||
}
|
||||
|
||||
fun `test appendDelta for unknown part id does not repaint message or parent`() {
|
||||
val parent = JPanel()
|
||||
val mv = MessageView(msg("a1", "assistant"), openFile)
|
||||
parent.add(mv)
|
||||
val repaint = TrackingRepaintManager(setOf(parent, mv))
|
||||
val old = RepaintManager.currentManager(parent)
|
||||
|
||||
try {
|
||||
RepaintManager.setCurrentManager(repaint)
|
||||
|
||||
assertFalse(mv.appendDelta("unknown", "delta"))
|
||||
|
||||
assertTrue(repaint.dirty.isEmpty())
|
||||
assertTrue(repaint.invalid.isEmpty())
|
||||
} finally {
|
||||
RepaintManager.setCurrentManager(old)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test MessageView pre-populates parts from Message on creation`() {
|
||||
val message = msg("a1", "assistant")
|
||||
val text = ai.kilocode.client.session.model.Text("p1").also { it.content.append("preloaded") }
|
||||
@@ -240,4 +262,19 @@ class TurnViewTest : BasePlatformTestCase() {
|
||||
|
||||
private fun msg(id: String, role: String): Message =
|
||||
Message(MessageDto(id = id, sessionID = "ses", role = role, time = MessageTimeDto(0.0)))
|
||||
|
||||
private class TrackingRepaintManager(private val watched: Set<JComponent>) : RepaintManager() {
|
||||
val dirty = mutableListOf<JComponent>()
|
||||
val invalid = mutableListOf<JComponent>()
|
||||
|
||||
override fun addDirtyRegion(c: JComponent, x: Int, y: Int, w: Int, h: Int) {
|
||||
if (c in watched) dirty.add(c)
|
||||
super.addDirtyRegion(c, x, y, w, h)
|
||||
}
|
||||
|
||||
override fun addInvalidComponent(invalidComponent: JComponent) {
|
||||
if (invalidComponent in watched) invalid.add(invalidComponent)
|
||||
super.addInvalidComponent(invalidComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Last status check: 2026-06-02.
|
||||
|
||||
- Implemented high-priority work: streaming markdown no longer rebuilds the full rendered tree on normal deltas, and hidden cached session UIs are disposed after a configurable timeout while `SessionUpdateQueue` uses a shared coroutine ticker instead of per-session scheduler threads.
|
||||
- Remaining high-priority work: none. `SessionController` subscription-state mutation is now EDT-confined while RPC event collection remains on background coroutines.
|
||||
- Remaining non-high-priority work: repaint/revalidate cleanup, lazy collapsed bodies, question editor disposal, EDT annotations/assertions, style callback disposal guards, `SessionUpdateQueue` Swing listener EDT confinement, question body retention, semantic timeline colors, and additional retained Swing regression tests.
|
||||
- Remaining findings: none. Semantic timeline colors and retained Swing regression coverage are complete.
|
||||
|
||||
## High Priority
|
||||
|
||||
@@ -82,17 +82,18 @@ Last status check: 2026-06-02.
|
||||
|
||||
## Low Priority
|
||||
|
||||
- [ ] Replace hardcoded timeline runtime colors with semantic named colors
|
||||
- [x] Replace hardcoded timeline runtime colors with semantic named colors
|
||||
- Severity: Low/Medium
|
||||
- Files: `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt`
|
||||
- Issue: timeline color tokens use numeric `JBColor(Color(...), Color(...))`; AGENTS.md prefers theme-derived or `JBColor.namedColor(...)` semantic keys.
|
||||
- Plan direction: Introduce named color keys with appropriate fallbacks or map to existing platform theme colors.
|
||||
- Implemented: timeline tokens now use `JBColor.namedColor(...)` keys under `Kilo.Session.Timeline.*`, with platform semantic fallbacks for success/error/text/step and centralized blue fallbacks for read/write/tool.
|
||||
- Tests/release note: `SessionHeaderPanelTest` verifies timeline colors honor semantic UIManager named-color overrides; `.changeset/semantic-jetbrains-timeline.md` documents the user-facing theming improvement.
|
||||
|
||||
- [ ] Add focused regression tests for retained Swing behavior
|
||||
- [x] Add focused regression tests for retained Swing behavior
|
||||
- Severity: Low
|
||||
- Files: `packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/**`
|
||||
- Issue: Several risks need tests: no parent refresh on no-op deltas, lazy body creation, editor disposal, hidden-session queue bounds, and EDT assertions.
|
||||
- Plan direction: Extend existing session UI/controller tests using real IntelliJ EDT fixtures, not mocks.
|
||||
- Implemented: added no-op repaint coverage for unknown message deltas and empty text deltas, custom question editor detachment coverage after forcing lazy editor creation, hidden queued transcript discard coverage after timeout disposal, and an EDT assertion regression for `SessionModel` mutation.
|
||||
|
||||
## Confirmed OK
|
||||
|
||||
|
||||
Reference in New Issue
Block a user