Skip to content
Merged
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/past-chats-worktree-family.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Fix the `@` "Past chats" picker in Agent Manager showing only the current session's directory. It now lists previous sessions across the whole worktree family — the local workspace and every Agent Manager worktree — each labeled with its worktree name, matching the Agent Manager session search. Any listed session can be attached as context, including chats from other worktrees of the same repository.
26 changes: 19 additions & 7 deletions packages/kilo-vscode/src/kilo-provider/session-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type Item = {
id: string
title: string
updated: number
worktreeName?: string
}

type Message = {
Expand All @@ -22,11 +23,14 @@ type Input = {
}

/**
* Past-chat mention search. Lists root sessions for the directory the current
* chat runs in (workspace root for the sidebar, the worktree for Agent Manager
* sessions) — the same directory-scoped `session.list` the session history and
* Agent Manager search are built on. Fuzzy title filtering happens in the
* webview (same mechanism as the Agent Manager sidebar search).
* Past-chat mention search. Lists root sessions across the current directory's
* worktree family (the repo root and its sibling worktrees for git projects,
* just the directory itself otherwise) — the same family-wide listing the
* Agent Manager session search and the CLI's past-chat picker are built on.
* Every session in the family shares the project, so any of them can be
* attached regardless of which worktree the current chat runs in. Fuzzy title
* filtering happens in the webview (same mechanism as the Agent Manager
* sidebar search).
*/
export async function handleSessionSearch(input: Input): Promise<void> {
const client = input.client
Expand All @@ -39,10 +43,18 @@ export async function handleSessionSearch(input: Input): Promise<void> {
const dir = input.dir(id)

try {
const res = await client.session.list({ directory: dir, roots: true, limit: 50 }, { throwOnError: true })
const res = await client.experimental.session.list(
{ worktrees: true, roots: true, directory: dir, limit: 50 },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: worktrees: true moves this onto a noticeably more expensive server path than the old directory-scoped session.list

With worktrees set, the handler runs WorktreeFamily.list() (a git worktree list --porcelain subprocess) and then KiloSession.listGlobal skips the SQL LIMIT entirely because directories is non-empty (packages/opencode/src/kilocode/session/index.ts:400), loading every non-archived root session in the project family and filtering in JS — with a existsSync(.git) parent walk per row via nested(). Since the webview refetches candidates on every picker open (useFileMention.ts openSessionPicker), that cost is paid each time @ past-chats is opened, and it scales with the project's total session count rather than with limit: 50.

Probably fine in practice, but if it shows up on large histories, consider caching the candidate list for the lifetime of the composer instead of refetching per open.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

{ throwOnError: true },
)
const sessions: Item[] = res.data
.filter((session) => session.id !== input.exclude && session.title)
.map((session) => ({ id: session.id, title: session.title, updated: session.time.updated }))
.map((session) => ({
id: session.id,
title: session.title,
updated: session.time.updated,
worktreeName: session.worktreeName,
}))
input.post({ type: "sessionSearchResult", sessions, requestId: input.message.requestId })
} catch (err) {
console.error("[Kilo New] Session search failed:", err)
Expand Down
115 changes: 115 additions & 0 deletions packages/kilo-vscode/tests/unit/session-search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, expect, it } from "bun:test"
import { handleSessionSearch } from "../../src/kilo-provider/session-search"

type Query = Record<string, unknown>

function stub(data: Array<Record<string, unknown>> | Error) {
const calls: Query[] = []
const client = {
experimental: {
session: {
list: async (query: Query) => {
calls.push(query)
if (data instanceof Error) throw data
return { data }
},
},
},
}
return { calls, client }
}

function session(id: string, title: string, updated: number, worktreeName?: string) {
return { id, title, time: { updated }, worktreeName }
}

describe("handleSessionSearch", () => {
it("lists root sessions across the worktree family for the resolved directory", async () => {
const { calls, client } = stub([session("ses_a", "Alpha", 2, "neon-author")])
const posted: unknown[] = []

await handleSessionSearch({
client: client as never,
message: { requestId: "r1", sessionID: "ses_current" },
dir: (id) => (id === "ses_current" ? "/repo/.kilo/worktrees/wt-1" : "/repo"),
post: (msg) => posted.push(msg),
})

expect(calls).toEqual([{ worktrees: true, roots: true, directory: "/repo/.kilo/worktrees/wt-1", limit: 50 }])
expect(posted).toEqual([
{
type: "sessionSearchResult",
sessions: [{ id: "ses_a", title: "Alpha", updated: 2, worktreeName: "neon-author" }],
requestId: "r1",
},
])
})

it("falls back to the current and context sessions for directory resolution", async () => {
const { calls, client } = stub([])

await handleSessionSearch({
client: client as never,
message: { requestId: "r2" },
current: "ses_current",
context: "ses_context",
dir: (id) => `/dir/${id}`,
post: () => {},
})

expect(calls[0]?.directory).toBe("/dir/ses_current")

await handleSessionSearch({
client: client as never,
message: { requestId: "r3" },
context: "ses_context",
dir: (id) => `/dir/${id}`,
post: () => {},
})

expect(calls[1]?.directory).toBe("/dir/ses_context")
})

it("excludes the given session and sessions without titles", async () => {
const { client } = stub([
session("ses_keep", "Keep", 3),
session("ses_exclude", "Excluded", 2),
session("ses_untitled", "", 1),
])
const posted: Array<{ sessions: Array<{ id: string }> }> = []

await handleSessionSearch({
client: client as never,
message: { requestId: "r4" },
dir: () => "/repo",
exclude: "ses_exclude",
post: (msg) => posted.push(msg as never),
})

expect(posted[0]?.sessions.map((s) => s.id)).toEqual(["ses_keep"])
})

it("posts an empty result when the client is missing or the list fails", async () => {
const posted: unknown[] = []

await handleSessionSearch({
client: null,
message: { requestId: "r5" },
dir: () => "/repo",
post: (msg) => posted.push(msg),
})

const failing = stub(new Error("boom"))
await handleSessionSearch({
client: failing.client as never,
message: { requestId: "r6" },
dir: () => "/repo",
post: (msg) => posted.push(msg),
})

expect(posted).toEqual([
{ type: "sessionSearchResult", sessions: [], requestId: "r5" },
{ type: "sessionSearchResult", sessions: [], requestId: "r6" },
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function SessionMentionPicker(props: Props) {
<List<SessionSearchItem>
items={props.sessions}
key={(item) => item.id}
filterKeys={["title"]}
filterKeys={["title", "worktreeName"]}
search={{ placeholder: "Search sessions", autofocus: true }}
onSelect={(item) => {
if (item) props.onSelect(item)
Expand All @@ -52,6 +52,7 @@ export function SessionMentionPicker(props: Props) {
<span class="session-mention-item">
<Icon name="history" class="file-mention-icon" />
<span class="session-mention-title">{item.title}</span>
{item.worktreeName && <span class="session-mention-worktree">{item.worktreeName}</span>}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: the worktree label is rendered even when it carries no information

The server sets worktreeName for every session whenever the family listing is used — path.basename(root ?? session.directory) in handlers/experimental.ts:245 — so in the plain sidebar of a repo with no Agent Manager worktrees, every row now gets an identical badge with the repo folder name (and for non-git directories, the basename of the directory itself). That eats up to 40% of the row width without disambiguating anything.

Consider only rendering the badge when the candidates actually span more than one worktree, e.g. compute new Set(props.sessions.map((s) => s.worktreeName)).size > 1 in a memo and gate the span on it. Fuzzy matching on worktreeName can stay unconditional.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<span class="session-mention-time">{formatRelativeDate(new Date(item.updated).toISOString())}</span>
</span>
)}
Expand Down
10 changes: 10 additions & 0 deletions packages/kilo-vscode/webview-ui/src/styles/prompt-dropdowns.css
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@
font-size: var(--kilo-font-size-11);
}

.session-mention-worktree {
flex-shrink: 0;
opacity: 0.6;
font-size: var(--kilo-font-size-11);
max-width: 40%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* ============================================
Slash Command Dropdown
============================================ */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ export interface SessionSearchItem {
id: string
title: string
updated: number
/** Name of the worktree the session runs in, when listed across the worktree family. */
worktreeName?: string
}

export interface SessionSearchResultMessage {
Expand Down
Loading