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/worktree-rename-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Keep multi-project worktree rename inputs focused while selection updates are still settling.
20 changes: 19 additions & 1 deletion packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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)
})
})
7 changes: 7 additions & 0 deletions packages/kilo-vscode/webview-ui/agent-manager/focus.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
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
review: () => boolean
}) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -418,16 +419,20 @@ 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 })
}
focus()
if (!(event instanceof CustomEvent) || !event.detail?.restore) return
const restore = () => {
if (defer()) return
if (defer() || ownsFocus()) return
window.focus()
focus()
}
Expand Down
9 changes: 9 additions & 0 deletions packages/kilo-vscode/webview-ui/src/utils/focus.ts
Original file line number Diff line number Diff line change
@@ -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"
}
Loading