From 42c63f48e4fbf4b2fe66c54b11e3c042a7a5aea0 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Fri, 31 Jul 2026 16:46:17 +0200 Subject: [PATCH] fix(pi): focus composer when clicking chat thread --- .../features/pi-sessions/PiSessionView.tsx | 10 +++- .../sessions/components/SessionView.tsx | 18 ++----- .../focusComposerOnPaneClick.test.ts | 53 +++++++++++++++++++ .../components/focusComposerOnPaneClick.ts | 21 ++++++++ 4 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 packages/ui/src/features/sessions/components/focusComposerOnPaneClick.test.ts create mode 100644 packages/ui/src/features/sessions/components/focusComposerOnPaneClick.ts diff --git a/packages/ui/src/features/pi-sessions/PiSessionView.tsx b/packages/ui/src/features/pi-sessions/PiSessionView.tsx index 934adccfc1..64a87562f5 100644 --- a/packages/ui/src/features/pi-sessions/PiSessionView.tsx +++ b/packages/ui/src/features/pi-sessions/PiSessionView.tsx @@ -29,6 +29,7 @@ import { } from "@posthog/ui/features/sessions/components/CloudSessionLifecycle"; import { ChatThread } from "@posthog/ui/features/sessions/components/chat-thread/ChatThread"; import type { PromptRecallHandler } from "@posthog/ui/features/sessions/components/chat-thread/composerPromptRecall"; +import { focusComposerOnPaneClick } from "@posthog/ui/features/sessions/components/focusComposerOnPaneClick"; import { SessionInitializingView } from "@posthog/ui/features/sessions/components/SessionInitializingView"; import { CHAT_CONTENT_MAX_WIDTH } from "@posthog/ui/features/sessions/constants"; import { useMessagingModeStore } from "@posthog/ui/features/sessions/messagingModeStore"; @@ -224,6 +225,13 @@ export function PiSessionView({ ); }, [handleControllerError, piSessionController, taskId]); + const handleThreadClick = useCallback( + (event: React.MouseEvent) => { + focusComposerOnPaneClick(event, () => draftActions.requestFocus(taskId)); + }, + [draftActions, taskId], + ); + const restart = useCallback(() => { void piSessionController .restart(taskId) @@ -379,7 +387,7 @@ export function PiSessionView({ onRestart={restart} /> )} - + toast.error("Failed to attach files")); }, []); - const handlePaneClick = useCallback((e: React.MouseEvent) => { - const target = e.target as HTMLElement; - - const interactiveSelector = - 'button, a, input, textarea, select, [role="button"], [role="link"], [contenteditable="true"], [data-interactive]'; - if (target.closest(interactiveSelector)) { - return; - } - - const selection = window.getSelection(); - if (selection && selection.toString().length > 0) { - return; - } - - editorRef.current?.focus(); + const handlePaneClick = useCallback((event: React.MouseEvent) => { + focusComposerOnPaneClick(event, () => editorRef.current?.focus()); }, []); useAutoFocusOnTyping(editorRef, !isActiveSession); diff --git a/packages/ui/src/features/sessions/components/focusComposerOnPaneClick.test.ts b/packages/ui/src/features/sessions/components/focusComposerOnPaneClick.test.ts new file mode 100644 index 0000000000..bde422324c --- /dev/null +++ b/packages/ui/src/features/sessions/components/focusComposerOnPaneClick.test.ts @@ -0,0 +1,53 @@ +import type { MouseEvent as ReactMouseEvent } from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { focusComposerOnPaneClick } from "./focusComposerOnPaneClick"; + +function clickTarget(target: EventTarget): Pick { + return { target }; +} + +afterEach(() => { + window.getSelection()?.removeAllRanges(); + document.body.replaceChildren(); +}); + +describe("focusComposerOnPaneClick", () => { + it.each([ + { + name: "chat content", + markup: '

Chat content

', + expectsFocus: true, + }, + { + name: "an interactive element", + markup: '', + expectsFocus: false, + }, + { + name: "content marked as interactive", + markup: 'Open', + expectsFocus: false, + }, + ])("focuses for $name only when appropriate", ({ markup, expectsFocus }) => { + document.body.innerHTML = markup; + const focusComposer = vi.fn(); + const target = document.getElementById("target") as HTMLElement; + + focusComposerOnPaneClick(clickTarget(target), focusComposer); + + expect(focusComposer).toHaveBeenCalledTimes(expectsFocus ? 1 : 0); + }); + + it("does not focus while selecting chat content", () => { + document.body.innerHTML = '

Chat content

'; + const target = document.getElementById("target") as HTMLElement; + const range = document.createRange(); + range.selectNodeContents(target); + window.getSelection()?.addRange(range); + const focusComposer = vi.fn(); + + focusComposerOnPaneClick(clickTarget(target), focusComposer); + + expect(focusComposer).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/features/sessions/components/focusComposerOnPaneClick.ts b/packages/ui/src/features/sessions/components/focusComposerOnPaneClick.ts new file mode 100644 index 0000000000..a23d29698c --- /dev/null +++ b/packages/ui/src/features/sessions/components/focusComposerOnPaneClick.ts @@ -0,0 +1,21 @@ +import type { MouseEvent } from "react"; + +const INTERACTIVE_SELECTOR = + 'button, a, input, textarea, select, [role="button"], [role="link"], [contenteditable="true"], [data-interactive]'; + +export function focusComposerOnPaneClick( + event: Pick, + focusComposer: () => void, +): void { + const target = event.target; + if (!(target instanceof Element) || target.closest(INTERACTIVE_SELECTOR)) { + return; + } + + const selection = window.getSelection(); + if (selection && selection.toString().length > 0) { + return; + } + + focusComposer(); +}