mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 14:07:20 +08:00
feat(jetbrains): show worktree activity badges
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": minor
|
||||
---
|
||||
|
||||
Show an activity badge on each Agent Manager worktree row, reflecting whether that worktree's sessions are running or waiting on a question or permission — even when the worktree's editor tab is not open.
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package ai.kilocode.backend.app
|
||||
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityKindDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Tracks live per-session activity (busy + pending question/permission)
|
||||
* with each session's directory, exposed to the frontend so the Agent
|
||||
* Manager worktree list can badge rows.
|
||||
*
|
||||
* **Not an IntelliJ service** — owned by [KiloBackendAppService] which
|
||||
* calls [start] after [KiloAppState.Ready] and [stop] on disconnect.
|
||||
*
|
||||
* Baseline `busy` comes from the session status stream; question/permission
|
||||
* overlays come from the global chat events. State is confined to a single
|
||||
* lock so the two collectors don't race.
|
||||
*/
|
||||
class KiloBackendActivityManager(
|
||||
private val cs: CoroutineScope,
|
||||
private val log: KiloLog,
|
||||
) {
|
||||
private val permissions = mutableMapOf<String, MutableSet<String>>()
|
||||
private val questions = mutableMapOf<String, MutableMap<String, Boolean>>()
|
||||
private val lock = Any()
|
||||
private val _activity = MutableStateFlow<Map<String, SessionActivityDto>>(emptyMap())
|
||||
val activity: StateFlow<Map<String, SessionActivityDto>> = _activity.asStateFlow()
|
||||
|
||||
private var statuses: StateFlow<Map<String, SessionStatusDto>>? = null
|
||||
private var directory: (String) -> String? = { null }
|
||||
private var status: Job? = null
|
||||
private var events: Job? = null
|
||||
|
||||
fun start(
|
||||
statuses: StateFlow<Map<String, SessionStatusDto>>,
|
||||
directory: (String) -> String?,
|
||||
chatEvents: SharedFlow<ChatEventDto>,
|
||||
) {
|
||||
this.statuses = statuses
|
||||
this.directory = directory
|
||||
if (status?.isActive == true || events?.isActive == true) return
|
||||
status = cs.launch {
|
||||
statuses.collect { synchronized(lock) { recompute() } }
|
||||
}
|
||||
events = cs.launch {
|
||||
chatEvents.collect { event ->
|
||||
synchronized(lock) {
|
||||
handle(event)
|
||||
recompute()
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Activity manager started")
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
status?.cancel()
|
||||
events?.cancel()
|
||||
status = null
|
||||
events = null
|
||||
statuses = null
|
||||
directory = { null }
|
||||
synchronized(lock) {
|
||||
permissions.clear()
|
||||
questions.clear()
|
||||
}
|
||||
_activity.value = emptyMap()
|
||||
log.info("Activity manager stopped")
|
||||
}
|
||||
|
||||
private fun handle(event: ChatEventDto) {
|
||||
when (event) {
|
||||
is ChatEventDto.PermissionAsked -> permissions.getOrPut(event.sessionID) { mutableSetOf() }.add(event.request.id)
|
||||
is ChatEventDto.PermissionReplied -> removeSet(permissions, event.sessionID, event.requestID)
|
||||
is ChatEventDto.QuestionAsked -> questions.getOrPut(event.sessionID) { mutableMapOf() }[event.request.id] = plan(event)
|
||||
is ChatEventDto.QuestionReplied -> removeMap(questions, event.sessionID, event.requestID)
|
||||
is ChatEventDto.QuestionRejected -> removeMap(questions, event.sessionID, event.requestID)
|
||||
is ChatEventDto.SessionIdle -> clear(event.sessionID)
|
||||
is ChatEventDto.SessionStatusChanged -> if (event.status.type == "idle") clear(event.sessionID)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun recompute() {
|
||||
val current = statuses?.value ?: emptyMap()
|
||||
val ids = LinkedHashSet<String>()
|
||||
ids.addAll(current.filterValues { it.type == "busy" }.keys)
|
||||
ids.addAll(permissions.keys)
|
||||
ids.addAll(questions.keys)
|
||||
_activity.value = ids.mapNotNull { id ->
|
||||
val dir = directory(id) ?: return@mapNotNull null
|
||||
val kind = kind(id, current[id]?.type == "busy") ?: return@mapNotNull null
|
||||
id to SessionActivityDto(dir, kind)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun kind(id: String, busy: Boolean): SessionActivityKindDto? {
|
||||
if (permissions[id]?.isNotEmpty() == true) return SessionActivityKindDto.PERMISSION
|
||||
val pending = questions[id]
|
||||
if (pending?.isNotEmpty() == true) {
|
||||
if (pending.values.any { it }) return SessionActivityKindDto.PLAN
|
||||
return SessionActivityKindDto.QUESTION
|
||||
}
|
||||
if (busy) return SessionActivityKindDto.RUNNING
|
||||
return null
|
||||
}
|
||||
|
||||
private fun plan(event: ChatEventDto.QuestionAsked): Boolean =
|
||||
event.request.questions.any { it.questionKey == "plan.followup.question" || it.headerKey == "plan.followup.header" }
|
||||
|
||||
private fun clear(id: String) {
|
||||
permissions.remove(id)
|
||||
questions.remove(id)
|
||||
}
|
||||
|
||||
private fun <T> removeSet(map: MutableMap<String, MutableSet<T>>, id: String, value: T) {
|
||||
val set = map[id] ?: return
|
||||
set.remove(value)
|
||||
if (set.isEmpty()) map.remove(id)
|
||||
}
|
||||
|
||||
private fun <T> removeMap(map: MutableMap<String, MutableMap<T, Boolean>>, id: String, value: T) {
|
||||
val items = map[id] ?: return
|
||||
items.remove(value)
|
||||
if (items.isEmpty()) map.remove(id)
|
||||
}
|
||||
}
|
||||
+3
@@ -135,6 +135,7 @@ class KiloBackendAppService private constructor(
|
||||
|
||||
val sessions = KiloBackendSessionManager(cs, log)
|
||||
val chat = KiloBackendChatManager(cs, log)
|
||||
val activity = KiloBackendActivityManager(cs, log)
|
||||
val models = KiloBackendModelStateManager(log)
|
||||
val workspaces = KiloBackendWorkspaceManager(cs, sessions, log)
|
||||
@Volatile var profile: KiloProfile200Response? = null
|
||||
@@ -458,6 +459,7 @@ class KiloBackendAppService private constructor(
|
||||
models.start(connection.apiClient!!, connection.port)
|
||||
sessions.start(connection.api!!, connection.apiClient!!, connection.port, connection.events)
|
||||
chat.start(connection.apiClient!!, connection.port, connection.events)
|
||||
activity.start(sessions.statuses, sessions::sessionDirectory, chat.events)
|
||||
workspaces.start(connection.api!!, connection.apiClient!!, connection.port, connection.events)
|
||||
startWatchingGlobalSseEvents()
|
||||
setTelemetry(true)
|
||||
@@ -878,6 +880,7 @@ class KiloBackendAppService private constructor(
|
||||
private fun stopRuntime() {
|
||||
workspaces.stop()
|
||||
models.stop()
|
||||
activity.stop()
|
||||
chat.stop()
|
||||
sessions.stop()
|
||||
}
|
||||
|
||||
+33
-17
@@ -50,6 +50,9 @@ class KiloBackendSessionManager(
|
||||
/** Per-session directory overrides (sessionId → worktree path). */
|
||||
private val directories = ConcurrentHashMap<String, String>()
|
||||
|
||||
/** Session directory cache populated while mapping CLI sessions. */
|
||||
private val owned = ConcurrentHashMap<String, String>()
|
||||
|
||||
private val _statuses = MutableStateFlow<Map<String, SessionStatusDto>>(emptyMap())
|
||||
val statuses: StateFlow<Map<String, SessionStatusDto>> = _statuses.asStateFlow()
|
||||
|
||||
@@ -95,6 +98,7 @@ class KiloBackendSessionManager(
|
||||
client = null
|
||||
http = null
|
||||
base = null
|
||||
owned.clear()
|
||||
_statuses.value = emptyMap()
|
||||
log.info("Session manager stopped")
|
||||
}
|
||||
@@ -154,6 +158,7 @@ class KiloBackendSessionManager(
|
||||
val dto = KiloCliDataParser.parseSession(raw!!)
|
||||
val meta = if (log.isDebugEnabled) ChatLogSummary.dir(dir) else "kind=session"
|
||||
log.info("${ChatLogSummary.sid(dto.id)} kind=session $meta created=true code=${response.code}")
|
||||
owned[dto.id] = dto.directory
|
||||
return dto
|
||||
}
|
||||
}
|
||||
@@ -168,6 +173,7 @@ class KiloBackendSessionManager(
|
||||
fun delete(id: String, dir: String) {
|
||||
requireClient().sessionDelete(sessionID = id, directory = dir)
|
||||
directories.remove(id)
|
||||
owned.remove(id)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +202,9 @@ class KiloBackendSessionManager(
|
||||
log.warn("Session rename failed: HTTP ${response.code}, body=$raw")
|
||||
throw RuntimeException("Session rename failed: HTTP ${response.code} — $raw")
|
||||
}
|
||||
return KiloCliDataParser.parseSession(raw!!)
|
||||
val dto = KiloCliDataParser.parseSession(raw!!)
|
||||
owned[dto.id] = dto.directory
|
||||
return dto
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +249,9 @@ class KiloBackendSessionManager(
|
||||
log.warn("Cloud session import failed: HTTP ${response.code}, body=$raw")
|
||||
throw RuntimeException("Cloud session import failed: HTTP ${response.code} — $raw")
|
||||
}
|
||||
return KiloCliDataParser.parseSession(raw)
|
||||
val dto = KiloCliDataParser.parseSession(raw)
|
||||
owned[dto.id] = dto.directory
|
||||
return dto
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +276,9 @@ class KiloBackendSessionManager(
|
||||
fun getDirectory(id: String, fallback: String): String =
|
||||
directories[id] ?: fallback
|
||||
|
||||
fun sessionDirectory(id: String): String? =
|
||||
directories[id] ?: owned[id]
|
||||
|
||||
// ------ mapping (generated API model → DTO) ------
|
||||
|
||||
private fun dto(s: ai.kilocode.jetbrains.api.model.Session) = dto(
|
||||
@@ -322,21 +335,24 @@ class KiloBackendSessionManager(
|
||||
archived: Double?,
|
||||
summary: SessionSummaryDto?,
|
||||
revert: SessionRevertDto?,
|
||||
) = SessionDto(
|
||||
id = id,
|
||||
projectID = project,
|
||||
directory = dir,
|
||||
parentID = parent,
|
||||
title = title,
|
||||
version = version,
|
||||
time = SessionTimeDto(
|
||||
created = time(id, "created", created),
|
||||
updated = time(id, "updated", updated),
|
||||
archived = archived,
|
||||
),
|
||||
summary = summary,
|
||||
revert = revert,
|
||||
)
|
||||
): SessionDto {
|
||||
owned[id] = dir
|
||||
return SessionDto(
|
||||
id = id,
|
||||
projectID = project,
|
||||
directory = dir,
|
||||
parentID = parent,
|
||||
title = title,
|
||||
version = version,
|
||||
time = SessionTimeDto(
|
||||
created = time(id, "created", created),
|
||||
updated = time(id, "updated", updated),
|
||||
archived = archived,
|
||||
),
|
||||
summary = summary,
|
||||
revert = revert,
|
||||
)
|
||||
}
|
||||
|
||||
private fun summary(add: Double?, del: Double?, files: Double?) = SessionSummaryDto(
|
||||
additions = count(add),
|
||||
|
||||
+8
@@ -3,6 +3,7 @@
|
||||
package ai.kilocode.backend.rpc
|
||||
|
||||
import ai.kilocode.backend.app.KiloBackendAppService
|
||||
import ai.kilocode.backend.app.KiloBackendActivityManager
|
||||
import ai.kilocode.backend.app.KiloBackendChatManager
|
||||
import ai.kilocode.backend.app.KiloBackendSessionManager
|
||||
import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager
|
||||
@@ -21,6 +22,7 @@ import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import com.intellij.openapi.components.service
|
||||
@@ -58,6 +60,9 @@ class KiloSessionRpcApiImpl internal constructor(
|
||||
private val chat: KiloBackendChatManager
|
||||
get() = app.chat
|
||||
|
||||
private val activity: KiloBackendActivityManager
|
||||
get() = app.activity
|
||||
|
||||
private val app: KiloBackendAppService
|
||||
get() = appOverride ?: service()
|
||||
|
||||
@@ -103,6 +108,9 @@ class KiloSessionRpcApiImpl internal constructor(
|
||||
override suspend fun statuses(): Flow<Map<String, SessionStatusDto>> =
|
||||
sessions.statuses
|
||||
|
||||
override suspend fun activity(): Flow<Map<String, SessionActivityDto>> =
|
||||
activity.activity
|
||||
|
||||
override suspend fun setDirectory(id: String, directory: String) =
|
||||
sessions.setDirectory(id, directory)
|
||||
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package ai.kilocode.backend.app
|
||||
|
||||
import ai.kilocode.backend.testing.TestLog
|
||||
import ai.kilocode.rpc.dto.ChatEventDto
|
||||
import ai.kilocode.rpc.dto.PermissionRequestDto
|
||||
import ai.kilocode.rpc.dto.QuestionInfoDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityKindDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
class KiloBackendActivityManagerTest {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val events = MutableSharedFlow<ChatEventDto>(extraBufferCapacity = 16)
|
||||
private val statuses = MutableStateFlow<Map<String, SessionStatusDto>>(emptyMap())
|
||||
private val directories = mutableMapOf<String, String>()
|
||||
private val manager = KiloBackendActivityManager(scope, TestLog())
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
manager.stop()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun start() = manager.start(statuses, { directories[it] }, events)
|
||||
|
||||
@Test
|
||||
fun `busy status with known directory emits running activity`() = runBlocking {
|
||||
directories["ses_1"] = "/repo/wt"
|
||||
start()
|
||||
|
||||
statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
|
||||
val snap = await("ses_1", SessionActivityKindDto.RUNNING)
|
||||
assertEquals("/repo/wt", snap["ses_1"]?.directory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `permission asked overlays running and reply reverts`() = runBlocking {
|
||||
directories["ses_1"] = "/repo/wt"
|
||||
statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
start()
|
||||
|
||||
events.emit(ChatEventDto.PermissionAsked("ses_1", PermissionRequestDto("perm_1", "ses_1", "edit", emptyList())))
|
||||
await("ses_1", SessionActivityKindDto.PERMISSION)
|
||||
|
||||
events.emit(ChatEventDto.PermissionReplied("ses_1", "perm_1"))
|
||||
|
||||
await("ses_1", SessionActivityKindDto.RUNNING)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `question kinds distinguish plain and plan followup`() = runBlocking {
|
||||
directories["ses_plain"] = "/repo/a"
|
||||
directories["ses_plan"] = "/repo/b"
|
||||
start()
|
||||
|
||||
events.emit(ChatEventDto.QuestionAsked("ses_plain", question("q_1", "ses_plain")))
|
||||
events.emit(ChatEventDto.QuestionAsked("ses_plan", question("q_2", "ses_plan", plan = true)))
|
||||
|
||||
await("ses_plain", SessionActivityKindDto.QUESTION)
|
||||
await("ses_plan", SessionActivityKindDto.PLAN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `idle clears pending overlays and removes inactive entry`() = runBlocking {
|
||||
directories["ses_1"] = "/repo/wt"
|
||||
statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
start()
|
||||
events.emit(ChatEventDto.QuestionAsked("ses_1", question("q_1", "ses_1")))
|
||||
await("ses_1", SessionActivityKindDto.QUESTION)
|
||||
|
||||
statuses.value = mapOf("ses_1" to SessionStatusDto("idle"))
|
||||
events.emit(ChatEventDto.SessionIdle("ses_1"))
|
||||
|
||||
withTimeout(5_000) { manager.activity.first { "ses_1" !in it } }
|
||||
assertFalse("ses_1" in manager.activity.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown session directory is omitted`() = runBlocking {
|
||||
directories["ses_known"] = "/repo/wt"
|
||||
start()
|
||||
|
||||
statuses.value = mapOf(
|
||||
"ses_known" to SessionStatusDto("busy"),
|
||||
"ses_unknown" to SessionStatusDto("busy"),
|
||||
)
|
||||
|
||||
val snap = await("ses_known", SessionActivityKindDto.RUNNING)
|
||||
assertFalse("ses_unknown" in snap)
|
||||
}
|
||||
|
||||
private suspend fun await(id: String, kind: SessionActivityKindDto) = withTimeout(5_000) {
|
||||
manager.activity.first { it[id]?.kind == kind }
|
||||
}
|
||||
|
||||
private fun question(id: String, session: String, plan: Boolean = false): QuestionRequestDto {
|
||||
val info = if (plan) {
|
||||
QuestionInfoDto(
|
||||
question = "Ready to implement?",
|
||||
header = "Implement",
|
||||
questionKey = "plan.followup.question",
|
||||
headerKey = "plan.followup.header",
|
||||
)
|
||||
} else {
|
||||
QuestionInfoDto(question = "Pick one", header = "Choice")
|
||||
}
|
||||
return QuestionRequestDto(id = id, sessionID = session, questions = listOf(info))
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -2,6 +2,7 @@ package ai.kilocode.client
|
||||
|
||||
import ai.kilocode.client.app.KiloWorkspaceService
|
||||
import ai.kilocode.client.app.Workspace
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.session.SessionManager
|
||||
import ai.kilocode.client.session.SessionSidePanelManager
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
@@ -88,7 +89,12 @@ internal class KiloToolWindowSetupService(
|
||||
try {
|
||||
val manager = SessionSidePanelManager(project, workspace)
|
||||
|
||||
val worktrees = WorktreeController(service<KiloWorktreeService>(), workspace.directory, cs)
|
||||
val worktrees = WorktreeController(
|
||||
service<KiloWorktreeService>(),
|
||||
workspace.directory,
|
||||
cs,
|
||||
activity = project.service<KiloSessionService>().activity,
|
||||
)
|
||||
val agentManagerPanel = AgentManagerPanel(manager, worktrees, project)
|
||||
|
||||
val chat = object : JPanel(BorderLayout()), DataProvider {
|
||||
|
||||
+16
-2
@@ -9,8 +9,10 @@ import ai.kilocode.client.agentManager.worktree.WorktreeSessionEditorKind
|
||||
import ai.kilocode.client.agentManager.worktree.ensureWorktreeSessionEditorKind
|
||||
import ai.kilocode.client.agentManager.worktree.worktreeSessionParams
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.list.ActiveList
|
||||
import ai.kilocode.client.ui.list.ActiveListBadge
|
||||
import ai.kilocode.client.ui.list.ActiveListCell
|
||||
import ai.kilocode.client.ui.list.ActiveListDeleteOptions
|
||||
import ai.kilocode.client.ui.list.ActiveListItem
|
||||
@@ -90,6 +92,7 @@ class AgentManagerPanel(
|
||||
}
|
||||
controller.onCreateFailure = { err -> notifyCreateFailed(err) }
|
||||
controller.onRemoveSuccess = { item -> close(item) }
|
||||
controller.onActivityChanged = { sync() }
|
||||
// Reflect names adopted or renamed in a worktree session editor tab in the list live.
|
||||
service<WorktreeNameCache>().addListener(this) { path, name -> controller.applyName(path, name) }
|
||||
ActionManager.getInstance().getAction("RenameElement")?.shortcutSet?.let { set ->
|
||||
@@ -239,7 +242,7 @@ class AgentManagerPanel(
|
||||
list.update(
|
||||
(0 until controller.model.size).map {
|
||||
val item = controller.model.getElementAt(it)
|
||||
WorktreeRow(item, controller.isPending(item.id), controller.isDeleting(item.id))
|
||||
WorktreeRow(item, controller.isPending(item.id), controller.isDeleting(item.id), controller.kind(item.path))
|
||||
},
|
||||
ActiveListSelection.PreserveNoScroll,
|
||||
)
|
||||
@@ -267,6 +270,7 @@ class AgentManagerPanel(
|
||||
controller.onSelect = null
|
||||
controller.onCreateFailure = null
|
||||
controller.onRemoveSuccess = null
|
||||
controller.onActivityChanged = null
|
||||
}
|
||||
|
||||
override fun uiDataSnapshot(sink: DataSink) {
|
||||
@@ -306,13 +310,23 @@ class AgentManagerPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private data class WorktreeRow(val dto: WorktreeDto, val pending: Boolean, override val deleting: Boolean) : ActiveListItem {
|
||||
private data class WorktreeRow(
|
||||
val dto: WorktreeDto,
|
||||
val pending: Boolean,
|
||||
override val deleting: Boolean,
|
||||
val kind: SessionActivityKind?,
|
||||
) : ActiveListItem {
|
||||
override val key: String get() = dto.id
|
||||
override val title: String get() = dto.name
|
||||
override val description: String get() = dto.path.trimEnd('/').substringAfterLast('/')
|
||||
override val tooltip: String get() = dto.path
|
||||
override val icon = WorktreeIcons.forRow(dto.locked, pending)
|
||||
override val search: String get() = listOfNotNull(dto.name, dto.branch, dto.path, dto.lockReason).joinToString(" ")
|
||||
override val badges: List<ActiveListBadge>
|
||||
get() {
|
||||
if (pending || deleting) return emptyList()
|
||||
return listOfNotNull(kind?.let { ActiveListBadge(it.label(), it.style()) })
|
||||
}
|
||||
override val cells: List<ActiveListCell>
|
||||
get() = if (dto.main || pending) emptyList() else listOf(
|
||||
ActiveListCell(
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package ai.kilocode.client.agentManager.worktree
|
||||
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityKindDto
|
||||
|
||||
internal fun aggregateWorktreeActivity(
|
||||
activity: Map<String, SessionActivityDto>,
|
||||
): Map<String, SessionActivityKind> = activity.values
|
||||
.groupBy { normalize(it.directory) }
|
||||
.mapValues { (_, items) -> items.map { kind(it.kind) }.minBy(::rank) }
|
||||
|
||||
internal fun normalizeWorktreePath(path: String): String = normalize(path)
|
||||
|
||||
private fun normalize(path: String): String = path.trimEnd('/')
|
||||
|
||||
private fun kind(kind: SessionActivityKindDto): SessionActivityKind = when (kind) {
|
||||
SessionActivityKindDto.RUNNING -> SessionActivityKind.RUNNING
|
||||
SessionActivityKindDto.QUESTION -> SessionActivityKind.QUESTION
|
||||
SessionActivityKindDto.PLAN -> SessionActivityKind.PLAN
|
||||
SessionActivityKindDto.PERMISSION -> SessionActivityKind.PERMISSION
|
||||
}
|
||||
|
||||
private fun rank(kind: SessionActivityKind): Int = when (kind) {
|
||||
SessionActivityKind.PERMISSION -> 0
|
||||
SessionActivityKind.QUESTION -> 1
|
||||
SessionActivityKind.PLAN -> 2
|
||||
SessionActivityKind.RUNNING -> 3
|
||||
SessionActivityKind.LOGIN_REQUIRED -> 4
|
||||
}
|
||||
+22
@@ -1,14 +1,18 @@
|
||||
package ai.kilocode.client.agentManager.worktree
|
||||
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.rpc.dto.CreateWorktreeRequestDto
|
||||
import ai.kilocode.rpc.dto.RemoveWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.WorktreeDto
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.ui.CollectionListModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Collections
|
||||
|
||||
@@ -20,6 +24,7 @@ class WorktreeController(
|
||||
private val service: KiloWorktreeService,
|
||||
private val directory: String,
|
||||
private val cs: CoroutineScope,
|
||||
activity: StateFlow<Map<String, SessionActivityDto>> = MutableStateFlow(emptyMap()),
|
||||
private val telemetry: (String, Map<String, String>) -> Unit = { event, props -> Telemetry.send(event, props) },
|
||||
) {
|
||||
val model = CollectionListModel<WorktreeDto>()
|
||||
@@ -28,6 +33,21 @@ class WorktreeController(
|
||||
var onSelect: ((String) -> Unit)? = null
|
||||
var onCreateFailure: ((String?) -> Unit)? = null
|
||||
var onRemoveSuccess: ((WorktreeDto) -> Unit)? = null
|
||||
var onActivityChanged: (() -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
private var kinds: Map<String, SessionActivityKind> = emptyMap()
|
||||
|
||||
init {
|
||||
cs.launch {
|
||||
activity.collect { snap ->
|
||||
edt {
|
||||
kinds = aggregateWorktreeActivity(snap)
|
||||
onActivityChanged?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Branch checked out in the main worktree; used as the base for quick worktree creation. */
|
||||
@Volatile
|
||||
@@ -47,6 +67,8 @@ class WorktreeController(
|
||||
|
||||
fun isDeleting(id: String): Boolean = id in deleting
|
||||
|
||||
fun kind(path: String): SessionActivityKind? = kinds[normalizeWorktreePath(path)]
|
||||
|
||||
fun reload() {
|
||||
cs.launch {
|
||||
val listing = async { service.list(directory) }
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ open class WorktreeSessionEditorManager(
|
||||
service<SessionUiFactory>().create(project, workspace, manager, ref, timers)
|
||||
},
|
||||
resolve: (String) -> Workspace = { dir -> service<KiloWorkspaceService>().workspace(dir) },
|
||||
status: () -> Map<String, SessionActivityKind> = { project.service<KiloSessionService>().activity() },
|
||||
status: () -> Map<String, SessionActivityKind> = { project.service<KiloSessionService>().activitySnapshot() },
|
||||
timers: UiTimerSource = UiTimers,
|
||||
request: (JComponent) -> Unit = { focus ->
|
||||
ApplicationManager.getApplication().invokeLater({
|
||||
|
||||
+6
-1
@@ -17,6 +17,7 @@ import ai.kilocode.rpc.dto.PartDto
|
||||
import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
@@ -65,6 +66,10 @@ class KiloSessionService internal constructor(
|
||||
val statuses: StateFlow<Map<String, SessionStatusDto>> =
|
||||
stream { statuses() }.stateIn(cs, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
/** Live session activity map from backend global events. */
|
||||
val activity: StateFlow<Map<String, SessionActivityDto>> =
|
||||
stream { activity() }.stateIn(cs, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
// ------ RPC helpers ------
|
||||
|
||||
private suspend fun <T> call(block: suspend KiloSessionRpcApi.() -> T): T {
|
||||
@@ -91,7 +96,7 @@ class KiloSessionService internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun activity(): Map<String, SessionActivityKind> =
|
||||
internal fun activitySnapshot(): Map<String, SessionActivityKind> =
|
||||
statuses.value
|
||||
.filterValues { it.type == "busy" }
|
||||
.mapValues { SessionActivityKind.RUNNING }
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ class SessionSidePanelManager(
|
||||
service<SessionUiFactory>().create(project, workspace, manager, ref, timers)
|
||||
},
|
||||
resolve: (String) -> Workspace = { dir -> service<KiloWorkspaceService>().workspace(dir) },
|
||||
status: () -> Map<String, SessionActivityKind> = { project.service<KiloSessionService>().activity() },
|
||||
status: () -> Map<String, SessionActivityKind> = { project.service<KiloSessionService>().activitySnapshot() },
|
||||
private val history: ((Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null,
|
||||
timers: UiTimerSource = UiTimers,
|
||||
request: (JComponent) -> Unit = { focus ->
|
||||
|
||||
+1
-1
@@ -509,7 +509,7 @@ class SessionUi(
|
||||
controller,
|
||||
event.recents,
|
||||
history = { manager?.showHistory() },
|
||||
activity = { manager?.activity() ?: sessions.activity() },
|
||||
activity = { manager?.activity() ?: sessions.activitySnapshot() },
|
||||
titles = { manager?.titles().orEmpty() },
|
||||
timers = timers,
|
||||
)
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ class HistoryController(
|
||||
reloadCloud()
|
||||
}
|
||||
|
||||
internal fun activity() = sessions.activity()
|
||||
internal fun activity() = sessions.activitySnapshot()
|
||||
|
||||
fun reloadLocal() {
|
||||
edt { local.start() }
|
||||
|
||||
+44
@@ -8,6 +8,8 @@ import ai.kilocode.client.agentManager.worktree.worktreeSessionParams
|
||||
import ai.kilocode.client.testing.FakeWorktreeRpcApi
|
||||
import ai.kilocode.client.testing.TestCoroutines
|
||||
import ai.kilocode.client.testing.fire
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.ui.list.ActiveListBadge
|
||||
import ai.kilocode.client.ui.list.ActiveListItem
|
||||
import ai.kilocode.client.ui.list.activeListToolWindowBackground
|
||||
import ai.kilocode.client.vfs.KiloPath
|
||||
@@ -15,6 +17,8 @@ import ai.kilocode.client.vfs.KiloVfsManager
|
||||
import ai.kilocode.client.vfs.KiloVirtualFile
|
||||
import ai.kilocode.client.vfs.KiloVirtualFileSystem
|
||||
import ai.kilocode.rpc.dto.WorktreeDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityKindDto
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
@@ -27,6 +31,7 @@ import java.awt.event.MouseEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
class AgentManagerPanelTest : BasePlatformTestCase() {
|
||||
@@ -207,6 +212,40 @@ class AgentManagerPanelTest : BasePlatformTestCase() {
|
||||
assertNull(KiloVirtualFileSystem.getInstance().cached(path))
|
||||
}
|
||||
|
||||
fun `test worktree row shows activity badge for matching directory`() {
|
||||
val item = WorktreeDto("/repo/.kilo/worktrees/feature-x", "feature-x", "feature/x", "/repo/.kilo/worktrees/feature-x")
|
||||
val activity = MutableStateFlow(mapOf(
|
||||
"ses_1" to SessionActivityDto(item.path, SessionActivityKindDto.QUESTION),
|
||||
))
|
||||
rpc.listed += item
|
||||
val controller = WorktreeController(service, "/test", coroutines.scope, activity = activity)
|
||||
val panel = edt { AgentManagerPanel(testRootDisposable, controller) }
|
||||
edt { controller.reload() }
|
||||
flush()
|
||||
|
||||
val row = row(panel, 0)
|
||||
assertEquals(listOf(ActiveListBadge(SessionActivityKind.QUESTION.label(), SessionActivityKind.QUESTION.style())), row.badges)
|
||||
}
|
||||
|
||||
fun `test worktree row hides badge while pending or deleting`() {
|
||||
val path = "feature/y"
|
||||
val activity = MutableStateFlow(mapOf(
|
||||
"ses_1" to SessionActivityDto(path, SessionActivityKindDto.RUNNING),
|
||||
))
|
||||
val gate = CompletableDeferred<Unit>()
|
||||
rpc.beforeCreate = { gate.await() }
|
||||
val controller = WorktreeController(service, "/test", coroutines.scope, activity = activity)
|
||||
val panel = edt { AgentManagerPanel(testRootDisposable, controller) }
|
||||
|
||||
edt { controller.create("feature/y", null) }
|
||||
flush()
|
||||
|
||||
val pending = row(panel, 0)
|
||||
assertEquals(emptyList<ActiveListBadge>(), pending.badges)
|
||||
gate.complete(Unit)
|
||||
flush()
|
||||
}
|
||||
|
||||
private fun <T> edt(block: () -> T): T {
|
||||
val out = arrayOfNulls<Any?>(1)
|
||||
ApplicationManager.getApplication().invokeAndWait { out[0] = block() }
|
||||
@@ -216,6 +255,11 @@ class AgentManagerPanelTest : BasePlatformTestCase() {
|
||||
|
||||
private fun flush() = coroutines.drain(::pump)
|
||||
|
||||
private fun row(panel: AgentManagerPanel, idx: Int): ActiveListItem {
|
||||
val list = edt { UIUtil.findComponentOfType(panel, JBList::class.java)!! }
|
||||
return edt { list.model.getElementAt(idx) as ActiveListItem }
|
||||
}
|
||||
|
||||
private fun pump() {
|
||||
ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() }
|
||||
}
|
||||
|
||||
+21
-2
@@ -5,17 +5,21 @@ import ai.kilocode.client.agentManager.worktree.KiloWorktreeService
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeController
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeNameCache
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeNames
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.testing.FakeWorktreeRpcApi
|
||||
import ai.kilocode.client.testing.TestCoroutines
|
||||
import ai.kilocode.rpc.dto.CreateWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.RemoveWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.RenameWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityKindDto
|
||||
import ai.kilocode.rpc.dto.WorktreeDto
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
@@ -331,6 +335,21 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
assertSame(before, controller.model.getElementAt(0))
|
||||
}
|
||||
|
||||
fun `test activity flow updates worktree kind and notifies on EDT`() {
|
||||
val activity = MutableStateFlow<Map<String, SessionActivityDto>>(emptyMap())
|
||||
val controller = controller(activity)
|
||||
val calls = mutableListOf<Boolean>()
|
||||
controller.onActivityChanged = { calls.add(ApplicationManager.getApplication().isDispatchThread) }
|
||||
|
||||
activity.value = mapOf("ses_1" to SessionActivityDto("/repo/wt/", SessionActivityKindDto.RUNNING))
|
||||
flush()
|
||||
|
||||
assertEquals(SessionActivityKind.RUNNING, controller.kind("/repo/wt"))
|
||||
assertEquals(SessionActivityKind.RUNNING, controller.kind("/repo/wt/"))
|
||||
assertTrue(calls.isNotEmpty())
|
||||
assertTrue(calls.all { it })
|
||||
}
|
||||
|
||||
fun `test cache notifies on single put and remove but not on bulk sync`() {
|
||||
val cache = cache()
|
||||
val events = mutableListOf<Pair<String, String?>>()
|
||||
@@ -347,8 +366,8 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf("/wt" to "Name", "/wt" to null), events)
|
||||
}
|
||||
|
||||
private fun controller() =
|
||||
WorktreeController(service, "/test", coroutines.scope)
|
||||
private fun controller(activity: MutableStateFlow<Map<String, SessionActivityDto>> = MutableStateFlow(emptyMap())) =
|
||||
WorktreeController(service, "/test", coroutines.scope, activity = activity)
|
||||
|
||||
private fun flush() = coroutines.drain(::pump)
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package ai.kilocode.client.agentManager.worktree
|
||||
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityKindDto
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WorktreeActivityTest {
|
||||
@Test
|
||||
fun `aggregates multiple sessions by directory with deterministic precedence`() {
|
||||
val result = aggregateWorktreeActivity(mapOf(
|
||||
"ses_run" to SessionActivityDto("/repo/wt", SessionActivityKindDto.RUNNING),
|
||||
"ses_plan" to SessionActivityDto("/repo/wt", SessionActivityKindDto.PLAN),
|
||||
"ses_question" to SessionActivityDto("/repo/wt", SessionActivityKindDto.QUESTION),
|
||||
"ses_permission" to SessionActivityDto("/repo/wt", SessionActivityKindDto.PERMISSION),
|
||||
))
|
||||
|
||||
assertEquals(SessionActivityKind.PERMISSION, result["/repo/wt"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `question beats plan and running while running is used alone`() {
|
||||
val result = aggregateWorktreeActivity(mapOf(
|
||||
"ses_run" to SessionActivityDto("/repo/a", SessionActivityKindDto.RUNNING),
|
||||
"ses_plan" to SessionActivityDto("/repo/b", SessionActivityKindDto.PLAN),
|
||||
"ses_question" to SessionActivityDto("/repo/b", SessionActivityKindDto.QUESTION),
|
||||
))
|
||||
|
||||
assertEquals(SessionActivityKind.RUNNING, result["/repo/a"])
|
||||
assertEquals(SessionActivityKind.QUESTION, result["/repo/b"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizes trailing slashes`() {
|
||||
val result = aggregateWorktreeActivity(mapOf(
|
||||
"ses_1" to SessionActivityDto("/repo/wt/", SessionActivityKindDto.RUNNING),
|
||||
))
|
||||
|
||||
assertEquals(mapOf("/repo/wt" to SessionActivityKind.RUNNING), result)
|
||||
assertEquals("/repo/wt", normalizeWorktreePath("/repo/wt/"))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -320,7 +320,7 @@ class WorktreeSessionEditorManagerTest : BasePlatformTestCase() {
|
||||
}
|
||||
},
|
||||
resolve = { workspaces.workspace(it) },
|
||||
status = { sessions.activity() },
|
||||
status = { sessions.activitySnapshot() },
|
||||
timers = timers,
|
||||
request = { requested += it },
|
||||
notify = { title, content -> notified += title to content },
|
||||
|
||||
+1
-1
@@ -699,7 +699,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() {
|
||||
}
|
||||
},
|
||||
resolve = { workspaces.workspace(it) },
|
||||
status = { sessions.activity() },
|
||||
status = { sessions.activitySnapshot() },
|
||||
history = history,
|
||||
timers = timers,
|
||||
request = request,
|
||||
|
||||
+2
-2
@@ -162,7 +162,7 @@ class HistoryControllerTest : BasePlatformTestCase() {
|
||||
)
|
||||
flush()
|
||||
|
||||
val activity = sessions.activity()
|
||||
val activity = sessions.activitySnapshot()
|
||||
|
||||
assertEquals(mapOf("ses_busy" to SessionActivityKind.RUNNING), activity)
|
||||
}
|
||||
@@ -269,7 +269,7 @@ class HistoryControllerTest : BasePlatformTestCase() {
|
||||
override fun newSession() {}
|
||||
override fun showHistory() {}
|
||||
override fun openSession(ref: SessionRef) {}
|
||||
override fun activity() = sessions.activity() + kind?.let { mapOf("ses_1" to it) }.orEmpty()
|
||||
override fun activity() = sessions.activitySnapshot() + kind?.let { mapOf("ses_1" to it) }.orEmpty()
|
||||
})
|
||||
rpc.statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
flush()
|
||||
|
||||
+3
-3
@@ -286,7 +286,7 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
|
||||
fun `test renderer shows overlay badge for active recent session`() {
|
||||
val panel = panel(
|
||||
recents = listOf(session("ses_1")),
|
||||
activity = { sessions.activity() + mapOf("ses_1" to SessionActivityKind.QUESTION) },
|
||||
activity = { sessions.activitySnapshot() + mapOf("ses_1" to SessionActivityKind.QUESTION) },
|
||||
)
|
||||
rpc.statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
flush()
|
||||
@@ -301,7 +301,7 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
|
||||
var kind: SessionActivityKind? = null
|
||||
val panel = panel(
|
||||
recents = listOf(session("ses_1")),
|
||||
activity = { sessions.activity() + kind?.let { mapOf("ses_1" to it) }.orEmpty() },
|
||||
activity = { sessions.activitySnapshot() + kind?.let { mapOf("ses_1" to it) }.orEmpty() },
|
||||
)
|
||||
rpc.statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
|
||||
flush()
|
||||
@@ -350,7 +350,7 @@ class EmptySessionPanelTest : BasePlatformTestCase() {
|
||||
private fun panel(
|
||||
recents: List<SessionDto> = emptyList(),
|
||||
history: () -> Unit = {},
|
||||
activity: () -> Map<String, SessionActivityKind> = { sessions.activity() },
|
||||
activity: () -> Map<String, SessionActivityKind> = { sessions.activitySnapshot() },
|
||||
titles: () -> Map<String, String> = { emptyMap() },
|
||||
) = EmptySessionPanel(testRootDisposable, controller, recents, history, activity, titles)
|
||||
|
||||
|
||||
+9
@@ -15,6 +15,7 @@ import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import ai.kilocode.rpc.dto.SessionTimeDto
|
||||
@@ -70,6 +71,9 @@ class FakeSessionRpcApi : KiloSessionRpcApi {
|
||||
/** Push status updates here. */
|
||||
val statuses = MutableStateFlow<Map<String, SessionStatusDto>>(emptyMap())
|
||||
|
||||
/** Push activity updates here. */
|
||||
val activity = MutableStateFlow<Map<String, SessionActivityDto>>(emptyMap())
|
||||
|
||||
/** Pending permissions returned by [pendingPermissions]. */
|
||||
val pendingPermissionList = mutableListOf<PermissionRequestDto>()
|
||||
|
||||
@@ -189,6 +193,11 @@ class FakeSessionRpcApi : KiloSessionRpcApi {
|
||||
return statuses
|
||||
}
|
||||
|
||||
override suspend fun activity(): Flow<Map<String, SessionActivityDto>> {
|
||||
assertNotEdt("activity")
|
||||
return activity
|
||||
}
|
||||
|
||||
override suspend fun setDirectory(id: String, directory: String) {
|
||||
assertNotEdt("setDirectory")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import ai.kilocode.rpc.dto.PromptDto
|
||||
import ai.kilocode.rpc.dto.QuestionReplyDto
|
||||
import ai.kilocode.rpc.dto.QuestionRequestDto
|
||||
import ai.kilocode.rpc.dto.SessionDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
import ai.kilocode.rpc.dto.SessionListDto
|
||||
import ai.kilocode.rpc.dto.SessionStatusDto
|
||||
import com.intellij.platform.rpc.RemoteApiProviderService
|
||||
@@ -64,6 +65,9 @@ interface KiloSessionRpcApi : RemoteApi<Unit> {
|
||||
/** Observe live session status changes. */
|
||||
suspend fun statuses(): Flow<Map<String, SessionStatusDto>>
|
||||
|
||||
/** Observe live per-session activity with the session's directory. */
|
||||
suspend fun activity(): Flow<Map<String, SessionActivityDto>>
|
||||
|
||||
/** Register a worktree directory override for a session. */
|
||||
suspend fun setDirectory(id: String, directory: String)
|
||||
|
||||
|
||||
@@ -46,6 +46,20 @@ data class SessionStatusDto(
|
||||
val requestID: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class SessionActivityKindDto {
|
||||
RUNNING,
|
||||
QUESTION,
|
||||
PLAN,
|
||||
PERMISSION,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SessionActivityDto(
|
||||
val directory: String,
|
||||
val kind: SessionActivityKindDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionListDto(
|
||||
val sessions: List<SessionDto>,
|
||||
|
||||
Reference in New Issue
Block a user