mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 04:46:43 +08:00
Merge pull request #12571 from Kilo-Org/developing-liver
feat(jetbrains): support queued prompts
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Allow sending prompts while a session is busy and show queued prompts with a remove action.
|
||||
+27
-4
@@ -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")
|
||||
|
||||
+6
@@ -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)
|
||||
|
||||
+3
@@ -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)) }
|
||||
|
||||
|
||||
+43
@@ -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<SseEvent>(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)
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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("""
|
||||
|
||||
+7
@@ -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
|
||||
|
||||
+3
@@ -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) }
|
||||
}
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+27
-1
@@ -464,6 +464,27 @@ class SessionController(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteQueuedMessage(message: String) {
|
||||
assertEdt()
|
||||
val id = sid ?: return
|
||||
cs.launch {
|
||||
try {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unrevert() {
|
||||
assertEdt()
|
||||
val id = sid ?: return
|
||||
@@ -1368,6 +1389,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 +1429,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 +1452,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 +2337,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
|
||||
|
||||
+15
@@ -74,6 +74,9 @@ class SessionModel {
|
||||
|
||||
private var revert: SessionRevertDto? = null
|
||||
|
||||
var queued: Set<String> = 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<String>) {
|
||||
if (queued == ids) return
|
||||
queued = ids
|
||||
fire(SessionModelEvent.QueueChanged(ids))
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun setDiff(diff: List<DiffFileDto>) {
|
||||
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()
|
||||
|
||||
+3
@@ -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<String>) : SessionModelEvent() {
|
||||
override fun toString() = "QueueChanged [${queued.sorted().joinToString(", ")}]"
|
||||
}
|
||||
data class HeaderUpdated(val header: SessionHeaderSnapshot) : SessionModelEvent() {
|
||||
override fun toString() = "HeaderUpdated visible=${header.visible}"
|
||||
}
|
||||
|
||||
+23
-3
@@ -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.
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+16
-9
@@ -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) + "<br>" +
|
||||
XmlStringUtil.escapeString(KiloBundle.message("prompt.button.send.tooltip.stop", stop))
|
||||
XmlStringUtil.escapeString(KiloBundle.message("prompt.button.send.tooltip.stop", shortcut))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+61
-7
@@ -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()
|
||||
|
||||
+7
@@ -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<String, MessageView>()
|
||||
@@ -71,6 +72,12 @@ class TurnView(
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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=جلسة بدون عنوان
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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=名前なしのセッション
|
||||
|
||||
|
||||
@@ -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=제목 없는 세션
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
|
||||
@@ -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=Незаголовок сессия
|
||||
|
||||
|
||||
@@ -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=เซสชันไม่มีชื่อ
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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=Сесія без назви
|
||||
|
||||
|
||||
+2
@@ -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=无标题会话
|
||||
|
||||
|
||||
+2
@@ -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=未命名的工作階段
|
||||
|
||||
|
||||
+40
@@ -102,6 +102,46 @@ 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<String>(), 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)
|
||||
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`() {
|
||||
val (m, _, _) = prompted()
|
||||
|
||||
|
||||
+3
-2
@@ -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`() {
|
||||
|
||||
+27
@@ -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<JBLabel>().any { it.text == KiloBundle.message("session.queued") })
|
||||
assertTrue(components(u2).filterIsInstance<JBLabel>().any { it.text == KiloBundle.message("session.queued") })
|
||||
|
||||
val remove = components(u2).filterIsInstance<HoverIcon>().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`() {
|
||||
|
||||
+9
@@ -96,6 +96,8 @@ class FakeSessionRpcApi : KiloSessionRpcApi {
|
||||
val aborts = mutableListOf<Pair<String, String>>()
|
||||
val compacts = mutableListOf<Triple<String, String, ModelSelectionDto>>()
|
||||
val reverts = mutableListOf<RevertCall>()
|
||||
val messageDeletes = mutableListOf<MessageDeleteCall>()
|
||||
var messageDeleteResult = true
|
||||
val unreverts = mutableListOf<Pair<String, String>>()
|
||||
val configs = mutableListOf<Pair<String, ConfigUpdateDto>>()
|
||||
val permissionReplies = mutableListOf<Triple<String, String, PermissionReplyDto>>()
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -90,6 +90,9 @@ interface KiloSessionRpcApi : RemoteApi<Unit> {
|
||||
/** 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)
|
||||
|
||||
|
||||
@@ -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<String> = emptyList(),
|
||||
) : ChatEventDto()
|
||||
|
||||
@Serializable
|
||||
@SerialName("session.compacted")
|
||||
data class SessionCompacted(
|
||||
|
||||
Reference in New Issue
Block a user