backnotprop / backnotprop/plannotator

Mobile: in selection mode, the annotation toolbar commits before I can adjust the selection with Android's handles

Open
#1,041 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.7k
Forks
649
Avg merge
11h 12m
Merged PRs (30d)
109

Description

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:

1. 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.
2. 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):

1. Open a plan, or `plannotator annotate `, on the phone.
2. Drag to select a few words.
3. Try to fine-tune the selection with the endpoint handles.
4. 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(...)`:

```js
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 ``. 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.

Contributor guide

Open the contributing guide

Research direction

Start in packages/ui/hooks/useAnnotationHighlighter.ts around line 959 and trace the coarse-pointer selectionchange handler, its 400 ms timer, and highlighter.fromRange(...). Reproduce the issue in Android Chrome using selection mode, then verify that selection handles remain adjustable until an explicit annotation action and that trivial selections are not committed.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.