mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(jetbrains): show worktree progress in agent rows
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show worktree creation and move progress in the Agent Manager row instead of the chat dock.
|
||||
+8
-3
@@ -99,6 +99,7 @@ internal class KiloToolWindowSetupService(
|
||||
workspace.directory,
|
||||
cs,
|
||||
activity = project.service<KiloSessionService>().activity,
|
||||
abort = { id, dir -> project.service<KiloSessionService>().abort(id, dir) },
|
||||
)
|
||||
val agentManagerPanel = AgentManagerPanel(manager, worktrees, project)
|
||||
|
||||
@@ -138,13 +139,17 @@ internal class KiloToolWindowSetupService(
|
||||
agentContent.putUserData(ToolWindow.SHOW_CONTENT_ICON, true)
|
||||
toolWindow.contentManager.addContent(chatContent)
|
||||
toolWindow.contentManager.addContent(agentContent)
|
||||
val agents = { toolWindow.contentManager.setSelectedContent(agentContent, true) }
|
||||
// The chat branch dock's "New Worktree" action switches to the Agent Manager tab and
|
||||
// opens its New Worktree dialog, matching the VS Code sidebar dock.
|
||||
manager.onNewWorktree = {
|
||||
Telemetry.send("New Worktree Clicked", mapOf("surface" to "chat_dock"))
|
||||
agentManagerPanel.configure {
|
||||
toolWindow.contentManager.setSelectedContent(agentContent)
|
||||
}
|
||||
agents()
|
||||
agentManagerPanel.configure()
|
||||
}
|
||||
manager.onMoveToWorktree = { id, dir ->
|
||||
agents()
|
||||
agentManagerPanel.move(id, dir)
|
||||
}
|
||||
val listener = object : ContentManagerListener {
|
||||
override fun selectionChanged(event: ContentManagerEvent) {
|
||||
|
||||
+1
-20
@@ -2,19 +2,16 @@ package ai.kilocode.client.actions
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.header.ChatDockKeys
|
||||
import ai.kilocode.rpc.dto.MoveStage
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.ex.ActionUtil
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
|
||||
/**
|
||||
* "Move to Worktree" action shown in the chat branch dock. Visible only when there is something to
|
||||
* move (a conversation or local changes). While a move runs it stays visible but disabled, showing a
|
||||
* spinner and the current stage.
|
||||
* move (a conversation or local changes).
|
||||
*/
|
||||
class ChatMoveToWorktreeAction : AnAction(), DumbAware {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
|
||||
@@ -26,14 +23,6 @@ class ChatMoveToWorktreeAction : AnAction(), DumbAware {
|
||||
e.presentation.isEnabledAndVisible = false
|
||||
return
|
||||
}
|
||||
if (dock.moving()) {
|
||||
e.presentation.isVisible = true
|
||||
e.presentation.isEnabled = false
|
||||
e.presentation.icon = AnimatedIcon.Default.INSTANCE
|
||||
e.presentation.text = progressLabel(dock.moveStage())
|
||||
e.presentation.description = null
|
||||
return
|
||||
}
|
||||
e.presentation.isEnabledAndVisible = dock.moveEnabled()
|
||||
e.presentation.icon = BRANCH
|
||||
e.presentation.text = KiloBundle.message("session.dock.move")
|
||||
@@ -44,14 +33,6 @@ class ChatMoveToWorktreeAction : AnAction(), DumbAware {
|
||||
e.getData(ChatDockKeys.DOCK)?.triggerMove()
|
||||
}
|
||||
|
||||
private fun progressLabel(stage: MoveStage?): String = when (stage) {
|
||||
MoveStage.CAPTURING -> KiloBundle.message("session.dock.progress.capturing")
|
||||
MoveStage.CREATING -> KiloBundle.message("session.dock.progress.creating")
|
||||
MoveStage.TRANSFERRING -> KiloBundle.message("session.dock.progress.transferring")
|
||||
MoveStage.FORKING -> KiloBundle.message("session.dock.progress.forking")
|
||||
else -> KiloBundle.message("session.dock.move")
|
||||
}
|
||||
|
||||
private fun moveTooltip(count: Int): String = when (count) {
|
||||
0 -> KiloBundle.message("session.dock.move.tooltip.empty")
|
||||
1 -> KiloBundle.message("session.dock.move.tooltip.one")
|
||||
|
||||
+26
-26
@@ -13,7 +13,7 @@ import ai.kilocode.client.agentManager.worktree.WorktreeEditorMatchers
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeSessionEditorMatcher
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeSessionEditorKind
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeTitle
|
||||
import ai.kilocode.client.agentManager.worktree.ensureWorktreeSessionEditorKind
|
||||
import ai.kilocode.client.agentManager.worktree.openWorktreeSession
|
||||
import ai.kilocode.client.agentManager.worktree.normalizeWorktreePath
|
||||
import ai.kilocode.client.agentManager.worktree.worktreeSessionParams
|
||||
import ai.kilocode.client.ui.prTooltip
|
||||
@@ -95,7 +95,7 @@ class AgentManagerPanel(
|
||||
(row as? WorktreeRow)?.dto?.takeIf { canRename(it) || canDelete(it) || canOpenPr(it) || canOpenDiff(it) }
|
||||
}),
|
||||
reorder = ActiveListReorder(
|
||||
movable = { row -> row is WorktreeRow && !row.current && !row.pending && !row.deleting },
|
||||
movable = { row -> row is WorktreeRow && !row.current && row.progress == null },
|
||||
onMove = { move -> controller.reorder(move.keys) },
|
||||
),
|
||||
)
|
||||
@@ -114,9 +114,10 @@ class AgentManagerPanel(
|
||||
// Focus the list so the freshly created worktree renders as an active selection rather
|
||||
// than the inactive highlight it would get while focus stays on the toolbar.
|
||||
if (list.select(key)) list.focusList()
|
||||
item(key)?.takeIf { !controller.isPending(it.id) }?.let { open(it, focus = false) }
|
||||
item(key)?.takeIf { controller.progress(it.id) == null }?.let { open(it, focus = false) }
|
||||
}
|
||||
controller.onCreateFailure = { err -> notifyCreateFailed(err) }
|
||||
controller.onMoveFailure = { err -> notifyMoveFailed(err) }
|
||||
controller.onRemoveSuccess = { item, index -> onRemoved(item, index) }
|
||||
controller.onActivityChanged = {
|
||||
sync()
|
||||
@@ -155,7 +156,7 @@ class AgentManagerPanel(
|
||||
}
|
||||
|
||||
/** Opens the New Worktree dialog. */
|
||||
fun configure(afterClose: (() -> Unit)? = null) {
|
||||
fun configure() {
|
||||
val target = project ?: return
|
||||
NewWorktreeDialog(
|
||||
this,
|
||||
@@ -168,9 +169,10 @@ class AgentManagerPanel(
|
||||
controller.create(branch, base, prompt = prompt)
|
||||
},
|
||||
).show()
|
||||
afterClose?.invoke()
|
||||
}
|
||||
|
||||
internal fun move(sessionId: String, directory: String) = controller.move(sessionId, directory)
|
||||
|
||||
private fun remove(item: WorktreeDto, force: Boolean) {
|
||||
controller.remove(item, force, onFailure = { result -> notifyFailed(item, result, force) })
|
||||
}
|
||||
@@ -205,9 +207,8 @@ class AgentManagerPanel(
|
||||
|
||||
private fun open(item: WorktreeDto, focus: Boolean) {
|
||||
val target = project ?: return
|
||||
if (controller.isPending(item.id)) return
|
||||
ensureWorktreeSessionEditorKind()
|
||||
target.service<KiloVfsManager>().open(WorktreeSessionEditorKind.ID, worktreeSessionParams(item), focus)
|
||||
if (controller.progress(item.id) != null) return
|
||||
openWorktreeSession(target, item, controller.takeSession(item.id), focus)
|
||||
}
|
||||
|
||||
private fun close(item: WorktreeDto) {
|
||||
@@ -226,13 +227,13 @@ class AgentManagerPanel(
|
||||
/** The PR URL for [item], or null when it has none or is not in a stable, openable state. */
|
||||
private fun prUrl(item: WorktreeDto?): String? {
|
||||
if (item == null || item.main) return null
|
||||
if (controller.isPending(item.id) || controller.isDeleting(item.id)) return null
|
||||
if (controller.progress(item.id) != null) return null
|
||||
return prs[normalizeWorktreePath(item.path)]?.url
|
||||
}
|
||||
|
||||
internal fun canOpenDiff(item: WorktreeDto?): Boolean {
|
||||
if (item == null || item.main || project == null) return false
|
||||
return !controller.isPending(item.id) && !controller.isDeleting(item.id)
|
||||
return controller.progress(item.id) == null
|
||||
}
|
||||
|
||||
internal fun openDiff(item: WorktreeDto) {
|
||||
@@ -255,8 +256,7 @@ class AgentManagerPanel(
|
||||
}
|
||||
|
||||
private fun deletable(item: WorktreeDto?): Boolean {
|
||||
if (!worktreeDeletable(item, item?.id?.let(controller::isPending) == true)) return false
|
||||
return item?.id?.let(controller::isDeleting) != true
|
||||
return worktreeDeletable(item, item?.id?.let(controller::progress) != null)
|
||||
}
|
||||
|
||||
private fun renameable(item: WorktreeDto?): Boolean {
|
||||
@@ -266,7 +266,7 @@ class AgentManagerPanel(
|
||||
|
||||
private fun renameVisible(item: WorktreeDto?): Boolean {
|
||||
if (item == null || item.main) return false
|
||||
return !controller.isPending(item.id) && !controller.isDeleting(item.id)
|
||||
return controller.progress(item.id) == null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,6 +311,10 @@ class AgentManagerPanel(
|
||||
KiloNotifications.error(project, KiloBundle.message("worktree.create.failed.title"), err)
|
||||
}
|
||||
|
||||
private fun notifyMoveFailed(err: String?) {
|
||||
KiloNotifications.error(project, KiloBundle.message("worktree.move.failed.title"), err)
|
||||
}
|
||||
|
||||
/** Surfaces a failed removal; offers a force-delete retry when git reported a lock. */
|
||||
private fun notifyFailed(item: WorktreeDto, result: RemoveWorktreeResultDto, forced: Boolean) {
|
||||
val title = KiloBundle.message("worktree.delete.failed.title", item.name)
|
||||
@@ -352,8 +356,7 @@ class AgentManagerPanel(
|
||||
val current = controller.current?.let { item ->
|
||||
WorktreeRow(
|
||||
item,
|
||||
pending = false,
|
||||
deleting = false,
|
||||
progress = null,
|
||||
kind = controller.kind(item.path),
|
||||
stats = null,
|
||||
pr = null,
|
||||
@@ -368,8 +371,7 @@ class AgentManagerPanel(
|
||||
service<WorktreeNameCache>().putPr(item.path, pull)
|
||||
WorktreeRow(
|
||||
item,
|
||||
controller.isPending(item.id),
|
||||
controller.isDeleting(item.id),
|
||||
controller.progress(item.id),
|
||||
controller.kind(item.path),
|
||||
stats[key],
|
||||
pull,
|
||||
@@ -418,6 +420,7 @@ class AgentManagerPanel(
|
||||
override fun dispose() {
|
||||
controller.onSelect = null
|
||||
controller.onCreateFailure = null
|
||||
controller.onMoveFailure = null
|
||||
controller.onRemoveSuccess = null
|
||||
controller.onActivityChanged = null
|
||||
}
|
||||
@@ -469,8 +472,7 @@ class AgentManagerPanel(
|
||||
*/
|
||||
private inner class WorktreeRow(
|
||||
val dto: WorktreeDto,
|
||||
val pending: Boolean,
|
||||
override val deleting: Boolean,
|
||||
override val progress: String?,
|
||||
val kind: SessionActivityKind?,
|
||||
val stats: WorktreeStatsDto?,
|
||||
val pr: WorktreePrDto?,
|
||||
@@ -481,13 +483,13 @@ class AgentManagerPanel(
|
||||
override val title: String get() = if (current) dto.branch else WorktreeTitle.text(dto.name, dto.path, pr)
|
||||
override val description: String get() = WorktreeTitle.fallback(dto.path)
|
||||
override val tooltip: String? get() = null
|
||||
override val icon = WorktreeIcons.forRow(pending, kind, dto.locked, current)
|
||||
override val icon = WorktreeIcons.forRow(progress != null, kind, dto.locked, current)
|
||||
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)
|
||||
override val metrics: ActiveListMetrics?
|
||||
get() {
|
||||
if (pending || deleting) return null
|
||||
if (progress != null) return null
|
||||
val s = stats
|
||||
val p = pr
|
||||
if (s == null && p == null) return null
|
||||
@@ -506,8 +508,7 @@ class AgentManagerPanel(
|
||||
override fun equals(other: Any?): Boolean {
|
||||
val row = other as? WorktreeRow ?: return false
|
||||
return dto == row.dto &&
|
||||
pending == row.pending &&
|
||||
deleting == row.deleting &&
|
||||
progress == row.progress &&
|
||||
kind == row.kind &&
|
||||
stats == row.stats &&
|
||||
pr == row.pr &&
|
||||
@@ -516,8 +517,7 @@ class AgentManagerPanel(
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = dto.hashCode()
|
||||
result = 31 * result + pending.hashCode()
|
||||
result = 31 * result + deleting.hashCode()
|
||||
result = 31 * result + (progress?.hashCode() ?: 0)
|
||||
result = 31 * result + (kind?.hashCode() ?: 0)
|
||||
result = 31 * result + (stats?.hashCode() ?: 0)
|
||||
result = 31 * result + (pr?.hashCode() ?: 0)
|
||||
@@ -537,4 +537,4 @@ class AgentManagerPanel(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun worktreeDeletable(item: WorktreeDto?, pending: Boolean): Boolean = item != null && !item.main && !pending
|
||||
internal fun worktreeDeletable(item: WorktreeDto?, busy: Boolean): Boolean = item != null && !item.main && !busy
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
package ai.kilocode.client.agentManager.worktree
|
||||
|
||||
import ai.kilocode.client.KiloNotifications
|
||||
import ai.kilocode.client.app.KiloSessionService
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.telemetry.Telemetry
|
||||
import ai.kilocode.client.vfs.KiloVfsManager
|
||||
import ai.kilocode.log.KiloLog
|
||||
import ai.kilocode.rpc.dto.MoveProgressDto
|
||||
import ai.kilocode.rpc.dto.MoveStage
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Sidebar "Move to Worktree": aborts the running session, generates a friendly branch name, then
|
||||
* drives the backend move flow (capture → create → transfer → fork). On success it opens the
|
||||
* worktree session editor on the forked session; on error it notifies through the worktree pattern.
|
||||
* The source sidebar session stays in the sidebar (aborted, not deleted), matching VS Code fork
|
||||
* semantics.
|
||||
*/
|
||||
internal class MoveToWorktree(
|
||||
private val project: Project,
|
||||
private val cs: CoroutineScope,
|
||||
) {
|
||||
private companion object {
|
||||
private val LOG = KiloLog.create(MoveToWorktree::class.java)
|
||||
}
|
||||
|
||||
/** [progress] is invoked on the EDT for each stage so the dock button can reflect it. */
|
||||
@RequiresEdt
|
||||
fun launch(directory: String, sessionId: String, progress: (MoveStage, String?) -> Unit) {
|
||||
cs.launch {
|
||||
runCatching { service<KiloSessionService>().abort(sessionId, directory) }
|
||||
.onFailure { LOG.info("worktree move: abort failed (session may be idle): ${it.message}") }
|
||||
val known = service<KiloWorktreeService>().listBranches(directory).branches.toSet()
|
||||
val branch = WorktreeNames.generate(known)
|
||||
var stage = MoveStage.CAPTURING
|
||||
val flow = service<KiloWorktreeService>().moveToWorktree(directory, sessionId, branch)
|
||||
flow.collect { event ->
|
||||
if (event.stage != MoveStage.ERROR) stage = event.stage
|
||||
withContext(Dispatchers.Main) { handle(event, stage, progress) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun handle(event: MoveProgressDto, failing: MoveStage, progress: (MoveStage, String?) -> Unit) {
|
||||
progress(event.stage, event.detail)
|
||||
when (event.stage) {
|
||||
MoveStage.DONE -> {
|
||||
val worktree = event.worktree ?: return
|
||||
ensureWorktreeSessionEditorKind()
|
||||
project.service<KiloVfsManager>().open(
|
||||
WorktreeSessionEditorKind.ID,
|
||||
worktreeSessionParams(worktree, session = event.session),
|
||||
)
|
||||
Telemetry.send("Continue in Worktree", mapOf("surface" to "sidebar"))
|
||||
}
|
||||
MoveStage.ERROR -> {
|
||||
LOG.warn("worktree move failed stage=$failing error=${event.error}")
|
||||
KiloNotifications.error(project, KiloBundle.message("session.dock.move.failed.title"), event.error)
|
||||
Telemetry.send("Continue in Worktree Failed", mapOf("stage" to failing.name))
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
-6
@@ -6,18 +6,19 @@ import ai.kilocode.client.util.edt
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.rpc.dto.CreateWorktreeRequestDto
|
||||
import ai.kilocode.rpc.dto.CreateWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.MoveStage
|
||||
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 com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
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
|
||||
|
||||
/**
|
||||
* Owns the worktree list model and drives the [KiloWorktreeService] off the EDT. Model mutations
|
||||
@@ -28,13 +29,17 @@ class WorktreeController(
|
||||
val directory: String,
|
||||
private val cs: CoroutineScope,
|
||||
activity: StateFlow<Map<String, SessionActivityDto>> = MutableStateFlow(emptyMap()),
|
||||
private val abort: suspend (String, String) -> Unit = { _, _ -> },
|
||||
private val telemetry: (String, Map<String, String>) -> Unit = { event, props -> Telemetry.send(event, props) },
|
||||
) {
|
||||
val model = CollectionListModel<WorktreeDto>()
|
||||
private val pending = LinkedHashMap<String, WorktreeDto>()
|
||||
private val deleting = Collections.synchronizedSet(LinkedHashSet<String>())
|
||||
private val tasks = LinkedHashMap<String, String>()
|
||||
private val moves = LinkedHashSet<String>()
|
||||
private val sessions = LinkedHashMap<String, String>()
|
||||
var onSelect: ((String) -> Unit)? = null
|
||||
var onCreateFailure: ((String?) -> Unit)? = null
|
||||
var onMoveFailure: ((String?) -> Unit)? = null
|
||||
var onRemoveSuccess: ((WorktreeDto, Int) -> Unit)? = null
|
||||
var onActivityChanged: (() -> Unit)? = null
|
||||
|
||||
@@ -73,7 +78,9 @@ class WorktreeController(
|
||||
|
||||
fun isPending(id: String): Boolean = id in pending
|
||||
|
||||
fun isDeleting(id: String): Boolean = id in deleting
|
||||
fun progress(id: String): String? = tasks[id]
|
||||
|
||||
fun takeSession(id: String): String? = sessions.remove(id)
|
||||
|
||||
fun kind(path: String): SessionActivityKind? = kinds[normalizeWorktreePath(path)]
|
||||
|
||||
@@ -116,6 +123,7 @@ class WorktreeController(
|
||||
val temp = WorktreeDto(id, branch, branch, id)
|
||||
edt {
|
||||
pending[temp.id] = temp
|
||||
tasks[temp.id] = KiloBundle.message("worktree.progress.creating")
|
||||
model.add(temp)
|
||||
onSelect?.invoke(temp.id)
|
||||
}
|
||||
@@ -130,6 +138,7 @@ class WorktreeController(
|
||||
val temp = WorktreeDto(id, KiloBundle.message("worktree.import.pr.section"), "", id)
|
||||
edt {
|
||||
pending[temp.id] = temp
|
||||
tasks[temp.id] = KiloBundle.message("worktree.progress.creating")
|
||||
model.add(temp)
|
||||
onSelect?.invoke(temp.id)
|
||||
}
|
||||
@@ -148,6 +157,7 @@ class WorktreeController(
|
||||
val created = result.worktree
|
||||
edt {
|
||||
pending.remove(temp.id)
|
||||
tasks.remove(temp.id)
|
||||
val idx = model.getElementIndex(temp)
|
||||
if (created != null) {
|
||||
if (idx >= 0) model.setElementAt(created, idx) else model.add(created)
|
||||
@@ -174,13 +184,14 @@ class WorktreeController(
|
||||
onSuccess: () -> Unit = {},
|
||||
onFailure: (RemoveWorktreeResultDto) -> Unit = {},
|
||||
) {
|
||||
if (!deleting.add(dto.id)) return
|
||||
if (dto.id in tasks) return
|
||||
tasks[dto.id] = KiloBundle.message("common.deleting")
|
||||
edt { refresh(dto) }
|
||||
cs.launch {
|
||||
val result = service.remove(directory, dto.path, dto.branch, force)
|
||||
if (result.ok) {
|
||||
edt {
|
||||
deleting.remove(dto.id)
|
||||
tasks.remove(dto.id)
|
||||
val index = model.getElementIndex(dto)
|
||||
model.remove(dto)
|
||||
cache().remove(dto.path)
|
||||
@@ -193,7 +204,7 @@ class WorktreeController(
|
||||
// Removal failed: git still tracks the worktree. Keep the row and reconcile with
|
||||
// ground truth so a stale optimistic delete can't make the entry reappear later.
|
||||
edt {
|
||||
deleting.remove(dto.id)
|
||||
tasks.remove(dto.id)
|
||||
refresh(dto)
|
||||
telemetry(
|
||||
"Worktree Delete Failed",
|
||||
@@ -205,6 +216,46 @@ class WorktreeController(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun move(sessionId: String, source: String = directory) {
|
||||
if (!moves.add(sessionId)) return
|
||||
val branch = suggestName()
|
||||
val temp = WorktreeDto("pending:$branch:${System.nanoTime()}", branch, branch, "pending:$branch")
|
||||
pending[temp.id] = temp
|
||||
tasks[temp.id] = label(MoveStage.CAPTURING)
|
||||
model.add(temp)
|
||||
onSelect?.invoke(temp.id)
|
||||
cs.launch {
|
||||
var stage = MoveStage.CAPTURING
|
||||
runCatching {
|
||||
abort(sessionId, source)
|
||||
service.moveToWorktree(source, sessionId, branch).collect { event ->
|
||||
edt {
|
||||
if (event.stage != MoveStage.ERROR) stage = event.stage
|
||||
tasks[temp.id] = label(event.stage)
|
||||
refresh(temp)
|
||||
when (event.stage) {
|
||||
MoveStage.DONE -> {
|
||||
moves.remove(sessionId)
|
||||
pending.remove(temp.id)
|
||||
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)
|
||||
cache().put(worktree)
|
||||
event.session?.let { sessions[worktree.id] = it }
|
||||
onSelect?.invoke(worktree.id)
|
||||
telemetry("Continue in Worktree", mapOf("surface" to "sidebar"))
|
||||
}
|
||||
MoveStage.ERROR -> failMove(sessionId, temp, event.error, stage)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure { err -> edt { failMove(sessionId, temp, err.message, stage) } }
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(
|
||||
dto: WorktreeDto,
|
||||
name: String,
|
||||
@@ -281,6 +332,24 @@ class WorktreeController(
|
||||
return (0 until model.size).firstOrNull { model.getElementAt(it).id == id } ?: -1
|
||||
}
|
||||
|
||||
private fun failMove(session: String, temp: WorktreeDto, err: String?, stage: MoveStage) {
|
||||
moves.remove(session)
|
||||
pending.remove(temp.id)
|
||||
tasks.remove(temp.id)
|
||||
model.remove(temp)
|
||||
onMoveFailure?.invoke(err)
|
||||
telemetry("Continue in Worktree Failed", mapOf("stage" to stage.name))
|
||||
}
|
||||
|
||||
private fun label(stage: MoveStage): String = when (stage) {
|
||||
MoveStage.CAPTURING -> KiloBundle.message("worktree.progress.capturing")
|
||||
MoveStage.CREATING -> KiloBundle.message("worktree.progress.creating")
|
||||
MoveStage.TRANSFERRING -> KiloBundle.message("worktree.progress.transferring")
|
||||
MoveStage.FORKING -> KiloBundle.message("worktree.progress.starting")
|
||||
MoveStage.DONE -> ""
|
||||
MoveStage.ERROR -> ""
|
||||
}
|
||||
|
||||
private fun cache(): WorktreeNameCache {
|
||||
return ApplicationManager.getApplication().service()
|
||||
}
|
||||
|
||||
+4
-4
@@ -22,16 +22,16 @@ 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. Creation ([pending]) outranks all of it, and an
|
||||
* errored session falls back to the resting glyph instead of shouting in the leading slot.
|
||||
* 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.
|
||||
*/
|
||||
fun forRow(
|
||||
pending: Boolean,
|
||||
busy: Boolean,
|
||||
kind: SessionActivityKind? = null,
|
||||
locked: Boolean = false,
|
||||
current: Boolean = false,
|
||||
): Icon {
|
||||
if (pending) return spinner
|
||||
if (busy) return spinner
|
||||
return when (kind) {
|
||||
SessionActivityKind.RUNNING -> running
|
||||
SessionActivityKind.QUESTION,
|
||||
|
||||
+6
@@ -7,6 +7,7 @@ import ai.kilocode.client.session.SessionUiFactory
|
||||
import ai.kilocode.client.vfs.KiloEditorKind
|
||||
import ai.kilocode.client.vfs.KiloEditorKindRegistry
|
||||
import ai.kilocode.client.vfs.KiloVirtualFile
|
||||
import ai.kilocode.client.vfs.KiloVfsManager
|
||||
import ai.kilocode.rpc.dto.WorktreeDto
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.service
|
||||
@@ -62,3 +63,8 @@ internal fun unregisterWorktreeSessionEditorKind() {
|
||||
internal fun worktreeSessionParams(item: WorktreeDto, session: String? = null): Map<String, String> = linkedMapOf(
|
||||
"path" to item.path,
|
||||
).apply { session?.takeIf { it.isNotBlank() }?.let { put("session", it) } }
|
||||
|
||||
internal fun openWorktreeSession(project: Project, worktree: WorktreeDto, session: String? = null, focus: Boolean = true) {
|
||||
ensureWorktreeSessionEditorKind()
|
||||
project.service<KiloVfsManager>().open(WorktreeSessionEditorKind.ID, worktreeSessionParams(worktree, session), focus)
|
||||
}
|
||||
|
||||
+3
-5
@@ -557,7 +557,7 @@ class WorktreeSessionEditorPanel(
|
||||
private inner class SessionRow(
|
||||
val session: SessionDto,
|
||||
val kind: SessionActivityKind?,
|
||||
override val deleting: Boolean = false,
|
||||
private val deleting: Boolean = false,
|
||||
// Live title of the open session, if any; reflects the agent-generated name as it streams in
|
||||
// before the listed snapshot catches up.
|
||||
private val live: String? = null,
|
||||
@@ -572,11 +572,9 @@ class WorktreeSessionEditorPanel(
|
||||
return name
|
||||
}
|
||||
override val tooltip: String get() = title
|
||||
override val progress: String? get() = if (deleting) KiloBundle.message("common.deleting") else null
|
||||
override val badges: List<ActiveListBadge>
|
||||
get() {
|
||||
if (deleting) return emptyList()
|
||||
return listOfNotNull(kind?.let { ActiveListBadge(it.label(), it.style()) })
|
||||
}
|
||||
get() = listOfNotNull(kind?.let { ActiveListBadge(it.label(), it.style()) })
|
||||
override val section: String get() = HistoryTime.title(HistoryTime.section(item))
|
||||
override val search: String get() = listOf(session.title, session.id, session.directory).joinToString(" ")
|
||||
}
|
||||
|
||||
+6
@@ -21,6 +21,12 @@ interface SessionManager {
|
||||
/** Opens the New Worktree flow. No-op unless [supportsNewWorktree] is true. */
|
||||
fun newWorktree() {}
|
||||
|
||||
/** Whether this surface can move the current chat into a worktree (sidebar only). */
|
||||
val supportsMoveToWorktree: Boolean get() = false
|
||||
|
||||
/** Opens the Move to Worktree flow. No-op unless [supportsMoveToWorktree] is true. */
|
||||
fun moveToWorktree(sessionId: String, directory: String) {}
|
||||
|
||||
fun showHistory(back: (() -> Unit)? = null)
|
||||
|
||||
fun openSession(ref: SessionRef)
|
||||
|
||||
+9
@@ -52,12 +52,21 @@ class SessionSidePanelManager(
|
||||
/** Wired by the tool window to open the Agent Manager's New Worktree flow from the chat dock. */
|
||||
var onNewWorktree: (() -> Unit)? = null
|
||||
|
||||
/** Wired by the tool window to move the current chat into an Agent Manager worktree row. */
|
||||
var onMoveToWorktree: ((String, String) -> Unit)? = null
|
||||
|
||||
override val supportsNewWorktree: Boolean get() = onNewWorktree != null
|
||||
|
||||
override val supportsMoveToWorktree: Boolean get() = onMoveToWorktree != null
|
||||
|
||||
override fun newWorktree() {
|
||||
onNewWorktree?.invoke()
|
||||
}
|
||||
|
||||
override fun moveToWorktree(sessionId: String, directory: String) {
|
||||
onMoveToWorktree?.invoke(sessionId, directory)
|
||||
}
|
||||
|
||||
val defaultFocusedComponent: JComponent? get() = currentUi()?.defaultFocusedComponent ?: (panel as? HistoryPanel)?.defaultFocusedComponent
|
||||
|
||||
@RequiresEdt
|
||||
|
||||
+4
-7
@@ -44,7 +44,6 @@ import ai.kilocode.client.session.ui.attachment.attachmentParams
|
||||
import ai.kilocode.client.session.ui.attachment.ensureAttachmentEditorKind
|
||||
import ai.kilocode.client.session.ui.attachment.isEmbeddedAttachment
|
||||
import ai.kilocode.client.agentManager.worktree.KiloWorktreeService
|
||||
import ai.kilocode.client.agentManager.worktree.MoveToWorktree
|
||||
import ai.kilocode.client.session.ui.header.BranchDock
|
||||
import ai.kilocode.client.session.ui.header.SessionHeaderPanel
|
||||
import ai.kilocode.client.session.ui.selection.SessionContextMenu
|
||||
@@ -201,8 +200,6 @@ class SessionUi(
|
||||
|
||||
private var bottom: JComponent? = null
|
||||
|
||||
private val move by lazy { MoveToWorktree(project, cs) }
|
||||
|
||||
internal lateinit var scroll: SessionScroll
|
||||
|
||||
private lateinit var question: QuestionView
|
||||
@@ -430,7 +427,8 @@ class SessionUi(
|
||||
if (!readonly && showBranchDock()) {
|
||||
val owner = manager
|
||||
val newWorktree = if (owner?.supportsNewWorktree == true) owner::newWorktree else null
|
||||
dock = BranchDock(openDiff = ::openBranchChanges, onMove = ::moveToWorktree, onNewWorktree = newWorktree)
|
||||
val move = if (owner?.supportsMoveToWorktree == true) ::moveToWorktree else null
|
||||
dock = BranchDock(openDiff = ::openBranchChanges, onMove = move, onNewWorktree = newWorktree)
|
||||
}
|
||||
|
||||
scroll = SessionScroll(root, sessionContent, messageBody, blankBody)
|
||||
@@ -990,12 +988,11 @@ class SessionUi(
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the Move to Worktree flow for the current session, wiring progress into the dock. */
|
||||
/** Starts the Move to Worktree flow for the current session through the side-panel manager. */
|
||||
@RequiresEdt
|
||||
private fun moveToWorktree() {
|
||||
val dock = dock ?: return
|
||||
val id = controller.id ?: return
|
||||
move.launch(controller.sessionDirectory, id) { stage, detail -> dock.setMoveProgress(stage, detail) }
|
||||
manager?.moveToWorktree(id, controller.sessionDirectory)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
|
||||
+5
-9
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.ui.list.ActiveListBadge
|
||||
import ai.kilocode.client.ui.list.ActiveListCell
|
||||
@@ -12,22 +13,17 @@ internal data class LocalHistoryRow(
|
||||
private val text: String,
|
||||
private val kind: SessionActivityKind?,
|
||||
override val section: String?,
|
||||
override val deleting: Boolean,
|
||||
private val deleting: Boolean,
|
||||
) : ActiveListItem {
|
||||
override val key: String get() = item.id
|
||||
override val title: String get() = text
|
||||
override val trailing: String get() = HistoryTime.relative(item)
|
||||
override val search: String get() = listOfNotNull(text, item.id, item.directory).joinToString(" ")
|
||||
override val progress: String? get() = if (deleting) KiloBundle.message("common.deleting") else null
|
||||
override val badges: List<ActiveListBadge>
|
||||
get() {
|
||||
if (deleting) return emptyList()
|
||||
return listOfNotNull(kind?.let { ActiveListBadge(it.label(), it.style()) })
|
||||
}
|
||||
get() = listOfNotNull(kind?.let { ActiveListBadge(it.label(), it.style()) })
|
||||
override val cells: List<ActiveListCell>
|
||||
get() {
|
||||
if (deleting) return emptyList()
|
||||
return listOf(activeListRenameCell(), activeListDeleteCell())
|
||||
}
|
||||
get() = listOf(activeListRenameCell(), activeListDeleteCell())
|
||||
}
|
||||
|
||||
internal data class CloudHistoryRow(
|
||||
|
||||
+7
-21
@@ -11,7 +11,6 @@ import ai.kilocode.client.ui.layout.align
|
||||
import ai.kilocode.rpc.dto.BranchStatusDto
|
||||
import ai.kilocode.rpc.dto.DiffFileDto
|
||||
import ai.kilocode.rpc.dto.GhAvailability
|
||||
import ai.kilocode.rpc.dto.MoveStage
|
||||
import com.intellij.ide.ActivityTracker
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.DataSink
|
||||
@@ -37,12 +36,12 @@ import java.awt.Dimension
|
||||
* not enabled, so the dock never shows a lone action. The dock exposes its state to those actions via
|
||||
* [ChatDockKeys.DOCK]. Collapses to nothing unless it has a PR, changes, or an enabled action.
|
||||
*
|
||||
* The action row is offered only while the session is idle: an active turn ([setBusy]) withdraws it,
|
||||
* a move in progress keeps it. The PR row stays through a turn — it is informational, not an action.
|
||||
* The action row is offered only while the session is idle: an active turn ([setBusy]) withdraws it.
|
||||
* The PR row stays through a turn — it is informational, not an action.
|
||||
*/
|
||||
internal class BranchDock(
|
||||
openDiff: () -> Unit,
|
||||
private val onMove: () -> Unit,
|
||||
private val onMove: (() -> Unit)?,
|
||||
private val onNewWorktree: (() -> Unit)? = null,
|
||||
) : BorderLayoutPanel(), SessionEditorStyleTarget, UiDataProvider {
|
||||
private val core = PrHeaderView(openDiff)
|
||||
@@ -60,8 +59,6 @@ internal class BranchDock(
|
||||
private var branch: BranchStatusDto? = null
|
||||
private var hasMessages = false
|
||||
private var busy = false
|
||||
private var moving = false
|
||||
private var stage: MoveStage? = null
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
@@ -113,28 +110,17 @@ internal class BranchDock(
|
||||
sync()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun setMoveProgress(stage: MoveStage, detail: String?) {
|
||||
moving = stage != MoveStage.DONE && stage != MoveStage.ERROR
|
||||
this.stage = if (moving) stage else null
|
||||
sync()
|
||||
}
|
||||
|
||||
// ---- state read by the toolbar actions ----
|
||||
|
||||
fun newWorktreeEnabled(): Boolean = onNewWorktree != null && dockActive() && !moving
|
||||
fun newWorktreeEnabled(): Boolean = onNewWorktree != null && dockActive()
|
||||
|
||||
fun moveEnabled(): Boolean = dockActive() && !moving
|
||||
|
||||
fun moving(): Boolean = moving
|
||||
|
||||
fun moveStage(): MoveStage? = stage
|
||||
fun moveEnabled(): Boolean = onMove != null && dockActive()
|
||||
|
||||
fun changeCount(): Int = files.size
|
||||
|
||||
fun triggerNewWorktree() = onNewWorktree?.invoke() ?: Unit
|
||||
|
||||
fun triggerMove() = onMove()
|
||||
fun triggerMove() = onMove?.invoke() ?: Unit
|
||||
|
||||
private fun dockActive(): Boolean = gitAvailable() && !busy && (hasMessages || files.isNotEmpty())
|
||||
|
||||
@@ -151,7 +137,7 @@ internal class BranchDock(
|
||||
|
||||
// PR present -> the informational PR header; otherwise the centered action row.
|
||||
core.isVisible = pull != null
|
||||
val rowVisible = pull == null && (dockActive() || moving)
|
||||
val rowVisible = pull == null && dockActive()
|
||||
actionRow.isVisible = rowVisible
|
||||
|
||||
val next = pull != null || rowVisible
|
||||
|
||||
+3
-2
@@ -109,7 +109,8 @@ internal interface ActiveListItem {
|
||||
val metrics: ActiveListMetrics? get() = null
|
||||
val cells: List<ActiveListCell> get() = emptyList()
|
||||
val disabled: Boolean get() = false
|
||||
val deleting: Boolean get() = false
|
||||
/** Non-null while a background operation owns this row; the text is shown trailing. */
|
||||
val progress: String? get() = null
|
||||
/** Extra text matched by the filter field in addition to [title]; null matches title only. */
|
||||
val search: String? get() = null
|
||||
}
|
||||
@@ -126,7 +127,7 @@ internal fun activeListVisibleCells(
|
||||
menu: Boolean = false,
|
||||
): List<ActiveListCell> {
|
||||
if (item.disabled) return emptyList()
|
||||
if (item.deleting) return emptyList()
|
||||
if (item.progress != null) return emptyList()
|
||||
val cells = item.cells.filter { active || it.alwaysVisible }
|
||||
if (!menu) return cells
|
||||
return cells + activeListMenuCell()
|
||||
|
||||
+5
-6
@@ -1,7 +1,6 @@
|
||||
package ai.kilocode.client.ui.list
|
||||
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeStatsView
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.ui.PickerRow
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.LayeredOverlayPanel
|
||||
@@ -192,7 +191,7 @@ internal class ActiveListRenderer(
|
||||
val active = selected && (focused || list.hasFocus() || (list as? ActiveListActive)?.active() == true)
|
||||
val fg = UIUtil.getListForeground(active, active || focused)
|
||||
val weak = UiStyle.Colors.weak()
|
||||
val titleFg = if (value.deleting) weak else fg
|
||||
val titleFg = if (value.progress != null) weak else fg
|
||||
val section = activeListSectionTitle(model.items, index)
|
||||
|
||||
background = list.background
|
||||
@@ -239,13 +238,13 @@ internal class ActiveListRenderer(
|
||||
JBUI.Borders.empty()
|
||||
}
|
||||
desc.foreground = weak
|
||||
val data = if (value.deleting) null else value.metrics
|
||||
val data = if (value.progress != null) null else value.metrics
|
||||
metrics.update(data?.let { WorktreeStatsDto("", it.additions, it.deletions, it.ahead, it.behind) }, data?.pr, data?.prTooltip ?: data?.pr?.text)
|
||||
metrics.setActions(data?.onChanges, data?.onPr)
|
||||
val end = if (value.deleting) KiloBundle.message("common.deleting") else value.trailing.orEmpty()
|
||||
val end = value.progress ?: value.trailing.orEmpty()
|
||||
trail.text = end
|
||||
trail.isVisible = end.isNotBlank() && data == null
|
||||
metrics.isVisible = data != null && !value.deleting
|
||||
metrics.isVisible = data != null
|
||||
// Hide the wrapper too so a metrics-less row does not reserve the trailing gap on its
|
||||
// description, and collapse the whole second row when it would be empty so title-only rows
|
||||
// stay vertically centered.
|
||||
@@ -347,7 +346,7 @@ internal class ActiveListRenderer(
|
||||
}
|
||||
|
||||
private fun syncBadges(item: ActiveListItem) {
|
||||
val items = if (item.deleting) emptyList() else item.badges
|
||||
val items = if (item.progress != null) emptyList() else item.badges
|
||||
while (badges.componentCount > items.size) badges.remove(badges.componentCount - 1)
|
||||
while (badges.componentCount < items.size) {
|
||||
badges.add(JBLabel().apply {
|
||||
|
||||
+11
-11
@@ -144,7 +144,7 @@ internal class ActiveListView(
|
||||
if (selection(e)) return
|
||||
val hit = hit(e, enabled = false) ?: return
|
||||
if (hit.id != null) return
|
||||
if (hit.item.deleting) return
|
||||
if (hit.item.progress != null) return
|
||||
if (!openOnClick) {
|
||||
onClick?.invoke(hit.item) ?: return
|
||||
e.consume()
|
||||
@@ -158,7 +158,7 @@ internal class ActiveListView(
|
||||
if (e.clickCount != 2 || !UIUtil.isActionClick(e, MouseEvent.MOUSE_CLICKED, true)) return
|
||||
val hit = hit(e, enabled = false) ?: return
|
||||
if (hit.id != null) return
|
||||
if (hit.item.deleting) return
|
||||
if (hit.item.progress != null) return
|
||||
val action = onOpen
|
||||
if (action != null) action(hit.item, true) else activate(hit.item)
|
||||
e.consume()
|
||||
@@ -174,7 +174,7 @@ internal class ActiveListView(
|
||||
press = null
|
||||
val hit = hit(e) ?: return
|
||||
if (hit.item.key != down.key || hit.id != down.id) return
|
||||
if (hit.item.deleting) return
|
||||
if (hit.item.progress != null) return
|
||||
fire(hit.item, down.id)
|
||||
e.consume()
|
||||
}
|
||||
@@ -517,7 +517,7 @@ internal class ActiveListView(
|
||||
private fun open(focus: Boolean) {
|
||||
val item = active() ?: return
|
||||
if (item is ActiveListGap) return
|
||||
if (item.deleting) return
|
||||
if (item.progress != null) return
|
||||
val action = onOpen
|
||||
if (action != null) {
|
||||
action(item, focus)
|
||||
@@ -529,7 +529,7 @@ internal class ActiveListView(
|
||||
private fun source() {
|
||||
val item = list.selectedValue ?: return
|
||||
if (item is ActiveListGap) return
|
||||
if (item.deleting) return
|
||||
if (item.progress != null) return
|
||||
onOpen?.invoke(item, true)
|
||||
}
|
||||
|
||||
@@ -548,7 +548,7 @@ internal class ActiveListView(
|
||||
*/
|
||||
private fun activate(item: ActiveListItem) {
|
||||
if (item is ActiveListGap) return
|
||||
if (item.deleting) return
|
||||
if (item.progress != null) return
|
||||
val action = onActivate
|
||||
if (action != null) {
|
||||
action(item)
|
||||
@@ -565,7 +565,7 @@ internal class ActiveListView(
|
||||
|
||||
private fun primary(item: ActiveListItem) {
|
||||
if (item is ActiveListGap) return
|
||||
if (item.deleting) return
|
||||
if (item.progress != null) return
|
||||
val cells = activeListVisibleCells(item, true)
|
||||
val cell = cells.firstOrNull { it.enabled && it.primary }
|
||||
if (cell != null) {
|
||||
@@ -606,7 +606,7 @@ internal class ActiveListView(
|
||||
if (!list.isEnabled || filter.isNotBlank() || drag != null) return null
|
||||
val idx = rowAt(point) ?: return null
|
||||
val item = model.getElementAt(idx)
|
||||
if (item is ActiveListGap || item.disabled || item.deleting) return null
|
||||
if (item is ActiveListGap || item.disabled || item.progress != null) return null
|
||||
if (!cfg.movable(item)) return null
|
||||
val selected = list.isSelectedIndex(idx)
|
||||
if (activeListCellAt(list, idx, point, selected, menu?.takeIf { it.available(item) } != null) != null) return null
|
||||
@@ -746,7 +746,7 @@ internal class ActiveListView(
|
||||
if (!bounds.contains(point)) return false
|
||||
val item = model.getElementAt(idx)
|
||||
if (item is ActiveListGap) return false
|
||||
if (item.disabled || item.deleting || !cfg.available(item)) return false
|
||||
if (item.disabled || item.progress != null || !cfg.available(item)) return false
|
||||
val rect = activeListCellBounds(list, idx, list.isSelectedIndex(idx))[ACTIVE_LIST_MENU_CELL] ?: return false
|
||||
if (!rect.contains(point)) return false
|
||||
val popup = JBPopupFactory.getInstance().createActionGroupPopup(
|
||||
@@ -858,7 +858,7 @@ private data class ActiveListHeightRow(
|
||||
val trailing: String?,
|
||||
val cells: List<ActiveListCell>,
|
||||
val disabled: Boolean,
|
||||
val deleting: Boolean,
|
||||
val progress: String?,
|
||||
)
|
||||
|
||||
private fun activeListHeightRow(item: ActiveListItem): ActiveListHeightRow {
|
||||
@@ -873,7 +873,7 @@ private fun activeListHeightRow(item: ActiveListItem): ActiveListHeightRow {
|
||||
item.trailing,
|
||||
item.cells,
|
||||
item.disabled,
|
||||
item.deleting,
|
||||
item.progress,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -145,11 +145,6 @@ session.dock.move=Move to Worktree
|
||||
session.dock.move.tooltip.empty=Move this conversation and your current local changes into a dedicated worktree for isolated follow-up work.
|
||||
session.dock.move.tooltip.one=Move this conversation and 1 changed file into a dedicated worktree for isolated follow-up work.
|
||||
session.dock.move.tooltip.other=Move this conversation and {0} changed files into a dedicated worktree for isolated follow-up work.
|
||||
session.dock.progress.capturing=Capturing changes...
|
||||
session.dock.progress.creating=Creating worktree...
|
||||
session.dock.progress.transferring=Transferring changes...
|
||||
session.dock.progress.forking=Starting session...
|
||||
session.dock.move.failed.title=Failed to move to worktree
|
||||
diff.editor.session.title=Session Changes
|
||||
diff.editor.inline.title.named={0} ({1})
|
||||
diff.editor.changedFiles.title=Changed files
|
||||
@@ -416,7 +411,11 @@ worktree.configure.base.invalid.content=Select an existing base branch before cr
|
||||
worktree.dialog.name.placeholder=Worktree name (optional)
|
||||
worktree.dialog.prompt.placeholder=Describe what you want to start working on ({0} to create)
|
||||
worktree.dialog.create=Create Worktree
|
||||
worktree.dialog.creating=Creating…
|
||||
worktree.progress.creating=Creating worktree…
|
||||
worktree.progress.capturing=Capturing changes…
|
||||
worktree.progress.transferring=Transferring changes…
|
||||
worktree.progress.starting=Starting session…
|
||||
worktree.move.failed.title=Failed to move to worktree
|
||||
worktree.import.pr.section=Pull Request
|
||||
worktree.import.pr.failed.title=Couldn''t import pull request
|
||||
worktree.import.branch.failed.title=Couldn''t import branch "{0}"
|
||||
|
||||
+4
-1
@@ -13,6 +13,7 @@ import ai.kilocode.client.agentManager.worktree.WorktreeStatusService
|
||||
import ai.kilocode.client.agentManager.worktree.ensureWorktreeSessionEditorKind
|
||||
import ai.kilocode.client.agentManager.worktree.worktreeSessionParams
|
||||
import ai.kilocode.client.diff.KiloDiffEditorKind
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.testing.FakeWorktreeRpcApi
|
||||
import ai.kilocode.client.testing.TestCoroutines
|
||||
@@ -632,7 +633,7 @@ class AgentManagerPanelTest : BasePlatformTestCase() {
|
||||
assertEquals("<html>Open #8<br>Click to open the pull request in your browser.</html>", row.metrics?.prTooltip)
|
||||
}
|
||||
|
||||
fun `test worktree row hides badge while pending or deleting`() {
|
||||
fun `test worktree row hides badge while in progress`() {
|
||||
val path = "feature/y"
|
||||
val activity = MutableStateFlow(mapOf(
|
||||
"ses_1" to SessionActivityDto(path, SessionActivityKindDto.RUNNING),
|
||||
@@ -647,7 +648,9 @@ class AgentManagerPanelTest : BasePlatformTestCase() {
|
||||
|
||||
val pending = row(panel, 0)
|
||||
assertSame(WorktreeIcons.spinner, pending.icon)
|
||||
assertEquals(KiloBundle.message("worktree.progress.creating"), pending.progress)
|
||||
assertEquals(emptyList<ActiveListBadge>(), pending.badges)
|
||||
assertNull(pending.metrics)
|
||||
gate.complete(Unit)
|
||||
flush()
|
||||
}
|
||||
|
||||
+90
-15
@@ -7,11 +7,14 @@ import ai.kilocode.client.agentManager.worktree.PendingPrompt
|
||||
import ai.kilocode.client.agentManager.worktree.PendingWorktreePrompt
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeNameCache
|
||||
import ai.kilocode.client.agentManager.worktree.WorktreeNames
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.testing.FakeWorktreeRpcApi
|
||||
import ai.kilocode.client.testing.TestCoroutines
|
||||
import ai.kilocode.client.testing.pumpEdt
|
||||
import ai.kilocode.rpc.dto.CreateWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.MoveProgressDto
|
||||
import ai.kilocode.rpc.dto.MoveStage
|
||||
import ai.kilocode.rpc.dto.RemoveWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.RenameWorktreeResultDto
|
||||
import ai.kilocode.rpc.dto.SessionActivityDto
|
||||
@@ -161,13 +164,13 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
|
||||
controller.remove(controller.model.getElementAt(0))
|
||||
|
||||
assertTrue(controller.isDeleting(item.id))
|
||||
assertEquals(KiloBundle.message("common.deleting"), controller.progress(item.id))
|
||||
assertEquals(1, controller.model.size)
|
||||
|
||||
gate.complete(Unit)
|
||||
flush()
|
||||
|
||||
assertFalse(controller.isDeleting(item.id))
|
||||
assertNull(controller.progress(item.id))
|
||||
assertEquals(0, controller.model.size)
|
||||
}
|
||||
|
||||
@@ -186,7 +189,7 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
// git rejected the removal, so the entry must remain instead of vanishing optimistically.
|
||||
assertEquals(1, controller.model.size)
|
||||
assertEquals("feature/x", controller.model.getElementAt(0).branch)
|
||||
assertFalse(controller.isDeleting(item.id))
|
||||
assertNull(controller.progress(item.id))
|
||||
assertEquals(1, failures.size)
|
||||
assertTrue(failures.first().locked)
|
||||
}
|
||||
@@ -321,6 +324,75 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
assertEquals("/wt/pr-7", controller.model.getElementAt(0).path)
|
||||
}
|
||||
|
||||
fun `test move adds placeholder tracks progress and swaps to worktree`() {
|
||||
val done = WorktreeDto("/wt/moved", "moved", "moved", "/wt/moved")
|
||||
rpc.moveScript = listOf(
|
||||
MoveProgressDto(MoveStage.CREATING),
|
||||
MoveProgressDto(MoveStage.TRANSFERRING),
|
||||
MoveProgressDto(MoveStage.FORKING),
|
||||
MoveProgressDto(MoveStage.DONE, worktree = done, session = "ses_fork"),
|
||||
)
|
||||
val selected = mutableListOf<String>()
|
||||
val aborts = mutableListOf<Pair<String, String>>()
|
||||
val events = mutableListOf<Pair<String, Map<String, String>>>()
|
||||
val controller = controller(abort = { id, dir -> aborts += id to dir }, telemetry = { name, props -> events += name to props })
|
||||
controller.onSelect = { selected += it }
|
||||
|
||||
ApplicationManager.getApplication().invokeAndWait { controller.move("ses_source", "/repo") }
|
||||
val temp = controller.model.getElementAt(0)
|
||||
assertTrue(controller.isPending(temp.id))
|
||||
assertEquals(KiloBundle.message("worktree.progress.capturing"), controller.progress(temp.id))
|
||||
assertEquals(temp.id, selected.single())
|
||||
flush()
|
||||
|
||||
assertEquals(listOf("ses_source" to "/repo"), aborts)
|
||||
assertEquals("/repo", rpc.moves.single().first)
|
||||
assertEquals("ses_source", rpc.moves.single().second)
|
||||
assertEquals(done, controller.model.getElementAt(0))
|
||||
assertNull(controller.progress(temp.id))
|
||||
assertEquals("ses_fork", controller.takeSession(done.id))
|
||||
assertNull(controller.takeSession(done.id))
|
||||
assertEquals(done.id, selected.last())
|
||||
assertTrue(events.any { it.first == "Continue in Worktree" && it.second["surface"] == "sidebar" })
|
||||
}
|
||||
|
||||
fun `test move failure removes placeholder and reports last stage`() {
|
||||
rpc.moveScript = listOf(
|
||||
MoveProgressDto(MoveStage.CREATING),
|
||||
MoveProgressDto(MoveStage.ERROR, error = "boom"),
|
||||
)
|
||||
val failures = mutableListOf<String?>()
|
||||
val events = mutableListOf<Pair<String, Map<String, String>>>()
|
||||
val controller = controller(telemetry = { name, props -> events += name to props })
|
||||
controller.onMoveFailure = { failures += it }
|
||||
|
||||
ApplicationManager.getApplication().invokeAndWait { controller.move("ses_source", "/repo") }
|
||||
flush()
|
||||
|
||||
assertEquals(0, controller.model.size)
|
||||
assertEquals(listOf("boom"), failures)
|
||||
assertTrue(events.any { it.first == "Continue in Worktree Failed" && it.second["stage"] == "CREATING" })
|
||||
}
|
||||
|
||||
fun `test duplicate move for session is ignored while in flight`() {
|
||||
val gate = CompletableDeferred<Unit>()
|
||||
rpc.moveScript = listOf(MoveProgressDto(MoveStage.DONE, worktree = WorktreeDto("/wt/moved", "moved", "moved", "/wt/moved")))
|
||||
val controller = controller(abort = { _, _ -> gate.await() })
|
||||
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
controller.move("ses_source", "/repo")
|
||||
controller.move("ses_source", "/repo")
|
||||
}
|
||||
flush()
|
||||
|
||||
assertEquals(1, controller.model.size)
|
||||
assertEquals(0, rpc.moves.size)
|
||||
gate.complete(Unit)
|
||||
flush()
|
||||
|
||||
assertEquals(1, rpc.moves.size)
|
||||
}
|
||||
|
||||
fun `test create stashes the prompt with its picked selection for the created worktree`() {
|
||||
val controller = controller()
|
||||
|
||||
@@ -354,22 +426,22 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
fun `test worktree row icons show only while running or waiting`() {
|
||||
assertSame(
|
||||
WorktreeIcons.spinner,
|
||||
WorktreeIcons.forRow(pending = true, kind = SessionActivityKind.RUNNING),
|
||||
WorktreeIcons.forRow(busy = true, kind = SessionActivityKind.RUNNING),
|
||||
)
|
||||
assertSame(
|
||||
WorktreeIcons.running,
|
||||
WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.RUNNING),
|
||||
WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.RUNNING),
|
||||
)
|
||||
assertSame(
|
||||
SessionActivityKind.QUESTION.icon(),
|
||||
WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.QUESTION),
|
||||
WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.QUESTION),
|
||||
)
|
||||
assertSame(
|
||||
SessionActivityKind.PLAN.icon(),
|
||||
WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.PLAN),
|
||||
WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.PLAN),
|
||||
)
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.ERROR))
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(pending = false, kind = null))
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.ERROR))
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(busy = false, kind = null))
|
||||
}
|
||||
|
||||
fun `test worktree icons load at the same size`() {
|
||||
@@ -402,10 +474,10 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
val main = WorktreeDto("/repo", "repo", "main", "/repo", main = true)
|
||||
val child = WorktreeDto("/repo/.kilo/worktrees/feature-x", "feature-x", "feature/x", "/repo/.kilo/worktrees/feature-x")
|
||||
|
||||
assertFalse(worktreeDeletable(null, pending = false))
|
||||
assertFalse(worktreeDeletable(main, pending = false))
|
||||
assertFalse(worktreeDeletable(child, pending = true))
|
||||
assertTrue(worktreeDeletable(child, pending = false))
|
||||
assertFalse(worktreeDeletable(null, busy = false))
|
||||
assertFalse(worktreeDeletable(main, busy = false))
|
||||
assertFalse(worktreeDeletable(child, busy = true))
|
||||
assertTrue(worktreeDeletable(child, busy = false))
|
||||
}
|
||||
|
||||
fun `test applyName updates the matching row so an adopted name shows live`() {
|
||||
@@ -468,8 +540,11 @@ class WorktreeControllerTest : BasePlatformTestCase() {
|
||||
assertEquals(listOf("/wt" to "Name", "/wt" to null), events)
|
||||
}
|
||||
|
||||
private fun controller(activity: MutableStateFlow<Map<String, SessionActivityDto>> = MutableStateFlow(emptyMap())) =
|
||||
WorktreeController(service, "/test", coroutines.scope, activity = activity)
|
||||
private fun controller(
|
||||
activity: MutableStateFlow<Map<String, SessionActivityDto>> = MutableStateFlow(emptyMap()),
|
||||
abort: suspend (String, String) -> Unit = { _, _ -> },
|
||||
telemetry: (String, Map<String, String>) -> Unit = { _, _ -> },
|
||||
) = WorktreeController(service, "/test", coroutines.scope, activity = activity, abort = abort, telemetry = telemetry)
|
||||
|
||||
private fun flush() = coroutines.drain(::pump)
|
||||
|
||||
|
||||
+11
-11
@@ -12,7 +12,7 @@ import java.awt.Color
|
||||
|
||||
class WorktreeIconsTest : BasePlatformTestCase() {
|
||||
fun `test running session resolves to the animated spinner`() {
|
||||
assertSame(WorktreeIcons.running, WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.RUNNING))
|
||||
assertSame(WorktreeIcons.running, WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.RUNNING))
|
||||
}
|
||||
|
||||
fun `test running icon is animated and sized to the row icon`() {
|
||||
@@ -58,36 +58,36 @@ class WorktreeIconsTest : BasePlatformTestCase() {
|
||||
return channels.max() - channels.min()
|
||||
}
|
||||
|
||||
fun `test pending outranks running and uses the platform spinner`() {
|
||||
assertSame(WorktreeIcons.spinner, WorktreeIcons.forRow(pending = true, kind = SessionActivityKind.RUNNING))
|
||||
fun `test busy outranks running and uses the platform spinner`() {
|
||||
assertSame(WorktreeIcons.spinner, WorktreeIcons.forRow(busy = true, kind = SessionActivityKind.RUNNING))
|
||||
}
|
||||
|
||||
fun `test waiting kinds resolve to the attention glyph`() {
|
||||
assertSame(
|
||||
SessionActivityKind.QUESTION.icon(),
|
||||
WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.QUESTION),
|
||||
WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.QUESTION),
|
||||
)
|
||||
assertSame(SessionActivityKind.PLAN.icon(), WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.PLAN))
|
||||
assertSame(SessionActivityKind.PLAN.icon(), WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.PLAN))
|
||||
}
|
||||
|
||||
fun `test rows at rest show what the checkout is`() {
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(pending = false))
|
||||
assertSame(WorktreeIcons.locked, WorktreeIcons.forRow(pending = false, locked = true))
|
||||
assertSame(WorktreeIcons.local, WorktreeIcons.forRow(pending = false, current = true))
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(busy = false))
|
||||
assertSame(WorktreeIcons.locked, WorktreeIcons.forRow(busy = false, locked = true))
|
||||
assertSame(WorktreeIcons.local, WorktreeIcons.forRow(busy = false, current = true))
|
||||
}
|
||||
|
||||
fun `test errored session falls back to the resting glyph`() {
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.ERROR))
|
||||
assertSame(WorktreeIcons.branch, WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.ERROR))
|
||||
assertSame(
|
||||
WorktreeIcons.local,
|
||||
WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.ERROR, current = true),
|
||||
WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.ERROR, current = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun `test activity outranks the resting glyph on the local row`() {
|
||||
assertSame(
|
||||
WorktreeIcons.running,
|
||||
WorktreeIcons.forRow(pending = false, kind = SessionActivityKind.RUNNING, current = true),
|
||||
WorktreeIcons.forRow(busy = false, kind = SessionActivityKind.RUNNING, current = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -289,7 +289,7 @@ class WorktreeSessionEditorPanelTest : BasePlatformTestCase() {
|
||||
|
||||
val row = row("ses_1")
|
||||
|
||||
assertTrue(row.deleting)
|
||||
assertEquals(KiloBundle.message("common.deleting"), row.progress)
|
||||
}
|
||||
|
||||
fun `test pending new session groups under today`() {
|
||||
|
||||
+29
@@ -190,6 +190,35 @@ class SessionUiLayoutTest : SessionUiTestBase() {
|
||||
assertTrue(dock.isVisible)
|
||||
}
|
||||
|
||||
fun `test dock move delegates to manager without progress state`() {
|
||||
val calls = mutableListOf<Pair<String, String>>()
|
||||
val owner = object : SessionManager {
|
||||
override fun newSession() {}
|
||||
override fun showHistory(back: (() -> Unit)?) {}
|
||||
override fun openSession(ref: SessionRef) {}
|
||||
override val supportsMoveToWorktree: Boolean get() = true
|
||||
override fun moveToWorktree(sessionId: String, directory: String) {
|
||||
calls += sessionId to directory
|
||||
}
|
||||
}
|
||||
val worktree = FakeWorktreeRpcApi().apply {
|
||||
branchResult = BranchStatusDto(branch = "main", availability = GhAvailability.OK)
|
||||
}
|
||||
ApplicationManager.getApplication()
|
||||
.replaceService(KiloWorktreeService::class.java, KiloWorktreeService(scope, worktree), testRootDisposable)
|
||||
rpc.history.addAll(history(1))
|
||||
ui = newUi(id = "ses_test", manager = owner)
|
||||
settle()
|
||||
val dock = find<BranchDock>(ui)
|
||||
assertTrue(dock.isVisible)
|
||||
|
||||
dock.triggerMove()
|
||||
|
||||
assertEquals(listOf("ses_test" to "/test"), calls)
|
||||
assertTrue(dock.isVisible)
|
||||
assertTrue(dock.moveEnabled())
|
||||
}
|
||||
|
||||
fun `test dock branch changes refresh on finish and revert`() {
|
||||
workspaceRpc.branchDiffs.clear()
|
||||
workspaceRpc.branchDiffs.add(DiffFileDto("src/A.kt", 2, 1))
|
||||
|
||||
+3
-3
@@ -214,9 +214,9 @@ class HistoryControllerTest : BasePlatformTestCase() {
|
||||
|
||||
val row = localHistoryRows(listOf(item), snapshot) { true }[0]
|
||||
|
||||
assertTrue(row.deleting)
|
||||
assertTrue(row.cells.isEmpty())
|
||||
assertTrue(row.badges.isEmpty())
|
||||
assertEquals(KiloBundle.message("common.deleting"), row.progress)
|
||||
assertEquals(listOf(ACTIVE_LIST_RENAME_CELL, ACTIVE_LIST_DELETE_CELL), row.cells.map { it.id })
|
||||
assertEquals(listOf(SessionActivityKind.RUNNING.label()), row.badges.map { it.text })
|
||||
}
|
||||
|
||||
fun `test history panel sync updates running badges`() {
|
||||
|
||||
+9
-29
@@ -8,7 +8,6 @@ import ai.kilocode.rpc.dto.BranchStatusDto
|
||||
import ai.kilocode.rpc.dto.DiffFileDto
|
||||
import ai.kilocode.rpc.dto.GhAvailability
|
||||
import ai.kilocode.rpc.dto.GhState
|
||||
import ai.kilocode.rpc.dto.MoveStage
|
||||
import ai.kilocode.rpc.dto.WorktreePrDto
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
@@ -136,21 +135,6 @@ class BranchDockTest : BasePlatformTestCase() {
|
||||
assertFalse(update(ChatNewWorktreeAction(), dock).isVisible)
|
||||
}
|
||||
|
||||
fun `test move progress keeps the row while session is busy`() {
|
||||
val dock = dock()
|
||||
edt {
|
||||
dock.setBranch(BranchStatusDto(branch = "feature-x", worktree = false, availability = GhAvailability.OK))
|
||||
dock.setHasMessages(true)
|
||||
dock.setBusy(true)
|
||||
dock.setMoveProgress(MoveStage.CREATING, null)
|
||||
}
|
||||
assertTrue(edt { dock.isVisible })
|
||||
val p = update(ChatMoveToWorktreeAction(), dock)
|
||||
assertTrue(p.isVisible)
|
||||
assertFalse(p.isEnabled)
|
||||
assertEquals(KiloBundle.message("session.dock.progress.creating"), p.text)
|
||||
}
|
||||
|
||||
// ---- Move to Worktree action ----
|
||||
|
||||
fun `test move action visible with messages`() {
|
||||
@@ -191,19 +175,6 @@ class BranchDockTest : BasePlatformTestCase() {
|
||||
assertFalse(update(ChatMoveToWorktreeAction(), dock).isVisible)
|
||||
}
|
||||
|
||||
fun `test move action shows spinner while moving`() {
|
||||
val dock = dock()
|
||||
edt {
|
||||
dock.setBranch(BranchStatusDto(branch = "feature-x", worktree = false, availability = GhAvailability.OK))
|
||||
dock.setHasMessages(true)
|
||||
dock.setMoveProgress(MoveStage.CREATING, null)
|
||||
}
|
||||
val p = update(ChatMoveToWorktreeAction(), dock)
|
||||
assertTrue(p.isVisible)
|
||||
assertFalse(p.isEnabled)
|
||||
assertEquals(KiloBundle.message("session.dock.progress.creating"), p.text)
|
||||
}
|
||||
|
||||
fun `test move action invokes callback`() {
|
||||
var moved = 0
|
||||
val dock = edt { BranchDock(openDiff = {}, onMove = { moved++ }) }
|
||||
@@ -214,6 +185,15 @@ class BranchDockTest : BasePlatformTestCase() {
|
||||
assertEquals(1, moved)
|
||||
}
|
||||
|
||||
fun `test move action hidden without a move host`() {
|
||||
val dock = edt { BranchDock(openDiff = {}, onMove = null) }
|
||||
edt {
|
||||
dock.setBranch(BranchStatusDto(branch = "feature-x", worktree = false, availability = GhAvailability.OK))
|
||||
dock.setHasMessages(true)
|
||||
}
|
||||
assertFalse(update(ChatMoveToWorktreeAction(), dock).isVisible)
|
||||
}
|
||||
|
||||
// ---- New Worktree action ----
|
||||
|
||||
fun `test new worktree action visible when dock active`() {
|
||||
|
||||
+12
@@ -212,6 +212,18 @@ class ActiveListSelectionTest : BasePlatformTestCase() {
|
||||
assertEquals("created", view.selected()?.key)
|
||||
}
|
||||
|
||||
fun `test progress row keeps cells in model but hides visible actions`() {
|
||||
val item = object : ActiveListItem {
|
||||
override val key = "busy"
|
||||
override val title = "Busy"
|
||||
override val progress = "Working..."
|
||||
override val cells = listOf(ActiveListCell("open", "Open", primary = true))
|
||||
}
|
||||
|
||||
assertEquals(listOf("open"), item.cells.map { it.id })
|
||||
assertEquals(emptyList<ActiveListCell>(), activeListVisibleCells(item, active = true))
|
||||
}
|
||||
|
||||
private fun view(onSelect: () -> Unit = {}): ActiveListView {
|
||||
return ActiveListView("") { _, _ -> }.apply { this.onSelect = onSelect }
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ enum class MoveStage { CAPTURING, CREATING, TRANSFERRING, FORKING, DONE, ERROR }
|
||||
@Serializable
|
||||
data class MoveProgressDto(
|
||||
val stage: MoveStage,
|
||||
val detail: String? = null,
|
||||
val error: String? = null,
|
||||
val worktree: WorktreeDto? = null,
|
||||
val session: String? = null,
|
||||
|
||||
Reference in New Issue
Block a user