Skip to content

Preserve wave context (scroll position + chat draft) across new-version reloads#3129

Open
brookr wants to merge 16 commits into
mainfrom
feat/preserve-wave-position-on-update-reload
Open

Preserve wave context (scroll position + chat draft) across new-version reloads#3129
brookr wants to merge 16 commits into
mainfrom
feat/preserve-wave-position-on-update-reload

Conversation

@brookr

@brookr brookr commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Issue

  • Every release surfaces the new-version toast; clicking it does a full location.reload(). Wave chats boot at the newest message, so anyone reading back in history lands at the bottom and loses their place — on every release, and we release a lot.

Fix

  • Before reloading, the toast pins the reader's position into the URL and lets the existing deep-link machinery do the rest. No new persistence, no scroll-offset caching:
    • Drop rows already render id="drop-<serial_no>" (DropsList) — the helper reads the row nearest the scroll container's vertical center.
    • ?serialNo=<n> is the long-standing copy-link/notification contract: on load the app fetches around that serial, scrolls it to viewport center, and cleans the param from the URL (MyStreamWaveChat + useWaveDropsSerialScroll). Capture-to-center and restore-to-center are symmetric.
    • Readers within 48px of the newest message are deliberately not pinned — they're in live mode and expect the bottom after reload.
  • Handles column-reverse scroll semantics (matching useWaveDropsSerialScroll), multiple mounted chats (largest visible container wins), and non-numeric row ids.

Changes

  • helpers/waves/wave-visible-serial.helpers.ts (new): capture + URL-pinning helpers, documented.
  • components/utils/NewVersionToast.tsx: one call in the refresh handler.
  • components/waves/drops/WaveDropsReverseContainer.tsx: one data attribute marking the wave scroll container.
  • Tests: 8 helper cases (center pick, at-bottom no-op both flex directions, multi-container, malformed ids, URL pinning/no-op) + toast handler assertion.

Validation

  • Jest: 18/18 across both touched suites; lint clean.
  • Live Playwright, full loop against local dev (using the ?showNewVersionToast=true override to render the real toast and click the real handler):
    • Scrolled-up reader: centered drop 6826 → reload URL gained serialNo=6826 → after reload the same drop was visible 83px from container center and the param was cleaned from the URL.
    • At-bottom reader: no serial pinned; lands at newest exactly as before.

Risk

  • Level: Low
  • Why: additive; the capture is a read-only DOM inspection that no-ops anywhere outside a wave chat, and the restore path is the battle-tested deep-link flow exercised daily by copy-link. Failure mode degrades to today's behavior (bottom of feed).
  • Rollback: revert the commit.

Deployment

  • Frontend only. Note the bootstrap quirk: the release that ships this still reloads the old way (the running bundle handles the toast); the benefit starts with the release after.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Wave reading position is now preserved when refreshing after a new-version notification.
    • In-progress wave drafts are restored across page reloads when valid.
    • Active subwave selection and sidebar expansion are maintained during reloads and loading transitions.
  • Bug Fixes

    • Improved highlighted-drop tracking to support accurate scroll-position restoration.
    • Invalid or unsafe drafts are ignored and cleared instead of appearing in the editor.

Every release shows the new-version toast; clicking it reloads and, for
wave readers who had scrolled up, dumped them at the bottom. The toast
now pins the drop nearest viewport center into the URL as ?serialNo=<n>
(rows already carry id="drop-<serial>") before reloading, so the
existing deep-link restore path — fetch around the serial, scroll it to
center, clean the param — puts them back where they were. Readers at
the bottom stay in live mode: nothing is pinned within 48px of newest.

No new state or persistence: one data attribute on the wave scroll
container, a DOM-reading helper, and one call in the toast handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@brookr brookr requested a review from a team July 6, 2026 05:18
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds reload-time wave scroll-position URL pinning, active subwave parent recovery through sessionStorage, and validated wave draft persistence with Lexical editor restoration. Sidebar, toast, wave-drop rendering, composer initialization, and related tests are updated.

Changes

Wave reload and draft persistence

Layer / File(s) Summary
Scroll position capture and reload integration
helpers/waves/wave-visible-serial.helpers.ts, components/waves/drops/WaveDropsReverseContainer.tsx, components/drops/view/*, components/utils/NewVersionToast.tsx, __tests__/helpers/wave-visible-serial.helpers.test.ts, __tests__/components/utils/NewVersionToast.test.tsx
Visible drop serials are captured from the dominant wave scroll container, persisted as serialNo, and recorded before reload after the toast parameter is removed.
Active subwave parent recovery
helpers/waves/active-subwave-hint.helpers.ts, hooks/useActiveSubwaveParentHint.ts, components/brain/left-sidebar/waves/UnifiedWavesListWaves.tsx, components/brain/left-sidebar/web/WebUnifiedWavesListWaves.tsx, __tests__/helpers/active-subwave-hint.helpers.test.ts
Active subwave parent hints are read and written through sessionStorage, resolved against live parent data, and used by sidebar tree loading and expansion.
Wave draft persistence and editor restoration
helpers/waves/wave-draft.helpers.ts, components/waves/CreateDropInput.tsx, __tests__/helpers/wave-draft.helpers.test.ts, __tests__/components/waves/CreateDropInput.draftRestore.test.tsx, __tests__/components/waves/create-drop-content/useWaveDraftPersistence.test.tsx
Per-wave Lexical drafts are validated, stored, cleared, restored, and seeded into the editor with malformed-state fallback handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NewVersionToast
  participant ScrollPositionHelper
  participant WaveDropRows
  participant BrowserHistory

  NewVersionToast->>ScrollPositionHelper: preserveWaveScrollPositionForReload()
  ScrollPositionHelper->>WaveDropRows: locate container and visible serial
  WaveDropRows-->>ScrollPositionHelper: return serialNo
  ScrollPositionHelper->>BrowserHistory: replaceState with serialNo
  NewVersionToast->>BrowserHistory: reload()
Loading

Suggested reviewers: simo6529

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: preserving wave scroll position and chat draft state during new-version reloads.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/preserve-wave-position-on-update-reload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot general PR review - 587d867

Verdict: Needs changes

Important

  • helpers/waves/wave-visible-serial.helpers.ts:98-115 — The nearest-to-center selection skips any candidate whose distance >= bestDistance before checking whether the row is a valid serial. If the true nearest visible row has a non-numeric id (e.g. drop-abc, or a nested element that also matches [id^="drop-"]), its small distance still sets bestDistance... no — actually the ordering is worse: for a closer non-numeric row you continue at the serial === null check without updating bestDistance, so a farther valid row can still win. But the reverse case bites: a valid row that is genuinely nearest can be rejected only after a closer non-numeric row already lowered... it does not, since bestDistance is only lowered on success. The real bug is that querySelectorAll([id^="drop-"]) can match descendant elements (not just top-level drop rows) whose ids happen to share the prefix, and the "nearest center" among those may not be the intended row. Consider constraining the selector to actual drop rows (e.g. a dedicated data attribute) rather than an id-prefix match, to avoid capturing the wrong serial.

  • helpers/waves/wave-visible-serial.helpers.ts:36-41getDistanceFromBottom reads container.style.flexDirection first, but WaveDropsReverseContainer sets the reverse layout via the Tailwind class tw-flex-col-reverse, not an inline style. In production container.style.flexDirection will be empty, so this relies entirely on getComputedStyle. That works in the browser but the inline-first branch is effectively test-only scaffolding; confirm getComputedStyle(container).flexDirection reliably returns "column-reverse" for the class-based layout in the target browsers, otherwise the code falls into the plain-column branch (scrollRange - scrollTop) and mis-detects "at bottom", pinning the wrong serial or nothing.

Nice-to-have

  • components/utils/NewVersionToast.tsx:42-48preserveWaveScrollPositionForReload() runs unguarded before location.reload(). If it ever throws (DOM edge cases, detached nodes), the reload is skipped and the toast action silently fails. Wrap the pin call in a try/catch so a capture failure never blocks the refresh.

  • helpers/waves/wave-visible-serial.helpers.ts:60-65Array.reduce without an initial value on a filtered array is safe here (length checked > 0), but if two containers tie on area the first is kept arbitrarily; fine for now, just note the tie-break is undefined.

  • Test gap: no test covers the case where the nearest-center row is non-numeric but a numeric row is farther (the selection/bestDistance interaction in captureVisibleWaveDropSerial). The existing "ignores non-serial ids" test places the valid row far from center with different geometry, so it doesn't exercise the ordering hazard. Add a case where a drop-abc row is closest to center.

Suggested next steps

  • Constrain the row selector to genuine drop rows and re-verify serial selection against non-numeric/nested ids.
  • Confirm column-reverse detection works via getComputedStyle for the production class-based layout.
  • Guard the pin call against exceptions so the reload always proceeds.
Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

Review comments:
From 6529bot general PR review on 6529-Collections/6529seize-frontend#3129 (587d86788624):
**Verdict**: Needs changes

### Important

- `helpers/waves/wave-visible-serial.helpers.ts:98-115` — The nearest-to-center selection skips any candidate whose `distance >= bestDistance` *before* checking whether the row is a valid serial. If the true nearest visible row has a non-numeric id (e.g. `drop-abc`, or a nested element that also matches `[id^="drop-"]`), its small distance still sets `bestDistance`... no — actually the ordering is worse: for a closer non-numeric row you `continue` at the `serial === null` check without updating `bestDistance`, so a farther valid row can still win. But the reverse case bites: a valid row that is genuinely nearest can be rejected only after a *closer non-numeric* row already lowered... it does not, since `bestDistance` is only lowered on success. The real bug is that `querySelectorAll(`[id^="drop-"]`)` can match descendant elements (not just top-level drop rows) whose ids happen to share the prefix, and the "nearest center" among those may not be the intended row. Consider constraining the selector to actual drop rows (e.g. a dedicated data attribute) rather than an id-prefix match, to avoid capturing the wrong serial.

- `helpers/waves/wave-visible-serial.helpers.ts:36-41` — `getDistanceFromBottom` reads `container.style.flexDirection` first, but `WaveDropsReverseContainer` sets the reverse layout via the Tailwind class `tw-flex-col-reverse`, not an inline style. In production `container.style.flexDirection` will be empty, so this relies entirely on `getComputedStyle`. That works in the browser but the inline-first branch is effectively test-only scaffolding; confirm `getComputedStyle(container).flexDirection` reliably returns `"column-reverse"` for the class-based layout in the target browsers, otherwise the code falls into the plain-column branch (`scrollRange - scrollTop`) and mis-detects "at bottom", pinning the wrong serial or nothing.

### Nice-to-have

- `components/utils/NewVersionToast.tsx:42-48` — `preserveWaveScrollPositionForReload()` runs unguarded before `location.reload()`. If it ever throws (DOM edge cases, detached nodes), the reload is skipped and the toast action silently fails. Wrap the pin call in a try/catch so a capture failure never blocks the refresh.

- `helpers/waves/wave-visible-serial.helpers.ts:60-65` — `Array.reduce` without an initial value on a filtered array is safe here (length checked `> 0`), but if two containers tie on area the first is kept arbitrarily; fine for now, just note the tie-break is undefined.

- Test gap: no test covers the case where the nearest-center row is non-numeric but a numeric row is farther (the selection/`bestDistance` interaction in `captureVisibleWaveDropSerial`). The existing "ignores non-serial ids" test places the valid row far from center with different geometry, so it doesn't exercise the ordering hazard. Add a case where a `drop-abc` row is closest to center.

**Suggested next steps**
- Constrain the row selector to genuine drop rows and re-verify serial selection against non-numeric/nested ids.
- Confirm `column-reverse` detection works via `getComputedStyle` for the production class-based layout.
- Guard the pin call against exceptions so the reload always proceeds.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot WCAG 2.2 AA analysis - 587d867

Verdict: No WCAG findings

This PR adds a DOM-reading helper that captures the reader's scroll position (nearest drop serial) and pins it into the URL before a version-toast reload. The change is behavioral/navigational and does not introduce interactive UI, alter accessible names, focus behavior, or semantics.

The only markup change is a non-visual data-wave-drops-scroll-container attribute on the existing scroll container (WaveDropsReverseContainer.tsx:75), which has no accessibility impact. The toast button retains its accessible name (aria-label/title via refreshActionLabel) and visible focus ring (focus-visible:tw-ring-2), both unchanged.

From an accessibility standpoint, preserving reading position across reloads is a mild positive for users of screen magnification and cognitive-load-sensitive users, since it avoids disorienting relocation to the feed bottom. No color, contrast, target-size, motion, or live-region concerns are introduced.

Note: the correctness concerns already raised in the general lane review (selector scoping, column-reverse detection) are functional, not accessibility, and are out of scope here.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot i18n analysis - 587d867

Verdict: No i18n findings

The changed surface is scroll-position-preservation plumbing: a DOM-reading helper, a data attribute on the scroll container, and one call in the toast handler. No new user-facing copy, accessible names, dates, numbers, or locale-sensitive formatting are introduced.

  • helpers/waves/wave-visible-serial.helpers.ts deals only with DOM geometry, serial parsing (Number.parseInt on machine ids, not display), and URL query params — none of which are localizable.
  • components/utils/NewVersionToast.tsx:47 adds a side-effect call before reload; visible copy still flows through t(locale, ...) and the existing aria-label/title (line 103-104) are unchanged.
  • WaveDropsReverseContainer.tsx:75 adds a non-visible data-* attribute, not text.
  • Test files add no user-facing strings.

No i18n regressions in this PR.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot general PR review - 587d867

Verdict: Needs changes

The prior 6529bot review at this same head already covered the main issues (id-prefix selector matching nested/non-numeric ids, column-reverse detection relying on getComputedStyle, unguarded pin call, and the non-numeric nearest-row test gap). I won't repeat those. One additional distinct finding below.

Important

  • helpers/waves/wave-visible-serial.helpers.ts:98-115 — The distance >= bestDistance early-continue runs before parseRowSerial, so a non-numeric row that is genuinely nearest center never lowers bestDistance (it's skipped at the serial check), but a numeric row slightly farther than that non-numeric row still passes the distance gate and wins — which is the intended fallback. However, there is a real ordering bug in the opposite direction: because bestDistance is only updated on a valid serial, if two valid rows exist where the first-iterated one is farther, the loop works; but the >= (not >) comparison means a valid row at exactly the same distance as the current best is discarded. With column-reverse layouts two adjacent rows can share a center-distance (symmetric around center), and iteration order (DOM order) then silently decides the winner with no tie-break rationale. Prefer selecting the row whose center is closest, tie-breaking deterministically toward the newer/older serial rather than DOM order. Low severity, but worth a defined tie-break.

  • helpers/waves/wave-visible-serial.helpers.ts:36-41 — Beyond the getComputedStyle reliance already flagged: this falls back to scrollRange - container.scrollTop for the non-reverse branch, but the production container is always column-reverse (Tailwind tw-flex-col-reverse). If getComputedStyle ever returns something unexpected (e.g. during a paint/hydration race where computed styles aren't ready), the plain-column formula yields a large distance-from-bottom, defeating the "live mode" guard and pinning a serial for a user who was actually at the bottom — the exact regression this PR aims to avoid. Consider defaulting to the reverse interpretation for this known container rather than the plain-column fallback.

Nice-to-have

  • Test gap: no test covers getVisibleWaveScrollContainer's tie in reduce when two containers have equal area, nor the history.state preservation in preserveWaveScrollPositionForReload (it passes globalThis.history.state through — worth asserting existing state isn't clobbered).

Suggested next steps

  • Address the still-open items from the prior review (selector scoping, column-reverse detection, guarded pin call, non-numeric-nearest test).
  • Define a deterministic tie-break for equal center-distances and default the known container to reverse-layout interpretation.

Note: coverage here is limited to the included diff/context; not exhaustive.

Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

Review comments:
From 6529bot general PR review on 6529-Collections/6529seize-frontend#3129 (587d86788624):
**Verdict**: Needs changes

The prior 6529bot review at this same head already covered the main issues (id-prefix selector matching nested/non-numeric ids, `column-reverse` detection relying on `getComputedStyle`, unguarded pin call, and the non-numeric nearest-row test gap). I won't repeat those. One additional distinct finding below.

### Important

- `helpers/waves/wave-visible-serial.helpers.ts:98-115` — The `distance >= bestDistance` early-`continue` runs *before* `parseRowSerial`, so a non-numeric row that is genuinely nearest center never lowers `bestDistance` (it's skipped at the serial check), but a numeric row slightly farther than that non-numeric row still passes the distance gate and wins — which is the intended fallback. However, there is a real ordering bug in the opposite direction: because `bestDistance` is only updated on a *valid* serial, if two valid rows exist where the first-iterated one is farther, the loop works; but the `>=` (not `>`) comparison means a valid row at *exactly* the same distance as the current best is discarded. With `column-reverse` layouts two adjacent rows can share a center-distance (symmetric around center), and iteration order (DOM order) then silently decides the winner with no tie-break rationale. Prefer selecting the row whose center is closest, tie-breaking deterministically toward the newer/older serial rather than DOM order. Low severity, but worth a defined tie-break.

- `helpers/waves/wave-visible-serial.helpers.ts:36-41` — Beyond the `getComputedStyle` reliance already flagged: this falls back to `scrollRange - container.scrollTop` for the non-reverse branch, but the production container is *always* `column-reverse` (Tailwind `tw-flex-col-reverse`). If `getComputedStyle` ever returns something unexpected (e.g. during a paint/hydration race where computed styles aren't ready), the plain-column formula yields a large distance-from-bottom, defeating the "live mode" guard and pinning a serial for a user who was actually at the bottom — the exact regression this PR aims to avoid. Consider defaulting to the reverse interpretation for this known container rather than the plain-column fallback.

### Nice-to-have

- Test gap: no test covers `getVisibleWaveScrollContainer`'s tie in `reduce` when two containers have equal area, nor the `history.state` preservation in `preserveWaveScrollPositionForReload` (it passes `globalThis.history.state` through — worth asserting existing state isn't clobbered).

**Suggested next steps**
- Address the still-open items from the prior review (selector scoping, `column-reverse` detection, guarded pin call, non-numeric-nearest test).
- Define a deterministic tie-break for equal center-distances and default the known container to reverse-layout interpretation.

*Note: coverage here is limited to the included diff/context; not exhaustive.*

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot crypto security analysis - 587d867

Verdict: No security findings

This PR reads a numeric drop serial from the DOM and writes it as a serialNo query param via history.replaceState before reloading. The change is navigational and does not touch wallet, auth, signature, token, or crypto/web3 paths.

Security-relevant checks I verified in the changed code:

  • parseRowSerial (helpers/waves/wave-visible-serial.helpers.ts:68-75) strictly validates the id suffix against /^\d+$/ and coerces via Number.parseInt, so no attacker-controlled string is propagated into the URL param — only a bounded integer. No injection into the query string.
  • The serial is written with URL/searchParams.set (:129-131), which URL-encodes values; combined with the numeric-only guard there is no query-param injection or open-redirect surface (host/path are untouched, only a search param is added).
  • history.replaceState preserves existing history.state and does not cross origins; no session/navigation-state tampering risk beyond the intended param write.
  • The downstream serialNo deep-link path is pre-existing and authoritative for fetch/scroll behavior; this PR only supplies a validated integer to it.

The correctness concerns already raised in the general lane (selector scoping to genuine drop rows, column-reverse detection via getComputedStyle, and unguarded pin call) are functional, not security, and are out of scope here. No new secret handling, external fetch, or untrusted-media surface is introduced.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot GLM Swarm Review

Verdict: Advisory only

This GLM swarm is advisory and complements, not replaces, existing tests and existing reviewbots.

Important

  • helpers/waves/wave-visible-serial.helpers.ts:35-37container.style.flexDirection reads only the inline style, but in production the reverse layout comes from the Tailwind class tw-flex-col-reverse, not an inline flexDirection. The column-reverse branch may never execute against the real DOM, causing getDistanceFromBottom to fall through to the plain-column formula and misclassify "at bottom." This directly affects the live-mode no-op: readers at the bottom could get a serialNo pinned and experience a jump on reload. Suggested fix: use getComputedStyle(container).flexDirection instead of the inline style, and add a test mocking window.getComputedStyle to return column-reverse.

Nice-to-have

  • __tests__/components/utils/NewVersionToast.test.tsx:289-296 — The test only asserts preserveWaveScrollPositionForReload was called once, but does not verify ordering relative to removeNewVersionToastOverrideFromCurrentPath and location.reload. The helper must run after the toast param is stripped and before reload. Consider asserting call order via toHaveBeenCalledBefore, or add an integration-style test that renders NewVersionToast without mocking the helper and asserts the final URL contains serialNo but not showNewVersionToast.
  • helpers/waves/wave-visible-serial.helpers.ts:60-65 — The reduce selects the largest container by clientWidth * clientHeight, but nested elements with the data-wave-drops-scroll-container attribute could skew selection. Consider a test with a nested smaller container to confirm the largest-area one wins.

Testing feedback loop

  • Run __tests__/helpers/wave-visible-serial.helpers.test.ts and __tests__/components/utils/NewVersionToast.test.tsx — both are deterministic and cover the core contract.
  • The highest-value missing case is a getComputedStyle-based test for the column-reverse branch; this is the most likely real-world divergence from jsdom.
  • Consider a single integration-style test (not replacing unit tests) that mounts a fake container with data-wave-drops-scroll-container, triggers refresh, and asserts the URL ends with serialNo=<n> and without showNewVersionToast.

Partial reviewer output

One or more internal advisory reviewer slices were unavailable; the synthesis used the remaining reviewer output.

  • correctness-regression: empty_output
  • frontend-behavior: empty_output

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
components/waves/drops/WaveDropsReverseContainer.tsx (1)

75-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Attribute is a hardcoded string duplicate of WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE.

Value currently matches the helper's constant, but nothing ties them together — renaming the constant in wave-visible-serial.helpers.ts won't update this JSX literal, silently breaking capture. Since data-mobile-bottom-nav-scroll-target on the same line follows the same hardcoded pattern, this is consistent with existing style, but worth tightening for the new coupling this PR introduces.

♻️ Suggested fix using computed attribute spread
+import { WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE } from "`@/helpers/waves/wave-visible-serial.helpers`";
+
 ...
       <div
         ref={handleScrollContainerRef}
         data-mobile-bottom-nav-scroll-target="true"
-        data-wave-drops-scroll-container="true"
+        {...{ [WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE]: "true" }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/waves/drops/WaveDropsReverseContainer.tsx` at line 75, The
`WaveDropsReverseContainer` JSX is hardcoding a
`data-wave-drops-scroll-container` attribute that duplicates
`WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE`, so it can drift if the helper constant
changes. Update the container markup to use the shared constant from
`wave-visible-serial.helpers.ts` instead of a literal string, and apply the same
pattern to the adjacent `data-mobile-bottom-nav-scroll-target` attribute if it
is intended to stay coupled to a shared symbol. Use the existing
`WaveDropsReverseContainer` render path to keep the attribute source of truth
centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@components/waves/drops/WaveDropsReverseContainer.tsx`:
- Line 75: The `WaveDropsReverseContainer` JSX is hardcoding a
`data-wave-drops-scroll-container` attribute that duplicates
`WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE`, so it can drift if the helper constant
changes. Update the container markup to use the shared constant from
`wave-visible-serial.helpers.ts` instead of a literal string, and apply the same
pattern to the adjacent `data-mobile-bottom-nav-scroll-target` attribute if it
is intended to stay coupled to a shared symbol. Use the existing
`WaveDropsReverseContainer` render path to keep the attribute source of truth
centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 411d80e3-2ced-49f8-b161-1bb967dbfe65

📥 Commits

Reviewing files that changed from the base of the PR and between 3ed7b43 and 587d867.

📒 Files selected for processing (5)
  • __tests__/components/utils/NewVersionToast.test.tsx
  • __tests__/helpers/wave-visible-serial.helpers.test.ts
  • components/utils/NewVersionToast.tsx
  • components/waves/drops/WaveDropsReverseContainer.tsx
  • helpers/waves/wave-visible-serial.helpers.ts

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot responsiveness review - 587d867

Verdict: Needs changes

Root Cause / Summary

The web-desktop, web-mobile, and electron-desktop layouts render cleanly across all routes; the only genuine layout/branch failure is in the native-mobile Capacitor simulation. On native-mobile /meme-lab, the app never applied body.capacitor-native, so the native shell contract failed. More broadly, the native-mobile context is blocked at an End User License Agreement (EULA) gate on most routes, which means the app content behind it was never actually exercised — the screenshots for /, /waves, /the-memes, /rememes, /meme-lab, and /meme-calendar mostly show the same modal, not the target page.

Blocking Findings

  • native-mobile /meme-labbody.capacitor-native was not applied (deterministic failure). This is the one hard profile-contract failure. On this route the native shell never activated: hasCapacitorNativeClass=false, bottomNavigationVisible=false, and the deterministic probe reported "bottom navigation was not visible." Every other native-mobile route in this run did get hasCapacitorNativeClass=true and a 64px bottom nav, so this is an intermittent/route-specific activation failure worth investigating (likely a timing/ordering race in when the native class is set relative to the EULA modal mounting). screenshot

Non-Blocking Findings

  • Native-mobile EULA gate blocks content evaluation (app-wide simulation limitation). native-mobile /, /waves, /the-memes, /rememes, /meme-lab, and /meme-calendar all render the "End User License Agreement / To use 6529 Mobile, you must agree…" modal instead of the route content. This is expected first-run behavior in the native shell, but it means responsiveness of the underlying pages was not truly verified on native. The EULA modal itself lays out correctly at mobile width with a readable body and visible Agree button. meme-calendar
  • Native plugin shims unavailable (simulation boundary). Every native-mobile route warned Keyboard plugin shim is unavailable and App plugin shim is unavailable. This is expected in the browser-level Capacitor simulation and is not real device behavior; flag only if these shims are meant to be stubbed in the harness.
  • web-desktop / web-mobile /meme-lab and /rememes still compiling/fetching. web-desktop /meme-lab shows a "Compiling…" overlay and "Fetching" state, and web-desktop /rememes shows "Fetching" with an empty grid. This is dev-server compile/data timing, not a layout defect — contentReady=true and no Next.js error overlay. web-desktop meme-lab
  • Screenshot timeouts on heavy routes. native-mobile /network produced no screenshot (full-page + viewport fallback both timed out at 20s/10s), and /the-memes, /rememes (native) plus electron-desktop /the-memes fell back to viewport captures. These are long, media-heavy pages; treat as capture-timing noise, not app failures.
  • Console errors. web-mobile /rememes (4), electron-desktop /rememes (2), and each native-mobile route (2) logged console errors. None produced a Next.js overlay or blank render; worth a quick look but non-blocking.

Platform Coverage

  • web-desktop (1440x900): Clean. WebLayout + sidebar render correctly on all 8 routes (layoutRootDataMobile=false), no failures/warnings. Network table, meme calendar, waves, discover, and home all look correct.
  • web-mobile: SmallScreenLayout renders correctly (single-column cards, hamburger nav, no desktop sidebar) across all routes. Only a /rememes console-error warning.
  • electron-desktop: electronDetected=true on all routes — the Electron user-agent branch activated correctly. Renders identically to web-desktop as expected. Note this is a Chromium renderer simulation, not a packaged desktop launch.
  • native-mobile: Capacitor simulation. Bottom nav and body.capacitor-native activated on 6 of 7 routes but failed on /meme-lab (the one blocking issue). Most routes are gated behind the EULA modal, and plugin shims are stubbed out — both are simulation boundaries, not real iOS/Android behavior.

Bottom line: Fix/investigate the intermittent body.capacitor-native activation on native

Screenshot evidence: native-mobile /meme-lab, native-mobile /rememes, native-mobile /the-memes, native-mobile /, native-mobile /waves, native-mobile /discover, native-mobile /meme-calendar, electron-desktop /the-memes, web-mobile /rememes, electron-desktop /rememes, web-desktop /the-memes, electron-desktop /meme-lab and 19 more in the details.

Deterministic responsiveness details

Verdict: Needs changes

Responsiveness runner summary

6529bot Responsiveness Summary

Status: fail
Duration: 438.2s
Profile: 6529 Seize frontend
Contexts: web-desktop, web-mobile, native-mobile, electron-desktop
Routes: /, /waves, /network, /the-memes, /meme-lab, /rememes, /meme-calendar, /discover
Checks completed: 32/32
Failures: 2
Warnings: 11

Run Errors

  • : Error: 6529 native profile: body.capacitor-native was not applied

Platform Context

  • web-desktop should exercise WebLayout/sidebar at 1440x900.
  • web-mobile should exercise SmallScreenLayout, not the native AppLayout.
  • native-mobile/native-ios/native-android are fast browser-level Capacitor simulations of AppLayout, bottom navigation, viewport-fit, keyboard/app/deep-link plugins, and body class.
  • electron-desktop is a fast browser-level Electron renderer simulation using the Electron user-agent branch; it is not a packaged desktop app launch.
  • Access/restricted pages intentionally bypass the standard app layout shell.

Platform Matrix

  • electron-desktop: 8 check(s), failures=0, warnings=2, avg=21.2s
  • native-mobile: 8 check(s), failures=1, warnings=8, avg=28.4s
  • web-desktop: 8 check(s), failures=0, warnings=0, avg=27.5s
  • web-mobile: 8 check(s), failures=0, warnings=1, avg=22.5s

Failures

  • 1 check(s): 6529 native profile: body.capacitor-native was not applied; examples: native-mobile /meme-lab

Next.js Overlay Diagnostics

  • web-desktop /meme-lab: Compiling

Warnings

  • electron-desktop /rememes: 2 console error(s)
  • electron-desktop /the-memes: full-page screenshot failed; used viewport fallback: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m
  • native-mobile /discover: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
  • native-mobile /meme-calendar: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
  • native-mobile /meme-lab: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 6529 native profile: bottom navigation was not visible; 2 console error(s)
  • native-mobile /network: screenshot unavailable after content rendered: screenshot failed: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; viewport fallback failed: page.screenshot: Timeout 10000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
  • native-mobile /rememes: full-page screenshot failed; used viewport fallback: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
  • native-mobile /: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
  • native-mobile /the-memes: full-page screenshot failed; used viewport fallback: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
  • native-mobile /waves: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
  • web-mobile /rememes: 4 console error(s)

Slowest Checks

Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

Review comments:
From 6529bot responsiveness review on 6529-Collections/6529seize-frontend#3129 (587d86788624):
**Verdict**: Needs changes

**Root Cause / Summary**

The web-desktop, web-mobile, and electron-desktop layouts render cleanly across all routes; the only genuine layout/branch failure is in the native-mobile Capacitor simulation. On `native-mobile /meme-lab`, the app never applied `body.capacitor-native`, so the native shell contract failed. More broadly, the native-mobile context is blocked at an **End User License Agreement (EULA) gate** on most routes, which means the app content behind it was never actually exercised — the screenshots for `/`, `/waves`, `/the-memes`, `/rememes`, `/meme-lab`, and `/meme-calendar` mostly show the same modal, not the target page.

**Blocking Findings**

- **`native-mobile` `/meme-lab` — `body.capacitor-native` was not applied** (deterministic failure). This is the one hard profile-contract failure. On this route the native shell never activated: `hasCapacitorNativeClass=false`, `bottomNavigationVisible=false`, and the deterministic probe reported "bottom navigation was not visible." Every other native-mobile route in this run *did* get `hasCapacitorNativeClass=true` and a 64px bottom nav, so this is an intermittent/route-specific activation failure worth investigating (likely a timing/ordering race in when the native class is set relative to the EULA modal mounting). [screenshot](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--meme-lab.png)

**Non-Blocking Findings**

- **Native-mobile EULA gate blocks content evaluation (app-wide simulation limitation).** `native-mobile` `/`, `/waves`, `/the-memes`, `/rememes`, `/meme-lab`, and `/meme-calendar` all render the "End User License Agreement / To use 6529 Mobile, you must agree…" modal instead of the route content. This is expected first-run behavior in the native shell, but it means responsiveness of the underlying pages was not truly verified on native. The EULA modal itself lays out correctly at mobile width with a readable body and visible Agree button. [meme-calendar](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--meme-calendar.png)
- **Native plugin shims unavailable (simulation boundary).** Every native-mobile route warned `Keyboard plugin shim is unavailable` and `App plugin shim is unavailable`. This is expected in the browser-level Capacitor simulation and is not real device behavior; flag only if these shims are meant to be stubbed in the harness.
- **web-desktop / web-mobile `/meme-lab` and `/rememes` still compiling/fetching.** `web-desktop /meme-lab` shows a "Compiling…" overlay and "Fetching" state, and `web-desktop /rememes` shows "Fetching" with an empty grid. This is dev-server compile/data timing, not a layout defect — `contentReady=true` and no Next.js error overlay. [web-desktop meme-lab](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/web-desktop--meme-lab.png)
- **Screenshot timeouts on heavy routes.** `native-mobile /network` produced no screenshot (full-page + viewport fallback both timed out at 20s/10s), and `/the-memes`, `/rememes` (native) plus `electron-desktop /the-memes` fell back to viewport captures. These are long, media-heavy pages; treat as capture-timing noise, not app failures.
- **Console errors.** `web-mobile /rememes` (4), `electron-desktop /rememes` (2), and each native-mobile route (2) logged console errors. None produced a Next.js overlay or blank render; worth a quick look but non-blocking.

**Platform Coverage**

- **web-desktop (1440x900):** Clean. WebLayout + sidebar render correctly on all 8 routes (`layoutRootDataMobile=false`), no failures/warnings. Network table, meme calendar, waves, discover, and home all look correct.
- **web-mobile:** SmallScreenLayout renders correctly (single-column cards, hamburger nav, no desktop sidebar) across all routes. Only a `/rememes` console-error warning.
- **electron-desktop:** `electronDetected=true` on all routes — the Electron user-agent branch activated correctly. Renders identically to web-desktop as expected. Note this is a Chromium renderer simulation, not a packaged desktop launch.
- **native-mobile:** Capacitor simulation. Bottom nav and `body.capacitor-native` activated on 6 of 7 routes but **failed on `/meme-lab`** (the one blocking issue). Most routes are gated behind the EULA modal, and plugin shims are stubbed out — both are simulation boundaries, not real iOS/Android behavior.

**Bottom line:** Fix/investigate the intermittent `body.capacitor-native` activation on native

Screenshot evidence: [native-mobile /meme-lab](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--meme-lab.png), [native-mobile /rememes](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--rememes.png), [native-mobile /the-memes](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--the-memes.png), [native-mobile /](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--root.png), [native-mobile /waves](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--waves.png), [native-mobile /discover](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--discover.png), [native-mobile /meme-calendar](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--meme-calendar.png), [electron-desktop /the-memes](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/electron-desktop--the-memes.png), [web-mobile /rememes](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/web-mobile--rememes.png), [electron-desktop /rememes](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/electron-desktop--rememes.png), [web-desktop /the-memes](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/web-desktop--the-memes.png), [electron-desktop /meme-lab](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/electron-desktop--meme-lab.png) and 19 more in the details.

<details>
<summary>Deterministic responsiveness details</summary>

**Verdict**: Needs changes

- Workflow: [responsiveness-review #28769676800](https://github.com/6529-Collections/6529reviewbot/actions/runs/28769676800)
- Screenshot viewer: [artifact index](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/artifact-upload.json)
- Screenshots: [responsiveness artifact](https://github.com/6529-Collections/6529reviewbot/actions/runs/28769676800/artifacts/8100463728)
- Contexts: `web-desktop`, `web-mobile`, `native-mobile`, `electron-desktop`
- Routes: `/`, `/waves`, `/network`, `/the-memes`, `/meme-lab`, `/rememes`, `/meme-calendar`, `/discover`
- Profile: 6529 Seize frontend
- Checks completed: 32/32
- Failures: 2
- Warnings: 11

<details>
<summary>Responsiveness runner summary</summary>

# 6529bot Responsiveness Summary

Status: fail
Duration: 438.2s
Profile: 6529 Seize frontend
Contexts: `web-desktop`, `web-mobile`, `native-mobile`, `electron-desktop`
Routes: `/`, `/waves`, `/network`, `/the-memes`, `/meme-lab`, `/rememes`, `/meme-calendar`, `/discover`
Checks completed: 32/32
Failures: 2
Warnings: 11

## Run Errors

- <route>: Error: 6529 native profile: body.capacitor-native was not applied

## Platform Context

- web-desktop should exercise WebLayout/sidebar at 1440x900.
- web-mobile should exercise SmallScreenLayout, not the native AppLayout.
- native-mobile/native-ios/native-android are fast browser-level Capacitor simulations of AppLayout, bottom navigation, viewport-fit, keyboard/app/deep-link plugins, and body class.
- electron-desktop is a fast browser-level Electron renderer simulation using the Electron user-agent branch; it is not a packaged desktop app launch.
- Access/restricted pages intentionally bypass the standard app layout shell.

## Platform Matrix

- `electron-desktop`: 8 check(s), failures=0, warnings=2, avg=21.2s
- `native-mobile`: 8 check(s), failures=1, warnings=8, avg=28.4s
- `web-desktop`: 8 check(s), failures=0, warnings=0, avg=27.5s
- `web-mobile`: 8 check(s), failures=0, warnings=1, avg=22.5s

## Failures

- 1 check(s): 6529 native profile: body.capacitor-native was not applied; examples: `native-mobile` `/meme-lab`

## Next.js Overlay Diagnostics

- `web-desktop` `/meme-lab`: Compiling

## Warnings

- `electron-desktop` `/rememes`: 2 console error(s)
- `electron-desktop` `/the-memes`: full-page screenshot failed; used viewport fallback: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m 
- `native-mobile` `/discover`: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
- `native-mobile` `/meme-calendar`: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
- `native-mobile` `/meme-lab`: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 6529 native profile: bottom navigation was not visible; 2 console error(s)
- `native-mobile` `/network`: screenshot unavailable after content rendered: screenshot failed: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; viewport fallback failed: page.screenshot: Timeout 10000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
- `native-mobile` `/rememes`: full-page screenshot failed; used viewport fallback: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
- `native-mobile` `/`: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
- `native-mobile` `/the-memes`: full-page screenshot failed; used viewport fallback: page.screenshot: Timeout 20000ms exceeded. Call log: �[2m - taking page screenshot�[22m �[2m - waiting for fonts to load...�[22m �[2m - fonts loaded�[22m ; 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
- `native-mobile` `/waves`: 6529 native profile: Keyboard plugin shim is unavailable; 6529 native profile: App plugin shim is unavailable; 2 console error(s)
- `web-mobile` `/rememes`: 4 console error(s)

## Slowest Checks

- `native-mobile` `/rememes`: 43.2s, screenshot [`screenshots/native-mobile--rememes.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--rememes.png)
- `native-mobile` `/network`: 41.3s, screenshot unavailable
- `web-desktop` `/the-memes`: 41.1s, screenshot [`screenshots/web-desktop--the-memes.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/web-desktop--the-memes.png)
- `electron-desktop` `/the-memes`: 36.7s, screenshot [`screenshots/electron-desktop--the-memes.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/electron-desktop--the-memes.png)
- `native-mobile` `/the-memes`: 34.8s, screenshot [`screenshots/native-mobile--the-memes.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/native-mobile--the-memes.png)
- `electron-desktop` `/meme-lab`: 32.4s, screenshot [`screenshots/electron-desktop--meme-lab.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/electron-desktop--meme-lab.png)
- `web-mobile` `/the-memes`: 31.3s, screenshot [`screenshots/web-mobile--the-memes.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/web-mobile--the-memes.png)
- `web-mobile` `/rememes`: 29.9s, screenshot [`screenshots/web-mobile--rememes.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/web-mobile--rememes.png)
- `web-desktop` `/meme-lab`: 28.9s, screenshot [`screenshots/web-desktop--meme-lab.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/web-desktop--meme-lab.png)
- `electron-desktop` `/rememes`: 28.6s, screenshot [`screenshots/electron-desktop--rememes.png`](https://reviewbot.6529.io/artifacts/responsiveness/DxhMOjW14S2szOJz1ACk_RqpJIONCN6e/screenshots/electron-desktop--rememes.png)

</details>

</details>

Rows now carry data-drop-serial-no (HighlightDropWrapper prop from
DropsList) so capture no longer id-prefix-matches descendants; the
container attribute is documented as a column-reverse contract,
removing the inline-style/getComputedStyle sniffing whose reverse
branch never ran in production; the largest-container pick uses an
explicit loop (fixes the Sonar reduce-without-initial-value bug);
preserveWaveScrollPositionForReload never throws so the reload always
proceeds; the JSX attribute references the exported constant. Tests:
malformed-nearest-row ordering, nested containers, at-bottom epsilon,
DOM-throw safety, and pin-runs-after-toast-param-strip ordering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@brookr

brookr commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Round 1 pushed, addressing every thread:

  • 6529bot Important (selector): rows now carry a dedicated data-drop-serial-no (new serialNo prop on HighlightDropWrapper, set by DropsList) and capture selects on that — no more id-prefix matching that could catch descendants. The malformed-nearest/valid-farther ordering case the bot asked for is now a test.
  • 6529bot Important (flexDirection): sniffing removed entirely. The container attribute is now documented as a contract — the element carrying it is the column-reverse feed — so bottom detection is |scrollTop| with no style inspection and no test-only branch.
  • Sonar Reliability C: the reduce() without initial value is gone (explicit loop).
  • Nice-to-haves: pin call can never block the reload (helper is throw-proof + test), tie-break documented, GLM's ordering coverage added (the mock captures location.search at call time and asserts the toast param was already stripped), nested-container preference tested, CodeRabbit's hardcoded-attribute nitpick fixed via computed spread of the exported constant.

Validation: 21/21 unit tests, lint clean, and the full Playwright loop re-run against local dev — scrolled-up reader restored to the same drop (83px from center, param auto-cleaned), at-bottom reader stays in live mode.

@brookr

brookr commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Re the responsiveness verdict: the one blocking finding (native-mobile /meme-lab missing body.capacitor-native) is — per the review's own analysis — an intermittent, route-specific shell-activation race (every other native route activated; the EULA gate also masked most native captures). This PR adds two inert data attributes inside the wave chat and a URL-pinning call in the update toast; nothing here participates in Capacitor shell activation or the /meme-lab route.

Worth noting for the lane's signal quality: the desktop /waves blank flagged on #3123 did not reproduce in this run — consistent with the intermittent anonymous-fetch behavior tracked in #3124. If the capacitor-native activation race recurs on other PRs, it likely deserves the same treatment: its own issue against main rather than attribution to unrelated diffs.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - 219a8d8

Verdict: No new findings

The follow-up commit resolves the substantive concerns from the prior reviews. New findings are absent; one minor non-blocking observation below.

Resolved since last review

  • Selector scoping (prior Important): rows now carry a dedicated data-drop-serial-no (HighlightDropWrapper.tsx:257, DropsList.tsx:309) and capture selects on [data-drop-serial-no] (wave-visible-serial.helpers.ts:88-90), eliminating the id-prefix/descendant match hazard. The malformed-nearest/valid-farther ordering case is now covered (wave-visible-serial.helpers.test.ts:112-121).
  • column-reverse detection (prior Important): style sniffing removed; bottom detection is now Math.abs(container.scrollTop) against a documented container contract (wave-visible-serial.helpers.ts:77-81), removing the test-only branch and hydration-race fallback.
  • Sonar reliability (reduce without initial value): replaced with an explicit loop (wave-visible-serial.helpers.ts:42-53).
  • Unguarded pin call (prior Nice-to-have): preserveWaveScrollPositionForReload is now wrapped in try/catch so the reload always proceeds (wave-visible-serial.helpers.ts:114-125), with a DOM-throw test (wave-visible-serial.helpers.test.ts:187-198). history.state is preserved on replaceState.
  • Ordering test (GLM): the toast test now asserts the pin runs against the cleaned URL (NewVersionToast.test.tsx:292-304).
  • Hardcoded attribute (CodeRabbit): JSX now spreads the exported constant (WaveDropsReverseContainer.tsx:76).

Nice-to-have

  • wave-visible-serial.helpers.ts:56-63parseRowSerial runs /^\d+$/ and then Number.parseInt; Number.isFinite on an all-digit string is always true, so the finite check is dead. Harmless, but could be simplified to just the regex + parseInt.

Coverage note: assessment is limited to the included diff/context and is not exhaustive.

Companion to the scroll-position fix: a forced reload (new-version
toast) also wiped whatever the user was typing. The primary wave chat
composer now autosaves its serialized Lexical editor state to
sessionStorage (debounced, per wave), restores it at editor-creation
time on the next mount, and clears it once the editor is emptied or the
drop is submitted.

- Restore via initialConfig.editorState (Lexical reads it once), guarded
  to the first mount so the post-submit remount can't re-restore.
- Scoped to the primary composer (not reply/quote/edit) so a
  keyed-by-wave draft can't bleed across composers.
- Media-safe: drafts containing an image node are skipped (blob URLs
  die on reload); uploaded attachments live outside the editor state
  and are intentionally not persisted.
- sessionStorage (per-tab, cleared on close) is the right lifetime for a
  transient draft.

Verified live: type -> reload -> exact text restored; empty editor and
submit both drop the key. Helper is unit-tested (empty/whitespace/
image/malformed/oversized/round-trip/per-wave isolation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@brookr brookr changed the title Keep wave readers' place across new-version reloads Preserve wave context (scroll position + chat draft) across new-version reloads Jul 6, 2026
@brookr

brookr commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Extended: also persists the in-progress chat message

Per follow-up: a forced reload was still wiping whatever the user had typed. Added draft persistence alongside the scroll-position restore, so the update reload preserves both where you were reading and what you were writing.

How (commit 9173b67):

  • The primary wave chat composer autosaves its serialized Lexical editor state to sessionStorage (debounced 400ms, keyed per wave), restores it at editor-creation time via initialConfig.editorState on the next mount, and clears it once the editor is emptied or the drop is submitted.
  • Restore is guarded to the first mount (dropEditorRefreshKey === 0) so the post-submit remount can't re-restore a cleared draft.
  • Scoped to the primary composer (not reply/quote/edit) so a keyed-by-wave draft can't bleed across composers.
  • Media-safe: drafts with an image node are skipped (blob URLs die on reload); uploaded attachments live outside the editor state and aren't persisted.
  • sessionStorage (per-tab, cleared on tab close) is the right lifetime for a transient draft.

Verified live: typed a message → reloaded → exact text restored; emptying the editor and submitting both drop the key. New helper unit-tested (empty/whitespace/image/malformed/oversized/round-trip/per-wave isolation) — 12 cases. Full composer suite green (the one unrelated CreateWaveDatesApproveEnd failure is pre-existing TZ flakiness — passes under UTC/CI).

Scope note: this is FE-only, no deploy ordering; the benefit (like the scroll fix) starts with the release after the one that ships it, since the running bundle owns the reload.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - 9173b67

Verdict: Needs changes

The prior follow-up review covered the wave-visible-serial changes. This commit set adds a new, previously-unreviewed feature: draft persistence (wave-draft.helpers.ts, CreateDropContent.tsx, CreateDropInput.tsx). New findings below focus on that surface.

Important

  • components/waves/CreateDropContent.tsx:578-582 — The restore-vs-clear lifecycle has a stale-draft leak. initialDraftJson is read once at mount, but the autosave effect (583-605) reads the current draftWaveId. If a session begins editing while activeDrop/editingDropId is set (draftWaveId === null, no restore), then transitions to the primary composer state (draftWaveId becomes wave.id) without a remount, the debounced effect will begin writing — but initialDraftJson captured null, so nothing restores. Conversely, if draftWaveId flips to null mid-session (entering reply/quote), the cleanup runs but no new write clears an already-persisted draft, so a prior draft can linger in sessionStorage and be restored on the next primary-composer mount even though the user has since moved on. Confirm the composer actually remounts on these transitions (the comment claims "Wave view remounts per wave" but says nothing about the reply/quote/edit transitions within the same wave). If it does not remount, the draft can bleed across composer modes — the exact risk the draftWaveId guard was meant to prevent.

  • components/waves/CreateDropInput.tsx:176editorState: initialEditorStateJson ?? editorState passes a JSON string into Lexical's initialConfig.editorState. Lexical accepts a stringified editor state, but the parse throws for malformed/incompatible-schema JSON, and onError here (178-180) re-throws, which will crash the editor mount (and the surrounding React tree) rather than degrading gracefully. isRestorableDraftJson validates shape loosely (root + text + no image) but does not guarantee the serialized state is parseable by this editor's node schema — a draft written by an older/newer app version with a since-removed node type would pass the restorability check yet throw on parse. Guard the restore path: wrap the initial-state parse or fall back to an empty editor on parse failure so a bad draft cannot brick the composer.

Nice-to-have

  • helpers/waves/wave-draft.helpers.ts:78-86walkNodes collects text from every node with a string text field but only flags type === "image" for media. Other non-text embed nodes (e.g. a future video/attachment node type) would serialize with no text and no image type, so a draft consisting solely of such a node yields text.trim().length === 0 and is correctly rejected — good. But a draft mixing real text and a non-image media node (e.g. a link-preview/embed node referencing a transient URL) would pass and restore a dangling reference. If any such node types exist today, consider an allowlist of restorable node types rather than an image-only denylist.

  • helpers/waves/wave-visible-serial.helpers.ts:56-63 — (repeat-adjacent, still open) Number.isFinite after /^\d+$/ is dead code — an all-digit string always parses finite. Prior review flagged this; harmless but unaddressed.

Suggested next steps

  • Verify composer remount behavior across reply/quote/edit transitions within a wave; if no remount occurs, tie initialDraftJson restoration and draft clearing to the current draftWaveId rather than mount-time capture.
  • Wrap the Lexical initial-state parse so a malformed/incompatible draft falls back to an empty editor instead of throwing through onError.

Coverage note: assessment is limited to the included diff/context and is not exhaustive.

Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

Review comments:
From 6529bot follow-up commit review on 6529-Collections/6529seize-frontend#3129 (9173b67db047):
**Verdict**: Needs changes

The prior follow-up review covered the wave-visible-serial changes. This commit set adds a new, previously-unreviewed feature: draft persistence (`wave-draft.helpers.ts`, `CreateDropContent.tsx`, `CreateDropInput.tsx`). New findings below focus on that surface.

### Important

- `components/waves/CreateDropContent.tsx:578-582` — The restore-vs-clear lifecycle has a stale-draft leak. `initialDraftJson` is read once at mount, but the autosave effect (`583-605`) reads the *current* `draftWaveId`. If a session begins editing while `activeDrop`/`editingDropId` is set (`draftWaveId === null`, no restore), then transitions to the primary composer state (`draftWaveId` becomes `wave.id`) without a remount, the debounced effect will begin writing — but `initialDraftJson` captured `null`, so nothing restores. Conversely, if `draftWaveId` flips to `null` mid-session (entering reply/quote), the cleanup runs but no *new* write clears an already-persisted draft, so a prior draft can linger in sessionStorage and be restored on the next primary-composer mount even though the user has since moved on. Confirm the composer actually remounts on these transitions (the comment claims "Wave view remounts per wave" but says nothing about the reply/quote/edit transitions within the same wave). If it does not remount, the draft can bleed across composer modes — the exact risk the `draftWaveId` guard was meant to prevent.

- `components/waves/CreateDropInput.tsx:176` — `editorState: initialEditorStateJson ?? editorState` passes a JSON *string* into Lexical's `initialConfig.editorState`. Lexical accepts a stringified editor state, but the parse throws for malformed/incompatible-schema JSON, and `onError` here (`178-180`) **re-throws**, which will crash the editor mount (and the surrounding React tree) rather than degrading gracefully. `isRestorableDraftJson` validates shape loosely (root + text + no image) but does not guarantee the serialized state is parseable by *this* editor's node schema — a draft written by an older/newer app version with a since-removed node type would pass the restorability check yet throw on parse. Guard the restore path: wrap the initial-state parse or fall back to an empty editor on parse failure so a bad draft cannot brick the composer.

### Nice-to-have

- `helpers/waves/wave-draft.helpers.ts:78-86` — `walkNodes` collects `text` from every node with a string `text` field but only flags `type === "image"` for media. Other non-text embed nodes (e.g. a future video/attachment node type) would serialize with no `text` and no `image` type, so a draft consisting solely of such a node yields `text.trim().length === 0` and is correctly rejected — good. But a draft mixing real text *and* a non-image media node (e.g. a link-preview/embed node referencing a transient URL) would pass and restore a dangling reference. If any such node types exist today, consider an allowlist of restorable node types rather than an image-only denylist.

- `helpers/waves/wave-visible-serial.helpers.ts:56-63` — (repeat-adjacent, still open) `Number.isFinite` after `/^\d+$/` is dead code — an all-digit string always parses finite. Prior review flagged this; harmless but unaddressed.

**Suggested next steps**
- Verify composer remount behavior across reply/quote/edit transitions within a wave; if no remount occurs, tie `initialDraftJson` restoration and draft clearing to the current `draftWaveId` rather than mount-time capture.
- Wrap the Lexical initial-state parse so a malformed/incompatible draft falls back to an empty editor instead of throwing through `onError`.

*Coverage note: assessment is limited to the included diff/context and is not exhaustive.*

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

- Parse the restored draft inside a try/catch at editor creation
  (function-form initialConfig.editorState). onError re-throws, so an
  older-version or schema-incompatible draft would otherwise crash the
  composer mount during the very update-reload this feature serves; it
  now degrades to an empty editor. Verified live with a poisoned draft:
  composer mounts empty, zero page errors.
- Document the draft lifecycle: autosave derives draftWaveId live so a
  reply/quote/edit on the same stream-composer instance never writes
  under the primary wave key (no cross-mode bleed); restore is
  mount-only. Confirmed the stream composer is one instance whose
  activeDrop flips (MyStreamWaveChat), which is why the live guard, not
  a frozen value, is correct.
- Drop the dead Number.isFinite check after the digits-only regex
  (repeat reviewer nit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@brookr

brookr commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the draft-persistence review (63096a5):

  • Important — parse crash resilience (the serious one): the restored draft is now parsed inside a try/catch at editor creation (function-form initialConfig.editorState). Since onError re-throws, an older-version or schema-incompatible draft would previously have crashed the composer mount — during the exact update-reload this feature exists for. It now degrades to an empty editor. Verified live: injected a shape-valid but schema-incompatible draft (unknown node type), reloaded → composer mounts empty, zero page errors, no Page of Doom.
  • Important — lifecycle/bleed: confirmed the stream composer is a single instance whose activeDrop flips to reply/quote (MyStreamWaveChat), so draftWaveId is derived live — the autosave effect early-returns whenever it's null, which is precisely what prevents reply/quote/edit content from ever being written under the primary wave key. No cross-mode bleed; restoration is mount-only by construction. Rewrote the comment to state this invariant explicitly. (A primary draft that survives a reply excursion and restores later is the user's own unsent text in the same wave — intended, and no worse than today where it's lost immediately.)
  • Nice-to-have: media denylist — the parse guard now also backstops any future transient-URL node schema (unparseable → empty); image remains explicitly rejected. Dropped the dead Number.isFinite check after the digits-only regex (repeat nit).

23/23 helper tests, lint, typecheck clean.

@6529bot

6529bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - 63096a5

Verdict: No new findings

The final commit (63096a5) resolves both Important findings from the prior follow-up review, and the changes are sound.

Resolved since last review

  • Parse-crash resilience (prior Important, CreateDropInput.tsx:180-191): the restored draft is now parsed inside a try/catch via the function-form initialConfig.editorState. A malformed or schema-incompatible draft now degrades to an empty editor instead of throwing through the re-throwing onError. This is the correct fix — parseEditorState/setEditorState throw synchronously here and the catch contains it without touching the general editor onError contract.
  • Lifecycle/cross-mode bleed (prior Important, CreateDropContent.tsx:573-609): confirmed the guard is correct. draftWaveId is derived live from activeDrop === null && !editingDropId, so the autosave effect early-returns whenever the composer is in reply/quote/edit mode — reply/quote/edit content is never written under the primary wave key. The rewritten comment states this invariant explicitly. The prior concern about a lingering primary draft surviving a reply excursion is, as the author noted, the user's own unsent text in the same wave (intended, no worse than today's immediate loss).
  • Dead Number.isFinite (repeat nit, wave-visible-serial.helpers.ts:56-60): removed; parseRowSerial is now /^\d+$/.test(raw) ? Number.parseInt(...) : null.

Nice-to-have

  • components/waves/CreateDropContent.tsx:587-609 — The autosave effect serializes editorState.toJSON() but does not re-check isRestorableDraftJson before deferring the empty/clear decision; it relies on writeWaveDraft to clear when non-restorable. That's correct today (writeWaveDraft clears on non-restorable), just noting the write path's clear-on-empty semantics are load-bearing and only covered indirectly. No change required.

Coverage note: assessment is limited to the included diff/context and is not exhaustive.

Third piece of the reload-context work: after the new-version toast
reload, the sidebar left the active subwave's parent collapsed for a
beat (or longer on slower/larger accounts) because a cold load must
await the active-wave -> parent_wave -> subwaves fetch waterfall before
it can expand; live SPA nav is instant because that data is cached.

The active subwave's parent is now persisted (keyed to the active wave
id) and used as an immediate fallback for the sidebar's
activeParentWaveId until the live value resolves — so the tree expands
and highlights the current subwave right away on reload, then the live
value takes over seamlessly. Purely additive: when the live parent is
known it is used unchanged, so no path regresses. Wired into both the
web and mobile sidebar consumers.

Helper logic unit-tested (round-trip, malformed guards, live-vs-hint
precedence, wave-id keying so a stale hint never expands the wrong
parent). Existing sidebar/consumer suites remain green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@brookr

brookr commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Also: expand & highlight the active subwave after reload

Added the subwave piece (fb4300c). After the toast reload the sidebar left the active subwave's parent collapsed — because a cold load must await the active-wave → parent_wave → subwaves fetch waterfall before it can expand, whereas live SPA nav is instant (data cached). Reproduced locally: right after reload the parent shows collapsed, then pops open once the fetches land; on larger/slower accounts that window is longer and jarring.

Fix: persist the active subwave's parent (keyed to the active wave id) and use it as an immediate fallback for the sidebar's activeParentWaveId until the live value resolves — so the tree expands + highlights the current subwave right away, then the live value takes over. Wired into both web and mobile sidebar consumers. Purely additive: when the live parent is known it's used unchanged, so no path regresses.

Verification note (honest): the hint-write and resolve logic are unit-tested (8 cases: round-trip, malformed guards, live-vs-hint precedence, wave-id keying so a stale hint never expands the wrong parent), all existing sidebar/consumer suites stay green (64 tests), and I confirmed live that the hint is written to sessionStorage as expected. I could not cleanly A/B the flicker-elimination end-to-end locally today: my local stack runs on custom ports which trips a ws://localhost CSP block, and the QA session degraded mid-test — both confound the exact post-reload timing. Given the change is strictly additive (can't regress the live-parent path), I'm relying on unit coverage + the mechanism here; the staging E2E (real env, real CSP) is the right place to confirm the timing win after merge.

This is the third context-preservation piece on this PR: scroll position + chat draft + active subwave.

@6529bot

6529bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - fb4300c

Verdict: No new findings

The final commit (fb4300c) adds the active-subwave-parent hint feature — a new, previously-unreviewed surface. It's well-scoped, defensively coded, and the prior draft-persistence findings remain resolved. I reviewed the new code against the storage/lifecycle hazards raised earlier in this PR and found no distinct issues worth blocking.

Notes on the new surface (active-subwave-hint.helpers.ts, useActiveSubwaveParentHint.ts, both sidebar consumers):

  • resolveActiveSubwaveParent correctly gates the hint on hint.waveId === activeWaveId, so a stale hint from a previously-viewed subwave cannot expand the wrong parent — the exact keying hazard flagged for the draft/serial features. Live parent always wins (liveParentWaveId short-circuits first), so no path regresses when data is cached. Tests cover live-wins, hint-fallback, wrong-wave, and top-level cases.
  • readActiveSubwaveParentHint validates both fields are strings before returning, guards JSON.parse in try/catch, and tolerates a throwing/absent sessionStorage — consistent with the hardening applied to wave-draft.helpers.ts.
  • The useActiveSubwaveParentHint write effect only persists when activeWaveId && liveParentWaveId are both known, so it never writes a null/partial hint, and hint is read once at mount (useState(readActiveSubwaveParentHint)) — the value legitimately carried across the reload.

Nice-to-have

  • hooks/useActiveSubwaveParentHint.ts — The hint is written on every (activeWaveId, liveParentWaveId) change but never cleared when the user navigates to a top-level wave (no parent). A stale subwave hint then persists in sessionStorage until the next subwave visit overwrites it. It's harmless in practice — resolveActiveSubwaveParent only applies it when hint.waveId === activeWaveId, so a top-level wave (whose id won't match the stale subwave hint) correctly resolves to null (covered by the "top-level wave" test) — but clearing the hint when activeWaveId is a known top-level wave would keep storage tidy. No change required.

Coverage note: assessment is limited to the included diff/context and is not exhaustive.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
__tests__/helpers/wave-visible-serial.helpers.test.ts (1)

135-147: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding a non-tied nested-container case.

This test only covers a tie between the outer and nested rows (identical rects). Adding a variant where the nested row is strictly closer to the container center than the outer's own row would surface the scoping gap noted in wave-visible-serial.helpers.ts (querySelectorAll matching nested-container rows).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/helpers/wave-visible-serial.helpers.test.ts` around lines 135 -
147, Add a non-tied nested-container test in wave-visible-serial.helpers.test.ts
to cover the case where a row inside the nested container is strictly closer to
the container center than the outer container’s row. Use buildContainer, outer,
nested, and captureVisibleWaveDropSerial to verify the helper still scopes to
the correct container and does not let querySelectorAll pick the
nested-container row.
helpers/waves/wave-visible-serial.helpers.ts (1)

85-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Row scan doesn't scope to the container's own rows when containers can nest.

container.querySelectorAll([data-drop-serial-no]) matches all descendants, including rows belonging to a nested WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE element (if one is ever mounted inside the dominant container). The added test ("prefers the outer container when a smaller one is nested inside it") only exercises a tie (identical rects for both rows), which happens to resolve correctly via document-order tie-breaking — it doesn't cover the case where the nested container's row is strictly closer to center than the outer container's own row, which would incorrectly select a serial from a different chat feed.

♻️ Suggested scoping fix
   for (const row of container.querySelectorAll(
     `[${DROP_SERIAL_ATTRIBUTE}]`
   )) {
+    // Skip rows that belong to a nested scroll container (e.g. an
+    // embedded/side-panel chat) rather than this one.
+    if (row.closest(`[${WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE}]`) !== container) {
+      continue;
+    }
     const serial = parseRowSerial(row);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@helpers/waves/wave-visible-serial.helpers.ts` around lines 85 - 101, The row
selection logic in the visible-serial helper is too broad because
`container.querySelectorAll` inside the scan loop can pick up rows from nested
scroll containers instead of only the current container’s own rows. Update the
scan in the helper that computes the best serial (the loop using
`parseRowSerial`, `getBoundingClientRect`, and `bestSerial`) so it filters out
descendants belonging to a nested `WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE`
container and only considers rows directly owned by the active container. Make
the scope explicit in this selector/iteration path so the closest row is chosen
from the correct chat feed even when containers are nested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/waves/CreateDropContent.tsx`:
- Around line 572-609: Clear the composer state when canceling reply/quote or
edit mode, because only the mode flag is being reset and the existing
editorState can still be autosaved into the primary draft via
CreateDropContent’s draftWaveId effect. Update the cancel handlers
(onCancelReplyQuote and the edit-cancel path) to also reset/clear the editor
state or otherwise prevent stale content from surviving after exit. Make sure
the fix works with CreateDropInput’s mount-only initialEditorStateJson behavior
so reply/edit text is not persisted under the primary wave key.

In `@components/waves/CreateDropInput.tsx`:
- Around line 174-191: The draft restore logic in CreateDropInput’s editorState
initializer currently swallows parseEditorState failures but leaves the bad
stored JSON in place, causing the same broken draft to be retried on every
mount. Update the catch path inside the initialEditorStateJson restore callback
to also clear/remove the persisted draft when parsing fails, alongside the
existing fallback to an empty editor, so the composer stops reloading the
invalid state on subsequent mounts.

---

Nitpick comments:
In `@__tests__/helpers/wave-visible-serial.helpers.test.ts`:
- Around line 135-147: Add a non-tied nested-container test in
wave-visible-serial.helpers.test.ts to cover the case where a row inside the
nested container is strictly closer to the container center than the outer
container’s row. Use buildContainer, outer, nested, and
captureVisibleWaveDropSerial to verify the helper still scopes to the correct
container and does not let querySelectorAll pick the nested-container row.

In `@helpers/waves/wave-visible-serial.helpers.ts`:
- Around line 85-101: The row selection logic in the visible-serial helper is
too broad because `container.querySelectorAll` inside the scan loop can pick up
rows from nested scroll containers instead of only the current container’s own
rows. Update the scan in the helper that computes the best serial (the loop
using `parseRowSerial`, `getBoundingClientRect`, and `bestSerial`) so it filters
out descendants belonging to a nested `WAVE_DROPS_SCROLL_CONTAINER_ATTRIBUTE`
container and only considers rows directly owned by the active container. Make
the scope explicit in this selector/iteration path so the closest row is chosen
from the correct chat feed even when containers are nested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 826e576f-c31a-4df1-8d0a-72225b97a80f

📥 Commits

Reviewing files that changed from the base of the PR and between 587d867 and fb4300c.

📒 Files selected for processing (15)
  • __tests__/components/utils/NewVersionToast.test.tsx
  • __tests__/helpers/active-subwave-hint.helpers.test.ts
  • __tests__/helpers/wave-draft.helpers.test.ts
  • __tests__/helpers/wave-visible-serial.helpers.test.ts
  • components/brain/left-sidebar/waves/UnifiedWavesListWaves.tsx
  • components/brain/left-sidebar/web/WebUnifiedWavesListWaves.tsx
  • components/drops/view/DropsList.tsx
  • components/drops/view/HighlightDropWrapper.tsx
  • components/waves/CreateDropContent.tsx
  • components/waves/CreateDropInput.tsx
  • components/waves/drops/WaveDropsReverseContainer.tsx
  • helpers/waves/active-subwave-hint.helpers.ts
  • helpers/waves/wave-draft.helpers.ts
  • helpers/waves/wave-visible-serial.helpers.ts
  • hooks/useActiveSubwaveParentHint.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/helpers/wave-draft.helpers.test.ts
  • components/drops/view/DropsList.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/waves/drops/WaveDropsReverseContainer.tsx
  • tests/components/utils/NewVersionToast.test.tsx

Comment thread components/waves/CreateDropContent.tsx Outdated
Comment thread components/waves/CreateDropInput.tsx
Main rewrote CreateDropContent into create-drop-content/ modules, so the
draft-persistence feature is re-ported onto the new structure: autosave
effect and restore seed live in CreateDropContent, the initial JSON
threads through CreateDropLayout, and CreateDropInput parses the restored
draft at editor creation. Also addresses CodeRabbit: a draft that fails
to parse on restore is now removed from storage instead of being retried
on every mount.

Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - a668091

Verdict: No new findings

This is the same head (fb4300c) as the prior follow-up review marker, so the diff since the marker is empty — no new commits to evaluate. The active-subwave-hint feature was already reviewed at this head and found sound; the draft-persistence and scroll-serial findings from earlier commits remain resolved.

One note surfaced by re-reading the full context (not a new finding, and not blocking): the refreshState in the excerpt (CreateDropContent.tsx:533-540) is now a useCallback wrapping baseRefreshState plus clearWaveDraft(draftWaveId), which differs from the inline refreshState shown in the commit-3 diff — the clear-on-submit path is intact either way. No action needed.

The prior nice-to-have (stale top-level hint never cleared, useActiveSubwaveParentHint.ts) also remains open by design and is harmless given the hint.waveId === activeWaveId gate. Nothing to add.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - 57b5afa

Verdict: No new findings

This diff since the prior same-lane marker (a668091) contains the full accumulated commit set already reviewed piecemeal across prior follow-up reviews (219a8d82, 9173b67d, 63096a53, fb4300c4). I re-verified the current head against those and the CreateDropContent refactor (the baseRefreshState/useCallback wrapping now visible at CreateDropContent.tsx:533-540, which was previously inline) introduces no new hazard: clearWaveDraft(draftWaveId) remains gated on draftWaveId and the callback deps are correct.

All previously-raised Important findings remain resolved:

  • Selector scoping via data-drop-serial-no (wave-visible-serial.helpers.ts:24, DropsList.tsx:309, HighlightDropWrapper.tsx:257).
  • Column-reverse contract, no style sniffing (wave-visible-serial.helpers.ts:14-21).
  • Guarded pin call (preserveWaveScrollPositionForReload try/catch).
  • Draft parse-crash resilience via function-form initialConfig.editorState (CreateDropInput.tsx:176-191).
  • Cross-mode bleed prevented by live-derived draftWaveId.
  • Subwave hint keyed to activeWaveId (active-subwave-hint.helpers.ts:88).

The prior nice-to-have (stale subwave hint never cleared on top-level nav, useActiveSubwaveParentHint.ts) remains open but is harmless by construction (resolveActiveSubwaveParent gates on hint.waveId === activeWaveId) and was already acknowledged — not re-raising.

Coverage note: assessment is limited to the included diff/context and is not exhaustive.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - c236ac2

Verdict: No new findings

This is the same head (57b5afa) as the prior follow-up marker, and the diff since the last same-lane marker (587d868) recaps the full PR arc already reviewed commit-by-commit. I re-checked the current state of each surface and the previously-raised findings remain resolved; the only new code relative to 587d868 (draft persistence, subwave hint) was already reviewed in the 9173b67 / 63096a5 / fb4300c follow-ups.

One observation worth noting, not previously raised in this exact form:

Nice-to-have

  • components/waves/CreateDropContent.tsx:533-540refreshState is now a useCallback wrapping baseRefreshState plus the clearWaveDraft(draftWaveId) call, and it is passed into useCreateDropSubmission. This differs from the earlier reviewed inline version (which cleared the draft inside a plain refreshState). The clearWaveDraft fires only when draftWaveId is non-null at call time — correct for a primary-composer submit. Since draftWaveId is null during reply/quote/edit, a submit in those modes won't clear a lingering primary draft, but that draft is the user's own unsent text in the same wave (the intended behavior established in the fb4300c discussion), so no change required. Flagging only because the refreshState shape changed between the reviewed commit and the current context excerpt.

Resolved since last review

  • All Important findings from the prior follow-ups (selector scoping, column-reverse detection, unguarded pin, Lexical parse-crash resilience, cross-mode draft bleed) remain fixed in the current code.
  • The repeat Number.isFinite dead-code nit is resolved in parseRowSerial (wave-visible-serial.helpers.ts:56-57).

Coverage note: assessment is limited to the included diff/context since 587d868 and is not exhaustive.

Main's refactor left CreateDropContent.tsx at 796 lines, four under the
800-line ceiling, so the re-ported draft logic tripped oversized_files.
The persistence now lives in create-drop-content/useWaveDraftPersistence
and is wired inside CreateDropLayout (which already receives activeDrop,
editorState and the refresh key); CreateDropContent matches main exactly.
Submit-time draft clearing now keys off the editor refresh-key bump
instead of wrapping refreshState.

Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - c094f01

Verdict: No new findings

The final commit (c094f01) is a pure refactor: it lifts the draft-persistence logic out of CreateDropContent.tsx (to keep it under the 800-line debt ratchet) into a dedicated useWaveDraftPersistence hook, wired inside CreateDropLayout. The behavior is preserved and the earlier resolved findings still hold.

Resolved / preserved since last review

  • Draft lifecycle & submit-clear (prior Important, now useWaveDraftPersistence.ts:41-76): the draftWaveId live-derivation invariant is intact — activeDrop === null && !editingDropId still gates the autosave effect, so reply/quote/edit content never writes under the primary wave key. The old refreshState wrapper is gone; submit-time clearing now keys off the editor refresh-key bump (isMountEditor false + empty editor → immediate clearWaveDraft), which is a cleaner equivalent of the prior immediate-clear.
  • Parse-crash resilience (prior Important): the CreateDropInput.tsx try/catch initial-state guard is unchanged.
  • Mount-only restore: initialDraftJson is still captured once (useState initializer) and now nulled via isMountEditor ? initialDraftJson : null in the return — equivalent to the prior dropEditorRefreshKey === 0 guard, but robust to a nonzero mount key since it snapshots the mount value in mountRefreshKeyRef.

Nice-to-have

  • components/waves/create-drop-content/useWaveDraftPersistence.ts:42-43mountRefreshKeyRef snapshots the first dropEditorRefreshKey this hook instance sees. Since CreateDropLayout is not itself remounted per submit (only the inner CreateDropInput is, via its key), this correctly treats any later bump as "reset." Just note the restore→null transition is now driven purely by the key advancing past its initial value; if a future refactor ever resets dropEditorRefreshKey back to the mount value mid-session, restore would re-arm. No change required today.

No behavioral regressions introduced by the move; the extracted hook matches the prior inline implementation.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - 06d8353

Verdict: No new findings

The newest commit (c094f01, "Move draft persistence into a hook") is a pure extraction refactor to satisfy the debt ratchet. It moves the draft-persistence logic out of CreateDropContent.tsx into create-drop-content/useWaveDraftPersistence.ts and wires it inside CreateDropLayout. I reviewed the extraction for behavioral equivalence and the one genuinely new mechanism (refresh-key–based clearing) and found no distinct issues worth blocking.

Notes on the refactor:

  • Behavioral equivalence preserved. draftWaveId, initialDraftJson mount-capture, the 400ms debounced autosave, and the media/empty clear semantics all move verbatim into useWaveDraftPersistence (useWaveDraftPersistence.ts:41-76). The prior findings (parse-crash resilience in CreateDropInput.tsx, cross-mode bleed guard via live draftWaveId) remain intact — CreateDropInput still receives initialEditorStateJson and the try/catch parse path is unchanged.

  • New submit-clear mechanism is sound. The old code wrapped refreshState to call clearWaveDraft on submit. The new approach keys off dropEditorRefreshKey moving past its mount value: isMountEditor compares the current key to the mount-captured ref (useWaveDraftPersistence.ts:42-43), and when the editor is reset (!isMountEditor && !editorState) the draft is cleared immediately (52-56), otherwise the debounced write runs. initialDraftJson is also nulled once isMountEditor is false (78), preserving the prior dropEditorRefreshKey === 0 guard against re-restoring a cleared draft. This correctly replaces the wrapped-refreshState clear.

  • mountRefreshKeyRef initialization is robust. Capturing the mount-time key via useRef rather than hardcoding 0 is actually more correct than the prior dropEditorRefreshKey === 0 literal — it tolerates a non-zero starting key.

The remaining lanes' concerns (wave-visible-serial, active-subwave-hint) are untouched by this commit and their prior resolutions stand.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - f522d6d

Verdict: No new findings

This commit set (through f522d6d) is dominated by the PATCH 6 refactor that moves draft persistence out of CreateDropContent.tsx into the useWaveDraftPersistence hook (wired via CreateDropLayout) to satisfy the file-length ratchet. I reviewed the extraction and the new submit-clear mechanism against the prior findings; the behavior is preserved and no new issues surfaced.

Resolved since last review

  • Prior submit-clear via refreshState wrapper: the extracted hook now derives clearing from the dropEditorRefreshKey bump instead of wrapping refreshState (useWaveDraftPersistence.ts:42-57). mountRefreshKeyRef freezes the mount-time key; a later bump with an empty editorState clears the draft immediately, and initialDraftJson is returned only for the mount editor (:78). This is functionally equivalent to the old dropEditorRefreshKey === 0 guard and the old immediate-clear-on-submit, without the extra useCallback.
  • Lifecycle/cross-mode bleed (prior Important, still correct post-extraction): draftWaveId remains derived live from activeDrop === null && !editingDropId (:41), so reply/quote/edit content is never written under the primary wave key. The invariant survives the move to a hook.
  • Parse-crash resilience and image denylist remain intact in CreateDropInput.tsx / wave-draft.helpers.ts (unchanged by this commit set).

Nice-to-have

  • components/waves/create-drop-content/useWaveDraftPersistence.ts:42-43mountRefreshKeyRef assumes the composer mounts at dropEditorRefreshKey === 0 (the old explicit === 0 guard was self-documenting). The ref-freeze is more general (handles a non-zero mount key), but if a parent ever remounts the hook with a non-mount refresh key as the initial value, isMountEditor would be true for that key and could restore a draft the old code wouldn't. In practice the hook mounts with the layout at key 0, so this is theoretical. No change required; worth a one-line comment that the mount key is expected to be the pristine editor.

Coverage note: assessment is limited to the included diff/context and is not exhaustive; the extraction is behavior-preserving as far as the included excerpts show.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - c974fe3

Verdict: No new findings

This commit set (through the prior marker to c974fe35) is dominated by the debt-ratchet refactor (PATCH 6/6, c094f01a) that lifts the draft-persistence logic out of CreateDropContent.tsx into the new useWaveDraftPersistence hook and wires it through CreateDropLayout. I verified the move against the prior review's resolved findings and against the mount/reset semantics; the behavior is preserved.

Resolved / verified since last review

  • Draft-clear-on-submit relocated correctly (useWaveDraftPersistence.ts:42-56): the previous refreshState wrapper (which explicitly clearWaveDraftd on submit) was removed from CreateDropContent. The new hook reproduces that guarantee via mountRefreshKeyRef + isMountEditor: when dropEditorRefreshKey bumps past its mount value and editorState is null, the draft is cleared immediately rather than after the 400ms debounce. This matches the prior "a reload right after submit must not restore it" invariant. Note: the mount value is captured with useRef(dropEditorRefreshKey) rather than assuming 0, which is more robust than the earlier dropEditorRefreshKey === 0 check.
  • Cross-mode bleed guard intact (useWaveDraftPersistence.ts:41): draftWaveId is still derived live from activeDrop === null && !editingDropId; the autosave effect early-returns when null, so reply/quote/edit content is never written under the primary wave key. useEditingDrop is now consumed inside the hook — same source of truth, correctly relocated.
  • Restore gating (CreateDropLayout.tsx:319, useWaveDraftPersistence.ts:78): initialDraftJson is returned as null once past the mount editor, so CreateDropInput only seeds the restored draft on the mount instance — equivalent to the prior dropEditorRefreshKey === 0 ? initialDraftJson : null inline check.
  • Parse-crash resilience (CreateDropInput.tsx:180-191, unchanged this range) and the subwave-hint / scroll-serial surfaces remain as previously reviewed.

One minor observation, not blocking: mountRefreshKeyRef freezes the mount-time key, so isMountEditor correctly stays true across re-renders that don't remount, and correctly flips false after a reset bump. This relies on CreateDropLayout not being remounted with a fresh key while the same draft should still restore — but per the documented WavesView-remounts-per-wave contract that path already remounts the whole subtree, so the ref re-initializes as intended. No issue.

Coverage note: assessment is limited to the included diff/context and is not exhaustive; the omitted 17th file was not reviewable here.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - 49fcd52

Verdict: No new findings

The primary change since the last same-lane marker (c974fe35) is the refactor commit (c094f01a) that moves draft persistence out of CreateDropContent.tsx into the useWaveDraftPersistence hook wired inside CreateDropLayout. I reviewed the extracted hook against the lifecycle invariants established in prior reviews and found the behavior preserved, with one benign observation.

Resolved since last review

  • Debt-ratchet / oversized_files (per commit message): draft logic extracted to components/waves/create-drop-content/useWaveDraftPersistence.ts; CreateDropContent.tsx now matches main. The prior submit-time clear that wrapped refreshState is replaced by refresh-key-based clearing (useWaveDraftPersistence.ts:42-56), preserving the "reload right after submit must not restore" guarantee via isMountEditor.
  • Cross-mode bleed guard (prior Important): the draftWaveId = activeDrop === null && !editingDropId ? waveId : null derivation and early-return are carried into the hook unchanged (useWaveDraftPersistence.ts:41,49), so reply/quote/edit content is still never written under the primary wave key.

Nice-to-have

  • components/waves/create-drop-content/useWaveDraftPersistence.ts:42-43mountRefreshKeyRef captures the first dropEditorRefreshKey this hook instance ever sees. Because the hook now lives in CreateDropLayout (which is not itself keyed by dropEditorRefreshKey, unlike the inner CreateDropInput), isMountEditor is true only while the key equals its initial value and flips permanently to false after the first bump. That matches the intended "restore only on the true first mount" semantics, but it also means initialDraftJson is returned as null for the rest of the layout's lifetime even across a subsequent genuine wave remount that does not re-mount CreateDropLayout. If WavesView's per-wave key remounts CreateDropLayout (as the comment asserts), this is correct; if only a deeper subtree remounts, a same-tab wave switch after an editor bump would silently skip restore. Non-blocking — consistent with the documented "wave view remounts per wave" contract, just now one component-boundary further from where that key lives. Worth a quick confirmation that CreateDropLayout sits inside the WavesView remount boundary.

Coverage note: assessment is limited to the included diff/context and is not exhaustive; the extraction is behavior-preserving on the evidence here.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - 9f3eb3b

Verdict: No new findings

This commit set (through the prior same-lane marker at 49fcd52) reorganizes the draft-persistence logic into create-drop-content/useWaveDraftPersistence.ts to satisfy the file-size debt ratchet, keeping CreateDropContent aligned with main. The extraction preserves the previously-reviewed invariants and I found no new issues in the included diff.

Resolved / verified since last review

  • Draft-clear-on-submit refactor (useWaveDraftPersistence.ts:42-56): the prior refreshState wrapper is gone; submit-time clearing now keys off dropEditorRefreshKey moving past its mount value. mountRefreshKeyRef captures the mount key via useRef, and the !isMountEditor && !editorState branch drops the persisted draft immediately on reset — functionally equivalent to the old immediate clearWaveDraft in refreshState, and it no longer waits for the debounce.
  • Mount-only restore guard (useWaveDraftPersistence.ts:78): initialDraftJson is nulled once isMountEditor is false, so the post-submit remount can't re-restore a cleared draft — the same guard the prior dropEditorRefreshKey === 0 ? initialDraftJson : null gate provided, now generalized to the captured mount key rather than a hardcoded 0. Correct.
  • Cross-mode bleed invariant (useWaveDraftPersistence.ts:41): draftWaveId is still derived live from activeDrop === null && !editingDropId, preserving the reply/quote/edit early-return. Moving the hook into CreateDropLayout doesn't change this — activeDrop and editingDropId (via useEditingDrop) are read live there too.

Nice-to-have

  • components/waves/CreateDropInput.tsx:16clearWaveDraft is now imported into CreateDropInput but the visible excerpt (through line 251) doesn't show its usage; the draft-clear responsibility otherwise lives in useWaveDraftPersistence. If this import is unused after the refactor it will trip lint; if it is used further down (e.g. an on-empty clear inside the editor), it's a second clear path worth keeping consistent with the hook's ownership. Worth a quick confirmation. No change required if the usage exists and is intentional.

Coverage note: assessment is limited to the included diff/context and is not exhaustive; the extraction is behavior-preserving relative to the previously-reviewed logic.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - ab6f95e

Verdict: No new findings

This commit set is the debt-ratchet refactor (patch 6, c094f01a) that moved draft persistence out of CreateDropContent.tsx into create-drop-content/useWaveDraftPersistence.ts, wired inside CreateDropLayout. The behavior-equivalence claim holds up under review, and the prior Important findings (parse-crash resilience, cross-mode bleed) remain resolved in the new location.

Resolved / carried-forward correctly

  • Cross-mode bleed guarddraftWaveId = activeDrop === null && !editingDropId ? waveId : null (useWaveDraftPersistence.ts:41) preserves the live-derived guard; the autosave effect still early-returns on null, so reply/quote/edit content is never written under the primary wave key.
  • Submit-time clear — the old refreshState wrapper is replaced by keying off dropEditorRefreshKey. isMountEditor (:43) is false after a bump, and the new !isMountEditor && !editorState branch (:52-56) clears the draft immediately, matching the prior intent (a reload right after submit must not restore). initialDraftJson is nulled when !isMountEditor (:78), replacing the previous dropEditorRefreshKey === 0 ? … : null gate.
  • Parse-crash resilience / restoreinitialDraftJson still flows into CreateDropInput's try/catch initialConfig.editorState unchanged (CreateDropLayout.tsx:319).

Nice-to-have

  • useWaveDraftPersistence.ts:42-43mountRefreshKeyRef captures the first dropEditorRefreshKey this hook instance sees and treats that as the mount value. This is correct only if the layout hook mounts with the same refresh key the composer started at (here 0). Since CreateDropLayout and the composer share the dropEditorRefreshKey lifecycle from CreateDropContent, this holds — but the invariant is now implicit across two files rather than the single === 0 literal it replaced. A brief assertion or comment noting the expected mount value would make the coupling less fragile to future refactors. No behavior change.

Coverage note: assessment is limited to the included diff/context and is not exhaustive. The moved logic has no direct unit test at the hook level (the helper-level tests in wave-draft.helpers.test.ts still pass through the same storage contract).

The sidebar already remembered which subwave menus were expanded
(sessionStorage), but after a full reload only the active wave's parent
had its subwaves re-fetched, so manually expanded menus stayed
collapsed. A new useLoadPersistedExpandedSubwaves hook eagerly re-fetches
subwaves for every remembered parent in both sidebar variants.

The new reload e2e also exposed a real gap: a draft restored at editor
creation never fires OnChangePlugin, leaving the parent convinced the
composer is empty and the Post button disabled until the user types.
NotifyInitialEditorStatePlugin pushes the seeded state up once on mount.

New coverage: sandbox e2e for type -> reload -> restored-and-sendable;
unit tests for useWaveDraftPersistence (debounced save, reply/edit
gating, submit-time clear, serialization-failure clear), the failed
draft-restore cleanup in CreateDropInput, and the persisted-expansion
eager load.

Signed-off-by: brookr <11095+brookr@users.noreply.github.com>
@brookr

brookr commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed b79ad6c expanding this PR's scope per Brook:

Subwave menu expansion now survives the new-version reload. The sidebar already persisted which menus were expanded, but after a reload only the active wave's parent had its subwaves re-fetched — manually expanded menus stayed collapsed. useLoadPersistedExpandedSubwaves now eagerly re-fetches subwaves for every remembered parent (both sidebar variants).

Real bug found by the new reload e2e: a restored draft never fired OnChangePlugin, so the Post button stayed disabled until the user typed. NotifyInitialEditorStatePlugin pushes the seeded state to the parent once on mount — restored drafts are immediately sendable.

New coverage: sandbox e2e (type → reload → restored and sendable, mutation-guarded), unit tests for useWaveDraftPersistence (debounced save, reply/edit gating, submit-time clear, serialization-failure clear), the failed-restore storage cleanup, and the persisted-expansion eager load — 14 new unit tests, all sidebar/composer suites green (42 tests).

Note: direct-message-sandbox.spec.ts currently fails on this branch and without these changes (pre-existing; likely from a recent main merge) — tracked separately.

@6529bot

6529bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

6529bot follow-up commit review - b79ad6c

Verdict: No new findings

The commit set since the prior same-lane marker (ab6f95e4) reflects the debt-ratchet refactor (draft persistence moved into useWaveDraftPersistence), the NotifyInitialEditorStatePlugin fix, and the useLoadPersistedExpandedSubwaves expansion-restore addition. I reviewed each against the hazards raised earlier in this PR and found no distinct blocking issues.

Resolved / verified since last review

  • Post-move lifecycle equivalence (useWaveDraftPersistence.ts): the extracted hook preserves the live-derived draftWaveId guard (activeDrop === null && !editingDropId) and the mount-only restore, matching the previously-approved inline logic. The refresh-key-based submit-clear (isMountEditor gate) is a clean replacement for the old refreshState wrapper and is well-covered (debounce save, reply/edit gating, submit-time clear, serialization-failure clear).
  • Submit-disabled-until-typed gap (CreateDropInput.tsx:150-167,436-440): NotifyInitialEditorStatePlugin correctly pushes the seeded state up exactly once, gated on typeof initialEditorStateJson === "string", so a restored draft is immediately sendable without regressing the empty-composer path. The notifiedRef guard prevents repeat notifications.
  • Draft parse-crash resilience confirmed still intact via the new CreateDropInput.draftRestore.test.tsx (malformed JSON → empty editor + clearWaveDraft).

Nice-to-have

  • hooks/useLoadPersistedExpandedSubwaves.ts:40-52requestedParentIdsRef is added to before the hasLoadedSubwaves check, so a parent that appears with subwaves-not-yet-loaded but is (correctly) skipped this pass is still marked "requested." If that parent's subwaves never arrive through another path, it will never be retried. In practice the active-parent and toggle paths cover the reload window, so this is benign — but the marking is slightly eager relative to the actual fetch. Consider only adding to the requested set when loadSubwaves is actually called. No change required.

Coverage note: assessment is limited to the included diff/context and is not exhaustive; 8 relevant files were omitted after context limits.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
__tests__/components/waves/CreateDropInput.draftRestore.test.tsx (1)

20-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify plugin mocks by removing forwardRef.

In React 19, ref is treated as a regular prop, so forwardRef is no longer required for components to accept it. You can safely remove the jest.requireActual call and simplify the mock to return a standard functional component.

♻️ Proposed refactor
 function createNoopPluginMock() {
-  const { forwardRef } = jest.requireActual<typeof React>("react");
   return {
     __esModule: true,
-    default: forwardRef(() => null),
+    default: () => null,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/components/waves/CreateDropInput.draftRestore.test.tsx` around
lines 20 - 26, Update createNoopPluginMock to remove the jest.requireActual
React import and forwardRef usage. Return the default plugin mock as a standard
functional component while preserving the existing no-op behavior and module
shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@__tests__/components/waves/CreateDropInput.draftRestore.test.tsx`:
- Around line 20-26: Update createNoopPluginMock to remove the
jest.requireActual React import and forwardRef usage. Return the default plugin
mock as a standard functional component while preserving the existing no-op
behavior and module shape.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12898ff2-7e8e-4065-acd8-ab4f9c430368

📥 Commits

Reviewing files that changed from the base of the PR and between fb4300c and b79ad6c.

📒 Files selected for processing (3)
  • __tests__/components/waves/CreateDropInput.draftRestore.test.tsx
  • __tests__/components/waves/create-drop-content/useWaveDraftPersistence.test.tsx
  • __tests__/hooks/useLoadPersistedExpandedSubwaves.test.tsx

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.

1 participant