fix: focus composer when typing (#102210)

This commit is contained in:
Shakker 2026-07-08 15:20:03 +01:00 committed by Shakker
parent fc05a8103b
commit 9d7a6b5da6
2 changed files with 79 additions and 0 deletions

View file

@ -617,6 +617,53 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
}
});
it("routes page typing to the active composer without stealing text input focus", async () => {
const context = await newBrowserContext({
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
await installMockGateway(page, {
historyMessages: [
{
content: [{ text: "Type whenever you are ready.", type: "text" }],
role: "assistant",
timestamp: Date.now(),
},
],
});
try {
await page.goto(`${server.baseUrl}chat`);
await page.getByText("Type whenever you are ready.").click();
const composer = page.locator(".agent-chat__composer-combobox textarea");
await expect
.poll(() => composer.evaluate((element) => element === document.activeElement))
.toBe(false);
await page.keyboard.type("first character preserved");
expect(await composer.inputValue()).toBe("first character preserved");
await expect
.poll(() => composer.evaluate((element) => element === document.activeElement))
.toBe(true);
await page.getByRole("button", { name: "Open command palette" }).click();
const paletteInput = page.locator(".cmd-palette__input");
await paletteInput.waitFor({ state: "visible", timeout: 10_000 });
await expect
.poll(() => paletteInput.evaluate((element) => element === document.activeElement))
.toBe(true);
await page.keyboard.type("session search");
expect(await paletteInput.inputValue()).toBe("session search");
expect(await composer.inputValue()).toBe("first character preserved");
} finally {
await closeBrowserContext(context);
}
});
it("keeps stale context visible as approximate without warning or compaction", async () => {
const context = await newBrowserContext({
locale: "en-US",

View file

@ -88,6 +88,12 @@ type PaneSessionChangeOptions = { replace?: boolean };
const CHAT_OPEN_DETAILS_SELECTOR =
".chat-controls__inline-select[open], .context-usage details[open], .agent-chat__talk-select[open], .agent-chat__attach-menu[open]";
const CHAT_COMPOSER_TEXTAREA_SELECTOR = ".agent-chat__composer-combobox > textarea";
const CHAT_TEXT_ENTRY_SELECTOR =
"input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='combobox'], [role='listbox'], [role='textbox']";
const CHAT_SPACE_ACTIVATION_SELECTOR =
"a[href], button, summary, [role='button'], [role='checkbox'], [role='link'], [role='radio'], [role='switch']";
const CHAT_MODAL_SELECTOR = "dialog[open], [aria-modal='true']";
const NEW_SESSION_ACTIVE_RUN_MESSAGE =
"Start a new session after the active run or queued messages finish.";
@ -96,6 +102,12 @@ const NEW_SESSION_LIST_LOADING_MESSAGE =
const NEW_SESSION_CREATE_FAILED_MESSAGE =
"New Chat could not create a new session. Try again in a moment.";
function keyboardEventPathMatches(event: KeyboardEvent, selector: string): boolean {
return event
.composedPath()
.some((target) => target instanceof Element && target.matches(selector));
}
class ChatPane extends LitElement {
@consume({ context: applicationContext, subscribe: false })
private context!: ChatPageContext;
@ -341,6 +353,26 @@ class ChatPane extends LitElement {
}
private readonly handleDocumentKeydown = (event: KeyboardEvent) => {
if (
this.active &&
!event.defaultPrevented &&
!event.isComposing &&
!event.metaKey &&
!event.ctrlKey &&
!event.altKey &&
event.key.length === 1 &&
!keyboardEventPathMatches(event, CHAT_TEXT_ENTRY_SELECTOR) &&
!(event.key === " " && keyboardEventPathMatches(event, CHAT_SPACE_ACTIVATION_SELECTOR)) &&
!document.querySelector(CHAT_MODAL_SELECTOR)
) {
const composer = this.querySelector<HTMLTextAreaElement>(CHAT_COMPOSER_TEXTAREA_SELECTOR);
if (composer && !composer.disabled && !composer.readOnly) {
// Focus during keydown capture so the browser delivers beforeinput/input,
// including the first character, through the composer's normal pipeline.
composer.focus({ preventScroll: true });
}
}
if (event.defaultPrevented || event.key !== "Escape") {
return;
}