diff --git a/.changeset/worktree-rename-focus.md b/.changeset/worktree-rename-focus.md new file mode 100644 index 00000000000..fef1fe11e23 --- /dev/null +++ b/.changeset/worktree-rename-focus.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep multi-project worktree rename inputs focused while selection updates are still settling. diff --git a/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts index 1c63c08b4fa..c5c33eea67f 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test" import { Window } from "happy-dom" -import { focusQuestionOption, hasQuestionOption } from "../../webview-ui/agent-manager/focus" +import { focusQuestionOption, hasQuestionOption, preservesTextFocus } from "../../webview-ui/agent-manager/focus" +import { isTextControl } from "../../webview-ui/src/utils/focus" describe("Agent Manager focus", () => { it("focuses the first enabled question option", () => { @@ -53,4 +54,21 @@ describe("Agent Manager focus", () => { dock.setAttribute("inert", "") expect(hasQuestionOption(root)).toBe(false) }) + + it("preserves focus for an active editable control", () => { + const window = new Window() + const rename = window.document.createElement("input") + rename.className = "am-worktree-rename-input" + const prompt = window.document.createElement("textarea") + prompt.className = "prompt-input" + const editor = window.document.createElement("div") + editor.contentEditable = "plaintext-only" + const button = window.document.createElement("button") + + expect(isTextControl(rename)).toBe(true) + expect(preservesTextFocus(rename)).toBe(true) + expect(preservesTextFocus(prompt)).toBe(false) + expect(isTextControl(editor)).toBe(true) + expect(isTextControl(button)).toBe(false) + }) }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/focus.ts b/packages/kilo-vscode/webview-ui/agent-manager/focus.ts index e1acb284611..0553003b306 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/focus.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/focus.ts @@ -1,5 +1,11 @@ +import { isTextControl } from "../src/utils/focus" + const OPTION = '[data-component="question-dock"] button[data-slot="question-option"]' +/** Keep an active editor, such as the worktree rename input, in control. */ +export const preservesTextFocus = (active: Element | null): boolean => + active !== null && isTextControl(active) && !active.classList.contains("prompt-input") + export function createChatFocus(deps: { term: () => string | undefined history: () => boolean @@ -7,6 +13,7 @@ export function createChatFocus(deps: { }) { const focus = (force: boolean) => { if ((!force && !document.hasFocus()) || deps.term() || deps.history() || deps.review()) return + if (preservesTextFocus(document.activeElement)) return if (!force && document.activeElement?.matches('[role="tab"]')) return if (!force && document.activeElement?.closest('[data-component="question-dock"]')) return if (focusQuestionOption()) return diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 7aceb25a52e..075b925b73b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -10,6 +10,7 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { FileIcon } from "@kilocode/kilo-ui/file-icon" import { Icon } from "@kilocode/kilo-ui/icon" import { showToast } from "@kilocode/kilo-ui/toast" +import { isTextControl } from "../../utils/focus" import { useSession } from "../../context/session" import { useLocalTabs } from "../../context/local-tabs" import { useServer } from "../../context/server" @@ -418,8 +419,12 @@ export const PromptInput: Component = (props) => { const onFocusPrompt = (event: Event) => { const defer = () => event instanceof CustomEvent && event.detail?.deferFocusToQuestion && props.deferFocusToQuestion?.() + const ownsFocus = () => { + const active = document.activeElement + return active !== textareaRef && isTextControl(active) + } const focus = () => { - if (defer()) return + if (defer() || ownsFocus()) return const ref = textareaRef if (!ref) return ref.focus({ preventScroll: true }) @@ -427,7 +432,7 @@ export const PromptInput: Component = (props) => { focus() if (!(event instanceof CustomEvent) || !event.detail?.restore) return const restore = () => { - if (defer()) return + if (defer() || ownsFocus()) return window.focus() focus() } diff --git a/packages/kilo-vscode/webview-ui/src/utils/focus.ts b/packages/kilo-vscode/webview-ui/src/utils/focus.ts new file mode 100644 index 00000000000..a11abee27f9 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/focus.ts @@ -0,0 +1,9 @@ +const nonText = new Set(["button", "checkbox", "file", "hidden", "image", "radio", "range", "reset", "submit"]) + +/** Whether an element owns editable text focus that should not be stolen. */ +export const isTextControl = (el: Element | null): boolean => { + if (!el) return false + if (el.tagName === "TEXTAREA" || el.tagName === "SELECT") return true + if (el.tagName === "INPUT") return !nonText.has((el as HTMLInputElement).type.toLowerCase()) + return ("isContentEditable" in el && (el as HTMLElement).isContentEditable) || el.getAttribute("role") === "textbox" +}