diff --git a/.changeset/tui-submodule-session-list.md b/.changeset/tui-submodule-session-list.md new file mode 100644 index 00000000000..fcb2ecbf3fc --- /dev/null +++ b/.changeset/tui-submodule-session-list.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix empty TUI session list when launching kilo from inside a git submodule. `git worktree list --porcelain` reports the submodule's gitdir (`/.git/modules/`) instead of the working tree, so the worktree-family filter dropped every session whose directory was the actual submodule path. Include `Instance.worktree` in the returned set so submodule sessions stay in scope. diff --git a/packages/opencode/src/kilocode/worktree-family.ts b/packages/opencode/src/kilocode/worktree-family.ts index 519516b4268..35fad2d3609 100644 --- a/packages/opencode/src/kilocode/worktree-family.ts +++ b/packages/opencode/src/kilocode/worktree-family.ts @@ -25,6 +25,13 @@ export namespace WorktreeFamily { }) if (dirs.length > 0) { + // In a git submodule, `git worktree list --porcelain` reports the + // gitdir (`/.git/modules/`) instead of the actual working + // tree, so the parsed list never contains the directory sessions are + // recorded under. Including Instance.worktree keeps submodule sessions + // in scope without affecting normal repos (already present) or linked + // worktrees (also already present). + dirs.push(Filesystem.resolve(Instance.worktree)) return [...new Set(dirs)] } } diff --git a/packages/opencode/test/kilocode/worktree-family-submodule.test.ts b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts new file mode 100644 index 00000000000..a448eb00232 --- /dev/null +++ b/packages/opencode/test/kilocode/worktree-family-submodule.test.ts @@ -0,0 +1,40 @@ +import { $ } from "bun" +import { afterEach, describe, expect, test } from "bun:test" +import * as fs from "fs/promises" +import path from "path" +import { Instance } from "../../src/project/instance" +import { WorktreeFamily } from "../../src/kilocode/worktree-family" +import { Log } from "../../src/util" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +afterEach(async () => { + await Instance.disposeAll() +}) + +describe("WorktreeFamily.list — git submodule", () => { + test("returns the submodule's working tree, not its gitdir", async () => { + await using parent = await tmpdir({ git: true }) + await using child = await tmpdir({ git: true }) + + // `protocol.file.allow=always` so the local clone is permitted, then commit + // the .gitmodules entry so the submodule is part of the parent's history. + await $`git -c protocol.file.allow=always submodule add ${child.path} sub`.cwd(parent.path).quiet() + await $`git commit -m "add submodule"`.cwd(parent.path).quiet() + + const submodule = path.join(parent.path, "sub") + const submoduleReal = await fs.realpath(submodule) + + await Instance.provide({ + directory: submodule, + fn: async () => { + const dirs = await WorktreeFamily.list() + // `git worktree list --porcelain` from inside a submodule reports the + // gitdir (`/.git/modules/sub`) as the worktree, so without the + // submodule guard the actual working tree is missing. + expect(dirs).toContain(submoduleReal) + }, + }) + }) +})