diff --git a/apps/app/src/features/chat/components/chat-composer-frame.test.tsx b/apps/app/src/features/chat/components/chat-composer-frame.test.tsx
new file mode 100644
index 000000000..777919f89
--- /dev/null
+++ b/apps/app/src/features/chat/components/chat-composer-frame.test.tsx
@@ -0,0 +1,78 @@
+import { PromptInputButton, PromptInputSubmit } from "@getpie/ui/ai-elements/prompt-input";
+import { describe, expect, it, vi } from "vitest";
+import { render } from "vitest-browser-react";
+import { page } from "vitest/browser";
+
+import "@/index.css";
+
+import { ChatComposerFrame } from "./chat-composer-frame";
+import { ChatInputController } from "./input/chat-input-controller";
+import { createChatBaseExtensions } from "./input/extensions/chat-base-extensions";
+
+describe("ChatComposerFrame", () => {
+ it.each([undefined, "inline"] as const)(
+ "focuses from the whole input surface without intercepting controls (%s)",
+ async (layout) => {
+ const controller = new ChatInputController({
+ extensions: () => createChatBaseExtensions(),
+ onSubmit: () => {},
+ });
+ const focus = vi.spyOn(controller, "focus");
+ const action = vi.fn<() => void>();
+ const screen = await render(
+ }
+ toolbar={
+ <>
+ space
+
+ icon
+
+ >
+ }
+ />,
+ );
+ try {
+ const editor = controller.editor.view.dom;
+ const input = editor.closest('[data-slot="chat-input"]');
+ const form = editor.closest("form");
+ if (!input || !form) throw new Error("Composer input surface is missing");
+
+ for (const surface of [input, form]) {
+ controller.editor.commands.blur();
+ focus.mockClear();
+ await page.elementLocator(surface).click({
+ position: {
+ x: surface.clientWidth / 2,
+ y: surface.clientHeight - 3,
+ },
+ });
+ expect(focus).toHaveBeenCalledOnce();
+ await expect.poll(() => document.activeElement).toBe(editor);
+ }
+
+ controller.editor.commands.blur();
+ focus.mockClear();
+ await page.getByTestId("toolbar-space").click();
+ expect(focus).toHaveBeenCalledOnce();
+ await expect.poll(() => document.activeElement).toBe(editor);
+
+ focus.mockClear();
+ await page.getByTestId("action-icon").click();
+ expect(action).toHaveBeenCalledOnce();
+ expect(focus).not.toHaveBeenCalled();
+ await expect.element(page.getByRole("button", { name: "Composer action" })).toHaveFocus();
+
+ await page.elementLocator(editor).click();
+ expect(focus).not.toHaveBeenCalled();
+ await expect.poll(() => document.activeElement).toBe(editor);
+ } finally {
+ await screen.unmount();
+ controller.dispose();
+ }
+ },
+ );
+});
diff --git a/apps/app/src/features/chat/components/chat-composer-frame.tsx b/apps/app/src/features/chat/components/chat-composer-frame.tsx
index 57d0aa629..1f832dfb7 100644
--- a/apps/app/src/features/chat/components/chat-composer-frame.tsx
+++ b/apps/app/src/features/chat/components/chat-composer-frame.tsx
@@ -40,6 +40,18 @@ export function ChatComposerFrame({
{
+ if (
+ event.defaultPrevented ||
+ !(event.target instanceof Element) ||
+ event.target.closest(
+ "button, a, input, textarea, select, [contenteditable], [tabindex]",
+ )
+ ) {
+ return;
+ }
+ controller?.focus();
+ }}
onSubmit={(event) => {
event.preventDefault();
void controller?.submit();