From 9950739e36b40a682c0a25173e62f5236e60f81a Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 26 Jul 2026 13:43:02 -0400 Subject: [PATCH 1/3] feat(jetbrains): support queued prompts --- .changeset/jetbrains-queued-prompts.md | 5 ++ .../backend/app/KiloBackendChatManager.kt | 31 +++++++-- .../kilocode/backend/cli/KiloCliDataParser.kt | 6 ++ .../backend/rpc/KiloSessionRpcApiImpl.kt | 3 + .../backend/app/KiloBackendChatManagerTest.kt | 43 ++++++++++++ .../backend/cli/KiloCliDataParserTest.kt | 13 ++++ .../kilocode/backend/testing/MockCliServer.kt | 7 ++ .../kilocode/client/app/KiloSessionService.kt | 3 + .../ai/kilocode/client/session/SessionUi.kt | 3 + .../session/controller/SessionController.kt | 20 +++++- .../client/session/model/SessionModel.kt | 15 ++++ .../client/session/model/SessionModelEvent.kt | 3 + .../session/ui/SessionMessageListPanel.kt | 26 ++++++- .../session/ui/header/SessionHeaderPanel.kt | 1 + .../client/session/ui/prompt/PromptPanel.kt | 25 ++++--- .../client/session/views/MessageToolbar.kt | 17 ++++- .../client/session/views/MessageView.kt | 68 +++++++++++++++++-- .../kilocode/client/session/views/TurnView.kt | 8 +++ .../resources/messages/KiloBundle.properties | 2 + .../session/controller/PromptLifecycleTest.kt | 27 ++++++++ .../client/session/ui/PromptPanelTest.kt | 5 +- .../session/ui/SessionMessageListPanelTest.kt | 27 ++++++++ .../client/testing/FakeSessionRpcApi.kt | 9 +++ packages/kilo-jetbrains/package.json | 2 +- .../kotlin/ai/kilocode/log/ChatLogSummary.kt | 7 ++ .../ai/kilocode/rpc/KiloSessionRpcApi.kt | 3 + .../kotlin/ai/kilocode/rpc/dto/ChatDto.kt | 7 ++ 27 files changed, 358 insertions(+), 28 deletions(-) create mode 100644 .changeset/jetbrains-queued-prompts.md diff --git a/.changeset/jetbrains-queued-prompts.md b/.changeset/jetbrains-queued-prompts.md new file mode 100644 index 00000000000..b22d8c8353c --- /dev/null +++ b/.changeset/jetbrains-queued-prompts.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Allow sending prompts while a session is busy and show queued prompts with a remove action. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt index d7c2be2cdf8..4ebd49744e2 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt @@ -65,6 +65,7 @@ class KiloBackendChatManager( "session.status", "session.updated", "session.idle", + "session.queue.changed", "session.compacted", "session.diff", "permission.asked", @@ -90,14 +91,15 @@ class KiloBackendChatManager( if (watcher?.isActive == true) return watcher = cs.launch { sse.collect { event -> - if (event.type in CHAT_EVENTS) { + val type = if (event.type in CHAT_EVENTS) event.type else KiloCliDataParser.extractEventType(event.data) + if (type in CHAT_EVENTS) { val events = try { - normalizer.parse(event.type, event.data) + normalizer.parse(type, event.data) } catch (e: CancellationException) { throw e } catch (e: Exception) { log.warn( - "route=chat-events parse=false type=${event.type} bytes=${event.data.length} ${ChatLogSummary.body(event.data)}", + "route=chat-events parse=false type=$type raw=${event.type} bytes=${event.data.length} ${ChatLogSummary.body(event.data)}", e, ) return@collect @@ -120,7 +122,7 @@ class KiloBackendChatManager( _events.emit(parsed) } } else { - log.warn("route=chat-events parse=null type=${event.type} bytes=${event.data.length} ${ChatLogSummary.body(event.data)}") + log.warn("route=chat-events parse=null type=$type raw=${event.type} bytes=${event.data.length} ${ChatLogSummary.body(event.data)}") } } } @@ -264,6 +266,27 @@ class KiloBackendChatManager( postCancellable("/session/$id/revert?directory=${encode(dir)}", body, "revert", "${ChatLogSummary.sid(id)} kind=revert") } + suspend fun deleteMessage(id: String, dir: String, message: String): Boolean { + log.info("${ChatLogSummary.sid(id)} kind=deleteMessage ${ChatLogSummary.dir(dir)} message=$message") + val http = requireClient() + val url = requireBase() + val request = Request.Builder() + .url("$url/session/$id/message/$message?directory=${encode(dir)}") + .delete() + .build() + val call = http.newCall(request) + call.timeout().timeout(REVERT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + return call.await().use { response -> + val raw = response.body?.string().orEmpty().trim() + if (!response.isSuccessful) { + log.warn("deleteMessage failed: HTTP ${response.code}") + raw.takeIf { it.isNotBlank() }?.let { log.debug { "${ChatLogSummary.sid(id)} kind=deleteMessage error=${ChatLogSummary.body(it)}" } } + return@use false + } + raw != "false" + } + } + suspend fun unrevert(id: String, dir: String) { log.info("${ChatLogSummary.sid(id)} kind=unrevert ${ChatLogSummary.dir(dir)}") postCancellable("/session/$id/unrevert?directory=${encode(dir)}", "{}", "unrevert", "${ChatLogSummary.sid(id)} kind=unrevert") diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index a87fd32c6aa..1d4fa2ee4b7 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -248,6 +248,12 @@ object KiloCliDataParser { ChatEventDto.SessionIdle(sid) } + "session.queue.changed" -> { + val sid = props.str("sessionID") ?: return null + val queued = props["queued"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList() + ChatEventDto.SessionQueueChanged(sid, queued) + } + "session.compacted" -> { val sid = props.str("sessionID") ?: return null ChatEventDto.SessionCompacted(sid) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt index 531b66ed244..597404c665f 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt @@ -132,6 +132,9 @@ class KiloSessionRpcApiImpl internal constructor( override suspend fun revert(id: String, directory: String, messageID: String, partID: String?) = ready { chat.revert(id, sessions.getDirectory(id, directory), messageID, partID) } + override suspend fun deleteMessage(id: String, directory: String, messageID: String): Boolean = + ready { chat.deleteMessage(id, sessions.getDirectory(id, directory), messageID) } + override suspend fun unrevert(id: String, directory: String) = ready { chat.unrevert(id, sessions.getDirectory(id, directory)) } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt index 0d62f668dc4..3225fef1a03 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt @@ -90,6 +90,32 @@ class KiloBackendChatManagerTest { assertEquals("{}", mock.lastUnrevertBody) } + @Test + fun `delete message sends queued message delete request`() = runBlocking { + val port = mock.start() + val chat = KiloBackendChatManager(scope, TestLog()) + chat.start(OkHttpClient(), port, MutableSharedFlow()) + + val result = chat.deleteMessage("ses_abc", "/test/project", "msg1") + + assertTrue(result) + assertEquals(1, mock.requestCount("/session/ses_abc/message/msg1")) + assertTrue(mock.lastMessageDeletePath!!.startsWith("/session/ses_abc/message/msg1?directory=")) + } + + @Test + fun `delete message returns false for queued drop miss`() = runBlocking { + val port = mock.start() + val chat = KiloBackendChatManager(scope, TestLog()) + chat.start(OkHttpClient(), port, MutableSharedFlow()) + mock.messageDeleteResponse = "false" + + val result = chat.deleteMessage("ses_abc", "/test/project", "msg1") + + assertEquals(false, result) + assertEquals(1, mock.requestCount("/session/ses_abc/message/msg1")) + } + @Test fun `revert failure throws on non successful response`() = runBlocking { val port = mock.start() @@ -202,4 +228,21 @@ class KiloBackendChatManagerTest { assertEquals("ses_abc", event.sessionID) assertTrue(log.messages.any { it.contains("route=chat-events parse=false type=session.error") }, log.messages.joinToString("\n")) } + + @Test + fun `global message event type is extracted from payload`() = runBlocking { + val port = mock.start() + val sse = MutableSharedFlow(replay = 8) + val chat = KiloBackendChatManager(scope, TestLog()) + chat.start(OkHttpClient(), port, sse) + + val received = async(start = CoroutineStart.UNDISPATCHED) { withTimeout(5_000) { chat.events.first() } } + withTimeout(5_000) { sse.subscriptionCount.first { it > 0 } } + sse.emit(SseEvent("message", """{"payload":{"type":"session.queue.changed","properties":{"sessionID":"ses_abc","queued":["msg2"]}}}""")) + + val event = received.await() + assertTrue(event is ChatEventDto.SessionQueueChanged) + assertEquals("ses_abc", event.sessionID) + assertEquals(listOf("msg2"), event.queued) + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 8e71d6ce373..2a32a5060bd 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -651,6 +651,19 @@ class KiloCliDataParserTest { assertTrue(result is ChatEventDto.SessionCompacted) } + @Test + fun `parseChatEvent - session queue changed`() { + val data = globalEvent(""" + "type": "session.queue.changed", + "properties": { "sessionID": "ses_1", "queued": ["msg2", "msg3"] } + """) + val result = KiloCliDataParser.parseChatEvent("session.queue.changed", data) + assertNotNull(result) + assertTrue(result is ChatEventDto.SessionQueueChanged) + assertEquals("ses_1", result.sessionID) + assertEquals(listOf("msg2", "msg3"), result.queued) + } + @Test fun `parseChatEvent - session updated`() { val data = globalEvent(""" diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index 5e05d255733..d016709a685 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -117,11 +117,14 @@ class MockCliServer : AutoCloseable { @Volatile var lastCloudSessionImportBody: String? = null @Volatile var summarizeStatus = 200 @Volatile var revertStatus = 200 + @Volatile var messageDeleteStatus = 200 + @Volatile var messageDeleteResponse = "true" @Volatile var unrevertStatus = 200 @Volatile var lastSummarizePath: String? = null @Volatile var lastSummarizeBody: String? = null @Volatile var lastRevertPath: String? = null @Volatile var lastRevertBody: String? = null + @Volatile var lastMessageDeletePath: String? = null @Volatile var lastUnrevertPath: String? = null @Volatile var lastUnrevertBody: String? = null @Volatile var promptStatus = 200 @@ -438,6 +441,10 @@ class MockCliServer : AutoCloseable { lastRevertBody = body respond(output, revertStatus, sessionCreate) } + bare.matches(Regex("/session/ses_[^/]+/message/[^/]+")) && method == "DELETE" -> { + lastMessageDeletePath = path + respond(output, messageDeleteStatus, messageDeleteResponse) + } bare.matches(Regex("/session/ses_[^/]+/unrevert")) && method == "POST" -> { lastUnrevertPath = path lastUnrevertBody = body diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index 2de1eb64ded..1aef23518ff 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -202,6 +202,9 @@ class KiloSessionService internal constructor( log.info("${ChatLogSummary.sid(id)} kind=revert ok=true") } + suspend fun deleteMessage(id: String, dir: String, message: String): Boolean = + call { deleteMessage(id, dir, message) } + suspend fun unrevert(id: String, dir: String) { call { unrevert(id, dir) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 86333825399..c59014686dc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -367,6 +367,7 @@ class SessionUi( resize = { anchor, fn -> scroll.preserve(anchor, fn) }, revert = ::revert, cancelRevert = ::cancelRevert, + deleteQueued = { id -> controller.deleteQueuedMessage(id) }, banner = RevertBanner(controller.model, ::redo, controller::redoAll, ::cancelRevert, focus), ).also { it.onHover = { view, on -> if (on) popup.show(view) else popup.notifyExit(view) } @@ -543,6 +544,8 @@ class SessionUi( is SessionModelEvent.RevertChanged -> onRevertChanged(event.revert) + is SessionModelEvent.QueueChanged -> Unit + is SessionModelEvent.TurnAdded, is SessionModelEvent.TurnUpdated, is SessionModelEvent.ContentAdded, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 0570d4bd366..ec3299f6b02 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -464,6 +464,19 @@ class SessionController( } } + fun deleteQueuedMessage(message: String) { + assertEdt() + val id = sid ?: return + capture("Conversation Queued Message Removed", sessionProps(id)) + cs.launch { + try { + sessions.deleteMessage(id, directory, message) + } catch (e: Exception) { + LOG.warn("${ChatLogSummary.sid(id)} kind=deleteMessage failed message=${e.message}", e) + } + } + } + fun unrevert() { assertEdt() val id = sid ?: return @@ -1368,6 +1381,8 @@ class SessionController( idle() } + is ChatEventDto.SessionQueueChanged -> updateModel { model.setQueued(event.queued.toSet()) } + is ChatEventDto.SessionCompacted -> { capture("Context Condensed", sessionProps(event.sessionID)) model.markCompacted() @@ -1406,7 +1421,8 @@ class SessionController( is ChatEventDto.QuestionRejected, is ChatEventDto.SessionStatusChanged, is ChatEventDto.SessionUpdated, - is ChatEventDto.SessionIdle -> { + is ChatEventDto.SessionIdle, + is ChatEventDto.SessionQueueChanged -> { edt { if (disposed) return@edt updateModel { handleMetadata(event) } @@ -1428,6 +1444,7 @@ class SessionController( is ChatEventDto.SessionStatusChanged -> status(event.status) is ChatEventDto.SessionUpdated -> model.setSession(event.session) is ChatEventDto.SessionIdle -> idle() + is ChatEventDto.SessionQueueChanged -> model.setQueued(event.queued.toSet()) else -> Unit } } @@ -2312,6 +2329,7 @@ private fun matchesSession(event: ChatEventDto, id: String): Boolean = when (eve is ChatEventDto.SessionStatusChanged -> event.sessionID == id is ChatEventDto.SessionUpdated -> event.sessionID == id is ChatEventDto.SessionIdle -> event.sessionID == id + is ChatEventDto.SessionQueueChanged -> event.sessionID == id is ChatEventDto.SessionCompacted -> event.sessionID == id is ChatEventDto.SessionDiffChanged -> event.sessionID == id is ChatEventDto.TodoUpdated -> event.sessionID == id diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt index fa9412ff3e9..14f7b8c0271 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt @@ -74,6 +74,9 @@ class SessionModel { private var revert: SessionRevertDto? = null + var queued: Set = emptySet() + private set + var header: SessionHeaderSnapshot = emptyHeader() private set @@ -125,6 +128,9 @@ class SessionModel { return idx >= 0 && pos >= idx } + @RequiresEdt + fun isQueued(id: String): Boolean = id in queued + @RequiresEdt fun turn(id: String): Turn? = turnEntries[id] @@ -295,6 +301,13 @@ class SessionModel { fire(SessionModelEvent.RevertChanged(revert)) } + @RequiresEdt + fun setQueued(ids: Set) { + if (queued == ids) return + queued = ids + fire(SessionModelEvent.QueueChanged(ids)) + } + @RequiresEdt fun setDiff(diff: List) { this.diff = diff @@ -329,6 +342,7 @@ class SessionModel { hiddenText.clear() session = null revert = null + queued = emptySet() state = SessionState.Idle diff = emptyList() todos = emptyList() @@ -363,6 +377,7 @@ class SessionModel { hiddenText.clear() session = null revert = null + queued = emptySet() state = SessionState.Idle diff = emptyList() todos = emptyList() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModelEvent.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModelEvent.kt index bc553cfe58c..d3af00f6388 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModelEvent.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModelEvent.kt @@ -60,6 +60,9 @@ sealed class SessionModelEvent { data class RevertChanged(val revert: SessionRevertDto?) : SessionModelEvent() { override fun toString() = "RevertChanged ${revert?.messageID ?: "none"}" } + data class QueueChanged(val queued: Set) : SessionModelEvent() { + override fun toString() = "QueueChanged [${queued.sorted().joinToString(", ")}]" + } data class HeaderUpdated(val header: SessionHeaderSnapshot) : SessionModelEvent() { override fun toString() = "HeaderUpdated visible=${header.visible}" } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index a0dc4d7dfdc..3396a1c1cec 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -60,6 +60,7 @@ class SessionMessageListPanel( private val resize: ((JComponent, () -> Unit) -> Unit)? = null, private val revert: ((String) -> Unit)? = null, private val cancelRevert: (() -> Unit)? = null, + private val deleteQueued: ((String) -> Unit)? = null, private val banner: RevertBanner? = null, ) : SessionLayoutPanel( SessionUiStyle.SessionLayout.GAP, @@ -148,6 +149,12 @@ class SessionMessageListPanel( refresh() } + is SessionModelEvent.QueueChanged -> { + syncQueued() + syncSettled() + refresh() + } + // Message events: structural changes are handled via turn events above. is SessionModelEvent.MessageAdded, is SessionModelEvent.MessageUpdated, @@ -216,7 +223,7 @@ class SessionMessageListPanel( // ------ private event handlers ------ private fun onTurnAdded(turn: ai.kilocode.client.session.model.Turn) { - val tv = TurnView(turn.id, openFile, style, openUrl, selection, openAttachment, resize, repo, ::hover, revert) + val tv = TurnView(turn.id, openFile, style, openUrl, selection, openAttachment, resize, repo, ::hover, revert, deleteQueued) turnViews[turn.id] = tv for (msgId in turn.messageIds) { val msg = model.message(msgId) ?: continue @@ -224,6 +231,7 @@ class SessionMessageListPanel( register(msgId, tv, mv) } tv.syncCopyToolbars() + syncQueued(tv) syncReverted() add(tv) syncSettled() @@ -251,6 +259,7 @@ class SessionMessageListPanel( register(id, tv, mv) } tv.syncCopyToolbars() + syncQueued(tv) syncReverted() syncSettled() @@ -279,7 +288,7 @@ class SessionMessageListPanel( removeAll() for (turn in model.turns()) { - val tv = TurnView(turn.id, openFile, style, openUrl, selection, openAttachment, resize, repo, ::hover, revert) + val tv = TurnView(turn.id, openFile, style, openUrl, selection, openAttachment, resize, repo, ::hover, revert, deleteQueued) turnViews[turn.id] = tv for (msgId in turn.messageIds) { val msg = model.message(msgId) ?: continue @@ -287,11 +296,13 @@ class SessionMessageListPanel( register(msgId, tv, mv) } tv.syncCopyToolbars() + syncQueued(tv) add(tv) } syncActive(model.state) syncSettled(model.state) + syncQueued() syncReverted() syncReverting(model.state) banner?.update() @@ -321,6 +332,7 @@ class SessionMessageListPanel( removeAll() syncActive(model.state) syncSettled(model.state) + syncQueued() syncReverting(model.state) banner?.update() anchorFooter() @@ -384,10 +396,18 @@ class SessionMessageListPanel( } private fun syncSettled(state: SessionState = model.state) { - val active = if (state.isBusy()) turnViews.values.lastOrNull() else null + val active = if (state.isBusy()) turnViews.values.lastOrNull { !model.isQueued(it.id) } else null for (view in turnViews.values) view.setSettled(view !== active) } + private fun syncQueued() { + for (view in turnViews.values) syncQueued(view) + } + + private fun syncQueued(view: TurnView) { + view.setQueued(model.isQueued(view.id)) { id -> deleteQueued?.invoke(id) } + } + /** * Re-insert [question], [permission], [login], and [progress] as the last children * so active views always render after all turn views, and progress is last. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index d3a47c3260d..96d2a8e7682 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -207,6 +207,7 @@ class SessionHeaderPanel( is SessionModelEvent.TodosUpdated, is SessionModelEvent.SessionUpdated, is SessionModelEvent.RevertChanged, + is SessionModelEvent.QueueChanged, is SessionModelEvent.Compacted, is SessionModelEvent.HistoryLoaded, is SessionModelEvent.Cleared, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index ec5f7e7c1ae..9a4fd04d14c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -212,7 +212,7 @@ class PromptPanel( isFocusPainted = false addActionListener { syncTooltip() - val id = if (busy) StopSessionAction.ID else SendPromptAction.ID + val id = if (busy && !hasDraft()) StopSessionAction.ID else SendPromptAction.ID val action = ActionManager.getInstance().getAction(id) ?: return@addActionListener val ctx = DataManager.getInstance().getDataContext(button) @@ -258,7 +258,7 @@ class PromptPanel( private var request = 0L override val isSendEnabled: Boolean - get() = ready && !busy && !submitting && (text().isNotEmpty() || attachments.isNotEmpty()) + get() = ready && !submitting && (text().isNotEmpty() || attachments.isNotEmpty()) override val isStopEnabled: Boolean get() = busy @@ -273,6 +273,7 @@ class PromptPanel( syncEditorHeight() triggerCompletion(e) syncHighlights() + syncButton() onChange() } }) @@ -418,7 +419,7 @@ class PromptPanel( fun setBusy(value: Boolean) { busy = value if (value) invalidateEnhancement() else syncEnhance() - button.icon = if (value) STOP_ICON else SEND_ICON + syncButton() syncTooltip() } @@ -628,6 +629,11 @@ class PromptPanel( } } + @RequiresEdt + private fun syncButton() { + button.icon = if (busy && !hasDraft()) STOP_ICON else SEND_ICON + } + @RequiresEdt private fun submit(src: String) { if (!isSendEnabled) return @@ -885,19 +891,20 @@ class PromptPanel( } private fun tooltip(): String { - val id = if (busy) StopSessionAction.ID else SendPromptAction.ID - val text = if (busy) { + val stop = busy && !hasDraft() + val id = if (stop) StopSessionAction.ID else SendPromptAction.ID + val text = if (stop) { KiloBundle.message("prompt.button.stop") } else { KiloBundle.message("prompt.button.send") } val tip = KeymapUtil.createTooltipText(text, id) - if (busy) return tip - val stop = KeymapUtil.getFirstKeyboardShortcutText(StopSessionAction.ID) - if (stop.isEmpty()) return tip + if (stop) return tip + val shortcut = KeymapUtil.getFirstKeyboardShortcutText(StopSessionAction.ID) + if (shortcut.isEmpty()) return tip return XmlStringUtil.wrapInHtml( XmlStringUtil.escapeString(tip) + "
" + - XmlStringUtil.escapeString(KiloBundle.message("prompt.button.send.tooltip.stop", stop)) + XmlStringUtil.escapeString(KiloBundle.message("prompt.button.send.tooltip.stop", shortcut)) ) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt index 4ffeee14468..1df7642dc3d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt @@ -33,6 +33,7 @@ internal class MessageToolbar( buttons.forEach { next(it) } next(button) } + private var custom: JComponent? = null init { isOpaque = false @@ -41,10 +42,12 @@ internal class MessageToolbar( @RequiresEdt fun sync(value: Boolean) { - if (isVisible == value && button.isEnabled == value) return + val controls = customButtons() + if (isVisible == value && button.isEnabled == value && controls.all { it.isEnabled == value }) return isVisible = value button.isEnabled = value buttons.forEach { it.isEnabled = value } + controls.forEach { it.isEnabled = value } revalidate() repaint() } @@ -60,6 +63,16 @@ internal class MessageToolbar( @RequiresEdt fun copyButton() = button + @RequiresEdt + fun setCustom(node: JComponent?) { + if (custom === node) return + remove(custom ?: row) + custom = node + add(node ?: row) + revalidate() + repaint() + } + fun placeholder(): JComponent = object : JPanel() { init { isOpaque = false @@ -78,4 +91,6 @@ internal class MessageToolbar( copy.dismiss() super.removeNotify() } + + private fun customButtons() = custom?.components?.filterIsInstance().orEmpty() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index aed735293b8..5ccc77850f3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -19,13 +19,21 @@ import ai.kilocode.client.session.ui.selection.SessionSelection import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.session.views.base.PartView import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.ui.ToolbarButtonAction import ai.kilocode.client.ui.layout.HAlign import ai.kilocode.client.ui.layout.VAlign import ai.kilocode.client.ui.layout.align +import ai.kilocode.client.ui.toolbarButton +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack +import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBLabel import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil import java.awt.BorderLayout import java.awt.Point import java.awt.Graphics @@ -397,6 +405,12 @@ class MessageView( wrap?.setReverting(active, text, onCancel) } + @RequiresEdt + fun setQueued(active: Boolean, onDelete: () -> Unit) { + if (role != SessionUiStyle.View.Message.USER_ROLE) return + wrap?.setQueued(active, onDelete) + } + private val promptToolbar: MessageToolbar? get() = wrap?.bar @@ -494,21 +508,26 @@ class MessageView( private inner class PromptWrap( private val box: JPanel, ) : JPanel(BorderLayout()), SessionCopyTarget { + private val footer = JPanel(BorderLayout()).also { it.isOpaque = false } val bar = MessageToolbar( { prompt?.copyMarkdown(trim = false) }, revert?.let { fn -> { fn(msg.info.id) } }, ) private val placeholder = bar.placeholder() - private var progress: RevertProgress? = null private var reverting = false + private var progress: RevertProgress? = null + private var queuedRow: JPanel? = null + private var queued = false override val copyAnchor: JComponent get() = placeholder - override val copyToolbar: JComponent? get() = if (reverting) null else bar + override val copyToolbar: JComponent? get() = if (reverting || queued) null else bar init { isOpaque = false add(box, BorderLayout.CENTER) - add(placeholder.align(HAlign.RIGHT, VAlign.TOP), BorderLayout.SOUTH) + footer.border = JBUI.Borders.emptyTop(UiStyle.Gap.xs()) + footer.add(placeholder.align(HAlign.RIGHT, VAlign.TOP), BorderLayout.CENTER) + add(footer, BorderLayout.SOUTH) } override fun copyText(): String? = prompt?.copyMarkdown(trim = false) @@ -523,19 +542,54 @@ class MessageView( node.setText(text) if (reverting) return reverting = true - remove((layout as BorderLayout).getLayoutComponent(BorderLayout.SOUTH)) - add(node.align(HAlign.LEFT, VAlign.TOP), BorderLayout.SOUTH) + swapFooter(node.align(HAlign.LEFT, VAlign.TOP)) revalidate() repaint() return } if (!reverting) return reverting = false - remove((layout as BorderLayout).getLayoutComponent(BorderLayout.SOUTH)) - add(placeholder.align(HAlign.RIGHT, VAlign.TOP), BorderLayout.SOUTH) + swapFooter(placeholder.align(HAlign.RIGHT, VAlign.TOP)) + revalidate() + repaint() + } + + @RequiresEdt + fun setQueued(active: Boolean, onDelete: () -> Unit) { + if (active) { + val node = queuedRow ?: queue(onDelete).also { queuedRow = it } + if (queued) return + queued = true + swapFooter(node.align(HAlign.RIGHT, VAlign.TOP)) + revalidate() + repaint() + return + } + if (!queued) return + queued = false + swapFooter(placeholder.align(HAlign.RIGHT, VAlign.TOP)) revalidate() repaint() } + + private fun swapFooter(node: JComponent) { + footer.removeAll() + footer.add(node, BorderLayout.CENTER) + } + + private fun queue(onDelete: () -> Unit) = Stack.horizontal(UiStyle.Gap.sm()).also { row -> + row.isOpaque = false + row.next(JBLabel(KiloBundle.message("session.queued")).apply { + foreground = UIUtil.getContextHelpForeground() + }) + row.next(toolbarButton( + ToolbarButtonAction( + AllIcons.Actions.Close, + KiloBundle.message("session.queued.remove"), + onDelete, + ), + )) + } } private fun assistantBorder() = JBUI.Borders.empty() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt index b4d8684004f..cc3cc7d74c5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt @@ -36,6 +36,7 @@ class TurnView( private val repo: String? = null, private val hover: ((PartView, Boolean) -> Unit)? = null, private val revert: ((String) -> Unit)? = null, + private val deleteQueued: ((String) -> Unit)? = null, ) : SessionLayoutPanel(SessionUiStyle.SessionLayout.GAP), Disposable, SessionEditorStyleTarget, SessionView { private val messages = LinkedHashMap() @@ -66,11 +67,18 @@ class TurnView( val view = MessageView(msg, openFile, style, openUrl, selection, openAttachment, resize, repo, hover, revert) messages[msg.info.id] = view add(view) + if (msg.info.id == id && deleteQueued != null) view.setQueued(false) { deleteQueued.invoke(id) } syncCopyToolbars() revalidate() return view } + @RequiresEdt + fun setQueued(active: Boolean, onDelete: (String) -> Unit) { + val anchor = messages.values.firstOrNull { it.role == SessionUiStyle.View.Message.USER_ROLE } ?: return + anchor.setQueued(active) { onDelete(id) } + } + /** Remove the [MessageView] for [msgId] if present. */ fun removeMessage(msgId: String) { removeMessageChanged(msgId) diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 2557da487ff..6f26bb566d9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -31,6 +31,8 @@ session.copy.hover=Copy session.copy.prompt=Copy prompt session.copy.response=Copy response session.copy.copied=Copied +session.queued=Queued +session.queued.remove=Remove queued message session.drop.files.title=Drop files here session.drop.files.subtitle=to add them to the prompt session.file.missing=Couldn''t find ''{0}'' in this repository. diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt index b84cdb5da6f..05dd2e9a6e8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt @@ -102,6 +102,33 @@ class PromptLifecycleTest : SessionControllerTestBase() { assertFalse(message.properties.containsValue("git-changes")) } + fun `test session queue changed updates queued set`() { + val (c, _, modelEvents) = prompted() + + emit(ChatEventDto.SessionQueueChanged("ses_test", listOf("u2"))) + + assertEquals(setOf("u2"), c.model.queued) + assertModelEvents( + """ + QueueChanged [u2] + """, + modelEvents, + ) + + emit(ChatEventDto.SessionQueueChanged("ses_test", emptyList())) + + assertEquals(emptySet(), c.model.queued) + } + + fun `test delete queued message delegates to RPC`() { + val (c, _, _) = prompted() + + edt { c.deleteQueuedMessage("u2") } + flush() + + assertEquals(listOf(ai.kilocode.client.testing.FakeSessionRpcApi.MessageDeleteCall("ses_test", "/test", "u2")), rpc.messageDeletes) + } + fun `test PermissionAsked moves state to AwaitingPermission`() { val (m, _, _) = prompted() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt index 70a6dc52bba..745062e15f5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt @@ -1056,7 +1056,7 @@ class PromptPanelTest : BasePlatformTestCase() { assertTrue(resource("/icons/send_dark.svg").contains("fill=\"#0A7BD8\"")) } - fun `test busy disables send button`() { + fun `test busy allows sending draft`() { val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }) panel.setReady(true) ApplicationManager.getApplication().invokeAndWait { panel.setText("hello") } @@ -1065,8 +1065,9 @@ class PromptPanelTest : BasePlatformTestCase() { panel.setBusy(true) - assertFalse(panel.isSendEnabled) + assertTrue(panel.isSendEnabled) assertTrue(panel.isStopEnabled) + assertNotSame(AllIcons.Actions.Suspend, panel.buttonForTest().icon) } fun `test auto approve button toggles and updates tooltip`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt index d5e72cfc081..b27219697a3 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt @@ -28,6 +28,7 @@ import ai.kilocode.client.session.views.tool.TaskToolView import ai.kilocode.client.session.views.tool.ToolView import ai.kilocode.client.session.views.todo.TodoWriteView import ai.kilocode.client.ui.DiffStatBadge +import ai.kilocode.client.ui.HoverIcon import ai.kilocode.client.ui.layout.Stack import ai.kilocode.rpc.dto.DiffFileDto import ai.kilocode.rpc.dto.MessageDto @@ -129,6 +130,32 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { ) } + fun `test queued turn shows badge and remove action`() { + var deleted: String? = null + Disposer.dispose(parent) + parent = Disposer.newDisposable("test-queued") + model = SessionModel() + panel = SessionMessageListPanel(model, parent, openFile = openFile, deleteQueued = { deleted = it }) + model.upsertMessage(msg("u1", "user")) + model.updateContent("u1", part("p1", "u1", "text", text = "first")) + model.upsertMessage(msg("u2", "user")) + model.updateContent("u2", part("p2", "u2", "text", text = "second")) + + model.setQueued(setOf("u2")) + + val u1 = panel.findMessage("u1")!! + val u2 = panel.findMessage("u2")!! + assertFalse(components(u1).filterIsInstance().any { it.text == KiloBundle.message("session.queued") }) + assertTrue(components(u2).filterIsInstance().any { it.text == KiloBundle.message("session.queued") }) + + val remove = components(u2).filterIsInstance().single() + assertEquals(KiloBundle.message("session.queued.remove"), remove.toolTipText) + assertEquals(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR), remove.cursor) + remove.doClick() + + assertEquals("u2", deleted) + } + // ------ TurnAdded ------ fun `test user message creates turn and is findable by message id`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt index cb14e09f07c..65f282db4e5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt @@ -96,6 +96,8 @@ class FakeSessionRpcApi : KiloSessionRpcApi { val aborts = mutableListOf>() val compacts = mutableListOf>() val reverts = mutableListOf() + val messageDeletes = mutableListOf() + var messageDeleteResult = true val unreverts = mutableListOf>() val configs = mutableListOf>() val permissionReplies = mutableListOf>() @@ -117,6 +119,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi { data class AttachmentCall(val id: String, val directory: String, val messageId: String, val partId: String, val attachmentKey: String?) data class CommandCall(val id: String, val directory: String, val command: String, val arguments: String, val prompt: PromptDto) data class RevertCall(val id: String, val directory: String, val message: String, val part: String?) + data class MessageDeleteCall(val id: String, val directory: String, val message: String) // --- Implementation --- @@ -229,6 +232,12 @@ class FakeSessionRpcApi : KiloSessionRpcApi { reverts.add(RevertCall(id, directory, messageID, partID)) } + override suspend fun deleteMessage(id: String, directory: String, messageID: String): Boolean { + assertNotEdt("deleteMessage") + messageDeletes.add(MessageDeleteCall(id, directory, messageID)) + return messageDeleteResult + } + override suspend fun unrevert(id: String, directory: String) { assertNotEdt("unrevert") unrevertGate?.await() diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index 8c092f68212..fee84e7b2a4 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -8,7 +8,7 @@ "test": "./gradlew test", "test:ci": "bun script/test-ci.ts" }, - "version": "7.4.15", + "version": "7.4.16", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt index 4f8e06f1693..588c0c9d0e6 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/ChatLogSummary.kt @@ -33,6 +33,7 @@ object ChatLogSummary { is ChatEventDto.SessionStatusChanged -> event.sessionID is ChatEventDto.SessionUpdated -> event.sessionID is ChatEventDto.SessionIdle -> event.sessionID + is ChatEventDto.SessionQueueChanged -> event.sessionID is ChatEventDto.SessionCompacted -> event.sessionID is ChatEventDto.SessionDiffChanged -> event.sessionID is ChatEventDto.TodoUpdated -> event.sessionID @@ -211,6 +212,12 @@ object ChatLogSummary { "evt=session.idle", ) + is ChatEventDto.SessionQueueChanged -> join( + sid(event.sessionID), + "evt=session.queue.changed", + "queued=${event.queued.size}", + ) + is ChatEventDto.SessionCompacted -> join( sid(event.sessionID), "evt=session.compacted", diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt index 6b6cfa4f97f..0fab2e8696e 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt @@ -90,6 +90,9 @@ interface KiloSessionRpcApi : RemoteApi { /** Revert a session to a prior user message or part. */ suspend fun revert(id: String, directory: String, messageID: String, partID: String?) + /** Delete a single message (used to remove a queued prompt). */ + suspend fun deleteMessage(id: String, directory: String, messageID: String): Boolean + /** Redo all reverted changes for a session. */ suspend fun unrevert(id: String, directory: String) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index dbb053f8fd5..edbbed7b02b 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -254,6 +254,13 @@ sealed class ChatEventDto { val sessionID: String, ) : ChatEventDto() + @Serializable + @SerialName("session.queue.changed") + data class SessionQueueChanged( + val sessionID: String, + val queued: List = emptyList(), + ) : ChatEventDto() + @Serializable @SerialName("session.compacted") data class SessionCompacted( From 0178e3361b5df6ed138281bd98d9ded90b66f524 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 27 Jul 2026 12:14:33 -0400 Subject: [PATCH 2/3] fix(jetbrains): handle queued prompt deletion misses --- .../session/controller/SessionController.kt | 12 ++++++++++-- .../client/session/views/MessageToolbar.kt | 17 +---------------- .../kilocode/client/session/views/TurnView.kt | 1 - .../session/controller/PromptLifecycleTest.kt | 13 +++++++++++++ 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index ec3299f6b02..9b1e517ac6d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -467,11 +467,19 @@ class SessionController( fun deleteQueuedMessage(message: String) { assertEdt() val id = sid ?: return - capture("Conversation Queued Message Removed", sessionProps(id)) cs.launch { try { - sessions.deleteMessage(id, directory, message) + val ok = sessions.deleteMessage(id, directory, message) + if (!ok) { + capture("Session Error", sessionProps(id) + mapOf("context" to "delete-message", "errorClass" to "DeleteMiss")) + LOG.warn("${ChatLogSummary.sid(id)} kind=deleteMessage missed message=$message") + return@launch + } + capture("Conversation Queued Message Removed", sessionProps(id)) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { + capture("Session Error", sessionProps(id) + mapOf("context" to "delete-message", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(id)} kind=deleteMessage failed message=${e.message}", e) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt index 1df7642dc3d..4ffeee14468 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageToolbar.kt @@ -33,7 +33,6 @@ internal class MessageToolbar( buttons.forEach { next(it) } next(button) } - private var custom: JComponent? = null init { isOpaque = false @@ -42,12 +41,10 @@ internal class MessageToolbar( @RequiresEdt fun sync(value: Boolean) { - val controls = customButtons() - if (isVisible == value && button.isEnabled == value && controls.all { it.isEnabled == value }) return + if (isVisible == value && button.isEnabled == value) return isVisible = value button.isEnabled = value buttons.forEach { it.isEnabled = value } - controls.forEach { it.isEnabled = value } revalidate() repaint() } @@ -63,16 +60,6 @@ internal class MessageToolbar( @RequiresEdt fun copyButton() = button - @RequiresEdt - fun setCustom(node: JComponent?) { - if (custom === node) return - remove(custom ?: row) - custom = node - add(node ?: row) - revalidate() - repaint() - } - fun placeholder(): JComponent = object : JPanel() { init { isOpaque = false @@ -91,6 +78,4 @@ internal class MessageToolbar( copy.dismiss() super.removeNotify() } - - private fun customButtons() = custom?.components?.filterIsInstance().orEmpty() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt index cc3cc7d74c5..8723fc4ce5d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt @@ -67,7 +67,6 @@ class TurnView( val view = MessageView(msg, openFile, style, openUrl, selection, openAttachment, resize, repo, hover, revert) messages[msg.info.id] = view add(view) - if (msg.info.id == id && deleteQueued != null) view.setQueued(false) { deleteQueued.invoke(id) } syncCopyToolbars() revalidate() return view diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt index 05dd2e9a6e8..9d5e4225e15 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt @@ -127,6 +127,19 @@ class PromptLifecycleTest : SessionControllerTestBase() { flush() assertEquals(listOf(ai.kilocode.client.testing.FakeSessionRpcApi.MessageDeleteCall("ses_test", "/test", "u2")), rpc.messageDeletes) + assertTrue(appRpc.telemetry.any { it.event == "Conversation Queued Message Removed" }) + } + + fun `test delete queued message miss captures error`() { + val (c, _, _) = prompted() + rpc.messageDeleteResult = false + + edt { c.deleteQueuedMessage("u2") } + flush() + + assertEquals(listOf(ai.kilocode.client.testing.FakeSessionRpcApi.MessageDeleteCall("ses_test", "/test", "u2")), rpc.messageDeletes) + assertFalse(appRpc.telemetry.any { it.event == "Conversation Queued Message Removed" }) + assertTrue(appRpc.telemetry.any { it.event == "Session Error" && it.properties["context"] == "delete-message" }) } fun `test PermissionAsked moves state to AwaitingPermission`() { From da6d20a32444e71d121cbb6eed90935439b906f8 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 27 Jul 2026 12:31:41 -0400 Subject: [PATCH 3/3] fix(jetbrains): localize queued prompt labels --- .../src/main/resources/messages/KiloBundle_ar.properties | 2 ++ .../src/main/resources/messages/KiloBundle_bs.properties | 2 ++ .../src/main/resources/messages/KiloBundle_da.properties | 2 ++ .../src/main/resources/messages/KiloBundle_de.properties | 2 ++ .../src/main/resources/messages/KiloBundle_es.properties | 2 ++ .../src/main/resources/messages/KiloBundle_fr.properties | 2 ++ .../src/main/resources/messages/KiloBundle_ja.properties | 2 ++ .../src/main/resources/messages/KiloBundle_ko.properties | 2 ++ .../src/main/resources/messages/KiloBundle_nl.properties | 2 ++ .../src/main/resources/messages/KiloBundle_no.properties | 2 ++ .../src/main/resources/messages/KiloBundle_pl.properties | 2 ++ .../src/main/resources/messages/KiloBundle_pt_BR.properties | 2 ++ .../src/main/resources/messages/KiloBundle_ru.properties | 2 ++ .../src/main/resources/messages/KiloBundle_th.properties | 2 ++ .../src/main/resources/messages/KiloBundle_tr.properties | 2 ++ .../src/main/resources/messages/KiloBundle_uk.properties | 2 ++ .../src/main/resources/messages/KiloBundle_zh_CN.properties | 2 ++ .../src/main/resources/messages/KiloBundle_zh_TW.properties | 2 ++ 18 files changed, 36 insertions(+) diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index f439e041589..999dcb7c540 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -23,6 +23,8 @@ session.copy.hover=نسخ session.copy.prompt=نسخ الموجه session.copy.response=نسخ الرد session.copy.copied=تم النسخ +session.queued=في قائمة الانتظار +session.queued.remove=إزالة الرسالة من قائمة الانتظار session.tab.new=جلسة جديدة session.tab.untitled=جلسة بدون عنوان diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index 3c3fe1f71a9..fb61208a23c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -23,6 +23,8 @@ session.copy.hover=Kopiraj session.copy.prompt=Kopiraj prompt session.copy.response=Kopiraj odgovor session.copy.copied=Kopirano +session.queued=U redu čekanja +session.queued.remove=Ukloni poruku iz reda čekanja session.tab.new=Nova sesija session.tab.untitled=Sesija bez naslova diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index 8d0d26a7223..a785788ba07 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -23,6 +23,8 @@ session.copy.hover=Kopiér session.copy.prompt=Kopiér prompt session.copy.response=Kopiér svar session.copy.copied=Kopieret +session.queued=I kø +session.queued.remove=Fjern besked fra køen session.tab.new=Ny session session.tab.untitled=Unavngivet session diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index 181ac9f1654..837e4d8dd70 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -23,6 +23,8 @@ session.copy.hover=Kopieren session.copy.prompt=Prompt kopieren session.copy.response=Antwort kopieren session.copy.copied=Kopiert +session.queued=In Warteschlange +session.queued.remove=Nachricht aus Warteschlange entfernen session.tab.new=Neue Sitzung session.tab.untitled=Unbenannte Sitzung diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index fb67a0de551..25279f34921 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -23,6 +23,8 @@ session.copy.hover=Copiar session.copy.prompt=Copiar prompt session.copy.response=Copiar respuesta session.copy.copied=Copiado +session.queued=En cola +session.queued.remove=Eliminar mensaje en cola session.tab.new=Nueva sesión session.tab.untitled=Sesión sin título diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index 3848ae7ee98..3c279709d25 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -23,6 +23,8 @@ session.copy.hover=Copier session.copy.prompt=Copier le prompt session.copy.response=Copier la réponse session.copy.copied=Copié +session.queued=En attente +session.queued.remove=Supprimer le message en attente session.tab.new=Nouvelle session session.tab.untitled=Session sans titre diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index 8c9e3d12c06..9d853d2a796 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -23,6 +23,8 @@ session.copy.hover=コピー session.copy.prompt=プロンプトをコピー session.copy.response=応答をコピー session.copy.copied=コピーしました +session.queued=キュー済み +session.queued.remove=キュー済みメッセージを削除 session.tab.new=新しいセッション session.tab.untitled=名前なしのセッション diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index b463a9b1951..9b1797052c7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -23,6 +23,8 @@ session.copy.hover=복사 session.copy.prompt=프롬프트 복사 session.copy.response=응답 복사 session.copy.copied=복사됨 +session.queued=대기 중 +session.queued.remove=대기 중인 메시지 제거 session.tab.new=새 세션 session.tab.untitled=제목 없는 세션 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index b29e1b9dc21..85fc3f6b31c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -23,6 +23,8 @@ session.copy.hover=Kopiëren session.copy.prompt=Prompt kopiëren session.copy.response=Antwoord kopiëren session.copy.copied=Gekopieerd +session.queued=In wachtrij +session.queued.remove=Bericht uit wachtrij verwijderen session.tab.new=Nieuwe sessie session.tab.untitled=Naamloze sessie diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index c577c420520..c160fdf4000 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -23,6 +23,8 @@ session.copy.hover=Kopier session.copy.prompt=Kopier prompt session.copy.response=Kopier svar session.copy.copied=Kopiert +session.queued=I kø +session.queued.remove=Fjern melding fra køen session.tab.new=Ny økt session.tab.untitled=Uten tittel diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index d57e58ac718..db9776e10b5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -23,6 +23,8 @@ session.copy.hover=Kopiuj session.copy.prompt=Kopiuj prompt session.copy.response=Kopiuj odpowiedź session.copy.copied=Skopiowano +session.queued=W kolejce +session.queued.remove=Usuń wiadomość z kolejki session.tab.new=Nowa sesja session.tab.untitled=Sesja bez tytułu diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 6655ce84b0f..35dda74bfed 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -23,6 +23,8 @@ session.copy.hover=Copiar session.copy.prompt=Copiar prompt session.copy.response=Copiar resposta session.copy.copied=Copiado +session.queued=Na fila +session.queued.remove=Remover mensagem da fila session.tab.new=Nova sessão session.tab.untitled=Sessão sem título diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index c1eaee006ad..3d53c6262d6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -23,6 +23,8 @@ session.copy.hover=Копировать session.copy.prompt=Скопировать промпт session.copy.response=Скопировать ответ session.copy.copied=Скопировано +session.queued=В очереди +session.queued.remove=Удалить сообщение из очереди session.tab.new=Новая сессия session.tab.untitled=Незаголовок сессия diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index 89e6d9a8df9..3b400b23d01 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -23,6 +23,8 @@ session.copy.hover=คัดลอก session.copy.prompt=คัดลอกพรอมต์ session.copy.response=คัดลอกคำตอบ session.copy.copied=คัดลอกแล้ว +session.queued=อยู่ในคิว +session.queued.remove=ลบข้อความในคิว session.tab.new=เซสชันใหม่ session.tab.untitled=เซสชันไม่มีชื่อ diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index d6ee1e84d97..8b7a2de3cda 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -23,6 +23,8 @@ session.copy.hover=Kopyala session.copy.prompt=Promptu kopyala session.copy.response=Yanıtı kopyala session.copy.copied=Kopyalandı +session.queued=Kuyrukta +session.queued.remove=Kuyruktaki mesajı kaldır session.tab.new=Yeni oturum session.tab.untitled=Başlıksız oturum diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index f385d236f4e..896ee37e7ad 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -23,6 +23,8 @@ session.copy.hover=Копіювати session.copy.prompt=Скопіювати промпт session.copy.response=Скопіювати відповідь session.copy.copied=Скопійовано +session.queued=У черзі +session.queued.remove=Видалити повідомлення з черги session.tab.new=Нова сесія session.tab.untitled=Сесія без назви diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index 3f4b3fbc3db..864bce7a28c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -23,6 +23,8 @@ session.copy.hover=复制 session.copy.prompt=复制提示词 session.copy.response=复制回复 session.copy.copied=已复制 +session.queued=已排队 +session.queued.remove=移除排队消息 session.tab.new=新建会话 session.tab.untitled=无标题会话 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index dd2898d09a8..48c5418ae78 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -23,6 +23,8 @@ session.copy.hover=複製 session.copy.prompt=複製提示詞 session.copy.response=複製回覆 session.copy.copied=已複製 +session.queued=已排入佇列 +session.queued.remove=移除佇列中的訊息 session.tab.new=新建工作階段 session.tab.untitled=未命名的工作階段