Skip to content

fix(publish): deliver overflowing waves and deck threads to the composer#1237

Open
feruzm wants to merge 1 commit into
developfrom
bugfix/waves-overflow-publish-handoff
Open

fix(publish): deliver overflowing waves and deck threads to the composer#1237
feruzm wants to merge 1 commit into
developfrom
bugfix/waves-overflow-publish-handoff

Conversation

@feruzm

@feruzm feruzm commented Jul 24, 2026

Copy link
Copy Markdown
Member

When a wave or a deck thread goes past its character limit, the form builds the content with its attachments, stashes it, and opens /publish so it can become a post instead.

It stashed it under the submit page's local draft key (ecency_local_draft). Nothing on /publish reads that key, so the content was silently dropped and the composer opened empty.

The write was also partial: setLocalDraft({ ...localDraft, body: content }) over a localDraft that defaults to {}. That left the key holding a draft with neither title nor tags, which the submit page passed straight into applyTitle -> value.slice(...), crashing every later visit to /submit (ECENCY-NEXT-1GJC).

Change

  • New usePublishHandoffWriter / usePublishHandoff pair on a dedicated one-shot key (ecency_pub_handoff), consumed and cleared on composer mount.
  • use-wave-submit and the deck threads form stage content through the writer instead of writing into the submit draft.
  • The publish page seeds both publish state and the editor, matching what the import flow already does, since the editor prefills from publish state only if it initialises after the handoff runs.

ecency_local_draft is now written only by the submit page, which always writes a complete PostBase.

Notes

  • The entry paired with fix(submit): handle missing title in local drafts #1236, which guards the read side so users who already have the malformed draft in localStorage stop crashing. This one stops it being written.
  • Tests cover staging, one-shot delivery, empty and missing entries, the round trip, and that staging leaves an in-progress submit draft untouched.
  • pnpm --filter @ecency/web test 240 files / 2332 tests pass, typecheck clean, lint shows no new warnings.

Summary by CodeRabbit

  • New Features

    • Added seamless handoff of oversized thread and wave content to the Publish composer.
    • Preserves attached content during the handoff and opens it in a new tab for editing.
    • Automatically pre-fills the Publish composer with handed-off content.
  • Bug Fixes

    • Prevents oversized content from overwriting local drafts.
    • Ensures handed-off content is delivered only once and then cleared.
  • Tests

    • Added coverage for handoff, delivery, draft preservation, and empty-content handling.

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.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Oversized 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.

Changes

Publish handoff

Layer / File(s) Summary
Handoff channel
apps/web/src/app/publish/_hooks/*
Adds the publish handoff payload, storage key, writer hook, receiver hook, and hook re-export.
Producer and composer integration
apps/web/src/app/decks/..., apps/web/src/features/waves/..., apps/web/src/app/publish/_page.tsx
Stages over-length thread and wave content, then consumes it to initialize the publish editor.
Handoff behavior validation
apps/web/src/specs/app/publish/publish-handoff.spec.ts
Tests storage isolation, delivery, one-time consumption, no-op cases, and round-trip content preservation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: patch

Poem

I’m a rabbit with a parcel to send,
Through publish’s door, the drafts now wend.
Stored just once, then cleared away,
The editor blooms with words today.
Hop, hop—content finds its way!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 change: handing overflowing wave and deck thread content off to the publish composer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 bugfix/waves-overflow-publish-handoff

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between bd002e8 and 4875c68.

📒 Files selected for processing (6)
  • apps/web/src/app/decks/_components/deck-threads-form/index.tsx
  • apps/web/src/app/publish/_hooks/index.ts
  • apps/web/src/app/publish/_hooks/use-publish-handoff.ts
  • apps/web/src/app/publish/_page.tsx
  • apps/web/src/features/waves/hooks/use-wave-submit.ts
  • apps/web/src/specs/app/publish/publish-handoff.spec.ts

Comment on lines +39 to +49
const [handoff, , removeHandoff] = useLocalStorage<PublishHandoff>(PUBLISH_HANDOFF_KEY);

useMount(() => {
const body = handoff?.body;

if (!body) {
return;
}

removeHandoff();
onReceive(body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.ts

Repository: 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}")
PY

Repository: 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
done

Repository: 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)$') || true

Repository: 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-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes two bugs: overflowing wave/deck-thread content was written into the submit page's local-draft key (ecency_local_draft) which the publish composer never reads, silently dropping the content; and the partial write (no title or tags) caused a crash on every subsequent visit to /submit. The fix introduces a dedicated one-shot channel (ecency_pub_handoff) via a new usePublishHandoffWriter / usePublishHandoff hook pair that is staged by the wave and deck forms and consumed exactly once by the publish composer on mount.

  • A new dedicated handoff key isolates the channel from the submit-page draft, preventing corruption of in-progress drafts.
  • The publish page seeds both publishState and the editor content on receipt, covering the timing gap where the editor may initialise before or after the handoff runs.
  • Six focused tests cover staging, delivery, one-shot semantics, the empty-key case, an empty body, and the full round-trip.

Confidence Score: 4/5

Safe 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

Filename Overview
apps/web/src/app/publish/_hooks/use-publish-handoff.ts New one-shot handoff channel via a dedicated localStorage key. Core logic is sound, but a falsy-body entry escapes cleanup (removeHandoff not called on the early return path).
apps/web/src/app/publish/_page.tsx Consumes the handoff via usePublishHandoff correctly; seeds both publish state and editor to handle timing differences between state and editor initialization. No issues found.
apps/web/src/features/waves/hooks/use-wave-submit.ts Replaces the broken write to ecency_local_draft with stageForPublish; logic and overflow condition unchanged.
apps/web/src/app/decks/_components/deck-threads-form/index.tsx Same fix as use-wave-submit: hands off overflowing content via the new channel instead of writing a partial submit draft.
apps/web/src/specs/app/publish/publish-handoff.spec.ts Good coverage of happy path, one-shot semantics, empty-key, and round-trip. Missing assertion that the empty-body test cleans up the localStorage entry.
apps/web/src/app/publish/_hooks/index.ts Adds the new hook pair to the barrel export. No issues.

Sequence Diagram

sequenceDiagram
    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)
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(publish): deliver overflowing waves ..." | Re-trigger Greptile

Comment on lines +42 to +49
const body = handoff?.body;

if (!body) {
return;
}

removeHandoff();
onReceive(body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

Suggested change
const body = handoff?.body;
if (!body) {
return;
}
removeHandoff();
onReceive(body);
const body = handoff?.body;
removeHandoff();
if (!body) {
return;
}
onReceive(body);

Fix in Claude Code

Comment on lines +64 to +71
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();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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!

Fix in Claude Code

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