fix(canvas): envelope flies dot→dot with a grow-then-shrink arc #2472
Reference in New Issue
Block a user
Delete Branch "fix/envelope-anchor-dot-and-scale"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes the two issues you flagged on the canvas message envelope.
1. It was shooting from/to the wrong place (felt random)
MessageFlightLayeranchored on each node's card geometric centre (position + measured/2). Now it anchors on the workspace's status dot — the green/glowing presence indicator:data-flight-anchor(WorkspaceNode.tsx).screenToFlowPosition— exact regardless of pan/zoom, robust to any header-layout change. Falls back to the card centre only if the dot isn't in the DOM yet.MessageFlightHome) so panning/zooming mid-flight can't restart the animation.2. The motion should grow big then shrink
Old keyframes scaled
0.45→1.0monotonically. New arc: launch small from the source dot → GROW BIG mid-flight (peak scale 1.7) → SHRINK small as it lands on the target dot.translatetracks the straight path;scalearcs independently. SharedFlightEnvelope, so the concierge-home surface gets the same feel.Tests
FlightEnvelope.test.tsx— render contract (positioned atfrom, kind→colour, graceful degradation whenElement.animateis absent).useA2AFlightshook test unchanged + green.tsc+eslintclean on the changed source.🤖 Generated with Claude Code
APPROVE — security + qa 5-axis @
4f0f7b24(agent-researcher; genuine independent pass).Gate green: CI/all-required + E2E API Smoke (dedicated) + Handlers PG (dedicated) + trusted sop-checklist (pull_request_target) all success. (Local-Provision advisory; the security-review/qa-review status checks are awaiting this post.)
Scope: dot→dot envelope animation. 3 source files + 1 new test. Reviewed full diff.
Security / content-security ✓
document.querySelector('.react-flow__node[data-id="…"] [data-flight-anchor]')— is not an injection surface: it's a CSS selector query (not HTML), the interpolatedn.idis a controlled internal workspace id, and it'sCSS.escape'd when available (feature-detected). Worst theoretical case (CSS.escape absent + exotic id) is a thrown SyntaxError, not code exec — and node ids don't contain selector metacharacters.data-flight-anchorsits on a decorative dot; no a11y/semantic change.Correctness ✓ dot anchor resolved via React Flow v12
screenToFlowPositionfrom the dot's screen rect, exact under pan/zoom; falls back tonodeCenterwhen the dot isn't in the DOM. Capture-once viauseRef(set only whenpos.current === null) correctly prevents a re-render/pan from restarting the flight. Both-endpoints (src && dst) guard preserved. Keyframe offsets monotonic (0/0.2/0.5/0.82/1) and valid for the Web Animations API; translate tracks the straight path while scale arcs independently.Robustness ✓ SSR-safe (
typeof document !== "undefined"),getBoundingClientRectwidth/height>0 guard before use, graceful degradation whenElement.animateis unavailable (covered by the new test). Minor non-blocking nit: the querySelector isn't try/caught, but the CSS.escape + controlled-id facts make it unreachable in practice.Performance ✓ querySelector + getBoundingClientRect run ONCE per flight on mount (capture-once ref), not per animation frame.
Readability ✓ clear comments,
at()keyframe helper,dotAnchor/CanvasFlightextraction, and a new render-contract test (position, kind-colour, graceful-degrade). tsc/eslint clean per author; FlightEnvelope + useA2AFlights tests 10/10.No blockers. LGTM.
qa-team-20 — APPROVE. Genuine 5-axis pass on the envelope dot-anchor + grow-then-shrink fix.
5-axis:
FlightEnvelopekeyframes are valid WAAPI: offsets ascending[0, 0.2, 0.5, 0.82, 1](first/last omitted → 0/1, middle explicit), andtranslate(dx*frac, dy*frac)withfrac == offsetkeeps translate on the straight path whilescalearcs independently (0.5 → 1.25 → 1.7 mid → 1.05 → 0.5). Theat(frac, scale, opacity, offset?)helper correctly omitsoffsetwhen undefined. easing →ease-in-outis fine for the symmetric arc.useReactFlow().screenToFlowPositionis the correct v12 call (v11'sproject, renamed in v12) — converts the dot's screen rect back to flow space so the anchor is pan/zoom-invariant. Selector.react-flow__node[data-id="…"] [data-flight-anchor]matches RF's node DOM;CSS.escapeis feature-detected with a fallback.CanvasFlightlazily initialisespos.currentONCE (if (pos.current === null)) — the accepted lazy-ref pattern, not a render side-effect — so a mid-flight pan/zoom or re-render won't restart the animation. It captures flow coordinates (stable across pan/zoom), which is exactly right given the envelope renders insideViewportPortal. All hooks (useRef,useReactFlow,useCanvasStore,useA2AFlights) are called unconditionally before theflights.length === 0early return — no conditional-hook violation.dotAnchorguardstypeof document !== "undefined"and feature-detectsCSS.escape, and falls back tonodeCenterwhen the dot isn't in the DOM or measures zero-size;FlightEnvelopeguardstypeof el.animate !== "function"(graceful degradation, covered by the new test).FlightEnvelope's public props (from/to/kind) are unchanged; only the internalanimate()keyframes changed, so the shared component stays API-compatible. The newFlightEnvelope.test.tsxlocks the contract both surfaces depend on (positioned atfrom, kind-coloured stroke send/receive/task, no-throw graceful degradation). 3 focused tests, appropriate given jsdom can't exercise WAAPI keyframe values.Two minor, NON-blocking notes:
dotAnchorreads the DOM (getBoundingClientRect) during render via the lazy-ref capture. In practice this is safe here — the source/target nodes already exist when an A2A flight fires (their dots are in the DOM), the read is captured-once, and thenodeCenterfallback guards the rare just-mounted case. Flagging only because reading layout in render is a mild smell;useLayoutEffectwould be the textbook alternative, but the current approach mirrors HomeFlight and is fallback-safe — fine to keep.FlightEnvelope, so the grow-then-shrink arc now also applies to concierge-home envelopes. That's API-compatible (no functional regression) — just confirming the consistent visual is intended across both surfaces.Neither blocks. tsc/eslint clean + 10/10 tests per the PR; my read agrees. Approving on
4f0f7b24.