On Android (Chrome) I can't reliably annotate in selection mode, because the selection commits to an annotation before I get a chance to adjust it. (Pinpoint mode works much better for me on the phone, so this seems specific to selection mode.) As soon as I select any text, the toolbar pops up and takes over, and I lose control of what I selected. In practice I have to get the whole selection right in one continuous drag, which is hard to do accurately on a phone. If I pause even briefly to grab a handle, it's already too late.
Two things seem to be going on:
- The toolbar fires on almost any selection, including a single character or whitespace, so it feels like it triggers even when nothing is really selected.
- Once it fires, I lose the ability to adjust the selection, but the exact behaviour is inconsistent. Sometimes the native selection is torn down completely; sometimes the handles stay visible but I can't move them; and in rare cases I can still adjust them.
Repro (Android Chrome, in selection mode, with "Touch to Search" disabled so that's not the cause):
- Open a plan, or
plannotator annotate <file.md>, on the phone.
- Drag to select a few words.
- Try to fine-tune the selection with the endpoint handles.
- The toolbar appears on the initial settle, and from there the selection can't reliably be adjusted.
The root cause looks like the mobile bridge in packages/ui/hooks/useAnnotationHighlighter.ts (about line 959). On a coarse pointer it listens to selectionchange, waits 400 ms for the selection to settle, and then calls highlighter.fromRange(...):
selectionTimer = setTimeout(() => {
const sel = window.getSelection();
if (!sel || sel.isCollapsed || sel.rangeCount === 0) return;
if (!containerRef.current?.contains(sel.anchorNode)) return;
highlighter.fromRange(sel.getRangeAt(0));
}, 400);
So the 400 ms idle is exactly the pause I take to reach for a handle, and fromRange then wraps the range in <mark>. The inconsistency I see (handles gone, or present-but-frozen, or occasionally still usable) makes me think it's a timing race: the timer refiring as the selection changes while I drag a handle rather than a clean deterministic teardown. The only guard is isCollapsed, so a 1-character or whitespace-only selection still fires (which is why it feels like it triggers on nothing). On desktop this is fine, because mouseup is a clean end-of-gesture, but on Android the model is select, then refine with the handles, then act, and the bridge has no refine step or explicit confirm.
I think the fix is to decouple "selection settled" from "create the annotation" on touch. This looks feasible, because the code already branches on pointer type (the mobile bridge itself only runs on a coarse pointer, and pinpoint mode already behaves differently on touch). Rather than auto-committing on a timer, I would show a small floating affordance (e.g. the Comment / Redline buttons) anchored to the live selection, and only call fromRange when I actually tap it, much like pinpoint mode already waits for an explicit action. That would leave the native selection (and its handles) intact so I can still adjust it. A minimum-length / non-whitespace guard would help too, and I think that part overlaps with #881 (empty/whitespace selection creating garbage annotations), though the handle-adjustment problem here is a bit different, so I'm filing it separately.
Environment:
- Plannotator 0.22.0. The
useAnnotationHighlighter.ts path looks unchanged on current main (0.23.0), so I believe it affects the latest release too.
- Android 16, Chrome 150.0.7871.64, selection mode (pinpoint mode is fine). The server was run in remote mode and reached from the phone over the LAN.
On Android (Chrome) I can't reliably annotate in selection mode, because the selection commits to an annotation before I get a chance to adjust it. (Pinpoint mode works much better for me on the phone, so this seems specific to selection mode.) As soon as I select any text, the toolbar pops up and takes over, and I lose control of what I selected. In practice I have to get the whole selection right in one continuous drag, which is hard to do accurately on a phone. If I pause even briefly to grab a handle, it's already too late.
Two things seem to be going on:
Repro (Android Chrome, in selection mode, with "Touch to Search" disabled so that's not the cause):
plannotator annotate <file.md>, on the phone.The root cause looks like the mobile bridge in
packages/ui/hooks/useAnnotationHighlighter.ts(about line 959). On a coarse pointer it listens toselectionchange, waits 400 ms for the selection to settle, and then callshighlighter.fromRange(...):So the 400 ms idle is exactly the pause I take to reach for a handle, and
fromRangethen wraps the range in<mark>. The inconsistency I see (handles gone, or present-but-frozen, or occasionally still usable) makes me think it's a timing race: the timer refiring as the selection changes while I drag a handle rather than a clean deterministic teardown. The only guard isisCollapsed, so a 1-character or whitespace-only selection still fires (which is why it feels like it triggers on nothing). On desktop this is fine, becausemouseupis a clean end-of-gesture, but on Android the model is select, then refine with the handles, then act, and the bridge has no refine step or explicit confirm.I think the fix is to decouple "selection settled" from "create the annotation" on touch. This looks feasible, because the code already branches on pointer type (the mobile bridge itself only runs on a coarse pointer, and pinpoint mode already behaves differently on touch). Rather than auto-committing on a timer, I would show a small floating affordance (e.g. the Comment / Redline buttons) anchored to the live selection, and only call
fromRangewhen I actually tap it, much like pinpoint mode already waits for an explicit action. That would leave the native selection (and its handles) intact so I can still adjust it. A minimum-length / non-whitespace guard would help too, and I think that part overlaps with #881 (empty/whitespace selection creating garbage annotations), though the handle-adjustment problem here is a bit different, so I'm filing it separately.Environment:
useAnnotationHighlighter.tspath looks unchanged on current main (0.23.0), so I believe it affects the latest release too.