fix: session list on tui

This commit is contained in:
Catriel Müller
2026-06-15 21:08:16 -03:00
parent 8c1cdf53a9
commit e91eef2b38
17 changed files with 226 additions and 45 deletions
@@ -14,7 +14,6 @@ import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast"
import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"
import { Spinner } from "./spinner"
import path from "path" // kilocode_change
import { errorMessage } from "@/util/error"
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
import { WorkspaceLabel } from "./workspace-label"
@@ -31,7 +30,7 @@ export function DialogSessionList() {
const toast = useToast()
const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150)
const [global, setGlobal] = createSignal(true) // kilocode_change - show all worktrees by default
const [global, setGlobal] = createSignal(false) // kilocode_change - show current worktree by default
const deleteHint = useCommandShortcut("session.delete")
const quickSwitch1 = useCommandShortcut("session.quick_switch.1")
const quickSwitch9 = useCommandShortcut("session.quick_switch.9")
@@ -39,15 +38,17 @@ export function DialogSessionList() {
// kilocode_change start - always fetch from experimental endpoint (returns GlobalSession with worktree info)
// TODO: extend /experimental/session to accept `scope`/`path` so this dialog can respect the
// upstream `session_directory_filter_enabled` KV toggle (via sync.session.query()) while
// keeping worktree grouping. Currently the toggle has no effect here.
// keeping worktree grouping.
const [searchResults, searchActions] = createResource(
() => search(),
async (query) => {
() => ({ query: search(), global: global(), directory: project.instance.directory() }), // kilocode_change
async (input) => {
const result = await sdk.client.experimental.session.list(
{
search: query || undefined,
search: input.query || undefined,
roots: true,
worktrees: true,
current: input.global ? undefined : "true",
directory: input.global ? undefined : input.directory || undefined,
limit: 30,
},
{ throwOnError: true },
@@ -59,15 +60,7 @@ export function DialogSessionList() {
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
// kilocode_change start - client-side worktree filtering when global is off
const sessions = createMemo(() => {
const all = searchResults() ?? []
if (global()) return all
const root = project.instance.path().worktree
if (!root || root === "/") return all
return all.filter((s) => s.directory === root || s.directory.startsWith(root + path.sep))
})
// kilocode_change end
const sessions = createMemo(() => searchResults() ?? []) // kilocode_change - endpoint applies worktree scope
function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
const workspace = project.workspace.get(session.workspaceID!)
@@ -154,8 +147,6 @@ export function DialogSessionList() {
.map((x) => x.id)
}
const [browseOrder] = createSignal<string[]>(orderByRecency(sync.data.session))
const quickSwitchHint = createMemo(() => {
const first = quickSwitch1()
const last = quickSwitch9()
@@ -176,8 +167,7 @@ export function DialogSessionList() {
.map((x) => [x.id, x]),
)
const searchResult = searchResults()
const displayOrder = searchResult ? orderByRecency(searchResult) : browseOrder()
const displayOrder = orderByRecency(sessions()) // kilocode_change - respect current scope
const pinned = local.session.pinned().filter((id) => sessionMap.has(id))
const pinnedSet = new Set(pinned)
@@ -335,6 +325,7 @@ export function DialogSessionList() {
{
command: "session.scope.toggle",
title: global() ? "current" : "all",
requiresSelection: false,
onTrigger: async () => {
setToDelete(undefined)
setGlobal((v) => !v)
+4 -3
View File
@@ -84,12 +84,13 @@ async function input(value?: string) {
}
export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) {
// kilocode_change start - ignore stale PWD from wrappers such as `bun --cwd`
// kilocode_change start - ignore stale PWD from wrappers such as `bun --cwd`, except kilo-dev's caller cwd
const dev = process.env.KILO_DEV_CWD
const real = Filesystem.resolve(cwd)
const root = envPWD && Filesystem.resolve(envPWD) === real ? Filesystem.resolve(envPWD) : real
const root = dev ? Filesystem.resolve(dev) : envPWD && Filesystem.resolve(envPWD) === real ? Filesystem.resolve(envPWD) : real
// kilocode_change end
if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project))
return real // kilocode_change
return dev ? root : real // kilocode_change
}
export const TuiThreadCommand = cmd({
@@ -31,13 +31,7 @@ export interface DialogSelectProps<T> {
onSelect?: (option: DialogSelectOption<T>) => void
skipFilter?: boolean
renderFilter?: boolean
actions?: {
command: string
title: string
side?: "left" | "right"
disabled?: boolean
onTrigger: (option: DialogSelectOption<T>) => void
}[]
actions?: DialogSelectAction<T>[] // kilocode_change - supports actions without a selected option
footerHints?: {
title: string
label: string
@@ -47,6 +41,27 @@ export interface DialogSelectProps<T> {
current?: T
}
// kilocode_change start - support list-level actions when no option is selected
type DialogSelectActionBase = {
command: string
title: string
side?: "left" | "right"
disabled?: boolean
}
type DialogSelectAction<T> = DialogSelectActionBase &
(
| {
requiresSelection?: true
onTrigger: (option: DialogSelectOption<T>) => void
}
| {
requiresSelection: false
onTrigger: () => void
}
)
// kilocode_change end
export interface DialogSelectOption<T = any> {
title: string
value: T
@@ -303,6 +318,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
category: "Dialog",
run() {
setStore("input", "keyboard")
// kilocode_change start - allow actions such as scope toggles on empty lists
if (item.requiresSelection === false) {
item.onTrigger()
return
}
// kilocode_change end
const option = selected()
if (!option) return
item.onTrigger(option)
@@ -17,6 +17,7 @@ import type { Provider } from "@/provider/provider"
import { zod as toZod } from "@opencode-ai/core/effect-zod"
import { ENV_FEATURE } from "@kilocode/kilo-gateway"
import { fn } from "@/kilocode/fn"
import path from "path"
export namespace KiloSession {
const log = Log.create({ service: "session.kilo" })
@@ -316,6 +317,7 @@ export namespace KiloSession {
projectID?: string
directory?: string
directories?: string[]
currentDirectory?: string
roots?: boolean
start?: number
cursor?: number
@@ -360,6 +362,19 @@ export namespace KiloSession {
const limit = input.limit ?? 100
const dirs = [...new Set((input.directories ?? []).map((dir) => Filesystem.resolve(dir)))]
const sorted = [...dirs].sort((a, b) => b.length - a.length)
const worktree = (dir: string) => {
for (const root of sorted) {
if (!Filesystem.contains(root, dir)) continue
const rel = path.relative(root, dir)
const parts = rel.split(path.sep)
if ((parts[0] === ".kilo" || parts[0] === ".kilocode") && parts[1] === "worktrees" && parts[2]) {
return path.join(root, parts[0], parts[1], parts[2])
}
return root
}
}
const current = input.currentDirectory ? worktree(Filesystem.resolve(input.currentDirectory)) : undefined
const rows = Database.use((db) => {
const query =
@@ -377,7 +392,10 @@ export namespace KiloSession {
dirs.length > 0
? rows.filter((row) => {
const dir = Filesystem.resolve(row.directory)
return dirs.some((root) => Filesystem.contains(root, dir))
const root = worktree(dir)
if (!root) return false
if (input.currentDirectory) return root === current
return true
})
: rows
@@ -77,6 +77,7 @@ export const SessionListQuery = Schema.Struct({
// kilocode_change start
projectID: Schema.optional(Schema.String),
worktrees: Schema.optional(QueryBoolean),
current: Schema.optional(QueryBoolean),
// kilocode_change end
roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString),
@@ -195,14 +195,18 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
// kilocode_change start
const state = yield* InstanceState.context
const projectID = ctx.query.worktrees && !ctx.query.projectID ? state.project.id : ctx.query.projectID
const directories = ctx.query.worktrees ? yield* WorktreeFamily.list() : undefined
const sorted = directories ? [...directories].sort((a, b) => b.length - a.length) : undefined
const roots = ctx.query.worktrees ? yield* WorktreeFamily.list() : undefined
const directory = ctx.query.current ? ctx.query.directory : undefined
const sorted = roots ? [...roots].sort((a, b) => b.length - a.length) : undefined
const current = sorted && directory ? sorted.find((dir) => Filesystem.contains(dir, directory)) : undefined
// kilocode_change end
if (roots && directory && !current) return HttpServerResponse.jsonUnsafe([]) // kilocode_change
const sessions = Array.from(
Session.listGlobal({
projectID, // kilocode_change
directory: ctx.query.worktrees ? undefined : ctx.query.directory, // kilocode_change
directories, // kilocode_change
directories: roots, // kilocode_change
currentDirectory: directory, // kilocode_change
roots: ctx.query.roots,
start: ctx.query.start,
cursor: ctx.query.cursor,
+1
View File
@@ -1027,6 +1027,7 @@ export function* listGlobal(input?: {
projectID?: string
directory?: string
directories?: string[]
currentDirectory?: string
roots?: boolean
start?: number
cursor?: number
+2 -2
View File
@@ -30,9 +30,9 @@ const readRuntimeFlags = () =>
export function getChannelPath(flags: Pick<DatabaseFlags, "disableChannelDb"> = readRuntimeFlags()) {
if (["latest", "beta", "prod"].includes(InstallationChannel) || flags.disableChannelDb)
return path.join(Global.Path.data, "kilo.db")
return path.join(Global.Path.data, "kilo.db") // kilocode_change
const safe = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")
return path.join(Global.Path.data, `opencode-${safe}.db`)
return path.join(Global.Path.data, `kilo-${safe}.db`) // kilocode_change
}
export const getPath = (flags?: Pick<DatabaseFlags, "disableChannelDb">) => {