Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tui-submodule-session-list.md
Original file line number Diff line number Diff line change
@@ -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 (`<repo>/.git/modules/<sub>`) 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.
7 changes: 7 additions & 0 deletions packages/opencode/src/kilocode/worktree-family.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export namespace WorktreeFamily {
})

if (dirs.length > 0) {
// In a git submodule, `git worktree list --porcelain` reports the
// gitdir (`<repo>/.git/modules/<sub>`) 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)]
}
}
Expand Down
40 changes: 40 additions & 0 deletions packages/opencode/test/kilocode/worktree-family-submodule.test.ts
Original file line number Diff line number Diff line change
@@ -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 (`<parent>/.git/modules/sub`) as the worktree, so without the
// submodule guard the actual working tree is missing.
expect(dirs).toContain(submoduleReal)
},
})
})
})