Skip to content

Let a Doppler drag clear of the markers draw a new curve - #341

Merged
IanMayo merged 3 commits into
mainfrom
claude/confident-cerf-wy0yze
Sep 18, 2026
Merged

IanMayo merged 3 commits into
mainfrom
claude/confident-cerf-wy0yze

Conversation

@IanMayo

@IanMayo IanMayo commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Problem

An analyst reported that Doppler mode sometimes stops responding: a click & drag on the gram does nothing, switching mode and back does not help, and a refresh sometimes does. Reproduced against debug.html and confirmed in the published 0.3.0 bundle.

resolveDopplerDrag offered a placement only while no marker existed. Once any one did, a press anywhere but on a marker resolved to null, so every drag was silently ignored. Three ways in, none a misuse of the tool:

  • a curve already on the gram, when the analyst drags to draw another;
  • a curve restored from storage, possibly outside the zoomed view (trainer pages keep it across days);
  • an invisible lone f+: a press released before any movement seeded f+, completeMarkerPlacement bailed on the missing f− and kept it, nothing drew (only a complete curve renders), and hasPersistableAnnotations counted it as an annotation, so it was saved at once and restored on reload.

Fix

  • One curve per gram. resolveDopplerDrag looks for a marker under the pointer first and otherwise starts a placement. A drag clear of the markers replaces the curve; the replaced curve rides on the drag target, so Escape, a mode switch or a moveless release puts it back.
  • A press released without moving places nothing.
  • isCompleteCurve in utils/doppler.js is the one rule for "there is a curve"; the renderer, hasPersistentFeatures and storage all read it, so a lone marker can neither draw, persist nor block a placement.

Hardening (not reproduced)

Tracing the pointer path found one way every mode can go dead, which would also fit the Cross Cursor incident: a region-zoom or wheel-pan drag whose release is lost with the pointer still over the gram. hasActiveDrag then drops every press and right-click, and _cancelAllDrags could not reach those two handlers, so only a reload cleared it. A mode switch now ends with the engine's cancelActiveDrag, and a window blur cancels whichever drag is running.

Tests

  • doppler-mode.spec.js: replacement, moveless press with and without a curve, Escape restoring the old curve, a partial stored curve no longer blocking placement.
  • region-zoom.spec.js: recovery after a blur and after a mode switch mid-selection.
  • storage.spec.js: the doppler persistence test now drags a curve; it used to click twice, which only ever "placed" the orphan f+.
  • doppler-hotspot.spec.js: a press clear of every marker now asserts a placement rather than nothing.
  • storage-validation.test.js: the storage predicate follows what renders.

The branch carries a merge of main (the control-panel rebuild and v0.3.0); the one conflict was the Doppler guidance text, resolved into the new { trigger, outcome } form. On the merged head yarn typecheck, yarn lint, yarn hygiene, yarn test:unit and the full Playwright suite (535 tests) pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RsjY9pBg5RAmyqbThX4Uae

An analyst reported that Doppler mode sometimes stops responding: a
click & drag on the gram does nothing, and switching mode and back does
not help. The cause is that placement was only offered while *no*
marker existed. Once any one did, a press anywhere but on a marker
resolved to nothing, so every drag was silently ignored. Three ways in,
none of them a misuse of the tool:

- a curve already on the gram, when the analyst drags to draw another;
- a curve restored from storage, possibly outside the zoomed view;
- an invisible lone f+, left behind when a press was released before
  any movement: `completeMarkerPlacement` bailed on the missing f- and
  kept the seeded f+, nothing drew (only a complete curve renders), and
  `hasPersistableAnnotations` counted it as an annotation, so it was
  saved at once and came back on reload.

Now:

- `resolveDopplerDrag` looks for a marker under the pointer first and
  otherwise starts a placement, whatever the marker state. There is one
  curve per gram: a drag clear of the markers replaces it. The replaced
  curve rides on the drag target, so Escape, a mode switch or a moveless
  release puts it back rather than leaving nothing.
- A press released without moving places nothing.
- `isCompleteCurve` in `utils/doppler.js` is the one rule for "there is
  a curve"; the renderer, `hasPersistentFeatures` and storage all read
  it, so a lone marker can neither draw, persist nor block a placement.

Two hardenings for a related way every mode can go dead, found while
tracing the pointer path but not reproduced: a region-zoom or wheel-pan
drag whose release is lost with the pointer still on the gram. Such a
drag blocked every press and right-click, and a mode switch could not
clear it because `_cancelAllDrags` only reached the modes' handlers. A
mode switch now ends with the engine's `cancelActiveDrag`, and a window
`blur` cancels whichever drag is running.

Tests: the doppler spec covers replacement, the moveless press with and
without an existing curve, Escape restoring the old curve, and a partial
stored curve no longer blocking placement; the region-zoom spec covers
recovery after a blur and after a mode switch; the storage spec's
doppler persistence test now drags a curve instead of clicking twice,
which only ever "placed" the orphan f+; the hotspot spec's clear-press
test asserts a placement rather than nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsjY9pBg5RAmyqbThX4Uae
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

PR Preview

Preview deployment is ready! View Preview

Last updated: 2026-09-18T13:06:53.893Z

CI's coverage ratchet caught what the local run had not: `snapshotCurve`
and `replacedCurveOf` in utils/doppler.js had no unit test, so the
uncovered-line count rose from 409 to 413. They are pure, so the unit
lane pins them: a curve is complete only with all three markers, a
snapshot copies a complete curve and is null for anything less, and the
replaced curve rides on the place target alone. Also lowers the
`instance.state` reach-in baseline the merge and `restoreCurve` brought
down, as the ratchet asks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsjY9pBg5RAmyqbThX4Uae
@IanMayo
IanMayo merged commit 02cd148 into main Sep 18, 2026
6 checks passed
@IanMayo
IanMayo deleted the claude/confident-cerf-wy0yze branch September 18, 2026 15:37
github-actions Bot added a commit that referenced this pull request Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants