diff --git a/ui/src/components/DocumentAnnotationPanel.tsx b/ui/src/components/DocumentAnnotationPanel.tsx
index c92684b2659..97fd3edefe2 100644
--- a/ui/src/components/DocumentAnnotationPanel.tsx
+++ b/ui/src/components/DocumentAnnotationPanel.tsx
@@ -26,7 +26,7 @@ import { cn, relativeTime } from "@/lib/utils";
import { documentAnnotationsApi, type DocumentAnnotationTarget } from "@/api/document-annotations";
import { authApi } from "@/api/auth";
import { queryKeys } from "@/lib/queryKeys";
-import { hasActiveNativeSelection, isCoarsePointerDevice } from "@/lib/document-annotation-selection";
+import { isCoarsePointerDevice } from "@/lib/document-annotation-selection";
import { AgentIcon } from "./AgentIconPicker";
import { deriveInitials } from "./Identity";
import { MarkdownBody } from "./MarkdownBody";
@@ -312,7 +312,7 @@ function AnnotationPanelBody(props: AnnotationPanelProps) {
useEffect(() => {
if (!props.pendingAnchor || !props.open || props.isMobile) return;
- if (isCoarsePointerDevice() || hasActiveNativeSelection()) return;
+ if (isCoarsePointerDevice()) return;
composerRef.current?.focus();
}, [props.isMobile, props.open, props.pendingAnchor]);
diff --git a/ui/src/components/IssueDocumentAnnotations.test.tsx b/ui/src/components/IssueDocumentAnnotations.test.tsx
index 4998e87badb..176899f7d18 100644
--- a/ui/src/components/IssueDocumentAnnotations.test.tsx
+++ b/ui/src/components/IssueDocumentAnnotations.test.tsx
@@ -1058,4 +1058,59 @@ describe("IssueDocumentAnnotations", () => {
});
}
});
+
+ it("auto-focuses the composer for desktop selection requests", async () => {
+ const originalMatchMedia = window.matchMedia;
+ Object.defineProperty(window, "matchMedia", {
+ configurable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+ const focusSpy = vi.spyOn(HTMLTextAreaElement.prototype, "focus").mockImplementation(() => {});
+ mockAnnotationsApi.list.mockResolvedValue([]);
+ const root = createRoot(container);
+ const queryClient = makeQueryClient();
+ const doc = makeDoc();
+
+ try {
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flush();
+ await flush();
+
+ const body = container.querySelector("p");
+ const range = document.createRange();
+ range.selectNodeContents(body!);
+ window.getSelection()?.removeAllRanges();
+ window.getSelection()?.addRange(range);
+ focusSpy.mockClear();
+
+ const selectButton = container.querySelector('[data-testid="mock-annotation-selection"]');
+ await act(async () => selectButton?.click());
+ await flush();
+
+ expect(container.querySelector('[data-testid="document-annotation-composer"]')).not.toBeNull();
+ expect(focusSpy).toHaveBeenCalled();
+ } finally {
+ window.getSelection()?.removeAllRanges();
+ focusSpy.mockRestore();
+ Object.defineProperty(window, "matchMedia", {
+ configurable: true,
+ value: originalMatchMedia,
+ });
+ }
+ });
});
diff --git a/ui/src/lib/document-annotation-selection.test.ts b/ui/src/lib/document-annotation-selection.test.ts
index 6dcbc78a9a1..b144bdc6fff 100644
--- a/ui/src/lib/document-annotation-selection.test.ts
+++ b/ui/src/lib/document-annotation-selection.test.ts
@@ -4,6 +4,7 @@ import { verifyDocumentAnchorSelector } from "@paperclipai/shared";
import {
buildAnchorFromContainerSelection,
getContainerTextOffset,
+ isCoarsePointerDevice,
rangesForNormalizedSpan,
} from "./document-annotation-selection";
@@ -55,6 +56,34 @@ function selectText(container: HTMLElement, needle: string): Range {
throw new Error(`Could not find "${needle}" in container`);
}
+describe("isCoarsePointerDevice", () => {
+ it("uses the primary pointer media query on hybrid touch laptops", () => {
+ const targetWindow = {
+ navigator: { maxTouchPoints: 5 },
+ matchMedia: (query: string) => ({ matches: query === "(pointer: fine)" }),
+ } as unknown as Window;
+
+ expect(isCoarsePointerDevice(targetWindow)).toBe(false);
+ });
+
+ it("detects coarse primary pointers when media queries are available", () => {
+ const targetWindow = {
+ navigator: { maxTouchPoints: 0 },
+ matchMedia: (query: string) => ({ matches: query === "(pointer: coarse)" }),
+ } as unknown as Window;
+
+ expect(isCoarsePointerDevice(targetWindow)).toBe(true);
+ });
+
+ it("falls back to legacy touch points without matchMedia", () => {
+ const targetWindow = {
+ navigator: { maxTouchPoints: 1 },
+ } as unknown as Window;
+
+ expect(isCoarsePointerDevice(targetWindow)).toBe(true);
+ });
+});
+
describe("buildAnchorFromContainerSelection", () => {
it("produces a selector that verifies against the same markdown", () => {
const container = makeContainer();
diff --git a/ui/src/lib/document-annotation-selection.ts b/ui/src/lib/document-annotation-selection.ts
index 44349f469d9..a1e37d89bf9 100644
--- a/ui/src/lib/document-annotation-selection.ts
+++ b/ui/src/lib/document-annotation-selection.ts
@@ -23,10 +23,11 @@ export function isCoarsePointerDevice(targetWindow: Window = window): boolean {
const navigatorWithLegacyTouchPoints = targetWindow.navigator as Navigator & {
msMaxTouchPoints?: number;
};
- if ((navigatorWithLegacyTouchPoints.maxTouchPoints ?? 0) > 0) return true;
- if ((navigatorWithLegacyTouchPoints.msMaxTouchPoints ?? 0) > 0) return true;
- return typeof targetWindow.matchMedia === "function"
- && targetWindow.matchMedia("(pointer: coarse)").matches;
+ if (typeof targetWindow.matchMedia === "function") {
+ return targetWindow.matchMedia("(pointer: coarse)").matches;
+ }
+ return (navigatorWithLegacyTouchPoints.maxTouchPoints ?? 0) > 0
+ || (navigatorWithLegacyTouchPoints.msMaxTouchPoints ?? 0) > 0;
}
export function hasActiveNativeSelection(targetWindow: Window = window): boolean {