fix(publish): deliver overflowing waves and deck threads to the composer#1237
fix(publish): deliver overflowing waves and deck threads to the composer#1237feruzm wants to merge 1 commit into
Conversation
A wave or deck thread past its character limit wrote its content into the
submit page's local draft key and opened /publish. Nothing on /publish reads
that key, so the content was silently dropped.
The write was also partial ({ ...localDraft, body } over a localDraft that
defaults to {}), so it left that key holding a draft with neither title nor
tags. The submit page fed those straight into applyTitle, crashing every
later visit to /submit (ECENCY-NEXT-1GJC).
Both surfaces now stage content on a dedicated one-shot key that the publish
composer consumes on mount, so the handoff arrives and the submit draft is
left to the submit page.
📝 WalkthroughWalkthroughOversized deck-thread and wave content now uses a one-shot local-storage handoff to populate the publish composer. The publish page consumes the handoff into both editor states, replacing the prior local-draft flow. ChangesPublish handoff
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@apps/web/src/app/publish/_hooks/use-publish-handoff.ts`:
- Around line 39-49: Update the useMount logic in use-publish-handoff.ts to
remove the staged handoff immediately when processing begins, before checking
whether handoff?.body is deliverable; add a ref-based guard so the same staged
entry is consumed only once across later mounts, while preserving onReceive for
non-empty bodies. In apps/web/src/specs/app/publish/publish-handoff.spec.ts
lines 64-70, add coverage asserting entries with an empty body are removed and
not delivered.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b7d220d9-f0a1-457b-9326-ac15ceaea2e1
📒 Files selected for processing (6)
apps/web/src/app/decks/_components/deck-threads-form/index.tsxapps/web/src/app/publish/_hooks/index.tsapps/web/src/app/publish/_hooks/use-publish-handoff.tsapps/web/src/app/publish/_page.tsxapps/web/src/features/waves/hooks/use-wave-submit.tsapps/web/src/specs/app/publish/publish-handoff.spec.ts
| const [handoff, , removeHandoff] = useLocalStorage<PublishHandoff>(PUBLISH_HANDOFF_KEY); | ||
|
|
||
| useMount(() => { | ||
| const body = handoff?.body; | ||
|
|
||
| if (!body) { | ||
| return; | ||
| } | ||
|
|
||
| removeHandoff(); | ||
| onReceive(body); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether Strict Mode is enabled and locate existing Strict Mode test patterns.
rg -n -C2 'reactStrictMode|<StrictMode>|React\.StrictMode' apps/web next.config.* 2>/dev/null || true
# Inspect the handoff hook and its focused tests.
sed -n '1,90p' apps/web/src/app/publish/_hooks/use-publish-handoff.ts
sed -n '55,90p' apps/web/src/specs/app/publish/publish-handoff.spec.tsRepository: ecency/vision-next
Length of output: 2745
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect relevant package/test config for reactStrictMode and StrictModes usage in tests.
rg -n -C2 'reactStrictMode|<StrictMode>|React\.StrictMode|strict mode|strictMode' apps/web next.config.* 2>/dev/null || true
# Look at surrounding test setup for act/imports.
sed -n '1,100p' apps/web/src/specs/app/publish/publish-handoff.spec.ts
# Deterministic control-flow probe against the hook's body logic, modeled as functions:
python3 - <<'PY'
def hook_body(handoff, remove_call_count, consumed, strict_replay=1):
calls = 0
for i in range(strict_replay):
body = handoff.get("body") if handoff else None
if not body:
continue
# current hook only calls remove after if body, so removal happens in same effect body
remove_call_count = True
calls += 1
return calls, remove_call_count, "entry" if handoff and not remove_call_count else None
for body in ["", "wave text"]:
calls, removed, left = hook_body({"body": body}, False, False, strict_replay=1)
print(f"body={body!r}, strict=1 -> onReceive_calls={calls}, removed_after_mount={removed}, left_in_storage={left}")
calls, removed, left = hook_body({"body": ""}, False, False, strict_replay=2)
print(f"body='' strict=2 -> onReceive_calls={calls}, removed_after_mount={removed}, left_in_storage={left}")
PYRepository: ecency/vision-next
Length of output: 3465
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- relevant strict mode searches ---\n'
rg -n -C3 'reactStrictMode|<StrictMode>|React\.StrictMode' next.config.* apps/web 2>/dev/null || true
printf '\n--- package/workflow strict-mode mentions ---\n'
printf 'scripts in apps/web/package.json:\n'
python3 - <<'PY'
import json
from pathlib import Path
p=Path('apps/web/package.json')
if p.exists():
data=json.loads(p.read_text())
print(data.get('scripts',{}))
PY
printf '\n--- workflow test command context ---\n'
for f in $(git ls-files | rg '^(\.github/workflows/|package.json)$'); do
echo "--- $f ---"
rg -n -C2 'reactStrictMode|<StrictMode>|strict mode|CI\.NEXT|NEXT|test|lint' "$f" 2>/dev/null || true
doneRepository: ecency/vision-next
Length of output: 247
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- relevant strict mode searches ---'
rg -n -C3 'reactStrictMode|<StrictMode>|React\.StrictMode' next.config.* apps/web 2>/dev/null || true
printf '%s\n' ''
printf '%s\n' '--- package/workflow strict-mode mentions ---'
printf '%s\n' 'scripts in apps/web/package.json:'
python3 - <<'PY'
import json
from pathlib import Path
p=Path('apps/web/package.json')
if p.exists():
data=json.loads(p.read_text())
print(data.get('scripts',{}))
PY
printf '%s\n' ''
printf '%s\n' '--- workflow test command context ---'
while IFS= read -r f; do
echo "--- $f ---"
rg -n -C2 'reactStrictMode|<StrictMode>|strict mode|CI\.NEXT|NEXT|test|lint' "$f" 2>/dev/null || true
done < <(git ls-files | rg '(^\.github/workflows/|^package.json)$') || trueRepository: ecency/vision-next
Length of output: 669
Enforce one-shot handoff consumption and cover the empty-body retention case. The current hook does not remove entries with body: "", so they remain in local storage for later visits.
apps/web/src/app/publish/_hooks/use-publish-handoff.ts: remove the staged entry before deciding whether its body is deliverable, and use a ref-based consumption guard if later mounts must not consume the same staged handoff.apps/web/src/specs/app/publish/publish-handoff.spec.ts: assert empty entries are removed.
📍 Affects 2 files
apps/web/src/app/publish/_hooks/use-publish-handoff.ts#L39-L49(this comment)apps/web/src/specs/app/publish/publish-handoff.spec.ts#L64-L70
🤖 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 `@apps/web/src/app/publish/_hooks/use-publish-handoff.ts` around lines 39 - 49,
Update the useMount logic in use-publish-handoff.ts to remove the staged handoff
immediately when processing begins, before checking whether handoff?.body is
deliverable; add a ref-based guard so the same staged entry is consumed only
once across later mounts, while preserving onReceive for non-empty bodies. In
apps/web/src/specs/app/publish/publish-handoff.spec.ts lines 64-70, add coverage
asserting entries with an empty body are removed and not delivered.
Greptile SummaryThis PR fixes two bugs: overflowing wave/deck-thread content was written into the submit page's local-draft key (
Confidence Score: 4/5Safe to merge with the cleanup fix applied; the core handoff channel works correctly and the crash path is fully eliminated. The new hook correctly stages and delivers content in the happy path, and the one-shot removal prevents repeated delivery. One defect exists in usePublishHandoff: when an entry is present but body is falsy, the early return fires without calling removeHandoff(), leaving the key in localStorage indefinitely. Current callers never write a falsy body (the overflow guard requires >250 chars of text), so there is no immediate user-visible impact, but it contradicts the documented guarantee and will silently leak storage if the write path ever changes. apps/web/src/app/publish/_hooks/use-publish-handoff.ts — the early-return path skips removeHandoff; apps/web/src/specs/app/publish/publish-handoff.spec.ts — the empty-body test should assert the key is cleaned up. Important Files Changed
Sequence DiagramsequenceDiagram
participant W as Wave / Deck Form
participant LS as localStorage ecency_pub_handoff
participant P as /publish page
participant E as Editor TipTap
W->>LS: stageForPublish(content)
W->>P: window.open /publish blank
P->>LS: usePublishHandoff read on mount
LS-->>P: body content
P->>LS: removeHandoff clear key
P->>P: setContent(body)
P->>E: setEditorContent(body)
Reviews (1): Last reviewed commit: "fix(publish): deliver overflowing waves ..." | Re-trigger Greptile |
| const body = handoff?.body; | ||
|
|
||
| if (!body) { | ||
| return; | ||
| } | ||
|
|
||
| removeHandoff(); | ||
| onReceive(body); |
There was a problem hiding this comment.
Stale entry not cleaned up when body is falsy. When the handoff key is present in localStorage but
body is an empty string (or any other falsy value after parsing), the early return fires without calling removeHandoff(). That entry stays in localStorage forever, causing this mount effect to re-evaluate the check on every subsequent /publish visit without ever consuming the key. Moving removeHandoff() before the guard ensures any entry — even a malformed one — is cleaned up exactly once.
| const body = handoff?.body; | |
| if (!body) { | |
| return; | |
| } | |
| removeHandoff(); | |
| onReceive(body); | |
| const body = handoff?.body; | |
| removeHandoff(); | |
| if (!body) { | |
| return; | |
| } | |
| onReceive(body); |
| it("ignores a staged entry with an empty body", () => { | ||
| localStorage.setItem(PUBLISH_HANDOFF_KEY, JSON.stringify({ body: "" })); | ||
|
|
||
| const onReceive = vi.fn(); | ||
| renderHook(() => usePublishHandoff(onReceive)); | ||
|
|
||
| expect(onReceive).not.toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
The test verifies that
onReceive is not called for an empty body, but doesn't assert that the localStorage key is removed afterward. Without this assertion, the gap described above (key persisting forever for falsy-body entries) would remain undetected by the test suite.
| it("ignores a staged entry with an empty body", () => { | |
| localStorage.setItem(PUBLISH_HANDOFF_KEY, JSON.stringify({ body: "" })); | |
| const onReceive = vi.fn(); | |
| renderHook(() => usePublishHandoff(onReceive)); | |
| expect(onReceive).not.toHaveBeenCalled(); | |
| }); | |
| it("ignores a staged entry with an empty body", () => { | |
| localStorage.setItem(PUBLISH_HANDOFF_KEY, JSON.stringify({ body: "" })); | |
| const onReceive = vi.fn(); | |
| renderHook(() => usePublishHandoff(onReceive)); | |
| expect(onReceive).not.toHaveBeenCalled(); | |
| expect(localStorage.getItem(PUBLISH_HANDOFF_KEY)).toBeNull(); | |
| }); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
When a wave or a deck thread goes past its character limit, the form builds the content with its attachments, stashes it, and opens
/publishso it can become a post instead.It stashed it under the submit page's local draft key (
ecency_local_draft). Nothing on/publishreads that key, so the content was silently dropped and the composer opened empty.The write was also partial:
setLocalDraft({ ...localDraft, body: content })over alocalDraftthat defaults to{}. That left the key holding a draft with neithertitlenortags, which the submit page passed straight intoapplyTitle->value.slice(...), crashing every later visit to/submit(ECENCY-NEXT-1GJC).Change
usePublishHandoffWriter/usePublishHandoffpair on a dedicated one-shot key (ecency_pub_handoff), consumed and cleared on composer mount.use-wave-submitand the deck threads form stage content through the writer instead of writing into the submit draft.ecency_local_draftis now written only by the submit page, which always writes a completePostBase.Notes
pnpm --filter @ecency/web test240 files / 2332 tests pass, typecheck clean, lint shows no new warnings.Summary by CodeRabbit
New Features
Bug Fixes
Tests