Skip to content

feat(jetbrains): move sessions to a worktree from the worktree editor - #13756

Merged
kirillk merged 3 commits into
mainfrom
lucky-keyboard
Sep 3, 2026
Merged

feat(jetbrains): move sessions to a worktree from the worktree editor#13756
kirillk merged 3 commits into
mainfrom
lucky-keyboard

Conversation

@kirillk

@kirillk kirillk commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes #

Context

The Agent Manager's worktree editor tab for the base checkout (the main working tree, not a linked
worktree) only showed a plain session list. Moving a session into a new worktree, or starting a new
worktree, required switching to the Agent Manager tool window panel — those flows had no way to reach
the editor tab. That tab's header was also blank for the base checkout, so its uncommitted changes —
exactly what a move carries — were reported nowhere.

Implementation

  • WorktreeSessionEditorManager resolves once, via git worktree list, whether its directory is the
    repo's main working tree before the first session opens, and gates
    showsBranchDock/supportsNewWorktree/supportsMoveToWorktree on that. A linked worktree's own tab
    keeps today's plain session view.
  • A new project-level AgentManagerHost service bridges the editor tab to the Agent Manager's
    move/new-worktree flows, which otherwise only exist inside the tool window panel
    (AgentManagerPanel/WorktreeController). It queues a request and activates the tool window when
    nothing is bound yet — e.g. a tab restored before the tool window has been created.
  • The session list's row menu gets a new "Move to Worktree" action (MoveWorktreeSessionAction) as its
    first item, followed by a separator, then the existing Rename/Delete. It hides while the session is
    running, matching the chat dock's rule.
  • Sessions in the base tab get the same New Worktree / Move to Worktree toolbar (BranchDock) shown
    above the prompt in the tool window, aligned to the prompt's readable width so it does not run the
    full editor width. In an editor tab that dock is the action strip alone: the branch, its PR, and its
    counts are the tab header's job, so BranchDock gained a header flag that drops its PR row without
    letting a PR suppress the action row underneath it.
  • dirty() now includes the repo's main working tree, which is what makes the base tab's header report
    its uncommitted counts. Unlike stats(), which compares a worktree against the base branch and so
    has nothing to say about the checkout that branch lives on, the uncommitted comparison is local to
    each working tree. Unmanaged worktrees stay excluded from both.
  • Threaded a surface parameter through WorktreeController.move/AgentManagerPanel.move so the
    "Continue in Worktree" telemetry event can tell the row menu, the dock, and the sidebar chat dock
    apart.

Screenshots / Video

Screen Shot 2026-09-03 at 12 36 05 PM

How to Test

Manual/local verification

  • ./gradlew typecheck and ./gradlew test from packages/kilo-jetbrains/ — both pass (agent-run).

Reviewer test steps

  1. Open Agent Manager, open the top ("current"/base) worktree row.
  2. Right-click a session in its list: "Move to Worktree" appears first, followed by a separator, then
    Rename/Delete.
  3. With uncommitted changes in the base checkout, confirm the tab header shows their counts (N files +x -y) in the same place a linked worktree tab shows its own, and that clicking them opens the local
    diff.
  4. With a session that has messages, or with those uncommitted changes, confirm the New Worktree /
    Move to Worktree strip appears above the prompt at the prompt's width — and that it carries only
    those actions, with no changes summary of its own.
  5. Click "Move to Worktree" (row menu or toolbar): the Agents tab activates and the session/changes
    move into a new worktree.
  6. Open a linked worktree's own tab: confirm neither the row menu's Move to Worktree entry nor the
    toolbar above the prompt appear — unchanged behavior.
  7. Restart the IDE with a restored base tab and confirm Move still works (the tool window is created
    and the queued move runs).

Blocked checks and substitute verification

  • ./gradlew runIdeSplitMode / ./gradlew runIde (manual sandbox verification, reviewer test steps
    above) could not be run in this non-interactive environment. Substitute verification (agent-run):
    ./gradlew typecheck and ./gradlew test from packages/kilo-jetbrains/, plus new unit/UI tests
    exercising the real Swing component tree, real action classes, and real git repositories
    (WorktreeSessionEditorManagerTest, WorktreeSessionEditorPanelTest, AgentManagerHostTest,
    BranchDockTest, SessionUiLayoutTest, WorktreeControllerTest, HistorySessionActionsTest,
    KiloWorktreeRpcApiImplTest, BranchLocalDiffTest).

Checklist

  • Issue linked above, or exception explained
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A
  • Changeset considered for user-facing changes
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

Get in Touch

On the base checkout's worktree editor tab (not a linked worktree), add
a "Move to Worktree" entry as the first item in the session list's row
menu, followed by a separator before Rename/Delete. Sessions in that
tab also get the same New Worktree / Move to Worktree toolbar shown
above the prompt in the tool window, with its header summarizing the
uncommitted local changes a move would carry (the tab's own header
already shows the branch and its PR). The dock is aligned to the same
readable width as the prompt so it does not run the full editor width.

WorktreeSessionEditorManager resolves once, via git worktree list,
whether its directory is the repo's main working tree before the first
session opens, and gates showsBranchDock/supportsNewWorktree/
supportsMoveToWorktree on that. A new project-level AgentManagerHost
service bridges the editor to the Agent Manager's move/new-worktree
flows, which otherwise only exist inside the tool window panel; it
queues a request and activates the tool window when nothing is bound
yet (e.g. a restored tab before the tool window has been created).
…he dock

The base tab's dock carried a changes summary directly above the chat,
duplicating reporting that belongs in the tab's own header. Drop that
row entirely -- an editor-tab dock is now the action strip alone -- and
put the counts where every other worktree tab shows them.

The header was empty for the base checkout because dirty() excluded the
main working tree, so include it: unlike stats(), which compares a
worktree against the base branch and therefore has nothing to say about
the checkout that branch lives on, the uncommitted comparison is local
to each working tree. Unmanaged worktrees stay out of both.
@kilo-code-bot

kilo-code-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (7 files)
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerHost.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanel.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionActivityKind.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerHostTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManagerTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt
Previous Review Summary (commit fefb512)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit fefb512)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt 142 Async base resolve can open sessions after the editor is disposed

SUGGESTION

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanel.kt 232 Row-menu Move is offered in busy states the chat dock hides
Files Reviewed (22 files)
  • .changeset/jetbrains-worktree-editor-move.md - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/BranchLocalDiffTest.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/MoveWorktreeSessionAction.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerHost.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManager.kt - 1 issue
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanel.kt - 1 issue
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/plugin/KiloFrontendDynamicPluginListener.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/BranchDock.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml - 0 issues
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties - 0 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerHostTest.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/WorktreeControllerTest.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorManagerTest.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeSessionEditorPanelTest.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt - 0 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/BranchDockTest.kt - 0 issues

Fix these issues in Kilo Cloud


Reviewed by grok-4.6 · Input: 77.3K · Output: 12.6K · Cached: 348.2K

Review guidance: REVIEW.md from base branch main

…dings

Three defects the review caught, each with a test that fails without its
fix:

AgentManagerHost cleared its callbacks unconditionally when a bound tool
window was disposed. A plugin reload creates the replacement before
disposing the old window, so the stale disposer fired last and took the
live callbacks down with it. Track which bind owns them and clear only
when the disposer still does.

The base lookup's answer lands on the EDT after its coroutine has
already finished, so a second start() -- the migration path calls one,
and a tab shown twice can too -- launched a second lookup and opened the
first session twice. Guard the whole gap with a resolving flag, and skip
the EDT callback entirely once the editor is disposed, so a lookup that
lands late cannot open sessions into a dead tab.

The row menu hid Move to Worktree only for a RUNNING session, while the
chat dock hides it for anything SessionState.isBusy() covers -- a
session stopped on a question or a permission is as mid-turn as a
running one. SessionActivityKind.busy() now states that rule where the
enum can keep it exhaustive, and both surfaces read it.
@kirillk

kirillk commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Review feedback addressed in a1a6d4c. Both inline threads are resolved with replies; both automated findings are fixed too.

Finding Fix
AgentManagerHost — disposing an older binding cleared a newer binding's callbacks bind stamps a generation; the disposer clears only while it still owns the callbacks
WorktreeSessionEditorManager — base lookup could start twice A resolving flag guards the gap between the coroutine finishing and the EDT applying the answer, which a job-liveness check would not cover since edt {} is invokeLater
Bot WARNING — async base resolve could open sessions after disposal The EDT callback runs through edt({ !Disposer.isDisposed(this) }), so a late lookup opens nothing
Bot SUGGESTION — row-menu Move offered in busy states the chat dock hides New SessionActivityKind.busy() states the rule where the enum keeps the when exhaustive; the row menu now hides for PERMISSION/QUESTION/PLAN as well as RUNNING, and still offers Move for ERROR/LOGIN_REQUIRED — exactly the set SessionState.isBusy() covers for the dock

Each of the four has a test that I confirmed fails when its fix is reverted:

  • AgentManagerHostTest > test disposing a replaced tool window leaves the newer handlers installed
  • WorktreeSessionEditorManagerTest > test a start during the lookup neither repeats it nor opens a second session
  • WorktreeSessionEditorManagerTest > test a lookup that lands after disposal opens nothing
  • WorktreeSessionEditorPanelTest > test move to worktree hides for every in-flight turn state (plus ... stays offered for a failed or login-blocked session for the other half of the rule)

One incidental find while fixing the first item: the old unconditional clear was also what kept this project service clean between test methods on the reused light project. AgentManagerHostTest.tearDown now binds a throwaway no-op (swallowing anything left queued) and disposes it immediately, so it is the newest binding and the next test starts from a genuinely unbound host.

./gradlew typecheck and ./gradlew test from packages/kilo-jetbrains/ both pass (agent-run).

@kirillk
kirillk merged commit 3c298d4 into main Sep 3, 2026
24 checks passed
@kirillk
kirillk deleted the lucky-keyboard branch September 3, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants