diff --git a/.changeset/orphan-worktree-cleanup-ui.md b/.changeset/orphan-worktree-cleanup-ui.md new file mode 100644 index 000000000000..c86fe333c7cb --- /dev/null +++ b/.changeset/orphan-worktree-cleanup-ui.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Move the Agent Manager leftover worktree folder notice to the top of the worktree list and rework its cleanup dialog to match VS Code, including an explanation of what the listed folders are before any bulk delete diff --git a/.changeset/worktree-orphan-cleanup-dialog.md b/.changeset/worktree-orphan-cleanup-dialog.md new file mode 100644 index 000000000000..55b26e8c933f --- /dev/null +++ b/.changeset/worktree-orphan-cleanup-dialog.md @@ -0,0 +1,6 @@ +--- +"kilo-code": minor +"@kilocode/kilo-jetbrains": minor +--- + +Leftover worktree folders now show a count and total size in the warning banner, with a "Resolve…" action that opens a dialog listing each folder's path, size, and whether it still holds a git checkout. While the total is still being measured the banner says so, and if the measurement fails it reports the count alone instead of claiming 0 B. Measuring stops as soon as you start a delete, or whenever the folder list changes, rather than walking folders that are on their way out, and then re-measures whatever is left. The dialog's explanation starts collapsed to its first paragraph behind a "Show more" link, and its checkboxes are drawn the way each IDE draws its own. Deletion runs in the background so the UI never freezes, folders that still contain a checkout are unchecked by default, and a completion notification reports how many were removed. Worktree deletion is also more thorough: JetBrains now tears down backend state and removes the snapshot repository for a deleted worktree the same way VS Code already does, and a directory that reappears immediately after deletion is cleaned up once more automatically. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt index 48788a7b3e6d..16fcf26b30ee 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt @@ -36,9 +36,15 @@ import ai.kilocode.rpc.dto.WorktreePrDto import ai.kilocode.rpc.dto.WorktreePrListDto import ai.kilocode.rpc.dto.WorktreeStatsDto import ai.kilocode.rpc.dto.WorktreeStatsListDto +import ai.kilocode.rpc.dto.orphans.OrphanDto +import ai.kilocode.rpc.dto.orphans.OrphanKind +import ai.kilocode.rpc.dto.orphans.OrphanRemoveResultDto +import ai.kilocode.rpc.dto.orphans.RemoveOrphansResultDto +import ai.kilocode.jetbrains.api.model.KilocodeRemoveSnapshotRequest import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine.ParentEnvironmentType import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.ide.actions.RevealFileAction import com.intellij.ide.impl.OpenProjectTask import com.intellij.ide.impl.ProjectUtil import com.intellij.openapi.application.ApplicationManager @@ -50,6 +56,10 @@ import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.wm.IdeFocusManager import com.intellij.openapi.wm.WindowManager import com.intellij.util.concurrency.annotations.RequiresEdt +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -67,15 +77,21 @@ import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonPrimitive import java.awt.Frame +import java.io.IOException +import java.net.URLEncoder +import java.nio.file.FileVisitResult import java.nio.file.Files import java.nio.file.Path +import java.nio.file.SimpleFileVisitor import java.nio.file.StandardCopyOption +import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.ConcurrentHashMap class KiloWorktreeRpcApiImpl( @@ -197,6 +213,35 @@ class KiloWorktreeRpcApiImpl( return true } + /** + * Closes and disposes the frame of an already-open project whose base directory is [dir], using + * the same matching as [focusIfOpen]. A live IDE frame rooted at a worktree being deleted is the + * most likely source of leftover `.idea`-only directories: the frame keeps rewriting project + * files into the checkout while it is being renamed/removed out from under it. Returns false + * when nothing matched, which is also the common case — most deletes target a worktree that was + * never opened as its own project. + */ + @RequiresEdt + private fun closeIfOpen(dir: Path): Boolean { + val target = dir.toString() + val project: Project = ProjectManager.getInstance().openProjects.firstOrNull { + ProjectUtil.isSameProject(dir, it) || FileUtil.pathsEqual(it.basePath, target) || FileUtil.pathsEqual(it.presentableUrl, target) + } ?: return false + LOG.info("worktree close (backend): closing frame for ${project.name} dir=$dir") + val closed = ProjectManager.getInstance().closeAndDispose(project) + LOG.info("worktree close (backend): closed=$closed dir=$dir") + return closed + } + + /** + * [closeIfOpen], hopping to the EDT first — a no-op outside an IntelliJ Application (a plain + * unit test, or split-mode's own backend process with no frame to close in the first place). + */ + private suspend fun closeIfOpenSafe(dir: Path) { + if (ApplicationManager.getApplication() == null) return + withContext(Dispatchers.EDT) { closeIfOpen(dir) } + } + override suspend fun listBranches(directory: String): WorktreeBranchesDto = withContext(Dispatchers.IO) { val base = Path.of(directory).normalize() val refs = runGit(base, "for-each-ref", "--format=%(refname:short)", "refs/heads") @@ -240,7 +285,7 @@ class KiloWorktreeRpcApiImpl( private fun sync(root: Path): List? = reconcile(root)?.items /** Managed worktrees of one repository, plus directories nothing claims. */ - internal data class Reconciled(val items: List, val orphans: List) + internal data class Reconciled(val items: List, val orphans: List) private fun reconcile(root: Path): Reconciled? { if (!Files.isDirectory(root)) { @@ -303,11 +348,17 @@ class KiloWorktreeRpcApiImpl( /** * Directories under `.kilo/worktrees/` that git does not track. * - * Reported, never removed: a leftover directory can still hold files that exist nowhere else, so - * deleting one is a user's decision. They are worth naming because they accumulate silently — an - * interrupted delete or a hand-removed `.git/worktrees` entry leaves one behind every time. + * Reported, never removed automatically: a leftover directory can still hold files that exist + * nowhere else, so deleting one is the user's decision (see [removeOrphans]). They are worth + * naming because they accumulate silently — an interrupted delete or a hand-removed + * `.git/worktrees` entry leaves one behind every time. + * + * Classified into [OrphanKind.BROKEN] (still holds a `.git` file/dir — an interrupted delete + * left a real checkout behind) or [OrphanKind.LEFTOVER] (a plain directory, e.g. a hand-removed + * `.idea`/`.kilo-dev` folder), mirroring the VS Code extension's `worktree-reconcile.ts` so both + * clients agree on which rows are risky to delete. */ - private fun orphanDirs(all: List, base: Path?): List { + private fun orphanDirs(all: List, base: Path?): List { val dir = base?.resolve(".kilo")?.resolve("worktrees")?.normalize() ?: return emptyList() if (!Files.isDirectory(dir)) return emptyList() val tracked = all.map { Path.of(it.path).normalize().toString() }.toSet() @@ -317,7 +368,7 @@ class KiloWorktreeRpcApiImpl( .map { it.normalize() } .filter { it.fileName.toString().startsWith(".kilo-delete-").not() } .filter { it.toString() !in tracked } - .map { it.toString() } + .map { OrphanDto(it.toString(), if (Files.exists(it.resolve(".git"))) OrphanKind.BROKEN else OrphanKind.LEFTOVER) } .toList() } }.getOrElse { err -> @@ -325,7 +376,7 @@ class KiloWorktreeRpcApiImpl( emptyList() } if (orphans.isNotEmpty()) { - LOG.info("worktree orphan directories (not removed): ${orphans.joinToString(", ")}") + LOG.info("worktree orphan directories (not removed): ${orphans.joinToString(", ") { it.path }}") } return orphans } @@ -703,6 +754,12 @@ class KiloWorktreeRpcApiImpl( return@lock RemoveWorktreeResultDto(error = "Delete nested worktrees first:\n$names") } val store = worktreeNameStore(items) ?: base.resolve(".kilo").resolve(WORKTREE_NAMES_FILE) + // A live IDE frame rooted at the worktree keeps rewriting project files into the + // checkout while it is being renamed/removed underneath it — the most likely source + // of leftover `.idea`-only directories. Close it before anything else touches disk. + // No-op outside an IntelliJ Application (a plain unit test): there is no EDT to hop + // to and nothing could have opened a frame in the first place. + closeIfOpenSafe(Path.of(target.path)) trash?.mark(target.path) try { removeManaged(base, target, branch, force, store, storage, start) @@ -735,6 +792,10 @@ class KiloWorktreeRpcApiImpl( val unlock = runGit(base, "worktree", "unlock", target.path) if (!unlock.ok) LOG.info("worktree unlock skipped: path=${target.path} exit=${unlock.exit} stderr=${unlock.stderr.trim()}") } + // Best-effort, before anything touches disk: kill PTYs rooted here and dispose a loaded CLI + // backend instance for this worktree, so the CLI cannot rewrite `.kilo`/`.opencode` into a + // path this call is about to rename or delete. Never blocks or fails the removal. + teardownWorktree(base, target.path) val targetPath = Path.of(target.path) // Only skip git's own removal when the checkout directory is actually gone. Git also flags a // worktree prunable when its admin metadata is stale while the files remain; those must still @@ -782,12 +843,15 @@ class KiloWorktreeRpcApiImpl( if (!prune.ok) { LOG.warn("worktree prune failed: path=${target.path} exit=${prune.exit} stderr=${snippet(prune.stderr)}") } + // Best-effort, mirroring [teardownWorktree] above: the snapshot repository for this worktree + // is no longer reachable from any live checkout once the prune above lands. + removeSnapshot(base, target.path) // The worktree is gone; a failed branch delete must not fail the removal, only warn. branch?.trim()?.takeIf { it.isNotEmpty() }?.let { val del = runGit(base, "branch", "-D", it) if (!del.ok) LOG.warn("worktree branch delete failed: branch=$it exit=${del.exit} stderr=${del.stderr.trim()}") } - staged?.let { trash?.reap(it) } + staged?.let { temp -> trash?.reap(temp) { checkReappearance(target.path, temp) } } LOG.info("worktree removed: path=${target.path} branch=${branch ?: "(none)"} mode=$mode ms=${System.currentTimeMillis() - start}") invalidate() removeWorktreeState(store, target.path) @@ -797,6 +861,186 @@ class KiloWorktreeRpcApiImpl( return RemoveWorktreeResultDto(ok = true) } + /** + * Checks whether [originalPath] exists again once the reap of its staged sibling [temp] has + * settled, and if so stages and reaps it once more. Bounded to exactly one retry: anything that + * survives that simply reappears in the next reconcile and therefore in the orphan banner, which + * is an acceptable outcome for something actively recreating the directory (e.g. a JetBrains dev + * backend still writing `.kilo-dev` while its own worktree is deleted). + */ + internal fun checkReappearance(originalPath: String, temp: Path) { + val original = Path.of(originalPath) + if (!Files.exists(original)) return + LOG.warn("worktree reappeared after reap: path=$originalPath temp=$temp — staging once more") + val retry = trash?.stage(original) + if (retry != null) { + trash.reap(retry) + } else { + LOG.warn("worktree reappearance retry could not stage: path=$originalPath") + } + } + + /** + * Best-effort CLI call to kill the PTYs rooted in [path] and dispose a loaded backend instance + * for it, without booting an instance for the directory. Never throws into the caller and never + * blocks the removal it guards: a CLI that cannot be reached, or a pinned CLI release that + * predates this endpoint, is exactly the same as "nothing to tear down" from here. + * + * Called via a raw HTTP POST rather than the generated [ai.kilocode.jetbrains.api.client.DefaultApi] + * client: `kilocode.teardownWorktree` is new enough that a plugin pinned to an older CLI release + * has a client with no matching method at all, and this call must still compile and no-op against + * that CLI rather than fail to build. + */ + private fun teardownWorktree(base: Path, path: String) { + val app = runCatching { service() }.getOrNull() ?: return + val http = app.http ?: return + val port = app.port + if (port <= 0) return + val body = JsonObject(mapOf("worktree" to JsonPrimitive(path))).toString() + postKilocode(http, port, "kilocode/worktree/teardown", base, body, "teardown") + } + + /** + * Best-effort CLI call to remove the snapshot repository for an already-deleted worktree. Uses + * the generated client because `kilocode.removeSnapshot` predates this feature and is present in + * every pinned CLI release this plugin supports. + */ + private fun removeSnapshot(base: Path, path: String) { + val client = runCatching { service().api }.getOrNull() ?: return + runCatching { + client.kilocodeRemoveSnapshot(directory = base.toString(), kilocodeRemoveSnapshotRequest = KilocodeRemoveSnapshotRequest(worktree = path)) + }.onFailure { err -> LOG.info("worktree remove-snapshot skipped: path=$path message=${err.message}") } + } + + private fun postKilocode(http: OkHttpClient, port: Int, path: String, directory: Path, body: String, op: String) { + try { + val url = "http://127.0.0.1:$port/$path?directory=${URLEncoder.encode(directory.toString(), "UTF-8")}" + val req = Request.Builder() + .url(url) + .header("Accept", "application/json") + .post(body.toRequestBody("application/json".toMediaType())) + .build() + http.newCall(req).execute().use { res -> + if (!res.isSuccessful) LOG.info("worktree $op skipped: HTTP ${res.code}") + } + } catch (e: Exception) { + LOG.info("worktree $op skipped: ${e.message}") + } + } + + /** + * Delegates to [ai.kilocode.backend.worktree.orphans.orphanSizes] — a pure filesystem walk with no + * dependency on this class's worktree machinery, extracted so it can be read and tested on its own. + * [directory] is unused: sizing has never needed the repository root, only the paths themselves. + */ + override suspend fun orphanSizes(directory: String, paths: List): Map = + ai.kilocode.backend.worktree.orphans.orphanSizes(paths) + + /** One orphan path submitted to [removeOrphans], resolved and guard-checked. */ + private data class OrphanTarget(val raw: String, val path: Path, val error: String?) + + override suspend fun removeOrphans(directory: String, paths: List): RemoveOrphansResultDto = + withContext(Dispatchers.IO) { + if (paths.isEmpty()) return@withContext RemoveOrphansResultDto() + val base = Path.of(directory).normalize() + lock(base, "removeOrphans") { + // Fail-closed, exactly like `remove()`'s own `git worktree list` re-check: the dialog's + // selection can be stale by the time delete runs, so every path is re-validated against + // a fresh scan immediately before it is touched. + val reconciled = reconcile(base) + ?: return@lock RemoveOrphansResultDto(paths.map { OrphanRemoveResultDto(it, false, "git worktree list failed") }) + val freshOrphans = reconciled.orphans.mapTo(HashSet()) { Path.of(it.path).normalize().toString() } + val managed = reconciled.items.mapTo(HashSet()) { Path.of(it.path).normalize().toString() } + val main = reconciled.items.firstOrNull { it.main } + val storage = main?.let { Path.of(it.path).normalize().resolve(".kilo").resolve("worktrees").normalize() } + val targets = paths.map { raw -> + val norm = Path.of(raw).normalize() + val error = when { + // Checked first so an unrelated path gets the precise reason instead of the + // more general "not an orphan" — it was never a candidate at all. + storage == null || norm.parent != storage -> "Refusing to remove a path outside managed storage: $raw" + // Defense in depth: a registered worktree should never appear in the orphan + // list this was validated against, but nothing here should ever point a + // filesystem delete at one if it somehow did. + norm.toString() in managed -> "Refusing to remove a managed worktree: $raw" + norm.toString() !in freshOrphans -> "No longer an orphan directory: $raw" + else -> null + } + if (error != null) LOG.warn("worktree orphan remove rejected: path=$raw reason=$error") + OrphanTarget(raw, norm, error) + } + val ready = targets.filter { it.error == null } + // A live IDE frame rooted at an orphan directory (e.g. someone opened a hand-removed + // worktree's leftover folder directly) would keep rewriting project files into it, + // the same reason `remove()` closes one before staging. + ready.forEach { closeIfOpenSafe(it.path) } + val results = targets.map { target -> target.error?.let { OrphanRemoveResultDto(target.raw, false, it) } ?: removeOrphan(base, target.path) } + invalidate() + storage?.let { trash?.sweep(it) } + RemoveOrphansResultDto(results) + } + } + + /** + * Removes one already-validated orphan directory [target]: CLI teardown, stage-rename + + * background reap (the same primitive [removeManaged] uses for a managed worktree — orphans are + * never git-tracked, so no `git worktree` command ever runs against one), CLI snapshot removal, + * and the same post-reap reappearance guard. + */ + private fun removeOrphan(base: Path, target: Path): OrphanRemoveResultDto { + val key = target.toString() + teardownWorktree(base, key) + trash?.mark(key) + try { + val temp = trash?.stage(target) + if (temp != null) { + LOG.info("worktree orphan staged: path=$key temp=$temp") + trash.reap(temp) { checkReappearance(key, temp) } + } else { + val failure = runCatching { deleteOrphanSync(target) }.exceptionOrNull() + if (failure != null) { + LOG.warn("worktree orphan delete failed: path=$key message=${failure.message}", failure) + return OrphanRemoveResultDto(key, false, failure.message ?: "delete failed") + } + LOG.info( + "worktree orphan deleted synchronously: path=$key " + + "reason=${if (trash == null) "trash-unavailable" else "rename-failed"}", + ) + } + } finally { + trash?.unmark(key) + } + removeSnapshot(base, key) + LOG.info("worktree orphan removed: path=$key") + return OrphanRemoveResultDto(key, true) + } + + /** Synchronous recursive delete, used only when [trash] is unavailable or staging failed. */ + private fun deleteOrphanSync(target: Path) { + if (!Files.exists(target)) return + Files.walkFileTree( + target, + object : SimpleFileVisitor() { + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + Files.deleteIfExists(file) + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { + Files.deleteIfExists(dir) + return FileVisitResult.CONTINUE + } + }, + ) + } + + override suspend fun revealPath(path: String): Boolean = withContext(Dispatchers.IO) { + if (!RevealFileAction.isSupported()) return@withContext false + val target = Path.of(path).normalize() + if (!Files.isDirectory(target)) return@withContext false + runCatching { RevealFileAction.openDirectory(target) }.isSuccess + } + override suspend fun rename(directory: String, path: String, name: String): RenameWorktreeResultDto = withContext(Dispatchers.IO) { val title = name.trim() diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/worktree/WorktreeTrash.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/worktree/WorktreeTrash.kt index 3bdaa31f43d6..a84e653b4f3e 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/worktree/WorktreeTrash.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/worktree/WorktreeTrash.kt @@ -116,9 +116,18 @@ class WorktreeTrash(private val cs: CoroutineScope) { } } - /** Deletes [temp] recursively on this service's own scope. Never throws into the caller. */ - fun reap(temp: Path) { - track(cs.launch(Dispatchers.IO) { reapNow(temp) }) + /** + * Deletes [temp] recursively on this service's own scope. Never throws into the caller. + * + * [onDone] runs after the reap attempt finishes (success or failure), still on this service's + * own IO dispatcher — callers use it for the post-reap reappearance guard: staging a worktree + * created a fresh sibling that vanished the moment the rename completed, but something can still + * recreate the original path while the recursive delete of its sibling is still in flight (a + * dev-run backend re-materializing `.kilo-dev`, for example), and only checking after the delete + * settles observes that reliably. + */ + fun reap(temp: Path, onDone: () -> Unit = {}) { + track(cs.launch(Dispatchers.IO) { reapNow(temp); onDone() }) } /** diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/worktree/orphans/OrphanSize.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/worktree/orphans/OrphanSize.kt new file mode 100644 index 000000000000..e98a8bbb44f7 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/worktree/orphans/OrphanSize.kt @@ -0,0 +1,88 @@ +package ai.kilocode.backend.worktree.orphans + +import ai.kilocode.log.KiloLog +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext + +private val LOG = KiloLog.create(OrphanTree::class.java) + +/** Marker class for [KiloLog.create] — this file has no class of its own. */ +private class OrphanTree + +/** + * Apparent on-disk size of each leftover worktree directory under `.kilo/worktrees/`. + * + * No dependency on [ai.kilocode.backend.rpc.KiloWorktreeRpcApiImpl] or any other worktree machinery — + * this is a pure filesystem walk, callable from the RPC layer or a test. Mirrors VS Code's + * `orphans/size.ts`. + * + * Cancellation here has to be cooperative. The caller cancels this pass whenever the orphan set + * changes or a delete starts, but [Files.walkFileTree] is blocking and cannot be interrupted, so a + * cancelled coroutine on [Dispatchers.IO] would otherwise keep walking every remaining path. The loop + * re-checks between paths and the walk itself is handed an `isActive` probe. + * + * A path that fails to walk — permission error, disappeared mid-walk — is omitted from the result + * rather than failing the whole batch, so one bad orphan can never block the size of the rest. + */ +suspend fun orphanSizes(paths: List): Map = + withContext(Dispatchers.IO) { + val result = mutableMapOf() + for (raw in paths) { + ensureActive() + val path = Path.of(raw).normalize() + // A directory that is gone (already removed, mid-delete elsewhere) is a failure to + // measure, not a size of zero — omitted the same as a walk that throws below. + if (!Files.isDirectory(path)) { + LOG.info("worktree orphan size skipped: path=$raw reason=missing") + continue + } + val size = runCatching { walkSize(path) { isActive } }.getOrElse { err -> + LOG.info("worktree orphan size skipped: path=$raw message=${err.message}") + null + } ?: continue + result[raw] = size + } + result + } + +/** + * Apparent size of [root]: sum of regular-file sizes, never following symlinks. + * + * Answers null once [active] goes false, so a walk that stopped early contributes nothing rather + * than a partial sum that would be indistinguishable from a real measurement. + */ +private fun walkSize(root: Path, active: () -> Boolean): Long? { + if (!Files.isDirectory(root)) return 0 + var total = 0L + var stopped = false + Files.walkFileTree( + root, + object : SimpleFileVisitor() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult = step() + + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + val next = step() + if (next == FileVisitResult.TERMINATE) return next + if (!attrs.isSymbolicLink) total += attrs.size() + return next + } + + override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult = FileVisitResult.CONTINUE + + private fun step(): FileVisitResult { + if (active()) return FileVisitResult.CONTINUE + stopped = true + return FileVisitResult.TERMINATE + } + }, + ) + return if (stopped) null else total +} diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt index 72223ef021d1..711e9eb6f5cb 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt @@ -21,6 +21,8 @@ import ai.kilocode.rpc.dto.GhState import ai.kilocode.rpc.dto.MoveStage import ai.kilocode.rpc.dto.WorktreeDirtyDto import ai.kilocode.rpc.dto.WorktreeDto +import ai.kilocode.rpc.dto.orphans.OrphanKind +import ai.kilocode.rpc.dto.orphans.OrphanRemoveResultDto import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import com.intellij.execution.configurations.GeneralCommandLine @@ -30,11 +32,16 @@ import com.intellij.openapi.util.SystemInfo import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex +import java.util.concurrent.TimeUnit import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.Assumptions.assumeTrue import java.nio.file.Files @@ -136,12 +143,182 @@ class KiloWorktreeRpcApiImplTest { // Orphan paths are resolved the way git reports worktree paths (realpath), so they compare // equal to the other DTOs' paths on a symlinked temp dir. - assertEquals(listOf(leftover.toRealPath().toString()), listed.orphans) + assertEquals(listOf(leftover.toRealPath().toString()), listed.orphans.map { it.path }) + assertEquals(listOf(OrphanKind.LEFTOVER), listed.orphans.map { it.kind }, "a plain directory is a leftover, not a broken checkout") assertTrue(listed.worktrees.any { it.path == created.path }, "a live worktree is not an orphan") // Reported, never deleted: a leftover directory can hold files that exist nowhere else. assertTrue(Files.isDirectory(leftover.resolve(".kilo-dev"))) } + @Test + fun `list classifies an orphan that still holds a git checkout as broken`() = runBlocking { + initRepo() + val broken = repo.resolve(".kilo").resolve("worktrees").resolve("broken") + Files.createDirectories(broken) + // A plain file named `.git` is what a linked worktree checkout actually has (it points back + // at the shared `.git` dir) — a directory named `.git` is the primary repo's own shape. Either + // is enough to call the leftover "still holds a checkout". + Files.writeString(broken.resolve(".git"), "gitdir: /nowhere\n") + + val listed = api.list(repo.toString()) + + assertEquals(listOf(broken.toRealPath().toString()), listed.orphans.map { it.path }) + assertEquals(listOf(OrphanKind.BROKEN), listed.orphans.map { it.kind }) + } + + @Test + fun `orphanSizes sums regular file sizes and never follows symlinks`() = runBlocking { + initRepo() + val orphan = repo.resolve(".kilo").resolve("worktrees").resolve("leftover") + Files.createDirectories(orphan) + Files.write(orphan.resolve("a.bin"), ByteArray(10)) + Files.write(orphan.resolve("b.bin"), ByteArray(20)) + val target = Files.createTempDirectory("kilo-orphan-symlink-target") + Files.write(target.resolve("outside.bin"), ByteArray(1_000)) + runCatching { Files.createSymbolicLink(orphan.resolve("link"), target) } + try { + val sizes = api.orphanSizes(repo.toString(), listOf(orphan.toString())) + assertEquals(30L, sizes[orphan.toString()], "size must exclude the symlinked tree entirely") + } finally { + delete(target) + } + } + + @Test + fun `orphanSizes omits a path that fails to walk instead of failing the batch`() = runBlocking { + initRepo() + val ok = repo.resolve(".kilo").resolve("worktrees").resolve("leftover") + Files.createDirectories(ok) + Files.write(ok.resolve("a.bin"), ByteArray(5)) + val missing = repo.resolve(".kilo").resolve("worktrees").resolve("does-not-exist") + + val sizes = api.orphanSizes(repo.toString(), listOf(ok.toString(), missing.toString())) + + assertEquals(5L, sizes[ok.toString()]) + assertFalse(sizes.containsKey(missing.toString()), "a path that could not be measured must be omitted, not zero") + } + + /** + * The frontend cancels a size pass whenever the orphan set changes or a delete starts, and + * `Files.walkFileTree` cannot be interrupted — so the walk has to check cancellation itself rather + * than leave a cancelled coroutine walking every remaining path on Dispatchers.IO. + * + * Cancelling before the call is ever reached (`job.cancel()` then `withContext(job) { ... }`) would + * only prove that `withContext` on an already-dead job throws without entering the block — true of + * any suspend function and unrelated to the `ensureActive()`/`isActive` checks this is meant to + * cover. This instead cancels a walk that is demonstrably still running: a full, uncancelled pass + * over a large tree is timed first, then a second pass over the same tree is cancelled partway + * through and shown to finish in a small fraction of that baseline — which a walk with no + * cancellation checks could not do, since nothing would tell it to stop. + */ + @Test + fun `orphanSizes stops mid-walk once cancelled, instead of running to completion`() = runBlocking { + initRepo() + val big = repo.resolve(".kilo").resolve("worktrees").resolve("big") + Files.createDirectories(big) + repeat(20_000) { i -> Files.write(big.resolve("f$i.bin"), ByteArray(1)) } + val target = listOf(big.toString()) + + val baselineStart = System.nanoTime() + api.orphanSizes(repo.toString(), target) + val baseline = System.nanoTime() - baselineStart + + val job = Job() + val cancelledStart = System.nanoTime() + val deferred = CoroutineScope(Dispatchers.IO + job).async { api.orphanSizes(repo.toString(), target) } + // A quarter of the uncancelled duration is comfortably inside the walk, not before or after it. + delay(TimeUnit.NANOSECONDS.toMillis(baseline / 4).coerceAtLeast(1)) + job.cancel() + val outcome = runCatching { deferred.await() } + val elapsed = System.nanoTime() - cancelledStart + + assertTrue( + outcome.exceptionOrNull() is CancellationException, + "a cancelled pass must not answer with sizes -> ${outcome.getOrNull()}", + ) + assertTrue( + elapsed < baseline * 3 / 4, + "cancelling took ${elapsed / 1_000_000}ms against an uncancelled ${baseline / 1_000_000}ms " + + "-- a walk with no cancellation checks would not finish any faster than the baseline", + ) + } + + @Test + fun `removeOrphans deletes a validated leftover directory`() = runBlocking { + initRepo() + val orphan = repo.resolve(".kilo").resolve("worktrees").resolve("leftover") + Files.createDirectories(orphan.resolve("nested")) + Files.writeString(orphan.resolve("nested").resolve("file.txt"), "hi") + val real = orphan.toRealPath().toString() + + val result = api.removeOrphans(repo.toString(), listOf(real)) + + assertEquals(listOf(OrphanRemoveResultDto(real, ok = true)), result.results) + assertFalse(Files.exists(orphan), "the trash-unavailable fallback must delete synchronously, not defer") + assertTrue(api.list(repo.toString()).orphans.isEmpty()) + } + + @Test + fun `removeOrphans refuses a path outside managed storage`() = runBlocking { + initRepo() + val outside = repo.resolve("outside") + Files.createDirectories(outside) + + val result = api.removeOrphans(repo.toString(), listOf(outside.toString())) + + val entry = result.results.single() + assertFalse(entry.ok) + assertTrue(entry.error?.contains("Refusing") == true, entry.error) + assertTrue(Files.isDirectory(outside), "unmanaged directory must not be touched") + } + + @Test + fun `removeOrphans refuses a path that is actually a registered worktree`() = runBlocking { + initRepo() + val created = assertNotNull(api.create(repo.toString(), CreateWorktreeRequestDto("feature/x")).worktree) + + val result = api.removeOrphans(repo.toString(), listOf(created.path)) + + val entry = result.results.single() + assertFalse(entry.ok) + assertTrue(entry.error?.contains("managed worktree") == true, entry.error) + assertTrue(Files.isDirectory(Path.of(created.path)), "a live managed worktree must not be touched") + } + + @Test + fun `removeOrphans rejects a stale selection that is no longer an orphan`() = runBlocking { + initRepo() + val orphan = repo.resolve(".kilo").resolve("worktrees").resolve("leftover") + Files.createDirectories(orphan) + val real = orphan.toRealPath().toString() + // Someone else already cleaned it up between the dialog's scan and this call. + delete(orphan) + + val result = api.removeOrphans(repo.toString(), listOf(real)) + + val entry = result.results.single() + assertFalse(entry.ok) + assertTrue(entry.error?.contains("No longer an orphan") == true, entry.error) + } + + @Test + fun `removeOrphans reports independent results per path`() = runBlocking { + initRepo() + val ok = repo.resolve(".kilo").resolve("worktrees").resolve("leftover") + Files.createDirectories(ok) + val okReal = ok.toRealPath().toString() + val outside = repo.resolve("outside") + Files.createDirectories(outside) + + val result = api.removeOrphans(repo.toString(), listOf(okReal, outside.toString())) + + assertEquals(2, result.results.size) + assertTrue(result.results.first { it.path == okReal }.ok) + assertFalse(result.results.first { it.path == outside.toString() }.ok) + assertFalse(Files.exists(ok)) + assertTrue(Files.isDirectory(outside)) + } + @Test fun `list and the polls agree on which worktrees exist`() = runBlocking { initRepo() @@ -417,6 +594,53 @@ class KiloWorktreeRpcApiImplTest { assertFalse(listed.contains(created.path), "a poll with a free lock should prune: $listed") } + @Test + fun `checkReappearance re-stages and reaps once when the original path came back`() = runBlocking { + val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + val trash = WorktreeTrash(scope) + val trashApi = KiloWorktreeRpcApiImpl(trash) + try { + val dir = repo.resolve("reappeared") + Files.createDirectories(dir) + // Simulate a reap that already settled (the sibling from the original stage is long gone) + // while something recreated the original path afterward — the exact race the guard exists + // to catch, made deterministic instead of racing a real background reap. + val temp = assertNotNull(trash.stage(dir)) + trash.reap(temp) + trash.drain() + Files.createDirectories(dir) + + trashApi.checkReappearance(dir.toString(), temp) + trash.drain() + + assertFalse(Files.exists(dir), "a path that reappeared after reap must be staged and reaped once more") + } finally { + scope.cancel() + } + } + + @Test + fun `checkReappearance does nothing when the original path did not come back`() = runBlocking { + val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + val trash = WorktreeTrash(scope) + val trashApi = KiloWorktreeRpcApiImpl(trash) + try { + val dir = repo.resolve("gone-for-good") + Files.createDirectories(dir) + val temp = assertNotNull(trash.stage(dir)) + trash.reap(temp) + trash.drain() + + trashApi.checkReappearance(dir.toString(), temp) + trash.drain() + + assertFalse(Files.exists(dir)) + assertEquals(0, trash.pending(), "nothing should have been staged again") + } finally { + scope.cancel() + } + } + @Test fun `staleWorktrees excludes a worktree already marked doomed in WorktreeTrash`() { val trash = WorktreeTrash(CoroutineScope(Dispatchers.Default + SupervisorJob())) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/worktree/WorktreeTrashTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/worktree/WorktreeTrashTest.kt index 357662b4b089..3076bda3ad09 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/worktree/WorktreeTrashTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/worktree/WorktreeTrashTest.kt @@ -131,6 +131,29 @@ class WorktreeTrashTest { trash.drain() // must not throw } + @Test + fun `reap with onDone runs the callback once the delete settles`() = runBlocking { + val dir = root.resolve("${WorktreeTrash.PREFIX}reap-onDone") + Files.createDirectories(dir) + var ran = false + + trash.reap(dir) { ran = true } + trash.drain() + + assertFalse(Files.exists(dir)) + assertTrue(ran, "onDone must run after the reap attempt finishes") + } + + @Test + fun `reap with onDone still runs the callback when the directory never existed`() = runBlocking { + var ran = false + + trash.reap(root.resolve("${WorktreeTrash.PREFIX}never-existed-onDone")) { ran = true } + trash.drain() + + assertTrue(ran, "onDone must run even when there was nothing to delete") + } + @Test fun `sweep reaps every delete-prefixed directory and ignores ordinary ones`() = runBlocking { val orphanA = root.resolve("${WorktreeTrash.PREFIX}a").also { Files.createDirectories(it) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt index 22fe2728d327..be8d9ef15ad2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt @@ -8,6 +8,7 @@ import ai.kilocode.client.agentManager.worktree.NewWorktreeDialog import ai.kilocode.client.agentManager.worktree.NewWorktreeHandle import ai.kilocode.client.agentManager.worktree.NewWorktreePlan import ai.kilocode.client.agentManager.worktree.GhBanner +import ai.kilocode.client.agentManager.orphans.OrphanBanner import ai.kilocode.client.agentManager.worktree.WorktreeController import ai.kilocode.client.agentManager.worktree.WorktreeDataKeys import ai.kilocode.client.agentManager.worktree.WorktreeIcons @@ -47,6 +48,7 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionActivityKind import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack import ai.kilocode.client.ui.list.ActiveList import ai.kilocode.client.ui.list.ActiveListBadge import ai.kilocode.client.ui.list.ActiveListConfig @@ -160,6 +162,7 @@ class AgentManagerPanel( private var dirty: Map = emptyMap() private var running: Set = emptySet() private var hovered: String? = null + private var orphanBanner: OrphanBanner? = null init { Disposer.register(parent, this) @@ -168,7 +171,11 @@ class AgentManagerPanel( // busy list or a rebuilt model has already dropped the hover the popup was opened from. list.onScroll = { popup.hideAll() } isOpaque = true - project?.let { addToTop(GhBanner(it, this)) } + project?.let { + val orphan = OrphanBanner(it, controller, this) + orphanBanner = orphan + addToTop(Stack.vertical().next(GhBanner(it, this)).next(orphan)) + } addToCenter(body()) list.installPopup(group) sync() @@ -187,7 +194,7 @@ class AgentManagerPanel( project?.service()?.refreshPr(force = true, maxAge = 0) autoRunSetupScript(created) } - controller.onReload = { sync() } + controller.onReload = { sync(); orphanBanner?.refresh() } controller.onCreateFailure = { err -> notifyCreateFailed(err) } controller.onMoveFailure = { err -> notifyMoveFailed(err) } controller.onRemoveSuccess = { item, index -> onRemoved(item, index) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/orphans/OrphanBanner.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/orphans/OrphanBanner.kt new file mode 100644 index 000000000000..70eb21d57726 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/orphans/OrphanBanner.kt @@ -0,0 +1,214 @@ +package ai.kilocode.client.agentManager.orphans + +import ai.kilocode.client.agentManager.worktree.KiloWorktreeService +import ai.kilocode.client.agentManager.worktree.WorktreeController +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.util.edt +import ai.kilocode.rpc.dto.orphans.OrphanRemoveResultDto +import com.intellij.notification.Notification +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.util.Disposer +import com.intellij.ui.EditorNotificationPanel +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +/** + * Warning banner for leftover worktree folders (see [ai.kilocode.rpc.dto.orphans.OrphanDto]), modelled + * on [ai.kilocode.client.agentManager.worktree.GhBanner]'s warning treatment. Count comes from + * [WorktreeController.orphans] — the same `reload` that fills the worktree list — so the banner and + * the list can never disagree about which paths are orphans. Sizes are requested lazily off the EDT + * and cached until the orphan path set changes. + */ +internal class OrphanBanner( + private val project: Project, + private val controller: WorktreeController, + parent: Disposable, +) : EditorNotificationPanel(Status.Warning) { + private var syncedCount = -1 + private var syncedSize: Long? = null + private var syncedPending = false + + /** Path set the cached [sizes] answer for. Empty until the first size pass lands. */ + private var sizedPaths: Set = emptySet() + private var sizes: Map = emptyMap() + + /** Path set a size fetch is currently in flight for, or null when none is running. */ + private var requested: Set? = null + + /** + * Path set whose size pass came back with nothing at all. Kept so a failed walk is not retried on + * every [refresh] — the banner drops the size claim instead, and a changed orphan set asks again. + */ + private var failed: Set? = null + + /** The size pass in flight, kept so it can be cancelled rather than left to finish unwatched. */ + private var job: Job? = null + + init { + refresh() + Disposer.register(parent) { job?.cancel() } + } + + /** Re-reads [WorktreeController.orphans] and updates the banner. Call after every [reload]. */ + @RequiresEdt + fun refresh() { + val orphans = controller.orphans + if (orphans.isEmpty()) { + sizedPaths = emptySet() + sizes = emptyMap() + requested = null + failed = null + if (isVisible) { + isVisible = false + changed() + } + return + } + val paths = orphans.mapTo(HashSet()) { it.path } + if (paths != sizedPaths && paths != requested && paths != failed) { + requested = paths + failed = null + requestSizes(paths) + } + // Only a pass that measured *every* path is a total worth showing. orphanSizes omits paths it + // could not walk and answers empty for a failed batch, so summing a partial answer would + // report a confident "0 B" (or a silent undercount) for folders that are actually large. + val total = if (paths == sizedPaths && sizes.keys.containsAll(paths)) sizes.values.sum() else null + // No total yet, but a walk is still running: say it is being calculated. Once the walk has + // settled without a full answer, drop the size claim entirely rather than keep promising one. + val pending = total == null && requested != null + val dirty = orphans.size != syncedCount || total != syncedSize || pending != syncedPending + if (dirty) sync(orphans.size, total, pending) + if (!isVisible) { + isVisible = true + changed() + return + } + if (dirty) changed() + } + + private fun sync(count: Int, total: Long?, pending: Boolean) { + clear() + val summary = KiloBundle.message("worktree.orphans.summary", count) + text( + when { + total != null -> KiloBundle.message("worktree.orphans.summarySize", count, StringUtil.formatFileSize(total)) + // Sizing is a background fs walk (see requestSizes/refresh above) — while it is in + // flight, say so instead of showing the count as if it were the final answer. + pending -> "$summary \u00b7 ${KiloBundle.message("worktree.orphans.calculating")}" + else -> summary + }, + ) + createActionLabel(KiloBundle.message("worktree.orphans.resolve")) { openDialog() } + syncedCount = count + syncedSize = total + syncedPending = pending + } + + private fun requestSizes(paths: Set) { + // A pass for a superseded path set is worthless the moment the set changes, and the fs walk is + // the expensive part — cancel it instead of letting it run to completion just to be discarded + // on arrival. Cancellation is cooperative on the backend (see KiloWorktreeRpcApiImpl). + job?.cancel() + job = service().scope.launch { + val result = service().orphanSizes(controller.directory, paths.toList()) + // Belt and braces for the cancelled case: in split mode the call can still answer normally + // after cancellation, and an empty answer must not be recorded as a failed walk. + if (!isActive) return@launch + edt { + if (requested != paths) return@edt + // A wholly empty answer means the walk failed, not that the folders are empty. Remember + // the failure instead of caching it as a 0-byte answer, so refresh stops claiming a size + // without re-running the failing walk on every reload. + if (result.isEmpty()) { + failed = paths + } else { + sizedPaths = paths + sizes = result + } + requested = null + refresh() + } + } + } + + @RequiresEdt + private fun openDialog() { + val dialog = OrphanDialog(this, project, controller.orphans, sizes) + if (!dialog.showAndGet()) return + val selected = dialog.result() + if (selected.isNullOrEmpty()) return + remove(selected) + } + + /** + * Delete a confirmed selection. + * + * Split from [openDialog] so the delete path is the "do" half of an ask-then-do pair: it is driven + * by the dialog in production and directly by tests, which would otherwise have to show a modal. + */ + @RequiresEdt + internal fun remove(selected: List) { + // The apparent size of what was asked to be removed, from the same cache the dialog showed — + // not a promise of freed disk space (APFS clones/reflinks and block rounding can differ), just + // the number the user already saw and agreed to. Read before cancelling, which drops the cache. + val requestedSize = selected.sumOf { sizes[it] ?: 0L } + cancelSizes() + service().scope.launch { + val result = service().removeOrphans(controller.directory, selected) + edt { + notify(result.results, requestedSize) + // Brings the banner back through refresh, which starts a fresh pass for the leftovers + // that are still on disk — measured once, not once per deleted folder. + controller.reload() + } + } + } + + /** + * Drop the in-flight size pass and the cached answer it was for. + * + * Called when a delete starts: the walk is holding the very paths that are about to be renamed + * away, so it is measuring folders the user already decided to destroy, and any total it produced + * would describe a disk state that no longer exists. Clearing [sizedPaths] and [failed] is what + * makes the next [refresh] treat the remaining set as unmeasured and ask again; [sizes] is left + * alone so a dialog reopened before the new pass lands can still show last-known numbers. + */ + @RequiresEdt + private fun cancelSizes() { + job?.cancel() + job = null + requested = null + sizedPaths = emptySet() + failed = null + } + + private fun notify(results: List, requestedSize: Long) { + val ok = results.count { it.ok } + val total = results.size + val (type, title) = when { + ok == total -> NotificationType.INFORMATION to + KiloBundle.message("worktree.orphans.notification.success", ok, StringUtil.formatFileSize(requestedSize)) + ok > 0 -> NotificationType.WARNING to + KiloBundle.message("worktree.orphans.notification.partial", ok, total, total - ok) + else -> NotificationType.ERROR to KiloBundle.message("worktree.orphans.notification.failure") + } + val notification = NotificationGroupManager.getInstance().getNotificationGroup("Kilo Code") + ?.createNotification(title, "", type) + ?: Notification("Kilo Code", title, "", type) + notification.notify(project) + } + + private fun changed() { + parent?.revalidate() + parent?.repaint() + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/orphans/OrphanDialog.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/orphans/OrphanDialog.kt new file mode 100644 index 000000000000..f16f0117f8a5 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/orphans/OrphanDialog.kt @@ -0,0 +1,351 @@ +package ai.kilocode.client.agentManager.orphans + +import ai.kilocode.client.agentManager.worktree.KiloWorktreeService +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.rpc.dto.orphans.OrphanDto +import ai.kilocode.rpc.dto.orphans.OrphanKind +import com.intellij.icons.AllIcons +import com.intellij.ide.actions.RevealFileAction +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.EditorNotificationPanel +import com.intellij.ui.InlineBanner +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.labels.LinkLabel +import com.intellij.xml.util.XmlStringUtil +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.table.JBTable +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.awt.Component +import java.awt.Cursor +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.event.MouseMotionAdapter +import javax.swing.Icon +import javax.swing.JComponent +import javax.swing.JTable +import javax.swing.table.AbstractTableModel +import javax.swing.table.TableCellRenderer +import javax.swing.table.TableColumn + +private const val COLUMN_CHECK = 0 +private const val COLUMN_PATH = 1 +private const val COLUMN_SIZE = 2 +private const val COLUMN_CONTENTS = 3 +private const val COLUMN_REVEAL = 4 + +private const val WIDTH = 560 +private const val HEIGHT = 360 + +/** + * Resolve dialog for leftover worktree folders: select which to delete, see their full path and + * apparent size, open one in the host OS's file manager, and confirm a background delete. + * + * The dialog performs no deletion itself — it records [result] and closes; the caller ([OrphanBanner]) + * runs the removal in the background so the modal never blocks on it, matching + * [ai.kilocode.client.agentManager.worktree.NewWorktreeDialog]'s split between "what the user + * confirmed" and "what happens next". + */ +internal class OrphanDialog( + parent: Component, + private val project: Project, + orphans: List, + initialSizes: Map, +) : DialogWrapper(parent, false) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val model = OrphanTableModel( + orphans.map { OrphanRow(it, selected = it.kind == OrphanKind.LEFTOVER, size = initialSizes[it.path]) }, + ) + private val table = object : JBTable(model) { + /** + * Per-cell tooltips: the path column truncates long paths, and the reveal column is icon-only, + * so both need to say what they are. Registration with the tooltip manager comes from the + * `toolTipText` assignment below — overriding this alone would never be consulted. + */ + override fun getToolTipText(event: MouseEvent): String? { + val row = rowAtPoint(event.point) + if (row < 0) return null + return when (columnAtPoint(event.point)) { + // Qualified: JBTable has a `model` of its own, and it is the untyped TableModel. + COLUMN_PATH -> this@OrphanDialog.model.rows[row].orphan.path + COLUMN_REVEAL -> RevealFileAction.getActionName() + else -> null + } + } + }.apply { + rowHeight = JBUI.scale(22) + toolTipText = "" + // A `HEADER_ROW` model event makes JTable rebuild its columns from the model, which throws away + // every width set below (and clears the row selection). The select-all header indicator needs a + // header repaint on every toggle, so the model is deliberately not allowed to drive that. + autoCreateColumnsFromModel = false + columnModel.getColumn(COLUMN_CHECK).fix(JBCheckBox().preferredSize.width + UiStyle.Gap.xs()) + columnModel.getColumn(COLUMN_REVEAL).fix(AllIcons.Nodes.Folder.iconWidth + UiStyle.Gap.pad()) + columnModel.getColumn(COLUMN_CONTENTS).preferredWidth = JBUI.scale(160) + } + private val footer = JBLabel() + + private var confirmed = false + private var center: JComponent? = null + private var north: JComponent? = null + + // The three bullets are the collapsible detail; the intro sentence always stays visible so + // collapsing never hides the point of the dialog, only the specifics. `helpWidth` is fixed at + // banner creation (see createNorthPanel) and reused on every toggle so the HTML body keeps + // wrapping at the same width instead of the dialog's current, possibly user-resized, one. + private var helpExpanded = false + private var helpWidth = 0 + private lateinit var helpBanner: InlineBanner + private lateinit var helpToggle: LinkLabel + + init { + title = KiloBundle.message("worktree.orphans.dialogTitle") + setOKButtonText(deleteLabel()) + setCancelButtonText(KiloBundle.message("worktree.orphans.cancel")) + init() + model.addTableModelListener { + syncFooter() + table.tableHeader.repaint() + } + installSelectAllHeader() + installReveal() + syncFooter() + } + + /** + * Explains where these folders come from before offering to delete them: the list is the result of + * a heuristic over a directory Kilo owns, and "delete 47 folders" is not a decision a user can make + * from paths and sizes alone. + */ + override fun createNorthPanel(): JComponent { + // Standard info banner: rounded fill and border from JBUI.CurrentTheme.Banner.INFO_*, plus the + // platform information icon. Not dismissible — it is the explanation for the choice below it. + val banner = InlineBanner("", EditorNotificationPanel.Status.Info).showCloseButton(false) + // Bounded in CSS pixels, which are this component's own scaled pixels, so the text wraps with the + // table instead of stretching the dialog to one long line. Budget is the dialog content width less + // the banner's own padding (read from it rather than restated) and the strip its status icon takes. + helpWidth = JBUI.scale(WIDTH) - 2 * UiStyle.Gap.pad() - banner.insets.left - banner.insets.right - + EditorNotificationPanel.Status.Info.icon.iconWidth - UiStyle.Gap.lg() + helpBanner = banner + banner.setMessage(XmlStringUtil.wrapInHtml(helpBody())) + // Public overload only — createActionLabel/removeAllActions are @ApiStatus.Internal. The link + // lands in InlineBanner's action row, which InlineBannerBase's VerticalLayout places under the + // message, so it reads as "intro [+ bullets] / Show more|less" in both states. + helpToggle = banner.addAction(helpToggleLabel(), null) { toggleHelp() } + val panel = BorderLayoutPanel() + // The outer margin goes on a wrapper: the banner's own border is padding inside its rounded fill. + panel.addToCenter(banner) + panel.border = JBUI.Borders.empty(UiStyle.Gap.PAD, UiStyle.Gap.PAD, 0, UiStyle.Gap.PAD) + north = banner + return panel + } + + private fun helpBody(): String = buildString { + append("") + append(escape("worktree.orphans.help.intro")) + if (helpExpanded) { + append("
    ") + append("
  • ").append(escape("worktree.orphans.help.checkout")).append("
  • ") + append("
  • ").append(escape("worktree.orphans.help.causes")).append("
  • ") + append("
  • ").append(escape("worktree.orphans.help.delete")).append("
  • ") + append("
") + } + append("") + } + + private fun helpToggleLabel(): String = + KiloBundle.message(if (helpExpanded) "worktree.orphans.help.less" else "worktree.orphans.help.more") + + private fun toggleHelp() { + helpExpanded = !helpExpanded + helpBanner.setMessage(XmlStringUtil.wrapInHtml(helpBody())) + helpToggle.text = helpToggleLabel() + helpBanner.revalidate() + helpBanner.repaint() + // The banner grew or shrank; repack so the table keeps its own WIDTH x HEIGHT instead of being + // squeezed by (or leaving slack under) the new banner height. + pack() + } + + override fun createCenterPanel(): JComponent { + val panel = BorderLayoutPanel() + // The overlay scrollbar paints over the viewport instead of taking layout space, so without a + // right inset it covers the reveal column. A viewport border is what reserves that strip: Swing + // widens the column header by the same insets, so header and rows stay aligned. + panel.addToCenter(JBScrollPane(table).apply { viewportBorder = JBUI.Borders.emptyRight(UiStyle.Gap.XL) }) + panel.addToBottom(footer.apply { border = JBUI.Borders.empty(UiStyle.Gap.SM, 0, 0, 0) }) + panel.border = JBUI.Borders.empty(UiStyle.Gap.PAD) + panel.preferredSize = JBUI.size(WIDTH, HEIGHT) + center = panel + return panel + } + + private fun escape(key: String): String = XmlStringUtil.escapeString(KiloBundle.message(key)) + + /** The built content, so tests can drive the real Swing tree without showing the modal dialog. */ + internal fun centerComponent(): JComponent = center ?: error("center panel not built") + + /** The built explanation header. A headless [DialogWrapper] peer has no root pane to walk. */ + internal fun northComponent(): JComponent = north ?: error("north panel not built") + + /** The rendered "Delete N folders (X)" label, so tests can assert it without showing the dialog. */ + internal fun okButtonText(): String? = getOKAction().getValue(javax.swing.Action.NAME) as? String + + public override fun doOKAction() { + confirmed = true + close(OK_EXIT_CODE) + } + + override fun dispose() { + scope.cancel() + super.dispose() + } + + /** Paths the user chose to delete, or null when the dialog was cancelled or nothing was picked. */ + fun result(): List? { + if (!confirmed) return null + val paths = model.rows.filter { it.selected }.map { it.orphan.path } + return paths.ifEmpty { null } + } + + private fun installSelectAllHeader() { + table.tableHeader.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (table.columnAtPoint(e.point) != COLUMN_CHECK) return + val all = model.rows.all { it.selected } + model.rows.forEach { it.selected = !all } + model.fireTableDataChanged() + } + }) + table.tableHeader.defaultRenderer = HeaderRenderer( + model, + table.tableHeader.defaultRenderer, + table.getDefaultRenderer(Boolean::class.javaObjectType), + ) + } + + /** + * Makes the trailing icon column a button: click reveals that row's folder, and the pointer turns + * into a hand over it so it reads as clickable rather than as a decorative cell. + * + * The reveal itself is a backend RPC — in split mode the frontend runs on the client machine while + * the folder lives on the host, so it cannot be done here. + */ + private fun installReveal() { + table.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (table.columnAtPoint(e.point) != COLUMN_REVEAL) return + val row = table.rowAtPoint(e.point).takeIf { it >= 0 } ?: return + val path = model.rows[row].orphan.path + scope.launch { service().revealPath(path) } + } + }) + table.addMouseMotionListener(object : MouseMotionAdapter() { + override fun mouseMoved(e: MouseEvent) { + val over = table.columnAtPoint(e.point) == COLUMN_REVEAL && table.rowAtPoint(e.point) >= 0 + table.cursor = Cursor.getPredefinedCursor(if (over) Cursor.HAND_CURSOR else Cursor.DEFAULT_CURSOR) + } + }) + } + + private fun syncFooter() { + val selected = model.rows.filter { it.selected } + val size = selected.sumOf { it.size ?: 0L } + val checkouts = selected.count { it.orphan.kind == OrphanKind.BROKEN } + val base = KiloBundle.message("worktree.orphans.footerSelected", selected.size, StringUtil.formatFileSize(size)) + footer.text = if (checkouts > 0) { + "$base \u00b7 ${KiloBundle.message("worktree.orphans.footerCheckouts", checkouts)}" + } else { + base + } + setOKButtonText(deleteLabel()) + isOKActionEnabled = selected.isNotEmpty() + } + + private fun deleteLabel(): String { + val selected = model.rows.filter { it.selected } + val size = selected.sumOf { it.size ?: 0L } + return KiloBundle.message("worktree.orphans.deleteButton", selected.size, StringUtil.formatFileSize(size)) + } +} + +/** Pins a column to [width] so it neither resizes with the dialog nor absorbs slack from its neighbours. */ +private fun TableColumn.fix(width: Int) { + minWidth = width + maxWidth = width + preferredWidth = width + resizable = false +} + +private class OrphanRow(val orphan: OrphanDto, var selected: Boolean, var size: Long?) + +/** [AbstractTableModel] backing [OrphanDialog]'s table: a boolean selection column plus read-only ones. */ +private class OrphanTableModel(val rows: List) : AbstractTableModel() { + override fun getRowCount(): Int = rows.size + + override fun getColumnCount(): Int = 5 + + override fun getColumnName(column: Int): String = when (column) { + COLUMN_PATH -> KiloBundle.message("worktree.orphans.columnPath") + COLUMN_SIZE -> KiloBundle.message("worktree.orphans.columnSize") + COLUMN_CONTENTS -> KiloBundle.message("worktree.orphans.columnContents") + else -> "" + } + + override fun getColumnClass(column: Int): Class<*> = when (column) { + COLUMN_CHECK -> Boolean::class.javaObjectType + COLUMN_REVEAL -> Icon::class.java + else -> String::class.java + } + + override fun isCellEditable(row: Int, column: Int): Boolean = column == COLUMN_CHECK + + override fun getValueAt(row: Int, column: Int): Any = when (column) { + COLUMN_CHECK -> rows[row].selected + COLUMN_PATH -> rows[row].orphan.path + COLUMN_SIZE -> rows[row].size?.let { StringUtil.formatFileSize(it) } ?: KiloBundle.message("worktree.orphans.calculating") + COLUMN_CONTENTS -> if (rows[row].orphan.kind == OrphanKind.BROKEN) KiloBundle.message("worktree.orphans.checkoutWarning") else "" + COLUMN_REVEAL -> AllIcons.Nodes.Folder + else -> "" + } + + override fun setValueAt(value: Any?, row: Int, column: Int) { + if (column != COLUMN_CHECK || value !is Boolean) return + rows[row].selected = value + // Deliberately a cell update rather than a header event: the latter would make JTable rebuild + // its columns and drop the fixed widths. [OrphanDialog] repaints the header from its listener. + fireTableCellUpdated(row, column) + } +} + +/** Header renderer for [COLUMN_CHECK] that reads as a select-all box: checked once every row is selected. */ +private class HeaderRenderer( + private val model: OrphanTableModel, + private val fallback: TableCellRenderer, + private val check: TableCellRenderer, +) : TableCellRenderer { + override fun getTableCellRendererComponent( + table: JTable?, + value: Any?, + isSelected: Boolean, + hasFocus: Boolean, + row: Int, + column: Int, + ): Component { + if (column != COLUMN_CHECK || table == null) { + return fallback.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column) + } + val all = model.rows.isNotEmpty() && model.rows.all { it.selected } + return check.getTableCellRendererComponent(table, all, false, false, row, column) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/KiloWorktreeService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/KiloWorktreeService.kt index 526c68321b3a..694b34cff5a2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/KiloWorktreeService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/KiloWorktreeService.kt @@ -16,6 +16,8 @@ import ai.kilocode.rpc.dto.WorktreeDirtyListDto import ai.kilocode.rpc.dto.WorktreeListDto import ai.kilocode.rpc.dto.WorktreePrListDto import ai.kilocode.rpc.dto.WorktreeStatsListDto +import ai.kilocode.rpc.dto.orphans.OrphanRemoveResultDto +import ai.kilocode.rpc.dto.orphans.RemoveOrphansResultDto import com.intellij.openapi.components.Service import fleet.rpc.client.durable import kotlinx.coroutines.CancellationException @@ -186,4 +188,31 @@ class KiloWorktreeService internal constructor( LOG.warn("worktree session list state write failed for $directory", e) false } + + /** Apparent size of every orphan in [paths]. A failed lookup answers empty, never a partial throw. */ + suspend fun orphanSizes(directory: String, paths: List): Map = try { + call { orphanSizes(directory, paths) } + } catch (e: CancellationException) { + // This pass is cancelled whenever the orphan set changes or a delete starts. Rethrow so the + // caller's job actually ends: swallowing it would hand back an empty map that reads exactly + // like a walk that failed, which the banner remembers and stops retrying. + throw e + } catch (e: Exception) { + LOG.warn("worktree orphan sizes failed for $directory", e) + emptyMap() + } + + suspend fun removeOrphans(directory: String, paths: List): RemoveOrphansResultDto = try { + call { removeOrphans(directory, paths) } + } catch (e: Exception) { + LOG.warn("worktree orphan remove failed for $directory", e) + RemoveOrphansResultDto(paths.map { OrphanRemoveResultDto(it, ok = false, error = e.message) }) + } + + suspend fun revealPath(path: String): Boolean = try { + call { revealPath(path) } + } catch (e: Exception) { + LOG.warn("worktree reveal failed for $path", e) + false + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt index 05c5c6ef3b06..47b6b5e58357 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeController.kt @@ -8,6 +8,7 @@ import ai.kilocode.log.KiloLog import ai.kilocode.rpc.dto.CreateWorktreeRequestDto import ai.kilocode.rpc.dto.CreateWorktreeResultDto import ai.kilocode.rpc.dto.MoveStage +import ai.kilocode.rpc.dto.orphans.OrphanDto import ai.kilocode.rpc.dto.RemoveWorktreeResultDto import ai.kilocode.rpc.dto.SessionActivityDto import ai.kilocode.rpc.dto.WorktreeDto @@ -102,6 +103,16 @@ class WorktreeController( @Volatile private var known: Set = emptySet() + /** + * Directories under `.kilo/worktrees/` that git does not track, from the most recent [reload]. + * [ai.kilocode.client.agentManager.orphans.OrphanBanner] reads this on the EDT after [onReload] + * fires — never polled independently, so the banner and the worktree list always agree on which + * paths are orphans. + */ + @Volatile + var orphans: List = emptyList() + private set + /** `owner/repo` for the checkout's origin remote; null when there is no GitHub origin. */ @Volatile var origin: String? = null @@ -130,6 +141,7 @@ class WorktreeController( branches = branchInfo.branches.filter { it !in worktreeBranches } known = branchInfo.branches.toMutableSet().apply { addAll(rows.map { it.branch }) } origin = branchInfo.origin + orphans = result.orphans onReload?.invoke() telemetry("Worktree List Loaded", mapOf("count" to extra.size.toString())) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeDiagnosticsAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeDiagnosticsAction.kt index e5cd305ed8f8..592d86a7cd5f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeDiagnosticsAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeDiagnosticsAction.kt @@ -51,7 +51,7 @@ internal object WorktreeDiagnosticsAction { root = root, gh = service.ghStatus(root), worktrees = listed.worktrees, - orphans = listed.orphans, + orphans = listed.orphans.map { it.path }, stats = service.stats(root).items.associateBy { normalizeWorktreePath(it.path) }, dirty = service.dirty(root).items.associateBy { normalizeWorktreePath(it.path) }, ), diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index d9fe28e9bfbe..63b37b8b343d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -535,6 +535,28 @@ worktree.gh.authorize=Authorize worktree.gh.learnMore=Learn more worktree.gh.disable=Turn off GitHub integration worktree.gh.disable.tooltip=Kilo stops running gh. You can turn this back on in Settings | Tools | Kilo Code | Integrations. +worktree.orphans.resolve=Resolve\u2026 +worktree.orphans.dialogTitle=Leftover worktree folders +worktree.orphans.summary={0} leftover worktree folder(s) +worktree.orphans.summarySize={0} leftover worktree folder(s) \u00b7 {1} +worktree.orphans.calculating=calculating size\u2026 +worktree.orphans.columnPath=Path +worktree.orphans.columnSize=Size +worktree.orphans.columnContents=Contents +worktree.orphans.checkoutWarning=Contains a git checkout +worktree.orphans.footerSelected={0} selected \u00b7 {1} +worktree.orphans.footerCheckouts={0} still contain a git checkout +worktree.orphans.deleteButton=Delete {0} folders ({1}) +worktree.orphans.cancel=Cancel +worktree.orphans.notification.success=Removed {0} folders ({1}) +worktree.orphans.notification.partial=Removed {0} of {1}, {2} failed +worktree.orphans.notification.failure=Failed to remove leftover folders +worktree.orphans.help.intro=Kilo keeps every worktree it creates inside the .kilo/worktrees folder of this repository. The folders below are in that folder, but git does not list any of them as a worktree, so nothing is using them anymore. +worktree.orphans.help.checkout=A folder marked "Contains a git checkout" still has a .git entry inside and may hold uncommitted work. Those are left unselected, so open one and check it before you delete it. +worktree.orphans.help.causes=Leftovers usually come from a deletion that was interrupted, a worktree removed outside Kilo, or a tool that wrote into the folder after it was removed. Deletions that are still running are not listed here. +worktree.orphans.help.delete=Deleting removes the selected folders from disk for good, without going through the Trash. No branch and no active worktree is touched. Sizes are what each folder takes up on disk right now. +worktree.orphans.help.more=Show more +worktree.orphans.help.less=Show less action.Kilo.Settings.text=Settings action.Kilo.Settings.description=Kilo Code settings diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index 58941259c5ea..869464a42ada 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=فتح في إطار جديد\u2026 worktree.run.failed=فشل تشغيل ''{0}'': {1} worktree.run.hint=للحصول على دعم كامل للتشغيل والتصحيح، افتح شجرة العمل هذه في إطار جديد worktree.menu.configure=تكوين Worktree جديد... +worktree.orphans.resolve=حل… +worktree.orphans.dialogTitle=مجلدات شجرة العمل المتبقية +worktree.orphans.summary={0} مجلد(ات) شجرة عمل متبقية +worktree.orphans.summarySize={0} مجلد(ات) شجرة عمل متبقية · {1} +worktree.orphans.calculating=جارٍ حساب الحجم… +worktree.orphans.columnPath=المسار +worktree.orphans.columnSize=الحجم +worktree.orphans.columnContents=المحتوى +worktree.orphans.checkoutWarning=يحتوي على نسخة git +worktree.orphans.footerSelected={0} محدد · {1} +worktree.orphans.footerCheckouts={0} لا تزال تحتوي على نسخة git +worktree.orphans.deleteButton=حذف {0} مجلدات ({1}) +worktree.orphans.cancel=إلغاء +worktree.orphans.notification.success=تم حذف {0} مجلدات ({1}) +worktree.orphans.notification.partial=تم حذف {0} من {1}، فشل {2} +worktree.orphans.notification.failure=فشل حذف المجلدات المتبقية +worktree.orphans.help.intro=يحفظ Kilo كل شجرة عمل ينشئها داخل مجلد .kilo/worktrees في هذا المستودع. المجلدات أدناه موجودة في ذلك المجلد، لكن git لا يعرض أياً منها كشجرة عمل، لذا لم يعد أي شيء يستخدمها. +worktree.orphans.help.checkout=المجلد المُعلَّم بـ "يحتوي على نسخة git" لا يزال يحتوي على مدخل .git وقد يحتوي على عمل غير مُودَع. تبقى هذه المجلدات غير محددة، لذا افتح المجلد وتحقق منه قبل حذفه. +worktree.orphans.help.causes=تنشأ المتبقيات عادةً من عملية حذف تمت مقاطعتها، أو من شجرة عمل أُزيلت خارج Kilo، أو من أداة كتبت في المجلد بعد إزالته. عمليات الحذف الجارية حالياً غير مدرجة هنا. +worktree.orphans.help.delete=يؤدي الحذف إلى إزالة المجلدات المحددة من القرص نهائياً، دون المرور بسلة المهملات. لا يتم المساس بأي فرع أو شجرة عمل نشطة. الأحجام هي ما يشغله كل مجلد على القرص حالياً. +worktree.orphans.help.more=إظهار المزيد +worktree.orphans.help.less=إظهار أقل session.part.tool.openSubagent=فتح الوكيل الفرعي في المحرر session.subagent.title=جلسة الوكيل الفرعي session.subagent.path=Kilo / الوكلاء الفرعيون / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index d5078663969d..3fb600e5f367 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Otvori u novom okviru\u2026 worktree.run.failed=Nije uspjelo pokretanje ''{0}'': {1} worktree.run.hint=Za potpunu podršku za pokretanje i debugovanje otvorite ovaj worktree u novom okviru worktree.menu.configure=Konfiguriši novi worktree... +worktree.orphans.resolve=Riješi… +worktree.orphans.dialogTitle=Preostali direktoriji radnih stabala +worktree.orphans.summary={0} preostalih direktorija radnog stabla +worktree.orphans.summarySize={0} preostalih direktorija radnog stabla · {1} +worktree.orphans.calculating=računanje veličine… +worktree.orphans.columnPath=Putanja +worktree.orphans.columnSize=Veličina +worktree.orphans.columnContents=Sadržaj +worktree.orphans.checkoutWarning=Sadrži git checkout +worktree.orphans.footerSelected={0} odabrano · {1} +worktree.orphans.footerCheckouts={0} još sadrže git checkout +worktree.orphans.deleteButton=Obriši {0} direktorija ({1}) +worktree.orphans.cancel=Otkaži +worktree.orphans.notification.success=Uklonjeno {0} direktorija ({1}) +worktree.orphans.notification.partial=Uklonjeno {0} od {1}, {2} neuspješno +worktree.orphans.notification.failure=Uklanjanje preostalih direktorija nije uspjelo +worktree.orphans.help.intro=Kilo čuva svaki worktree koji napravi u folderu .kilo/worktrees ovog repozitorija. Folderi ispod nalaze se u tom folderu, ali git nijedan od njih ne prikazuje kao worktree, pa ih više ništa ne koristi. +worktree.orphans.help.checkout=Folder označen sa "Sadrži git checkout" još ima .git unos u sebi i može sadržavati nekomitovani rad. Takvi folderi ostaju neoznačeni, pa otvorite folder i provjerite ga prije brisanja. +worktree.orphans.help.causes=Ostaci obično nastaju od brisanja koje je prekinuto, worktreeja uklonjenog izvan Kila ili alata koji je pisao u folder nakon što je uklonjen. Brisanja koja su još u toku nisu prikazana ovdje. +worktree.orphans.help.delete=Brisanje trajno uklanja označene foldere s diska, bez korpe za smeće. Nijedna grana ni aktivni worktree se ne dira. Veličine su ono što svaki folder trenutno zauzima na disku. +worktree.orphans.help.more=Prikaži još +worktree.orphans.help.less=Prikaži manje session.part.tool.openSubagent=Otvori pod-agenta u uređivaču session.subagent.title=Sesija pod-agenta session.subagent.path=Kilo / Pod-agenti / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index f64c75896cdf..b4749156dc13 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Åbn i nyt vindue\u2026 worktree.run.failed=Kunne ikke køre ''{0}'': {1} worktree.run.hint=For fuld kørsel- og debug-understøttelse skal du åbne dette worktree i et nyt vindue worktree.menu.configure=Konfigurer nyt worktree... +worktree.orphans.resolve=Løs… +worktree.orphans.dialogTitle=Efterladte worktree-mapper +worktree.orphans.summary={0} efterladte worktree-mapper +worktree.orphans.summarySize={0} efterladte worktree-mapper · {1} +worktree.orphans.calculating=beregner størrelse… +worktree.orphans.columnPath=Sti +worktree.orphans.columnSize=Størrelse +worktree.orphans.columnContents=Indhold +worktree.orphans.checkoutWarning=Indeholder et git-checkout +worktree.orphans.footerSelected={0} valgt · {1} +worktree.orphans.footerCheckouts={0} indeholder stadig et git-checkout +worktree.orphans.deleteButton=Slet {0} mapper ({1}) +worktree.orphans.cancel=Annuller +worktree.orphans.notification.success=Fjernede {0} mapper ({1}) +worktree.orphans.notification.partial=Fjernede {0} af {1}, {2} mislykkedes +worktree.orphans.notification.failure=Kunne ikke fjerne efterladte mapper +worktree.orphans.help.intro=Kilo gemmer alle worktrees, den opretter, i mappen .kilo/worktrees i dette repository. Mapperne nedenfor ligger i den mappe, men git viser ingen af dem som et worktree, så der er ikke længere noget, der bruger dem. +worktree.orphans.help.checkout=En mappe markeret med "Indeholder et git-checkout" har stadig en .git-post indeni og kan indeholde ikke-committet arbejde. De er ikke valgt, så åbn mappen og kontrollér den, før du sletter den. +worktree.orphans.help.causes=Rester kommer typisk fra en sletning, der blev afbrudt, et worktree fjernet uden om Kilo eller et værktøj, der skrev i mappen efter den blev fjernet. Sletninger, der stadig kører, vises ikke her. +worktree.orphans.help.delete=Sletning fjerner de valgte mapper permanent fra disken uden om Papirkurven. Ingen branch og intet aktivt worktree berøres. Størrelserne er det, hver mappe fylder på disken lige nu. +worktree.orphans.help.more=Vis mere +worktree.orphans.help.less=Vis mindre session.part.tool.openSubagent=Åbn underagent i editor session.subagent.title=Underagent-session session.subagent.path=Kilo / Underagenter / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index db442609634a..10cb9a69f8fa 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=In neuem Fenster öffnen\u2026 worktree.run.failed=''{0}'' konnte nicht ausgeführt werden: {1} worktree.run.hint=Für vollständige Ausführungs- und Debug-Unterstützung öffne diesen Worktree in einem neuen Fenster worktree.menu.configure=Neuen Worktree konfigurieren... +worktree.orphans.resolve=Auflösen… +worktree.orphans.dialogTitle=Übrig gebliebene Worktree-Ordner +worktree.orphans.summary={0} übrig gebliebene(r) Worktree-Ordner +worktree.orphans.summarySize={0} übrig gebliebene(r) Worktree-Ordner · {1} +worktree.orphans.calculating=Größe wird berechnet… +worktree.orphans.columnPath=Pfad +worktree.orphans.columnSize=Größe +worktree.orphans.columnContents=Inhalt +worktree.orphans.checkoutWarning=Enthält einen Git-Checkout +worktree.orphans.footerSelected={0} ausgewählt · {1} +worktree.orphans.footerCheckouts={0} enthalten noch einen Git-Checkout +worktree.orphans.deleteButton={0} Ordner löschen ({1}) +worktree.orphans.cancel=Abbrechen +worktree.orphans.notification.success={0} Ordner entfernt ({1}) +worktree.orphans.notification.partial={0} von {1} entfernt, {2} fehlgeschlagen +worktree.orphans.notification.failure=Entfernen der übrig gebliebenen Ordner fehlgeschlagen +worktree.orphans.help.intro=Kilo legt jeden Worktree, den es erstellt, im Ordner .kilo/worktrees dieses Repositorys ab. Die Ordner unten liegen in diesem Ordner, aber git führt keinen davon als Worktree, sie werden also nicht mehr verwendet. +worktree.orphans.help.checkout=Ein Ordner mit der Markierung "Enthält ein git-Checkout" hat noch einen .git-Eintrag und kann nicht committete Arbeit enthalten. Diese bleiben nicht ausgewählt, öffnen Sie den Ordner also und prüfen Sie ihn vor dem Löschen. +worktree.orphans.help.causes=Übrig gebliebene Ordner entstehen meist durch einen abgebrochenen Löschvorgang, einen außerhalb von Kilo entfernten Worktree oder ein Tool, das nach dem Entfernen in den Ordner geschrieben hat. Noch laufende Löschvorgänge werden hier nicht aufgeführt. +worktree.orphans.help.delete=Beim Löschen werden die ausgewählten Ordner endgültig von der Festplatte entfernt, ohne den Papierkorb. Kein Branch und kein aktiver Worktree wird angetastet. Die Größen entsprechen dem aktuellen Speicherbedarf jedes Ordners. +worktree.orphans.help.more=Mehr anzeigen +worktree.orphans.help.less=Weniger anzeigen session.part.tool.openSubagent=Unter-Agent im Editor öffnen session.subagent.title=Unter-Agent-Sitzung session.subagent.path=Kilo / Unter-Agenten / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index 8b6c70aaded0..2750abbac9fb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Abrir en una ventana nueva\u2026 worktree.run.failed=No se pudo ejecutar ''{0}'': {1} worktree.run.hint=Para soporte completo de ejecución y depuración, abre este worktree en una ventana nueva worktree.menu.configure=Configurar nuevo worktree... +worktree.orphans.resolve=Resolver… +worktree.orphans.dialogTitle=Carpetas de worktree sobrantes +worktree.orphans.summary={0} carpeta(s) de worktree sobrante(s) +worktree.orphans.summarySize={0} carpeta(s) de worktree sobrante(s) · {1} +worktree.orphans.calculating=calculando tamaño… +worktree.orphans.columnPath=Ruta +worktree.orphans.columnSize=Tamaño +worktree.orphans.columnContents=Contenido +worktree.orphans.checkoutWarning=Contiene un checkout de git +worktree.orphans.footerSelected={0} seleccionada(s) · {1} +worktree.orphans.footerCheckouts={0} todavía contienen un checkout de git +worktree.orphans.deleteButton=Eliminar {0} carpetas ({1}) +worktree.orphans.cancel=Cancelar +worktree.orphans.notification.success=Se eliminaron {0} carpetas ({1}) +worktree.orphans.notification.partial=Se eliminaron {0} de {1}, {2} fallaron +worktree.orphans.notification.failure=No se pudieron eliminar las carpetas sobrantes +worktree.orphans.help.intro=Kilo guarda todos los worktrees que crea en la carpeta .kilo/worktrees de este repositorio. Las carpetas de abajo están en esa carpeta, pero git no muestra ninguna como worktree, así que ya nada las usa. +worktree.orphans.help.checkout=Una carpeta marcada con "Contiene un checkout de git" todavía tiene una entrada .git dentro y puede contener trabajo sin confirmar. Esas quedan sin seleccionar, así que ábrela y revísala antes de eliminarla. +worktree.orphans.help.causes=Los restos suelen venir de una eliminación interrumpida, de un worktree eliminado fuera de Kilo o de una herramienta que escribió en la carpeta después de eliminarla. Las eliminaciones en curso no aparecen aquí. +worktree.orphans.help.delete=Eliminar borra las carpetas seleccionadas del disco de forma definitiva, sin pasar por la papelera. No se toca ninguna rama ni worktree activo. Los tamaños son lo que ocupa cada carpeta en el disco ahora mismo. +worktree.orphans.help.more=Mostrar más +worktree.orphans.help.less=Mostrar menos session.part.tool.openSubagent=Abrir subagente en el editor session.subagent.title=Sesión de subagente session.subagent.path=Kilo / Subagentes / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index a33c47269e3f..7e0db2bc434a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Ouvrir dans une nouvelle fenêtre\u2026 worktree.run.failed=Échec de l’exécution de ''{0}'': {1} worktree.run.hint=Pour une prise en charge complète de l’exécution et du débogage, ouvrez ce worktree dans une nouvelle fenêtre worktree.menu.configure=Configurer un nouveau worktree... +worktree.orphans.resolve=Résoudre… +worktree.orphans.dialogTitle=Dossiers de worktree restants +worktree.orphans.summary={0} dossier(s) de worktree restant(s) +worktree.orphans.summarySize={0} dossier(s) de worktree restant(s) · {1} +worktree.orphans.calculating=calcul de la taille… +worktree.orphans.columnPath=Chemin +worktree.orphans.columnSize=Taille +worktree.orphans.columnContents=Contenu +worktree.orphans.checkoutWarning=Contient un checkout git +worktree.orphans.footerSelected={0} sélectionné(s) · {1} +worktree.orphans.footerCheckouts={0} contiennent encore un checkout git +worktree.orphans.deleteButton=Supprimer {0} dossiers ({1}) +worktree.orphans.cancel=Annuler +worktree.orphans.notification.success={0} dossiers supprimés ({1}) +worktree.orphans.notification.partial={0} sur {1} supprimés, {2} échoué(s) +worktree.orphans.notification.failure=Échec de la suppression des dossiers restants +worktree.orphans.help.intro=Kilo place chaque worktree qu'il crée dans le dossier .kilo/worktrees de ce dépôt. Les dossiers ci-dessous se trouvent dans ce dossier, mais git n'en répertorie aucun comme worktree : plus rien ne les utilise. +worktree.orphans.help.checkout=Un dossier marqué "Contient un checkout git" contient encore une entrée .git et peut renfermer du travail non commité. Ceux-là restent décochés : ouvrez le dossier et vérifiez-le avant de le supprimer. +worktree.orphans.help.causes=Les résidus proviennent généralement d'une suppression interrompue, d'un worktree supprimé en dehors de Kilo ou d'un outil qui a écrit dans le dossier après sa suppression. Les suppressions encore en cours ne sont pas listées ici. +worktree.orphans.help.delete=La suppression retire définitivement les dossiers sélectionnés du disque, sans passer par la corbeille. Aucune branche ni worktree actif n'est touché. Les tailles correspondent à l'espace occupé actuellement par chaque dossier. +worktree.orphans.help.more=Afficher plus +worktree.orphans.help.less=Afficher moins session.part.tool.openSubagent=Ouvrir le sous-agent dans l'éditeur session.subagent.title=Session du sous-agent session.subagent.path=Kilo / Sous-agents / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index fee3ddba8597..70d939656e82 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=新しいフレームで開く\u2026 worktree.run.failed=''{0}'' の実行に失敗しました: {1} worktree.run.hint=実行とデバッグを完全にサポートするには、このワークツリーを新しいフレームで開いてください worktree.menu.configure=新しいワークツリーを設定... +worktree.orphans.resolve=解決… +worktree.orphans.dialogTitle=残された worktree フォルダ +worktree.orphans.summary={0} 件の残された worktree フォルダ +worktree.orphans.summarySize={0} 件の残された worktree フォルダ · {1} +worktree.orphans.calculating=サイズを計算中… +worktree.orphans.columnPath=パス +worktree.orphans.columnSize=サイズ +worktree.orphans.columnContents=内容 +worktree.orphans.checkoutWarning=git チェックアウトが含まれています +worktree.orphans.footerSelected={0} 件選択 · {1} +worktree.orphans.footerCheckouts={0} 件はまだ git チェックアウトを含んでいます +worktree.orphans.deleteButton={0} 件のフォルダを削除 ({1}) +worktree.orphans.cancel=キャンセル +worktree.orphans.notification.success={0} 件のフォルダを削除しました ({1}) +worktree.orphans.notification.partial={1} 件中 {0} 件を削除、{2} 件失敗 +worktree.orphans.notification.failure=残されたフォルダの削除に失敗しました +worktree.orphans.help.intro=Kilo は作成したワークツリーをすべて、このリポジトリの .kilo/worktrees フォルダ内に保持します。以下のフォルダはそのフォルダ内にありますが、git はどれもワークツリーとして認識していないため、現在は使われていません。 +worktree.orphans.help.checkout=「git チェックアウトが含まれています」と表示されたフォルダには .git エントリが残っており、コミットされていない作業が含まれている可能性があります。これらは未選択のままにしてあるので、削除する前にフォルダを開いて確認してください。 +worktree.orphans.help.causes=残されたフォルダは通常、中断された削除、Kilo 以外で削除されたワークツリー、または削除後にフォルダへ書き込んだツールが原因で生じます。実行中の削除はここには表示されません。 +worktree.orphans.help.delete=削除すると、選択したフォルダはゴミ箱を経由せずディスクから完全に削除されます。ブランチや使用中のワークツリーには影響しません。サイズは各フォルダが現在ディスク上で占めている容量です。 +worktree.orphans.help.more=さらに表示 +worktree.orphans.help.less=表示を減らす session.part.tool.openSubagent=サブエージェントをエディタで開く session.subagent.title=サブエージェントセッション session.subagent.path=Kilo / サブエージェント / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index e24cfef5d18f..2cc10d6973f2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=새 프레임에서 열기\u2026 worktree.run.failed=''{0}'' 실행 실패: {1} worktree.run.hint=전체 실행 및 디버그 지원을 사용하려면 이 worktree를 새 프레임에서 여세요 worktree.menu.configure=새 워크트리 구성... +worktree.orphans.resolve=해결… +worktree.orphans.dialogTitle=남은 worktree 폴더 +worktree.orphans.summary={0}개의 남은 worktree 폴더 +worktree.orphans.summarySize={0}개의 남은 worktree 폴더 · {1} +worktree.orphans.calculating=크기 계산 중… +worktree.orphans.columnPath=경로 +worktree.orphans.columnSize=크기 +worktree.orphans.columnContents=내용 +worktree.orphans.checkoutWarning=git 체크아웃이 포함되어 있습니다 +worktree.orphans.footerSelected={0}개 선택됨 · {1} +worktree.orphans.footerCheckouts={0}개에 아직 git 체크아웃이 남아 있습니다 +worktree.orphans.deleteButton={0}개 폴더 삭제 ({1}) +worktree.orphans.cancel=취소 +worktree.orphans.notification.success={0}개 폴더 제거됨 ({1}) +worktree.orphans.notification.partial={1}개 중 {0}개 제거됨, {2}개 실패 +worktree.orphans.notification.failure=남은 폴더 제거 실패 +worktree.orphans.help.intro=Kilo는 생성한 모든 워크트리를 이 저장소의 .kilo/worktrees 폴더 안에 보관합니다. 아래 폴더들은 그 폴더에 있지만 git이 어느 것도 워크트리로 표시하지 않으므로 더 이상 사용되지 않습니다. +worktree.orphans.help.checkout="git 체크아웃이 포함되어 있습니다"로 표시된 폴더에는 아직 .git 항목이 있으며 커밋되지 않은 작업이 남아 있을 수 있습니다. 이런 폴더는 선택되지 않은 상태로 두었으니 삭제하기 전에 열어서 확인하세요. +worktree.orphans.help.causes=남은 폴더는 보통 중단된 삭제, Kilo 외부에서 제거된 워크트리, 또는 제거된 뒤 폴더에 기록한 도구 때문에 생깁니다. 진행 중인 삭제는 여기에 표시되지 않습니다. +worktree.orphans.help.delete=삭제하면 선택한 폴더가 휴지통을 거치지 않고 디스크에서 영구적으로 제거됩니다. 브랜치나 사용 중인 워크트리는 영향을 받지 않습니다. 크기는 각 폴더가 현재 디스크에서 차지하는 용량입니다. +worktree.orphans.help.more=더보기 +worktree.orphans.help.less=간략히 보기 session.part.tool.openSubagent=하위 에이전트를 편집기에서 열기 session.subagent.title=하위 에이전트 세션 session.subagent.path=Kilo / 하위 에이전트 / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index a7ab5a264126..c2992f0160cd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Openen in nieuw venster\u2026 worktree.run.failed=Kan ''{0}'' niet uitvoeren: {1} worktree.run.hint=Open deze worktree in een nieuw venster voor volledige run- en debugondersteuning worktree.menu.configure=Nieuwe worktree configureren... +worktree.orphans.resolve=Oplossen… +worktree.orphans.dialogTitle=Overgebleven worktree-mappen +worktree.orphans.summary={0} overgebleven worktree-map(pen) +worktree.orphans.summarySize={0} overgebleven worktree-map(pen) · {1} +worktree.orphans.calculating=grootte berekenen… +worktree.orphans.columnPath=Pad +worktree.orphans.columnSize=Grootte +worktree.orphans.columnContents=Inhoud +worktree.orphans.checkoutWarning=Bevat een git-checkout +worktree.orphans.footerSelected={0} geselecteerd · {1} +worktree.orphans.footerCheckouts={0} bevatten nog een git-checkout +worktree.orphans.deleteButton={0} mappen verwijderen ({1}) +worktree.orphans.cancel=Annuleren +worktree.orphans.notification.success={0} mappen verwijderd ({1}) +worktree.orphans.notification.partial={0} van {1} verwijderd, {2} mislukt +worktree.orphans.notification.failure=Verwijderen van overgebleven mappen mislukt +worktree.orphans.help.intro=Kilo bewaart elke worktree die het aanmaakt in de map .kilo/worktrees van deze repository. De mappen hieronder staan in die map, maar git vermeldt er geen enkele als worktree, dus niets gebruikt ze nog. +worktree.orphans.help.checkout=Een map met de markering "Bevat een git-checkout" heeft nog een .git-item en kan niet-gecommit werk bevatten. Die blijven niet aangevinkt, dus open de map en controleer hem voordat je hem verwijdert. +worktree.orphans.help.causes=Restanten komen meestal van een verwijdering die is afgebroken, een worktree die buiten Kilo is verwijderd, of een tool die na het verwijderen in de map heeft geschreven. Verwijderingen die nog lopen staan hier niet in. +worktree.orphans.help.delete=Verwijderen haalt de geselecteerde mappen definitief van de schijf, zonder de prullenbak. Geen enkele branch of actieve worktree wordt aangeraakt. De groottes zijn wat elke map nu op de schijf inneemt. +worktree.orphans.help.more=Meer weergeven +worktree.orphans.help.less=Minder weergeven session.part.tool.openSubagent=Subagent openen in editor session.subagent.title=Subagent-sessie session.subagent.path=Kilo / Subagents / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index e9c3683f18de..707b3c862a9d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Åpne i nytt vindu\u2026 worktree.run.failed=Kunne ikke kjøre ''{0}'': {1} worktree.run.hint=For full støtte for kjøring og feilsøking, åpne dette worktreeet i et nytt vindu worktree.menu.configure=Konfigurer nytt worktree... +worktree.orphans.resolve=Løs… +worktree.orphans.dialogTitle=Gjenværende worktree-mapper +worktree.orphans.summary={0} gjenværende worktree-mappe(r) +worktree.orphans.summarySize={0} gjenværende worktree-mappe(r) · {1} +worktree.orphans.calculating=beregner størrelse… +worktree.orphans.columnPath=Sti +worktree.orphans.columnSize=Størrelse +worktree.orphans.columnContents=Innhold +worktree.orphans.checkoutWarning=Inneholder en git-checkout +worktree.orphans.footerSelected={0} valgt · {1} +worktree.orphans.footerCheckouts={0} inneholder fremdeles en git-checkout +worktree.orphans.deleteButton=Slett {0} mapper ({1}) +worktree.orphans.cancel=Avbryt +worktree.orphans.notification.success=Fjernet {0} mapper ({1}) +worktree.orphans.notification.partial=Fjernet {0} av {1}, {2} mislyktes +worktree.orphans.notification.failure=Kunne ikke fjerne gjenværende mapper +worktree.orphans.help.intro=Kilo lagrer alle worktrees den oppretter i mappen .kilo/worktrees i dette repositoryet. Mappene nedenfor ligger i den mappen, men git viser ingen av dem som en worktree, så ingenting bruker dem lenger. +worktree.orphans.help.checkout=En mappe merket med "Inneholder en git-checkout" har fortsatt en .git-oppføring inni og kan inneholde arbeid som ikke er committet. Disse er ikke valgt, så åpne mappen og sjekk den før du sletter den. +worktree.orphans.help.causes=Rester kommer vanligvis fra en sletting som ble avbrutt, en worktree som ble fjernet utenfor Kilo, eller et verktøy som skrev til mappen etter at den ble fjernet. Slettinger som fortsatt kjører, vises ikke her. +worktree.orphans.help.delete=Sletting fjerner de valgte mappene permanent fra disken, uten å gå via papirkurven. Ingen branch og ingen aktiv worktree berøres. Størrelsene er det hver mappe bruker på disken nå. +worktree.orphans.help.more=Vis mer +worktree.orphans.help.less=Vis færre session.part.tool.openSubagent=Åpne underagent i editor session.subagent.title=Underagent-økt session.subagent.path=Kilo / Underagenter / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index caa62296093d..5ffedb25ae0c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Otwórz w nowym oknie\u2026 worktree.run.failed=Nie udało się uruchomić ''{0}'': {1} worktree.run.hint=Aby uzyskać pełną obsługę uruchamiania i debugowania, otwórz ten worktree w nowym oknie worktree.menu.configure=Skonfiguruj nowy worktree... +worktree.orphans.resolve=Rozwiąż… +worktree.orphans.dialogTitle=Pozostałe foldery worktree +worktree.orphans.summary={0} pozostałych folderów worktree +worktree.orphans.summarySize={0} pozostałych folderów worktree · {1} +worktree.orphans.calculating=obliczanie rozmiaru… +worktree.orphans.columnPath=Ścieżka +worktree.orphans.columnSize=Rozmiar +worktree.orphans.columnContents=Zawartość +worktree.orphans.checkoutWarning=Zawiera checkout git +worktree.orphans.footerSelected=Wybrano {0} · {1} +worktree.orphans.footerCheckouts={0} wciąż zawiera checkout git +worktree.orphans.deleteButton=Usuń {0} folderów ({1}) +worktree.orphans.cancel=Anuluj +worktree.orphans.notification.success=Usunięto {0} folderów ({1}) +worktree.orphans.notification.partial=Usunięto {0} z {1}, {2} nieudanych +worktree.orphans.notification.failure=Nie udało się usunąć pozostałych folderów +worktree.orphans.help.intro=Kilo przechowuje każde tworzone worktree w folderze .kilo/worktrees tego repozytorium. Foldery poniżej znajdują się w tym folderze, ale git nie wykazuje żadnego z nich jako worktree, więc nic już ich nie używa. +worktree.orphans.help.checkout=Folder oznaczony jako "Zawiera checkout gita" nadal ma w środku wpis .git i może zawierać niezatwierdzone zmiany. Takie foldery pozostają niezaznaczone, więc otwórz folder i sprawdź go przed usunięciem. +worktree.orphans.help.causes=Pozostałości zwykle wynikają z przerwanego usuwania, worktree usuniętego poza Kilo albo narzędzia, które zapisało dane do folderu po jego usunięciu. Usuwania nadal trwające nie są tu wymienione. +worktree.orphans.help.delete=Usunięcie trwale kasuje wybrane foldery z dysku, bez kosza. Żadna gałąź ani aktywne worktree nie zostaną zmienione. Rozmiary to miejsce, jakie każdy folder zajmuje obecnie na dysku. +worktree.orphans.help.more=Pokaż więcej +worktree.orphans.help.less=Pokaż mniej session.part.tool.openSubagent=Otwórz podagenta w edytorze session.subagent.title=Sesja podagenta session.subagent.path=Kilo / Podagenci / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 756ea586a705..f87246e5e2ca 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Abrir em nova janela\u2026 worktree.run.failed=Falha ao executar ''{0}'': {1} worktree.run.hint=Para suporte completo a execução e depuração, abra esta worktree em uma nova janela worktree.menu.configure=Configurar novo worktree... +worktree.orphans.resolve=Resolver… +worktree.orphans.dialogTitle=Pastas de worktree remanescentes +worktree.orphans.summary={0} pasta(s) de worktree remanescente(s) +worktree.orphans.summarySize={0} pasta(s) de worktree remanescente(s) · {1} +worktree.orphans.calculating=calculando tamanho… +worktree.orphans.columnPath=Caminho +worktree.orphans.columnSize=Tamanho +worktree.orphans.columnContents=Conteúdo +worktree.orphans.checkoutWarning=Contém um checkout do git +worktree.orphans.footerSelected={0} selecionada(s) · {1} +worktree.orphans.footerCheckouts={0} ainda contêm um checkout do git +worktree.orphans.deleteButton=Excluir {0} pastas ({1}) +worktree.orphans.cancel=Cancelar +worktree.orphans.notification.success={0} pastas removidas ({1}) +worktree.orphans.notification.partial={0} de {1} removidas, {2} falharam +worktree.orphans.notification.failure=Falha ao remover pastas remanescentes +worktree.orphans.help.intro=O Kilo mantém todas as worktrees que cria dentro da pasta .kilo/worktrees deste repositório. As pastas abaixo estão nessa pasta, mas o git não lista nenhuma delas como worktree, então nada mais as usa. +worktree.orphans.help.checkout=Uma pasta marcada com "Contém um checkout do git" ainda tem uma entrada .git dentro e pode conter trabalho não commitado. Essas ficam sem seleção, então abra a pasta e verifique antes de excluir. +worktree.orphans.help.causes=As sobras costumam vir de uma exclusão interrompida, de uma worktree removida fora do Kilo ou de uma ferramenta que escreveu na pasta depois que ela foi removida. Exclusões ainda em andamento não aparecem aqui. +worktree.orphans.help.delete=Excluir remove as pastas selecionadas do disco definitivamente, sem passar pela Lixeira. Nenhum branch e nenhuma worktree ativa é afetada. Os tamanhos são o que cada pasta ocupa no disco agora. +worktree.orphans.help.more=Mostrar mais +worktree.orphans.help.less=Mostrar menos session.part.tool.openSubagent=Abrir subagente no editor session.subagent.title=Sessão do subagente session.subagent.path=Kilo / Subagentes / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index dd0bb7aa1fac..0fd94f4666b8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Открыть в новом окне\u2026 worktree.run.failed=Не удалось запустить ''{0}'': {1} worktree.run.hint=Для полной поддержки запуска и отладки откройте этот worktree в новом окне worktree.menu.configure=Настроить новое рабочее дерево... +worktree.orphans.resolve=Решить… +worktree.orphans.dialogTitle=Оставшиеся папки worktree +worktree.orphans.summary={0} оставшихся папок worktree +worktree.orphans.summarySize={0} оставшихся папок worktree · {1} +worktree.orphans.calculating=вычисление размера… +worktree.orphans.columnPath=Путь +worktree.orphans.columnSize=Размер +worktree.orphans.columnContents=Содержимое +worktree.orphans.checkoutWarning=Содержит git checkout +worktree.orphans.footerSelected=Выбрано {0} · {1} +worktree.orphans.footerCheckouts={0} всё ещё содержат git checkout +worktree.orphans.deleteButton=Удалить {0} папок ({1}) +worktree.orphans.cancel=Отмена +worktree.orphans.notification.success=Удалено {0} папок ({1}) +worktree.orphans.notification.partial=Удалено {0} из {1}, {2} не удалось +worktree.orphans.notification.failure=Не удалось удалить оставшиеся папки +worktree.orphans.help.intro=Kilo хранит все создаваемые рабочие деревья в папке .kilo/worktrees этого репозитория. Папки ниже находятся в этой папке, но git не считает ни одну из них рабочим деревом, поэтому они больше не используются. +worktree.orphans.help.checkout=Папка с пометкой "Содержит git checkout" всё ещё содержит запись .git и может хранить незакоммиченную работу. Такие папки остаются невыбранными, поэтому откройте папку и проверьте её перед удалением. +worktree.orphans.help.causes=Остатки обычно появляются из-за прерванного удаления, рабочего дерева, удалённого вне Kilo, или инструмента, который записал данные в папку после её удаления. Выполняющиеся сейчас удаления здесь не показаны. +worktree.orphans.help.delete=Удаление безвозвратно убирает выбранные папки с диска, минуя корзину. Ни одна ветка и ни одно активное рабочее дерево не затрагиваются. Размеры — это объём, который каждая папка занимает на диске сейчас. +worktree.orphans.help.more=Показать ещё +worktree.orphans.help.less=Показать меньше session.part.tool.openSubagent=Открыть субагента в редакторе session.subagent.title=Сессия субагента session.subagent.path=Kilo / Субагенты / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index 26559d1ad6de..176e1918bf69 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=เปิดในเฟรมใหม่\u2026 worktree.run.failed=เรียกใช้ ''{0}'' ไม่สำเร็จ: {1} worktree.run.hint=เพื่อรองรับการรันและดีบักอย่างเต็มรูปแบบ ให้เปิด worktree นี้ในเฟรมใหม่ worktree.menu.configure=กำหนดค่า worktree ใหม่... +worktree.orphans.resolve=แก้ไข… +worktree.orphans.dialogTitle=โฟลเดอร์ worktree ที่หลงเหลือ +worktree.orphans.summary={0} โฟลเดอร์ worktree ที่หลงเหลือ +worktree.orphans.summarySize={0} โฟลเดอร์ worktree ที่หลงเหลือ · {1} +worktree.orphans.calculating=กำลังคำนวณขนาด… +worktree.orphans.columnPath=พาธ +worktree.orphans.columnSize=ขนาด +worktree.orphans.columnContents=เนื้อหา +worktree.orphans.checkoutWarning=มีการเช็คเอาต์ git อยู่ +worktree.orphans.footerSelected=เลือก {0} รายการ · {1} +worktree.orphans.footerCheckouts={0} รายการยังมีการเช็คเอาต์ git +worktree.orphans.deleteButton=ลบ {0} โฟลเดอร์ ({1}) +worktree.orphans.cancel=ยกเลิก +worktree.orphans.notification.success=ลบ {0} โฟลเดอร์แล้ว ({1}) +worktree.orphans.notification.partial=ลบ {0} จาก {1} รายการ ล้มเหลว {2} รายการ +worktree.orphans.notification.failure=ลบโฟลเดอร์ที่หลงเหลือไม่สำเร็จ +worktree.orphans.help.intro=Kilo เก็บ worktree ทุกอันที่สร้างไว้ในโฟลเดอร์ .kilo/worktrees ของที่เก็บนี้ โฟลเดอร์ด้านล่างอยู่ในโฟลเดอร์นั้น แต่ git ไม่ได้ระบุว่าโฟลเดอร์ใดเป็น worktree จึงไม่มีอะไรใช้งานอยู่แล้ว +worktree.orphans.help.checkout=โฟลเดอร์ที่ทำเครื่องหมายว่า "มี git checkout อยู่" ยังมีรายการ .git อยู่ภายในและอาจมีงานที่ยังไม่ได้คอมมิต โฟลเดอร์เหล่านี้จะไม่ถูกเลือกไว้ ดังนั้นให้เปิดดูและตรวจสอบก่อนลบ +worktree.orphans.help.causes=โฟลเดอร์ที่เหลือค้างมักเกิดจากการลบที่ถูกขัดจังหวะ worktree ที่ถูกลบจากภายนอก Kilo หรือเครื่องมือที่เขียนข้อมูลลงในโฟลเดอร์หลังจากถูกลบไปแล้ว การลบที่ยังทำงานอยู่จะไม่แสดงที่นี่ +worktree.orphans.help.delete=การลบจะเอาโฟลเดอร์ที่เลือกออกจากดิสก์อย่างถาวรโดยไม่ผ่านถังขยะ ไม่มีการแตะต้องสาขาหรือ worktree ที่ใช้งานอยู่ ขนาดคือพื้นที่ที่แต่ละโฟลเดอร์ใช้อยู่บนดิสก์ในขณะนี้ +worktree.orphans.help.more=แสดงเพิ่มเติม +worktree.orphans.help.less=แสดงน้อยลง session.part.tool.openSubagent=เปิดซับเอเจนต์ในตัวแก้ไข session.subagent.title=เซสชันซับเอเจนต์ session.subagent.path=Kilo / ซับเอเจนต์ / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index 8abe10f74121..b1939cff9222 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Yeni pencerede aç\u2026 worktree.run.failed=''{0}'' çalıştırılamadı: {1} worktree.run.hint=Tam çalıştırma ve hata ayıklama desteği için bu worktreeyi yeni bir pencerede açın worktree.menu.configure=Yeni worktree yapılandır... +worktree.orphans.resolve=Çöz… +worktree.orphans.dialogTitle=Kalıntı worktree klasörleri +worktree.orphans.summary={0} kalıntı worktree klasörü +worktree.orphans.summarySize={0} kalıntı worktree klasörü · {1} +worktree.orphans.calculating=boyut hesaplanıyor… +worktree.orphans.columnPath=Yol +worktree.orphans.columnSize=Boyut +worktree.orphans.columnContents=İçerik +worktree.orphans.checkoutWarning=Bir git checkout içeriyor +worktree.orphans.footerSelected={0} seçildi · {1} +worktree.orphans.footerCheckouts={0} hâlâ bir git checkout içeriyor +worktree.orphans.deleteButton={0} klasörü sil ({1}) +worktree.orphans.cancel=İptal +worktree.orphans.notification.success={0} klasör kaldırıldı ({1}) +worktree.orphans.notification.partial={1} klasörden {0} kaldırıldı, {2} başarısız +worktree.orphans.notification.failure=Kalıntı klasörler kaldırılamadı +worktree.orphans.help.intro=Kilo, oluşturduğu her worktree'yi bu deponun .kilo/worktrees klasöründe tutar. Aşağıdaki klasörler o klasörün içinde, ancak git hiçbirini worktree olarak listelemiyor; dolayısıyla artık hiçbir şey onları kullanmıyor. +worktree.orphans.help.checkout="Bir git checkout içeriyor" olarak işaretlenen klasörde hâlâ bir .git girdisi var ve commit edilmemiş çalışma barındırabilir. Bunlar seçilmemiş halde bırakılır; silmeden önce klasörü açıp kontrol edin. +worktree.orphans.help.causes=Artıklar genellikle yarıda kesilen bir silme işleminden, Kilo dışında kaldırılan bir worktree'den ya da kaldırıldıktan sonra klasöre yazan bir araçtan kaynaklanır. Hâlâ süren silme işlemleri burada listelenmez. +worktree.orphans.help.delete=Silme, seçilen klasörleri Çöp Kutusu'na göndermeden kalıcı olarak diskten kaldırır. Hiçbir dal ve etkin worktree etkilenmez. Boyutlar her klasörün şu anda diskte kapladığı alandır. +worktree.orphans.help.more=Daha fazla göster +worktree.orphans.help.less=Daha az göster session.part.tool.openSubagent=Alt aracıyı düzenleyicide aç session.subagent.title=Alt aracı oturumu session.subagent.path=Kilo / Alt aracılar / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index 32deffc3795b..9712d4bce6cd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=Відкрити в новому вікні\u2026 worktree.run.failed=Не вдалося запустити ''{0}'': {1} worktree.run.hint=Для повної підтримки запуску та налагодження відкрийте цей worktree у новому вікні worktree.menu.configure=Налаштувати нове робоче дерево... +worktree.orphans.resolve=Вирішити… +worktree.orphans.dialogTitle=Залишкові папки worktree +worktree.orphans.summary={0} залишкових папок worktree +worktree.orphans.summarySize={0} залишкових папок worktree · {1} +worktree.orphans.calculating=обчислення розміру… +worktree.orphans.columnPath=Шлях +worktree.orphans.columnSize=Розмір +worktree.orphans.columnContents=Вміст +worktree.orphans.checkoutWarning=Містить git checkout +worktree.orphans.footerSelected=Вибрано {0} · {1} +worktree.orphans.footerCheckouts={0} досі містять git checkout +worktree.orphans.deleteButton=Видалити {0} папок ({1}) +worktree.orphans.cancel=Скасувати +worktree.orphans.notification.success=Видалено {0} папок ({1}) +worktree.orphans.notification.partial=Видалено {0} з {1}, {2} не вдалося +worktree.orphans.notification.failure=Не вдалося видалити залишкові папки +worktree.orphans.help.intro=Kilo зберігає кожне створене робоче дерево в теці .kilo/worktrees цього репозиторію. Теки нижче містяться в цій теці, але git не вважає жодну з них робочим деревом, тож вони більше не використовуються. +worktree.orphans.help.checkout=Тека з позначкою "Містить git checkout" усе ще має всередині запис .git і може зберігати незакомічену роботу. Такі теки залишаються невибраними, тому відкрийте теку й перевірте її перед видаленням. +worktree.orphans.help.causes=Залишки зазвичай виникають через перерване видалення, робоче дерево, вилучене поза Kilo, або інструмент, який записав дані в теку після її вилучення. Видалення, що досі виконуються, тут не показані. +worktree.orphans.help.delete=Видалення безповоротно вилучає вибрані теки з диска, не проходячи через смітник. Жодна гілка й жодне активне робоче дерево не змінюються. Розміри — це обсяг, який кожна тека займає на диску зараз. +worktree.orphans.help.more=Показати більше +worktree.orphans.help.less=Показати менше session.part.tool.openSubagent=Відкрити субагента в редакторі session.subagent.title=Сесія субагента session.subagent.path=Kilo / Субагенти / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index 8648f6341be6..e7f065dadb74 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=在新窗口中打开\u2026 worktree.run.failed=无法运行 ''{0}'': {1} worktree.run.hint=要获得完整的运行和调试支持,请在新窗口中打开此工作树 worktree.menu.configure=配置新工作树... +worktree.orphans.resolve=解决… +worktree.orphans.dialogTitle=遗留的 worktree 文件夹 +worktree.orphans.summary={0} 个遗留的 worktree 文件夹 +worktree.orphans.summarySize={0} 个遗留的 worktree 文件夹 · {1} +worktree.orphans.calculating=正在计算大小… +worktree.orphans.columnPath=路径 +worktree.orphans.columnSize=大小 +worktree.orphans.columnContents=内容 +worktree.orphans.checkoutWarning=包含 git 签出 +worktree.orphans.footerSelected=已选择 {0} 个 · {1} +worktree.orphans.footerCheckouts={0} 个仍包含 git 签出 +worktree.orphans.deleteButton=删除 {0} 个文件夹 ({1}) +worktree.orphans.cancel=取消 +worktree.orphans.notification.success=已移除 {0} 个文件夹 ({1}) +worktree.orphans.notification.partial=已移除 {1} 个中的 {0} 个,{2} 个失败 +worktree.orphans.notification.failure=移除遗留文件夹失败 +worktree.orphans.help.intro=Kilo 会把它创建的所有工作树都放在本仓库的 .kilo/worktrees 文件夹中。下面的文件夹位于该文件夹内,但 git 并未把它们中的任何一个列为工作树,因此已没有任何东西在使用它们。 +worktree.orphans.help.checkout=标记为“包含 git 检出”的文件夹内仍有 .git 条目,可能保存着未提交的工作。这类文件夹默认不勾选,请先打开查看确认后再删除。 +worktree.orphans.help.causes=残留文件夹通常来自被中断的删除、在 Kilo 之外移除的工作树,或在移除后又向该文件夹写入内容的工具。仍在进行中的删除不会列在这里。 +worktree.orphans.help.delete=删除会将所选文件夹从磁盘上彻底移除,不经过回收站。不会改动任何分支或使用中的工作树。大小为每个文件夹当前在磁盘上占用的空间。 +worktree.orphans.help.more=显示更多 +worktree.orphans.help.less=显示更少 session.part.tool.openSubagent=在编辑器中打开子代理 session.subagent.title=子代理会话 session.subagent.path=Kilo / 子代理 / {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index 718979257f0b..f991c39f0863 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -507,6 +507,28 @@ worktree.run.open.frame=在新視窗中開啟\u2026 worktree.run.failed=無法執行 ''{0}'': {1} worktree.run.hint=若要取得完整的執行與偵錯支援,請在新視窗中開啟此工作樹 worktree.menu.configure=設定新工作樹... +worktree.orphans.resolve=解決… +worktree.orphans.dialogTitle=殘留的 worktree 資料夾 +worktree.orphans.summary={0} 個殘留的 worktree 資料夾 +worktree.orphans.summarySize={0} 個殘留的 worktree 資料夾 · {1} +worktree.orphans.calculating=正在計算大小… +worktree.orphans.columnPath=路徑 +worktree.orphans.columnSize=大小 +worktree.orphans.columnContents=內容 +worktree.orphans.checkoutWarning=包含 git 簽出 +worktree.orphans.footerSelected=已選取 {0} 個 · {1} +worktree.orphans.footerCheckouts={0} 個仍包含 git 簽出 +worktree.orphans.deleteButton=刪除 {0} 個資料夾 ({1}) +worktree.orphans.cancel=取消 +worktree.orphans.notification.success=已移除 {0} 個資料夾 ({1}) +worktree.orphans.notification.partial=已移除 {1} 個中的 {0} 個,{2} 個失敗 +worktree.orphans.notification.failure=移除殘留資料夾失敗 +worktree.orphans.help.intro=Kilo 會將它建立的所有工作樹都放在本儲存庫的 .kilo/worktrees 資料夾中。下方的資料夾位於該資料夾內,但 git 並未將其中任何一個列為工作樹,因此已沒有任何東西在使用它們。 +worktree.orphans.help.checkout=標記為「包含 git 檢出」的資料夾內仍有 .git 項目,可能保存著未提交的工作。這類資料夾預設不勾選,請先開啟查看確認後再刪除。 +worktree.orphans.help.causes=殘留資料夾通常來自被中斷的刪除、在 Kilo 之外移除的工作樹,或在移除後又向該資料夾寫入內容的工具。仍在進行中的刪除不會列在這裡。 +worktree.orphans.help.delete=刪除會將所選資料夾從磁碟上徹底移除,不經過資源回收筒。不會變動任何分支或使用中的工作樹。大小為每個資料夾目前在磁碟上佔用的空間。 +worktree.orphans.help.more=顯示更多 +worktree.orphans.help.less=顯示更少 session.part.tool.openSubagent=在編輯器中開啟子代理 session.subagent.title=子代理工作階段 session.subagent.path=Kilo / 子代理 / {0} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/orphans/OrphanBannerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/orphans/OrphanBannerTest.kt new file mode 100644 index 000000000000..63c543ee1bd4 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/orphans/OrphanBannerTest.kt @@ -0,0 +1,259 @@ +package ai.kilocode.client.agentManager.orphans + +import ai.kilocode.client.agentManager.worktree.KiloWorktreeService +import ai.kilocode.client.agentManager.worktree.WorktreeController +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.testing.FakeAppRpcApi +import ai.kilocode.client.testing.FakeWorktreeRpcApi +import ai.kilocode.client.testing.TestCoroutines +import ai.kilocode.client.testing.pumpEdt +import ai.kilocode.client.util.edtWait +import ai.kilocode.rpc.dto.orphans.OrphanDto +import ai.kilocode.rpc.dto.orphans.OrphanKind +import com.intellij.openapi.application.ApplicationManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.replaceService +import com.intellij.ui.HyperlinkLabel +import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * [OrphanBanner] visibility and text follow [WorktreeController.orphans], which [WorktreeController.reload] + * fills from the same `list()` RPC the worktree list itself uses — so these tests drive [refresh] + * directly against a controller already reloaded, mirroring how [AgentManagerPanel] wires it through + * `controller.onReload`. + */ +@Suppress("UnstableApiUsage") +class OrphanBannerTest : BasePlatformTestCase() { + private lateinit var coroutines: TestCoroutines + private lateinit var rpc: FakeWorktreeRpcApi + private lateinit var service: KiloWorktreeService + + override fun setUp() { + super.setUp() + coroutines = TestCoroutines() + rpc = FakeWorktreeRpcApi() + service = KiloWorktreeService(coroutines.scope, rpc) + ApplicationManager.getApplication().replaceService(KiloWorktreeService::class.java, service, testRootDisposable) + ApplicationManager.getApplication() + .replaceService(KiloAppService::class.java, KiloAppService(coroutines.scope, FakeAppRpcApi()), testRootDisposable) + } + + override fun tearDown() { + try { + coroutines.close(::pump) + } finally { + super.tearDown() + } + } + + fun `test banner is hidden when there are no orphans`() { + val controller = controller() + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + + assertFalse(edt { banner.isVisible }) + } + + fun `test banner shows the count immediately then fills in size once the size pass lands`() { + rpc.orphans = listOf(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + val gate = CompletableDeferred() + rpc.beforeOrphanSizes = { gate.await() } + rpc.orphanSizesResult = { mapOf("/repo/.kilo/worktrees/leftover" to 2048L) } + val controller = controller() + edt { controller.reload() } + flush() + + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + flush() + + assertTrue(edt { banner.isVisible }) + assertEquals("1 leftover worktree folder(s) \u00b7 calculating size\u2026", edt { banner.text }) + + gate.complete(Unit) + assertTrue(coroutines.pumpUntil { edt { banner.text } != "1 leftover worktree folder(s) \u00b7 calculating size\u2026" }) + + // The exact size format is StringUtil.formatFileSize's own choice — only assert the size + // pass landed, not the number's presentation. + val filled = edt { banner.text } + assertTrue("expected the size to be appended -> $filled", filled.startsWith("1 leftover worktree folder(s) \u00b7 ")) + assertNotNull(edt { links(banner).singleOrNull { it.text == "Resolve\u2026" } }) + } + + fun `test banner hides once orphans clear after a reload`() { + rpc.orphans = listOf(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + val controller = controller() + edt { controller.reload() } + flush() + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + flush() + assertTrue(edt { banner.isVisible }) + + rpc.orphans = emptyList() + edt { controller.reload() } + flush() + edt { banner.refresh() } + + assertFalse(edt { banner.isVisible }) + } + + /** + * `orphanSizes` answers empty when the walk fails outright, which must not be read as "these + * folders are 0 bytes" — the banner drops the size claim instead of stating a wrong one, and does + * not keep re-running the failing walk on every refresh. + */ + fun `test banner drops the size claim when the size pass fails instead of reporting zero bytes`() { + rpc.orphans = listOf(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + rpc.orphanSizesResult = { emptyMap() } + val controller = controller() + edt { controller.reload() } + flush() + + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + flush() + + assertEquals("1 leftover worktree folder(s)", edt { banner.text }) + assertEquals(1, rpc.orphanSizeCalls.size) + + edt { banner.refresh() } + flush() + + assertEquals("1 leftover worktree folder(s)", edt { banner.text }) + assertEquals("a failed walk must not be retried on every refresh", 1, rpc.orphanSizeCalls.size) + } + + /** + * A walk that omits some paths (permission error on one folder) is not a total either: summing it + * would silently undercount, so the banner reports the count alone. + */ + fun `test banner drops the size claim when the size pass covers only some folders`() { + rpc.orphans = listOf( + OrphanDto("/repo/.kilo/worktrees/first", OrphanKind.LEFTOVER), + OrphanDto("/repo/.kilo/worktrees/second", OrphanKind.LEFTOVER), + ) + rpc.orphanSizesResult = { mapOf("/repo/.kilo/worktrees/first" to 4096L) } + val controller = controller() + edt { controller.reload() } + flush() + + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + flush() + + assertEquals("2 leftover worktree folder(s)", edt { banner.text }) + } + + /** + * The fs walk is the expensive half of a size pass, so a pass whose answer is already worthless + * has to be cancelled rather than left running to be discarded when it lands. + */ + fun `test a size pass is cancelled when the orphan set changes under it`() { + rpc.orphans = listOf(OrphanDto("/repo/.kilo/worktrees/first", OrphanKind.LEFTOVER)) + val gate = CompletableDeferred() + val walked = AtomicInteger() + rpc.beforeOrphanSizes = { gate.await() } + rpc.orphanSizesResult = { paths -> + walked.incrementAndGet() + paths.associateWith { 512L } + } + val controller = controller() + edt { controller.reload() } + flush() + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + flush() + assertEquals(1, rpc.orphanSizeCalls.size) + + // The orphan set changes while the first walk is still parked on the gate. + rpc.orphans = listOf(OrphanDto("/repo/.kilo/worktrees/second", OrphanKind.LEFTOVER)) + edt { controller.reload() } + flush() + edt { banner.refresh() } + flush() + assertEquals(2, rpc.orphanSizeCalls.size) + + gate.complete(Unit) + assertTrue(coroutines.pumpUntil { walked.get() > 0 }) + flush() + + // Only the current pass ever produced an answer; the superseded one was cancelled at the gate + // instead of walking to completion. + assertEquals("the superseded walk must not run", 1, walked.get()) + assertEquals("1 leftover worktree folder(s) \u00b7 512 B", edt { banner.text }) + } + + /** + * Clicking delete makes the pass in flight pointless — it is holding the very paths about to be + * renamed away — so it is cancelled, and the reload afterwards measures only what is left. + */ + fun `test removing folders cancels the size pass and re-measures the leftovers`() { + val doomed = "/repo/.kilo/worktrees/doomed" + val survivor = "/repo/.kilo/worktrees/survivor" + rpc.orphans = listOf(OrphanDto(doomed, OrphanKind.LEFTOVER), OrphanDto(survivor, OrphanKind.LEFTOVER)) + val gate = CompletableDeferred() + val walked = AtomicInteger() + rpc.beforeOrphanSizes = { gate.await() } + rpc.orphanSizesResult = { paths -> + walked.incrementAndGet() + paths.associateWith { 1024L } + } + val controller = controller() + edt { controller.reload() } + flush() + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + // Mirrors AgentManagerPanel, which is what refreshes the banner after every reload. + controller.onReload = { banner.refresh() } + flush() + assertEquals(1, rpc.orphanSizeCalls.size) + assertEquals("2 leftover worktree folder(s) \u00b7 calculating size\u2026", edt { banner.text }) + + // Delete one folder; the walk over both is still parked on the gate. Later passes are ungated + // so the re-measure can actually land. + rpc.beforeOrphanSizes = {} + rpc.orphans = listOf(OrphanDto(survivor, OrphanKind.LEFTOVER)) + edt { banner.remove(listOf(doomed)) } + gate.complete(Unit) + assertTrue(coroutines.pumpUntil { rpc.orphanSizeCalls.size > 1 }) + flush() + + assertEquals(listOf(doomed), rpc.removeOrphansCalls.single().second) + // The cancelled pass never walked; the pass after the delete covers the survivor alone. + assertEquals("only the post-delete pass runs", 1, walked.get()) + assertEquals(listOf(survivor), rpc.orphanSizeCalls.last().second) + assertEquals("1 leftover worktree folder(s) \u00b7 1.02 kB", edt { banner.text }) + } + + fun `test banner does not re-fetch sizes when the orphan set is unchanged`() { + rpc.orphans = listOf(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + rpc.orphanSizesResult = { mapOf("/repo/.kilo/worktrees/leftover" to 10L) } + val controller = controller() + edt { controller.reload() } + flush() + val banner = edt { OrphanBanner(project, controller, testRootDisposable) } + flush() + assertEquals(1, rpc.orphanSizeCalls.size) + + // A reload that reports the exact same orphan path set must not trigger another size fetch. + edt { controller.reload() } + flush() + edt { banner.refresh() } + flush() + + assertEquals(1, rpc.orphanSizeCalls.size) + } + + private fun controller( + activity: MutableStateFlow> = MutableStateFlow(emptyMap()), + ) = WorktreeController(service, "/repo", coroutines.scope, activity = activity) + + private fun links(root: java.awt.Component): List = components(root).filterIsInstance() + + private fun components(root: java.awt.Component): List = buildList { + add(root) + if (root is java.awt.Container) root.components.forEach { addAll(components(it)) } + } + + private fun flush() = coroutines.drain(::pump) + + private fun edt(block: () -> T): T = edtWait(block) + + private fun pump() = pumpEdt() +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/orphans/OrphanDialogTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/orphans/OrphanDialogTest.kt new file mode 100644 index 000000000000..1abb26d65773 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/orphans/OrphanDialogTest.kt @@ -0,0 +1,224 @@ +package ai.kilocode.client.agentManager.orphans + +import ai.kilocode.client.util.edtWait +import ai.kilocode.rpc.dto.orphans.OrphanDto +import ai.kilocode.rpc.dto.orphans.OrphanKind +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.EditorNotificationPanel +import com.intellij.ui.InlineBanner +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.labels.LinkLabel +import com.intellij.ui.table.JBTable +import java.awt.Component +import java.awt.Container +import javax.swing.Icon +import javax.swing.JEditorPane +import javax.swing.table.TableColumn + +/** + * [OrphanDialog] default selection and delete-button label, driven through the real Swing table + * model rather than any test-only seam — [OrphanTableModel.setValueAt] is exactly what a checkbox + * cell editor calls when a user clicks it. + */ +private const val COLUMN_REVEAL_INDEX = 4 + +class OrphanDialogTest : BasePlatformTestCase() { + private var dialog: OrphanDialog? = null + + override fun tearDown() { + try { + dialog?.let { d -> edt { if (!d.isDisposed) d.close(0) } } + dialog = null + } finally { + super.tearDown() + } + } + + fun `test leftover rows are pre-selected and broken rows are not`() { + open( + OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER), + OrphanDto("/repo/.kilo/worktrees/broken", OrphanKind.BROKEN), + ) + + val table = table() + assertEquals(true, edt { table.model.getValueAt(0, 0) }) + assertEquals(false, edt { table.model.getValueAt(1, 0) }) + } + + fun `test delete button label reflects the current selection`() { + open( + OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER), + OrphanDto("/repo/.kilo/worktrees/broken", OrphanKind.BROKEN), + ) + val table = table() + + val initial = edt { requireNotNull(dialog).okButtonText() } + assertTrue("expected the initial label to mention 1 folder -> $initial", initial!!.contains("1")) + + edt { table.model.setValueAt(true, 1, 0) } + + val updated = edt { requireNotNull(dialog).okButtonText() } + assertTrue("expected the label to update to 2 folders -> $updated", updated!!.contains("2")) + } + + fun `test the ok action is disabled once every row is deselected`() { + open(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + val table = table() + assertTrue(edt { requireNotNull(dialog).isOKActionEnabled }) + + edt { table.model.setValueAt(false, 0, 0) } + + assertFalse(edt { requireNotNull(dialog).isOKActionEnabled }) + } + + fun `test result is null until confirmed`() { + open(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + + assertNull(edt { requireNotNull(dialog).result() }) + } + + fun `test result carries the selected paths once confirmed`() { + open( + OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER), + OrphanDto("/repo/.kilo/worktrees/broken", OrphanKind.BROKEN), + ) + + edt { requireNotNull(dialog).doOKAction() } + + assertEquals(listOf("/repo/.kilo/worktrees/leftover"), edt { requireNotNull(dialog).result() }) + } + + /** + * A `HEADER_ROW` model event makes JTable rebuild its columns from the model, dropping every fixed + * width — which is what a checkbox toggle used to do, leaving the check and reveal columns as wide + * as the resizable ones. + */ + fun `test the fixed columns keep their width and stay unresizable after a toggle`() { + open( + OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER), + OrphanDto("/repo/.kilo/worktrees/broken", OrphanKind.BROKEN), + ) + val table = table() + val widths = edt { listOf(column(table, 0).width, column(table, COLUMN_REVEAL_INDEX).width) } + + edt { table.model.setValueAt(true, 1, 0) } + + assertEquals(widths, edt { listOf(column(table, 0).width, column(table, COLUMN_REVEAL_INDEX).width) }) + assertFalse("the check column must not be resizable", edt { column(table, 0).resizable }) + assertFalse("the reveal column must not be resizable", edt { column(table, COLUMN_REVEAL_INDEX).resizable }) + assertTrue("the path column stays resizable", edt { column(table, 1).resizable }) + } + + fun `test the reveal column renders an icon and is not editable`() { + open(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + val table = table() + + assertTrue(edt { table.model.getValueAt(0, COLUMN_REVEAL_INDEX) } is Icon) + assertFalse(edt { table.model.isCellEditable(0, COLUMN_REVEAL_INDEX) }) + assertTrue("only the check column is editable", edt { table.model.isCellEditable(0, 0) }) + } + + fun `test the header explains where the folders come from`() { + open(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + + val north = edt { requireNotNull(dialog).northComponent() } + val text = components(north).filterIsInstance().joinToString(" ") { edt { it.text ?: "" } } + + assertTrue("the explanation must be a standard banner -> ${north.javaClass.name}", north is InlineBanner) + assertEquals( + "the banner must carry the platform info colors", + EditorNotificationPanel.Status.Info, + edt { (north as InlineBanner).status }, + ) + assertTrue("expected the worktree directory to be named -> $text", text.contains(".kilo/worktrees")) + assertTrue("expected git detection to be explained -> $text", text.contains("git")) + assertFalse("every help string must resolve -> $text", text.contains("!worktree.orphans")) + } + + /** + * The bullet detail (uncommitted-work warning, likely causes, delete semantics) is collapsed + * behind a Show more / Show less link; only the intro sentence is visible up front. + */ + fun `test the header explanation is collapsed to its intro until Show more is clicked`() { + open(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + val north = edt { requireNotNull(dialog).northComponent() } + + val collapsedText = edt { messageText(north) } + assertTrue("expected the intro up front -> $collapsedText", collapsedText.contains(".kilo/worktrees")) + assertFalse("bullets must stay hidden until expanded -> $collapsedText", collapsedText.contains("uncommitted")) + + val toggle = edt { linkLabel(north) } + assertEquals("Show more", edt { toggle.text }) + + edt { toggle.doClick() } + + val expandedText = edt { messageText(north) } + assertTrue("expected the checkout case once expanded -> $expandedText", expandedText.contains("uncommitted")) + assertEquals("Show less", edt { toggle.text }) + + edt { toggle.doClick() } + + val recollapsedText = edt { messageText(north) } + assertFalse("bullets must hide again on a second click -> $recollapsedText", recollapsedText.contains("uncommitted")) + assertEquals("Show more", edt { toggle.text }) + } + + /** + * The overlay scrollbar paints over the viewport, so the right-hand column needs a reserved strip. + * `ScrollPaneLayout` widens the column header by the viewport insets but shifts it left by the left + * inset only, so the strip has to be right-only or the header stops lining up with the rows. + */ + fun `test the viewport reserves a right-only strip for the scrollbar`() { + open(OrphanDto("/repo/.kilo/worktrees/leftover", OrphanKind.LEFTOVER)) + val scroll = components(edt { requireNotNull(dialog).centerComponent() }).filterIsInstance().single() + + val insets = edt { requireNotNull(scroll.viewportBorder).getBorderInsets(scroll) } + + assertTrue("expected a reserved strip on the right -> $insets", insets.right > 0) + assertEquals("a left inset would shift the rows out from under the header", 0, insets.left) + assertEquals(0, insets.top) + assertEquals(0, insets.bottom) + } + + fun `test footer mentions checkouts only when a broken row is selected`() { + open(OrphanDto("/repo/.kilo/worktrees/broken", OrphanKind.BROKEN)) + val table = table() + val footer = footer() + + assertFalse(edt { footer.text }.contains("checkout")) + + edt { table.model.setValueAt(true, 0, 0) } + + assertTrue(edt { footer.text }.contains("checkout")) + } + + private fun open(vararg orphans: OrphanDto, sizes: Map = emptyMap()) { + dialog = edt { OrphanDialog(JBPanel(), project, orphans.toList(), sizes) } + } + + private fun table(): JBTable = components(edt { requireNotNull(dialog).centerComponent() }).filterIsInstance().single() + + private fun footer(): JBLabel = components(edt { requireNotNull(dialog).centerComponent() }).filterIsInstance().first() + + private fun column(table: JBTable, index: Int): TableColumn = table.columnModel.getColumn(index) + + private fun messageText(north: Component): String = + components(north).filterIsInstance().joinToString(" ") { it.text ?: "" } + + /** + * [InlineBanner] always carries a second, initially-invisible `LinkLabel` — its overflow + * "More" dropdown, shown only once there are 4+ actions — so the one visible link is ours. + */ + @Suppress("UNCHECKED_CAST") + private fun linkLabel(north: Component): LinkLabel = + components(north).filterIsInstance>().single { it.isVisible } as LinkLabel + + private fun components(root: Component): List = buildList { + add(root) + if (root is Container) root.components.forEach { addAll(components(it)) } + } + + private fun edt(block: () -> T): T = edtWait(block) +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorktreeRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorktreeRpcApi.kt index 7af7a047d67e..5ee568a9e858 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorktreeRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorktreeRpcApi.kt @@ -14,6 +14,8 @@ import ai.kilocode.rpc.dto.WorktreeDto import ai.kilocode.rpc.dto.WorktreeListDto import ai.kilocode.rpc.dto.WorktreePrListDto import ai.kilocode.rpc.dto.WorktreeStatsListDto +import ai.kilocode.rpc.dto.orphans.OrphanRemoveResultDto +import ai.kilocode.rpc.dto.orphans.RemoveOrphansResultDto import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import java.util.concurrent.ConcurrentHashMap @@ -25,6 +27,8 @@ import java.util.concurrent.CopyOnWriteArrayList */ class FakeWorktreeRpcApi : KiloWorktreeRpcApi { val listed = CopyOnWriteArrayList() + /** Orphan directories [list] reports, alongside [listed]. See [ai.kilocode.rpc.dto.orphans.OrphanDto]. */ + var orphans: List = emptyList() val branchesList = CopyOnWriteArrayList() var statsResult = WorktreeStatsListDto() var dirtyResult = WorktreeDirtyListDto() @@ -88,6 +92,16 @@ class FakeWorktreeRpcApi : KiloWorktreeRpcApi { } var openResult: (String) -> Boolean = { true } var removeResult: (String, String?, Boolean) -> RemoveWorktreeResultDto = { _, _, _ -> RemoveWorktreeResultDto(ok = true) } + val orphanSizeCalls = CopyOnWriteArrayList>>() + var orphanSizesResult: (List) -> Map = { emptyMap() } + /** Gate for holding an [orphanSizes] answer open, so a test can observe the "calculating" state. */ + var beforeOrphanSizes: suspend () -> Unit = {} + val removeOrphansCalls = CopyOnWriteArrayList>>() + var removeOrphansResult: (List) -> RemoveOrphansResultDto = { paths -> + RemoveOrphansResultDto(paths.map { OrphanRemoveResultDto(it, ok = true) }) + } + val revealPaths = CopyOnWriteArrayList() + var revealPathResult: (String) -> Boolean = { true } var renameResult: (String, String) -> RenameWorktreeResultDto = { path, name -> val idx = listed.indexOfFirst { it.path == path } if (idx < 0) RenameWorktreeResultDto(error = "missing") else { @@ -99,7 +113,7 @@ class FakeWorktreeRpcApi : KiloWorktreeRpcApi { override suspend fun list(directory: String): WorktreeListDto { assertNotEdt("list") - return WorktreeListDto(listed.toList()) + return WorktreeListDto(listed.toList(), orphans = orphans) } override suspend fun listBranches(directory: String): WorktreeBranchesDto { @@ -218,4 +232,23 @@ class FakeWorktreeRpcApi : KiloWorktreeRpcApi { sessionLists[directory] = visible return true } + + override suspend fun orphanSizes(directory: String, paths: List): Map { + assertNotEdt("orphanSizes") + orphanSizeCalls.add(directory to paths) + beforeOrphanSizes() + return orphanSizesResult(paths) + } + + override suspend fun removeOrphans(directory: String, paths: List): RemoveOrphansResultDto { + assertNotEdt("removeOrphans") + removeOrphansCalls.add(directory to paths) + return removeOrphansResult(paths) + } + + override suspend fun revealPath(path: String): Boolean { + assertNotEdt("revealPath") + revealPaths.add(path) + return revealPathResult(path) + } } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorktreeRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorktreeRpcApi.kt index f95f8d79774f..482b24fe993d 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorktreeRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorktreeRpcApi.kt @@ -12,6 +12,7 @@ import ai.kilocode.rpc.dto.WorktreeDirtyListDto import ai.kilocode.rpc.dto.WorktreeListDto import ai.kilocode.rpc.dto.WorktreePrListDto import ai.kilocode.rpc.dto.WorktreeStatsListDto +import ai.kilocode.rpc.dto.orphans.RemoveOrphansResultDto import com.intellij.platform.rpc.RemoteApiProviderService import fleet.rpc.RemoteApi import fleet.rpc.Rpc @@ -124,4 +125,29 @@ interface KiloWorktreeRpcApi : RemoteApi { /** Records the session-list visibility for [directory]. Returns true when written. */ suspend fun setSessionList(directory: String, visible: Boolean): Boolean + + /** + * Apparent size (sum of regular-file sizes, never following symlinks) of every [paths] entry + * under [directory]'s repository. A path that fails to walk (permission error, disappeared mid + * walk) is simply omitted from the result rather than failing the whole batch. + */ + suspend fun orphanSizes(directory: String, paths: List): Map + + /** + * Removes every one of [paths] — directories under `.kilo/worktrees/` that git does not track + * (see [ai.kilocode.rpc.dto.orphans.OrphanDto]) — from [directory]'s repository. Each path is + * re-validated against a fresh scan immediately before it is touched and reported independently, + * so a stale selection or a path that is no longer an orphan is skipped rather than deleted. + * Deletion is a stage-rename followed by a background recursive delete, the same primitive + * [remove] uses for a managed worktree — this call returns as soon as every path has been + * staged, not after the background delete finishes. + */ + suspend fun removeOrphans(directory: String, paths: List): RemoveOrphansResultDto + + /** + * Reveals [path] in the host OS's file manager. Backend-only: in split mode the frontend runs on + * the client machine while the worktree lives on the host, the same reason [open] is backend + * (see its doc). Returns false when reveal is unsupported on this OS or [path] does not exist. + */ + suspend fun revealPath(path: String): Boolean } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt index 19ffa14171ce..e0b623fb1340 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorktreeDto.kt @@ -1,5 +1,6 @@ package ai.kilocode.rpc.dto +import ai.kilocode.rpc.dto.orphans.OrphanDto import kotlinx.serialization.Serializable @Serializable @@ -22,7 +23,7 @@ data class WorktreeListDto( * deletes or hand-removed metadata. Reported so they can be surfaced and cleaned deliberately; * nothing removes them automatically. */ - val orphans: List = emptyList(), + val orphans: List = emptyList(), ) @Serializable diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/orphans/OrphanDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/orphans/OrphanDto.kt new file mode 100644 index 000000000000..1ee168597170 --- /dev/null +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/orphans/OrphanDto.kt @@ -0,0 +1,31 @@ +package ai.kilocode.rpc.dto.orphans + +import kotlinx.serialization.Serializable + +/** + * Whether an [OrphanDto] still holds a git checkout ([BROKEN], its `.git` file/dir is still there — + * likely an interrupted delete) or is a plain [LEFTOVER] directory (hand-removed metadata, a stray + * `.idea`/`.kilo-dev` directory, etc.). Mirrors VS Code's `worktree-reconcile.ts` classification so + * both clients agree on which rows are pre-selected for deletion and which are flagged as risky. + */ +@Serializable +enum class OrphanKind { BROKEN, LEFTOVER } + +/** One directory under `.kilo/worktrees/` that git does not track. See `WorktreeListDto.orphans`. */ +@Serializable +data class OrphanDto( + val path: String, + val kind: OrphanKind, +) + +@Serializable +data class OrphanRemoveResultDto( + val path: String, + val ok: Boolean, + val error: String? = null, +) + +@Serializable +data class RemoveOrphansResultDto( + val results: List = emptyList(), +) diff --git a/packages/kilo-ui/src/components/checkbox.css b/packages/kilo-ui/src/components/checkbox.css index 97b8260f4cb6..cf47ed0a096e 100644 --- a/packages/kilo-ui/src/components/checkbox.css +++ b/packages/kilo-ui/src/components/checkbox.css @@ -1,4 +1,6 @@ -/* Kilo Checkbox overrides – sized to match @vscode/webview-ui-toolkit */ +/* Kilo Checkbox overrides – sized and coloured to match @vscode/webview-ui-toolkit. The + --vscode-checkbox-* variables only exist inside the VS Code webview (see vscode-bridge.css); + everywhere else (Storybook, kilo-web-ui) the fallbacks reproduce today's look unchanged. */ [data-component="checkbox"] { margin: 4px 0; @@ -8,16 +10,34 @@ width: 18px; height: 18px; border-radius: 3px; - border-color: var(--text-weak); + background-color: var(--vscode-checkbox-background, transparent); + border-color: var(--vscode-checkbox-border, var(--text-weak)); } - &[data-checked] [data-slot="checkbox-checkbox-control"] { - background-color: var(--text-base); - border-color: var(--text-base); + /* Indeterminate reads the same as checked: a real VS Code checkbox does not have a third + visual language for "some selected" — only the glyph differs. */ + &[data-checked] [data-slot="checkbox-checkbox-control"], + &[data-indeterminate] [data-slot="checkbox-checkbox-control"] { + background-color: var(--vscode-checkbox-background, var(--text-base)); + border-color: var(--vscode-checkbox-border, var(--text-base)); } - &[data-checked] [data-slot="checkbox-checkbox-indicator"] { - color: var(--background-base); + &[data-checked] [data-slot="checkbox-checkbox-indicator"], + &[data-indeterminate] [data-slot="checkbox-checkbox-indicator"] { + color: var(--vscode-checkbox-foreground, var(--background-base)); + } + + /* Repeats upstream's selector shapes (packages/ui/src/components/checkbox.css) at equal + specificity so the box does not change colour under the pointer inside VS Code. */ + &:hover:not([data-disabled], [data-readonly]) [data-slot="checkbox-checkbox-control"], + &[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="checkbox-checkbox-control"], + &[data-indeterminate]:hover:not([data-disabled]) [data-slot="checkbox-checkbox-control"] { + background-color: var(--vscode-checkbox-background, var(--surface-hover)); + border-color: var(--vscode-checkbox-border, var(--border-hover)); + } + + &:not([data-readonly]) [data-slot="checkbox-checkbox-input"]:focus-visible + [data-slot="checkbox-checkbox-control"] { + border-color: var(--vscode-focusBorder, var(--border-focus)); } [data-slot="checkbox-checkbox-label"] { diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index c4b6648beb7d..66a553b5990d 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -16,6 +16,7 @@ import { closeLifecycleSession, createLifecycleWorktree, deleteLifecycleWorktree, + lifecycleSessions, promoteLifecycleSession, removeStaleLifecycleWorktree, removeWorktreeSnapshot, @@ -36,7 +37,7 @@ import { SessionTerminalManager } from "./SessionTerminalManager" import { createTerminalHost } from "./terminal-host" import { TerminalRouter } from "./terminal-routing" import { discardWorktree as discard } from "./discard-worktree" -import { acquirePtyCleanup } from "./pty-cleanup" +import { acquirePtyCleanup, teardown } from "./pty-cleanup" import { executeVscodeTask } from "./task-runner" import { runLifecycleSetup } from "./provider-lifecycle" import { RunController } from "./run/controller" @@ -92,7 +93,7 @@ import type { Host, PanelContext, OutputHandle, Disposable } from "./host" import { focusPanelPrompt, revealPanel } from "./focus-panel" import { formatLog } from "./log-format" import { HealthScheduler, applyPresence, healthPayload, needsReconcile, staleForState } from "./worktree-health" -import { broken } from "./worktree-reconcile" +import { broken, type WorktreeHealthReport } from "./worktree-reconcile" import { handleRecovery, type RecoveryMessage } from "./worktree-recovery" import { runDoctor } from "./worktree-doctor" import type { BrowserBroker } from "../services/browser-automation" @@ -131,7 +132,7 @@ export class AgentManagerProvider implements Disposable { /** Scratch set returned when no active context exists; mutations are discarded. */ private readonly staleScratch = new Set() private readonly healthScheduler = new HealthScheduler(async (ctx) => { - await reconcileProject(ctx, (...args: unknown[]) => this.log(...args)) + await this.reconcileAndPush(ctx) if (this.contexts.active()?.id === ctx.id) this.pushState(ctx) }) private unsubDestination: (() => void) | undefined @@ -604,6 +605,7 @@ export class AgentManagerProvider implements Disposable { if (m.type === "agentManager.removeStaleWorktree") return this.onRemoveStaleWorktree(m) if (m.type === "agentManager.restoreWorktree") return this.recover(m) if (m.type === "agentManager.cleanOrphanDirectories") return this.recover(m) + if (m.type === "agentManager.revealPath") return this.recover(m) if (m.type === "agentManager.promoteSession") return this.onPromoteSession(m.sessionId) if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId, m.sessionId) if (m.type === "agentManager.forkSession") return this.onForkSession(m.sessionId, m.worktreeId, m.messageId) @@ -1169,16 +1171,23 @@ export class AgentManagerProvider implements Disposable { return removeStaleLifecycleWorktree(ctx, this.lifecycleHost, m.worktreeId, m.keepSessions === true) } + private reconcileAndPush(ctx: ProjectContext): Promise { + // Sizes that land later push themselves through the context's `sized` hook (see project/wiring). + return reconcileProject(ctx, (...args: unknown[]) => this.log(...args)) + } + private recover(m: RecoveryMessage): Promise { return handleRecovery(m, this.context, { post: (message) => this.postToWebview(message), push: () => this.pushState(), log: (...args) => this.log(...args), - reconcile: (ctx) => reconcileProject(ctx, (...args: unknown[]) => this.log(...args)), - refresh: (worktreeId) => { - this.prBridge.poller.refresh(worktreeId, true) - this.statsPoller.revive(worktreeId) - }, + reconcile: (ctx) => this.reconcileAndPush(ctx), + refresh: (worktreeId) => (this.prBridge.poller.refresh(worktreeId, true), this.statsPoller.revive(worktreeId)), + reveal: (path) => this.host.revealInOS(path), + teardown: (root, path) => teardown((dir) => this.connectionService.getClientAsync(dir), root, path), + removeSnapshot: (root, path) => removeWorktreeSnapshot(this.lifecycleHost, root, path), + withProgress: (title, task) => this.host.withProgress(title, task), + notifyResult: (kind, message) => this.host.notify(kind, message), }) } @@ -1448,18 +1457,7 @@ export class AgentManagerProvider implements Disposable { createSession: (dir, branch, id, boot, timing) => this.createSessionInWorktree(dir, branch, id, undefined, boot, timing), notifyReady: (sid, result, id) => this.notifyWorktreeReady(sid, result, id), - sessions: { - register: (session) => this.panel?.sessions.registerSession(session), - clearDirectory: (sid) => (this.browserLifecycle?.close(sid), this.panel?.sessions.clearSessionDirectory(sid)), - setSessionDirectory: (sid, dir) => ( - this.browserLifecycle?.close(sid), - this.panel?.sessions.setSessionDirectory(sid, dir) - ), - registerSessionRoute: (ref, dir, gen) => this.panel?.sessions.registerSessionRoute?.(ref, dir, gen), - directories: () => this.panel?.sessions.getSessionDirectories(), - abort: (ids) => this.panel?.sessions.abortSessions(ids) ?? Promise.resolve(), - forget: (sid) => void this.panelSessions.delete(sid), - }, + sessions: lifecycleSessions(this.panel, this.browserLifecycle, this.panelSessions), push: () => this.pushState(), register: (sid, dir) => this.registerWorktreeSession(sid, dir), skipStats: (id) => this.statsPoller.skipWorktree(id), diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index 453bd2212e00..9b1dc664c623 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -740,14 +740,17 @@ export class WorktreeManager { * Delete a directory under `.kilo/worktrees/` that git no longer tracks. * * Only ever called for a user-confirmed cleanup of an orphaned directory: there is no worktree - * left to remove, so this is a plain recursive delete behind the managed-path guard. + * left to remove, so this stages the same rename-then-background-reap `detachWorktree` uses + * instead of a blocking `fs.rm` — a large `.kilo-dev`/`node_modules` tree cannot freeze the caller. + * Guards are identical to the ones `removeWorktree` relies on for a real worktree: a managed-path + * check, then a fail-closed `git worktree list` re-check immediately before the rename, because the + * worktree pool creates and removes slot checkouts on a timer and a stale webview orphan list must + * never be trusted over what git says right now. */ - async removeOrphanDirectory(target: string): Promise { + async detachOrphanDirectory(target: string): Promise<{ done: Promise }> { if (!this.isManagedPath(target)) { throw new Error(`Refusing to remove a path outside the worktrees directory: ${target}`) } - // Fail closed: an unanswerable `git worktree list` is not evidence the path is orphaned, and - // this is the only re-check between a stale webview orphan list and a recursive delete. const registered = await this.registeredPaths() if (!registered) { throw new Error(`Refusing to remove a worktree directory while git cannot list worktrees: ${target}`) @@ -755,8 +758,29 @@ export class WorktreeManager { if (registered.has(pathKey(target))) { throw new Error(`Refusing to remove a live worktree: ${target}`) } - await fs.promises.rm(target, RM_OPTS) - this.log(`Removed orphaned worktree directory: ${target}`) + const temp = await this.detach(target) + if (!temp) throw new Error(`Refusing to remove ${target}: rename failed`) + return { done: this.defer(target, this.reapOrphan(target, temp)) } + } + + /** + * Background reap for a staged orphan directory, with one reappearance retry. + * + * A dev backend or the worktree pool can recreate a directory moments after it was renamed away + * (e.g. `.kilo-dev` from a running JetBrains dev instance). One retry self-heals that race without + * looping forever: anything that survives the retry simply reappears in the next reconcile. + */ + private async reapOrphan(original: string, temp: string): Promise { + await fs.promises.rm(temp, RM_OPTS).catch((err: unknown) => { + this.log(`Background cleanup failed for ${temp}: ${err}`) + }) + if (!fs.existsSync(original)) return + this.log(`Orphaned directory reappeared after removal, retrying once: ${original}`) + const retry = await this.detach(original) + if (!retry) return + await fs.promises.rm(retry, RM_OPTS).catch((err: unknown) => { + this.log(`Background cleanup failed for ${retry}: ${err}`) + }) } /** diff --git a/packages/kilo-vscode/src/agent-manager/host.ts b/packages/kilo-vscode/src/agent-manager/host.ts index 7686b79c50be..11d1b3e50afc 100644 --- a/packages/kilo-vscode/src/agent-manager/host.ts +++ b/packages/kilo-vscode/src/agent-manager/host.ts @@ -171,6 +171,15 @@ export interface Host { /** Show an error notification. */ showError(msg: string): void + /** Show an info, warning, or error notification. */ + notify(kind: "info" | "warning" | "error", msg: string): void + + /** Reveal a path in the OS file manager. A no-op (logged) on a remote workspace. */ + revealInOS(path: string): void + + /** Run a cancellable background task behind a progress notification. */ + withProgress(title: string, task: (cancelled: () => boolean) => Promise): Promise + /** Open a text document in an editor (e.g. setup script). */ openDocument(path: string): Promise diff --git a/packages/kilo-vscode/src/agent-manager/orphans/size.ts b/packages/kilo-vscode/src/agent-manager/orphans/size.ts new file mode 100644 index 000000000000..31bb479e03b5 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/orphans/size.ts @@ -0,0 +1,79 @@ +/** + * Apparent on-disk size for orphaned worktree directories. + * + * Walks each path with `opendir`, summing `lstat` sizes for regular files only. Symlinks are + * skipped outright — never followed — so a broken or cyclic link cannot inflate the total or hang + * the walk. A directory that cannot be read (permissions, mid-flight deletion by the worktree pool) + * is simply omitted from the result: sizing is a UI nicety, never a reason to block or fail the + * orphan report it annotates. + * + * No vscode imports — pure fs, callable from the reconcile pass or a test. + */ + +import * as fs from "fs" +import * as path from "path" + +async function readInto(dir: string, pending: string[], signal: AbortSignal | undefined): Promise { + let total = 0 + const entries = await fs.promises.opendir(dir) + for await (const entry of entries) { + if (signal?.aborted) break + if (entry.isSymbolicLink()) continue + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + pending.push(full) + continue + } + if (!entry.isFile()) continue + const stat = await fs.promises.lstat(full).catch(() => undefined) + if (stat) total += stat.size + } + return total +} + +async function walk(root: string, signal?: AbortSignal): Promise { + const pending: string[] = [] + // The root itself has to be readable, or this path fails outright and the caller omits it — this + // call is deliberately not wrapped in a try/catch. + let total = await readInto(root, pending, signal) + while (pending.length > 0) { + if (signal?.aborted) return total + const dir = pending.pop() + if (dir === undefined) break + // A subdirectory discovered underneath the root is different: `opendir` can resolve for a + // directory the process cannot actually scan (Node defers the real `scandir` syscall to the + // first read, which throws out of the `for await` inside `readInto` rather than out of + // `opendir` itself), and a permission error partway through the tree should only drop that + // subtree, not the whole walk. + total += await readInto(dir, pending, signal).catch(() => 0) + } + return total +} + +/** + * Total apparent size of each path, walked with bounded concurrency. + * + * A path that fails outright — gone before the walk starts, unreadable throughout — is omitted + * from the returned map rather than throwing, so one bad orphan can never block the size of the + * rest. Aborting `signal` stops new work quickly; a walk already in flight for one path finishes + * its current directory rather than being torn down mid-read. + */ +export async function sizes( + paths: string[], + opts: { signal?: AbortSignal; concurrency?: number } = {}, +): Promise> { + const result = new Map() + const queue = [...paths] + const concurrency = Math.max(1, Math.min(opts.concurrency ?? 4, queue.length || 1)) + const workers = Array.from({ length: concurrency }, async () => { + for (;;) { + if (opts.signal?.aborted) return + const target = queue.shift() + if (target === undefined) return + const bytes = await walk(target, opts.signal).catch(() => undefined) + if (bytes !== undefined && !opts.signal?.aborted) result.set(target, bytes) + } + }) + await Promise.all(workers) + return result +} diff --git a/packages/kilo-vscode/src/agent-manager/orphans/sizing.ts b/packages/kilo-vscode/src/agent-manager/orphans/sizing.ts new file mode 100644 index 000000000000..eea9d1cd7470 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/orphans/sizing.ts @@ -0,0 +1,174 @@ +/** + * Tracks per-project orphan directory sizes and reports when a size pass lands. + * + * Sizing never decides orphan-ness — it only annotates the directories `worktree-reconcile.ts` + * already classified — so a slow or failed walk can never change what the banner and dialog show, + * only how long they wait to show a size next to it. + * + * Keyed by `ProjectContext` identity in a `WeakMap`: a disposed and garbage-collected context drops + * its tracker for free, and a project that is removed and re-added starts with a clean slate rather + * than inheriting a stale path set. + * + * No vscode imports. + */ + +import { sizes } from "./size" +import type { ProjectContext } from "../project/context" +import type { OrphanDirectory } from "../worktree-reconcile" + +type Tracker = { + abort: AbortController | undefined + paused: boolean + /** + * Paths the in-flight walk (if any) is measuring. Tracked separately from `known` so a later call + * can tell whether that walk is still trustworthy — see the abort check in {@link trackOrphanSizes}. + */ + pending: Set | undefined + /** + * Settled measurements by path. A present key means the walk is done with that directory; the value + * is its size, or undefined when it could not be measured at all. + * + * This is the only durable home for a size. Every reconcile rebuilds the report's orphan objects + * from scratch (see worktree-reconcile.ts), so a size written onto them is gone by the next + * worktree-health poll — and because the path set is unchanged at that point, no new pass would run + * to recover it. Keeping the numbers here, on a tracker that outlives the report, is what lets a + * rebuilt report inherit what is already known. + */ + known: Map +} + +const trackers = new WeakMap() + +function tracker(ctx: ProjectContext): Tracker { + const existing = trackers.get(ctx) + if (existing) return existing + const created: Tracker = { abort: undefined, paused: false, pending: undefined, known: new Map() } + trackers.set(ctx, created) + return created +} + +function isSubset(a: Set, b: Set): boolean { + for (const path of a) if (!b.has(path)) return false + return true +} + +/** + * Copy what is already known onto the report's current orphan objects. + * + * Read fresh from `ctx.report` rather than closed over, so a report replaced by a newer reconcile + * while a walk was in flight is never overwritten with sizes for directories it does not list. + */ +function apply(ctx: ProjectContext, known: Map): boolean { + let applied = false + for (const orphan of ctx.report?.orphans ?? []) { + if (!known.has(orphan.path)) continue + const bytes = known.get(orphan.path) + if (bytes !== undefined && orphan.bytes !== bytes) { + orphan.bytes = bytes + applied = true + } + // Marked even without a size, so the UI stops waiting for a number that is never coming. + if (!orphan.sized) { + orphan.sized = true + applied = true + } + } + return applied +} + +/** + * Annotate the current orphan set with sizes, walking only what is not already known. + * + * Called by every reconcile. Known sizes are re-applied to the objects that reconcile just built — + * that part is not an optimization, it is what stops the banner from losing its total on every + * worktree-health poll. Only genuinely new directories are walked, so a routine poll costs nothing and + * one new leftover folder does not re-read the other forty. + * + * A walk in flight is abandoned as soon as any path it covers leaves the orphan list — a directory + * removed outside Kilo, say — regardless of whether *this* call needs a walk of its own. Leaving it + * running would eventually write a size for a path nothing lists as an orphan anymore, and if that + * path reappears later, `apply` would resurrect the stale number with no new walk ever correcting it, + * since the path already has an entry in `known`. Checking this ahead of the usual "anything missing?" + * question below is what makes the abort happen even when this call's own path set does not need a + * walk (e.g. every orphan was removed at once, leaving nothing to be missing). + */ +export function trackOrphanSizes( + ctx: ProjectContext, + orphans: OrphanDirectory[], + log: (...args: unknown[]) => void, +): void { + const state = tracker(ctx) + if (state.paused) return + const next = orphans.map((orphan) => orphan.path).toSorted() + apply(ctx, state.known) + // Directories that have left the list stop being interesting; without this the cache would grow for + // the life of the window. + const covered = new Set(next) + for (const path of state.known.keys()) { + if (!covered.has(path)) state.known.delete(path) + } + if (state.pending && !isSubset(state.pending, covered)) { + state.abort?.abort() + state.abort = undefined + state.pending = undefined + } + const missing = next.filter((path) => !state.known.has(path) && !state.pending?.has(path)) + if (missing.length === 0) return + // Starting a new walk means abandoning whatever is still in flight — `sizes` has no way to add + // paths to a running call. The still-trustworthy `pending` paths excluded above were relying on + // that walk to land for them, so they are folded into this one rather than left to fall out of both + // `known` and `pending` once the old walk's result is discarded as aborted. + const walk = state.pending ? [...state.pending, ...missing] : missing + state.abort?.abort() + const controller = new AbortController() + state.abort = controller + state.pending = new Set(walk) + sizes(walk, { signal: controller.signal }) + .then((result) => { + if (controller.signal.aborted) return + // Recorded for every path walked, with or without an answer: `sizes` omits a directory it could + // not read rather than failing the batch, and "we tried" is what the UI needs to stop waiting. + for (const path of walk) state.known.set(path, result.get(path)) + state.pending = undefined + if (apply(ctx, state.known)) ctx.notifySized() + }) + .catch((err: unknown) => log(`Failed to compute orphan directory sizes: ${err}`)) +} + +/** + * Abort the in-flight size pass and hold off new ones until [resumeOrphanSizes]. + * + * Called when a delete starts: the walk is holding the very paths that are about to be renamed away, + * so it is measuring folders the user already decided to destroy. Pausing rather than merely aborting + * matters because a delete re-reconciles before it touches anything, and that reconcile would + * otherwise start a fresh pass over the doomed set immediately. + * + * Already-measured sizes are kept: deleting one folder does not change the size of the others, so the + * reconcile after the delete can show a correct total straight away instead of re-reading everything + * that survived. The deleted directories drop out of the cache on that same reconcile, since they are + * no longer in the list. + */ +export function pauseOrphanSizes(ctx: ProjectContext): void { + const state = tracker(ctx) + state.paused = true + state.abort?.abort() + state.abort = undefined + state.pending = undefined +} + +/** + * Allow size passes again after [pauseOrphanSizes]. + * + * Does not start one — the reconcile that follows a delete does, for whatever orphans are left. + */ +export function resumeOrphanSizes(ctx: ProjectContext): void { + const state = trackers.get(ctx) + if (!state) return + state.paused = false +} + +/** Abort any in-flight size pass for a project context that is going away. */ +export function disposeOrphanSizes(ctx: ProjectContext): void { + trackers.get(ctx)?.abort?.abort() + trackers.delete(ctx) +} diff --git a/packages/kilo-vscode/src/agent-manager/project/context.ts b/packages/kilo-vscode/src/agent-manager/project/context.ts index 0f9b0ee38a69..65014008df94 100644 --- a/packages/kilo-vscode/src/agent-manager/project/context.ts +++ b/packages/kilo-vscode/src/agent-manager/project/context.ts @@ -24,12 +24,21 @@ import { WorktreeManager } from "../WorktreeManager" import { SetupScriptService } from "../SetupScriptService" import type { GitOps } from "../GitOps" import type { WorktreeHealthReport } from "../worktree-reconcile" +import { disposeOrphanSizes } from "../orphans/sizing" import type { ProjectSessionView } from "./session-view" export interface ProjectContextDeps { log: (msg: string) => void git?: GitOps exists?: (dir: string) => boolean + /** + * Orphan directory sizes landed for this project and the webview needs the new numbers. + * + * Wired once here rather than threaded through every reconcile call site: sizing is started by + * whichever reconcile happens to run first — startup, an explicit repair, the diagnostics report — + * and a pass whose results are never pushed leaves the banner calculating forever. + */ + sized?: (ctx: ProjectContext) => void /** Whether background worktree pre-warming is enabled for this project. */ worktreePool?: () => boolean /** Factory overrides for tests. */ @@ -86,6 +95,12 @@ export class ProjectContext { return this.version === generation && this.phase !== "disposing" && this.phase !== "disposed" } + /** Orphan sizes landed on {@link report}; ask the host to push them. Silent for a dead context. */ + notifySized(): void { + if (this.phase === "disposing" || this.phase === "disposed") return + this.deps.sized?.(this) + } + /** Initialize repository state exactly once per context lifetime. */ ensureReady(run: (generation: number) => Promise>): Promise { if (this.phase === "disposed" || this.phase === "disposing") { @@ -230,6 +245,7 @@ export class ProjectContext { if (this.phase === "disposed") return this.version++ this.phase = "disposing" + disposeOrphanSizes(this) await this.init?.catch((err) => this.deps.log(`dispose: initialization failed: ${err}`)) await this.mutation.catch((err) => this.deps.log(`dispose: mutation failed: ${err}`)) await this.worktrees?.settle().catch((err) => this.deps.log(`dispose: worktree bookkeeping failed: ${err}`)) diff --git a/packages/kilo-vscode/src/agent-manager/project/init.ts b/packages/kilo-vscode/src/agent-manager/project/init.ts index e4ed2594f593..56d552455ddd 100644 --- a/packages/kilo-vscode/src/agent-manager/project/init.ts +++ b/packages/kilo-vscode/src/agent-manager/project/init.ts @@ -10,6 +10,7 @@ import * as fs from "fs" import { restoreWorktrees } from "../state-recovery" import { reconcileWorktrees, summarize, type WorktreeHealthReport } from "../worktree-reconcile" +import { trackOrphanSizes } from "../orphans/sizing" import type { ProjectContext, ProjectInitResult } from "./context" import type { Session } from "@kilocode/sdk/v2/client" import type { ProjectRef, SessionRef, WorktreeRef } from "./route" @@ -145,6 +146,7 @@ export async function reconcileProject( if (!report) return undefined ctx.report = report log(`worktree health: ${summarize(report)}`) + trackOrphanSizes(ctx, report.orphans, log) return report } diff --git a/packages/kilo-vscode/src/agent-manager/project/wiring.ts b/packages/kilo-vscode/src/agent-manager/project/wiring.ts index 181b09e8411a..c830f3aac1dc 100644 --- a/packages/kilo-vscode/src/agent-manager/project/wiring.ts +++ b/packages/kilo-vscode/src/agent-manager/project/wiring.ts @@ -60,7 +60,12 @@ export function createProjectWiring(opts: { opts.host.unregisterProjectRoutes(id) opts.removed?.(id) }, - deps: { log: opts.output, git: opts.git, worktreePool: () => opts.host.worktreePool() }, + deps: { + log: opts.output, + git: opts.git, + worktreePool: () => opts.host.worktreePool(), + sized: (ctx) => opts.pushState(ctx), + }, }) const messages: ProjectMessageDeps = { registry, diff --git a/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts b/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts index 53f501aab45c..17e46a6c6306 100644 --- a/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts +++ b/packages/kilo-vscode/src/agent-manager/provider-lifecycle.ts @@ -15,6 +15,7 @@ import { plan, type Start } from "./creation-plan" import { copyEnvFiles } from "./env-copy" import { runWorktreeSetupScript } from "./setup-script-task" import { broken } from "./worktree-reconcile" +import type { PanelContext } from "./host" export async function runLifecycleSetup( input: Parameters[0], @@ -135,6 +136,29 @@ export interface LifecycleHost { log: (...args: unknown[]) => void } +/** + * Build the `sessions` sub-object of {@link LifecycleHost} from the panel and its supporting state. + * + * Every call routes through the panel's `SessionProvider` (or is a safe no-op without one), closing + * a directory-scoped browser session first so a session move or removal never leaves a stale browser + * tab pointed at a directory it no longer owns. + */ +export function lifecycleSessions( + panel: PanelContext | undefined, + browserLifecycle: { close: (sessionId: string) => void } | undefined, + panelSessions: Set, +): LifecycleHost["sessions"] { + return { + register: (session) => panel?.sessions.registerSession(session), + clearDirectory: (sid) => (browserLifecycle?.close(sid), panel?.sessions.clearSessionDirectory(sid)), + setSessionDirectory: (sid, dir) => (browserLifecycle?.close(sid), panel?.sessions.setSessionDirectory(sid, dir)), + registerSessionRoute: (ref, dir, gen) => panel?.sessions.registerSessionRoute?.(ref, dir, gen), + directories: () => panel?.sessions.getSessionDirectories(), + abort: (ids) => panel?.sessions.abortSessions(ids) ?? Promise.resolve(), + forget: (sid) => void panelSessions.delete(sid), + } +} + /** Create a new worktree with an auto-created first session. */ export async function createLifecycleWorktree( ctx: ProjectContext, diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 1456757cf4d0..c8141e9a4a7d 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -633,6 +633,13 @@ interface CleanOrphanDirectoriesIn { paths: string[] } +/** Reveal an orphaned directory in the OS file manager. */ +interface RevealPathIn { + type: "agentManager.revealPath" + projectId?: string + path: string +} + interface PromoteSessionIn { type: "agentManager.promoteSession" projectId?: string @@ -1200,6 +1207,7 @@ export type AgentManagerInMessage = | RemoveStaleWorktreeIn | RestoreWorktreeIn | CleanOrphanDirectoriesIn + | RevealPathIn | PromoteSessionIn | OpenLocallyIn | OpenSessionLocallyIn diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts index 3e2de1de9b92..6d4f0f4173c0 100644 --- a/packages/kilo-vscode/src/agent-manager/vscode-host.ts +++ b/packages/kilo-vscode/src/agent-manager/vscode-host.ts @@ -344,6 +344,27 @@ export class VscodeHost implements Host { void vscode.window.showErrorMessage(msg) } + notify(kind: "info" | "warning" | "error", msg: string): void { + if (kind === "info") void vscode.window.showInformationMessage(msg) + else if (kind === "warning") void vscode.window.showWarningMessage(msg) + else void vscode.window.showErrorMessage(msg) + } + + revealInOS(path: string): void { + if (vscode.env.remoteName) { + console.warn(`[Kilo New] Cannot reveal ${path} in the OS file manager on a remote workspace`) + return + } + void vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(path)) + } + + async withProgress(title: string, task: (cancelled: () => boolean) => Promise): Promise { + return await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title, cancellable: true }, + (_progress, token) => task(() => token.isCancellationRequested), + ) + } + async openDocument(path: string): Promise { try { const doc = await vscode.workspace.openTextDocument(path) diff --git a/packages/kilo-vscode/src/agent-manager/worktree-health.ts b/packages/kilo-vscode/src/agent-manager/worktree-health.ts index 807799aed7a0..cb7d95b3b499 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-health.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-health.ts @@ -26,7 +26,9 @@ export function healthPayload( // Paths, not just a count: the confirmation dialog has to show exactly what will be deleted, and // the host re-validates every path against the current orphan set before removing anything. The // kind travels with them because "nothing here is tracked by git" is only true for a `leftover`: a - // `broken` orphan still holds a checkout, and its files can exist nowhere else. + // `broken` orphan still holds a checkout, and its files can exist nowhere else. `bytes` rides along + // too, filled in asynchronously by orphans/sizing.ts — absent until that pass lands, at which point + // a fresh push carries it to the webview without this function needing to know sizing exists. return { worktreeHealth: health, orphanDirectories: report.orphans } } diff --git a/packages/kilo-vscode/src/agent-manager/worktree-reconcile.ts b/packages/kilo-vscode/src/agent-manager/worktree-reconcile.ts index 81dc5519445d..bf5288e3a081 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-reconcile.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-reconcile.ts @@ -42,6 +42,16 @@ export type OrphanDirectory = { path: string /** `broken` still has a `.git` file; `leftover` is a bare directory, e.g. only `.kilo-dev/`. */ kind: "broken" | "leftover" + /** Apparent size in bytes, filled in asynchronously by orphans/sizing.ts. Absent until it lands. */ + bytes?: number + /** + * True once a size pass has finished with this directory, whether or not it produced a number. + * + * `bytes` alone cannot answer "is a size still coming?": a directory that could not be walked + * (permissions, or it vanished mid-pass) never gets one, and the UI would otherwise claim to be + * calculating forever. `sized` without `bytes` means the answer is in and it is "unknown". + */ + sized?: boolean } export type WorktreeHealthReport = { diff --git a/packages/kilo-vscode/src/agent-manager/worktree-recovery.ts b/packages/kilo-vscode/src/agent-manager/worktree-recovery.ts index ba52c74ad080..d07a30cd14e9 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-recovery.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-recovery.ts @@ -9,6 +9,7 @@ * - clean: delete directories under `.kilo/worktrees/` that no worktree claims */ +import { pauseOrphanSizes, resumeOrphanSizes } from "./orphans/sizing" import type { ProjectContext } from "./project/context" import type { WorktreeHealthReport } from "./worktree-reconcile" @@ -26,11 +27,22 @@ export interface RecoveryHost { * badges that do not move, which is the symptom the recovery action was clicked to end. */ refresh: (worktreeId: string) => void + /** Reveal a path in the OS file manager. A no-op on hosts where that is meaningless. */ + reveal: (path: string) => void + /** Tear down backend PTYs/instance state rooted at a worktree before its directory is staged. */ + teardown: (root: string, path: string) => Promise + /** Remove a worktree's snapshot repository. */ + removeSnapshot: (root: string, path: string) => Promise + /** Run a cancellable background task behind a progress notification. */ + withProgress: (title: string, task: (cancelled: () => boolean) => Promise) => Promise + /** Show the batch-delete completion notification. */ + notifyResult: (kind: "info" | "warning" | "error", message: string) => void } export type RecoveryMessage = | { type: "agentManager.restoreWorktree"; worktreeId: string } | { type: "agentManager.cleanOrphanDirectories"; paths: string[] } + | { type: "agentManager.revealPath"; path: string } /** Dispatch a recovery message for the active project. */ export async function handleRecovery( @@ -38,6 +50,10 @@ export async function handleRecovery( ctx: ProjectContext | undefined, host: RecoveryHost, ): Promise { + if (m.type === "agentManager.revealPath") { + host.reveal(m.path) + return null + } if (!ctx) return null if (m.type === "agentManager.restoreWorktree") await restoreWorktree(ctx, host, m.worktreeId) if (m.type === "agentManager.cleanOrphanDirectories") await cleanOrphans(ctx, host, m.paths) @@ -67,32 +83,99 @@ export async function restoreWorktree(ctx: ProjectContext, host: RecoveryHost, w /** * Delete orphaned directories under `.kilo/worktrees/`. * - * Only paths the last reconcile classified as orphans are accepted, and the manager re-checks that - * git does not track them, so a live worktree cannot be deleted through this path even if the - * webview sends a stale list. + * Every path is re-validated against a *fresh* reconcile — not the possibly-stale `ctx.report` the + * dialog was built from — before it is touched, so a directory the worktree pool just claimed in the + * meantime can never be deleted through this path even if the webview sends a stale selection. + * + * Runs behind `host.withProgress` so the caller (a webview message handler) returns immediately: the + * dialog closes instantly, and the actual teardown + delete + snapshot cleanup happens in the + * background. Cancelling stops the loop from starting new paths; a path already handed to + * `detachOrphanDirectory` keeps reaping regardless, since the rename already happened. + * + * Size walking is paused for the whole delete and resumed after it. Sizing a folder that is about to + * be renamed away is wasted I/O — potentially gigabytes of it — and the pass would be discarded on + * arrival anyway. Resuming before the closing reconcile is what re-measures whatever is still there, + * once, instead of once per deleted folder. */ export async function cleanOrphans(ctx: ProjectContext, host: RecoveryHost, paths: string[]): Promise { - const known = new Set(ctx.report?.orphans.map((orphan) => orphan.path) ?? []) const manager = ctx.worktreeManager() + const sizeBefore = new Map((ctx.report?.orphans ?? []).map((orphan) => [orphan.path, orphan.bytes] as const)) let removed = 0 - for (const target of paths) { - if (!known.has(target)) { - host.log(`Ignored cleanup for a path that is not a known orphan: ${target}`) - continue - } - const failure = await manager - .removeOrphanDirectory(target) - .then(() => undefined) - .catch((error: unknown) => (error instanceof Error ? error.message : String(error))) - if (failure) { - host.log(`Failed to remove orphaned directory ${target}: ${failure}`) - host.post({ type: "error", projectId: ctx.id, message: `Could not remove ${target}: ${failure}` }) - continue - } - removed++ + let failed = 0 + let removedBytes = 0 + let sizeKnown = true + pauseOrphanSizes(ctx) + try { + await host.withProgress("Removing leftover worktree folders", async (cancelled) => { + const fresh = await host.reconcile(ctx) + const known = new Set(fresh?.orphans.map((orphan) => orphan.path) ?? []) + for (const target of paths) { + if (cancelled()) break + if (!known.has(target)) { + host.log(`Ignored cleanup for a path that is not a known orphan: ${target}`) + continue + } + // Teardown/snapshot failures log and continue: the directory is still gone either way, and a + // backend that never had state for it (or already cleaned it up) is not a reason to refuse. + await host.teardown(ctx.root, target).catch((error: unknown) => { + host.log(`Failed to tear down backend state for ${target}: ${error}`) + }) + const failure = await manager + .detachOrphanDirectory(target) + .then(() => undefined) + .catch((error: unknown) => (error instanceof Error ? error.message : String(error))) + if (failure) { + host.log(`Failed to remove orphaned directory ${target}: ${failure}`) + host.post({ type: "error", projectId: ctx.id, message: `Could not remove ${target}: ${failure}` }) + failed++ + continue + } + await host.removeSnapshot(ctx.root, target) + removed++ + const bytes = sizeBefore.get(target) + if (bytes === undefined) sizeKnown = false + else removedBytes += bytes + } + }) + } finally { + resumeOrphanSizes(ctx) } - if (removed === 0) return + // Unconditional: this refreshes the report after failures *and* is what restarts the size pass that + // was paused above, for the leftovers that are still on disk. await host.reconcile(ctx) - host.push() + if (removed > 0) host.push() + notifyCleanupResult(host, removed, failed, paths.length, sizeKnown ? removedBytes : undefined) host.log(`Removed ${removed} orphaned worktree director${removed === 1 ? "y" : "ies"}`) } + +function notifyCleanupResult( + host: RecoveryHost, + removed: number, + failed: number, + total: number, + bytes: number | undefined, +): void { + const size = bytes !== undefined ? ` (${formatBytes(bytes)})` : "" + if (failed === 0 && removed === total) { + host.notifyResult("info", `Removed ${removed} folder${removed === 1 ? "" : "s"}${size}`) + return + } + if (removed > 0) { + host.notifyResult("warning", `Removed ${removed} of ${total}, ${failed} failed`) + return + } + host.notifyResult("error", `Failed to remove ${total} folder${total === 1 ? "" : "s"}`) +} + +/** Apparent size formatted for a completion notification, e.g. `1.2 GB`. */ +function formatBytes(bytes: number): string { + const units = ["B", "KB", "MB", "GB", "TB"] + let value = bytes + let unit = 0 + while (value >= 1024 && unit < units.length - 1) { + value /= 1024 + unit++ + } + const precision = unit === 0 ? 0 : 1 + return `${value.toFixed(precision)} ${units[unit]}` +} diff --git a/packages/kilo-vscode/tests/fixtures/orphan-help-toggle.tsx b/packages/kilo-vscode/tests/fixtures/orphan-help-toggle.tsx new file mode 100644 index 000000000000..3ba5be88223a --- /dev/null +++ b/packages/kilo-vscode/tests/fixtures/orphan-help-toggle.tsx @@ -0,0 +1,33 @@ +import assert from "node:assert/strict" +import { createSignal } from "solid-js" +import { harness } from "./comment-harness" + +const { node, mount, wait } = await harness<{ type: string }>() +const { OrphanHelp } = await import("../../webview-ui/agent-manager/orphans/OrphanDialog") +const [expanded, setExpanded] = createSignal(false) +const release = mount(() => setExpanded((prev) => !prev)} />) +try { + await wait() + const list = () => document.querySelector(".am-orphan-help-list") + const toggle = node(".am-orphan-help-toggle") + + assert.equal(list(), null, "bullets are hidden while collapsed") + assert.equal(toggle.textContent, "Show more") + assert.equal(toggle.getAttribute("aria-expanded"), "false") + + toggle.click() + await wait() + + assert.ok(list(), "bullets appear once expanded") + assert.equal(list()?.children.length, 3) + assert.equal(toggle.textContent, "Show less") + assert.equal(toggle.getAttribute("aria-expanded"), "true") + + toggle.click() + await wait() + + assert.equal(list(), null, "bullets collapse again on a second click") + assert.equal(toggle.textContent, "Show more") +} finally { + release() +} diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 385711de6c70..0fcc865252c2 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -11,7 +11,10 @@ import { describe, it, expect } from "bun:test" import fs from "node:fs" import path from "node:path" import { Project, SyntaxKind } from "ts-morph" +import { createProjectWiring } from "../../src/agent-manager/project/wiring" import { WorktreeImporter } from "../../src/agent-manager/worktree-importer" +import type { GitOps } from "../../src/agent-manager/GitOps" +import type { Host } from "../../src/agent-manager/host" const ROOT = path.resolve(import.meta.dir, "../..") const KILO_PROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts") @@ -67,7 +70,8 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/ProjectActions.tsx"), path.join(ROOT, "webview-ui/agent-manager/ProjectRowActions.tsx"), path.join(ROOT, "webview-ui/agent-manager/SidebarBody.tsx"), - path.join(ROOT, "webview-ui/agent-manager/OrphanNotice.tsx"), + path.join(ROOT, "webview-ui/agent-manager/orphans/OrphanNotice.tsx"), + path.join(ROOT, "webview-ui/agent-manager/orphans/OrphanDialog.tsx"), path.join(ROOT, "webview-ui/agent-manager/Skeleton.tsx"), path.join(ROOT, "webview-ui/agent-manager/TabBar.tsx"), path.join(ROOT, "webview-ui/agent-manager/ClosableTab.tsx"), @@ -226,6 +230,114 @@ describe("Agent Manager edit preview", () => { }) }) +describe("Agent Manager leftover worktree folders", () => { + const bodies = [ + path.join(ROOT, "webview-ui/agent-manager/SidebarBody.tsx"), + path.join(ROOT, "webview-ui/agent-manager/ProjectSidebarBody.tsx"), + ] + + it("puts the notice above the worktrees instead of below them", () => { + for (const file of bodies) { + const source = fs.readFileSync(file, "utf-8") + const list = source.indexOf('
') + const notice = source.indexOf("}", list) + expect(list, `${path.basename(file)} renders the worktree list`).toBeGreaterThan(-1) + expect(notice, `${path.basename(file)} renders the orphan notice inside the list`).toBeGreaterThan(list) + expect(notice, `${path.basename(file)} renders the orphan notice before the first worktree`).toBeLessThan(first) + } + }) + + it("offers the cleanup as an ordinary action button, not a ghost affordance", () => { + const source = fs.readFileSync(path.join(ROOT, "webview-ui/agent-manager/orphans/OrphanNotice.tsx"), "utf-8") + expect(source).toContain(' -
- } - > -
    - - {(orphan) => ( -
  • - {orphan.path} - - {t("agentManager.orphans.checkout")} - -
  • - )} -
    -
-
- {checkouts() > 0 - ? t("agentManager.orphans.confirmCheckout", { count: checkouts() }) - : t("agentManager.orphans.confirm")} -
-
- - -
- - - - ) -} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index f9e21375d65a..887eae013a2a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -21,8 +21,10 @@ import type { LanguageContextValue } from "../src/context/language" import { LocalActivity } from "../src/components/shared/ActivityIcon" import { label, type Activity } from "../src/utils/session-activity" import { useVSCode } from "../src/context/vscode" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" import SectionHeader from "./SectionHeader" -import { OrphanNotice } from "./OrphanNotice" +import { OrphanNotice } from "./orphans/OrphanNotice" +import { OrphanDialog } from "./orphans/OrphanDialog" import { WorktreeItem, actionable } from "./WorktreeItem" import { useBaseUpdate } from "./update-from-base" import { StatsSkeleton, WorktreeSkeleton } from "./Skeleton" @@ -76,6 +78,7 @@ interface Props { /** Permanent real sidebar body for one expanded project. */ export const ProjectSidebarBody: Component = (props) => { const vscode = useVSCode() + const dialog = useDialog() const updateBase = useBaseUpdate() const store = props.store ?? createProjectStore(props.project.id) if (!props.store) { @@ -125,6 +128,18 @@ export const ProjectSidebarBody: Component = (props) => { const sidebarOrder = createMemo(() => projectSidebarOrder(top(), sorted(), sections(), members)) const post = (message: Record) => vscode.postMessage({ ...message, projectId: props.project.id } as never) + const openOrphanDialog = () => + dialog.show(() => ( + post({ type: "agentManager.revealPath", path })} + onDelete={(paths) => { + post({ type: "agentManager.cleanOrphanDirectories", paths }) + dialog.close() + }} + onClose={() => dialog.close()} + /> + )) const localState = () => props.activityFor(null) const selectAfterDelete = (id: string) => { @@ -401,6 +416,7 @@ export const ProjectSidebarBody: Component = (props) => {
+ }> = (props) => { - post({ type: "agentManager.cleanOrphanDirectories", paths })} - />
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx index 098ad63f82ba..905336decd49 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx @@ -28,7 +28,9 @@ import { beginPromptMentionDrop, endPromptMentionDrop } from "../src/utils/promp import { outsideSidebar, sectionAwareDetector } from "./section-dnd" import { ConstrainDragXAxis } from "./constrain-drag-x" import { useVSCode } from "../src/context/vscode" -import { OrphanNotice } from "./OrphanNotice" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" +import { OrphanNotice } from "./orphans/OrphanNotice" +import { OrphanDialog } from "./orphans/OrphanDialog" import type { OrphanDirectory } from "./project/store" import SectionHeader from "./SectionHeader" import { SidebarSectionHeader } from "./SidebarSectionHeader" @@ -114,7 +116,20 @@ export const SidebarBody: Component = (props) => { buildTopLevelItems(props.sections(), ungrouped(), sorted(), props.sidebarWorktreeOrder()), ) const vscode = useVSCode() + const dialog = useDialog() const updateBase = useBaseUpdate() + const openOrphanDialog = () => + dialog.show(() => ( + vscode.postMessage({ type: "agentManager.revealPath", path })} + onDelete={(paths) => { + vscode.postMessage({ type: "agentManager.cleanOrphanDirectories", paths }) + dialog.close() + }} + onClose={() => dialog.close()} + /> + )) const localState = () => props.activityFor(null) // Captured at worktree drag start so a release outside the sidebar, or a drop // on the prompt, can undo a reorder applied while passing over sibling rows. @@ -222,6 +237,7 @@ export const SidebarBody: Component = (props) => { } />
+ }>
@@ -497,10 +513,6 @@ export const SidebarBody: Component = (props) => { - vscode.postMessage({ type: "agentManager.cleanOrphanDirectories", paths })} - />
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 3766443baf60..8ddb8b927f53 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -930,41 +930,181 @@ html[data-theme="kilo-vscode"] white-space: normal; } -/* Leftover worktree folders: informational until the user asks for the cleanup. */ +/* Leftover worktree folders: informational until the user asks for the cleanup. Sits at the top of + the worktree list, so the warning tint stays on the surface while the text reads as ordinary copy — + a yellow headline next to the real worktrees looks like an error the list is reporting. */ .am-orphan-notice { - margin: 6px 8px 2px; - padding: 6px 8px; + margin: 2px 8px 6px; + padding: 8px; border: 1px solid color-mix(in oklab, var(--text-warning, #f59e0b) 35%, transparent); border-radius: 4px; background: color-mix(in oklab, var(--text-warning, #f59e0b) 8%, transparent); - font-size: var(--font-size-small); + font-size: var(--kilo-font-size-13); } .am-orphan-notice-head { display: flex; align-items: center; - gap: 4px; + gap: 6px; +} + +.am-orphan-notice-head [data-slot="icon-svg"] { color: var(--text-warning, #f59e0b); + flex-shrink: 0; } .am-orphan-notice-title { - font-weight: 600; + color: var(--text-base); } -.am-orphan-notice-body { - margin-top: 2px; - color: var(--text-muted, inherit); +.am-orphan-notice-calculating { + margin-left: 4px; + color: var(--text-weak); +} + +.am-orphan-notice-actions { + display: flex; + justify-content: flex-end; + gap: 4px; + margin-top: 6px; +} + +/* Leftover worktree folders dialog. Chrome follows the VS Code dialog conventions the shared Dialog + already sets up: the explanation lives in the header description slot, and the body carries the + same 20px gutter as the header instead of bleeding text into the dialog edge. */ +/* The shared dialog gives its description slot an inline `margin-left: -4px`, so the gutters here are + 16/24 to land on the header's symmetric 20px. */ +.am-orphan-dialog-root [data-slot="dialog-description"] { + display: block; + padding: 0 16px 12px 24px; + margin-top: -8px; + color: var(--text-weak); + font-size: var(--kilo-font-size-13); +} + +.am-orphan-help-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; + padding-left: 18px; } -.am-orphan-notice-paths { - margin: 4px 0 0; - padding-left: 14px; - max-height: 88px; +.am-orphan-help-list > span { + display: list-item; + list-style: disc outside; +} + +.am-orphan-help-toggle { + display: block; + margin-top: 6px; + padding: 0; + border: none; + background: none; + font: inherit; + font-size: var(--kilo-font-size-13); + color: var(--vscode-textLink-foreground); + cursor: pointer; +} + +.am-orphan-help-toggle:hover { + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; +} + +.am-orphan-help-toggle:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + +.am-orphan-dialog-root [data-slot="dialog-body"] { + padding: 0 20px 20px; +} + +.am-orphan-dialog { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + gap: 12px; +} + +/* The scroll region is the element that grows, so the footer stays pinned to the dialog bottom and + the table itself can keep normal table layout for its header/row alignment. */ +.am-orphan-scroll { + flex: 1; + min-height: 0; overflow-y: auto; - color: var(--text-muted, inherit); + border: 1px solid var(--border-weak-base); + border-radius: 2px; } -.am-orphan-notice-paths li { +.am-orphan-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + font-size: var(--kilo-font-size-13); +} + +.am-orphan-table th { + position: sticky; + top: 0; + z-index: 1; + text-align: left; + padding: 5px 8px; + background: var(--background-base); + color: var(--text-weak); + font-weight: normal; + border-bottom: 1px solid var(--border-weak-base); +} + +.am-orphan-table td { + padding: 3px 8px; + text-align: left; + border-bottom: 1px solid color-mix(in oklab, var(--border-weak-base) 40%, transparent); + vertical-align: middle; +} + +.am-orphan-table tbody tr:last-child td { + border-bottom: none; +} + +.am-orphan-table tr[data-orphan-kind="broken"] .am-orphan-path { + color: var(--text-warning, #f59e0b); +} + +.am-orphan-col-check { + width: 26px; + padding-right: 0; +} + +.am-orphan-col-check [data-component="checkbox"] { + margin: 0; +} + +/* The path is the only column that absorbs slack: everything else is sized to its own content so a + short flag never sits marooned in the middle of an over-wide cell. */ +.am-orphan-col-path { + min-width: 0; +} + +.am-orphan-col-size { + width: 88px; + white-space: nowrap; + color: var(--text-weak); +} + +.am-orphan-col-contents { + width: 200px; +} + +.am-orphan-col-reveal { + width: 30px; + padding-left: 0; +} + +.am-orphan-path { + display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -972,21 +1112,36 @@ html[data-theme="kilo-vscode"] text-align: left; } -.am-orphan-notice-paths li[data-orphan-kind="broken"] { +.am-orphan-contents { + display: inline-flex; + align-items: center; + gap: 4px; color: var(--text-warning, #f59e0b); + white-space: nowrap; } -.am-orphan-notice-kind { - margin-left: 4px; - font-style: italic; - opacity: 0.85; +.am-orphan-dialog-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; } -.am-orphan-notice-actions { +.am-orphan-dialog-summary { display: flex; - justify-content: flex-end; - gap: 4px; - margin-top: 4px; + flex-direction: column; + gap: 2px; + color: var(--text-weak); + font-size: var(--kilo-font-size-13); +} + +.am-orphan-dialog-checkouts { + color: var(--text-warning, #f59e0b); +} + +.am-orphan-dialog-actions { + display: flex; + gap: 8px; } .am-worktree-spinner { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 2b16eb0b887b..7d80ebbcd407 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -479,13 +479,33 @@ export const dict = { "لم يستجب Git أو GitHub CLI في الوقت المناسب. تم إيقاف الاستعلام لهذا الـ worktree مؤقتًا وسيُعاد المحاولة.", "agentManager.worktree.restore": "استعادة الـ worktree", "agentManager.worktree.removeKeepSessions": "إزالة مع الاحتفاظ بالجلسات", - "agentManager.orphans.title": "مجلدات worktree متبقية", - "agentManager.orphans.summary": "‏{{count}} مجلد ضمن .kilo/worktrees ليست worktrees من git.", - "agentManager.orphans.clean": "تنظيف المجلدات المتبقية", - "agentManager.orphans.confirm": "حذف هذه المجلدات نهائيًا؟ لا شيء هنا يتتبعه git.", + "agentManager.orphans.resolve": "حل…", + "agentManager.orphans.summaryCount": "{{count}} مجلد worktree متبقٍ", + "agentManager.orphans.summarySize": "{{count}} مجلد worktree متبقٍ · {{size}}", + "agentManager.orphans.calculating": "جارٍ حساب الحجم…", + "agentManager.orphans.sizeUnknown": "غير معروف", + "agentManager.orphans.dialogTitle": "مجلدات worktree متبقية", + "agentManager.orphans.helpIntro": + "يحفظ Kilo كل worktree يُنشئه داخل مجلد .kilo/worktrees في هذا المستودع. المجلدات أدناه موجودة في ذلك المجلد، لكن git لا يسرد أيًا منها باعتباره worktree، لذا لم يعد أي شيء يستخدمها.", + "agentManager.orphans.helpCheckout": + "المجلد المُعلَّم بأنه يحتوي على نسخة عمل git لا يزال يضم مدخل .git بداخله وقد يحتوي على عمل غير مُودَع في commit. لذلك تُترك هذه المجلدات غير محددة؛ افتح أحدها وتحقق منه قبل حذفه.", + "agentManager.orphans.helpCauses": + "تنتج المجلدات المتبقية عادةً عن عملية حذف توقفت في منتصفها، أو worktree أُزيل من خارج Kilo، أو أداة كتبت في المجلد بعد إزالته. أما عمليات الحذف التي لا تزال قيد التنفيذ فلا تظهر هنا.", + "agentManager.orphans.helpDelete": + "يؤدي الحذف إلى إزالة المجلدات المحددة من القرص نهائيًا دون المرور بسلة المحذوفات. ولا يتأثر أي فرع أو أي worktree نشط. تمثل الأحجام المساحة التي يشغلها كل مجلد على القرص في الوقت الحالي.", + "agentManager.orphans.helpMore": "إظهار المزيد", + "agentManager.orphans.helpLess": "إظهار أقل", + "agentManager.orphans.columnPath": "المسار", + "agentManager.orphans.columnSize": "الحجم", + "agentManager.orphans.columnContents": "المحتوى", + "agentManager.orphans.checkoutWarning": "يحتوي على نسخة عمل git", + "agentManager.orphans.footerSelected": "{{count}} محدد · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} لا تزال تحتوي على نسخة عمل git", + "agentManager.orphans.reveal": "إظهار في نظام التشغيل", + "agentManager.orphans.revealMac": "إظهار في Finder", + "agentManager.orphans.revealWindows": "إظهار في مستكشف الملفات", + "agentManager.orphans.revealLinux": "إظهار في الملفات", + "agentManager.orphans.deleteButton": "حذف {{count}} مجلد ({{size}})", "agentManager.orphans.cancel": "إلغاء", - "agentManager.orphans.checkout": "يحتوي على نسخة عمل git", - "agentManager.orphans.confirmCheckout": - "حذف هذه المجلدات نهائيًا؟ ما زال {{count}} منها يحتوي على نسخة عمل git قد تتضمن تغييرات غير مُودعة.", "agentManager.error.title": "خطأ في Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index ba1f83f22982..731a1ee5ccae 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -491,13 +491,33 @@ export const dict = { "O Git ou o GitHub CLI não respondeu em tempo. A consulta deste worktree está pausada e será repetida.", "agentManager.worktree.restore": "Restaurar worktree", "agentManager.worktree.removeKeepSessions": "Remover e manter sessões", - "agentManager.orphans.title": "Pastas de worktree remanescentes", - "agentManager.orphans.summary": "{{count}} pasta(s) em .kilo/worktrees não são worktrees do git.", - "agentManager.orphans.clean": "Limpar pastas remanescentes", - "agentManager.orphans.confirm": "Excluir estas pastas permanentemente? Nada aqui é rastreado pelo git.", + "agentManager.orphans.resolve": "Resolver…", + "agentManager.orphans.summaryCount": "{{count}} pasta(s) de worktree remanescente(s)", + "agentManager.orphans.summarySize": "{{count}} pasta(s) de worktree remanescente(s) · {{size}}", + "agentManager.orphans.calculating": "calculando o tamanho…", + "agentManager.orphans.sizeUnknown": "desconhecido", + "agentManager.orphans.dialogTitle": "Pastas de worktree remanescentes", + "agentManager.orphans.helpIntro": + "O Kilo mantém cada worktree que cria dentro da pasta .kilo/worktrees deste repositório. As pastas abaixo estão nessa pasta, mas o git não lista nenhuma delas como worktree, portanto nada mais as usa.", + "agentManager.orphans.helpCheckout": + "Uma pasta sinalizada como contendo um checkout do git ainda tem uma entrada .git dentro dela e pode guardar trabalho sem commit. Essas pastas ficam desmarcadas, então abra uma e verifique-a antes de excluí-la.", + "agentManager.orphans.helpCauses": + "As pastas remanescentes geralmente vêm de uma exclusão interrompida, de um worktree removido fora do Kilo ou de uma ferramenta que gravou na pasta depois que ela foi removida. Exclusões que ainda estão em andamento não aparecem aqui.", + "agentManager.orphans.helpDelete": + "A exclusão remove as pastas selecionadas do disco definitivamente, sem passar pela Lixeira. Nenhuma branch e nenhum worktree ativo é afetado. Os tamanhos são o espaço que cada pasta ocupa no disco neste momento.", + "agentManager.orphans.helpMore": "Mostrar mais", + "agentManager.orphans.helpLess": "Mostrar menos", + "agentManager.orphans.columnPath": "Caminho", + "agentManager.orphans.columnSize": "Tamanho", + "agentManager.orphans.columnContents": "Conteúdo", + "agentManager.orphans.checkoutWarning": "contém um checkout do git", + "agentManager.orphans.footerSelected": "{{count}} selecionada(s) · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} ainda contêm um checkout do git", + "agentManager.orphans.reveal": "Mostrar no sistema", + "agentManager.orphans.revealMac": "Mostrar no Finder", + "agentManager.orphans.revealWindows": "Mostrar no Explorador", + "agentManager.orphans.revealLinux": "Mostrar nos Arquivos", + "agentManager.orphans.deleteButton": "Excluir {{count}} pastas ({{size}})", "agentManager.orphans.cancel": "Cancelar", - "agentManager.orphans.checkout": "contém um checkout do git", - "agentManager.orphans.confirmCheckout": - "Excluir estas pastas permanentemente? {{count}} ainda contêm um checkout do git que pode ter alterações não commitadas.", "agentManager.error.title": "Erro do Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index 7a8ab06bbd9f..5772efba72ef 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -487,13 +487,33 @@ export const dict = { "Git ili GitHub CLI nije odgovorio na vrijeme. Provjera ovog worktreeja je pauzirana i biće ponovljena.", "agentManager.worktree.restore": "Vrati worktree", "agentManager.worktree.removeKeepSessions": "Ukloni, zadrži sesije", - "agentManager.orphans.title": "Zaostali worktree folderi", - "agentManager.orphans.summary": "{{count}} folder(a) u .kilo/worktrees nisu git worktreeji.", - "agentManager.orphans.clean": "Očisti zaostale foldere", - "agentManager.orphans.confirm": "Trajno izbrisati ove foldere? Ništa ovdje git ne prati.", + "agentManager.orphans.resolve": "Riješi…", + "agentManager.orphans.summaryCount": "{{count}} zaostalih worktree foldera", + "agentManager.orphans.summarySize": "{{count}} zaostalih worktree foldera · {{size}}", + "agentManager.orphans.calculating": "računanje veličine…", + "agentManager.orphans.sizeUnknown": "nepoznato", + "agentManager.orphans.dialogTitle": "Zaostali worktree folderi", + "agentManager.orphans.helpIntro": + "Kilo čuva svaki worktree koji napravi u folderu .kilo/worktrees ovog repozitorija. Folderi ispod nalaze se u tom folderu, ali git ni jedan od njih ne navodi kao worktree, pa ih više ništa ne koristi.", + "agentManager.orphans.helpCheckout": + "Folder označen kao folder koji sadrži git checkout još ima .git zapis unutra i može sadržavati nekomitovan rad. Takvi folderi ostaju neodabrani, pa otvorite jedan i provjerite ga prije nego što ga izbrišete.", + "agentManager.orphans.helpCauses": + "Zaostali folderi najčešće nastaju zbog prekinutog brisanja, worktreeja uklonjenog izvan aplikacije Kilo ili alata koji je pisao u folder nakon što je uklonjen. Brisanja koja su još u toku nisu navedena ovdje.", + "agentManager.orphans.helpDelete": + "Brisanje trajno uklanja odabrane foldere s diska, bez prebacivanja u Korpu za smeće. Nijedna grana niti aktivan worktree se ne dira. Veličine pokazuju koliko prostora svaki folder trenutno zauzima na disku.", + "agentManager.orphans.helpMore": "Prikaži još", + "agentManager.orphans.helpLess": "Prikaži manje", + "agentManager.orphans.columnPath": "Putanja", + "agentManager.orphans.columnSize": "Veličina", + "agentManager.orphans.columnContents": "Sadržaj", + "agentManager.orphans.checkoutWarning": "sadrži git checkout", + "agentManager.orphans.footerSelected": "{{count}} odabrano · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} još sadrži git checkout", + "agentManager.orphans.reveal": "Prikaži u sistemu", + "agentManager.orphans.revealMac": "Prikaži u Finderu", + "agentManager.orphans.revealWindows": "Prikaži u Exploreru", + "agentManager.orphans.revealLinux": "Prikaži u datotekama", + "agentManager.orphans.deleteButton": "Izbriši {{count}} foldera ({{size}})", "agentManager.orphans.cancel": "Otkaži", - "agentManager.orphans.checkout": "sadrži git checkout", - "agentManager.orphans.confirmCheckout": - "Trajno izbrisati ove foldere? {{count}} još sadrži git checkout koji može imati necommitane promjene.", "agentManager.error.title": "Greška Agent Managera", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 2f19020fba32..d1d8aae70f06 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -488,13 +488,33 @@ export const dict = { "Git eller GitHub CLI svarede ikke i tid. Forespørgsler for dette worktree er sat på pause og prøves igen.", "agentManager.worktree.restore": "Gendan worktree", "agentManager.worktree.removeKeepSessions": "Fjern, behold sessioner", - "agentManager.orphans.title": "Efterladte worktree-mapper", - "agentManager.orphans.summary": "{{count}} mappe(r) under .kilo/worktrees er ikke git-worktrees.", - "agentManager.orphans.clean": "Ryd op i efterladte mapper", - "agentManager.orphans.confirm": "Slet disse mapper permanent? Intet her spores af git.", + "agentManager.orphans.resolve": "Løs…", + "agentManager.orphans.summaryCount": "{{count}} efterladt(e) worktree-mappe(r)", + "agentManager.orphans.summarySize": "{{count}} efterladt(e) worktree-mappe(r) · {{size}}", + "agentManager.orphans.calculating": "beregner størrelse…", + "agentManager.orphans.sizeUnknown": "ukendt", + "agentManager.orphans.dialogTitle": "Efterladte worktree-mapper", + "agentManager.orphans.helpIntro": + "Kilo gemmer alle worktrees, den opretter, i mappen .kilo/worktrees i dette repository. Mapperne nedenfor ligger i den mappe, men git viser ingen af dem som et worktree, så der er ikke længere noget, der bruger dem.", + "agentManager.orphans.helpCheckout": + "En mappe, der er markeret som indeholdende et git-checkout, har stadig en .git-post indeni og kan indeholde arbejde, der ikke er committet. Sådanne mapper er ikke valgt, så åbn en af dem og tjek den, før du sletter den.", + "agentManager.orphans.helpCauses": + "Efterladte mapper skyldes typisk en sletning, der blev afbrudt, et worktree, der blev fjernet uden om Kilo, eller et værktøj, der skrev i mappen, efter at den blev fjernet. Sletninger, der stadig kører, vises ikke her.", + "agentManager.orphans.helpDelete": + "Sletning fjerner de valgte mapper permanent fra disken uden om Papirkurven. Ingen branch og intet aktivt worktree bliver berørt. Størrelserne er den plads, hver mappe fylder på disken lige nu.", + "agentManager.orphans.helpMore": "Vis mere", + "agentManager.orphans.helpLess": "Vis mindre", + "agentManager.orphans.columnPath": "Sti", + "agentManager.orphans.columnSize": "Størrelse", + "agentManager.orphans.columnContents": "Indhold", + "agentManager.orphans.checkoutWarning": "indeholder et git-checkout", + "agentManager.orphans.footerSelected": "{{count}} valgt · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} indeholder stadig et git-checkout", + "agentManager.orphans.reveal": "Vis i systemet", + "agentManager.orphans.revealMac": "Vis i Finder", + "agentManager.orphans.revealWindows": "Vis i Stifinder", + "agentManager.orphans.revealLinux": "Vis i Filer", + "agentManager.orphans.deleteButton": "Slet {{count}} mapper ({{size}})", "agentManager.orphans.cancel": "Annuller", - "agentManager.orphans.checkout": "indeholder et git-checkout", - "agentManager.orphans.confirmCheckout": - "Slet disse mapper permanent? {{count}} indeholder stadig et git-checkout, der kan have ikke-committede ændringer.", "agentManager.error.title": "Agent Manager-fejl", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 891d0a41143b..427a47069ef9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -496,13 +496,33 @@ export const dict = { "Git oder GitHub CLI hat nicht rechtzeitig geantwortet. Das Abfragen dieses Worktrees pausiert und wird wiederholt.", "agentManager.worktree.restore": "Worktree wiederherstellen", "agentManager.worktree.removeKeepSessions": "Entfernen, Sitzungen behalten", - "agentManager.orphans.title": "Übrig gebliebene Worktree-Ordner", - "agentManager.orphans.summary": "{{count}} Ordner unter .kilo/worktrees sind keine git-Worktrees.", - "agentManager.orphans.clean": "Übrige Ordner aufräumen", - "agentManager.orphans.confirm": "Diese Ordner endgültig löschen? Nichts davon wird von git verfolgt.", + "agentManager.orphans.resolve": "Beheben…", + "agentManager.orphans.summaryCount": "{{count}} übrig gebliebene(r) Worktree-Ordner", + "agentManager.orphans.summarySize": "{{count}} übrig gebliebene(r) Worktree-Ordner · {{size}}", + "agentManager.orphans.calculating": "Größe wird berechnet…", + "agentManager.orphans.sizeUnknown": "unbekannt", + "agentManager.orphans.dialogTitle": "Übrig gebliebene Worktree-Ordner", + "agentManager.orphans.helpIntro": + "Kilo legt jeden Worktree, den es erstellt, im Ordner .kilo/worktrees dieses Repositorys ab. Die folgenden Ordner liegen in diesem Ordner, aber git führt keinen davon als Worktree, sodass sie von nichts mehr verwendet werden.", + "agentManager.orphans.helpCheckout": + "Ein Ordner, der als git-Checkout markiert ist, enthält noch einen .git-Eintrag und möglicherweise nicht committete Arbeit. Diese Ordner bleiben nicht ausgewählt; öffnen Sie einen davon und prüfen Sie ihn, bevor Sie ihn löschen.", + "agentManager.orphans.helpCauses": + "Übrig gebliebene Ordner entstehen meist durch eine abgebrochene Löschung, einen außerhalb von Kilo entfernten Worktree oder ein Tool, das nach dem Entfernen noch in den Ordner geschrieben hat. Löschvorgänge, die noch laufen, werden hier nicht aufgeführt.", + "agentManager.orphans.helpDelete": + "Beim Löschen werden die ausgewählten Ordner endgültig von der Festplatte entfernt, ohne den Papierkorb zu durchlaufen. Kein Branch und kein aktiver Worktree wird angetastet. Die Größen zeigen, wie viel Speicherplatz jeder Ordner aktuell belegt.", + "agentManager.orphans.helpMore": "Mehr anzeigen", + "agentManager.orphans.helpLess": "Weniger anzeigen", + "agentManager.orphans.columnPath": "Pfad", + "agentManager.orphans.columnSize": "Größe", + "agentManager.orphans.columnContents": "Inhalt", + "agentManager.orphans.checkoutWarning": "enthält einen git-Checkout", + "agentManager.orphans.footerSelected": "{{count}} ausgewählt · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} enthalten noch einen git-Checkout", + "agentManager.orphans.reveal": "Im Dateisystem anzeigen", + "agentManager.orphans.revealMac": "Im Finder anzeigen", + "agentManager.orphans.revealWindows": "Im Explorer anzeigen", + "agentManager.orphans.revealLinux": "In Dateien anzeigen", + "agentManager.orphans.deleteButton": "{{count}} Ordner löschen ({{size}})", "agentManager.orphans.cancel": "Abbrechen", - "agentManager.orphans.checkout": "enthält einen git-Checkout", - "agentManager.orphans.confirmCheckout": - "Diese Ordner endgültig löschen? {{count}} enthalten noch einen git-Checkout mit möglicherweise nicht committeten Änderungen.", "agentManager.error.title": "Agent-Manager-Fehler", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 5618aef5fde8..ab248b6a1e52 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -84,14 +84,34 @@ export const dict = { "Git or GitHub CLI did not answer in time. Polling is paused for this worktree and will retry.", "agentManager.worktree.restore": "Restore worktree", "agentManager.worktree.removeKeepSessions": "Remove, keep sessions", - "agentManager.orphans.title": "Leftover worktree folders", - "agentManager.orphans.summary": "{{count}} folder(s) under .kilo/worktrees are not git worktrees.", - "agentManager.orphans.clean": "Clean up leftover folders", - "agentManager.orphans.confirm": "Delete these folders permanently? Nothing here is tracked by git.", + "agentManager.orphans.resolve": "Resolve…", + "agentManager.orphans.summaryCount": "{{count}} leftover worktree folder(s)", + "agentManager.orphans.summarySize": "{{count}} leftover worktree folder(s) · {{size}}", + "agentManager.orphans.calculating": "calculating size…", + "agentManager.orphans.sizeUnknown": "unknown", + "agentManager.orphans.dialogTitle": "Leftover worktree folders", + "agentManager.orphans.helpIntro": + "Kilo keeps every worktree it creates inside the .kilo/worktrees folder of this repository. The folders below are in that folder, but git does not list any of them as a worktree, so nothing is using them anymore.", + "agentManager.orphans.helpCheckout": + "A folder flagged as containing a git checkout still has a .git entry inside and may hold uncommitted work. Those are left unselected, so open one and check it before you delete it.", + "agentManager.orphans.helpCauses": + "Leftovers usually come from a deletion that was interrupted, a worktree removed outside Kilo, or a tool that wrote into the folder after it was removed. Deletions that are still running are not listed here.", + "agentManager.orphans.helpDelete": + "Deleting removes the selected folders from disk for good, without going through the Trash. No branch and no active worktree is touched. Sizes are what each folder takes up on disk right now.", + "agentManager.orphans.helpMore": "Show more", + "agentManager.orphans.helpLess": "Show less", + "agentManager.orphans.columnPath": "Path", + "agentManager.orphans.columnSize": "Size", + "agentManager.orphans.columnContents": "Contents", + "agentManager.orphans.checkoutWarning": "contains a git checkout", + "agentManager.orphans.footerSelected": "{{count}} selected · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} still contain a git checkout", + "agentManager.orphans.reveal": "Reveal in OS", + "agentManager.orphans.revealMac": "Show in Finder", + "agentManager.orphans.revealWindows": "Show in Explorer", + "agentManager.orphans.revealLinux": "Show in Files", + "agentManager.orphans.deleteButton": "Delete {{count}} folders ({{size}})", "agentManager.orphans.cancel": "Cancel", - "agentManager.orphans.checkout": "contains a git checkout", - "agentManager.orphans.confirmCheckout": - "Delete these folders permanently? {{count}} still contain a git checkout that may hold uncommitted work.", "agentManager.worktree.doubleClickRename": "Double-click to rename", "agentManager.worktree.versions": "{{count}} versions", "agentManager.worktree.advancedOptions": "Advanced worktree options", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index 790d8bec8422..76ea00aedf87 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -496,13 +496,33 @@ export const dict = { "Git o GitHub CLI no respondió a tiempo. El sondeo de este worktree está en pausa y se reintentará.", "agentManager.worktree.restore": "Restaurar worktree", "agentManager.worktree.removeKeepSessions": "Eliminar y conservar sesiones", - "agentManager.orphans.title": "Carpetas de worktree sobrantes", - "agentManager.orphans.summary": "{{count}} carpeta(s) en .kilo/worktrees no son worktrees de git.", - "agentManager.orphans.clean": "Limpiar carpetas sobrantes", - "agentManager.orphans.confirm": "¿Eliminar estas carpetas permanentemente? Nada aquí está rastreado por git.", + "agentManager.orphans.resolve": "Resolver…", + "agentManager.orphans.summaryCount": "{{count}} carpeta(s) de worktree sobrante(s)", + "agentManager.orphans.summarySize": "{{count}} carpeta(s) de worktree sobrante(s) · {{size}}", + "agentManager.orphans.calculating": "calculando el tamaño…", + "agentManager.orphans.sizeUnknown": "desconocido", + "agentManager.orphans.dialogTitle": "Carpetas de worktree sobrantes", + "agentManager.orphans.helpIntro": + "Kilo guarda cada worktree que crea dentro de la carpeta .kilo/worktrees de este repositorio. Las carpetas siguientes están en esa carpeta, pero git no incluye ninguna de ellas como worktree, así que ya no se usan.", + "agentManager.orphans.helpCheckout": + "Una carpeta marcada como que contiene un checkout de git todavía tiene una entrada .git en su interior y puede guardar trabajo sin confirmar. Esas carpetas quedan sin seleccionar, así que abre una y revísala antes de eliminarla.", + "agentManager.orphans.helpCauses": + "Los restos suelen provenir de una eliminación que se interrumpió, de un worktree eliminado fuera de Kilo o de una herramienta que escribió en la carpeta después de que se eliminara. Las eliminaciones que aún están en curso no aparecen aquí.", + "agentManager.orphans.helpDelete": + "Al eliminar, las carpetas seleccionadas se borran del disco de forma definitiva, sin pasar por la Papelera. No se toca ninguna rama ni ningún worktree activo. Los tamaños son el espacio que cada carpeta ocupa en el disco ahora mismo.", + "agentManager.orphans.helpMore": "Mostrar más", + "agentManager.orphans.helpLess": "Mostrar menos", + "agentManager.orphans.columnPath": "Ruta", + "agentManager.orphans.columnSize": "Tamaño", + "agentManager.orphans.columnContents": "Contenido", + "agentManager.orphans.checkoutWarning": "contiene un checkout de git", + "agentManager.orphans.footerSelected": "{{count}} seleccionada(s) · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} aún contienen un checkout de git", + "agentManager.orphans.reveal": "Mostrar en el sistema", + "agentManager.orphans.revealMac": "Mostrar en Finder", + "agentManager.orphans.revealWindows": "Mostrar en el Explorador", + "agentManager.orphans.revealLinux": "Mostrar en Archivos", + "agentManager.orphans.deleteButton": "Eliminar {{count}} carpetas ({{size}})", "agentManager.orphans.cancel": "Cancelar", - "agentManager.orphans.checkout": "contiene un checkout de git", - "agentManager.orphans.confirmCheckout": - "¿Eliminar estas carpetas permanentemente? {{count}} aún contienen un checkout de git que puede tener cambios sin confirmar.", "agentManager.error.title": "Error de Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts index f0aaa9db9081..89531fe86cd0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -488,13 +488,33 @@ export const dict = { "Git یا GitHub CLI به‌موقع پاسخ نداد. بررسی این worktree موقتاً متوقف شد و دوباره تلاش می‌شود.", "agentManager.worktree.restore": "بازیابی worktree", "agentManager.worktree.removeKeepSessions": "حذف با نگه‌داشتن نشست‌ها", - "agentManager.orphans.title": "پوشه‌های باقی‌مانده worktree", - "agentManager.orphans.summary": "‏{{count}} پوشه در .kilo/worktrees، worktree گیت نیستند.", - "agentManager.orphans.clean": "پاک‌سازی پوشه‌های باقی‌مانده", - "agentManager.orphans.confirm": "این پوشه‌ها برای همیشه حذف شوند؟ هیچ‌چیز اینجا توسط گیت پیگیری نمی‌شود.", + "agentManager.orphans.resolve": "حل…", + "agentManager.orphans.summaryCount": "{{count}} پوشه باقی‌مانده worktree", + "agentManager.orphans.summarySize": "{{count}} پوشه باقی‌مانده worktree · {{size}}", + "agentManager.orphans.calculating": "در حال محاسبه حجم…", + "agentManager.orphans.sizeUnknown": "نامشخص", + "agentManager.orphans.dialogTitle": "پوشه‌های باقی‌مانده worktree", + "agentManager.orphans.helpIntro": + "Kilo هر worktree‌ای که می‌سازد را داخل پوشه .kilo/worktrees همین مخزن نگه می‌دارد. پوشه‌های زیر در همان پوشه قرار دارند، اما git هیچ‌کدام را به عنوان worktree فهرست نمی‌کند، بنابراین دیگر چیزی از آن‌ها استفاده نمی‌کند.", + "agentManager.orphans.helpCheckout": + "پوشه‌ای که به‌عنوان دارای checkout گیت علامت خورده است، همچنان یک ورودی .git در خود دارد و ممکن است کار commit‌نشده داشته باشد. این پوشه‌ها انتخاب‌نشده باقی می‌مانند؛ پیش از حذف، یکی را باز کنید و بررسی کنید.", + "agentManager.orphans.helpCauses": + "باقی‌مانده‌ها معمولاً از حذفی که نیمه‌کاره مانده، worktree‌ای که بیرون از Kilo حذف شده، یا ابزاری که پس از حذف پوشه در آن نوشته است به وجود می‌آیند. حذف‌هایی که هنوز در حال اجرا هستند اینجا فهرست نمی‌شوند.", + "agentManager.orphans.helpDelete": + "حذف، پوشه‌های انتخاب‌شده را برای همیشه از دیسک پاک می‌کند و آن‌ها را به سبد بازیافت نمی‌فرستد. هیچ شاخه و هیچ worktree فعالی دست‌کاری نمی‌شود. حجم‌ها نشان می‌دهند هر پوشه در این لحظه چه مقدار فضای دیسک را اشغال کرده است.", + "agentManager.orphans.helpMore": "نمایش بیشتر", + "agentManager.orphans.helpLess": "نمایش کمتر", + "agentManager.orphans.columnPath": "مسیر", + "agentManager.orphans.columnSize": "حجم", + "agentManager.orphans.columnContents": "محتوا", + "agentManager.orphans.checkoutWarning": "شامل یک checkout گیت است", + "agentManager.orphans.footerSelected": "{{count}} انتخاب‌شده · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} هنوز شامل checkout گیت هستند", + "agentManager.orphans.reveal": "نمایش در سیستم‌عامل", + "agentManager.orphans.revealMac": "نمایش در Finder", + "agentManager.orphans.revealWindows": "نمایش در Explorer", + "agentManager.orphans.revealLinux": "نمایش در فایل‌ها", + "agentManager.orphans.deleteButton": "حذف {{count}} پوشه ({{size}})", "agentManager.orphans.cancel": "لغو", - "agentManager.orphans.checkout": "شامل یک checkout گیت است", - "agentManager.orphans.confirmCheckout": - "این پوشه‌ها برای همیشه حذف شوند؟ {{count}} پوشه هنوز شامل checkout گیت است که ممکن است تغییرات ثبت‌نشده داشته باشد.", "agentManager.error.title": "خطای Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index a76a2f5f7dc2..06ad37ea9ada 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -496,13 +496,33 @@ export const dict = { "Git ou GitHub CLI n'a pas répondu à temps. L'interrogation de ce worktree est suspendue et sera réessayée.", "agentManager.worktree.restore": "Restaurer le worktree", "agentManager.worktree.removeKeepSessions": "Supprimer en gardant les sessions", - "agentManager.orphans.title": "Dossiers de worktree résiduels", - "agentManager.orphans.summary": "{{count}} dossier(s) sous .kilo/worktrees ne sont pas des worktrees git.", - "agentManager.orphans.clean": "Nettoyer les dossiers résiduels", - "agentManager.orphans.confirm": "Supprimer définitivement ces dossiers ? Rien ici n'est suivi par git.", + "agentManager.orphans.resolve": "Résoudre…", + "agentManager.orphans.summaryCount": "{{count}} dossier(s) de worktree résiduel(s)", + "agentManager.orphans.summarySize": "{{count}} dossier(s) de worktree résiduel(s) · {{size}}", + "agentManager.orphans.calculating": "calcul de la taille…", + "agentManager.orphans.sizeUnknown": "inconnue", + "agentManager.orphans.dialogTitle": "Dossiers de worktree résiduels", + "agentManager.orphans.helpIntro": + "Kilo place chaque worktree qu'il crée dans le dossier .kilo/worktrees de ce dépôt. Les dossiers ci-dessous se trouvent dans ce dossier, mais git n'en répertorie aucun comme worktree : plus rien ne les utilise.", + "agentManager.orphans.helpCheckout": + "Un dossier signalé comme contenant un checkout git possède encore une entrée .git et peut renfermer du travail non commité. Ces dossiers restent désélectionnés : ouvrez-en un et vérifiez-le avant de le supprimer.", + "agentManager.orphans.helpCauses": + "Les résidus proviennent généralement d'une suppression interrompue, d'un worktree supprimé en dehors de Kilo ou d'un outil qui a écrit dans le dossier après sa suppression. Les suppressions encore en cours ne sont pas listées ici.", + "agentManager.orphans.helpDelete": + "La suppression retire définitivement du disque les dossiers sélectionnés, sans passer par la Corbeille. Aucune branche ni aucun worktree actif n'est touché. Les tailles correspondent à l'espace disque occupé par chaque dossier en ce moment.", + "agentManager.orphans.helpMore": "Afficher plus", + "agentManager.orphans.helpLess": "Afficher moins", + "agentManager.orphans.columnPath": "Chemin", + "agentManager.orphans.columnSize": "Taille", + "agentManager.orphans.columnContents": "Contenu", + "agentManager.orphans.checkoutWarning": "contient un checkout git", + "agentManager.orphans.footerSelected": "{{count}} sélectionné(s) · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} contiennent encore un checkout git", + "agentManager.orphans.reveal": "Afficher dans l'explorateur du système", + "agentManager.orphans.revealMac": "Afficher dans le Finder", + "agentManager.orphans.revealWindows": "Afficher dans l'Explorateur", + "agentManager.orphans.revealLinux": "Afficher dans les fichiers", + "agentManager.orphans.deleteButton": "Supprimer {{count}} dossiers ({{size}})", "agentManager.orphans.cancel": "Annuler", - "agentManager.orphans.checkout": "contient un checkout git", - "agentManager.orphans.confirmCheckout": - "Supprimer définitivement ces dossiers ? {{count}} contiennent encore un checkout git pouvant avoir des modifications non commitées.", "agentManager.error.title": "Erreur Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 34a455488f44..f91d2146fbbd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -502,13 +502,33 @@ export const dict = { "Git o GitHub CLI non ha risposto in tempo. Il polling di questo worktree è in pausa e verrà ritentato.", "agentManager.worktree.restore": "Ripristina worktree", "agentManager.worktree.removeKeepSessions": "Rimuovi, mantieni le sessioni", - "agentManager.orphans.title": "Cartelle di worktree rimaste", - "agentManager.orphans.summary": "{{count}} cartella(e) in .kilo/worktrees non sono worktree git.", - "agentManager.orphans.clean": "Pulisci le cartelle rimaste", - "agentManager.orphans.confirm": "Eliminare definitivamente queste cartelle? Nulla qui è tracciato da git.", + "agentManager.orphans.resolve": "Risolvi…", + "agentManager.orphans.summaryCount": "{{count}} cartella(e) di worktree rimasta(e)", + "agentManager.orphans.summarySize": "{{count}} cartella(e) di worktree rimasta(e) · {{size}}", + "agentManager.orphans.calculating": "calcolo delle dimensioni…", + "agentManager.orphans.sizeUnknown": "sconosciuta", + "agentManager.orphans.dialogTitle": "Cartelle di worktree rimaste", + "agentManager.orphans.helpIntro": + "Kilo tiene ogni worktree che crea nella cartella .kilo/worktrees di questa repository. Le cartelle elencate qui sotto si trovano in quella cartella, ma git non ne elenca nessuna come worktree, quindi non sono più usate da niente.", + "agentManager.orphans.helpCheckout": + "Una cartella segnalata come contenente un checkout git ha ancora una voce .git al suo interno e può contenere lavoro non committato. Quelle cartelle restano deselezionate, quindi aprine una e controllala prima di eliminarla.", + "agentManager.orphans.helpCauses": + "Le cartelle rimaste derivano di solito da un'eliminazione interrotta, da un worktree rimosso fuori da Kilo o da uno strumento che ha scritto nella cartella dopo la rimozione. Le eliminazioni ancora in corso non sono elencate qui.", + "agentManager.orphans.helpDelete": + "L'eliminazione rimuove definitivamente dal disco le cartelle selezionate, senza passare dal Cestino. Nessun branch e nessun worktree attivo viene toccato. Le dimensioni indicano lo spazio che ogni cartella occupa sul disco in questo momento.", + "agentManager.orphans.helpMore": "Mostra altro", + "agentManager.orphans.helpLess": "Mostra meno", + "agentManager.orphans.columnPath": "Percorso", + "agentManager.orphans.columnSize": "Dimensione", + "agentManager.orphans.columnContents": "Contenuto", + "agentManager.orphans.checkoutWarning": "contiene un checkout git", + "agentManager.orphans.footerSelected": "{{count}} selezionate · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} contengono ancora un checkout git", + "agentManager.orphans.reveal": "Mostra nel sistema", + "agentManager.orphans.revealMac": "Mostra nel Finder", + "agentManager.orphans.revealWindows": "Mostra in Esplora file", + "agentManager.orphans.revealLinux": "Mostra nei file", + "agentManager.orphans.deleteButton": "Elimina {{count}} cartelle ({{size}})", "agentManager.orphans.cancel": "Annulla", - "agentManager.orphans.checkout": "contiene un checkout git", - "agentManager.orphans.confirmCheckout": - "Eliminare definitivamente queste cartelle? {{count}} contengono ancora un checkout git che potrebbe avere modifiche non committate.", "agentManager.error.title": "Errore di Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 290dc0cf2564..4218000dc061 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -490,13 +490,33 @@ export const dict = { "Git または GitHub CLI が時間内に応答しませんでした。この worktree のポーリングを一時停止し、後で再試行します。", "agentManager.worktree.restore": "worktree を復元", "agentManager.worktree.removeKeepSessions": "削除してセッションを保持", - "agentManager.orphans.title": "残された worktree フォルダー", - "agentManager.orphans.summary": ".kilo/worktrees 配下の {{count}} 個のフォルダーは git worktree ではありません。", - "agentManager.orphans.clean": "残ったフォルダーを整理", - "agentManager.orphans.confirm": "これらのフォルダーを完全に削除しますか? git が追跡しているものはありません。", + "agentManager.orphans.resolve": "解決…", + "agentManager.orphans.summaryCount": "{{count}} 件の残された worktree フォルダー", + "agentManager.orphans.summarySize": "{{count}} 件の残された worktree フォルダー · {{size}}", + "agentManager.orphans.calculating": "サイズを計算中…", + "agentManager.orphans.sizeUnknown": "不明", + "agentManager.orphans.dialogTitle": "残された worktree フォルダー", + "agentManager.orphans.helpIntro": + "Kilo は作成した worktree をすべて、このリポジトリの .kilo/worktrees フォルダー内に保存します。以下のフォルダーはそのフォルダー内にありますが、git はいずれも worktree として認識していないため、どこからも使用されていません。", + "agentManager.orphans.helpCheckout": + "git チェックアウトが残っていると表示されているフォルダーには、まだ .git エントリがあり、コミットされていない作業が含まれている可能性があります。これらは選択されていないため、削除する前に開いて内容を確認してください。", + "agentManager.orphans.helpCauses": + "残されたフォルダーは通常、中断された削除、Kilo の外で削除された worktree、または削除後にフォルダーへ書き込んだツールが原因で発生します。実行中の削除はここには表示されません。", + "agentManager.orphans.helpDelete": + "削除すると、選択したフォルダーはゴミ箱を経由せずにディスクから完全に削除されます。ブランチや使用中の worktree には影響しません。サイズは各フォルダーが現在ディスク上で占めている容量です。", + "agentManager.orphans.helpMore": "さらに表示", + "agentManager.orphans.helpLess": "表示を減らす", + "agentManager.orphans.columnPath": "パス", + "agentManager.orphans.columnSize": "サイズ", + "agentManager.orphans.columnContents": "内容", + "agentManager.orphans.checkoutWarning": "git チェックアウトが残っています", + "agentManager.orphans.footerSelected": "{{count}} 件選択 · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} 件はまだ git チェックアウトを含んでいます", + "agentManager.orphans.reveal": "OSで表示", + "agentManager.orphans.revealMac": "Finderで表示", + "agentManager.orphans.revealWindows": "エクスプローラーで表示", + "agentManager.orphans.revealLinux": "ファイルで表示", + "agentManager.orphans.deleteButton": "{{count}} 個のフォルダーを削除 ({{size}})", "agentManager.orphans.cancel": "キャンセル", - "agentManager.orphans.checkout": "git チェックアウトが残っています", - "agentManager.orphans.confirmCheckout": - "これらのフォルダーを完全に削除しますか?{{count}} 個には git チェックアウトが残っており、コミットされていない変更がある可能性があります。", "agentManager.error.title": "Agent Manager エラー", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index eef8a8516de3..67ef7890a570 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -481,13 +481,33 @@ export const dict = { "Git 또는 GitHub CLI가 제때 응답하지 않았습니다. 이 worktree의 폴링을 일시 중지했으며 다시 시도합니다.", "agentManager.worktree.restore": "worktree 복원", "agentManager.worktree.removeKeepSessions": "제거하고 세션 유지", - "agentManager.orphans.title": "남은 worktree 폴더", - "agentManager.orphans.summary": ".kilo/worktrees 아래 폴더 {{count}}개가 git worktree가 아닙니다.", - "agentManager.orphans.clean": "남은 폴더 정리", - "agentManager.orphans.confirm": "이 폴더를 영구히 삭제할까요? git이 추적하는 항목은 없습니다.", + "agentManager.orphans.resolve": "해결…", + "agentManager.orphans.summaryCount": "남은 worktree 폴더 {{count}}개", + "agentManager.orphans.summarySize": "남은 worktree 폴더 {{count}}개 · {{size}}", + "agentManager.orphans.calculating": "크기 계산 중…", + "agentManager.orphans.sizeUnknown": "알 수 없음", + "agentManager.orphans.dialogTitle": "남은 worktree 폴더", + "agentManager.orphans.helpIntro": + "Kilo는 생성한 모든 worktree를 이 저장소의 .kilo/worktrees 폴더 안에 보관합니다. 아래 폴더들은 그 폴더 안에 있지만 git이 어느 것도 worktree로 표시하지 않으므로 더 이상 사용되지 않습니다.", + "agentManager.orphans.helpCheckout": + "git 체크아웃이 남아 있다고 표시된 폴더에는 아직 .git 항목이 있으며 커밋하지 않은 작업이 남아 있을 수 있습니다. 이러한 폴더는 선택되지 않은 상태로 두므로 삭제하기 전에 열어서 확인하세요.", + "agentManager.orphans.helpCauses": + "남은 폴더는 보통 중단된 삭제, Kilo 외부에서 제거된 worktree 또는 폴더가 제거된 뒤에 그 안에 기록한 도구 때문에 생깁니다. 아직 진행 중인 삭제는 여기에 표시되지 않습니다.", + "agentManager.orphans.helpDelete": + "삭제하면 선택한 폴더가 휴지통을 거치지 않고 디스크에서 영구적으로 제거됩니다. 브랜치와 사용 중인 worktree는 변경되지 않습니다. 크기는 각 폴더가 현재 디스크에서 차지하는 용량입니다.", + "agentManager.orphans.helpMore": "더보기", + "agentManager.orphans.helpLess": "간략히 보기", + "agentManager.orphans.columnPath": "경로", + "agentManager.orphans.columnSize": "크기", + "agentManager.orphans.columnContents": "내용", + "agentManager.orphans.checkoutWarning": "git 체크아웃이 남아 있음", + "agentManager.orphans.footerSelected": "{{count}}개 선택됨 · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}}개에 여전히 git 체크아웃이 있습니다", + "agentManager.orphans.reveal": "OS에서 표시", + "agentManager.orphans.revealMac": "Finder에서 표시", + "agentManager.orphans.revealWindows": "탐색기에서 표시", + "agentManager.orphans.revealLinux": "파일 관리자에서 표시", + "agentManager.orphans.deleteButton": "폴더 {{count}}개 삭제 ({{size}})", "agentManager.orphans.cancel": "취소", - "agentManager.orphans.checkout": "git 체크아웃이 남아 있음", - "agentManager.orphans.confirmCheckout": - "이 폴더를 영구적으로 삭제할까요? {{count}}개에 아직 git 체크아웃이 남아 있어 커밋되지 않은 변경이 있을 수 있습니다.", "agentManager.error.title": "Agent Manager 오류", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 6fe7140b6798..1a076aa579cd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -496,13 +496,33 @@ export const dict = { "Git of GitHub CLI antwoordde niet op tijd. Het pollen van deze worktree is gepauzeerd en wordt opnieuw geprobeerd.", "agentManager.worktree.restore": "Worktree herstellen", "agentManager.worktree.removeKeepSessions": "Verwijderen, sessies behouden", - "agentManager.orphans.title": "Achtergebleven worktree-mappen", - "agentManager.orphans.summary": "{{count}} map(pen) in .kilo/worktrees zijn geen git-worktrees.", - "agentManager.orphans.clean": "Achtergebleven mappen opruimen", - "agentManager.orphans.confirm": "Deze mappen definitief verwijderen? Niets hiervan wordt door git gevolgd.", + "agentManager.orphans.resolve": "Oplossen…", + "agentManager.orphans.summaryCount": "{{count}} achtergebleven worktree-map(pen)", + "agentManager.orphans.summarySize": "{{count}} achtergebleven worktree-map(pen) · {{size}}", + "agentManager.orphans.calculating": "grootte berekenen…", + "agentManager.orphans.sizeUnknown": "onbekend", + "agentManager.orphans.dialogTitle": "Achtergebleven worktree-mappen", + "agentManager.orphans.helpIntro": + "Kilo bewaart elke worktree die het aanmaakt in de map .kilo/worktrees van deze repository. De mappen hieronder staan in die map, maar git vermeldt er geen enkele als worktree, dus niets gebruikt ze nog.", + "agentManager.orphans.helpCheckout": + "Een map die is gemarkeerd als map met een git-checkout heeft nog een .git-item erin en kan werk bevatten dat niet is gecommit. Die mappen blijven niet geselecteerd, dus open er een en controleer die voordat je hem verwijdert.", + "agentManager.orphans.helpCauses": + "Restanten komen meestal van een verwijdering die is afgebroken, een worktree die buiten Kilo is verwijderd of een tool die na het verwijderen nog in de map heeft geschreven. Verwijderingen die nog bezig zijn, staan hier niet.", + "agentManager.orphans.helpDelete": + "Verwijderen haalt de geselecteerde mappen definitief van de schijf, zonder via de Prullenbak te gaan. Er wordt geen branch en geen actieve worktree aangeraakt. De groottes zijn de ruimte die elke map op dit moment op de schijf inneemt.", + "agentManager.orphans.helpMore": "Meer weergeven", + "agentManager.orphans.helpLess": "Minder weergeven", + "agentManager.orphans.columnPath": "Pad", + "agentManager.orphans.columnSize": "Grootte", + "agentManager.orphans.columnContents": "Inhoud", + "agentManager.orphans.checkoutWarning": "bevat een git-checkout", + "agentManager.orphans.footerSelected": "{{count}} geselecteerd · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} bevatten nog een git-checkout", + "agentManager.orphans.reveal": "Weergeven in besturingssysteem", + "agentManager.orphans.revealMac": "Weergeven in Finder", + "agentManager.orphans.revealWindows": "Weergeven in Verkenner", + "agentManager.orphans.revealLinux": "Weergeven in Bestanden", + "agentManager.orphans.deleteButton": "{{count}} mappen verwijderen ({{size}})", "agentManager.orphans.cancel": "Annuleren", - "agentManager.orphans.checkout": "bevat een git-checkout", - "agentManager.orphans.confirmCheckout": - "Deze mappen permanent verwijderen? {{count}} bevatten nog een git-checkout die niet-gecommitte wijzigingen kan hebben.", "agentManager.error.title": "Agent Manager-fout", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 857cccf4e594..4e4107a3b08f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -486,13 +486,33 @@ export const dict = { "Git eller GitHub CLI svarte ikke i tid. Spørringer for dette worktreet er satt på pause og prøves igjen.", "agentManager.worktree.restore": "Gjenopprett worktree", "agentManager.worktree.removeKeepSessions": "Fjern, behold økter", - "agentManager.orphans.title": "Gjenglemte worktree-mapper", - "agentManager.orphans.summary": "{{count}} mappe(r) under .kilo/worktrees er ikke git-worktrees.", - "agentManager.orphans.clean": "Rydd opp i gjenglemte mapper", - "agentManager.orphans.confirm": "Slette disse mappene permanent? Ingenting her spores av git.", + "agentManager.orphans.resolve": "Løs…", + "agentManager.orphans.summaryCount": "{{count}} gjenglemt(e) worktree-mappe(r)", + "agentManager.orphans.summarySize": "{{count}} gjenglemt(e) worktree-mappe(r) · {{size}}", + "agentManager.orphans.calculating": "beregner størrelse…", + "agentManager.orphans.sizeUnknown": "ukjent", + "agentManager.orphans.dialogTitle": "Gjenglemte worktree-mapper", + "agentManager.orphans.helpIntro": + "Kilo lagrer alle worktrees den oppretter, i mappen .kilo/worktrees i dette repositoriet. Mappene nedenfor ligger i den mappen, men git viser ingen av dem som et worktree, så det er ikke lenger noe som bruker dem.", + "agentManager.orphans.helpCheckout": + "En mappe som er merket med at den inneholder en git-utsjekking, har fortsatt en .git-oppføring inni og kan inneholde arbeid som ikke er committet. Slike mapper er ikke valgt, så åpne en av dem og sjekk den før du sletter den.", + "agentManager.orphans.helpCauses": + "Gjenglemte mapper skyldes vanligvis en sletting som ble avbrutt, et worktree som ble fjernet utenom Kilo, eller et verktøy som skrev til mappen etter at den ble fjernet. Slettinger som fortsatt pågår, vises ikke her.", + "agentManager.orphans.helpDelete": + "Sletting fjerner de valgte mappene permanent fra disken, uten å gå via Papirkurven. Ingen branch og ingen aktive worktrees blir berørt. Størrelsene viser hvor mye plass hver mappe bruker på disken akkurat nå.", + "agentManager.orphans.helpMore": "Vis mer", + "agentManager.orphans.helpLess": "Vis færre", + "agentManager.orphans.columnPath": "Bane", + "agentManager.orphans.columnSize": "Størrelse", + "agentManager.orphans.columnContents": "Innhold", + "agentManager.orphans.checkoutWarning": "inneholder en git-utsjekking", + "agentManager.orphans.footerSelected": "{{count}} valgt · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} inneholder fremdeles en git-utsjekking", + "agentManager.orphans.reveal": "Vis i systemet", + "agentManager.orphans.revealMac": "Vis i Finder", + "agentManager.orphans.revealWindows": "Vis i Filutforsker", + "agentManager.orphans.revealLinux": "Vis i Filer", + "agentManager.orphans.deleteButton": "Slett {{count}} mapper ({{size}})", "agentManager.orphans.cancel": "Avbryt", - "agentManager.orphans.checkout": "inneholder en git-utsjekking", - "agentManager.orphans.confirmCheckout": - "Slette disse mappene permanent? {{count}} inneholder fortsatt en git-utsjekking som kan ha ucommittede endringer.", "agentManager.error.title": "Agent Manager-feil", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 28f8f9c76096..c7b4dc327cf2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -487,13 +487,33 @@ export const dict = { "Git lub GitHub CLI nie odpowiedział na czas. Odpytywanie tego worktree jest wstrzymane i zostanie ponowione.", "agentManager.worktree.restore": "Przywróć worktree", "agentManager.worktree.removeKeepSessions": "Usuń, zachowaj sesje", - "agentManager.orphans.title": "Pozostałe foldery worktree", - "agentManager.orphans.summary": "{{count}} folder(ów) w .kilo/worktrees nie jest worktree gita.", - "agentManager.orphans.clean": "Wyczyść pozostałe foldery", - "agentManager.orphans.confirm": "Trwale usunąć te foldery? Nic tutaj nie jest śledzone przez gita.", + "agentManager.orphans.resolve": "Rozwiąż…", + "agentManager.orphans.summaryCount": "{{count}} pozostały(ch) folder(ów) worktree", + "agentManager.orphans.summarySize": "{{count}} pozostały(ch) folder(ów) worktree · {{size}}", + "agentManager.orphans.calculating": "obliczanie rozmiaru…", + "agentManager.orphans.sizeUnknown": "nieznany", + "agentManager.orphans.dialogTitle": "Pozostałe foldery worktree", + "agentManager.orphans.helpIntro": + "Kilo przechowuje każdy tworzony worktree w folderze .kilo/worktrees tego repozytorium. Poniższe foldery znajdują się w tym folderze, ale git nie wymienia żadnego z nich jako worktree, więc nic już ich nie używa.", + "agentManager.orphans.helpCheckout": + "Folder oznaczony jako zawierający checkout git nadal ma w środku wpis .git i może zawierać zmiany bez commita. Takie foldery pozostają niezaznaczone, więc otwórz jeden z nich i sprawdź go, zanim go usuniesz.", + "agentManager.orphans.helpCauses": + "Pozostałości zwykle wynikają z przerwanego usuwania, worktree usuniętego poza Kilo albo narzędzia, które zapisało coś w folderze po jego usunięciu. Usuwanie, które nadal trwa, nie jest tu wymieniane.", + "agentManager.orphans.helpDelete": + "Usunięcie trwale usuwa wybrane foldery z dysku, bez przenoszenia ich do Kosza. Żadna gałąź ani żaden aktywny worktree nie zostaje zmieniony. Rozmiary pokazują, ile miejsca każdy folder zajmuje na dysku w tej chwili.", + "agentManager.orphans.helpMore": "Pokaż więcej", + "agentManager.orphans.helpLess": "Pokaż mniej", + "agentManager.orphans.columnPath": "Ścieżka", + "agentManager.orphans.columnSize": "Rozmiar", + "agentManager.orphans.columnContents": "Zawartość", + "agentManager.orphans.checkoutWarning": "zawiera checkout git", + "agentManager.orphans.footerSelected": "wybrano {{count}} · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} wciąż zawiera checkout git", + "agentManager.orphans.reveal": "Pokaż w systemie", + "agentManager.orphans.revealMac": "Pokaż w Finderze", + "agentManager.orphans.revealWindows": "Pokaż w Eksploratorze", + "agentManager.orphans.revealLinux": "Pokaż w Menedżerze plików", + "agentManager.orphans.deleteButton": "Usuń {{count}} folderów ({{size}})", "agentManager.orphans.cancel": "Anuluj", - "agentManager.orphans.checkout": "zawiera checkout git", - "agentManager.orphans.confirmCheckout": - "Trwale usunąć te foldery? {{count}} nadal zawiera checkout git, w którym mogą być niezatwierdzone zmiany.", "agentManager.error.title": "Błąd Agent Managera", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 46a8d63e0660..cf543922a434 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -489,13 +489,33 @@ export const dict = { "Git или GitHub CLI не ответил вовремя. Опрос этого worktree приостановлен и будет повторён.", "agentManager.worktree.restore": "Восстановить worktree", "agentManager.worktree.removeKeepSessions": "Удалить, сохранив сессии", - "agentManager.orphans.title": "Оставшиеся папки worktree", - "agentManager.orphans.summary": "{{count}} папк(и) в .kilo/worktrees не являются git worktree.", - "agentManager.orphans.clean": "Очистить оставшиеся папки", - "agentManager.orphans.confirm": "Удалить эти папки безвозвратно? Git ничего здесь не отслеживает.", + "agentManager.orphans.resolve": "Решить…", + "agentManager.orphans.summaryCount": "{{count}} оставшихся папок worktree", + "agentManager.orphans.summarySize": "{{count}} оставшихся папок worktree · {{size}}", + "agentManager.orphans.calculating": "вычисление размера…", + "agentManager.orphans.sizeUnknown": "неизвестно", + "agentManager.orphans.dialogTitle": "Оставшиеся папки worktree", + "agentManager.orphans.helpIntro": + "Kilo хранит все создаваемые worktree в папке .kilo/worktrees этого репозитория. Папки ниже находятся в этой папке, но git не считает ни одну из них worktree, поэтому они больше не используются.", + "agentManager.orphans.helpCheckout": + "Папка, помеченная как содержащая рабочую копию git, всё ещё содержит запись .git и может хранить незакоммиченную работу. Такие папки остаются невыбранными, поэтому откройте одну из них и проверьте её перед удалением.", + "agentManager.orphans.helpCauses": + "Остатки обычно появляются из-за прерванного удаления, worktree, удалённого вне Kilo, или инструмента, который записал данные в папку после её удаления. Удаления, которые ещё выполняются, здесь не показываются.", + "agentManager.orphans.helpDelete": + "Удаление безвозвратно убирает выбранные папки с диска, минуя Корзину. Ни одна ветка и ни один активный worktree не затрагиваются. Размеры показывают, сколько места каждая папка занимает на диске сейчас.", + "agentManager.orphans.helpMore": "Показать ещё", + "agentManager.orphans.helpLess": "Показать меньше", + "agentManager.orphans.columnPath": "Путь", + "agentManager.orphans.columnSize": "Размер", + "agentManager.orphans.columnContents": "Содержимое", + "agentManager.orphans.checkoutWarning": "содержит рабочую копию git", + "agentManager.orphans.footerSelected": "{{count}} выбрано · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} всё ещё содержат рабочую копию git", + "agentManager.orphans.reveal": "Показать в ОС", + "agentManager.orphans.revealMac": "Показать в Finder", + "agentManager.orphans.revealWindows": "Показать в проводнике", + "agentManager.orphans.revealLinux": "Показать в файловом менеджере", + "agentManager.orphans.deleteButton": "Удалить {{count}} папок ({{size}})", "agentManager.orphans.cancel": "Отмена", - "agentManager.orphans.checkout": "содержит рабочую копию git", - "agentManager.orphans.confirmCheckout": - "Удалить эти папки безвозвратно? {{count}} всё ещё содержат рабочую копию git с возможными незакоммиченными изменениями.", "agentManager.error.title": "Ошибка Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 1928b7f9738a..a0dad286de3e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -478,13 +478,33 @@ export const dict = { "Git หรือ GitHub CLI ไม่ตอบกลับทันเวลา การตรวจสอบ worktree นี้ถูกหยุดชั่วคราวและจะลองใหม่", "agentManager.worktree.restore": "กู้คืน worktree", "agentManager.worktree.removeKeepSessions": "ลบแต่เก็บเซสชันไว้", - "agentManager.orphans.title": "โฟลเดอร์ worktree ที่ตกค้าง", - "agentManager.orphans.summary": "มี {{count}} โฟลเดอร์ใน .kilo/worktrees ที่ไม่ใช่ git worktree", - "agentManager.orphans.clean": "ล้างโฟลเดอร์ที่ตกค้าง", - "agentManager.orphans.confirm": "ลบโฟลเดอร์เหล่านี้อย่างถาวรหรือไม่? ไม่มีสิ่งใดที่ git ติดตามอยู่", + "agentManager.orphans.resolve": "แก้ไข…", + "agentManager.orphans.summaryCount": "โฟลเดอร์ worktree ที่ตกค้าง {{count}} รายการ", + "agentManager.orphans.summarySize": "โฟลเดอร์ worktree ที่ตกค้าง {{count}} รายการ · {{size}}", + "agentManager.orphans.calculating": "กำลังคำนวณขนาด…", + "agentManager.orphans.sizeUnknown": "ไม่ทราบ", + "agentManager.orphans.dialogTitle": "โฟลเดอร์ worktree ที่ตกค้าง", + "agentManager.orphans.helpIntro": + "Kilo จะเก็บ worktree ทุกรายการที่สร้างขึ้นไว้ในโฟลเดอร์ .kilo/worktrees ของที่เก็บนี้ โฟลเดอร์ด้านล่างอยู่ในโฟลเดอร์นั้น แต่ git ไม่ได้แสดงรายการใดเป็น worktree จึงไม่มีสิ่งใดใช้งานโฟลเดอร์เหล่านี้อีกต่อไป", + "agentManager.orphans.helpCheckout": + "โฟลเดอร์ที่ถูกทำเครื่องหมายว่ามี git checkout อยู่ยังมีรายการ .git อยู่ข้างในและอาจมีงานที่ยังไม่ได้ commit จึงไม่ได้ถูกเลือกไว้ ให้เปิดขึ้นมาตรวจสอบก่อนลบ", + "agentManager.orphans.helpCauses": + "โฟลเดอร์ตกค้างมักเกิดจากการลบที่ถูกขัดจังหวะ worktree ที่ถูกลบจากภายนอก Kilo หรือเครื่องมือที่เขียนข้อมูลลงในโฟลเดอร์หลังจากที่ถูกลบไปแล้ว การลบที่ยังทำงานอยู่จะไม่แสดงที่นี่", + "agentManager.orphans.helpDelete": + "การลบจะนำโฟลเดอร์ที่เลือกออกจากดิสก์อย่างถาวรโดยไม่ผ่านถังขยะ ไม่มีแบรนช์และไม่มี worktree ที่ใช้งานอยู่ถูกแตะต้อง ขนาดที่แสดงคือพื้นที่ที่แต่ละโฟลเดอร์ใช้บนดิสก์ในขณะนี้", + "agentManager.orphans.helpMore": "แสดงเพิ่มเติม", + "agentManager.orphans.helpLess": "แสดงน้อยลง", + "agentManager.orphans.columnPath": "พาธ", + "agentManager.orphans.columnSize": "ขนาด", + "agentManager.orphans.columnContents": "เนื้อหา", + "agentManager.orphans.checkoutWarning": "มี git checkout อยู่", + "agentManager.orphans.footerSelected": "เลือกแล้ว {{count}} รายการ · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} รายการยังมี git checkout อยู่", + "agentManager.orphans.reveal": "แสดงในระบบปฏิบัติการ", + "agentManager.orphans.revealMac": "แสดงใน Finder", + "agentManager.orphans.revealWindows": "แสดงใน Explorer", + "agentManager.orphans.revealLinux": "แสดงในตัวจัดการไฟล์", + "agentManager.orphans.deleteButton": "ลบ {{count}} โฟลเดอร์ ({{size}})", "agentManager.orphans.cancel": "ยกเลิก", - "agentManager.orphans.checkout": "มี git checkout อยู่", - "agentManager.orphans.confirmCheckout": - "ลบโฟลเดอร์เหล่านี้อย่างถาวรหรือไม่? {{count}} รายการยังมี git checkout ที่อาจมีการแก้ไขที่ยังไม่ได้คอมมิต", "agentManager.error.title": "ข้อผิดพลาด Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 8c712dc179e4..2977b8f3a731 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -496,14 +496,33 @@ export const dict = { "Git veya GitHub CLI zamanında yanıt vermedi. Bu worktree için sorgulama duraklatıldı ve yeniden denenecek.", "agentManager.worktree.restore": "Worktree'yi geri yükle", "agentManager.worktree.removeKeepSessions": "Kaldır, oturumları koru", - "agentManager.orphans.title": "Artakalan worktree klasörleri", - "agentManager.orphans.summary": ".kilo/worktrees altındaki {{count}} klasör git worktree değil.", - "agentManager.orphans.clean": "Artakalan klasörleri temizle", - "agentManager.orphans.confirm": - "Bu klasörler kalıcı olarak silinsin mi? Burada git tarafından izlenen hiçbir şey yok.", + "agentManager.orphans.resolve": "Çöz…", + "agentManager.orphans.summaryCount": "{{count}} artakalan worktree klasörü", + "agentManager.orphans.summarySize": "{{count}} artakalan worktree klasörü · {{size}}", + "agentManager.orphans.calculating": "boyut hesaplanıyor…", + "agentManager.orphans.sizeUnknown": "bilinmiyor", + "agentManager.orphans.dialogTitle": "Artakalan worktree klasörleri", + "agentManager.orphans.helpIntro": + "Kilo, oluşturduğu her worktree'yi bu deponun .kilo/worktrees klasöründe tutar. Aşağıdaki klasörler bu klasörün içindedir, ancak git bunların hiçbirini worktree olarak listelemez; dolayısıyla artık hiçbir şey onları kullanmıyor.", + "agentManager.orphans.helpCheckout": + "İçinde git checkout bulunduğu belirtilen bir klasörde hâlâ bir .git girdisi vardır ve commit edilmemiş çalışma barındırabilir. Bu klasörler seçili bırakılmaz; silmeden önce birini açıp inceleyin.", + "agentManager.orphans.helpCauses": + "Artakalan klasörler genellikle yarıda kesilen bir silme işleminden, Kilo dışında kaldırılan bir worktree'den veya klasör kaldırıldıktan sonra içine yazan bir araçtan kaynaklanır. Hâlâ sürmekte olan silme işlemleri burada listelenmez.", + "agentManager.orphans.helpDelete": + "Silme işlemi, seçili klasörleri Geri Dönüşüm Kutusu'na göndermeden diskten kalıcı olarak kaldırır. Hiçbir dal ve etkin worktree etkilenmez. Boyutlar, her klasörün şu anda diskte kapladığı alanı gösterir.", + "agentManager.orphans.helpMore": "Daha fazla göster", + "agentManager.orphans.helpLess": "Daha az göster", + "agentManager.orphans.columnPath": "Yol", + "agentManager.orphans.columnSize": "Boyut", + "agentManager.orphans.columnContents": "İçerik", + "agentManager.orphans.checkoutWarning": "bir git checkout içeriyor", + "agentManager.orphans.footerSelected": "{{count}} seçildi · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} hâlâ git checkout içeriyor", + "agentManager.orphans.reveal": "İşletim sisteminde göster", + "agentManager.orphans.revealMac": "Finder'da göster", + "agentManager.orphans.revealWindows": "Explorer'da göster", + "agentManager.orphans.revealLinux": "Dosyalar'da göster", + "agentManager.orphans.deleteButton": "{{count}} klasörü sil ({{size}})", "agentManager.orphans.cancel": "İptal", - "agentManager.orphans.checkout": "bir git checkout içeriyor", - "agentManager.orphans.confirmCheckout": - "Bu klasörler kalıcı olarak silinsin mi? {{count}} tanesi hâlâ commit edilmemiş değişiklikler içerebilecek bir git checkout barındırıyor.", "agentManager.error.title": "Agent Manager hatası", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 0f233a8021a9..4c91fba63f9d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -497,13 +497,33 @@ export const dict = { "Git або GitHub CLI не відповів вчасно. Опитування цього worktree припинено й буде повторено.", "agentManager.worktree.restore": "Відновити worktree", "agentManager.worktree.removeKeepSessions": "Видалити, зберігши сеанси", - "agentManager.orphans.title": "Залишені теки worktree", - "agentManager.orphans.summary": "{{count}} тек(и) у .kilo/worktrees не є git worktree.", - "agentManager.orphans.clean": "Очистити залишені теки", - "agentManager.orphans.confirm": "Видалити ці теки безповоротно? Git тут нічого не відслідковує.", + "agentManager.orphans.resolve": "Вирішити…", + "agentManager.orphans.summaryCount": "{{count}} залишених тек worktree", + "agentManager.orphans.summarySize": "{{count}} залишених тек worktree · {{size}}", + "agentManager.orphans.calculating": "обчислення розміру…", + "agentManager.orphans.sizeUnknown": "невідомо", + "agentManager.orphans.dialogTitle": "Залишені теки worktree", + "agentManager.orphans.helpIntro": + "Kilo зберігає всі створені worktree у теці .kilo/worktrees цього репозиторію. Теки нижче містяться в цій теці, але git не вважає жодну з них worktree, тому ними більше ніщо не користується.", + "agentManager.orphans.helpCheckout": + "Тека, позначена як така, що містить робочу копію git, усе ще має всередині запис .git і може зберігати незакомічену роботу. Такі теки лишаються невибраними, тому відкрийте одну з них і перевірте перед видаленням.", + "agentManager.orphans.helpCauses": + "Залишки зазвичай з'являються через перерване видалення, worktree, видалений поза Kilo, або інструмент, який записав дані до теки після її видалення. Видалення, які ще тривають, тут не показуються.", + "agentManager.orphans.helpDelete": + "Видалення остаточно вилучає вибрані теки з диска, не переміщуючи їх до Кошика. Жодна гілка й жоден активний worktree не змінюються. Розміри показують, скільки місця кожна тека займає на диску зараз.", + "agentManager.orphans.helpMore": "Показати більше", + "agentManager.orphans.helpLess": "Показати менше", + "agentManager.orphans.columnPath": "Шлях", + "agentManager.orphans.columnSize": "Розмір", + "agentManager.orphans.columnContents": "Зміст", + "agentManager.orphans.checkoutWarning": "містить робочу копію git", + "agentManager.orphans.footerSelected": "{{count}} вибрано · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} досі містять робочу копію git", + "agentManager.orphans.reveal": "Показати в ОС", + "agentManager.orphans.revealMac": "Показати у Finder", + "agentManager.orphans.revealWindows": "Показати в провіднику", + "agentManager.orphans.revealLinux": "Показати у файловому менеджері", + "agentManager.orphans.deleteButton": "Видалити {{count}} тек ({{size}})", "agentManager.orphans.cancel": "Скасувати", - "agentManager.orphans.checkout": "містить робочу копію git", - "agentManager.orphans.confirmCheckout": - "Видалити ці теки безповоротно? {{count}} усе ще містять робочу копію git із можливими незакоміченими змінами.", "agentManager.error.title": "Помилка Agent Manager", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index fce7cd524d0d..32286a08d5be 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -470,12 +470,33 @@ export const dict = { "Git 或 GitHub CLI 未及时响应。已暂停该 worktree 的轮询,稍后会重试。", "agentManager.worktree.restore": "恢复 worktree", "agentManager.worktree.removeKeepSessions": "移除并保留会话", - "agentManager.orphans.title": "残留的 worktree 文件夹", - "agentManager.orphans.summary": ".kilo/worktrees 下有 {{count}} 个文件夹不是 git worktree。", - "agentManager.orphans.clean": "清理残留文件夹", - "agentManager.orphans.confirm": "永久删除这些文件夹?其中没有任何内容被 git 跟踪。", + "agentManager.orphans.resolve": "解决…", + "agentManager.orphans.summaryCount": "{{count}} 个残留的 worktree 文件夹", + "agentManager.orphans.summarySize": "{{count}} 个残留的 worktree 文件夹 · {{size}}", + "agentManager.orphans.calculating": "正在计算大小…", + "agentManager.orphans.sizeUnknown": "未知", + "agentManager.orphans.dialogTitle": "残留的 worktree 文件夹", + "agentManager.orphans.helpIntro": + "Kilo 会把创建的每个 worktree 都保存在本仓库的 .kilo/worktrees 文件夹中。下面这些文件夹位于该文件夹内,但 git 并未把它们列为 worktree,因此已经没有任何内容在使用它们。", + "agentManager.orphans.helpCheckout": + "被标记为仍包含 git 检出的文件夹内仍有 .git 条目,可能存在尚未提交的改动。这类文件夹默认不会被选中,请在删除前先打开查看。", + "agentManager.orphans.helpCauses": + "残留通常来自中断的删除、在 Kilo 之外移除的 worktree,或在文件夹被移除后仍向其中写入的工具。仍在进行中的删除不会列在这里。", + "agentManager.orphans.helpDelete": + "删除会将所选文件夹从磁盘上彻底移除,不经过回收站。不会改动任何分支或使用中的 worktree。显示的大小是每个文件夹当前在磁盘上占用的空间。", + "agentManager.orphans.helpMore": "显示更多", + "agentManager.orphans.helpLess": "显示更少", + "agentManager.orphans.columnPath": "路径", + "agentManager.orphans.columnSize": "大小", + "agentManager.orphans.columnContents": "内容", + "agentManager.orphans.checkoutWarning": "仍包含 git 检出", + "agentManager.orphans.footerSelected": "已选择 {{count}} 个 · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} 个仍包含 git 检出", + "agentManager.orphans.reveal": "在系统中显示", + "agentManager.orphans.revealMac": "在 Finder 中显示", + "agentManager.orphans.revealWindows": "在文件资源管理器中显示", + "agentManager.orphans.revealLinux": "在文件管理器中显示", + "agentManager.orphans.deleteButton": "删除 {{count}} 个文件夹 ({{size}})", "agentManager.orphans.cancel": "取消", - "agentManager.orphans.checkout": "仍包含 git 检出", - "agentManager.orphans.confirmCheckout": "永久删除这些文件夹?其中 {{count}} 个仍包含 git 检出,可能有未提交的更改。", "agentManager.error.title": "Agent Manager 错误", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 8f3f201dfc1d..8c64c85069cd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -469,12 +469,33 @@ export const dict = { "Git 或 GitHub CLI 未及時回應。已暫停此 worktree 的輪詢,稍後會重試。", "agentManager.worktree.restore": "還原 worktree", "agentManager.worktree.removeKeepSessions": "移除並保留工作階段", - "agentManager.orphans.title": "殘留的 worktree 資料夾", - "agentManager.orphans.summary": ".kilo/worktrees 下有 {{count}} 個資料夾不是 git worktree。", - "agentManager.orphans.clean": "清理殘留資料夾", - "agentManager.orphans.confirm": "永久刪除這些資料夾?其中沒有任何內容被 git 追蹤。", + "agentManager.orphans.resolve": "解決…", + "agentManager.orphans.summaryCount": "{{count}} 個殘留的 worktree 資料夾", + "agentManager.orphans.summarySize": "{{count}} 個殘留的 worktree 資料夾 · {{size}}", + "agentManager.orphans.calculating": "正在計算大小…", + "agentManager.orphans.sizeUnknown": "未知", + "agentManager.orphans.dialogTitle": "殘留的 worktree 資料夾", + "agentManager.orphans.helpIntro": + "Kilo 會將建立的每個 worktree 都保存在本儲存庫的 .kilo/worktrees 資料夾中。下列資料夾位於該資料夾內,但 git 並未將它們列為 worktree,因此已經沒有任何項目在使用它們。", + "agentManager.orphans.helpCheckout": + "被標記為仍包含 git 檢出的資料夾內仍有 .git 項目,可能存在尚未提交的變更。這類資料夾預設不會被選取,請在刪除前先開啟檢查。", + "agentManager.orphans.helpCauses": + "殘留通常來自中斷的刪除、在 Kilo 之外移除的 worktree,或在資料夾被移除後仍寫入其中的工具。仍在進行中的刪除不會列在這裡。", + "agentManager.orphans.helpDelete": + "刪除會將所選資料夾從磁碟永久移除,不會經過資源回收筒。不會變更任何分支或使用中的 worktree。顯示的大小是每個資料夾目前在磁碟上佔用的空間。", + "agentManager.orphans.helpMore": "顯示更多", + "agentManager.orphans.helpLess": "顯示更少", + "agentManager.orphans.columnPath": "路徑", + "agentManager.orphans.columnSize": "大小", + "agentManager.orphans.columnContents": "內容", + "agentManager.orphans.checkoutWarning": "仍包含 git 檢出", + "agentManager.orphans.footerSelected": "已選取 {{count}} 個 · {{size}}", + "agentManager.orphans.footerCheckouts": "{{count}} 個仍包含 git 檢出", + "agentManager.orphans.reveal": "在系統中顯示", + "agentManager.orphans.revealMac": "在 Finder 中顯示", + "agentManager.orphans.revealWindows": "在檔案總管中顯示", + "agentManager.orphans.revealLinux": "在檔案管理器中顯示", + "agentManager.orphans.deleteButton": "刪除 {{count}} 個資料夾 ({{size}})", "agentManager.orphans.cancel": "取消", - "agentManager.orphans.checkout": "仍包含 git 檢出", - "agentManager.orphans.confirmCheckout": "永久刪除這些資料夾?其中 {{count}} 個仍包含 git 檢出,可能有未提交的變更。", "agentManager.error.title": "Agent Manager 錯誤", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/orphans/OrphanDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/orphans/OrphanDialog.tsx new file mode 100644 index 000000000000..4732881c03b9 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/orphans/OrphanDialog.tsx @@ -0,0 +1,220 @@ +/** + * Table dialog for resolving leftover folders under `.kilo/worktrees/`. + * + * Opened from the `Resolve…` banner action. `leftover` rows (no `.git`, nothing tracked) are + * pre-selected; `broken` rows (still hold a git checkout) start unchecked and flagged, since their + * files can exist nowhere else. Deletion itself runs in the background after this dialog closes — + * see `worktree-recovery.ts` — so this component only ever collects a selection and hands it off. + */ +import { Component, For, Show, createSignal } from "solid-js" +import { Dialog } from "@kilocode/kilo-ui/dialog" +import { Button } from "@kilocode/kilo-ui/button" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Icon } from "@kilocode/kilo-ui/icon" +import { Checkbox } from "@kilocode/kilo-ui/checkbox" +import { useLanguage } from "../../src/context/language" +import { + defaultOrphanSelection, + formatOrphanBytes, + orphanSelectionStats, + revealPlatform, + type OrphanSelectionStats, +} from "./dialog-logic" +import type { OrphanDirectory } from "../project/store" + +const REVEAL_KEYS = { + mac: "agentManager.orphans.revealMac", + windows: "agentManager.orphans.revealWindows", + linux: "agentManager.orphans.revealLinux", +} as const + +/** OS-specific label, or the generic fallback when `navigator` cannot be read (e.g. SSR/tests). */ +function revealLabelKey(userAgent: string | undefined): string { + if (userAgent === undefined) return "agentManager.orphans.reveal" + return REVEAL_KEYS[revealPlatform(userAgent)] +} + +/** + * Header explanation, mirroring the JetBrains dialog's banner: the list comes from a heuristic over a + * directory Kilo owns, and "delete 48 folders" is not a decision anybody can make from paths alone. + * + * Rendered inside the shared dialog's description slot, which is a `

`, so every block here is a + * span laid out by CSS rather than a `

    `. The three bullets are the collapsible detail — the intro + * sentence always stays visible, so collapsing never hides the point of the dialog, only the specifics. + */ +export const OrphanHelp: Component<{ expanded: boolean; onToggle: () => void }> = (props) => { + const { t } = useLanguage() + return ( + <> + {t("agentManager.orphans.helpIntro")} + + + {t("agentManager.orphans.helpCheckout")} + {t("agentManager.orphans.helpCauses")} + {t("agentManager.orphans.helpDelete")} + + + + + ) +} + +interface OrphanDialogProps { + orphans: OrphanDirectory[] + onReveal: (path: string) => void + onDelete: (paths: string[]) => void + onClose: () => void +} + +export const OrphanDialog: Component = (props) => { + const { t } = useLanguage() + const [helpExpanded, setHelpExpanded] = createSignal(false) + const [selected, setSelected] = createSignal(defaultOrphanSelection(props.orphans)) + const stats = () => orphanSelectionStats(props.orphans, selected()) + const allChecked = () => props.orphans.length > 0 && props.orphans.every((orphan) => selected().has(orphan.path)) + const someChecked = () => !allChecked() && props.orphans.some((orphan) => selected().has(orphan.path)) + const revealKey = revealLabelKey(typeof navigator !== "undefined" ? navigator.userAgent : undefined) + + const toggleAll = (checked: boolean) => { + setSelected(checked ? new Set(props.orphans.map((orphan) => orphan.path)) : new Set()) + } + const toggleRow = (path: string, checked: boolean) => { + setSelected((prev) => { + const next = new Set(prev) + if (checked) next.add(path) + else next.delete(path) + return next + }) + } + /** Known total, "calculating…" while a pass is still coming, or "unknown" once it settled without one. */ + const sizeLabel = (current: OrphanSelectionStats) => { + if (current.bytes !== undefined) return formatOrphanBytes(current.bytes) + return current.pending ? t("agentManager.orphans.calculating") : t("agentManager.orphans.sizeUnknown") + } + /** Same three states, but the pending case has to stay short enough to sit inside a button label. */ + const buttonSize = (current: OrphanSelectionStats) => { + if (current.bytes !== undefined) return formatOrphanBytes(current.bytes) + return current.pending ? "…" : t("agentManager.orphans.sizeUnknown") + } + + return ( + setHelpExpanded((prev) => !prev)} />} + size="large" + > +
    +
    + + + + + + + + + + + + {(orphan) => ( + + + + + + + + )} + + +
    + } + > + {t("agentManager.orphans.dialogTitle")} + + {t("agentManager.orphans.columnPath")}{t("agentManager.orphans.columnSize")}{t("agentManager.orphans.columnContents")} +
    + toggleRow(orphan.path, checked)} + icon={} + > + {orphan.path} + + + + {orphan.path} + + + + {formatOrphanBytes(orphan.bytes ?? 0)} + + + + + + {t("agentManager.orphans.checkoutWarning")} + + + + props.onReveal(orphan.path)} + /> +
    +
    + + +
    +
    + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/orphans/OrphanNotice.tsx b/packages/kilo-vscode/webview-ui/agent-manager/orphans/OrphanNotice.tsx new file mode 100644 index 000000000000..0f67267eb2f3 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/orphans/OrphanNotice.tsx @@ -0,0 +1,56 @@ +/** + * Warning banner for leftover folders under `.kilo/worktrees/` that no git worktree claims. + * + * Deleting files is never automatic, so the only action here is opening `OrphanDialog` — the banner + * itself only ever names how many there are and their total size. Size is asynchronous (a background + * fs walk), so it is shown once every orphan's size has landed and a calculating affordance is shown + * until then rather than a wrong or stale number. + * + * Three states, not two: a folder the host could not walk never reports a size, so once the pass has + * settled without covering everything the banner drops the size and says how many folders there are. + * Treating that as "still calculating" left the affordance up forever. + */ +import { Component, Show } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { Icon } from "@kilocode/kilo-ui/icon" +import { useLanguage } from "../../src/context/language" +import { formatOrphanBytes, orphanSizesSettled, orphanTotalBytes } from "./dialog-logic" +import type { OrphanDirectory } from "../project/store" + +export const OrphanNotice: Component<{ + orphans: OrphanDirectory[] + onResolve: () => void +}> = (props) => { + const { t } = useLanguage() + const total = () => orphanTotalBytes(props.orphans) + const calculating = () => total() === undefined && !orphanSizesSettled(props.orphans) + + return ( + 0}> +
    +
    + + + + {t("agentManager.orphans.summarySize", { + count: props.orphans.length, + size: formatOrphanBytes(total() ?? 0), + })} + + + + {t("agentManager.orphans.calculating")} + +
    +
    + +
    +
    +
    + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/orphans/dialog-logic.ts b/packages/kilo-vscode/webview-ui/agent-manager/orphans/dialog-logic.ts new file mode 100644 index 000000000000..f35c7849304b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/orphans/dialog-logic.ts @@ -0,0 +1,68 @@ +/** + * Pure selection/footer math for the leftover-worktree-folders dialog, kept out of `OrphanDialog.tsx` + * so it is testable without a Solid render pass. + */ +import type { OrphanDirectory } from "../project/store" + +/** Selection defaults to every `leftover` row checked; `broken` rows start unchecked and flagged. */ +export function defaultOrphanSelection(orphans: OrphanDirectory[]): Set { + return new Set(orphans.filter((orphan) => orphan.kind === "leftover").map((orphan) => orphan.path)) +} + +export interface OrphanSelectionStats { + count: number + /** Total apparent size of the selected rows, or undefined unless every one of them is known. */ + bytes: number | undefined + /** True while a size pass is still expected to answer for one of the selected rows. */ + pending: boolean + /** How many selected rows still hold a git checkout. */ + checkouts: number +} + +/** Footer summary math for the current selection. */ +export function orphanSelectionStats(orphans: OrphanDirectory[], selected: ReadonlySet): OrphanSelectionStats { + const rows = orphans.filter((orphan) => selected.has(orphan.path)) + const checkouts = rows.filter((orphan) => orphan.kind === "broken").length + const bytes = rows.some((orphan) => orphan.bytes === undefined) + ? undefined + : rows.reduce((sum, orphan) => sum + (orphan.bytes ?? 0), 0) + return { count: rows.length, bytes, pending: !orphanSizesSettled(rows), checkouts } +} + +/** Total known size across every orphan, or undefined unless every one of them is known. */ +export function orphanTotalBytes(orphans: OrphanDirectory[]): number | undefined { + if (orphans.some((orphan) => orphan.bytes === undefined)) return undefined + return orphans.reduce((sum, orphan) => sum + (orphan.bytes ?? 0), 0) +} + +/** + * Whether the size pass is done with every one of these folders. + * + * A folder the host could not walk never gets a `bytes`, so "no size yet" is not the same question as + * "is a size still coming?" — without this the UI would offer to keep calculating indefinitely. + */ +export function orphanSizesSettled(orphans: OrphanDirectory[]): boolean { + return orphans.every((orphan) => orphan.bytes !== undefined || orphan.sized === true) +} + +/** Apparent size formatted for display, e.g. `1.2 GB`. Mirrors the host-side formatter. */ +export function formatOrphanBytes(bytes: number): string { + const units = ["B", "KB", "MB", "GB", "TB"] + let value = bytes + let unit = 0 + while (value >= 1024 && unit < units.length - 1) { + value /= 1024 + unit++ + } + const precision = unit === 0 ? 0 : 1 + return `${value.toFixed(precision)} ${units[unit]}` +} + +export type RevealPlatform = "mac" | "windows" | "linux" + +/** OS-family detection for the reveal button's platform-specific label. */ +export function revealPlatform(userAgent: string): RevealPlatform { + if (/Mac|iPhone|iPad/.test(userAgent)) return "mac" + if (/Win/.test(userAgent)) return "windows" + return "linux" +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 1207a1345b57..c5cde10a9d3a 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -901,8 +901,11 @@ export interface AgentManagerStateMessage { * * `broken` still holds a git checkout, so it can contain work that exists nowhere else; `leftover` * is a bare directory. The notice says which, because the two do not deserve the same warning. + * + * `sized` is set once the size pass is done with a folder; without `bytes` it means the folder + * could not be measured, which is how the UI knows to stop saying it is still calculating. */ - orphanDirectories?: { path: string; kind: "broken" | "leftover" }[] + orphanDirectories?: { path: string; kind: "broken" | "leftover"; bytes?: number; sized?: boolean }[] tabOrder?: Record worktreeOrder?: string[] sessionsCollapsed?: boolean diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 3be9ec6145c1..3990d763e29d 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -711,6 +711,13 @@ export interface CleanOrphanDirectoriesRequest { paths: string[] } +// Reveal an orphaned directory in the OS file manager +export interface RevealPathRequest { + type: "agentManager.revealPath" + projectId?: string + path: string +} + // Promote a session: create a worktree and move the session into it export interface PromoteSessionRequest { type: "agentManager.promoteSession" @@ -1695,6 +1702,7 @@ export type WebviewMessage = | RemoveStaleWorktreeRequest | RestoreWorktreeRequest | CleanOrphanDirectoriesRequest + | RevealPathRequest | PromoteSessionRequest | OpenLocallyRequest | OpenSessionLocallyRequest