diff --git a/.changeset/new-worktree-mentions.md b/.changeset/new-worktree-mentions.md new file mode 100644 index 000000000000..e6101887eadc --- /dev/null +++ b/.changeset/new-worktree-mentions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add `@model`, `@past-chats`, and `@worktrees` mentions to the Agent Manager New Worktree prompt. Past chats and worktrees are sent as attachments to the new session, and model references stay inline text. diff --git a/packages/kilo-vscode/tests/unit/agent-manager-worktree-reference.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-worktree-reference.test.ts index 27aa709ca4f8..3994a81cf75c 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-worktree-reference.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-worktree-reference.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "bun:test" import { createRoot } from "solid-js" -import { worktreeDropReference, worktreeReferences } from "../../webview-ui/agent-manager/worktree-references" +import { + worktreeDropReference, + worktreeReferences, + createWorktreeMentionReferences, +} from "../../webview-ui/agent-manager/worktree-references" import { createProjectStore } from "../../webview-ui/agent-manager/project/store" import { buildMentionResults, @@ -130,6 +134,27 @@ describe("Agent Manager worktree references", () => { scope.dispose() }) + it("keeps the selected worktree in the dialog list but leaves stale and busy disabled", () => { + const state = createProjectStore("project") + state.setWorktrees([tree("current"), tree("stale"), tree("busy"), tree("other")]) + state.setStaleWorktreeIds(new Set(["stale"])) + state.setBusy(new Map([["busy", { reason: "deleting" }]])) + let stored: Record | undefined + createRoot((dispose) => { + const { dialogRefs } = createWorktreeMentionReferences( + { getState: () => stored, setState: (value) => (stored = value) } as never, + () => state, + () => [], + () => "current", + ) + const dialog = dialogRefs() + expect(dialog.find((ref) => ref.id === "current")?.disabled).toBe(false) + expect(dialog.find((ref) => ref.id === "stale")?.disabled).toBe(true) + expect(dialog.find((ref) => ref.id === "busy")?.disabled).toBe(true) + dispose() + }) + }) + it("keeps project inventories separate even when worktree IDs match", () => { const first = createProjectStore("first") const second = createProjectStore("second") diff --git a/packages/kilo-vscode/tests/unit/escape-regexp.test.ts b/packages/kilo-vscode/tests/unit/escape-regexp.test.ts new file mode 100644 index 000000000000..523e9dd582cb --- /dev/null +++ b/packages/kilo-vscode/tests/unit/escape-regexp.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "bun:test" +import { escapeRegExp } from "../../webview-ui/src/utils/escape-regexp" + +describe("escapeRegExp", () => { + it("escapes regex syntax so the value only matches literally", () => { + const value = "a.b(c)+d?[e]\\f^g$h|i{j}" + const pattern = new RegExp(`^${escapeRegExp(value)}$`) + expect(pattern.test(value)).toBe(true) + expect(pattern.test(`${value}x`)).toBe(false) + expect(pattern.test(value.slice(0, -1))).toBe(false) + }) + + it("leaves plain text matchable", () => { + expect(new RegExp(`^${escapeRegExp("plain text")}$`).test("plain text")).toBe(true) + expect(new RegExp(`^${escapeRegExp("1.2.3")}$`).test("1x2x3")).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts b/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts index 59b7fbd17104..aac2c87b8b11 100644 --- a/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts @@ -12,6 +12,7 @@ import { isCursorAtMentionEnd, findMentionRange, mentionSettled, + segmentMentionText, sessionMentionFilename, sessionMentionText, sessionMentionToken, @@ -921,3 +922,46 @@ describe("modelReferenceToken", () => { expect(kept.has(token)).toBe(true) }) }) + +describe("segmentMentionText", () => { + it("marks inserted mention tokens and keeps plain text as-is", () => { + const tokens = new Set(["kilo/kilo-auto/free", "Fix auth bug", "/repo/.kilo/worktrees/feature"]) + const segments = segmentMentionText( + "see @Fix auth bug then @/repo/.kilo/worktrees/feature use @kilo/kilo-auto/free", + tokens, + ) + expect(segments.map((segment) => [segment.mention, segment.text])).toEqual([ + [false, "see "], + [true, "@Fix auth bug"], + [false, " then "], + [true, "@/repo/.kilo/worktrees/feature"], + [false, " use "], + [true, "@kilo/kilo-auto/free"], + ]) + }) + + it("keeps multiple spaces and newlines intact", () => { + const segments = segmentMentionText("a b\n @Fix auth bug", new Set(["Fix auth bug"])) + expect(segments.map((segment) => segment.text).join("")).toBe("a b\n @Fix auth bug") + expect(segments.filter((segment) => segment.mention).map((segment) => segment.text)).toEqual(["@Fix auth bug"]) + }) + + it("matches the longest token first", () => { + const segments = segmentMentionText("@Fix auth bug", new Set(["Fix", "Fix auth bug"])) + expect(segments.filter((segment) => segment.mention).map((segment) => segment.text)).toEqual(["@Fix auth bug"]) + }) + + it("matches tokens that contain regex metacharacters literally", () => { + const tokens = new Set(["pkg.name(v2)+beta", "C:\\repo\\wt"]) + const segments = segmentMentionText("@pkg.name(v2)+beta and @C:\\repo\\wt", tokens) + expect(segments.filter((segment) => segment.mention).map((segment) => segment.text)).toEqual([ + "@pkg.name(v2)+beta", + "@C:\\repo\\wt", + ]) + }) + + it("returns one plain segment when there are no tokens or no text", () => { + expect(segmentMentionText("plain", new Set())).toEqual([{ text: "plain", mention: false }]) + expect(segmentMentionText("", new Set(["model"]))).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/worktree-mention.test.ts b/packages/kilo-vscode/tests/unit/worktree-mention.test.ts new file mode 100644 index 000000000000..fbd101a95e0f --- /dev/null +++ b/packages/kilo-vscode/tests/unit/worktree-mention.test.ts @@ -0,0 +1,236 @@ +import { describe, expect, it } from "bun:test" +import { createRoot } from "solid-js" +import { + buildWorktreeMentionFiles, + buildWorktreeMentionResults, + useWorktreeMention, + WORKTREE_MENTION_ENTRIES, + type WorktreeMention, +} from "../../webview-ui/agent-manager/worktree-mention" +import type { WorktreeReference } from "../../webview-ui/src/hooks/file-mention-utils" +import type { ExtensionMessage, SessionSearchItem, WebviewMessage } from "../../webview-ui/src/types/messages" + +const CHAT: SessionSearchItem = { id: "ses_chat", title: "Fix auth bug", updated: 10 } +const WORKTREE: WorktreeReference = { + id: "wt-other", + name: "other-worktree", + branch: "other-branch", + path: "/repo/.kilo/worktrees/other", + base: "main", + sessions: [{ id: "ses_chat", title: "Fix auth bug" }], + disabled: false, +} + +/** Textarea stub whose value and selection mutate like a real prompt box. */ +function area(initial: string, cursor = initial.length) { + const state = { value: initial, start: cursor, end: cursor } + return { + get value() { + return state.value + }, + set value(value: string) { + state.value = value + }, + get selectionStart() { + return state.start + }, + get selectionEnd() { + return state.end + }, + isConnected: true, + focus: () => {}, + setSelectionRange: (start: number, end = start) => { + state.start = start + state.end = end + }, + } as unknown as HTMLTextAreaElement +} + +function harness(worktrees: WorktreeReference[] = [WORKTREE]) { + const posted: WebviewMessage[] = [] + const handlers = new Set<(message: ExtensionMessage) => void>() + const ctx = { + postMessage: (message: WebviewMessage) => posted.push(message), + onMessage: (handler: (message: ExtensionMessage) => void) => { + handlers.add(handler) + return () => handlers.delete(handler) + }, + } + const dispose: { fn?: () => void } = {} + const mention = createRoot((root) => { + dispose.fn = root + return useWorktreeMention(ctx, () => worktrees) + }) + const sessions = (list: SessionSearchItem[]) => { + for (const handler of handlers) { + handler({ type: "sessionSearchResult", sessions: list, requestId: "worktree-mention-session-1" }) + } + } + return { mention, posted, sessions, dispose } +} + +function pick(mention: WorktreeMention, type: string) { + return mention.mentionResults().find((item) => item.type === type) +} + +describe("buildWorktreeMentionResults", () => { + it("lists only the three worktree-independent entries for a bare @", () => { + expect(WORKTREE_MENTION_ENTRIES.map((entry) => entry.type)).toEqual(["model", "past-chats", "worktrees"]) + expect(buildWorktreeMentionResults("").map((entry) => entry.type)).toEqual(["model", "past-chats", "worktrees"]) + }) + + it("never offers file, folder, terminal or git changes", () => { + const types = buildWorktreeMentionResults("src").map((entry) => entry.type) + expect(types).not.toContain("file") + expect(types).not.toContain("folder") + expect(types).not.toContain("terminal") + expect(types).not.toContain("git-changes") + }) + + it("ranks a matching past chat next to the built-in entries", () => { + const session = { type: "session", value: CHAT.title, session: CHAT } as const + const results = buildWorktreeMentionResults("Fix", [session]) + expect(results.some((entry) => entry.type === "session")).toBe(true) + }) +}) + +describe("buildWorktreeMentionFiles", () => { + it("builds a session: attachment for a mentioned past chat", () => { + const files = buildWorktreeMentionFiles("@Fix auth bug ", new Map([[CHAT.title, CHAT]]), []) + expect(files).toHaveLength(1) + expect(files[0]!.url).toBe("session:ses_chat") + expect(files[0]!.source?.text.value).toBe("@Fix auth bug") + }) + + it("builds a data: metadata attachment for a mentioned worktree", () => { + const files = buildWorktreeMentionFiles(`@${WORKTREE.path}`, new Map(), [WORKTREE]) + expect(files).toHaveLength(1) + expect(files[0]!.url.startsWith("data:text/plain")).toBe(true) + expect(files[0]!.source?.path).toBe(WORKTREE.path) + }) + + it("produces no attachment for a model reference or unmatched text", () => { + expect(buildWorktreeMentionFiles("@provider/model", new Map(), [WORKTREE])).toEqual([]) + expect(buildWorktreeMentionFiles("plain prompt", new Map([[CHAT.title, CHAT]]), [WORKTREE])).toEqual([]) + }) +}) + +describe("useWorktreeMention", () => { + it("opens on a bare @ with the three entries and requests past chats once", () => { + const { mention, posted, dispose } = harness() + mention.onInput("@", 1) + expect(mention.showMention()).toBe(true) + expect(mention.mentionResults().map((entry) => entry.type)).toEqual(["model", "past-chats", "worktrees"]) + expect(posted.filter((message) => message.type === "requestSessionSearch")).toHaveLength(1) + + mention.onInput("@m", 2) + expect(posted.filter((message) => message.type === "requestSessionSearch")).toHaveLength(1) + dispose.fn?.() + }) + + it("inserts a past chat as @title and attaches its session id", () => { + const { mention, sessions, dispose } = harness() + const input = area("@Fix", 4) + mention.onInput("@Fix", 4) + sessions([CHAT]) + + const result = pick(mention, "session") + expect(result).toBeDefined() + mention.selectMention(result!, input, () => {}) + + expect(input.value).toBe("@Fix auth bug ") + expect(mention.mentionedSessions().get("Fix auth bug")?.id).toBe("ses_chat") + expect(mention.highlightTokens().has("Fix auth bug")).toBe(true) + expect(mention.parseAttachments(input.value).map((file) => file.url)).toEqual(["session:ses_chat"]) + dispose.fn?.() + }) + + it("inserts a worktree path and attaches metadata only", () => { + const { mention, dispose } = harness() + const input = area("@", 1) + mention.onInput("@", 1) + mention.selectWorktree(WORKTREE, input, () => {}) + + expect(input.value).toBe(`@${WORKTREE.path} `) + expect(mention.highlightTokens().has(WORKTREE.path)).toBe(true) + const files = mention.parseAttachments(input.value) + expect(files).toHaveLength(1) + expect(files[0]!.url.startsWith("data:text/plain")).toBe(true) + dispose.fn?.() + }) + + it("excludes disabled worktrees from the picker and from selection", () => { + const stale: WorktreeReference = { + ...WORKTREE, + id: "wt-stale", + name: "stale", + path: "/repo/.kilo/worktrees/stale", + disabled: true, + } + const { mention, dispose } = harness([WORKTREE, stale]) + mention.onInput("@", 1) + mention.selectMention(pick(mention, "worktrees")!, area("@", 1), () => {}) + expect(mention.worktreeCandidates().map((worktree) => worktree.id)).toEqual(["wt-other"]) + + const input = area("@", 1) + mention.selectWorktree(stale, input, () => {}) + expect(input.value).toBe("@") + dispose.fn?.() + }) + + it("inserts a model token as plain text with no attachment", () => { + const { mention, dispose } = harness() + const input = area("@", 1) + mention.onInput("@", 1) + const model = pick(mention, "model")! + mention.selectMention(model, input, () => {}) + expect(mention.modelPicker()).toBe(true) + + mention.selectModelReference("provider", "model-x") + expect(input.value).toBe("@provider/model-x ") + expect(mention.highlightTokens().has("provider/model-x")).toBe(true) + expect(mention.parseAttachments(input.value)).toEqual([]) + dispose.fn?.() + }) + + it("closes the menu once prose follows a completed mention", () => { + const { mention, sessions, dispose } = harness() + const input = area("@Fix", 4) + mention.onInput("@Fix", 4) + sessions([CHAT]) + mention.selectMention(pick(mention, "session")!, input, () => {}) + + mention.onInput("@Fix auth bug and then", 21) + expect(mention.showMention()).toBe(false) + dispose.fn?.() + }) + + it("closes the past-chat picker when the prompt text changes", () => { + const { mention, dispose } = harness() + const input = area("@", 1) + mention.onInput("@", 1) + mention.selectMention(pick(mention, "past-chats")!, input, () => {}) + expect(mention.sessionPicker()).toBe(true) + + mention.onInput("@q", 2) + expect(mention.sessionPicker()).toBe(false) + dispose.fn?.() + }) + + it("keeps the menu closed after Escape until the query is edited", () => { + const { mention, dispose } = harness() + const event = { + key: "Escape", + preventDefault: () => {}, + stopPropagation: () => {}, + isComposing: false, + } as unknown as KeyboardEvent + + mention.onInput("@Fix", 4) + expect(mention.onKeyDown(event, area("@Fix", 4), () => {})).toBe(true) + expect(mention.showMention()).toBe(false) + mention.onInput("@Fix", 4) + expect(mention.showMention()).toBe(false) + dispose.fn?.() + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index b83697542b42..3194cad5496f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -186,7 +186,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 { createWorktreeMentionReferences as createMentionRefs } from "./worktree-references" import type { ReviewComment } from "../diff-viewer/review-comments" import { createReviewComposers } from "./review-composers" import type { SidebarSearchMenuRef } from "./SidebarSearchMenu" @@ -831,7 +831,7 @@ const AgentManagerContent: Component = () => { managed: managedSessions, locals: localSet, }) - const references = createWorktreeReferences(vscode, registry.active, projectSessionsLive.current, selection) + const { references, dialogRefs } = createMentionRefs(vscode, registry.active, projectSessionsLive.current, selection) /** Session ids shown in the project-scoped history view (every session of the project). */ const historySessionIds = createMemo(() => { @@ -1812,6 +1812,7 @@ const AgentManagerContent: Component = () => { activeProjectId={activeProjectId()} defaultBase={defaultBase} onCreate={creation.schedule} + worktrees={dialogRefs} /> )) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 043fedf6a598..96c043c932bf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -48,6 +48,12 @@ import { insertSpacedText, undoKey } from "../src/components/chat/prompt-input-u import { GoalHeader } from "../src/components/chat/goal/GoalHeader" import { isEnterKeyCommitNotIme } from "../src/utils/ime-enter" import { useSlashCommand } from "../src/hooks/useSlashCommand" +import type { MentionResult, WorktreeReference } from "../src/hooks/file-mention-utils" +import { segmentMentionText } from "../src/hooks/file-mention-utils" +import { SessionMentionPicker } from "../src/components/chat/SessionMentionPicker" +import { WorktreeMentionPicker } from "../src/components/chat/WorktreeMentionPicker" +import { formatRelativeDate } from "../src/utils/date" +import { useWorktreeMention } from "./worktree-mention" import { BranchSelect, BranchSelectPopover } from "../src/components/shared/BranchSelect" import { tracker } from "./telemetry" import { cycleAgent } from "../src/context/session-agent" @@ -66,6 +72,10 @@ const VERSION_OPTIONS: VersionCount[] = [1, 2, 3, 4] const WORKTREE_PROMPT_COMMANDS = new Set(["models", "agents", "variant", "sandbox", "project"]) const WORKTREE_PROMPT_HIDDEN = ["init", "review", "resume-claude", "resume-codex"] const WORKTREE_PROMPT_SCOPE = "agent-manager-worktree-prompt" +// The `@model` entry opens the shared model selector through its programmatic +// open event, keyed to this prompt scope so the footer model selector and slash +// commands are unaffected. +const WORKTREE_MENTION_MODEL_TRIGGER = "agent-manager-worktree-mention-model" type DialogTab = "new" | "import" type Model = { providerID: string; modelID: string } @@ -102,6 +112,50 @@ function restoreAgent(value: string | undefined, list: Array<{ name: string }>, const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) +/** + * One row of the dialog's `@` menu. Only the three worktree-independent + * references plus past-chat matches can appear here. + */ +function MentionRow(props: { item: MentionResult }) { + const { t } = useLanguage() + const item = props.item + if (item.type === "model") + return ( + <> + + {item.label} + {item.description} + + ) + if (item.type === "past-chats") + return ( + <> + + {item.label} + {item.description} + + ) + if (item.type === "worktrees") + return ( + <> + + {t("prompt.worktrees.title")} + {t("prompt.worktrees.search")} + + ) + if (item.type === "session") + return ( + <> + + {item.session.title} + + {item.session.worktreeName ?? formatRelativeDate(new Date(item.session.updated).toISOString())} + + + ) + return null +} + export const NewWorktreeDialog: Component<{ onClose: () => void defaultBase?: (projectId: string) => string | undefined @@ -109,6 +163,7 @@ export const NewWorktreeDialog: Component<{ projects?: () => AgentProjectSnapshot[] activeProjectId?: string onCreate?: (projectId: string) => void + worktrees?: () => WorktreeReference[] mode: ModeRouter }> = (props) => { const { t } = useLanguage() @@ -326,6 +381,23 @@ export const NewWorktreeDialog: Component<{ window.addEventListener("focusPrompt", onFocusPrompt) onCleanup(() => window.removeEventListener("focusPrompt", onFocusPrompt)) + const mention = useWorktreeMention(vscode, () => props.worktrees?.() ?? []) + let highlightRef: HTMLDivElement | undefined + const mentionSegments = createMemo(() => segmentMentionText(prompt(), mention.highlightTokens())) + const syncHighlight = () => { + if (!highlightRef || !textareaRef) return + highlightRef.scrollTop = textareaRef.scrollTop + highlightRef.scrollLeft = textareaRef.scrollLeft + } + // Picking the `@` model entry opens the shared model selector, mounted hidden + // and keyed to its own trigger. The mention latch resets first because the + // selector owns its open state afterwards. + createEffect(() => { + if (!mention.modelPicker()) return + mention.closeMention() + window.dispatchEvent(new CustomEvent("openModelPicker", { detail: { source: WORKTREE_MENTION_MODEL_TRIGGER } })) + }) + onMount(() => { // Server commands must be known before submit so a pasted `/command` // prompt can be routed through the command path, not only via the menu. @@ -378,6 +450,18 @@ export const NewWorktreeDialog: Component<{ const total = () => (compareMode() ? totalAllocations(modelAllocations()) : versions()) const mode = () => (compareMode() ? "compare_models" : versions() > 1 ? "multiple_versions" : "single") + /** + * Attachments for the new sessions. Mentions are resolved here, at creation + * time: the new worktree does not exist yet, so nothing is read from it. Past + * chats and worktrees travel as attachments; model references stay inline. + */ + const resolveFiles = (text: string | undefined) => { + const mentionFiles = text ? mention.parseAttachments(text) : [] + const imgFiles = imageAttach.images().map((img) => ({ mime: img.mime, url: img.dataUrl })) + const files = [...mentionFiles, ...imgFiles] + return files.length > 0 ? files : undefined + } + const handleSubmit = () => { if (!canSubmit()) return const advanced = showAdvanced() @@ -405,8 +489,6 @@ export const NewWorktreeDialog: Component<{ const payload = submitPayload(goalMode(), draft, slash.commands()) const defaultAgent = session.agents()[0]?.name const selectedAgent = agent() !== defaultAgent ? agent() : undefined - const imgs = imageAttach.images() - const imgFiles = imgs.length > 0 ? imgs.map((img) => ({ mime: img.mime, url: img.dataUrl })) : undefined const isCompare = compareMode() const allocations = isCompare ? allocationsToArray(modelAllocations()) : undefined @@ -431,7 +513,7 @@ export const NewWorktreeDialog: Component<{ branchName: customBranch, modelAllocations: allocations, sandbox: sandboxVisible() ? sandboxOverride() : undefined, - files: imgFiles, + files: resolveFiles(payload.text), }) persistPrompt("") @@ -477,6 +559,11 @@ export const NewWorktreeDialog: Component<{ return } + if (mention.onKeyDown(e, textareaRef, setPromptValue, restorePrompt)) { + e.stopPropagation() + return + } + // The chat composer submits on Enter, so mirror that for the two-step goal // flow. Use the shared IME guard so confirming a composition does not submit. // Plain prompts keep Enter as a newline and still create with Cmd/Ctrl+Enter. @@ -514,6 +601,7 @@ export const NewWorktreeDialog: Component<{ box.style.height = "auto" const chrome = box.offsetHeight - area.offsetHeight box.style.height = `${Math.min(area.scrollHeight, 200) + chrome}px` + syncHighlight() } const insertSpeechText = (value: string) => { @@ -743,6 +831,80 @@ export const NewWorktreeDialog: Component<{ onDragLeave={imageAttach.handleDragLeave} onDrop={imageAttach.handleDrop} > + + +
+ { + if (textareaRef) mention.selectSession(picked, textareaRef, setPromptValue, restorePrompt) + }} + onClose={() => { + mention.closeMention() + restorePrompt() + }} + /> + } + > + { + if (textareaRef) mention.selectWorktree(picked, textareaRef, setPromptValue, restorePrompt) + }} + onClose={() => { + mention.closeMention() + restorePrompt() + }} + /> + } + > + 0} + fallback={
No mentions found
} + > + + {(item, index) => ( +
{ + e.preventDefault() + if (textareaRef) mention.selectMention(item, textareaRef, setPromptValue, restorePrompt) + }} + onMouseEnter={() => mention.setMentionIndex(index())} + > + +
+ )} +
+
+
+
+
+
setGoalMode(false)} /> @@ -801,6 +963,18 @@ export const NewWorktreeDialog: Component<{
+