mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
fix(jetbrains): clear the Agents tab dot once the attention has been read
The dot mirrored the activity snapshot, and an error stays in that snapshot until its session runs again, so a failed session left the dot lit for good. AgentAttention now tracks which sessions the user has already seen: attention that is pending while the Agent Manager is on screen counts as read, because the rows carry the badge there. The dot is therefore re-evaluated on tab selection and tool window visibility as well as on activity, and a session that recovers and fails again lights it once more.
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show worktree session titles in regular weight, keep the account switcher hidden when a new worktree starts with a prompt, add new worktrees at the top of the Agent Manager list, keep the running indicator on worktree rows when a stopped session is resumed, and keep session card popups inside the visible session view.
|
||||
Show worktree session titles in regular weight, keep the account switcher hidden when a new worktree starts with a prompt, add new worktrees at the top of the Agent Manager list, keep the running indicator on worktree rows when a stopped session is resumed, mark failed sessions on their worktree row, clear the Agents tab notification dot once the attention has been read, and keep session card popups inside the visible session view.
|
||||
|
||||
+23
-5
@@ -11,11 +11,12 @@ import ai.kilocode.client.agentManager.SidePanelKeys
|
||||
import ai.kilocode.client.agentManager.SidePanelMode
|
||||
import ai.kilocode.client.agentManager.applySidePanelMode
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeController
|
||||
import ai.kilocode.client.agentManager.AgentAttention
|
||||
import ai.kilocode.client.agentManager.AgentManagerPanel
|
||||
import ai.kilocode.client.agentManager.sessionAttentionNeeded
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.ui.AttentionDotIcon
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.DataProvider
|
||||
@@ -27,6 +28,7 @@ import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.wm.ToolWindow
|
||||
import com.intellij.openapi.wm.ToolWindowContentUiType
|
||||
import com.intellij.openapi.wm.ToolWindowFactory
|
||||
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
|
||||
import com.intellij.platform.project.projectIdOrNull
|
||||
import com.intellij.openapi.wm.impl.content.ToolWindowContentUi
|
||||
import com.intellij.ui.content.ContentManagerEvent
|
||||
@@ -34,7 +36,6 @@ import com.intellij.ui.content.ContentManagerListener
|
||||
import com.intellij.ui.content.ContentFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.BorderLayout
|
||||
@@ -151,23 +152,40 @@ internal class KiloToolWindowSetupService(
|
||||
agents()
|
||||
agentManagerPanel.move(id, dir)
|
||||
}
|
||||
// Notification dot on the Agents tab: attention the user has not looked at yet. Having
|
||||
// the tab on screen is the acknowledgement, so the dot has to be re-evaluated when the
|
||||
// selected tab or the tool window visibility changes, not only when activity arrives.
|
||||
val attention = AgentAttention()
|
||||
var snapshot = emptyMap<String, SessionActivityDto>()
|
||||
fun syncDot() {
|
||||
val showing = toolWindow.isVisible && toolWindow.contentManager.selectedContent === agentContent
|
||||
agentContent.icon = if (attention.update(snapshot, showing)) AttentionDotIcon else null
|
||||
}
|
||||
|
||||
val listener = object : ContentManagerListener {
|
||||
override fun selectionChanged(event: ContentManagerEvent) {
|
||||
if (event.operation == ContentManagerEvent.ContentOperation.add && event.content === agentContent) {
|
||||
agentManagerPanel.refresh()
|
||||
}
|
||||
syncDot()
|
||||
}
|
||||
}
|
||||
toolWindow.contentManager.addContentManagerListener(listener)
|
||||
Disposer.register(manager) { toolWindow.contentManager.removeContentManagerListener(listener) }
|
||||
val windows = object : ToolWindowManagerListener {
|
||||
override fun toolWindowShown(shown: ToolWindow) {
|
||||
if (shown.id == toolWindow.id) syncDot()
|
||||
}
|
||||
}
|
||||
project.messageBus.connect(manager).subscribe(ToolWindowManagerListener.TOPIC, windows)
|
||||
toolWindow.contentManager.setSelectedContent(chatContent)
|
||||
manager.newSession()
|
||||
|
||||
// Show a notification dot on the Agents tab whenever a worktree session needs attention.
|
||||
val dot = cs.launch {
|
||||
project.service<KiloSessionService>().activity.map(::sessionAttentionNeeded).collect { needed ->
|
||||
project.service<KiloSessionService>().activity.collect { current ->
|
||||
withContext(Dispatchers.Main) {
|
||||
agentContent.icon = if (needed) AttentionDotIcon else null
|
||||
snapshot = current
|
||||
syncDot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-8
@@ -4,13 +4,28 @@ import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityKindDto
|
||||
|
||||
/**
|
||||
* Whether any session in the activity snapshot is waiting on the user or has failed,
|
||||
* i.e. the Agents tab should show a notification dot.
|
||||
* Notification dot state for the Agents tab.
|
||||
*
|
||||
* The dot marks attention the user has not looked at yet. Sessions that need attention while the
|
||||
* Agent Manager is on screen count as seen, because the rows already carry the badge there. That is
|
||||
* what lets the dot clear for good: an error stays in the activity snapshot until its session runs
|
||||
* again, so a dot driven by the snapshot alone would come back every time the user left the tab.
|
||||
* A session that stops needing attention is forgotten again, so a later failure lights the dot.
|
||||
*/
|
||||
internal fun sessionAttentionNeeded(activity: Map<String, SessionActivityDto>): Boolean =
|
||||
activity.values.any {
|
||||
it.kind == SessionActivityKindDto.QUESTION ||
|
||||
it.kind == SessionActivityKindDto.PLAN ||
|
||||
it.kind == SessionActivityKindDto.PERMISSION ||
|
||||
it.kind == SessionActivityKindDto.ERROR
|
||||
internal class AgentAttention {
|
||||
private var seen = emptySet<String>()
|
||||
|
||||
/** Whether the dot should be visible, where [showing] means the Agent Manager is on screen. */
|
||||
fun update(activity: Map<String, SessionActivityDto>, showing: Boolean): Boolean {
|
||||
val pending = activity.filterValues(::attention).keys
|
||||
seen = if (showing) pending else seen intersect pending
|
||||
return (pending - seen).isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a session is waiting on the user or has failed. */
|
||||
private fun attention(item: SessionActivityDto): Boolean =
|
||||
item.kind == SessionActivityKindDto.QUESTION ||
|
||||
item.kind == SessionActivityKindDto.PLAN ||
|
||||
item.kind == SessionActivityKindDto.PERMISSION ||
|
||||
item.kind == SessionActivityKindDto.ERROR
|
||||
|
||||
+40
-3
@@ -15,13 +15,50 @@ class AgentAttentionTest {
|
||||
SessionActivityKindDto.PERMISSION,
|
||||
SessionActivityKindDto.ERROR,
|
||||
)) {
|
||||
assertTrue(sessionAttentionNeeded(mapOf("ses" to SessionActivityDto("/repo/wt", kind))), kind.name)
|
||||
assertTrue(AgentAttention().update(activity(kind), showing = false), kind.name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `running and empty do not light up the dot`() {
|
||||
assertFalse(sessionAttentionNeeded(emptyMap()))
|
||||
assertFalse(sessionAttentionNeeded(mapOf("ses" to SessionActivityDto("/repo/wt", SessionActivityKindDto.RUNNING))))
|
||||
assertFalse(AgentAttention().update(emptyMap(), showing = false))
|
||||
assertFalse(AgentAttention().update(activity(SessionActivityKindDto.RUNNING), showing = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `attention seen on screen stays clear after leaving the tab`() {
|
||||
val attention = AgentAttention()
|
||||
val errored = activity(SessionActivityKindDto.ERROR)
|
||||
|
||||
assertFalse(attention.update(errored, showing = true))
|
||||
// The error sticks in the snapshot until the session runs again; the dot must not come back.
|
||||
assertFalse(attention.update(errored, showing = false))
|
||||
assertFalse(attention.update(errored, showing = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `attention arriving while the tab is hidden lights the dot`() {
|
||||
val attention = AgentAttention()
|
||||
attention.update(activity(SessionActivityKindDto.ERROR), showing = true)
|
||||
|
||||
val another = mapOf(
|
||||
"ses_1" to SessionActivityDto("/repo/wt", SessionActivityKindDto.ERROR),
|
||||
"ses_2" to SessionActivityDto("/repo/other", SessionActivityKindDto.QUESTION),
|
||||
)
|
||||
|
||||
assertTrue(attention.update(another, showing = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a session that recovers and fails again lights the dot again`() {
|
||||
val attention = AgentAttention()
|
||||
val errored = activity(SessionActivityKindDto.ERROR)
|
||||
attention.update(errored, showing = true)
|
||||
|
||||
assertFalse(attention.update(emptyMap(), showing = false))
|
||||
assertTrue(attention.update(errored, showing = false))
|
||||
}
|
||||
|
||||
private fun activity(kind: SessionActivityKindDto) =
|
||||
mapOf("ses_1" to SessionActivityDto("/repo/wt", kind))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user