Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
057b48c
fix(jetbrains): render worktree session titles in regular weight
kirillk Aug 25, 2026
a2cd74b
fix(jetbrains): keep the account overlay hidden after a prompted work…
kirillk Aug 25, 2026
235d98d
fix(jetbrains): add new worktrees to the top of the list
kirillk Aug 25, 2026
53770f6
fix(jetbrains): keep the running icon after resuming a stopped session
kirillk Aug 25, 2026
047c989
fix(jetbrains): keep session popups inside the visible view
kirillk Aug 25, 2026
b629acc
fix(jetbrains): show failed sessions on their worktree row
kirillk Aug 25, 2026
f80d7d3
fix(jetbrains): clear the Agents tab dot once the attention has been …
kirillk Aug 25, 2026
a1ccea4
fix(jetbrains): badge failed sessions in session lists and raise the …
kirillk Aug 25, 2026
28d0f3f
fix(jetbrains): keep the Agents dot up until the attention is resolved
kirillk Aug 25, 2026
322426d
fix(jetbrains): anchor session popups on the card, not the session edge
kirillk Aug 25, 2026
64ab942
fix(jetbrains): keep shifted session popups pointing at the card
kirillk Aug 25, 2026
e1e0f75
fix(jetbrains): plain worktree labels, quieter icons, prune deleted s…
kirillk Aug 25, 2026
4078d7c
fix(jetbrains): hide session hover popup behind a blocking overlay
kirillk Aug 25, 2026
80e8213
fix(jetbrains): let overlays take the pointer over from the transcript
kirillk Aug 25, 2026
a5f62bc
fix(jetbrains): keep worktree PR badges clickable
kirillk Aug 25, 2026
20d547f
chore: ignore jetbrains config
kirillk Aug 25, 2026
b3e1988
Merge remote-tracking branch 'origin/main' into gentle-badger
kirillk Aug 25, 2026
cb7470b
chore: ignore JetBrains worktree state
kirillk Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/jetbrains-worktree-list-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@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, mark failed and waiting sessions on their worktree row and in session lists, keep the Agents tab notification dot up until every session that needs you is resolved, and keep session card popups inside the visible session view while pointing at their card.
5 changes: 5 additions & 0 deletions .changeset/jetbrains-worktree-pr-badge-clicks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---

Keep the PR badge in the JetBrains Agent Manager worktree list clickable and aligned with the rest of the row.
5 changes: 5 additions & 0 deletions .changeset/overlay-takes-hover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---

Let session overlays such as the connection banner take the pointer over from the transcript beneath them, so a covered card no longer stays hovered or keeps its popup open behind the overlay.
5 changes: 5 additions & 0 deletions .changeset/plain-worktree-headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---

Render Agent Manager worktree list labels in normal weight with quieter idle icons, tint monochrome row icons to the selection foreground while leaving status icons colored, and clear a deleted session's question/error status from the session list, worktree list, and tab attention dot.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ tsconfig.tsbuildinfo
.kilo/yarn.lock
.kilo/node_modules
.kilo/plans/*upstream-merge-report-*.md
**/.kilo/jetbrains.json
.kilocode/.gitignore
.kilocode/package.json
.kilocode/package-lock.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ class KiloBackendActivityManager(
is ChatEventDto.Error -> event.sessionID?.let { errors.add(it) }
is ChatEventDto.TurnOpen -> errors.remove(event.sessionID)
is ChatEventDto.SessionIdle -> clear(event.sessionID)
is ChatEventDto.SessionStatusChanged -> if (event.status.type == "idle") clear(event.sessionID)
is ChatEventDto.SessionStatusChanged -> when (event.status.type) {
"idle" -> clear(event.sessionID)
// Work restarted, so whatever ended the previous turn (a Stop publishes
// MessageAbortedError) is stale. Not every resume path publishes a turn event, so
// busy has to clear the error itself.
"busy" -> errors.remove(event.sessionID)
else -> Unit
}
else -> Unit
}
}
Expand All @@ -115,8 +122,11 @@ class KiloBackendActivityManager(
if (pending.values.any { it }) return SessionActivityKindDto.PLAN
return SessionActivityKindDto.QUESTION
}
if (id in errors) return SessionActivityKindDto.ERROR
// Live work outranks a past error: the status stream and the chat events are separate
// collectors, so a resumed session can go busy before the event that clears its error
// arrives, and the row must keep spinning instead of resting on the stale error.
if (busy) return SessionActivityKindDto.RUNNING
if (id in errors) return SessionActivityKindDto.ERROR
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ class KiloWorktreeRpcApiImpl : KiloWorktreeRpcApi {
val items = if (list.ok) managedWorktrees(parseWorktreeList(list.stdout)) else emptyList()
val store = worktreeNameStore(items) ?: base.resolve(".kilo").resolve(WORKTREE_NAMES_FILE)
val paths = worktreePaths(items).ifEmpty { listOf(path) }
appendWorktreeOrder(store, path, paths)
prependWorktreeOrder(store, path, paths)
return CreateWorktreeResultDto(worktree = WorktreeDto(path, dir.fileName.toString(), branch, path))
}

Expand Down Expand Up @@ -811,13 +811,12 @@ private fun syncWorktreeState(file: Path, paths: List<String>): WorktreeState {
return next
}

private fun appendWorktreeOrder(file: Path, path: String, paths: List<String>) {
private fun prependWorktreeOrder(file: Path, path: String, paths: List<String>) {
val state = readWorktreeState(file)
val set = paths.toSet()
val order = state.worktreeOrder.filter { it in set && !samePath(it, path) } +
paths.filter { it !in state.worktreeOrder && !samePath(it, path) } +
path
writeWorktreeState(file, state.copy(worktreeOrder = order.distinct()))
val rest = state.worktreeOrder.filter { it in set && !samePath(it, path) } +
paths.filter { it !in state.worktreeOrder && !samePath(it, path) }
writeWorktreeState(file, state.copy(worktreeOrder = (listOf(path) + rest).distinct()))
}

private fun removeWorktreeState(file: Path, path: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,8 @@ class KiloBackendActivityManagerTest {
statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))
start()

// Turn ends on an error: the session goes idle but the error must stay visible.
events.emit(ChatEventDto.Error("ses_1"))
await("ses_1", SessionActivityKindDto.ERROR)

// Turn ends: session goes idle but the error must stay visible.
statuses.value = mapOf("ses_1" to SessionStatusDto("idle"))
events.emit(ChatEventDto.SessionIdle("ses_1"))
await("ses_1", SessionActivityKindDto.ERROR)
Expand All @@ -116,6 +114,35 @@ class KiloBackendActivityManagerTest {
assertFalse("ses_1" in manager.activity.value)
}

@Test
fun `busy outranks a pending error so a resumed session runs`() = runBlocking {
directories["ses_1"] = "/repo/wt"
start()

// A Stop leaves the session errored and idle.
events.emit(ChatEventDto.Error("ses_1"))
await("ses_1", SessionActivityKindDto.ERROR)

// Resumed: busy arrives before anything clears the error.
statuses.value = mapOf("ses_1" to SessionStatusDto("busy"))

await("ses_1", SessionActivityKindDto.RUNNING)
}

@Test
fun `busy status event clears a pending error`() = runBlocking {
directories["ses_1"] = "/repo/wt"
start()

events.emit(ChatEventDto.Error("ses_1"))
await("ses_1", SessionActivityKindDto.ERROR)

events.emit(ChatEventDto.SessionStatusChanged("ses_1", SessionStatusDto("busy")))

withTimeout(5_000) { manager.activity.first { "ses_1" !in it } }
assertFalse("ses_1" in manager.activity.value)
}

@Test
fun `global error without session is ignored`() = runBlocking {
directories["ses_1"] = "/repo/wt"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,18 @@ class KiloWorktreeRpcApiImplTest {
}

@Test
fun `create records order so reload keeps creation order`() = runBlocking {
fun `create records newest worktree first so reload keeps it on top`() = runBlocking {
initRepo()

val first = assertNotNull(api.create(repo.toString(), CreateWorktreeRequestDto("zebra")).worktree)
val second = assertNotNull(api.create(repo.toString(), CreateWorktreeRequestDto("alpha")).worktree)

val listed = api.list(repo.toString()).worktrees.filter { !it.main }
assertEquals(listOf(first.path, second.path), listed.map { it.path })
assertEquals(listOf(first.path, second.path), readWorktreeState(repo.resolve(".kilo").resolve("jetbrains.json")).worktreeOrder)
assertEquals(listOf(second.path, first.path), listed.map { it.path })
assertEquals(
listOf(second.path, first.path),
readWorktreeState(repo.resolve(".kilo").resolve("jetbrains.json")).worktreeOrder,
)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ internal class KiloToolWindowSetupService(
toolWindow.contentManager.setSelectedContent(chatContent)
manager.newSession()

// Show a notification dot on the Agents tab whenever a worktree session needs attention.
// Notification dot on the Agents tab: up for as long as any worktree session is waiting
// on the user or has failed. Viewing the tab must not clear it — only resolving the
// attention does, so the dot stays a reliable "something still needs you" signal.
val dot = cs.launch {
project.service<KiloSessionService>().activity.map(::sessionAttentionNeeded).collect { needed ->
withContext(Dispatchers.Main) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ 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.
* 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.
*
* The dot mirrors that state for as long as it lasts, across every worktree and session. Viewing the
* tab does not clear it: only resolving the attention does, by answering the prompt or running the
* session again.
*/
internal fun sessionAttentionNeeded(activity: Map<String, SessionActivityDto>): Boolean =
activity.values.any {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import ai.kilocode.client.ui.list.ActiveListMetrics
import ai.kilocode.client.ui.list.ActiveListReorder
import ai.kilocode.client.ui.list.ActiveListSelection
import ai.kilocode.client.ui.list.ActiveListSurface
import ai.kilocode.client.ui.list.ActiveListWeight
import ai.kilocode.client.ui.list.activeListToolWindowBackground
import ai.kilocode.client.vfs.KiloVfsManager
import ai.kilocode.rpc.dto.RemoveWorktreeResultDto
Expand Down Expand Up @@ -95,7 +96,11 @@ class AgentManagerPanel(
private val group = ActionManager.getInstance().getAction("Kilo.Worktree.RowMenu") as? ActionGroup ?: DefaultActionGroup()
private val list = ActiveList(
KiloBundle.message("worktree.empty"),
cfg = ActiveListConfig(hoverActions = true),
cfg = ActiveListConfig(
hoverActions = true,
title = ActiveListWeight.PLAIN,
header = ActiveListWeight.PLAIN,
),
surface = ActiveListSurface.ToolWindow,
showSearch = false,
onCell = { _, _ -> },
Expand Down Expand Up @@ -494,6 +499,7 @@ class AgentManagerPanel(
override val description: String get() = WorktreeTitle.fallback(dto.path)
override val tooltip: String? get() = null
override val icon = WorktreeIcons.forRow(progress != null, kind, dto.locked, current)
override val tinted: Boolean get() = WorktreeIcons.neutral(icon)
override val section: String? get() = if (current) null else KiloBundle.message("worktree.section.local")
override val search: String get() = listOfNotNull(dto.name, dto.branch, dto.path, dto.lockReason).joinToString(" ")
private val customName: String? get() = WorktreeTitle.custom(dto.name, dto.path)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
package ai.kilocode.client.agentManager.worktree

import ai.kilocode.client.session.SessionActivityKind
import ai.kilocode.client.session.toKind
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) }
.mapValues { (_, items) -> items.map { it.kind.toKind() }.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
SessionActivityKindDto.ERROR -> SessionActivityKind.ERROR
}

/**
* Precedence for a worktree holding several sessions: anything waiting on the user first, then live
* work, then a session left in an error. Running beats error so one stopped session cannot hide the
* spinner of a sibling that is still working.
*/
private fun rank(kind: SessionActivityKind): Int = when (kind) {
SessionActivityKind.PERMISSION -> 0
SessionActivityKind.QUESTION -> 1
SessionActivityKind.PLAN -> 2
SessionActivityKind.ERROR -> 3
SessionActivityKind.RUNNING -> 4
SessionActivityKind.RUNNING -> 3
SessionActivityKind.ERROR -> 4
SessionActivityKind.LOGIN_REQUIRED -> 5
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class WorktreeController(
edt {
val main = result.worktrees.firstOrNull { it.main }
val extra = result.worktrees.filter { !it.main }
val rows = extra + pending.values
val rows = pending.values.toList().asReversed() + extra
current = main
model.replaceAll(rows)
cache().putAll(rows)
Expand Down Expand Up @@ -121,7 +121,7 @@ class WorktreeController(
edt {
pending[temp.id] = temp
tasks[temp.id] = KiloBundle.message("worktree.progress.creating")
model.add(temp)
model.add(0, temp)
onSelect?.invoke(temp.id)
}
cs.launch {
Expand All @@ -136,7 +136,7 @@ class WorktreeController(
edt {
pending[temp.id] = temp
tasks[temp.id] = KiloBundle.message("worktree.progress.creating")
model.add(temp)
model.add(0, temp)
onSelect?.invoke(temp.id)
}
cs.launch {
Expand All @@ -157,7 +157,7 @@ class WorktreeController(
tasks.remove(temp.id)
val idx = model.getElementIndex(temp)
if (created != null) {
if (idx >= 0) model.setElementAt(created, idx) else model.add(created)
if (idx >= 0) model.setElementAt(created, idx) else model.add(0, created)
cache().put(created)
prompt?.let { service<PendingWorktreePrompt>().put(created.path, it) }
onSelect?.invoke(created.id)
Expand Down Expand Up @@ -225,7 +225,7 @@ class WorktreeController(
val temp = WorktreeDto("pending:$branch:${System.nanoTime()}", branch, branch, "pending:$branch")
pending[temp.id] = temp
tasks[temp.id] = label(MoveStage.CAPTURING)
model.add(temp)
model.add(0, temp)
onSelect?.invoke(temp.id)
cs.launch {
var stage = MoveStage.CAPTURING
Expand All @@ -243,7 +243,7 @@ class WorktreeController(
tasks.remove(temp.id)
val worktree = event.worktree ?: return@edt
val idx = model.getElementIndex(temp)
if (idx >= 0) model.setElementAt(worktree, idx) else model.add(worktree)
if (idx >= 0) model.setElementAt(worktree, idx) else model.add(0, worktree)
cache().put(worktree)
// Queue the forked session for the editor the selection is about to
// open; the tab's identity stays the worktree path alone.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ internal object WorktreeIcons {

/**
* Leading icon for a worktree row. At rest the row shows what it is — the local machine, a locked
* checkout, or a branch checkout — while a running or waiting session takes the slot over so the
* list still surfaces activity at a glance. An operation on the row ([busy]) outranks all of it,
* and an errored session falls back to the resting glyph instead of shouting in the leading slot.
* checkout, or a branch checkout — while a running, waiting or failed session takes the slot over
* so the list still surfaces activity at a glance. An operation on the row ([busy]) outranks all
* of it.
*/
fun forRow(
busy: Boolean,
Expand All @@ -37,12 +37,16 @@ internal object WorktreeIcons {
SessionActivityKind.QUESTION,
SessionActivityKind.PERMISSION,
SessionActivityKind.PLAN,
SessionActivityKind.LOGIN_REQUIRED -> kind.icon()
SessionActivityKind.ERROR, null -> when {
SessionActivityKind.LOGIN_REQUIRED,
SessionActivityKind.ERROR -> kind.icon()
null -> when {
current -> local
locked -> this.locked
else -> branch
}
}
}

/** The monochrome at-rest glyphs that follow the row text color; status icons are excluded. */
fun neutral(icon: Icon?): Boolean = icon === local || icon === locked || icon === branch
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ai.kilocode.client.ui.list.ActiveListMenu
import ai.kilocode.client.ui.list.ActiveListRowHeight
import ai.kilocode.client.ui.list.ActiveListSelection
import ai.kilocode.client.ui.list.ActiveListSurface
import ai.kilocode.client.ui.list.ActiveListWeight
import ai.kilocode.client.ui.list.activeListToolWindowBackground
import ai.kilocode.client.ui.layout.Stack
import ai.kilocode.client.vfs.KiloVfsManager
Expand Down Expand Up @@ -95,6 +96,7 @@ class WorktreeSessionEditorPanel(
description = false,
selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION,
hoverActions = true,
title = ActiveListWeight.PLAIN,
),
surface = ActiveListSurface.ToolWindow,
showSearch = false,
Expand Down
Loading
Loading