mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(jetbrains): preserve collapsed subagent views
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show subagent tool activity inline in JetBrains session transcripts.
|
||||
+35
-3
@@ -894,7 +894,7 @@ class SessionController(
|
||||
val job = cs.launch {
|
||||
try {
|
||||
sessions.events(child, directory).collect { event ->
|
||||
if (!isChildPermissionEvent(event, child)) return@collect
|
||||
if (!isChildEvent(event, child)) return@collect
|
||||
LOG.debug { "${ChatLogSummary.sid(sid ?: "pending")} kind=child-event child=$child ${ChatLogSummary.eventBody(event)}" }
|
||||
updates.enqueue(event)
|
||||
}
|
||||
@@ -914,6 +914,7 @@ class SessionController(
|
||||
assertEdt()
|
||||
if (!childIds.add(child)) return
|
||||
subscribeChild(child)
|
||||
cs.launch { seedChild(child) }
|
||||
cs.launch { recoverChildPermissions(child) }
|
||||
}
|
||||
|
||||
@@ -948,6 +949,27 @@ class SessionController(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun seedChild(child: String) {
|
||||
try {
|
||||
val items = sessions.messages(child, directory)
|
||||
runEdt {
|
||||
if (disposed) return@runEdt
|
||||
updateModel {
|
||||
for (msg in items) {
|
||||
if (msg.info.role != "assistant") continue
|
||||
for (part in msg.parts) {
|
||||
if (part.type == "tool") model.upsertChildTool(child, part, replace = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("${ChatLogSummary.sid(sid ?: "pending")} kind=child-history child=$child dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** Rehydrate pending permissions/questions and current session status after history load. */
|
||||
private suspend fun recoverPending(id: String) {
|
||||
try {
|
||||
@@ -1026,6 +1048,10 @@ class SessionController(
|
||||
}
|
||||
|
||||
is ChatEventDto.PartUpdated -> {
|
||||
if (childIds.contains(event.sessionID)) {
|
||||
if (event.part.type == "tool") model.upsertChildTool(event.sessionID, event.part)
|
||||
return
|
||||
}
|
||||
partType = event.part.type
|
||||
tool = event.part.tool
|
||||
val key = PartKey(event.part.messageID, event.part.id)
|
||||
@@ -1051,6 +1077,10 @@ class SessionController(
|
||||
}
|
||||
|
||||
is ChatEventDto.PartRemoved -> {
|
||||
if (childIds.contains(event.sessionID)) {
|
||||
model.removeChildTool(event.sessionID, event.partID)
|
||||
return
|
||||
}
|
||||
snapshots.remove(PartKey(event.messageID, event.partID))
|
||||
model.removeContent(event.messageID, event.partID)
|
||||
}
|
||||
@@ -1918,8 +1948,10 @@ private fun childID(part: PartDto): String? {
|
||||
return part.metadata["sessionId"]
|
||||
}
|
||||
|
||||
/** Returns true when [event] is a permission event for [child] (used by child subscriptions). */
|
||||
private fun isChildPermissionEvent(event: ChatEventDto, child: String): Boolean = when (event) {
|
||||
/** Returns true when [event] should be routed from a child subscription. */
|
||||
private fun isChildEvent(event: ChatEventDto, child: String): Boolean = when (event) {
|
||||
is ChatEventDto.PartUpdated -> event.sessionID == child
|
||||
is ChatEventDto.PartRemoved -> event.sessionID == child
|
||||
is ChatEventDto.PermissionAsked -> event.sessionID == child
|
||||
is ChatEventDto.PermissionReplied -> event.sessionID == child
|
||||
else -> false
|
||||
|
||||
+2
@@ -82,6 +82,8 @@ class Tool(id: String, val name: String, var kind: ToolKind) : Content(id) {
|
||||
var title: String? = null
|
||||
var input: Map<String, String> = emptyMap()
|
||||
var metadata: Map<String, String> = emptyMap()
|
||||
var childSessionId: String? = null
|
||||
var childTools: List<Tool> = emptyList()
|
||||
var output: String? = null
|
||||
var error: String? = null
|
||||
var time: PartTimeDto? = null
|
||||
|
||||
+68
-2
@@ -47,6 +47,8 @@ class SessionModel {
|
||||
private val entries = LinkedHashMap<String, Message>()
|
||||
private val turnEntries = LinkedHashMap<String, Turn>()
|
||||
private val hiddenText = mutableSetOf<Pair<String, String>>()
|
||||
private val childRefs = HashMap<String, ChildRef>()
|
||||
private val childTools = HashMap<String, LinkedHashMap<String, Tool>>()
|
||||
|
||||
var app: KiloAppStateDto = KiloAppStateDto(KiloAppStatusDto.DISCONNECTED)
|
||||
var version: String? = null
|
||||
@@ -145,7 +147,8 @@ class SessionModel {
|
||||
|
||||
@RequiresEdt
|
||||
fun removeMessage(id: String) {
|
||||
if (entries.remove(id) == null) return
|
||||
val msg = entries.remove(id) ?: return
|
||||
for (part in msg.parts.values) untrackChild(part)
|
||||
hiddenText.removeAll { it.first == id }
|
||||
fire(SessionModelEvent.MessageRemoved(id))
|
||||
regroup()
|
||||
@@ -156,7 +159,8 @@ class SessionModel {
|
||||
fun removeContent(messageId: String, contentId: String) {
|
||||
hiddenText.remove(messageId to contentId)
|
||||
val msg = entries[messageId] ?: return
|
||||
if (msg.parts.remove(contentId) == null) return
|
||||
val old = msg.parts.remove(contentId) ?: return
|
||||
untrackChild(old)
|
||||
fire(SessionModelEvent.ContentRemoved(messageId, contentId))
|
||||
updateHeader()
|
||||
}
|
||||
@@ -186,10 +190,38 @@ class SessionModel {
|
||||
}
|
||||
val content = fromDto(dto)
|
||||
msg.parts[dto.id] = content
|
||||
trackChild(messageId, content)
|
||||
fire(SessionModelEvent.ContentAdded(messageId, content))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun upsertChildTool(child: String, dto: PartDto, replace: Boolean = true) {
|
||||
if (dto.type != "tool") return
|
||||
val ref = childRefs[child] ?: return
|
||||
val msg = entries[ref.messageId] ?: return
|
||||
val parent = msg.parts[ref.partId] as? Tool ?: return
|
||||
val tool = fromDto(dto) as? Tool ?: return
|
||||
val tools = childTools.getOrPut(child) { LinkedHashMap() }
|
||||
if (!replace && tools.containsKey(dto.id)) return
|
||||
tools[dto.id] = tool
|
||||
parent.childTools = tools.values.toList()
|
||||
fire(SessionModelEvent.ContentUpdated(ref.messageId, parent))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun removeChildTool(child: String, partId: String) {
|
||||
val ref = childRefs[child] ?: return
|
||||
val tools = childTools[child] ?: return
|
||||
if (tools.remove(partId) == null) return
|
||||
val msg = entries[ref.messageId] ?: return
|
||||
val parent = msg.parts[ref.partId] as? Tool ?: return
|
||||
parent.childTools = tools.values.toList()
|
||||
fire(SessionModelEvent.ContentUpdated(ref.messageId, parent))
|
||||
updateHeader()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun appendDelta(messageId: String, contentId: String, delta: String) {
|
||||
val msg = entries[messageId] ?: return
|
||||
@@ -257,6 +289,8 @@ class SessionModel {
|
||||
@RequiresEdt
|
||||
fun loadHistory(history: List<MessageWithPartsDto>) {
|
||||
entries.clear()
|
||||
childRefs.clear()
|
||||
childTools.clear()
|
||||
hiddenText.clear()
|
||||
session = null
|
||||
state = SessionState.Idle
|
||||
@@ -274,6 +308,7 @@ class SessionModel {
|
||||
if (empty(part)) continue
|
||||
val content = fromDto(part, part.text)
|
||||
item.parts[content.id] = content
|
||||
trackChild(msg.info.id, content)
|
||||
}
|
||||
entries[msg.info.id] = item
|
||||
}
|
||||
@@ -286,6 +321,8 @@ class SessionModel {
|
||||
fun clear() {
|
||||
entries.clear()
|
||||
turnEntries.clear()
|
||||
childRefs.clear()
|
||||
childTools.clear()
|
||||
hiddenText.clear()
|
||||
session = null
|
||||
state = SessionState.Idle
|
||||
@@ -410,17 +447,24 @@ class SessionModel {
|
||||
existing.source = dto.source
|
||||
}
|
||||
is Tool -> {
|
||||
val old = existing.childSessionId
|
||||
existing.kind = toolKind(dto.tool)
|
||||
existing.state = parseToolState(dto.state)
|
||||
existing.callId = dto.callID
|
||||
existing.title = dto.title
|
||||
existing.input = dto.input
|
||||
existing.metadata = dto.metadata
|
||||
existing.childSessionId = childID(existing)
|
||||
if (old != null && old != existing.childSessionId) {
|
||||
childRefs.remove(old)
|
||||
childTools.remove(old)
|
||||
}
|
||||
existing.output = dto.output
|
||||
existing.error = dto.error
|
||||
existing.time = dto.time
|
||||
existing.todos = dto.todos
|
||||
existing.todoView = dto.todoView
|
||||
trackChild(messageId, existing)
|
||||
}
|
||||
is Compaction -> return
|
||||
is StepFinish -> {
|
||||
@@ -461,6 +505,7 @@ class SessionModel {
|
||||
title = dto.title
|
||||
input = dto.input
|
||||
metadata = dto.metadata
|
||||
childSessionId = childID(this)
|
||||
output = dto.output
|
||||
error = dto.error
|
||||
time = dto.time
|
||||
@@ -481,6 +526,20 @@ class SessionModel {
|
||||
for (l in listeners) l.onEvent(event)
|
||||
}
|
||||
|
||||
private fun trackChild(messageId: String, content: Content) {
|
||||
val tool = content as? Tool ?: return
|
||||
val child = tool.childSessionId ?: return
|
||||
childRefs[child] = ChildRef(messageId, tool.id)
|
||||
tool.childTools = childTools[child]?.values?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
private fun untrackChild(content: Content) {
|
||||
val tool = content as? Tool ?: return
|
||||
val child = tool.childSessionId ?: return
|
||||
childRefs.remove(child)
|
||||
childTools.remove(child)
|
||||
}
|
||||
|
||||
private fun updateHeader() {
|
||||
val next = buildHeader()
|
||||
if (next == header) return
|
||||
@@ -599,6 +658,13 @@ private fun parseToolState(raw: String?): ToolExecState = when (raw) {
|
||||
else -> ToolExecState.PENDING
|
||||
}
|
||||
|
||||
private data class ChildRef(val messageId: String, val partId: String)
|
||||
|
||||
private fun childID(tool: Tool): String? {
|
||||
if (tool.name != "task") return null
|
||||
return tool.metadata["sessionId"]
|
||||
}
|
||||
|
||||
data class AgentItem(
|
||||
val name: String,
|
||||
val display: String,
|
||||
|
||||
+1
@@ -165,6 +165,7 @@ object SessionUiStyle {
|
||||
/** Tool session-view preview limits and state colors. */
|
||||
object Tool {
|
||||
const val BODY_LINES = 15
|
||||
const val TASK_LINES = 10
|
||||
const val PREVIEW_LIMIT = 20_000
|
||||
|
||||
fun pending(): Color = UiStyle.Colors.weak()
|
||||
|
||||
+4
@@ -8,6 +8,7 @@ import ai.kilocode.client.session.views.tool.GlobToolView
|
||||
import ai.kilocode.client.session.views.tool.ReadToolView
|
||||
import ai.kilocode.client.session.views.tool.SearchToolView
|
||||
import ai.kilocode.client.session.views.tool.ShellToolView
|
||||
import ai.kilocode.client.session.views.tool.TaskToolView
|
||||
import ai.kilocode.client.session.views.tool.ToolView
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.model.Compaction
|
||||
@@ -53,6 +54,7 @@ object ViewFactory {
|
||||
GlobToolView.canRender(content) -> GlobToolView(content, selection = selection, repo = repo)
|
||||
SearchToolView.canRender(content) -> SearchToolView(content, selection = selection, repo = repo)
|
||||
ReadToolView.canRender(content) -> ReadToolView(content, openFile, selection = selection)
|
||||
TaskToolView.canRender(content) -> TaskToolView(content, selection = selection)
|
||||
else -> ToolView(content, selection = selection)
|
||||
}
|
||||
is Compaction -> CompactionView(content)
|
||||
@@ -98,6 +100,8 @@ object ViewFactory {
|
||||
if (view !is SearchToolView && SearchToolView.canRender(content)) return true
|
||||
if (view is ReadToolView) return !ReadToolView.canRender(content) || QuestionResultView.canRender(content)
|
||||
if (view is ToolView && ReadToolView.canRender(content)) return true
|
||||
if (view is TaskToolView) return !TaskToolView.canRender(content) || QuestionResultView.canRender(content)
|
||||
if (view !is TaskToolView && TaskToolView.canRender(content)) return true
|
||||
if (view is ToolView) return QuestionResultView.canRender(content)
|
||||
return false
|
||||
}
|
||||
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
package ai.kilocode.client.session.views.tool
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.model.Content
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.ui.selection.SessionSelection
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.StackAxis
|
||||
import com.intellij.openapi.actionSystem.DataSink
|
||||
import com.intellij.openapi.actionSystem.UiDataProvider
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBDimension
|
||||
import com.intellij.util.ui.JBUI
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.Scrollable
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.math.abs
|
||||
|
||||
class TaskToolView(
|
||||
tool: Tool,
|
||||
private val selection: SessionSelection? = null,
|
||||
private val parts: ToolParts = toolParts(tool),
|
||||
) : SecondarySessionPartView(parts.header, { TaskBody(parts.glyph).scroll }), UiDataProvider {
|
||||
|
||||
override val contentId: String = tool.id
|
||||
|
||||
private var item = tool
|
||||
private var style = SessionEditorStyle.current()
|
||||
private val rows = LinkedHashMap<String, Row>()
|
||||
private var following = false
|
||||
private var collapsed = false
|
||||
|
||||
init {
|
||||
bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot)
|
||||
applyStyle(style)
|
||||
sync()
|
||||
if (item.childTools.isNotEmpty()) expand()
|
||||
}
|
||||
|
||||
override fun uiDataSnapshot(sink: DataSink) {
|
||||
selection?.provideCopy(sink) { copyText() }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun update(content: Content) {
|
||||
if (content !is Tool) return
|
||||
val fresh = item.childTools.isEmpty() && content.childTools.isNotEmpty()
|
||||
item = content
|
||||
val follow = tailVisible()
|
||||
var changed = sync()
|
||||
changed = syncRows() || changed
|
||||
if (content.childTools.isNotEmpty() && !collapsed) changed = expand() || changed
|
||||
followTail(follow || fresh)
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun expand(): Boolean {
|
||||
collapsed = false
|
||||
val changed = super.expand()
|
||||
syncRows()
|
||||
return changed
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun collapse(): Boolean {
|
||||
if (item.childTools.isNotEmpty() && isExpanded()) collapsed = true
|
||||
return super.collapse()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
|
||||
@RequiresEdt
|
||||
fun rowCount(): Int = rows.size
|
||||
|
||||
@RequiresEdt
|
||||
fun rowLabels(): List<String> = rows.values.map { row -> row.text() }
|
||||
|
||||
@RequiresEdt
|
||||
fun bodyCreated(): Boolean = hasBody()
|
||||
|
||||
@RequiresEdt
|
||||
fun bodyVisible(): Boolean = isExpanded()
|
||||
|
||||
@RequiresEdt
|
||||
fun controlCount(): Int = if (arrow.isVisible) 1 else 0
|
||||
@RequiresEdt
|
||||
internal fun bodyMaxRows() = SessionUiStyle.View.Tool.TASK_LINES
|
||||
@RequiresEdt
|
||||
internal fun bodyScrollValue() = taskBodyOrNull()?.verticalScrollBar?.value ?: 0
|
||||
@RequiresEdt
|
||||
internal fun bodyScrollBottom() = taskBodyOrNull()?.let(::bottom) ?: 0
|
||||
@RequiresEdt
|
||||
internal fun setBodyScrollValue(value: Int) {
|
||||
taskBodyOrNull()?.verticalScrollBar?.value = value
|
||||
}
|
||||
@RequiresEdt
|
||||
internal fun horizontalPolicy() = taskBodyOrNull()?.horizontalScrollBarPolicy ?: ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
@RequiresEdt
|
||||
internal fun verticalPolicy() = taskBodyOrNull()?.verticalScrollBarPolicy ?: ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
|
||||
@RequiresEdt
|
||||
internal fun bodyInsets() = taskBody().panel.border.getBorderInsets(taskBody().panel)
|
||||
@RequiresEdt
|
||||
internal fun rowTitleColor(id: String) = rows[id]?.title?.foreground
|
||||
|
||||
@RequiresEdt
|
||||
override fun applyStyle(style: SessionEditorStyle) {
|
||||
this.style = style
|
||||
var changed = false
|
||||
changed = setFont(parts.title, style.boldEditorFont) || changed
|
||||
changed = setFont(parts.sub, style.smallEditorFont) || changed
|
||||
changed = setFont(parts.state, style.smallEditorFont) || changed
|
||||
for (row in rows.values) changed = row.applyStyle(style) || changed
|
||||
if (changed) refresh()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
if (!bodyVisible()) return size
|
||||
val height = row.preferredSize.height + bodyMaxHeight()
|
||||
return Dimension(size.width, minOf(size.height, height))
|
||||
}
|
||||
|
||||
private fun sync(): Boolean {
|
||||
var changed = false
|
||||
changed = syncExpandable(item.childTools.isNotEmpty()) || changed
|
||||
changed = setVisible(parts.state, item.childTools.isEmpty()) || changed
|
||||
changed = setIcon(parts.glyph, icon(item)) || changed
|
||||
changed = setForeground(parts.glyph, color(item)) || changed
|
||||
changed = setText(parts.title, agentTitle(item)) || changed
|
||||
changed = setText(parts.sub, summary(item)) || changed
|
||||
changed = setForeground(parts.title, titleColor(item)) || changed
|
||||
changed = setText(parts.state, stateText(item)) || changed
|
||||
changed = setForeground(parts.state, color(item)) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun syncRows(): Boolean {
|
||||
if (!hasBody()) return false
|
||||
val body = taskBody()
|
||||
var changed = false
|
||||
val ids = item.childTools.map { tool -> tool.id }.toSet()
|
||||
val stale = rows.keys.filter { id -> id !in ids }
|
||||
for (id in stale) {
|
||||
val row = rows.remove(id) ?: continue
|
||||
body.rows.remove(row.panel)
|
||||
changed = true
|
||||
}
|
||||
for (tool in item.childTools) {
|
||||
val row = rows[tool.id]
|
||||
if (row == null) {
|
||||
val next = Row(tool).also { it.applyStyle(style) }
|
||||
rows[tool.id] = next
|
||||
body.rows.next(next.panel)
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
changed = row.update(tool) || changed
|
||||
}
|
||||
if (changed) {
|
||||
body.rows.revalidate()
|
||||
body.rows.repaint()
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun taskBody() = bodyComponent() as TaskBodyScroll
|
||||
|
||||
private fun taskBodyOrNull() = if (hasBody()) bodyComponent() as? TaskBodyScroll else null
|
||||
|
||||
private fun bodyMaxHeight(): Int {
|
||||
val body = taskBodyOrNull() ?: return 0
|
||||
val height = rows.values.firstOrNull()?.panel?.getFontMetrics(style.smallEditorFont)?.height
|
||||
?: body.rows.getFontMetrics(style.smallEditorFont).height
|
||||
return height * bodyMaxRows() + JBUI.scale(SessionUiStyle.View.Layout.BODY_EXTRA_HEIGHT)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun tailVisible(): Boolean {
|
||||
if (!bodyVisible()) return false
|
||||
val scroll = taskBodyOrNull() ?: return false
|
||||
val bar = scroll.verticalScrollBar
|
||||
val bottom = bar.maximum - bar.visibleAmount
|
||||
return bottom > 0 && abs(bar.value - bottom) <= UiStyle.Gap.pad()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun followTail(follow: Boolean) {
|
||||
if (!follow || !bodyVisible() || following) return
|
||||
val scroll = taskBodyOrNull() ?: return
|
||||
following = true
|
||||
SwingUtilities.invokeLater { followPass(scroll, 4) }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun followPass(scroll: JBScrollPane, passes: Int) {
|
||||
if (!bodyVisible()) {
|
||||
following = false
|
||||
return
|
||||
}
|
||||
val view = scroll.viewport.view
|
||||
view?.setSize(scroll.viewport.extentSize.width.coerceAtLeast(1), view.preferredSize.height)
|
||||
view?.doLayout()
|
||||
scroll.viewport.doLayout()
|
||||
scroll.doLayout()
|
||||
scroll.viewport.viewPosition = Point(0, bottom(scroll))
|
||||
scroll.verticalScrollBar.value = bottom(scroll)
|
||||
if (passes <= 0 || scroll.verticalScrollBar.value == bottom(scroll)) {
|
||||
following = false
|
||||
return
|
||||
}
|
||||
SwingUtilities.invokeLater { followPass(scroll, passes - 1) }
|
||||
}
|
||||
|
||||
private fun bottom(scroll: JBScrollPane): Int {
|
||||
val view = scroll.viewport.view ?: return 0
|
||||
return maxOf(0, view.height - scroll.viewport.extentSize.height)
|
||||
}
|
||||
|
||||
private fun copyText(): String = buildString {
|
||||
append(agentTitle(item))
|
||||
val desc = item.input["description"].orEmpty()
|
||||
if (desc.isNotBlank()) append(" - ").append(desc)
|
||||
for (tool in item.childTools) {
|
||||
append('\n')
|
||||
append(title(tool))
|
||||
val sub = subtitle(tool)
|
||||
if (sub.isNotBlank()) append(' ').append(sub)
|
||||
}
|
||||
}
|
||||
|
||||
private class Row(tool: Tool) {
|
||||
private var item = tool
|
||||
val icon = JBLabel()
|
||||
val title = JBLabel()
|
||||
val sub = JBLabel().apply { foreground = UiStyle.Colors.weak() }
|
||||
val panel = JPanel(BorderLayout(UiStyle.Gap.md(), 0)).apply {
|
||||
isOpaque = false
|
||||
add(icon, BorderLayout.WEST)
|
||||
add(JPanel(BorderLayout(UiStyle.Gap.sm(), 0)).apply {
|
||||
isOpaque = false
|
||||
add(title, BorderLayout.WEST)
|
||||
add(sub, BorderLayout.CENTER)
|
||||
}, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun update(tool: Tool): Boolean {
|
||||
item = tool
|
||||
var changed = false
|
||||
changed = setIcon(icon, icon(tool)) || changed
|
||||
changed = setForeground(icon, color(tool)) || changed
|
||||
changed = setText(title, title(tool)) || changed
|
||||
changed = setForeground(title, rowTitleColor(tool)) || changed
|
||||
changed = setText(sub, subtitle(tool)) || changed
|
||||
return changed
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun applyStyle(style: SessionEditorStyle): Boolean {
|
||||
var changed = false
|
||||
changed = setFont(title, style.boldEditorFont) || changed
|
||||
changed = setFont(sub, style.smallEditorFont) || changed
|
||||
return update(item) || changed
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun text(): String = listOf(title.text, sub.text).filter { it.isNotBlank() }.joinToString(" ")
|
||||
}
|
||||
|
||||
override fun dumpLabel() = "TaskToolView#$contentId(${labelText()})"
|
||||
|
||||
companion object {
|
||||
fun canRender(content: Tool): Boolean = content.name == "task"
|
||||
}
|
||||
}
|
||||
|
||||
private class TaskBody(glyph: JBLabel) {
|
||||
val rows = TaskRows()
|
||||
val panel = JPanel(BorderLayout()).apply {
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
border = JBUI.Borders.empty(
|
||||
UiStyle.Gap.sm(),
|
||||
glyph.preferredSize.width + JBUI.scale(SessionUiStyle.View.Layout.GAP) + UiStyle.Gap.md(),
|
||||
UiStyle.Gap.sm(),
|
||||
UiStyle.Gap.md(),
|
||||
)
|
||||
add(rows, BorderLayout.CENTER)
|
||||
}
|
||||
val scroll = TaskBodyScroll(this)
|
||||
}
|
||||
|
||||
private class TaskBodyScroll(val body: TaskBody) : JBScrollPane(body.panel) {
|
||||
val rows: Stack get() = body.rows
|
||||
val panel: JPanel get() = body.panel
|
||||
|
||||
init {
|
||||
border = JBUI.Borders.empty()
|
||||
isOpaque = true
|
||||
background = SessionUiStyle.View.Surface.bgColor()
|
||||
viewport.background = SessionUiStyle.View.Surface.bgColor()
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
}
|
||||
}
|
||||
|
||||
private class TaskRows : Stack(StackAxis.VERTICAL, UiStyle.Gap.sm()), Scrollable {
|
||||
override fun getScrollableTracksViewportWidth() = true
|
||||
override fun getScrollableTracksViewportHeight() = false
|
||||
override fun getPreferredScrollableViewportSize(): Dimension = preferredSize
|
||||
override fun getScrollableUnitIncrement(
|
||||
visibleRect: Rectangle,
|
||||
orientation: Int,
|
||||
direction: Int,
|
||||
) = JBUI.scale(SessionUiStyle.SessionLayout.SCROLL_INCREMENT)
|
||||
override fun getScrollableBlockIncrement(
|
||||
visibleRect: Rectangle,
|
||||
orientation: Int,
|
||||
direction: Int,
|
||||
) = visibleRect.height
|
||||
|
||||
override fun getMaximumSize() = JBDimension(Int.MAX_VALUE, super.getMaximumSize().height)
|
||||
}
|
||||
|
||||
private fun rowTitleColor(tool: Tool) = if (tool.state == ToolExecState.ERROR) {
|
||||
UiStyle.Colors.errorLabelForeground()
|
||||
} else {
|
||||
UiStyle.Colors.weak()
|
||||
}
|
||||
|
||||
private fun agentTitle(tool: Tool): String {
|
||||
val type = tool.input["subagent_type"]?.takeIf { it.isNotBlank() } ?: tool.name
|
||||
return KiloBundle.message("session.part.tool.agent", type.replaceFirstChar { it.titlecase() })
|
||||
}
|
||||
|
||||
private fun summary(tool: Tool): String {
|
||||
val desc = tool.input["description"].orEmpty()
|
||||
val count = tool.childTools.size
|
||||
if (count <= 0) return desc
|
||||
if (desc.isBlank()) return "($count)"
|
||||
return "$desc ($count)"
|
||||
}
|
||||
@@ -93,6 +93,7 @@ session.part.reasoning=Reasoning
|
||||
session.part.compaction=context compacted
|
||||
session.part.tool.copy=Copy
|
||||
session.part.tool.error=Error
|
||||
session.part.tool.agent={0} Agent
|
||||
session.part.tool.pending=Pending
|
||||
session.part.tool.read=Read
|
||||
session.part.tool.glob=Glob
|
||||
|
||||
+90
-1
@@ -1,12 +1,17 @@
|
||||
package ai.kilocode.client.session.controller
|
||||
|
||||
import ai.kilocode.client.plugin.KiloPluginSettings
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.client.session.model.PermissionFileDiff
|
||||
import ai.kilocode.client.session.model.PermissionMeta
|
||||
import ai.kilocode.client.session.model.SessionState
|
||||
import ai.kilocode.client.session.SessionRef
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.rpc.dto.AgentDto
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.ConfigDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStateDto
|
||||
import ai.kilocode.rpc.dto.KiloAppStatusDto
|
||||
import ai.kilocode.rpc.dto.MessageWithPartsDto
|
||||
import ai.kilocode.rpc.dto.ModelDto
|
||||
import ai.kilocode.rpc.dto.PartDto
|
||||
import ai.kilocode.rpc.dto.PartSourceDto
|
||||
@@ -23,6 +28,7 @@ import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.ToolRefDto
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
class PromptLifecycleTest : SessionControllerTestBase() {
|
||||
|
||||
@@ -626,6 +632,78 @@ class PromptLifecycleTest : SessionControllerTestBase() {
|
||||
assertTrue("Root state must not be changed by child non-permission events", stateEvents.isEmpty())
|
||||
}
|
||||
|
||||
fun `test child tool update is stored on parent task part`() {
|
||||
val (m, _, modelEvents) = prompted()
|
||||
emit(ChatEventDto.MessageUpdated("ses_test", msg("msg1", "ses_test", "assistant")), flush = false)
|
||||
emit(taskPart("ses_child"), flush = false)
|
||||
emit(ChatEventDto.PartUpdated("ses_child", childTool("child_read", "read")))
|
||||
|
||||
val task = m.model.content("msg1", "part_task") as Tool
|
||||
assertEquals("ses_child", task.childSessionId)
|
||||
assertEquals(1, task.childTools.size)
|
||||
assertEquals("read", task.childTools[0].name)
|
||||
assertNull(m.model.content("child_msg", "child_read"))
|
||||
assertTrue(modelEvents.any { it.toString() == "ContentUpdated msg1/part_task" })
|
||||
}
|
||||
|
||||
fun `test child tool removed updates parent task part`() {
|
||||
val (m, _, _) = prompted()
|
||||
emit(ChatEventDto.MessageUpdated("ses_test", msg("msg1", "ses_test", "assistant")), flush = false)
|
||||
emit(taskPart("ses_child"), flush = false)
|
||||
emit(ChatEventDto.PartUpdated("ses_child", childTool("child_read", "read")), flush = false)
|
||||
emit(ChatEventDto.PartRemoved("ses_child", "child_msg", "child_read"))
|
||||
|
||||
val task = m.model.content("msg1", "part_task") as Tool
|
||||
assertTrue(task.childTools.isEmpty())
|
||||
}
|
||||
|
||||
fun `test history load backfills child tools`() {
|
||||
appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5"))
|
||||
projectRpc.state.value = workspaceReady()
|
||||
rpc.histories["ses_test"] = mutableListOf(
|
||||
MessageWithPartsDto(
|
||||
msg("msg1", "ses_test", "assistant"),
|
||||
listOf(taskPart("ses_child").part),
|
||||
),
|
||||
)
|
||||
rpc.histories["ses_child"] = mutableListOf(
|
||||
MessageWithPartsDto(
|
||||
msg("child_msg", "ses_child", "assistant"),
|
||||
listOf(childTool("child_read", "read"), childTool("child_grep", "grep")),
|
||||
),
|
||||
)
|
||||
|
||||
val m = controller("ses_test")
|
||||
flush()
|
||||
|
||||
val task = m.model.content("msg1", "part_task") as Tool
|
||||
assertEquals(listOf("read", "grep"), task.childTools.map { it.name })
|
||||
assertNull(m.model.content("child_msg", "child_read"))
|
||||
}
|
||||
|
||||
fun `test stale child history does not overwrite live child tool update`() {
|
||||
rpc.historyGate = CompletableDeferred()
|
||||
rpc.histories["ses_child"] = mutableListOf(
|
||||
MessageWithPartsDto(
|
||||
msg("child_msg", "ses_child", "assistant"),
|
||||
listOf(childTool("child_read", "grep")),
|
||||
),
|
||||
)
|
||||
val (m, _, _) = prompted()
|
||||
emit(ChatEventDto.MessageUpdated("ses_test", msg("msg1", "ses_test", "assistant")), flush = false)
|
||||
emit(taskPart("ses_child"), flush = false)
|
||||
emit(ChatEventDto.PartUpdated("ses_child", childTool("child_read", "read")))
|
||||
|
||||
var task = m.model.content("msg1", "part_task") as Tool
|
||||
assertEquals(listOf("read"), task.childTools.map { it.name })
|
||||
|
||||
rpc.historyGate!!.complete(Unit)
|
||||
flush()
|
||||
|
||||
task = m.model.content("msg1", "part_task") as Tool
|
||||
assertEquals(listOf("read"), task.childTools.map { it.name })
|
||||
}
|
||||
|
||||
fun `test root permission event is not processed as child permission`() {
|
||||
val (m, _, _) = prompted()
|
||||
|
||||
@@ -646,9 +724,20 @@ class PromptLifecycleTest : SessionControllerTestBase() {
|
||||
type = "tool",
|
||||
tool = "task",
|
||||
metadata = mapOf("sessionId" to childSessionId),
|
||||
input = mapOf("subagent_type" to "explore", "description" to "Find files"),
|
||||
),
|
||||
)
|
||||
|
||||
private fun childTool(id: String, name: String) = PartDto(
|
||||
id = id,
|
||||
sessionID = "ses_child",
|
||||
messageID = "child_msg",
|
||||
type = "tool",
|
||||
tool = name,
|
||||
state = "completed",
|
||||
input = mapOf("filePath" to "src/Main.kt", "pattern" to "query"),
|
||||
)
|
||||
|
||||
private fun childPermission(id: String) = PermissionRequestDto(
|
||||
id = id,
|
||||
sessionID = "ses_child",
|
||||
|
||||
+38
@@ -20,6 +20,7 @@ import ai.kilocode.client.session.views.MessageToolbar
|
||||
import ai.kilocode.client.session.views.MessageView
|
||||
import ai.kilocode.client.session.views.TextView
|
||||
import ai.kilocode.client.session.views.base.PartView
|
||||
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.rpc.dto.MessageDto
|
||||
@@ -376,6 +377,33 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
assertTrue(mv.partIds().isEmpty())
|
||||
}
|
||||
|
||||
fun `test child tool update refreshes collapsed task view without replacing it`() {
|
||||
model.upsertMessage(msg("a1", "assistant"))
|
||||
model.updateContent(
|
||||
"a1",
|
||||
toolPart(
|
||||
"part_task",
|
||||
"a1",
|
||||
"task",
|
||||
"call_task",
|
||||
input = mapOf("subagent_type" to "explore", "description" to "Find files"),
|
||||
metadata = mapOf("sessionId" to "ses_child"),
|
||||
),
|
||||
)
|
||||
model.upsertChildTool("ses_child", childTool("child_read", "read"))
|
||||
val view = panel.findMessage("a1")!!.part("part_task") as TaskToolView
|
||||
|
||||
assertTrue(view.bodyVisible())
|
||||
view.collapse()
|
||||
model.upsertChildTool("ses_child", childTool("child_read", "grep"))
|
||||
|
||||
val updated = panel.findMessage("a1")!!.part("part_task") as TaskToolView
|
||||
assertSame(view, updated)
|
||||
assertFalse(updated.bodyVisible())
|
||||
assertTrue(updated.rowLabels().single().contains("Grep"))
|
||||
assertTrue(updated.rowLabels().single().contains("pattern=query"))
|
||||
}
|
||||
|
||||
// ------ HistoryLoaded ------
|
||||
|
||||
fun `test HistoryLoaded rebuilds panel from scratch`() {
|
||||
@@ -804,6 +832,16 @@ class SessionMessageListPanelTest : BasePlatformTestCase() {
|
||||
input = input, metadata = metadata, todos = todos,
|
||||
)
|
||||
|
||||
private fun childTool(id: String, tool: String) = PartDto(
|
||||
id = id,
|
||||
sessionID = "ses_child",
|
||||
messageID = "child_msg",
|
||||
type = "tool",
|
||||
tool = tool,
|
||||
state = "completed",
|
||||
input = mapOf("filePath" to "src/Main.kt", "pattern" to "query"),
|
||||
)
|
||||
|
||||
private fun root(view: QuestionResultView) = view.components[0] as JPanel
|
||||
|
||||
private fun header(view: QuestionResultView) = root(view).components[0] as JPanel
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package ai.kilocode.client.session.views
|
||||
|
||||
import ai.kilocode.client.session.model.Tool
|
||||
import ai.kilocode.client.session.model.ToolExecState
|
||||
import ai.kilocode.client.session.model.toolKind
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.session.views.base.SecondarySessionPartView
|
||||
import ai.kilocode.client.session.views.tool.TaskToolView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Color
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
class TaskToolViewTest : BasePlatformTestCase() {
|
||||
private val views = mutableListOf<TaskToolView>()
|
||||
|
||||
override fun tearDown() {
|
||||
try {
|
||||
views.forEach(Disposer::dispose)
|
||||
views.clear()
|
||||
} finally {
|
||||
super.tearDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun `test task tool uses secondary chrome`() {
|
||||
val base: Any = view(task())
|
||||
|
||||
assertTrue(base is SecondarySessionPartView)
|
||||
}
|
||||
|
||||
fun `test task header shows agent description and count`() {
|
||||
val view = view(task(children = listOf(child("c1", "read"), child("c2", "grep"))))
|
||||
|
||||
assertTrue(view.labelText().contains("Explore Agent"))
|
||||
assertTrue(view.labelText().contains("Find files (2)"))
|
||||
assertEquals(2, view.rowCount())
|
||||
assertTrue(view.bodyVisible())
|
||||
}
|
||||
|
||||
fun `test update adds child row without replacing existing rows`() {
|
||||
val view = view(task(children = listOf(child("c1", "read"))))
|
||||
val before = view.rowLabels().first()
|
||||
|
||||
view.update(task(children = listOf(child("c1", "read"), child("c2", "grep"))))
|
||||
|
||||
assertEquals(2, view.rowCount())
|
||||
assertEquals(before, view.rowLabels().first())
|
||||
assertTrue(view.rowLabels().any { it.contains("Grep") })
|
||||
}
|
||||
|
||||
fun `test removing child rows collapses body`() {
|
||||
val view = view(task(children = listOf(child("c1", "read"))))
|
||||
|
||||
view.update(task(children = emptyList()))
|
||||
|
||||
assertEquals(0, view.rowCount())
|
||||
assertFalse(view.bodyVisible())
|
||||
}
|
||||
|
||||
fun `test body is lazy until child tools arrive`() {
|
||||
val view = view(task(children = emptyList()))
|
||||
|
||||
assertFalse(view.bodyCreated())
|
||||
view.update(task(children = listOf(child("c1", "read"))))
|
||||
|
||||
assertTrue(view.bodyCreated())
|
||||
assertTrue(view.bodyVisible())
|
||||
}
|
||||
|
||||
fun `test collapsed task body stays collapsed on child update`() {
|
||||
val view = view(task(children = listOf(child("c1", "read"))))
|
||||
|
||||
view.collapse()
|
||||
view.update(task(children = listOf(child("c1", "grep"))))
|
||||
|
||||
assertFalse(view.bodyVisible())
|
||||
assertTrue(view.rowLabels().single().contains("Grep"))
|
||||
assertTrue(view.rowLabels().single().contains("pattern=query"))
|
||||
}
|
||||
|
||||
fun `test expanded task body is capped to ten rows`() {
|
||||
val view = view(task(children = children(20)))
|
||||
val taller = view(task(children = children(80)))
|
||||
|
||||
assertEquals(10, view.bodyMaxRows())
|
||||
assertTrue(view.preferredSize.height > 0)
|
||||
assertEquals(view.preferredSize.height, taller.preferredSize.height)
|
||||
}
|
||||
|
||||
fun `test task body uses nested vertical scroll`() {
|
||||
val view = view(task(children = children(20)))
|
||||
|
||||
assertEquals(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, view.horizontalPolicy())
|
||||
assertEquals(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, view.verticalPolicy())
|
||||
}
|
||||
|
||||
fun `test child tool titles use target color`() {
|
||||
val view = view(task(children = listOf(child("c1", "read"), child("c2", "grep", ToolExecState.ERROR))))
|
||||
|
||||
assertColor(UiStyle.Colors.weak(), view.rowTitleColor("c1"))
|
||||
assertColor(UiStyle.Colors.errorLabelForeground(), view.rowTitleColor("c2"))
|
||||
}
|
||||
|
||||
fun `test task body is indented beyond header padding`() {
|
||||
val view = view(task(children = listOf(child("c1", "read"))))
|
||||
|
||||
assertTrue(view.bodyInsets().left > JBUI.scale(SessionUiStyle.View.Layout.HORIZONTAL_PADDING))
|
||||
assertEquals(UiStyle.Gap.sm(), view.bodyInsets().top)
|
||||
assertEquals(UiStyle.Gap.sm(), view.bodyInsets().bottom)
|
||||
}
|
||||
|
||||
fun `test appended child tools scroll nested body to bottom`() {
|
||||
val view = view(task(children = children(40)))
|
||||
view.setSize(300, view.preferredSize.height)
|
||||
view.doLayout()
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
view.setBodyScrollValue(view.bodyScrollBottom() - 1)
|
||||
|
||||
view.update(task(children = children(70)))
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
assertEquals(view.bodyScrollBottom(), view.bodyScrollValue())
|
||||
}
|
||||
|
||||
fun `test appended child tools do not yank nested body above tail`() {
|
||||
val view = view(task(children = children(40)))
|
||||
view.setSize(300, view.preferredSize.height)
|
||||
view.doLayout()
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
view.setBodyScrollValue(0)
|
||||
|
||||
view.update(task(children = children(70)))
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
assertEquals(0, view.bodyScrollValue())
|
||||
}
|
||||
|
||||
private fun view(tool: Tool): TaskToolView = TaskToolView(tool).also { views.add(it) }
|
||||
|
||||
private fun task(children: List<Tool> = emptyList()) = Tool("part_task", "task", toolKind("task")).also {
|
||||
it.state = ToolExecState.COMPLETED
|
||||
it.input = mapOf("subagent_type" to "explore", "description" to "Find files")
|
||||
it.metadata = mapOf("sessionId" to "ses_child")
|
||||
it.childSessionId = "ses_child"
|
||||
it.childTools = children
|
||||
}
|
||||
|
||||
private fun child(id: String, name: String, state: ToolExecState = ToolExecState.COMPLETED) = Tool(id, name, toolKind(name)).also {
|
||||
it.state = state
|
||||
it.input = mapOf("filePath" to "src/Main.kt", "pattern" to "query")
|
||||
}
|
||||
|
||||
private fun children(count: Int) = (1..count).map { child("c$it", "read") }
|
||||
|
||||
private fun assertColor(expected: Color, actual: Color?) {
|
||||
assertNotNull(actual)
|
||||
assertEquals(expected.rgb, actual!!.rgb)
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -46,6 +46,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi {
|
||||
|
||||
/** Message history returned by [messages]. */
|
||||
val history = mutableListOf<MessageWithPartsDto>()
|
||||
val histories = mutableMapOf<String, MutableList<MessageWithPartsDto>>()
|
||||
var historyGate: CompletableDeferred<Unit>? = null
|
||||
var historyCalls = 0
|
||||
private set
|
||||
@@ -218,7 +219,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi {
|
||||
assertNotEdt("messages")
|
||||
historyCalls++
|
||||
historyGate?.await()
|
||||
return history.toList()
|
||||
return histories[id]?.toList() ?: history.toList()
|
||||
}
|
||||
|
||||
override suspend fun attachmentPart(id: String, directory: String, messageId: String, partId: String, attachmentKey: String?): PartDto? {
|
||||
|
||||
Reference in New Issue
Block a user