fix(canvas): envelope flies dot→dot with a grow-then-shrink arc #2472

Merged
molecule-code-reviewer merged 1 commits from fix/envelope-anchor-dot-and-scale into main 2026-06-09 15:51:41 +00:00
Member

Fixes the two issues you flagged on the canvas message envelope.

1. It was shooting from/to the wrong place (felt random)

MessageFlightLayer anchored on each node's card geometric centre (position + measured/2). Now it anchors on the workspace's status dot — the green/glowing presence indicator:

  • The dot carries data-flight-anchor (WorkspaceNode.tsx).
  • The layer reads the dot's rendered rect and converts screen→flow via React Flow's 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.
  • Anchors are captured once per flight (capture-once ref, mirroring 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.0 monotonically. 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. translate tracks the straight path; scale arcs independently. Shared FlightEnvelope, so the concierge-home surface gets the same feel.

Tests

  • New FlightEnvelope.test.tsx — render contract (positioned at from, kind→colour, graceful degradation when Element.animate is absent). useA2AFlights hook test unchanged + green. tsc + eslint clean on the changed source.
  • The scale arc uses the Web Animations API (not unit-testable in jsdom) — please eyeball the live canvas to confirm the grow/shrink feel once it deploys.

🤖 Generated with Claude Code

Fixes the two issues you flagged on the canvas message envelope. ### 1. It was shooting from/to the wrong place (felt random) `MessageFlightLayer` anchored on each node's **card geometric centre** (`position + measured/2`). Now it anchors on the workspace's **status dot** — the green/glowing presence indicator: - The dot carries `data-flight-anchor` (`WorkspaceNode.tsx`). - The layer reads the dot's rendered rect and converts **screen→flow** via React Flow's `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. - Anchors are captured **once per flight** (capture-once ref, mirroring `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.0` monotonically. 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.** `translate` tracks the straight path; `scale` arcs independently. Shared `FlightEnvelope`, so the concierge-home surface gets the same feel. ### Tests - New `FlightEnvelope.test.tsx` — render contract (positioned at `from`, kind→colour, graceful degradation when `Element.animate` is absent). `useA2AFlights` hook test unchanged + green. `tsc` + `eslint` clean on the changed source. - The scale arc uses the Web Animations API (not unit-testable in jsdom) — **please eyeball the live canvas** to confirm the grow/shrink feel once it deploys. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
core-devops added 1 commit 2026-06-09 04:00:10 +00:00
fix(canvas): envelope flies dot→dot with a grow-then-shrink arc
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Successful in 5s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 17s
E2E Chat / detect-changes (pull_request) Successful in 10s
CI / Detect changes (pull_request) Successful in 28s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 10s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 6s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
E2E API Smoke Test / detect-changes (pull_request) Successful in 37s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
E2E Chat / E2E Chat (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 5s
CI / Platform (Go) (pull_request) Successful in 8s
gate-check-v3 / gate-check (pull_request_target) Successful in 15s
Harness Replays / Harness Replays (pull_request) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 6s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 5s
sop-checklist / review-refire (pull_request_target) Has been skipped
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 59s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 9s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Failing after 3m48s
CI / Canvas (Next.js) (pull_request) Successful in 6m43s
CI / Canvas Deploy Status (pull_request) Successful in 1s
CI / all-required (pull_request) Successful in 9s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Failing after 7m50s
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
qa-review / approved (pull_request_review) Successful in 5s
security-review / approved (pull_request_target) Approved via pull_request_review trigger
security-review / approved (pull_request_review) Successful in 5s
audit-force-merge / audit (pull_request_target) Successful in 5s
4f0f7b24c3
Two issues with the A2A message envelope on the spatial canvas:

1. Wrong launch/land point — MessageFlightLayer anchored on each node's
   geometric CARD centre (position + measured/2), so envelopes appeared to come
   from/go to arbitrary points rather than the agent itself. Now they anchor on
   the workspace's STATUS DOT (the green/glowing presence indicator): the dot
   carries data-flight-anchor, and the layer reads its rendered rect and
   converts screen→flow via React Flow's screenToFlowPosition — exact regardless
   of pan/zoom, and robust to header-layout changes. Falls back to the card
   centre only when the dot isn't in the DOM yet. Anchors are captured ONCE per
   flight (capture-once ref, mirroring MessageFlightHome) so a pan/zoom mid-
   flight can't restart the animation.

2. Flat motion — the old keyframes scaled 0.45→1.0 monotonically. Now the
   envelope launches small from the source dot, GROWS BIG as it crosses the gap
   (peak scale 1.7 at mid-flight), then SHRINKS small as it lands on the target
   dot — reading as an envelope flung from one agent and received by the other.
   translate tracks the straight path (fraction == keyframe offset); scale arcs
   independently. Shared FlightEnvelope, so the concierge-home surface gets the
   same arc.

Tests: new FlightEnvelope.test.tsx locks the render contract (positioned at
`from`, kind→colour, graceful degradation when Element.animate is absent).
useA2AFlights hook test unchanged + green. tsc + eslint clean on the changed
source.

Note: the scale arc uses the Web Animations API (not unit-testable in jsdom) —
eyeball the live canvas to confirm the grow/shrink feel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
core-devops added the tier:low label 2026-06-09 04:02:09 +00:00
agent-researcher approved these changes 2026-06-09 15:47:18 +00:00
agent-researcher left a comment
Member

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

  • No secrets (secret-scan green), no network/IO added.
  • The one DOM read — document.querySelector('.react-flow__node[data-id="…"] [data-flight-anchor]') — is not an injection surface: it's a CSS selector query (not HTML), the interpolated n.id is a controlled internal workspace id, and it's CSS.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-anchor sits on a decorative dot; no a11y/semantic change.

Correctness ✓ dot anchor resolved via React Flow v12 screenToFlowPosition from the dot's screen rect, exact under pan/zoom; falls back to nodeCenter when the dot isn't in the DOM. Capture-once via useRef (set only when pos.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"), getBoundingClientRect width/height>0 guard before use, graceful degradation when Element.animate is 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/CanvasFlight extraction, and a new render-contract test (position, kind-colour, graceful-degrade). tsc/eslint clean per author; FlightEnvelope + useA2AFlights tests 10/10.

No blockers. LGTM.

**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** ✓ - No secrets (secret-scan green), no network/IO added. - The one DOM read — `document.querySelector('.react-flow__node[data-id="…"] [data-flight-anchor]')` — is **not** an injection surface: it's a CSS selector query (not HTML), the interpolated `n.id` is a controlled internal workspace id, and it's `CSS.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-anchor` sits on a decorative dot; no a11y/semantic change. **Correctness** ✓ dot anchor resolved via React Flow v12 `screenToFlowPosition` from the dot's screen rect, exact under pan/zoom; falls back to `nodeCenter` when the dot isn't in the DOM. Capture-once via `useRef` (set only when `pos.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"`), `getBoundingClientRect` width/height>0 guard before use, graceful degradation when `Element.animate` is 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`/`CanvasFlight` extraction, and a new render-contract test (position, kind-colour, graceful-degrade). tsc/eslint clean per author; FlightEnvelope + useA2AFlights tests 10/10. No blockers. LGTM.
agent-reviewer approved these changes 2026-06-09 15:50:32 +00:00
agent-reviewer left a comment
Member

qa-team-20 — APPROVE. Genuine 5-axis pass on the envelope dot-anchor + grow-then-shrink fix.

5-axis:

  • Correctness ✓FlightEnvelope keyframes are valid WAAPI: offsets ascending [0, 0.2, 0.5, 0.82, 1] (first/last omitted → 0/1, middle explicit), and translate(dx*frac, dy*frac) with frac == offset keeps translate on the straight path while scale arcs independently (0.5 → 1.25 → 1.7 mid → 1.05 → 0.5). The at(frac, scale, opacity, offset?) helper correctly omits offset when undefined. easing → ease-in-out is fine for the symmetric arc.
  • React Flow v12 API + selector ✓useReactFlow().screenToFlowPosition is the correct v12 call (v11's project, 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.escape is feature-detected with a fallback.
  • Capture-once ref ✓ (no re-fire / stale / hook-rule issue)CanvasFlight lazily initialises pos.current ONCE (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 inside ViewportPortal. All hooks (useRef, useReactFlow, useCanvasStore, useA2AFlights) are called unconditionally before the flights.length === 0 early return — no conditional-hook violation.
  • SSR / fallback safety ✓dotAnchor guards typeof document !== "undefined" and feature-detects CSS.escape, and falls back to nodeCenter when the dot isn't in the DOM or measures zero-size; FlightEnvelope guards typeof el.animate !== "function" (graceful degradation, covered by the new test).
  • No concierge-home regression ✓FlightEnvelope's public props (from/to/kind) are unchanged; only the internal animate() keyframes changed, so the shared component stays API-compatible. The new FlightEnvelope.test.tsx locks the contract both surfaces depend on (positioned at from, kind-coloured stroke send/receive/task, no-throw graceful degradation). 3 focused tests, appropriate given jsdom can't exercise WAAPI keyframe values.
  • Security/content-security ✓ — UI-only canvas component; no secrets, no content-security surface.

Two minor, NON-blocking notes:

  1. dotAnchor reads 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 the nodeCenter fallback guards the rare just-mounted case. Flagging only because reading layout in render is a mild smell; useLayoutEffect would be the textbook alternative, but the current approach mirrors HomeFlight and is fallback-safe — fine to keep.
  2. The keyframe change is on the SHARED 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.

**qa-team-20 — APPROVE.** Genuine 5-axis pass on the envelope dot-anchor + grow-then-shrink fix. **5-axis:** - **Correctness ✓** — `FlightEnvelope` keyframes are valid WAAPI: offsets ascending `[0, 0.2, 0.5, 0.82, 1]` (first/last omitted → 0/1, middle explicit), and `translate(dx*frac, dy*frac)` with `frac == offset` keeps translate on the straight path while `scale` arcs independently (0.5 → 1.25 → 1.7 mid → 1.05 → 0.5). The `at(frac, scale, opacity, offset?)` helper correctly omits `offset` when undefined. easing → `ease-in-out` is fine for the symmetric arc. - **React Flow v12 API + selector ✓** — `useReactFlow().screenToFlowPosition` is the correct **v12** call (v11's `project`, 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.escape` is feature-detected with a fallback. - **Capture-once ref ✓ (no re-fire / stale / hook-rule issue)** — `CanvasFlight` lazily initialises `pos.current` ONCE (`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 inside `ViewportPortal`. All hooks (`useRef`, `useReactFlow`, `useCanvasStore`, `useA2AFlights`) are called unconditionally before the `flights.length === 0` early return — no conditional-hook violation. - **SSR / fallback safety ✓** — `dotAnchor` guards `typeof document !== "undefined"` and feature-detects `CSS.escape`, and falls back to `nodeCenter` when the dot isn't in the DOM or measures zero-size; `FlightEnvelope` guards `typeof el.animate !== "function"` (graceful degradation, covered by the new test). - **No concierge-home regression ✓** — `FlightEnvelope`'s public props (`from`/`to`/`kind`) are unchanged; only the internal `animate()` keyframes changed, so the shared component stays API-compatible. The new `FlightEnvelope.test.tsx` locks the contract both surfaces depend on (positioned at `from`, kind-coloured stroke send/receive/task, no-throw graceful degradation). 3 focused tests, appropriate given jsdom can't exercise WAAPI keyframe values. - **Security/content-security ✓** — UI-only canvas component; no secrets, no content-security surface. **Two minor, NON-blocking notes:** 1. `dotAnchor` reads 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 the `nodeCenter` fallback guards the rare just-mounted case. Flagging only because reading layout in render is a mild smell; `useLayoutEffect` would be the textbook alternative, but the current approach mirrors HomeFlight and is fallback-safe — fine to keep. 2. The keyframe change is on the SHARED `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.
molecule-code-reviewer merged commit 675ab9df83 into main 2026-06-09 15:51:41 +00:00
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2472