Repository navigation
feat(agent-manager): reclaim disk space from leftover worktree folders - #14255
Conversation
Leftover folders under .kilo/worktrees were surfaced as a notice with one blind "clean up" action, so there was no way to see what a cleanup would actually delete. Deleting also only removed the directory: backend state and the snapshot repository were left behind, and a large tree blocked the caller for as long as the removal took. Both clients now report the count and total size, and open a dialog listing each folder with its full path, size, and whether it still holds a git checkout. Folders with a checkout may hold uncommitted work, so they start unselected. Deletion stages a rename and reaps in the background, tears down backend state, removes the snapshot repository, and stages once more if a directory reappears straight after removal. JetBrains also closes an open project frame rooted at the target before staging, and picks up the CLI teardown/snapshot parity VS Code already had. kilocode.teardownWorktree is absent from the pinned CLI spec, so it goes through a raw request that no-ops until the pin advances.
The notice sat below every worktree, where a yellow headline read as an error the list was reporting rather than a housekeeping prompt, and its ghost CTA did not look clickable. Move it above the first worktree, keep only the warning tint on the surface, and use an ordinary action button. The dialog asked for a bulk delete of dozens of folders while explaining nothing about where they came from, so add the same header explanation the JetBrains dialog carries. The rest is VS Code chrome the shared Dialog already implies: the header's 20px gutter on the body, one normal font instead of small bold headers, the shared check glyph, and columns sized to their content so the git checkout flag sits at the left edge of its cell instead of adrift in an over-wide one.
…er worktree dialog
The leftover-worktree-folders checkbox selection column used three unrelated
visual states instead of a checkbox: checked was a light gray filled blob,
unchecked was a transparent box with a border, and the select-all header's
indeterminate state fell through to a third, unrelated look. Map the kilo-ui
checkbox to @vscode/webview-ui-toolkit's own tokens instead, and treat
indeterminate exactly like checked (a VS Code checkbox has no third visual
language for "some selected", only the glyph changes). Fallbacks reproduce
today's colors outside the VS Code webview, so Storybook and kilo-web-ui are
unaffected.
Also collapse the dialog's explanation behind a Show more / Show less link on
both platforms, so the three-bullet detail no longer eats a third of the
JetBrains dialog by default; the intro sentence stays visible either way. The
JetBrains banner's pending state ("47 leftover worktree folder(s)") now says
it is still calculating the total size instead of presenting the count as if
it were final, composed from the existing summary/calculating strings so no
new translation was needed there.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (33 files)
Previous Review Summaries (5 snapshots, latest commit 53ab56a)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 53ab56a)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (4 files in this revision)
The previously reported early-return gap and the two verbatim-source assertions are resolved in this revision: the leave-check on Fix these issues in Kilo Cloud Previous review (commit 4515f66)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (2 files in this revision)
Fix these issues in Kilo Cloud Previous review (commit eed37cb)Status: 3 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Also verified against HEAD Files Reviewed (42 files in this revision)
Fix these issues in Kilo Cloud Previous review (commit 93658c3)Status: 10 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (5 files changed in this revision)
All findings above are carried forward from the previous review after re-verification against the current HEAD; the JetBrains banner Fix these issues in Kilo Cloud Previous review (commit a813609)Status: 12 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (90 files)
Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0 Review guidance: REVIEW.md from base branch |
…e walk falls short The leftover-folders banner claimed a definitive "· 0 B" whenever the size pass failed. orphanSizes catches its errors and answers an empty map, and requestSizes cached that as the answer, which also marked the path set as sized so it never asked again. A partial answer had the same shape of bug: the RPC contract omits paths it cannot walk, so summing them silently undercounted. Publish a total only when every path was actually measured, and distinguish "still walking" from "walked and came up short" — the banner shows the size when known, says it is calculating only while a walk is genuinely in flight, and otherwise falls back to the bare count rather than asserting a wrong number. Remember a failed path set so the failing walk is not re-run on every refresh, which is what naively dropping the cache would have caused. Also loosen the arch assertion added in the previous commit, which pinned the whole OrphanHelp prop expression verbatim, and fold this PR's changeset into the existing leftover-cleanup one so the JetBrains half of the change is covered by a release note.
…the set changes Choosing to delete a leftover folder left the size walk running over it. Both clients only reacted to a superseded path set, and only after the fact: VS Code aborted at the reconcile that follows a delete, by which point the directories were already renamed away, and JetBrains never cancelled at all -- requestSizes dropped its Job, so the walk always ran to completion just to be discarded on arrival, and walkSize had no cancellation check for a blocking walkFileTree that Dispatchers.IO cannot interrupt. Sizing now stops the moment the user commits to a delete, and restarts for whatever is still on disk afterwards. VS Code pauses the tracker for the whole delete rather than merely aborting, because the delete re-reconciles before it touches anything and that reconcile would otherwise start a fresh pass over the doomed set; the pause forgets the measured set so the closing reconcile actually re-walks the survivors instead of skipping them as already done. That closing reconcile is now unconditional, since the pass has to be restarted even when nothing was removed. JetBrains keeps the Job, cancels it on delete and on dispose, and cancels any pass still in flight before starting a new one. The backend cooperates: orphanSizes re-checks between paths and hands walkSize an isActive probe, and a walk that stops early answers null so a partial sum is never recorded as a measurement. KiloWorktreeService rethrows CancellationException instead of turning a cancelled pass into the empty map that means "this walk failed".
… forever The banner never left "calculating size…" in VS Code. Two separate reasons. The size pass is started by whichever reconcile runs first, and for the Agent Manager that is initContextState -- which never passed the push callback, unlike reconcileAndPush. So the walk finished (35s for 58 folders totalling 100 GB on the reporting machine), wrote its numbers onto the report, and nothing told the webview. No later reconcile recovered it either: the tracker had already recorded that path set, so every subsequent pass short-circuited and the callback that did exist was never reached. Sizing now notifies through a `sized` hook on the project context, wired once where contexts are built, so every reconcile path delivers its results and the per-call callback is gone. Second, the banner treated "no size" as "still measuring", but the walk omits a directory it cannot read rather than failing the batch, so a single unreadable folder among dozens would have kept the affordance up for good even with the push in place. A completed pass now marks every folder it covered as settled, with a size when it got one, and the UI distinguishes measuring from measured-and- unknown: the banner drops to the plain count, and the dialog's rows, footer and delete button say the size is unknown instead of pretending to still be working. The second cause did not reproduce on the reporting machine -- all 58 folders measured cleanly -- but one unreadable folder is enough to reintroduce the symptom, so both paths are fixed and covered.
The banner showed a total for one poll interval and then went back to "calculating size…" for good. Sizes only ever existed as a mutation on the report's orphan objects, and every reconcile rebuilds those objects from scratch, so the routine worktree-health poll threw the numbers away. The tracker meanwhile still remembered that it had already sized that path set, so the pass that would have recomputed them short-circuited: the sizes were gone and nothing was ever going to fetch them again. Measurements now live on the tracker, which outlives any single report, and are re-applied to whatever objects the current reconcile built. Only directories that have never been measured are walked, so a poll costs nothing and one new leftover folder no longer re-reads the other forty -- on the reporting repo that is 58 folders and 100 GB of walking saved per poll. Paths that leave the list are dropped from the cache, so it cannot grow for the life of the window and a folder that is deleted and recreated is measured again rather than resurrecting a stale number. A delete keeps the cache on purpose: removing one folder does not change the size of the others, so the reconcile that follows can show a correct total immediately instead of re-reading everything that survived.
…der leaves and rejoins the list Review follow-up on the sizing cache added in the previous commit. A walk in flight was only aborted when the reconcile that ran while it was running itself needed a walk of its own. If every path that walk covered left the orphan list at once -- removed outside Kilo, all in the same reconcile -- that reconcile's own missing set was empty, the early return fired first, and the abort never happened. The walk then landed later and wrote its answer into the cache anyway, for a path nothing had listed as an orphan for several reconciles. If that path reappeared, the stale number applied immediately, with no new walk ever running to correct it, because the cache already had an entry for it. The tracker now records which paths a walk is measuring and checks, on every call, whether any of them have left the current list -- aborting immediately if so, regardless of whether this call needs a walk of its own. Also fixes the pause path, which needed to forget the same bookkeeping or a delete followed by a resume would have permanently excluded whatever was mid-walk when the pause began. Separately hardens the two tests from the same review round that exercised the wrong thing without either failing: the JetBrains cancellation test cancelled a job before the suspend call was ever reached, proving only that `withContext` on a dead job throws, not that the cooperative checks inside the walk do anything -- it now times an uncancelled baseline over a large tree and shows a cancelled pass finishes in a fraction of that time. The wiring arch test asserted a literal `sized: (ctx) => opts.pushState(ctx)` source string, which the previous review round had already asked this file to stop doing elsewhere -- it now drives `createProjectWiring` for real and asserts the push actually happens.
…ed modules
Move every leftover-worktree-folder ("orphan") file into its own module on
both clients, per review feedback on this PR:
- VS Code: src/agent-manager/orphans/{size,sizing}.ts and
webview-ui/agent-manager/orphans/{OrphanDialog.tsx,OrphanNotice.tsx,
dialog-logic.ts}, previously flat in their parent directories.
- JetBrains shared: OrphanDto/OrphanKind/OrphanRemoveResultDto/
RemoveOrphansResultDto extracted from the large WorktreeDto.kt into their
own ai.kilocode.rpc.dto.orphans package.
- JetBrains frontend: OrphanBanner.kt/OrphanDialog.kt (+ tests) moved from
...agentManager.worktree into ai.kilocode.client.agentManager.orphans.
- JetBrains backend: the self-contained orphanSizes/walkSize filesystem walk
extracted out of the 1972-line KiloWorktreeRpcApiImpl.kt into
ai.kilocode.backend.worktree.orphans.OrphanSize.kt; the impl now delegates.
Worktree reconcile/classification and the removeOrphans staging/reap path
stay where they are on both clients: they share machinery with regular
worktree lifecycle management (registration checks, locks, the same
stage-rename-and-reap primitive) rather than being orphan-specific, mirroring
the existing split between orphan detection and worktree health classification.
Pure move + extraction, no behavior change. Verified with the full VS Code
unit suite (6071 tests) and the full JetBrains shared/backend/frontend suite
(4994 tests), plus typecheck/lint/knip on both sides.
…nt pass when the orphan set grows Addresses the still-open kilo-code-bot review comment on this PR (orphan-sizing.ts, "The replacement pass drops the paths of the walk it just aborted, so they stop being measured at all"). `missing` excluded any path already covered by a still-trustworthy in-flight walk (`pending`), but the very next statement unconditionally aborted that walk before starting a new pass over `missing` alone. When the orphan set grew while a walk was running, the growing call would abort the walk anyway to widen it, discard the aborted walk's result, and start the new one only over the newly-missing paths — dropping the excluded pending paths from both `known` and `pending` with nothing left to re-request them. Affected paths would show "calculating size…" until some unrelated reconcile happened to re-walk them, which is not guaranteed to ever happen. Fold `state.pending` into the new walk's target set whenever a replacement pass has to abort it, so the new pass covers everything the aborted one did. Added a regression test that grows the orphan set while the first walk is still in flight (the existing grow test at orphan-sizing.test.ts awaited the first pass first, so it never exercised this). Confirmed it fails on the prior code and passes with the fix.
Issue
Fixes #
No linked issue — this came from a report of an Agent Manager worktree list carrying 47 leftover folders worth 3.15 GB, with no way to find out what they were or clear them out safely.
Context
Every worktree Kilo creates lives under
.kilo/worktrees/. When one is removed outside Kilo, a deletion is interrupted, or something writes into the folder after git stopped tracking it, the directory stays on disk while git no longer lists it as a worktree. They pile up quietly — gigabytes of them — and some still hold a git checkout with uncommitted work in it.The only thing on offer before this PR was a notice with a single "clean up" action: no count, no size, no list. You could not tell whether clicking it would remove a stale empty directory or the last copy of work you had not pushed, so the safe move was to ignore it forever and let the folders accumulate. Cleanup was also only half a cleanup — the directory went away but its backend state and snapshot repository stayed behind, and a big folder froze the UI for as long as the delete took.
Implementation
Leftover folders are now something you can look at before you act on.
You can see what is there. The banner sits above the worktree list with the number of leftover folders and how much disk they are using. While that total is still being worked out it says so, and if it cannot be worked out it shows the count alone rather than inventing a number.
You can review before deleting. "Resolve…" opens a list of every leftover folder with its full path, its size, and whether it still contains a git checkout. Anything holding a checkout may hold uncommitted work, so it starts unselected — you have to opt in to deleting it. The explanation of where these folders come from starts collapsed behind a "Show more" link so it never crowds out the list.
Deleting is safe and stays out of the way. Removal happens in the background, so nothing freezes even on a huge folder, and each path is re-checked against git at the moment it is touched — a folder that became a real worktree in the meantime is skipped rather than deleted. Cleanup now also takes the backend state and snapshot repository with it, retries once if a directory reappears straight after removal, and finishes with a notification saying how many folders went. On JetBrains an open project window rooted at the folder is closed first.
It stops working when you stop caring. Measuring folder sizes is real disk reading, so it is cancelled the moment you start a delete, and whenever the list changes underneath it, instead of grinding through folders that are on their way out. Whatever is left afterwards gets measured again.
Both IDEs behave the same way throughout, and every new string is translated.
Two things worth knowing as a reviewer: the checkbox styling lands in the shared component, so it applies to every checkbox in the VS Code webview rather than only this dialog — that consistency is deliberate, and non-VS Code surfaces are unaffected. And one JetBrains cleanup call is absent from the pinned CLI version, so it quietly does nothing until that pin moves.
Screenshots / Video
JetBrains
VSCode
How to Test
.kilo/worktrees/— remove one withgit worktree removeoutside Kilo, or copy a directory in — and open the Agent Manager.Automated coverage runs on both sides: VS Code typecheck, lint, and 6061 unit tests, plus the JetBrains typecheck and full frontend suite. Verified after merging the latest
maininto the branch.Blocked checks and substitute verification
gh gist createrejects binary files and no other image host is reachable here. Substituted by rendering the built stylesheet in a real browser and reading back the resulting checkbox colours before and after, which is what the description above is based on.bun run lintfails in this workspace because a leftover, gitignored Agent Manager worktree from an unrelated session confuses the linter's config discovery. Unrelated to this diff. Substituted with the linter scoped to every file this PR touches: no errors, and the only warnings sit on lines this PR does not change.Checklist
Get in Touch