mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 01:51:21 +08:00
fix(jetbrains): session loading state, history panel loading indicator, and model dedup
- Add SessionState.Loading and handle it in ProgressPanel and isBusy() - Deduplicate setState calls in SessionModel (no-op when state unchanged) - HistoryController now opens cloud items as SessionRef.Cloud instead of importing inline - HistoryModel tracks loaded flag; HistoryPanel shows a loading card before first load - SessionManager interface takes SessionRef instead of raw SessionDto - build.gradle.kts: set splitModeServerPort for runIdeBackend task - Test: NewSessionActionTest, SessionUiTestBase, SessionModelTest updated accordingly
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show loading immediately when opening JetBrains sessions without showing duplicate progress for new sessions.
|
||||
@@ -66,6 +66,12 @@ intellijPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
runIdeBackend {
|
||||
splitModeServerPort.set(12345)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<JavaExec>("runIde") {
|
||||
dependsOn(":backend:processResources")
|
||||
jvmArgumentProviders += CommandLineArgumentProvider {
|
||||
|
||||
+5
-1
@@ -12,5 +12,9 @@ interface SessionManager {
|
||||
|
||||
fun showHistory()
|
||||
|
||||
fun openSession(session: SessionDto)
|
||||
fun openSession(ref: SessionRef)
|
||||
|
||||
fun openSession(session: SessionDto) {
|
||||
openSession(SessionRef.Local(session))
|
||||
}
|
||||
}
|
||||
|
||||
+4
-23
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.history
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.rpc.dto.CloudSessionDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
@@ -13,7 +14,7 @@ class HistoryController(
|
||||
private val sessions: KiloSessionService,
|
||||
private val workspace: Workspace,
|
||||
private val cs: CoroutineScope,
|
||||
open: (LocalHistoryItem) -> Unit = {},
|
||||
open: (SessionRef) -> Unit = {},
|
||||
private val deleted: (String) -> Unit = {},
|
||||
) {
|
||||
companion object {
|
||||
@@ -24,7 +25,6 @@ class HistoryController(
|
||||
val cloud = CloudHistoryModel()
|
||||
|
||||
private val deleting = mutableSetOf<String>()
|
||||
private val importing = mutableSetOf<String>()
|
||||
private val opener = open
|
||||
private var git: String? = null
|
||||
|
||||
@@ -82,30 +82,11 @@ class HistoryController(
|
||||
fun deleting(item: LocalHistoryItem): Boolean = item.id in deleting
|
||||
|
||||
fun open(item: LocalHistoryItem) {
|
||||
edt { opener(item) }
|
||||
edt { opener(SessionRef.Local(item.session)) }
|
||||
}
|
||||
|
||||
fun open(item: CloudHistoryItem) {
|
||||
edt {
|
||||
if (item.id in importing) return@edt
|
||||
importing.add(item.id)
|
||||
cloud.refresh()
|
||||
cs.launch {
|
||||
try {
|
||||
val session = sessions.importCloudSession(item.id, workspace.directory)
|
||||
edt {
|
||||
importing.remove(item.id)
|
||||
cloud.refresh()
|
||||
opener(LocalHistoryItem(session))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
edt {
|
||||
importing.remove(item.id)
|
||||
cloud.fail(e.message ?: KiloBundle.message("history.error.cloud"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
edt { opener(SessionRef.Cloud(item.session)) }
|
||||
}
|
||||
|
||||
private fun loadCloud(reset: Boolean) {
|
||||
|
||||
+6
-1
@@ -11,6 +11,8 @@ open class HistoryModel<T : HistoryItem> : AbstractListModel<T>() {
|
||||
private set
|
||||
var error: String? = null
|
||||
private set
|
||||
var loaded = false
|
||||
private set
|
||||
|
||||
val items: List<T> get() = all
|
||||
val visibleItems: List<T> get() = rows
|
||||
@@ -28,6 +30,7 @@ open class HistoryModel<T : HistoryItem> : AbstractListModel<T>() {
|
||||
fun replace(items: List<T>) {
|
||||
all = HistoryTime.sorted(items)
|
||||
loading = false
|
||||
loaded = true
|
||||
error = null
|
||||
filter()
|
||||
}
|
||||
@@ -35,6 +38,7 @@ open class HistoryModel<T : HistoryItem> : AbstractListModel<T>() {
|
||||
fun append(items: List<T>) {
|
||||
all = HistoryTime.sorted(all + items)
|
||||
loading = false
|
||||
loaded = true
|
||||
error = null
|
||||
filter()
|
||||
}
|
||||
@@ -46,6 +50,7 @@ open class HistoryModel<T : HistoryItem> : AbstractListModel<T>() {
|
||||
|
||||
fun fail(message: String) {
|
||||
loading = false
|
||||
loaded = true
|
||||
error = message
|
||||
refresh()
|
||||
}
|
||||
@@ -90,7 +95,7 @@ class CloudHistoryModel : HistoryModel<CloudHistoryItem>() {
|
||||
fun start(reset: Boolean) {
|
||||
cursor = cursor.takeUnless { reset }
|
||||
start()
|
||||
if (reset) clear()
|
||||
if (reset && !loaded) clear()
|
||||
}
|
||||
|
||||
fun replace(items: List<CloudHistoryItem>, next: String?) {
|
||||
|
||||
+23
-1
@@ -1,6 +1,8 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.LoadingPanel
|
||||
import ai.kilocode.client.session.ui.SessionStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.ide.ui.LafManagerListener
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
@@ -22,6 +24,7 @@ import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.event.HierarchyEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
@@ -48,6 +51,9 @@ class HistoryPanel(
|
||||
private val more = JButton(KiloBundle.message("history.cloud.load.more"))
|
||||
private val localPanel = panel(localSearch, localList)
|
||||
private val cloudPanel = panel(cloudSearch, cloudList, more)
|
||||
private val cards = CardLayout()
|
||||
private val body = BorderLayoutPanel().apply { layout = cards }
|
||||
private val load = LoadingPanel()
|
||||
private val localInfo = TabInfo(localPanel).setText(KiloBundle.message("history.tab.local"))
|
||||
private val cloudInfo = TabInfo(cloudPanel).setText(KiloBundle.message("history.tab.cloud"))
|
||||
private var stale = false
|
||||
@@ -79,7 +85,9 @@ class HistoryPanel(
|
||||
}
|
||||
if (!isShowing) stale = true
|
||||
}
|
||||
add(tabs.component, BorderLayout.CENTER)
|
||||
body.add(load, CARD_LOAD)
|
||||
body.add(tabs.component, CARD_TABS)
|
||||
add(body, BorderLayout.CENTER)
|
||||
sync()
|
||||
refresh()
|
||||
}
|
||||
@@ -107,6 +115,7 @@ class HistoryPanel(
|
||||
SwingUtilities.updateComponentTreeUI(this)
|
||||
SwingUtilities.updateComponentTreeUI(localPanel)
|
||||
SwingUtilities.updateComponentTreeUI(cloudPanel)
|
||||
load.applyStyle(SessionStyle.current())
|
||||
updateRenderer(localList)
|
||||
updateRenderer(cloudList)
|
||||
sync()
|
||||
@@ -219,10 +228,16 @@ class HistoryPanel(
|
||||
syncList(cloudList, controller.cloud)
|
||||
more.isEnabled = controller.cloud.cursor != null && !controller.cloud.loading
|
||||
more.isVisible = controller.cloud.cursor != null || controller.cloud.loading
|
||||
cards.show(body, if (loading()) CARD_LOAD else CARD_TABS)
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun loading(): Boolean {
|
||||
if (controller.local.loaded || controller.cloud.loaded) return false
|
||||
return controller.local.loading || controller.cloud.loading
|
||||
}
|
||||
|
||||
private fun <T : HistoryItem> syncList(list: JBList<T>, model: HistoryModel<T>) {
|
||||
list.setPaintBusy(model.loading)
|
||||
list.emptyText.text = when {
|
||||
@@ -336,4 +351,11 @@ class HistoryPanel(
|
||||
override fun dispose() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
internal fun showingLoading() = !controller.local.loaded && !controller.cloud.loaded && (controller.local.loading || controller.cloud.loading)
|
||||
|
||||
private companion object {
|
||||
const val CARD_LOAD = "load"
|
||||
const val CARD_TABS = "tabs"
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -175,6 +175,7 @@ class SessionModel {
|
||||
}
|
||||
|
||||
fun setState(state: SessionState) {
|
||||
if (this.state == state) return
|
||||
this.state = state
|
||||
fire(SessionModelEvent.StateChanged(state))
|
||||
updateHeader()
|
||||
|
||||
+3
-1
@@ -4,6 +4,8 @@ package ai.kilocode.client.session.model
|
||||
sealed class SessionState {
|
||||
data object Idle : SessionState()
|
||||
|
||||
data object Loading : SessionState()
|
||||
|
||||
data class Busy(val text: String) : SessionState()
|
||||
|
||||
data class AwaitingQuestion(val question: Question) : SessionState()
|
||||
@@ -17,7 +19,7 @@ sealed class SessionState {
|
||||
data class Error(val message: String, val kind: String? = null) : SessionState()
|
||||
|
||||
fun isBusy(): Boolean = when (this) {
|
||||
is Idle, is Error -> false
|
||||
is Idle, is Loading, is Error -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
package ai.kilocode.client.session.ui
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.md.MdView
|
||||
@@ -67,7 +68,7 @@ class EmptySessionPanel(
|
||||
val index = row(e)
|
||||
if (index < 0) return
|
||||
selectedIndex = index
|
||||
controller.openSession(model.getElementAt(index))
|
||||
controller.openSession(SessionRef.Local(model.getElementAt(index)))
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
@@ -153,7 +154,7 @@ class EmptySessionPanel(
|
||||
|
||||
internal fun clickRecent(index: Int) {
|
||||
list.selectedIndex = index
|
||||
controller.openSession(model.getElementAt(index))
|
||||
controller.openSession(SessionRef.Local(model.getElementAt(index)))
|
||||
}
|
||||
|
||||
internal fun recentVisible() = true
|
||||
|
||||
+2
-1
@@ -15,7 +15,7 @@ import java.awt.FlowLayout
|
||||
*
|
||||
* Reacts to [SessionModelEvent.StateChanged]:
|
||||
* - [SessionState.Busy] → shows an animated spinner and [SessionState.Busy.text]
|
||||
* - Any other state → hidden
|
||||
* - Any other state -> hidden
|
||||
*
|
||||
* Owned by [SessionMessageListPanel], which always re-anchors it as the last child so it
|
||||
* appears below all turn views inside the scroll pane.
|
||||
@@ -52,6 +52,7 @@ class ProgressPanel(
|
||||
label.text = state.text
|
||||
isVisible = true
|
||||
}
|
||||
is SessionState.Loading -> isVisible = false
|
||||
else -> isVisible = false
|
||||
}
|
||||
revalidate()
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
package ai.kilocode.client.actions
|
||||
|
||||
import ai.kilocode.client.session.SessionManager
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.actionSystem.Presentation
|
||||
@@ -49,7 +49,10 @@ class NewSessionActionTest : BasePlatformTestCase() {
|
||||
created++
|
||||
}
|
||||
|
||||
override fun openSession(session: SessionDto) {
|
||||
override fun showHistory() {
|
||||
}
|
||||
|
||||
override fun openSession(ref: SessionRef) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -10,6 +10,7 @@ import ai.kilocode.client.session.update.SessionController
|
||||
import ai.kilocode.client.testing.FakeAppRpcApi
|
||||
import ai.kilocode.client.testing.FakeSessionRpcApi
|
||||
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
@@ -76,10 +77,9 @@ abstract class SessionUiTestBase : BasePlatformTestCase() {
|
||||
protected fun newUi(
|
||||
id: String? = null,
|
||||
displayMs: Long = 0,
|
||||
loading: Boolean = id == null,
|
||||
open: (SessionDto) -> Unit = {},
|
||||
open: (SessionRef) -> Unit = {},
|
||||
): SessionUi {
|
||||
return SessionUi(project, workspace, sessions, app, scope, id = id, displayMs = displayMs, loading = loading, open = open).apply {
|
||||
return SessionUi(project, workspace, sessions, app, scope, id = id, displayMs = displayMs, open = open).apply {
|
||||
setSize(800, 600)
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -328,6 +328,17 @@ class SessionModelTest : UsefulTestCase() {
|
||||
assertEquals(state, (events.single() as SessionModelEvent.StateChanged).state)
|
||||
}
|
||||
|
||||
fun `test setState duplicate is no-op`() {
|
||||
val state = SessionState.Busy("thinking")
|
||||
model.setState(state)
|
||||
events.clear()
|
||||
|
||||
model.setState(state)
|
||||
|
||||
assertTrue(events.isEmpty())
|
||||
assertEquals(state, model.state)
|
||||
}
|
||||
|
||||
fun `test setState to Error stores error data`() {
|
||||
model.setState(SessionState.Error("something broke", "timeout"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user