Merge pull request #13512 from Kilo-Org/support-worktree-references-in-prompts

feat(vscode): add searchable worktree references
This commit is contained in:
Marius
2026-08-27 15:46:34 +02:00
committed by GitHub
40 changed files with 896 additions and 41 deletions
@@ -107,6 +107,15 @@ async function closeFiltered() {
input.dispatchEvent(new InputEvent("input", { bubbles: true, data: "be", inputType: "insertText" }))
await settle()
query<HTMLButtonElement>('[aria-label="Clear filter"]', "Clear button did not render").click()
await settle()
assert.equal(input.value, "", "Clearing did not reset the search input")
assert.equal(root.querySelectorAll('[data-slot="list-item"]').length, 3, "Clearing did not restore all tabs")
assert.equal(document.activeElement, input, "Clearing did not restore search focus")
input.value = "be"
input.dispatchEvent(new InputEvent("input", { bubbles: true, data: "be", inputType: "insertText" }))
await settle()
const close = query<HTMLButtonElement>(
'[aria-label="Close tab: Beta"]',
"Filtered result close button did not render",
@@ -0,0 +1,67 @@
import assert from "node:assert/strict"
import { createRoot, createSignal } from "solid-js"
import { createProjectStore } from "../../webview-ui/agent-manager/project/store"
import { createWorktreeReferences } from "../../webview-ui/agent-manager/worktree-references"
await createRoot(async (dispose) => {
const first = createProjectStore("first")
const second = createProjectStore("second")
const time = "2026-08-27T00:00:00.000Z"
for (const project of [first, second]) {
project.setWorktrees(
["one", "two"].map((id) => ({
id,
path: `/${project.id}/${id}`,
branch: id,
parentBranch: "main",
createdAt: time,
})),
)
project.setManagedSessions([{ id: "same", worktreeId: "one", createdAt: time }])
}
const [project, setProject] = createSignal(first)
const [selection, select] = createSignal<string | null>(null)
const storage = { value: {} as Record<string, unknown> }
const sessions = () => [{ id: "same", title: project().id, updatedAt: time }]
const refs = createWorktreeReferences(
{
getState: <T>() => storage.value as T,
setState: (value) => {
storage.value = value as Record<string, unknown>
},
},
project,
sessions,
selection,
)
try {
select("one")
await Promise.resolve()
assert.deepEqual(storage.value.worktreeMentionHistory, ["/first/one"])
first.setStaleWorktreeIds(new Set(["two"]))
select("two")
await Promise.resolve()
assert.deepEqual(storage.value.worktreeMentionHistory, ["/first/one"])
assert.equal(refs().find((item) => item.id === "two")?.disabled, true)
first.setStaleWorktreeIds(new Set())
await Promise.resolve()
assert.deepEqual(storage.value.worktreeMentionHistory, ["/first/two", "/first/one"])
first.setBusy(new Map([["one", { reason: "creating" }]]))
select("one")
await Promise.resolve()
assert.deepEqual(storage.value.worktreeMentionHistory, ["/first/two", "/first/one"])
first.setBusy(new Map())
await Promise.resolve()
assert.deepEqual(
refs().map((item) => item.path),
["/first/one", "/first/two"],
)
setProject(second)
await Promise.resolve()
assert.equal(refs()[0]?.sessions[0]?.title, "second")
assert.equal(refs()[0]?.path, "/second/one")
assert.equal((storage.value.worktreeMentionHistory as string[])[0], "/second/one")
} finally {
dispose()
}
})
@@ -0,0 +1,300 @@
import { describe, expect, it } from "bun:test"
import { createRoot } from "solid-js"
import { worktreeReferences } from "../../webview-ui/agent-manager/worktree-references"
import { createProjectStore } from "../../webview-ui/agent-manager/project/store"
import {
buildMentionResults,
buildWorktreeAttachments,
filterMentionResults,
type WorktreeReference,
} from "../../webview-ui/src/hooks/file-mention-utils"
import { useFileMention } from "../../webview-ui/src/hooks/useFileMention"
import type { ExtensionMessage, WebviewMessage, WorktreeState } from "../../webview-ui/src/types/messages"
function tree(id: string, values: Partial<WorktreeState> = {}): WorktreeState {
return {
id,
branch: `feature/${id}`,
path: `/repo/.kilo/worktrees/${id}`,
parentBranch: "main",
createdAt: "2026-08-01T00:00:00.000Z",
...values,
}
}
function reference(values: Partial<WorktreeReference> = {}): WorktreeReference {
return {
id: "wt-reference",
name: "Authentication",
branch: "feature/auth",
path: "/repo/.kilo/worktrees/reference",
base: "main",
sessions: [{ id: "ses_auth", title: "Fix login" }],
disabled: false,
...values,
}
}
function content(url: string) {
return decodeURIComponent(url.slice(url.indexOf(",") + 1))
}
function harness(worktrees: () => WorktreeReference[]) {
const posted: WebviewMessage[] = []
const handlers = new Set<(message: ExtensionMessage) => void>()
return createRoot((dispose) => ({
dispose,
posted,
receive: (message: ExtensionMessage) => handlers.forEach((handler) => handler(message)),
mention: useFileMention(
{
postMessage: (message) => posted.push(message),
onMessage: (handler) => {
handlers.add(handler)
return () => handlers.delete(handler)
},
},
undefined,
() => true,
worktrees,
),
}))
}
function reply(scope: ReturnType<typeof harness>, paths: string[] = []) {
const request = scope.posted.findLast((message) => message.type === "requestFileSearch")
if (request?.type !== "requestFileSearch") throw new Error("Missing file search request")
scope.receive({ type: "fileSearchResult", requestId: request.requestId, dir: "/repo", paths })
}
describe("Agent Manager worktree references", () => {
it("uses sidebar names and includes all sessions without selecting a transcript", () => {
const state = createProjectStore("project")
state.setWorktrees([tree("named", { label: "Custom name" }), tree("ordered"), tree("empty")])
state.setManagedSessions([
{ id: "ses_first", worktreeId: "ordered", createdAt: "" },
{ id: "ses_second", worktreeId: "ordered", createdAt: "" },
{ id: "ses_local", worktreeId: null, createdAt: "" },
])
state.setTabOrder({ ordered: ["ses_second", "ses_first"] })
const refs = worktreeReferences(
state,
[
{ id: "ses_first", title: "First task" },
{ id: "ses_second", title: "Second task" },
{ id: "ses_local", title: "Local task" },
],
"local",
)
expect(refs.map((ref) => ref.name)).toEqual(["Custom name", "Second task", "empty"])
expect(refs[1].sessions).toEqual([
{ id: "ses_first", title: "First task" },
{ id: "ses_second", title: "Second task" },
])
expect(refs[2].sessions).toEqual([])
})
it("omits current, stale, and busy worktrees from suggestions but retains their reference data", () => {
const state = createProjectStore("project")
state.setWorktrees([tree("current"), tree("stale"), tree("deleting"), tree("other")])
state.setStaleWorktreeIds(new Set(["stale"]))
state.setBusy(new Map([["deleting", { reason: "deleting" }]]))
const refs = worktreeReferences(state, [], "current")
expect(refs).toHaveLength(4)
const scope = harness(() => refs)
expect(scope.mention.worktreeCandidates().map((item) => item.path)).toEqual(["/repo/.kilo/worktrees/other"])
scope.dispose()
})
it("keeps project inventories separate even when worktree IDs match", () => {
const first = createProjectStore("first")
const second = createProjectStore("second")
first.setWorktrees([tree("same", { path: "/first/reference" })])
second.setWorktrees([tree("same", { path: "/second/reference" })])
expect(worktreeReferences(first, [], "local").map((ref) => ref.path)).toEqual(["/first/reference"])
expect(worktreeReferences(second, [], "local").map((ref) => ref.path)).toEqual(["/second/reference"])
})
it("keeps multi-version siblings distinct and supports Windows paths", () => {
const state = createProjectStore("project")
state.setWorktrees([
tree("one", { groupId: "group", label: "Compare", path: "C:\\repo\\one\\" }),
tree("two", { groupId: "group", label: "Compare", path: "C:\\repo\\two" }),
tree("empty", { path: "C:\\repo\\empty\\" }),
])
const refs = worktreeReferences(state, [], "one")
expect(refs.map((ref) => ref.name)).toEqual(["Compare", "Compare", "empty"])
expect(refs.map((ref) => ref.disabled)).toEqual([true, false, false])
expect(refs[0].path).not.toBe(refs[1].path)
})
it("shows one Worktrees entry instead of individual worktrees in the main menu", () => {
for (const query of ["", "w", "worktree", "WORKTREES", "branch"]) {
const results = buildMentionResults(query, [], true, true)
expect(results.filter((item) => item.type === "worktrees")).toEqual([{ type: "worktrees", value: "worktrees" }])
expect(filterMentionResults(query, results).filter((item) => item.type === "worktrees")).toHaveLength(1)
}
expect(buildMentionResults("unrelated", [], true, true).some((item) => item.type === "worktrees")).toBe(false)
expect(buildMentionResults("", []).some((item) => item.type === "worktrees")).toBe(false)
})
it("ranks recent visits ahead of activity, then uses newest activity and sidebar order", () => {
const state = createProjectStore("project")
state.setWorktrees([tree("old"), tree("active"), tree("new", { createdAt: "2026-08-25T00:00:00.000Z" })])
state.setManagedSessions([{ id: "ses_active", worktreeId: "active", createdAt: "" }])
const sessions = [{ id: "ses_active", title: "Recent task", updatedAt: "2026-08-26T00:00:00.000Z" }]
expect(worktreeReferences(state, sessions, "local").map((ref) => ref.id)).toEqual(["active", "new", "old"])
expect(worktreeReferences(state, sessions, "local", [tree("old").path]).map((ref) => ref.id)).toEqual([
"old",
"active",
"new",
])
expect(
worktreeReferences(state, sessions, "local", [tree("new").path, tree("old").path]).map((ref) => ref.id),
).toEqual(["new", "old", "active"])
state.setWorktrees([tree("first"), tree("second")])
state.setWorktreeOrder(["second", "first"])
expect(worktreeReferences(state, [], "local").map((ref) => ref.id)).toEqual(["second", "first"])
})
it("does not let a visit in another project boost a colliding worktree ID", () => {
const state = createProjectStore("first")
state.setWorktrees([tree("same", { path: "/first/same" }), tree("other", { path: "/first/other" })])
const refs = worktreeReferences(state, [], "local", ["/second/same", "/first/other"])
expect(refs.map((ref) => ref.path)).toEqual(["/first/other", "/first/same"])
})
it("attaches only metadata and preserves exact Unicode and spaced paths", () => {
const ref = reference({ name: 'Fix "登录"\nnow', path: "/repo/.kilo/worktrees/登录 100%" })
const text = `Compare with @${ref.path} and report differences.`
const files = buildWorktreeAttachments(text, [ref])
expect(files).toHaveLength(1)
expect(files[0]).toMatchObject({
mime: "text/plain",
source: {
type: "file",
path: ref.path,
text: { value: `@${ref.path}`, start: 13, end: 14 + ref.path.length },
},
})
expect(files[0].url.startsWith("data:text/plain;charset=utf-8,")).toBe(true)
const textfile = content(files[0].url)
expect(JSON.parse(textfile.slice(textfile.indexOf("{")))).toEqual({
worktreeID: ref.id,
name: ref.name,
directory: ref.path,
branch: ref.branch,
baseBranch: ref.base,
sessions: ref.sessions,
})
expect(textfile).toContain("metadata only")
expect(textfile).toContain("recall")
expect(files.some((file) => file.url.startsWith("file:") || file.url.startsWith("session:"))).toBe(false)
})
it("matches whole references, removes deleted references, and deduplicates repeated mentions", () => {
const ref = reference()
expect(buildWorktreeAttachments(`email@${ref.path}`, [ref])).toEqual([])
expect(buildWorktreeAttachments(`@${ref.path}-other`, [ref])).toEqual([])
expect(buildWorktreeAttachments(`@${ref.path} @${ref.path}`, [ref])).toHaveLength(1)
expect(buildWorktreeAttachments("Compare the changes", [ref])).toEqual([])
const longer = reference({ id: "longer", path: `${ref.path} backup` })
expect(buildWorktreeAttachments(`@${longer.path}`, [ref, longer]).map((file) => file.source?.path)).toEqual([
longer.path,
])
})
it("does not auto-read worktree directories inside the local workspace", () => {
const ref = reference({ path: "/repo/.kilo/worktrees/reference branch" })
const scope = harness(() => [ref])
scope.mention.onInput("@", 1)
reply(scope, ["src/a.ts"])
const text = `Compare @${ref.path} with @src/a.ts`
scope.mention.seedFromText(text)
expect(scope.mention.mentionedPaths()).toEqual(new Set([ref.path, "src/a.ts"]))
const files = scope.mention.parseFileAttachments(text)
expect(files).toHaveLength(2)
expect(files.filter((file) => file.url.startsWith("file:"))).toMatchObject([{ source: { path: "src/a.ts" } }])
expect(files.filter((file) => file.source?.path === ref.path)[0].url.startsWith("data:")).toBe(true)
scope.dispose()
})
it("does not attach a truncated path when worktree data arrives after draft restoration", () => {
const ref = reference({ path: "/repo/.kilo/worktrees/reference branch" })
const state = { refs: [] as WorktreeReference[] }
const scope = harness(() => state.refs)
scope.mention.onInput("@", 1)
reply(scope)
const text = `Compare @${ref.path}`
scope.mention.seedFromText(text)
state.refs = [ref]
const files = scope.mention.parseFileAttachments(text)
expect(files).toHaveLength(1)
expect(files[0].source?.path).toBe(ref.path)
expect(files[0].url.startsWith("data:")).toBe(true)
scope.dispose()
})
it("restores references without a prior picker selection and refreshes their metadata", () => {
const state = { refs: [reference()] }
const scope = harness(() => state.refs)
const text = `Compare @${state.refs[0].path}`
scope.mention.seedFromText(text)
expect(scope.mention.parseFileAttachments(text)).toHaveLength(1)
state.refs = [reference({ branch: "renamed", sessions: [{ id: "ses_new" }] })]
const files = scope.mention.parseFileAttachments(text)
expect(content(files[0].url)).toContain('"branch": "renamed"')
expect(content(files[0].url)).toContain("ses_new")
state.refs = []
expect(content(scope.mention.parseFileAttachments(text)[0].url)).toContain(reference().path)
expect(scope.mention.parseFileAttachments("Removed reference")).toEqual([])
scope.dispose()
})
it("retains reference metadata when a worktree disappears after search results arrive", () => {
const ref = reference()
const state = { refs: [] as WorktreeReference[] }
const scope = harness(() => state.refs)
scope.mention.onInput("@", 1)
state.refs = [ref]
reply(scope)
expect(scope.mention.worktreeCandidates().map((item) => item.path)).toEqual([ref.path])
state.refs = []
const text = `Compare @${ref.path}`
scope.mention.seedFromText(text)
const files = scope.mention.parseFileAttachments(text)
expect(files).toHaveLength(1)
expect(files[0].url.startsWith("data:")).toBe(true)
scope.dispose()
})
it("opens the local worktree picker without changing the prompt or requesting past chats", () => {
const state = { refs: [reference(), reference({ id: "tests", name: "Testing", path: "/repo/tests" })] }
const scope = harness(() => state.refs)
scope.mention.onInput("@", 1)
const count = scope.posted.length
const input = { value: "@", selectionStart: 1 } as HTMLTextAreaElement
scope.mention.selectMention({ type: "worktrees", value: "worktrees" }, input, () => {})
expect(scope.mention.worktreePicker()).toBe(true)
expect(scope.mention.sessionPicker()).toBe(false)
expect(scope.mention.worktreeCandidates().map((item) => item.name)).toEqual(["Authentication", "Testing"])
expect(scope.posted).toHaveLength(count)
expect(input.value).toBe("@")
state.refs = [reference({ name: "Renamed" })]
expect(scope.mention.worktreeCandidates().map((item) => item.name)).toEqual(["Renamed"])
expect(scope.mention.parseFileAttachments("@worktrees")).toEqual([])
scope.mention.closeMention()
expect(scope.mention.worktreePicker()).toBe(false)
expect(scope.mention.showMention()).toBe(false)
expect(scope.posted.some((message) => message.type === "requestSessionSearch")).toBe(false)
scope.dispose()
})
it("keeps the Worktrees entry available when there are no other worktrees", () => {
const scope = harness(() => [reference({ disabled: true })])
scope.mention.onInput("@", 1)
expect(scope.mention.mentionResults().filter((item) => item.type === "worktrees")).toHaveLength(1)
expect(scope.mention.worktreeCandidates()).toEqual([])
scope.dispose()
})
})
@@ -1,5 +1,9 @@
import { describe, expect, it } from "bun:test"
import { rootSessions } from "../../webview-ui/agent-manager/project/session-filter"
import {
rootSessions,
worktreeSessionIds,
worktreeSessions,
} from "../../webview-ui/agent-manager/project/session-filter"
import type { ProjectSessionInfo } from "../../webview-ui/src/types/messages"
const session = (id: string, worktreeId: string | null, parentID: string | null): ProjectSessionInfo => ({
@@ -23,4 +27,19 @@ describe("rootSessions", () => {
expect(rootSessions(sessions, null).map((item) => item.id)).toEqual(["root"])
})
it("preserves managed membership, chronological fallback, and custom tab order", () => {
const rows = [
{ ...session("new", "wt-1", null), createdAt: "2026-01-02T00:00:00.000Z" },
session("old", "wt-1", null),
session("child", "wt-1", "old"),
session("other", "wt-2", null),
]
const managed = rows.map((item) => ({ id: item.id, worktreeId: item.worktreeId, createdAt: item.createdAt }))
expect([...worktreeSessionIds("wt-1", managed)]).toEqual(["new", "old", "child"])
expect(worktreeSessions("wt-1", managed, rows, undefined).map((item) => item.id)).toEqual(["old", "new"])
expect(worktreeSessions("wt-1", managed, rows, ["missing", "new"]).map((item) => item.id)).toEqual(["new", "old"])
expect(rows.map((item) => item.id)).toEqual(["new", "old", "child", "other"])
expect(worktreeSessions("missing", managed, rows, undefined)).toEqual([])
})
})
@@ -31,6 +31,32 @@ describe("project session live state", () => {
title: "Restore worktree metadata",
worktreeId: "worktree-1",
})
expect(live.current()).toEqual(live().project!)
dispose()
})
})
it("returns only the active project and keeps the legacy source when disabled", () => {
createRoot((dispose) => {
const state = { pid: "second" as string | undefined, enabled: true, store: [] as SessionInfo[] }
const first = { ...session("same"), title: "First project" }
const second = { ...session("same"), title: "Second project" }
const live = createProjectSessionsLive({
base: () => ({ first: [first], second: [second] }),
pid: () => state.pid,
enabled: () => state.enabled,
store: () => state.store,
managed: () => [],
locals: () => new Set(),
})
expect(live.current()).toEqual([second])
state.pid = "first"
expect(live.current()).toEqual([first])
state.pid = undefined
expect(live.current()).toEqual([])
state.enabled = false
state.store = [first]
expect(live.current()).toEqual([first])
dispose()
})
})
@@ -0,0 +1,74 @@
import { describe, expect, it } from "bun:test"
import path from "node:path"
import { createWorktreeRecency } from "../../webview-ui/agent-manager/worktree-recency"
function storage(value: Record<string, unknown> = {}) {
const state = { value, writes: 0 }
return {
get: () => state.value,
set: (value: Record<string, unknown>) => {
state.value = value
state.writes++
},
state,
}
}
describe("worktree mention recency", () => {
it("tracks real selection changes and project-scoped session metadata", () => {
const file = path.join(import.meta.dir, "../fixtures/worktree-references.ts")
const child = Bun.spawnSync(["bun", "--conditions=browser", file], { stdout: "pipe", stderr: "pipe" })
expect(child.exitCode, child.stdout.toString() + child.stderr.toString()).toBe(0)
})
it("keeps the most recently opened worktree first without duplicate visits", () => {
const data = storage()
const history = createWorktreeRecency(data)
history.visit("/repo/first")
history.visit("/repo/second")
history.visit("/repo/first")
expect(history.recent()).toEqual(["/repo/first", "/repo/second"])
expect(data.get().worktreeMentionHistory).toEqual(history.recent())
history.visit("/repo/first")
history.visit("")
expect(data.state.writes).toBe(3)
})
it("restores visits across picker and webview recreation", () => {
const data = storage()
const first = createWorktreeRecency(data)
first.visit("/repo/first")
first.visit("/repo/second")
const restored = createWorktreeRecency(data)
expect(restored.recent()).toEqual(["/repo/second", "/repo/first"])
restored.visit("/repo/first")
expect(restored.recent()).toEqual(["/repo/first", "/repo/second"])
})
it("preserves unrelated webview state and uses paths rather than ambiguous IDs", () => {
const data = storage({ sidebarWidth: 240, localTabs: { project: ["ses_one"] } })
const history = createWorktreeRecency(data)
history.visit("/first/.kilo/worktrees/same")
data.state.value = { ...data.get(), sidebarWidth: 300 }
history.visit("/second/.kilo/worktrees/same")
expect(history.recent()).toEqual(["/second/.kilo/worktrees/same", "/first/.kilo/worktrees/same"])
expect(data.get()).toEqual({
sidebarWidth: 300,
localTabs: { project: ["ses_one"] },
worktreeMentionHistory: history.recent(),
})
})
it("ignores invalid saved entries and bounds the history", () => {
const data = storage({ worktreeMentionHistory: [null, 42, "", "/repo/one", "/repo/one", "/repo/two"] })
expect(createWorktreeRecency(data).recent()).toEqual(["/repo/one", "/repo/two"])
expect(createWorktreeRecency(storage({ worktreeMentionHistory: "invalid" })).recent()).toEqual([])
const many = storage({ worktreeMentionHistory: Array.from({ length: 110 }, (_, index) => `/repo/${index}`) })
const history = createWorktreeRecency(many)
expect(history.recent()).toHaveLength(100)
history.visit("/repo/new")
expect(history.recent()).toHaveLength(100)
expect(history.recent()[0]).toBe("/repo/new")
expect(history.recent()).not.toContain("/repo/99")
})
})
@@ -85,6 +85,7 @@ import { SidebarBody } from "./SidebarBody"
import { TabBar } from "./TabBar"
import { createProjectLive } from "./project/live"
import { createProjectSessionsLive } from "./project/sessions-live"
import { worktreeSessionIds as worktreeMembership, worktreeSessions } from "./project/session-filter"
import { applyProjectSelection, createTargetRememberer } from "./project/selection"
import { createLocalSessions, persistLocalTabs, projectLocalIds, projectLocalSessions } from "./project/local-tabs"
import { createProjectRegistry, type PersistedProjectTabs } from "./project/registry"
@@ -168,6 +169,7 @@ import { createRevertFile } from "./revert-file"
import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView"
import { createApplyToLocal } from "./apply-to-local"
import { createWorktreeDiffs, diffDataKey, wireDiffId } from "./worktree-diffs"
import { createWorktreeReferences } from "./worktree-references"
import type { ReviewComment } from "../diff-viewer/review-comments"
import { createReviewComposers } from "./review-composers"
import type { SidebarSearchMenuRef } from "./SidebarSearchMenu"
@@ -754,6 +756,7 @@ const AgentManagerContent: Component = () => {
managed: managedSessions,
locals: localSet,
})
const references = createWorktreeReferences(vscode, registry.active, projectSessionsLive.current, selection)
/** Session ids shown in the project-scoped history view (every session of the project). */
const historySessionIds = createMemo(() => {
@@ -776,20 +779,8 @@ const AgentManagerContent: Component = () => {
title: () => t("agentManager.session.newSession"),
})
const sessionsForWorktree = (worktreeId: string): SessionInfo[] => {
const ids = new Set(
managedSessions()
.filter((ms) => ms.worktreeId === worktreeId)
.map((ms) => ms.id),
)
return applyTabOrder(
session
.sessions()
.filter((s) => isKnownRootSession(s) && ids.has(s.id))
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()),
worktreeTabOrder()[worktreeId],
)
}
const sessionsForWorktree = (id: string) =>
worktreeSessions(id, managedSessions(), session.sessions(), worktreeTabOrder()[id])
const activeWorktreeSessions = createMemo((): SessionInfo[] => {
const sel = selection()
@@ -800,11 +791,7 @@ const AgentManagerContent: Component = () => {
const activeWorktreeSessionIds = createMemo<ReadonlySet<string> | undefined>(() => {
const sel = selection()
if (!sel || sel === LOCAL) return undefined
return new Set(
managedSessions()
.filter((item) => item.worktreeId === sel)
.map((item) => item.id),
)
return worktreeMembership(sel, managedSessions())
})
const activeTabs = createMemo((): SessionInfo[] => {
@@ -2540,6 +2527,7 @@ const AgentManagerContent: Component = () => {
</Show>
<div class="am-chat-wrapper" classList={{ "am-chat-wrapper-hidden": contextEmpty() }}>
<ChatView
worktrees={references}
onSelectSession={(id) => {
if (addSessionToCurrentWorktree(id)) return
if (localSessionIDs().includes(id)) {
@@ -1,6 +1,26 @@
import type { ProjectSessionInfo } from "../../src/types/messages"
import type { ManagedSessionState, ProjectSessionInfo, SessionInfo } from "../../src/types/messages"
import { isKnownRootSession } from "../navigate"
import { applyTabOrder } from "../tab-order"
export function rootSessions(sessions: ProjectSessionInfo[], worktreeId: string | null): ProjectSessionInfo[] {
return sessions.filter((session) => session.worktreeId === worktreeId && isKnownRootSession(session))
}
export function worktreeSessionIds(id: string, sessions: ManagedSessionState[]) {
return new Set(sessions.filter((session) => session.worktreeId === id).map((session) => session.id))
}
export function worktreeSessions(
id: string,
managed: ManagedSessionState[],
sessions: SessionInfo[],
order: string[] | undefined,
) {
const ids = worktreeSessionIds(id, managed)
return applyTabOrder(
sessions
.filter((session) => isKnownRootSession(session) && ids.has(session.id))
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()),
order,
)
}
@@ -19,7 +19,7 @@ export function createProjectSessionsLive(opts: {
managed: () => ManagedSessionState[]
locals: () => Set<string>
}) {
return createMemo(() => {
const sessions = createMemo(() => {
const base = opts.base()
const pid = opts.pid()
if (!pid || !opts.enabled()) return base
@@ -41,4 +41,7 @@ export function createProjectSessionsLive(opts: {
if (extra.length === 0 && merged.every((item, index) => item === pushed[index])) return base
return { ...base, [pid]: [...merged, ...extra] }
})
return Object.assign(sessions, {
current: () => (opts.enabled() ? (sessions()[opts.pid() ?? ""] ?? []) : opts.store()),
})
}
@@ -0,0 +1,23 @@
import { createSignal } from "solid-js"
export function createWorktreeRecency(storage: {
get: () => Record<string, unknown> | undefined
set: (state: Record<string, unknown>) => void
}) {
const value = storage.get()?.worktreeMentionHistory
const [recent, setRecent] = createSignal(
Array.isArray(value)
? [...new Set(value.filter((path): path is string => typeof path === "string" && path.length > 0))].slice(0, 100)
: [],
)
const visit = (path: string) => {
if (!path) return
setRecent((previous) => {
if (previous[0] === path) return previous
const next = [path, ...previous.filter((item) => item !== path)].slice(0, 100)
storage.set({ ...storage.get(), worktreeMentionHistory: next })
return next
})
}
return { recent, visit }
}
@@ -0,0 +1,75 @@
import { createEffect, createMemo, type Accessor } from "solid-js"
import type { SessionInfo } from "../src/types/messages"
import type { useVSCode } from "../src/context/vscode"
import type { WorktreeReference } from "../src/hooks/file-mention-utils"
import type { ProjectStore } from "./project/store"
import { firstOrderedTitle } from "./tab-order"
import { sortWorktrees } from "./section-helpers"
import { createWorktreeRecency } from "./worktree-recency"
type Session = Pick<SessionInfo, "id" | "title"> & Partial<Pick<SessionInfo, "updatedAt">>
export function worktreeReferences(
state: ProjectStore,
sessions: Session[],
current: string | null,
recent: string[] = [],
): WorktreeReference[] {
const titles = new Map(sessions.map((session) => [session.id, session.title]))
const updated = new Map(sessions.map((session) => [session.id, Date.parse(session.updatedAt ?? "") || 0]))
const recency = new Map(recent.map((path, index) => [path, index]))
const activity = new Map<string, number>()
const groups = new Map<string, WorktreeReference["sessions"]>()
for (const session of state.managedSessions()) {
if (!session.worktreeId) continue
const group = groups.get(session.worktreeId) ?? []
group.push({ id: session.id, title: titles.get(session.id) })
groups.set(session.worktreeId, group)
}
return sortWorktrees(state.worktrees(), state.worktreeOrder())
.map((worktree) => {
const sessions = groups.get(worktree.id) ?? []
const basename = worktree.path.replaceAll("\\", "/").replace(/\/+$/, "").split("/").pop()
activity.set(
worktree.path,
Math.max(Date.parse(worktree.createdAt) || 0, ...sessions.map((session) => updated.get(session.id) ?? 0)),
)
return {
id: worktree.id,
name: worktree.label || firstOrderedTitle(sessions, state.tabOrder()[worktree.id], basename || worktree.branch),
branch: worktree.branch,
path: worktree.path,
base: worktree.parentBranch,
sessions,
disabled: worktree.id === current || state.staleWorktreeIds().has(worktree.id) || state.busy().has(worktree.id),
}
})
.sort(
(a, b) =>
(recency.get(a.path) ?? recent.length) - (recency.get(b.path) ?? recent.length) ||
(activity.get(b.path) ?? 0) - (activity.get(a.path) ?? 0),
)
}
export function createWorktreeReferences(
vscode: Pick<ReturnType<typeof useVSCode>, "getState" | "setState">,
state: Accessor<ProjectStore>,
sessions: Accessor<Session[]>,
selection: Accessor<string | null>,
) {
const recency = createWorktreeRecency({
get: () => vscode.getState<Record<string, unknown>>(),
set: (value) => vscode.setState(value),
})
const current = createMemo(() => {
const project = state()
const id = selection()
if (!id || project.staleWorktreeIds().has(id) || project.busy().has(id)) return
return project.worktrees().find((worktree) => worktree.id === id)?.path
})
createEffect(() => {
const path = current()
if (path) recency.visit(path)
})
return createMemo(() => worktreeReferences(state(), sessions(), selection(), recency.recent()))
}
@@ -28,6 +28,7 @@ import { useServer } from "../../context/server"
import { TranscriptSearchProvider } from "../../context/transcript-search"
import { isPromptBlocked, isSuggesting, isQuestioning } from "./prompt-input-utils"
import { showTabStrip } from "../../utils/local-tabs"
import type { WorktreeReference } from "../../hooks/file-mention-utils"
interface ChatViewProps {
onSelectSession?: (id: string) => void
@@ -40,6 +41,7 @@ interface ChatViewProps {
worktree?: boolean
promptBoxId?: string
terminalContext?: () => string | undefined
worktrees?: () => WorktreeReference[]
deferFocusToQuestion?: () => boolean
pendingSessionID?: string
focusOnDraftChange?: () => boolean
@@ -391,6 +393,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
worktree={props.worktree}
boxId={props.promptBoxId}
terminalContext={props.terminalContext}
worktrees={props.worktrees}
deferFocusToQuestion={props.deferFocusToQuestion}
pendingSessionID={pendingSessionID()}
focusOnDraftChange={props.focusOnDraftChange}
@@ -27,7 +27,7 @@ import { SpeechToTextButton } from "../speech-to-text/SpeechToTextButton"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../speech-to-text/availability"
import { ThinkingSelector } from "../shared/ThinkingSelector"
import { useFileMention } from "../../hooks/useFileMention"
import type { MentionResult } from "../../hooks/file-mention-utils"
import type { MentionResult, WorktreeReference } from "../../hooks/file-mention-utils"
import { useTerminalContext } from "../../hooks/useTerminalContext"
import { useGitChangesContext } from "../../hooks/useGitChangesContext"
import { hasTerminalMention } from "../../hooks/terminal-context-utils"
@@ -40,6 +40,7 @@ import { createSpeechShortcut } from "../speech-to-text/shortcut"
import { useImageAttachments, type ImageAttachment } from "../../hooks/useImageAttachments"
import { convertToMentionPath } from "../../utils/path-mentions"
import { SessionMentionPicker } from "./SessionMentionPicker"
import { WorktreeMentionPicker } from "./WorktreeMentionPicker"
import { usePromptHistory } from "../../hooks/usePromptHistory"
import { cycleVariant } from "../../context/session-variant-store"
import { WandSparkles } from "@kilocode/kilo-ui/lucide"
@@ -120,6 +121,7 @@ interface PromptInputProps {
worktree?: boolean
boxId?: string
terminalContext?: () => string | undefined
worktrees?: () => WorktreeReference[]
pendingSessionID?: string
/** Agent Manager can suppress automatic prompt focus when this session last
* used its side terminal instead. Other callers retain the old behavior. */
@@ -130,6 +132,7 @@ interface PromptInputProps {
function MentionItemContent(props: { item: MentionResult }) {
const item = props.item
const language = useLanguage()
if (item.type === "terminal")
return (
<>
@@ -138,12 +141,16 @@ function MentionItemContent(props: { item: MentionResult }) {
<span class="file-mention-dir">{item.description}</span>
</>
)
if (item.type === "git-changes")
if (item.type === "git-changes" || item.type === "worktrees")
return (
<>
<Icon name="branch" class="file-mention-icon" />
<span class="file-mention-name">{item.label}</span>
<span class="file-mention-dir">{item.description}</span>
<span class="file-mention-name">
{item.type === "worktrees" ? language.t("prompt.worktrees.title") : item.label}
</span>
<span class="file-mention-dir">
{item.type === "worktrees" ? language.t("prompt.worktrees.search") : item.description}
</span>
</>
)
if (item.type === "past-chats")
@@ -194,7 +201,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return rest === "unassigned" ? undefined : rest
}
const hasGit = () => server.gitInstalled()
const mention = useFileMention(vscode, sid, hasGit)
const mention = useFileMention(vscode, sid, hasGit, props.worktrees)
const terminal = useTerminalContext(props.resolveEmbeddedTerminal)
const git = useGitChangesContext(vscode, ctx, hasGit)
const imageAttach = useImageAttachments()
@@ -391,6 +398,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pending = reviewDrafts.get(key) ?? []
const scroll = scrollDrafts.get(key) ?? 0
setText(draft)
mention.seedFromText(draft)
setReviewComments(pending)
imageAttach.replace(imageDrafts.get(key) ?? [])
setEnhancing(false)
@@ -1337,14 +1345,31 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
>
<Show
when={mention.mentionResults().length > 0}
fallback={<div class="file-mention-empty">No files or folders found</div>}
when={!mention.worktreePicker() && mention.mentionResults().length > 0}
fallback={
<Show
when={mention.worktreePicker()}
fallback={<div class="file-mention-empty">No files or folders found</div>}
>
<WorktreeMentionPicker
worktrees={mention.worktreeCandidates()}
onSelect={(picked) => {
if (textareaRef) mention.selectWorktree(picked, textareaRef, setText, adjustHeight)
}}
onClose={() => {
mention.closeMention()
textareaRef?.focus()
}}
/>
</Show>
}
>
<For each={mention.mentionResults()}>
{(item, index) => (
<>
<div
class="file-mention-item"
data-type={item.type}
classList={{ "file-mention-item--active": index() === mention.mentionIndex() }}
onMouseDown={(e) => {
e.preventDefault()
@@ -0,0 +1,59 @@
import { createMemo, onMount } from "solid-js"
import { Icon } from "@kilocode/kilo-ui/icon"
import { List } from "@kilocode/kilo-ui/list"
import { useLanguage } from "../../context/language"
import type { WorktreeReference } from "../../hooks/file-mention-utils"
interface Props {
worktrees: WorktreeReference[]
onSelect: (worktree: WorktreeReference) => void
onClose: () => void
}
export function WorktreeMentionPicker(props: Props) {
const language = useLanguage()
const items = createMemo(() =>
props.worktrees.map((worktree) => ({
...worktree,
search: [worktree.name, worktree.branch, ...worktree.sessions.map((session) => session.title)].join(" "),
})),
)
let root: HTMLDivElement | undefined
onMount(() => queueMicrotask(() => root?.querySelector("input")?.focus({ preventScroll: true })))
return (
<div
ref={root}
class="session-mention-picker worktree-mention-picker"
onKeyDown={(event) => {
if (event.key !== "Escape" || event.isComposing) return
event.preventDefault()
event.stopPropagation()
props.onClose()
}}
>
<List
items={items()}
key={(item) => item.path}
filterKeys={["name", "branch", "search"]}
search={{ placeholder: language.t("prompt.worktrees.search"), autofocus: true }}
onSelect={(item) => {
if (item) props.onSelect(item)
}}
onKeyEvent={(event, item) => {
if (event.key !== "Tab" || event.shiftKey || event.isComposing || !item) return
event.preventDefault()
props.onSelect(item)
}}
>
{(item) => (
<span class="session-mention-item" title={item.path}>
<Icon name="branch" class="file-mention-icon" />
<span class="session-mention-title">{item.name}</span>
<span class="session-mention-worktree">{item.branch}</span>
</span>
)}
</List>
</div>
)
}
@@ -4,6 +4,16 @@ import { TERMINAL_MENTION } from "./terminal-context-utils"
export const AT_PATTERN = /(?:^|\s)@(\S*)$/
export type WorktreeReference = {
id: string
name: string
branch: string
path: string
base: string
sessions: { id: string; title?: string }[]
disabled: boolean
}
export type MentionResult =
| { type: "terminal"; value: typeof TERMINAL_MENTION; label: string; description: string }
| { type: "git-changes"; value: typeof GIT_CHANGES_MENTION; label: string; description: string }
@@ -13,9 +23,11 @@ export type MentionResult =
| { type: "folder"; value: string }
| { type: "file-picker"; value: "file-picker"; label: string; description: string }
| { type: "session"; value: string; session: SessionSearchItem }
| { type: "worktrees"; value: "worktrees" }
export const PAST_CHATS_MENTION = "past-chats"
const PAST_CHATS_ALIASES = ["past", "chats", "sessions", "session", "history"]
const WORKTREE_ALIASES = ["worktrees", "branches"]
export const TERMINAL_RESULT: MentionResult = {
type: "terminal",
@@ -63,7 +75,16 @@ export function getPastChatsMentionResult(query: string): MentionResult[] {
return [PAST_CHATS_RESULT]
}
export function buildMentionResults(query: string, items: Array<FileSearchItem | string>, git = true): MentionResult[] {
export function buildMentionResults(
query: string,
items: Array<FileSearchItem | string>,
git = true,
worktrees = false,
): MentionResult[] {
const references: MentionResult[] =
worktrees && WORKTREE_ALIASES.some((alias) => alias.startsWith(query.toLowerCase()))
? [{ type: "worktrees", value: "worktrees" }]
: []
const results: MentionResult[] = items.map((item) => {
if (typeof item === "string") return { type: "file", value: item }
if (item.type === "folder") return { type: "folder", value: item.path }
@@ -74,6 +95,7 @@ export function buildMentionResults(query: string, items: Array<FileSearchItem |
...getTerminalMentionResult(query),
...(git ? getGitChangesMentionResult(query) : []),
...getPastChatsMentionResult(query),
...filterMentionResults(query, references),
...results,
FILE_PICKER_RESULT,
]
@@ -115,6 +137,7 @@ export function filterMentionResults(query: string, items: MentionResult[]): Men
if (item.type === "git-changes") return GIT_CHANGES_MENTION.startsWith(value) || "git".startsWith(value)
if (item.type === "past-chats") return PAST_CHATS_ALIASES.some((alias) => alias.startsWith(value))
if (item.type === "file-picker") return true
if (item.type === "worktrees") return WORKTREE_ALIASES.some((alias) => alias.startsWith(value))
return item.value.toLowerCase().includes(value)
})
}
@@ -337,6 +360,38 @@ export function buildFileAttachments(
return result
}
export function buildWorktreeAttachments(text: string, worktrees: WorktreeReference[]): FileAttachment[] {
const paths = syncMentionedPaths(new Set(worktrees.map((worktree) => worktree.path)), text)
return worktrees
.filter((worktree) => paths.has(worktree.path))
.map((worktree) => {
const value = `@${worktree.path}`
const start = text.indexOf(value)
const content = [
"Agent Manager worktree reference (metadata only, not file contents or conversation history).",
"Use the directory to inspect files or git changes. Use the session IDs with Agent Manager or recall if needed.",
JSON.stringify(
{
worktreeID: worktree.id,
name: worktree.name,
directory: worktree.path,
branch: worktree.branch,
baseBranch: worktree.base,
sessions: worktree.sessions,
},
null,
2,
),
].join("\n\n")
return {
mime: "text/plain",
url: `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`,
filename: `worktree-${worktree.id}.txt`,
source: { type: "file", path: worktree.path, text: { value, start, end: start + value.length } },
}
})
}
/**
* Sync mentioned sessions against the current text: drop entries whose
* `@title` token is no longer present. Uses the same boundary-aware matching
@@ -13,6 +13,7 @@ import {
buildFileAttachments,
buildMentionResults,
buildSessionAttachments,
buildWorktreeAttachments,
filterMentionResults,
isCursorAtMentionEnd,
getMentionRemovalRange,
@@ -22,6 +23,7 @@ import {
syncMentionedSessions as _syncMentionedSessions,
FILE_PICKER_RESULT,
type MentionResult,
type WorktreeReference,
} from "./file-mention-utils"
const FILE_SEARCH_DEBOUNCE_MS = 150
@@ -54,6 +56,14 @@ export interface FileMention {
sessionPicker: Accessor<boolean>
/** Directory-scoped past chats shown in the session picker. */
sessionCandidates: Accessor<SessionSearchItem[]>
worktreePicker: Accessor<boolean>
worktreeCandidates: Accessor<WorktreeReference[]>
selectWorktree: (
worktree: WorktreeReference,
textarea: HTMLTextAreaElement,
setText: (text: string) => void,
onSelect?: () => void,
) => void
mentionResults: Accessor<MentionResult[]>
mentionIndex: Accessor<number>
showMention: Accessor<boolean>
@@ -124,6 +134,7 @@ export function useFileMention(
vscode: VSCodeContext,
sessionID?: Accessor<string | undefined>,
git?: Accessor<boolean>,
worktrees?: Accessor<WorktreeReference[]>,
): FileMention {
const [mentionedPaths, setMentionedPaths] = createSignal<Set<string>>(new Set())
const [mentionedSessions, setMentionedSessions] = createSignal<Map<string, SessionSearchItem>>(new Map())
@@ -132,6 +143,8 @@ export function useFileMention(
const [mentionIndex, setMentionIndex] = createSignal(0)
const [sessionPicker, setSessionPicker] = createSignal(false)
const [sessionCandidates, setSessionCandidates] = createSignal<SessionSearchItem[]>([])
const [worktreePicker, setWorktreePicker] = createSignal(false)
const worktreeCandidates = () => worktrees?.().filter((worktree) => !worktree.disabled) ?? []
let workspaceDir = ""
const cache = new Map<string, FileSearchCache>()
const dirs = new Map<string, string>()
@@ -141,6 +154,18 @@ export function useFileMention(
// Same accumulation for past-chat mentions, keyed by their exact visible
// token. Duplicate titles receive a numeric suffix so they cannot overwrite.
const knownSessions = new Map<string, SessionSearchItem>()
const knownWorktrees = new Map<string, WorktreeReference>()
const references = () => {
for (const worktree of worktrees?.() ?? []) {
knownWorktrees.set(worktree.path, worktree)
knownPaths.add(worktree.path)
}
return [...knownWorktrees.values()]
}
const results = (query: string, items: Array<FileSearchItem | string>) => {
references()
return buildMentionResults(query, items, git?.() ?? true, worktrees !== undefined)
}
let fileSearchTimer: ReturnType<typeof setTimeout> | undefined
let fileSearchCounter = 0
@@ -172,6 +197,7 @@ export function useFileMention(
fileSearchRequest = undefined
prewarmRequest = undefined
workspaceDir = dirs.get(value) ?? ""
setWorktreePicker(false)
setMentionResults([])
setMentionIndex(0)
return value
@@ -274,7 +300,7 @@ export function useFileMention(
}
if (!request.query) writeCache(message.dir, items, request.revision)
if (!showMention() || request.query !== mentionQuery()) return
replaceResults(buildMentionResults(request.query, items, git?.() ?? true))
replaceResults(results(request.query, items))
})
onCleanup(() => {
@@ -315,6 +341,7 @@ export function useFileMention(
setMentionQuery(null)
setMentionResults([])
setSessionPicker(false)
setWorktreePicker(false)
}
const closeSessionPicker = () => {
@@ -322,6 +349,7 @@ export function useFileMention(
}
const syncMentionedPaths = (text: string) => {
references()
setMentionedPaths(() => _syncMentionedPaths(knownPaths, text))
setMentionedSessions(() => _syncMentionedSessions(knownSessions, text))
}
@@ -363,6 +391,12 @@ export function useFileMention(
return
}
if (result.type === "worktrees") {
references()
setWorktreePicker(true)
return
}
if (result.type === "past-chats") {
// Switch the dropdown into the AM-style session search; the actual
// insertion happens when a session is picked there.
@@ -404,6 +438,17 @@ export function useFileMention(
onSelect?.()
}
const selectWorktree = (
worktree: WorktreeReference,
textarea: HTMLTextAreaElement,
setText: (text: string) => void,
onSelect?: () => void,
) => {
if (worktree.disabled) return
knownWorktrees.set(worktree.path, worktree)
selectMention({ type: "file", value: worktree.path }, textarea, setText, onSelect)
}
const selectSession = (
session: SessionSearchItem,
textarea: HTMLTextAreaElement,
@@ -425,6 +470,7 @@ export function useFileMention(
syncMentionedPaths(val)
if (suppress) return
closeSessionPicker()
setWorktreePicker(false)
const before = val.substring(0, cursor)
const match = before.match(AT_PATTERN)
if (match) {
@@ -432,16 +478,19 @@ export function useFileMention(
setMentionQuery(query)
const items = readCache(workspaceDir)
if (!query) {
setMentionResults(buildMentionResults("", items, git?.() ?? true))
setMentionResults(results("", items))
setMentionIndex(0)
requestFileSearch("")
return
}
setMentionResults((prev) => {
const base = prev.length ? prev : buildMentionResults("", items, git?.() ?? true)
const next = filterMentionResults(query, base)
if (next.length) return next
return buildMentionResults(query, [], git?.() ?? true)
const base = prev.length ? prev : results("", items)
const files = filterMentionResults(query, base).flatMap((item) =>
item.type === "file" || item.type === "folder" || item.type === "opened-file"
? [{ path: item.value, type: item.type }]
: [],
)
return results(query, files)
})
setMentionIndex(0)
requestFileSearch(query)
@@ -500,10 +549,15 @@ export function useFileMention(
// and selection snapping: file paths plus past-chat title tokens.
const mentionTokens = () => new Set([...mentionedPaths(), ...mentionedSessions().keys()])
const parseFileAttachments = (text: string): FileAttachment[] => [
...buildFileAttachments(text, mentionedPaths(), workspaceDir),
...buildSessionAttachments(text, mentionedSessions()),
]
const parseFileAttachments = (text: string): FileAttachment[] => {
const worktrees = references()
const paths = new Set([..._syncMentionedPaths(knownPaths, text)].filter((path) => !knownWorktrees.has(path)))
return [
...buildFileAttachments(text, paths, workspaceDir),
...buildSessionAttachments(text, mentionedSessions()),
...buildWorktreeAttachments(text, worktrees),
]
}
const handleBackspace = (
e: KeyboardEvent,
@@ -696,6 +750,9 @@ export function useFileMention(
mentionedSessions,
sessionPicker,
sessionCandidates,
worktreePicker,
worktreeCandidates,
selectWorktree,
mentionResults,
mentionIndex,
showMention,
+2
View File
@@ -170,6 +170,8 @@ export const dict = {
"common.saving": "جارٍ الحفظ...",
"common.default": "افتراضي",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "البحث في Worktrees",
"prompt.thinking.tooltip": "جهد الاستدلال",
"prompt.action.send": "إرسال",
"prompt.action.send.blocked": "أجب عن السؤال المعلق أو تجاهله أولاً",
+2
View File
@@ -174,6 +174,8 @@ export const dict = {
"common.saving": "Salvando...",
"common.default": "Padrão",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Pesquisar Worktrees",
"prompt.thinking.tooltip": "Esforço de raciocínio",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda ou feche a pergunta pendente primeiro",
+2
View File
@@ -175,6 +175,8 @@ export const dict = {
"common.saving": "Čuvanje...",
"common.default": "Podrazumijevano",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Pretraži Worktree-ove",
"prompt.thinking.tooltip": "Napor razmišljanja",
"prompt.action.send": "Pošalji",
"prompt.action.send.blocked": "Prvo odgovorite ili odbacite pitanje na čekanju",
+2
View File
@@ -174,6 +174,8 @@ export const dict = {
"common.saving": "Gemmer...",
"common.default": "Standard",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Søg i Worktrees",
"prompt.thinking.tooltip": "Ræsonnementsindsats",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Besvar eller afvis det afventende spørgsmål først",
@@ -180,6 +180,8 @@ export const dict = {
"common.saving": "Speichert...",
"common.default": "Standard",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Worktrees durchsuchen",
"prompt.thinking.tooltip": "Reasoning-Aufwand",
"prompt.action.send": "Senden",
"prompt.action.send.blocked": "Beantworten oder verwerfen Sie zuerst die ausstehende Frage",
@@ -174,6 +174,8 @@ export const dict = {
"common.saving": "Saving...",
"common.default": "Default",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Search worktrees",
"prompt.thinking.tooltip": "Reasoning effort",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Answer or dismiss the pending question first",
+2
View File
@@ -175,6 +175,8 @@ export const dict = {
"common.saving": "Guardando...",
"common.default": "Predeterminado",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Buscar Worktrees",
"prompt.thinking.tooltip": "Esfuerzo de razonamiento",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda o descarte la pregunta pendiente primero",
+2
View File
@@ -175,6 +175,8 @@ export const dict = {
"common.saving": "در حال ذخیره...",
"common.default": "پیش‌فرض",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "جستجوی worktree‌ها",
"prompt.thinking.tooltip": "میزان استدلال",
"prompt.action.send": "ارسال",
"prompt.action.send.blocked": "ابتدا به سؤال در انتظار پاسخ دهید یا آن را رد کنید",
+2
View File
@@ -175,6 +175,8 @@ export const dict = {
"common.saving": "Enregistrement...",
"common.default": "Défaut",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Rechercher des Worktrees",
"prompt.thinking.tooltip": "Effort de raisonnement",
"prompt.action.send": "Envoyer",
"prompt.action.send.blocked": "Répondez ou rejetez d'abord la question en attente",
+2
View File
@@ -154,6 +154,8 @@ export const dict = {
"common.save": "Salva",
"common.saving": "Salvataggio...",
"common.default": "Predefinito",
"prompt.worktrees.title": "Worktree",
"prompt.worktrees.search": "Cerca worktree",
"prompt.thinking.tooltip": "Sforzo di ragionamento",
"prompt.action.send": "Invia",
"prompt.action.send.blocked": "Rispondi alla domanda in sospeso o ignorala prima di continuare",
+2
View File
@@ -174,6 +174,8 @@ export const dict = {
"common.saving": "保存中...",
"common.default": "デフォルト",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Worktreeを検索",
"prompt.thinking.tooltip": "推論の強度",
"prompt.action.send": "送信",
"prompt.action.send.blocked": "最初に保留中の質問に答えるか、閉じてください",
+2
View File
@@ -177,6 +177,8 @@ export const dict = {
"common.saving": "저장 중...",
"common.default": "기본값",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Worktree 검색",
"prompt.thinking.tooltip": "추론 강도",
"prompt.action.send": "전송",
"prompt.action.send.blocked": "먼저 대기 중인 질문에 답하거나 닫아주세요",
+2
View File
@@ -175,6 +175,8 @@ export const dict = {
"common.saving": "Bezig met opslaan...",
"common.default": "Standaard",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Worktrees doorzoeken",
"prompt.thinking.tooltip": "Redeneringsinspanning",
"prompt.action.send": "Verzenden",
"prompt.action.send.blocked": "Beantwoord of negeer eerst de openstaande vraag",
+2
View File
@@ -177,6 +177,8 @@ export const dict = {
"common.saving": "Lagrer...",
"common.default": "Standard",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Søk i Worktrees",
"prompt.thinking.tooltip": "Resonnementsinnsats",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Svar på eller avvis det ventende spørsmålet først",
+2
View File
@@ -174,6 +174,8 @@ export const dict = {
"common.saving": "Zapisywanie...",
"common.default": "Domyślny",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Wyszukaj Worktree",
"prompt.thinking.tooltip": "Wysiłek rozumowania",
"prompt.action.send": "Wyślij",
"prompt.action.send.blocked": "Najpierw odpowiedz na oczekujące pytanie lub je odrzuć",
+2
View File
@@ -173,6 +173,8 @@ export const dict = {
"common.saving": "Сохранение...",
"common.default": "По умолчанию",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "Поиск worktrees",
"prompt.thinking.tooltip": "Усилие рассуждения",
"prompt.action.send": "Отправить",
"prompt.action.send.blocked": "Сначала ответьте на ожидающий вопрос или отклоните его",
+2
View File
@@ -173,6 +173,8 @@ export const dict = {
"common.saving": "กำลังบันทึก...",
"common.default": "ค่าเริ่มต้น",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "ค้นหา Worktree",
"prompt.thinking.tooltip": "ความพยายามในการให้เหตุผล",
"prompt.action.send": "ส่ง",
"prompt.action.send.blocked": "โปรดตอบหรือข้ามคำถามที่รอดำเนินการก่อน",
+2
View File
@@ -174,6 +174,8 @@ export const dict = {
"common.saving": "Kaydediliyor...",
"common.default": "Varsayılan",
"prompt.worktrees.title": "Worktree'ler",
"prompt.worktrees.search": "Worktree'leri ara",
"prompt.thinking.tooltip": "Akıl yürütme eforu",
"prompt.action.send": "Gönder",
"prompt.action.send.blocked": "Bekleyen soruyu önce yanıtlayın veya kapatın",
+2
View File
@@ -175,6 +175,8 @@ export const dict = {
"common.saving": "Збереження...",
"common.default": "За замовчуванням",
"prompt.worktrees.title": "Робочі дерева",
"prompt.worktrees.search": "Пошук робочих дерев",
"prompt.thinking.tooltip": "Зусилля міркування",
"prompt.action.send": "Надіслати",
"prompt.action.send.blocked": "Спочатку дайте відповідь або закрийте очікуюче питання",
+2
View File
@@ -169,6 +169,8 @@ export const dict = {
"common.saving": "保存中...",
"common.default": "默认",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "搜索 Worktree",
"prompt.thinking.tooltip": "推理强度",
"prompt.action.indexing": "索引设置",
"prompt.action.autoApprove.enable": "启用自动审批",
+2
View File
@@ -169,6 +169,8 @@ export const dict = {
"common.saving": "儲存中...",
"common.default": "預設",
"prompt.worktrees.title": "Worktrees",
"prompt.worktrees.search": "搜尋 Worktree",
"prompt.thinking.tooltip": "推理強度",
"prompt.action.send": "傳送",
"prompt.action.send.blocked": "請先回答或忽略待處理的問題",
@@ -148,6 +148,10 @@
white-space: nowrap;
}
.worktree-mention-picker [data-slot="list-item"] {
text-align: start;
}
/* ============================================
Slash Command Dropdown
============================================ */
+1 -1
View File
@@ -307,7 +307,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
icon="circle-x"
variant="ghost"
onClick={() => {
setInternalFilter("")
applyFilter("") // kilocode_change
queueMicrotask(() => inputRef?.focus())
}}
aria-label={i18n.t("ui.list.clearFilter")}