fix(vscode): preserve worktree rename focus

This commit is contained in:
marius-kilocode
2026-08-04 12:29:44 +02:00
parent f023e77cb0
commit 87b53e4e79
5 changed files with 44 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep multi-project worktree rename inputs focused while selection updates are still settling.
@@ -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,18 @@ 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 button = window.document.createElement("button")
expect(isTextControl(rename)).toBe(true)
expect(preservesTextFocus(rename)).toBe(true)
expect(preservesTextFocus(prompt)).toBe(false)
expect(isTextControl(button)).toBe(false)
})
})
@@ -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
@@ -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<PromptInputProps> = (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<PromptInputProps> = (props) => {
focus()
if (!(event instanceof CustomEvent) || !event.detail?.restore) return
const restore = () => {
if (defer()) return
if (defer() || ownsFocus()) return
window.focus()
focus()
}
@@ -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 el.getAttribute("contenteditable") === "true" || el.getAttribute("role") === "textbox"
}