Skip to content

feat(app): replace release notes dialog with subtle toast (#486)#488

Merged
Astro-Han merged 18 commits intodevfrom
claude/release-notes-toast
May 7, 2026
Merged

feat(app): replace release notes dialog with subtle toast (#486)#488
Astro-Han merged 18 commits intodevfrom
claude/release-notes-toast

Conversation

@Astro-Han
Copy link
Copy Markdown
Owner

@Astro-Han Astro-Han commented May 7, 2026

Summary

Replaces the post-update release notes Dialog with a persistent subtle Toast in the bottom-right. Title shows Updated to v{version} (no brand prefix); description shows the parser-produced intro and bullets from the GitHub release; one action Full release notes → opens the GitHub release page; close-from-any-path marks the version seen so it never re-shows.

Why

The current 720x400 modal dialog blocks the screen on first launch after every update, forces internal scrolling (which docs/DESIGN.md reserves for sheets, not dialogs), and uses a two-column layout for media that releases never ship. Industry trend (Cursor 3, Claude Desktop, Codex in 2026) has moved to lightweight non-blocking notifications linking to a web changelog. The toast is non-blocking, dismissable, and follows DESIGN.md primitives.

Related Issue

Closes #486. The locked spec lives in the issue body and the first follow-up comment; the implementation plan lives in the second follow-up comment, edited to reflect each crosscheck round.

Human Review Status

Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.

Review Focus

The new subtle Toast variant in packages/ui/src/components/toast.{tsx,css} and its onDismiss hook (registered via Solid onCleanup so close button, action click, and programmatic dismiss all converge on one callback). The TOAST_COPY table in highlights.tsx deliberately duplicates strings from the deleted i18n keys: spec requires title and description to share a locale, and @solid-primitives/i18n has no per-call locale override; rationale is documented inline. Multi-version description layout (first segment description-only because newest tag is in title; subsequent segments prefix with their tag, separated by a blank line). Decision to drop the never-used structured-highlights parser branch (Array.isArray(value.highlights), MAX_STRUCTURED_HIGHLIGHTS, value.releases wrapper accessor, parseHighlight/parseMedia/Highlight.media) — verified against the full /releases history before deleting.

Risk Notes

User-visible: the dialog is gone on first launch after update; users who relied on the explicit "Don't show these in the future" button now toggle Settings → General → Release notes (the underlying preference and behavior are unchanged). Storage: highlights.v1 schema is unchanged; the markSeen semantics now run on dismiss instead of show, so a user who quits before dismissing keeps the toast queued for the next launch (matches spec). i18n: four dialog.releaseNotes.* keys are removed; no other consumers grep up. No platform, packaging, updater, signing, or migration risk.

How To Verify

typecheck (monorepo): 8/8 successful (turbo)
unit (highlights):    19/19 passed (35 expect calls, 103ms)
e2e (release-notes):  6/6 passed (chromium, 13.4s)
crosscheck plan:      both reviewers ok; 5 P1 findings addressed in plan revision
crosscheck code:      both reviewers ok; P1 dead i18n keys dropped, P2/P3 reviewed

E2E coverage in packages/app/e2e/release-notes/release-notes-toast.spec.ts: subtle toast appears with correct title/description/icon/action; close button writes markSeen to localStorage; releaseNotes=false suppresses; multi-version skipped releases merge with version labels; locale fallback keeps title and action in sync with the parser's effective locale (en title when zh release section is missing); zh title/action when zh release section is present.

Note: bun run dev:desktop cannot exercise the post-update path because the dev build reports "此构建不支持更新" — the Electron updater pipeline that gates highlights is disabled in dev. E2E covers the full controller flow with mocked GitHub Releases and seeded localStorage instead, which is the deterministic path.

Screenshots or Recordings

Visual verification is captured by the E2E suite (subtle variant render + locale + multi-version). Adding a screenshot of the running toast on top of that would not change the verdict; if a maintainer wants one, I can attach a Playwright trace artifact on request.

Checklist

  • Human review status is stated above as pending, approved, or not required
  • I linked the related issue, or stated why there is no issue
  • This PR has type, primary area, and priority labels, or I requested maintainer labeling
  • I described the review focus and any meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant
  • I reviewed the final diff for unrelated changes and suspicious dependency changes
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Maintainer label request: type:feat, area:app, area:ui, priority:p1.

Summary by CodeRabbit

  • New Features

    • Release notes now appear as a subtle, dismissible toast instead of a modal
    • Dismissing (close button or Escape) marks the release as seen
    • If configured to suppress UI, releases are still marked as seen
  • Behavior

    • Multiple skipped releases are consolidated into a single toast
    • Locale handling ensures a single language is used for the entire toast
  • Style

    • Added subtle toast variant styling for improved visual integration

Astro-Han added 6 commits May 7, 2026 15:23
The subtle variant uses surface-raised + fg-strong + border-weak for
non-urgent informational notices, distinct from the default deep tone
reserved for action receipts and errors. pre-line whitespace preserves
multi-line content.

The onDismiss option fires once when the toast root unmounts via
solid-js onCleanup, covering close button, action click, and
programmatic dismiss without leaking Kobalte lifecycle internals to
consumers.
Reshape parser to ReleaseSummary { tag, description, localeUsed }; drop
the structured-highlights branch (Array.isArray(value.highlights), the
value.releases wrapper accessor, parseHighlight/parseMedia/Highlight.media,
MAX_STRUCTURED_HIGHLIGHTS) which has never been used in any release.

Switch the controller from dialog.show to showToast with the new subtle
variant. markSeen moves from show-time to dismiss-time via onDismiss so
close button, action click, and programmatic dismiss all converge on
one hook.

For locale fallback, ReleaseSummary carries the locale the parser ended
up using; when the user-locale section is missing the toast title and
action copy fall back together with the description, avoiding mixed-
language output. The localized copy lives in TOAST_COPY since
@solid-primitives/i18n has no per-call locale override.

When the current platform version is not in the changelog (dev builds,
custom tags) sliceHighlights returns empty rather than slicing from
index 0 and surfacing every release. Multi-version merges keep the
newest segment description-only (its tag is in the title) and prefix
subsequent segments with their tag. Cap stays at 5 versions; older
skipped releases remain accessible via the GitHub link.
The release-notes dialog is fully replaced by the subtle toast in
highlights.tsx. Nothing else imports DialogReleaseNotes or the file
itself.
The dialog.releaseNotes.* keys (getStarted, next, hideFuture, media.alt)
served the deleted DialogReleaseNotes component and have no consumers
left. Replace with toast.releaseNotes.{title,action.viewFull} for
future consumers that want locale-following copy. Today's release-notes
toast goes through TOAST_COPY in highlights.tsx so it can honor the
parser's effective locale on fallback; the i18n strings happen to match
the en/zh values in TOAST_COPY.
The toast.releaseNotes.* keys added in the previous commit were never
read: TOAST_COPY in highlights.tsx owns the title and action strings
because the parsed body's locale (not the user's UI locale) drives the
copy when GitHub releases lack the user's locale section — the
@solid-primitives/i18n helper has no per-call locale override, so a
small inline copy table is the cleanest path. Document the rationale
inline so future maintainers don't restore the keys reflexively. Also
add a one-line comment on toast.tsx onCleanup explaining that it
covers all dismiss paths (close button, action click, swipe,
programmatic), which is the contract consumers rely on.
Six Playwright tests cover the user-visible toast path: subtle variant
appears with title/description/icon/action when the stored version is
older than current; close button writes markSeen to localStorage;
releaseNotes=false suppresses the toast; multi-version skipped releases
merge with version labels; locale fallback keeps title and action in
sync with the parsed body locale (en when zh section is missing); and
zh title/action render when a zh release section is present.

Mocks the GitHub Releases API via page.route and seeds highlights.v1
plus pawwork.global.dat:language via addInitScript so the controller's
update path runs deterministically without network.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b8bcc88b-2f94-4e4e-b1d1-a7fc70d15f20

📥 Commits

Reviewing files that changed from the base of the PR and between 3e55c4a and ee020cf.

📒 Files selected for processing (1)
  • packages/app/e2e/release-notes/release-notes-toast.spec.ts
📝 Walkthrough

Walkthrough

Replaces the blocking release-notes modal with a persistent locale-aware subtle toast that merges up to five skipped releases, introduces a ReleaseSummary model and toast onDismiss lifecycle, updates parsing to prefer release-body notices with locale detection, removes the DialogReleaseNotes component, and adds unit and E2E tests plus subtle toast styles.

Changes

Release Notes Toast Refactor

Layer / File(s) Summary
Highlights Data Model
packages/app/src/context/highlights.tsx
Adds exported ReleaseSummary (tag, description, localeUsed) and TOAST_COPY locale mapping.
Changelog Parsing & Description
packages/app/src/context/highlights.tsx
Parses release-body notices into ReleaseSummary[], slices by normalized tags, caps to 5 newest pages, and adds buildToastDescription to merge multi-version descriptions.
Context Integration
packages/app/src/context/highlights.tsx
Context init fetches changelog, ensures single-language window (retries in English if mixed), and shows a persistent toast using TOAST_COPY[newest.localeUsed] and the merged description; respects settings.general.releaseNotes.
Toast Variant & Lifecycle
packages/ui/src/components/toast.tsx, packages/ui/src/components/toast.css
Adds "subtle" variant and ToastOptions.onDismiss?: () => void; uses onCleanup to invoke onDismiss once on unmount, gated by user-driven dismissal; CSS adds subtle styling and white-space: pre-line for description.
Dialog Component
packages/app/src/components/dialog-release-notes.tsx
Introduces DialogReleaseNotes multi-page UI component with pagination, keyboard navigation, scroll reset, and media rendering.
Unit Tests
packages/app/src/context/highlights.test.ts
Updates tests to assert ReleaseSummary shape and adds skipped-version cap/newest-first tests plus a current-missing empty result case.
E2E Tests
packages/app/e2e/release-notes/release-notes-toast.spec.ts
Adds Playwright tests for toast visibility/content, close/Escape dismissal updating localStorage, suppression when releaseNotes=false, multi-version merge description, Chinese/English fallback and locale-consistency scenarios.
Test Inventory
packages/opencode/test/config/e2e-smoke-tagging.test.ts
Registers the new release-notes toast e2e spec in smoke test inventory.
Internationalization
packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts
Removes dialog-related English strings and adds Chinese plural key workspace.reset.archived.many.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested labels

enhancement, P1, app, ui

Poem

🐰 A modal closed, a subtle toast in sight,
Five skipped versions merged just right.
Locale kept whole, no mixed-language fray,
Dismissed once, remembered the day.
The rabbit cheered: "Hooray, hooray!" 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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
Title check ✅ Passed The title 'feat(app): replace release notes dialog with subtle toast (#486)' clearly and specifically summarizes the main change, following Conventional Commits conventions.
Description check ✅ Passed The PR description comprehensively covers Summary, Why, Related Issue, Human Review Status, Review Focus, Risk Notes, How To Verify, and Checklist. All required sections are present with substantive content.
Linked Issues check ✅ Passed All primary coding objectives from issue #486 are met: toast replaces dialog, title shows 'Updated to v{version}', description contains release intro and bullets, multi-version merge (up to 5) with version labels, unified locale with fallback, action opens GitHub release URL, no auto-dismiss, dismiss marks seen, releaseNotes setting respected, DialogReleaseNotes removed, subtle Toast variant introduced.
Out of Scope Changes check ✅ Passed Changes are narrowly scoped to the release-notes subsystem. Toast variant additions, toast.tsx/css updates, highlights.tsx refactoring, dialog removal, i18n key removals, and E2E/unit test updates all directly support the stated objective. No unrelated refactors, dependencies, or file changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/release-notes-toast

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 and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request replaces the release notes dialog with a subtle toast notification and simplifies the highlights parsing logic by removing support for structured highlights. It introduces a new 'subtle' toast variant and includes comprehensive E2E tests. The review feedback correctly identifies a TypeScript compilation error caused by a type mismatch between the application's full locale union and the subset supported by the release notes parser; suggestions were provided to narrow the internal locale type and update function signatures accordingly.

Comment thread packages/app/src/context/highlights.tsx
Comment thread packages/app/src/context/highlights.tsx
Comment thread packages/app/src/context/highlights.tsx Outdated
Comment thread packages/app/src/context/highlights.tsx Outdated
@Astro-Han
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/ui/src/components/toast.tsx (1)

121-133: 💤 Low value

Clarify that onCleanup also fires on parent unmount, not only dismiss paths.

The inline comment lists "close button, action click, swipe, programmatic toaster.dismiss" but onCleanup will also run when the enclosing reactive owner (i.e., the <Toast.Region> / app root) unmounts. Callers wiring onDismiss to side effects with "user-acknowledged" semantics (e.g., markSeen in highlights.tsx) will get the callback on app teardown too. Worth softening the comment so future readers don't treat onDismiss as strictly user-initiated. I'll raise the actual semantic risk on the call site.

📝 Proposed comment update
-    // onCleanup runs when the toast root unmounts (close button, action click,
-    // swipe, programmatic toaster.dismiss) — covering all dismiss paths in one
-    // hook. The dismissed guard is defense against future Kobalte upgrades
-    // re-invoking this render closure.
+    // onCleanup runs whenever the toast root unmounts: explicit dismiss paths
+    // (close button, action click, swipe, programmatic toaster.dismiss) AND
+    // ambient unmounts (parent owner teardown, e.g., app exit). Callers that
+    // require "user-acknowledged" semantics should not rely on this firing
+    // only after a deliberate dismiss. The `dismissed` guard is defense
+    // against future Kobalte upgrades re-invoking this render closure.
🤖 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 `@packages/ui/src/components/toast.tsx` around lines 121 - 133, The inline
comment inside the toaster.show render closure (around the onCleanup usage that
guards opts.onDismiss via the dismissed flag) incorrectly implies onCleanup only
runs for user-dismiss paths; update the comment near
toaster.show/onCleanup/opts.onDismiss to explicitly mention that onCleanup also
fires when the enclosing reactive owner (e.g., <Toast.Region> or app root)
unmounts, and soften the language so callers know onDismiss may run on
parent/unmount as well (preserving the dismissed guard logic with the dismissed
variable unchanged).
packages/app/src/context/highlights.tsx (1)

294-314: 💤 Low value

Document the 500ms setTimeout rationale (or replace with an explicit readiness signal).

The deferred showToast has no comment explaining why it waits 500ms — readers are left to guess (waiting for <Toast.Region> mount? avoiding splash overlap?). A brief inline note, or better, awaiting an explicit signal (e.g., the toast region's mount event), prevents this from looking like a flake-mitigation hack.

🤖 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 `@packages/app/src/context/highlights.tsx` around lines 294 - 314, The 500ms
setTimeout wrapping showToast (where timer is assigned, using summaries[0],
TOAST_COPY, buildToastDescription, platform.openLink, and markSeen) is
undocumented and fragile; either add a concise inline comment explaining the
exact reason for the delay (e.g., waiting for <Toast.Region> mount or avoiding
UI overlap) or, preferably, replace the magic delay with an explicit readiness
signal: introduce/await a toastRegionReady promise or subscribe to a
toastRegionMounted event before calling showToast, then remove the setTimeout
and the timer variable to ensure deterministic behavior.
packages/app/e2e/release-notes/release-notes-toast.spec.ts (1)

6-54: 💤 Low value

Constants should use SCREAMING_SNAKE_CASE per the e2e guidelines.

singleReleasePayload, multiVersionPayload, localizedPayload, and the various toast*Selector constants are file-level immutable values, which the project convention treats as constants. RELEASES_URL_PATTERN and HIGHLIGHTS_KEY already follow the convention; the rest should match.

As per coding guidelines: "Use SCREAMING_SNAKE_CASE for constants in tests".

🛠️ Proposed renames (apply consistently to all references)
-const singleReleasePayload = [
+const SINGLE_RELEASE_PAYLOAD = [
   …
 ]

-const multiVersionPayload = [
+const MULTI_VERSION_PAYLOAD = [
   …
 ]

-const localizedPayload = [
+const LOCALIZED_PAYLOAD = [
   …
 ]

-const toastSelector = '[data-component="toast"][data-variant="subtle"]'
-const toastTitleSelector = `${toastSelector} [data-slot="toast-title"]`
-const toastDescriptionSelector = `${toastSelector} [data-slot="toast-description"]`
-const toastActionSelector = `${toastSelector} [data-slot="toast-action"]`
-const toastCloseButtonSelector = `${toastSelector} [data-slot="toast-close-button"]`
-const toastIconSelector = `${toastSelector} [data-slot="toast-icon"]`
+const TOAST_SELECTOR = '[data-component="toast"][data-variant="subtle"]'
+const TOAST_TITLE_SELECTOR = `${TOAST_SELECTOR} [data-slot="toast-title"]`
+const TOAST_DESCRIPTION_SELECTOR = `${TOAST_SELECTOR} [data-slot="toast-description"]`
+const TOAST_ACTION_SELECTOR = `${TOAST_SELECTOR} [data-slot="toast-action"]`
+const TOAST_CLOSE_BUTTON_SELECTOR = `${TOAST_SELECTOR} [data-slot="toast-close-button"]`
+const TOAST_ICON_SELECTOR = `${TOAST_SELECTOR} [data-slot="toast-icon"]`
🤖 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 `@packages/app/e2e/release-notes/release-notes-toast.spec.ts` around lines 6 -
54, The file-level immutable test constants singleReleasePayload,
multiVersionPayload, localizedPayload and the selector constants toastSelector,
toastTitleSelector, toastDescriptionSelector, toastActionSelector,
toastCloseButtonSelector, toastIconSelector should be renamed to
SCREAMING_SNAKE_CASE (e.g., SINGLE_RELEASE_PAYLOAD, MULTI_VERSION_PAYLOAD,
LOCALIZED_PAYLOAD, TOAST_SELECTOR, TOAST_TITLE_SELECTOR,
TOAST_DESCRIPTION_SELECTOR, TOAST_ACTION_SELECTOR, TOAST_CLOSE_BUTTON_SELECTOR,
TOAST_ICON_SELECTOR) and all usages in release-notes-toast.spec.ts updated
accordingly; keep RELEASES_URL_PATTERN and HIGHLIGHTS_KEY as-is since they
already follow the convention. Ensure only the identifier names change (no
runtime behavior) and update any imports/references within the file.
🤖 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 `@packages/app/e2e/release-notes/release-notes-toast.spec.ts`:
- Around line 127-137: Replace the wall-clock wait in the test by polling
localStorage until the highlights version advances: instead of awaiting
page.waitForTimeout(1500), call expect.poll on a page.evaluate that reads
localStorage[HIGHLIGHTS_KEY] (the same key used by markSeen()/start()) and
returns the parsed version, and poll until it equals "2026.5.7"; once the poll
succeeds, assert page.locator(toastSelector) hasCount(0). This removes the flaky
sleep and uses the deterministic readiness signal produced when
start()/markSeen() runs with releaseNotes false.
- Around line 97-105: Replace the one-shot localStorage read with a polled
assertion using expect.poll so the test waits for the persistence write to
complete after the toast is closed; specifically, after clicking
toastCloseButtonSelector and awaiting toastSelector toBeHidden(), use
expect.poll to repeatedly evaluate localStorage.getItem(HIGHLIGHTS_KEY) (parse
JSON) and assert the stored?.version equals "2026.5.7" instead of calling
page.evaluate once — this ensures the markSeen/setStore persistence has finished
before asserting.

In `@packages/app/src/context/highlights.tsx`:
- Around line 294-314: The toast onDismiss is being called during unmount and
incorrectly marks releases seen; change the showToast usage in the highlights
code (the setTimeout block that calls showToast) to provide a user-initiated
guard: create a local boolean (e.g., userInitiated) scoped to that toast, set it
true only from user actions (the action onClick for viewFull and the toast
close-button handler in toast.tsx), and replace direct onDismiss: markSeen with
an onDismiss that only calls markSeen if userInitiated is true; update the toast
component’s close-button handler (and any explicit close-path) to flip that flag
so only explicit user dismissals or action clicks call markSeen while
unmount/cleanup still runs without marking versions seen.

---

Nitpick comments:
In `@packages/app/e2e/release-notes/release-notes-toast.spec.ts`:
- Around line 6-54: The file-level immutable test constants
singleReleasePayload, multiVersionPayload, localizedPayload and the selector
constants toastSelector, toastTitleSelector, toastDescriptionSelector,
toastActionSelector, toastCloseButtonSelector, toastIconSelector should be
renamed to SCREAMING_SNAKE_CASE (e.g., SINGLE_RELEASE_PAYLOAD,
MULTI_VERSION_PAYLOAD, LOCALIZED_PAYLOAD, TOAST_SELECTOR, TOAST_TITLE_SELECTOR,
TOAST_DESCRIPTION_SELECTOR, TOAST_ACTION_SELECTOR, TOAST_CLOSE_BUTTON_SELECTOR,
TOAST_ICON_SELECTOR) and all usages in release-notes-toast.spec.ts updated
accordingly; keep RELEASES_URL_PATTERN and HIGHLIGHTS_KEY as-is since they
already follow the convention. Ensure only the identifier names change (no
runtime behavior) and update any imports/references within the file.

In `@packages/app/src/context/highlights.tsx`:
- Around line 294-314: The 500ms setTimeout wrapping showToast (where timer is
assigned, using summaries[0], TOAST_COPY, buildToastDescription,
platform.openLink, and markSeen) is undocumented and fragile; either add a
concise inline comment explaining the exact reason for the delay (e.g., waiting
for <Toast.Region> mount or avoiding UI overlap) or, preferably, replace the
magic delay with an explicit readiness signal: introduce/await a
toastRegionReady promise or subscribe to a toastRegionMounted event before
calling showToast, then remove the setTimeout and the timer variable to ensure
deterministic behavior.

In `@packages/ui/src/components/toast.tsx`:
- Around line 121-133: The inline comment inside the toaster.show render closure
(around the onCleanup usage that guards opts.onDismiss via the dismissed flag)
incorrectly implies onCleanup only runs for user-dismiss paths; update the
comment near toaster.show/onCleanup/opts.onDismiss to explicitly mention that
onCleanup also fires when the enclosing reactive owner (e.g., <Toast.Region> or
app root) unmounts, and soften the language so callers know onDismiss may run on
parent/unmount as well (preserving the dismissed guard logic with the dismissed
variable unchanged).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 701252ed-5cc6-444e-8c3d-9295cdc115d6

📥 Commits

Reviewing files that changed from the base of the PR and between d216ec0 and 27e6601.

📒 Files selected for processing (8)
  • packages/app/e2e/release-notes/release-notes-toast.spec.ts
  • packages/app/src/components/dialog-release-notes.tsx
  • packages/app/src/context/highlights.test.ts
  • packages/app/src/context/highlights.tsx
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/ui/src/components/toast.css
  • packages/ui/src/components/toast.tsx
💤 Files with no reviewable changes (3)
  • packages/app/src/i18n/zh.ts
  • packages/app/src/components/dialog-release-notes.tsx
  • packages/app/src/i18n/en.ts

Comment thread packages/app/e2e/release-notes/release-notes-toast.spec.ts Outdated
Comment thread packages/app/e2e/release-notes/release-notes-toast.spec.ts
Comment thread packages/app/src/context/highlights.tsx
Astro-Han added 5 commits May 7, 2026 17:00
onCleanup also fires on ambient unmount (parent owner teardown / app exit),
so wiring `onDismiss: markSeen` directly silently marked unseen releases as
seen on app close. Spec #486 requires "quitting unread re-shows on next
launch", so onDismiss must distinguish user-initiated dismissal from
teardown.

Add a userDismissed flag set only by close-button click and action-button
click handlers. onCleanup gates the onDismiss callback behind that flag —
ambient unmounts no longer fire onDismiss. Update inline comment to reflect
the dual unmount semantics; document the 500ms toast defer rationale in
highlights.tsx.

Reported by CodeRabbit AI on PR #488.
Two race-prone patterns were violating packages/app/e2e/AGENTS.md
"Wait on state": page.waitForTimeout(1500) as the suppression signal,
and a one-shot localStorage read after the toast hides. Both can read
stale state because the persistence write and the visibility transition
are not strictly ordered.

Replace with expect.poll on localStorage["highlights.v1"].version — the
deterministic readiness signal produced by markSeen(). Suppression test
asserts toast count after the storage advances, no wall-clock wait.

Reported by CodeRabbit AI on PR #488.
packages/app/e2e/AGENTS.md "Naming Conventions" requires constants in
tests to use SCREAMING_SNAKE_CASE. RELEASES_URL_PATTERN and HIGHLIGHTS_KEY
already followed; bring the payload fixtures and selector strings in line.

Reported by CodeRabbit AI on PR #488.
parseChangelog used to drop releases without an "App Update Notice" /
"中文版本" section before sliceHighlights ran, so when an intermediate
release lacked an app-facing notice the previous boundary could not be
located and the slice spilled into older versions the user had already
seen. Example: bumping v1.0.4 → v1.0.5 with v1.0.4 carrying only download
links would surface v1.0.3 and v1.0.2 in the toast.

Introduce an internal RawRelease carrying the tag for every release plus
an optional summary. parseChangelog keeps every tagged release; window
slicing runs on the raw tag list, then summary filtering and the
five-version cap apply afterwards.

Adds a regression test for the exact "previous lacks notice" payload.
Per-release localeUsed allowed a multi-version toast to mix languages: a
zh user with newest release carrying a 中文版本 section but an older
skipped release carrying only an English notice would see a zh title and
action with an English segment in the middle. Spec #486 explicitly
forbids title and description ever mixing languages.

loadReleaseHighlights now resolves once in the user's locale, then checks
whether every selected release stayed in that locale. If any segment
fell back, the entire window is re-resolved in English so title, action,
and every description segment share a single locale.

Adds unit coverage (mixed window collapses to en, all-zh window stays
zh) and an e2e regression for the user-facing path.
@Astro-Han
Copy link
Copy Markdown
Owner Author

Two P2 findings from the latest review are addressed in this push:

  • P2-A: 388681b80c — pre-slice filter dropped releases without an App Update Notice, so an intermediate release missing a notice could break the previous-version boundary and spill the toast into older versions the user had already seen. parseChangelog now keeps a raw tag for every release; window slicing runs on the raw tag list, then the summary filter and five-version cap apply afterwards. Regression test in highlights.test.ts.

  • P2-B: 23df9960a8 — multi-version merging let title/action follow the newest release's locale while older segments could fall back to English, mixing languages within a single toast (forbidden by issue [Feature] Replace release notes dialog with persistent subtle toast #486 spec). loadReleaseHighlights now re-resolves the entire window in English whenever any selected release lacked the user's locale, so title, action, and every segment share one locale. Unit + e2e regression added.

Verification: typecheck 8/8 · unit 22/22 · e2e 7/7 (added 1 for the locale fix).

Kobalte's swipe-to-dismiss and Escape key paths call close() directly
without going through CloseButton's onClick, leaving userDismissed false
and skipping onDismiss. On touch devices a swipe-away (or any keyboard
Escape) was silently ignored by markSeen, so the release notes toast
re-appeared on next launch even after the user had dismissed it.

Wire markUserDismissed into onSwipeEnd and onEscapeKeyDown on the Toast
root, alongside the existing CloseButton and action-button hooks. Add an
e2e test that presses Escape and asserts highlights.v1 advances — the
swipe path uses the same hook so this guards both code paths.
@Astro-Han
Copy link
Copy Markdown
Owner Author

Astro-Han commented May 7, 2026

P1-1 fixed in 5216a8d. Remaining P2/P3 verdicts below — most are cosmetic or self-flagged non-blocking by the reviewer.

P1-1 swipe/escape skip markSeen — fixed

Verified against @kobalte/[email protected]. Reviewer described the symptom correctly, but the gap is broader than swipe alone: in dist/chunk/FQHPINZZ.js there are three user-driven close paths. CloseButton.onClick is one; the other two bypass it:

  • :365 Escape key → onEscapeKeyDownclose()
  • :444 swipe-end past threshold → onSwipeEndclose()

Fix: attach both onSwipeEnd={markUserDismissed} and onEscapeKeyDown={markUserDismissed} on the <Toast> root, sharing the same flag with CloseButton.onClick and the action button. Added an Escape E2E that uses page.keyboard.press("Escape") plus a localStorage poll to assert markSeen fires; the swipe path runs through the identical hook so it is covered by code-isomorphism (Playwright dragTo pointer simulation is too flaky to add as a separate test).

Validation: typecheck 8/8, unit 937/937, e2e 8/8 (1 new).

P2/P3 verdicts

Item Decision Reason
P2-1 500ms magic number Reject highlights.tsx:326-328 already documents the splash visual-buffer coupling. Extracting a constant is purely cosmetic and adds no information.
P2-2 quit-before-dismiss comment Reject toast.tsx:122-130 already explains the user-dismiss vs ambient-unmount contract in 8 lines (onCleanup runs on both unmount kinds; userDismissed is a user-driven-only flag). highlights' onDismiss: markSeen is just the callback site; semantics are documented in the component.
P2-3 TOAST_COPY No follow-up (reviewer self-flagged)
P2-4 legacy schema comment Reject The branch has no callers. Adding a comment guards against an undo that has not happened, which is anti-YAGNI.
P2-5 quit-before-dismiss E2E No follow-up (reviewer self-flagged technically infeasible)
P3-1 naming Fixed
P3-2 pre-line No follow-up (reviewer self-flagged)
P3-3 empty description guard No follow-up (reviewer said it cannot trigger)

Astro-Han added 4 commits May 7, 2026 17:49
Title and the GitHub release link were derived from summaries[0].tag,
which diverges from platform.version when the current release lacks a
notice in the resolved locale. Concrete failure: zh user updates from
v2026.5.5 to v2026.5.7; the v2026.5.7 body has only a 中文版本 section
(no App Update Notice), and v2026.5.6 has only an App Update Notice
(no 中文版本). First-pass zh resolves to mixed (zh + en), triggers an
English rebuild which drops v2026.5.7 (no English notice there), so
summaries[0] is v2026.5.6 and the toast shows "Updated to v2026.5.6"
linking to the older release.

Use platform.version (with v prefix) for both the title and the link
URL. summaries[0].localeUsed still drives the title/action copy locale
so spec #486's "title and description must always use the same locale"
holds. Added e2e covering the failure scenario above.
Two related Toast API fixes:

1) Spec #486 requires that "any dismiss path — close button, action
click, or programmatic dismiss — marks the version as seen". Raw
toaster.dismiss(id) bypassed userDismissed and skipped onDismiss, so
external programmatic dismissals failed the contract. Add a
dismissToast(id) wrapper that flags the id via a module-scope set
which the render closure's onCleanup consults. Raw toaster.dismiss
(used by promise-toast lifecycle, etc.) still counts as ambient
teardown, preserving the route/app-exit guard.

2) The action button ran action.onClick() before toaster.dismiss(id).
A synchronous throw in onClick would skip the dismiss and pin the
toast on screen. Wrap the call in try/finally so dismiss always runs
even if the action handler throws.
…ersion

When the resolved-locale window drops the current release (e.g. zh user
on v2026.5.7 with only a 中文版本 section, fallback to English drops it
because v2026.5.7 has no English notice), summaries[0] is an older
release. The toast title still reads "Updated to v2026.5.7" — anchored
on platform.version — but buildToastDescription assumed summaries[0]
was the current version and omitted its tag. Result: the description's
first segment looked like the current release's content.

Pass currentTag into buildToastDescription and only omit the tag prefix
when summaries[i].tag matches it. Strengthen the matching e2e to assert
v2026.5.6 appears in the description when it is summaries[0] but not
the current version.
@Astro-Han
Copy link
Copy Markdown
Owner Author

Astro-Han commented May 7, 2026

Review handling:

P2 description first segment missing tag (fixed in 3e55c4a)

buildToastDescription previously assumed summaries[0] was the current release and omitted its tag prefix. When the resolved-locale window drops the current release (e.g. zh user with a 中文版本-only newest release; English rebuild filters it out because there is no English notice there), summaries[0] is an older release. The title is already anchored on currentTag, but a tag-less first segment then reads as if the older release's bullets describe the current version.

Fix: pass currentTag into buildToastDescription and only omit the tag prefix when summaries[i].tag === currentTag. The e2e `title and link anchor on the app's current version...` now also asserts the v2026.5.6 tag appears in the description.

Verification: typecheck 8/8, unit 937/937, e2e 9/9.

P3 docs/DESIGN.md not in PR (declined)

`docs/DESIGN.md` is a repo-local file excluded via `.git/info/exclude` (not the tracked .gitignore), so it can never appear in any PR diff. The subtle variant is documented locally in DESIGN.md (zh, line 209). Issue #486's done criterion as worded is unenforceable at the repository level. A PR cannot modify a git-untracked file.

@Astro-Han
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/app/e2e/release-notes/release-notes-toast.spec.ts (1)

220-223: ⚡ Quick win

Extract the hardcoded "pawwork.global.dat:language" key into a named constant.

The string appears in four tests verbatim. If the key is ever renamed in the application, these locale tests silently degrade: the locale setting simply isn't applied, every "falls back to English" assertion vacuously passes, and the zh content assertions fail in a confusing way.

settingsKey already follows the pattern of being imported/shared; the language key deserves the same treatment — either exported from fixtures or at minimum declared at the top of this file alongside the other constants.

♻️ Proposed refactor
 const HIGHLIGHTS_KEY = "highlights.v1"
+const LANGUAGE_KEY = "pawwork.global.dat:language"

Then replace each inline string with LANGUAGE_KEY, e.g.:

-      localStorage.setItem("pawwork.global.dat:language", JSON.stringify({ locale: "zh" }))
+      localStorage.setItem(LANGUAGE_KEY, JSON.stringify({ locale: "zh" }))

Also applies to: 244-246, 281-283, 333-336

🤖 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 `@packages/app/e2e/release-notes/release-notes-toast.spec.ts` around lines 220
- 223, Extract the hardcoded "pawwork.global.dat:language" into a named constant
(e.g. LANGUAGE_KEY) and use it wherever localStorage.setItem is called in these
tests; either import the existing exported key from the shared fixtures
(preferred) or declare const LANGUAGE_KEY = "pawwork.global.dat:language" at the
top of the spec file next to other constants like HIGHLIGHTS_KEY/settingsKey,
then replace every inline occurrence of "pawwork.global.dat:language" (the four
localStorage.setItem usages) with LANGUAGE_KEY so tests will break loudly if the
app renames the key.
🤖 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 `@packages/app/e2e/release-notes/release-notes-toast.spec.ts`:
- Around line 220-223: Extract the hardcoded "pawwork.global.dat:language" into
a named constant (e.g. LANGUAGE_KEY) and use it wherever localStorage.setItem is
called in these tests; either import the existing exported key from the shared
fixtures (preferred) or declare const LANGUAGE_KEY =
"pawwork.global.dat:language" at the top of the spec file next to other
constants like HIGHLIGHTS_KEY/settingsKey, then replace every inline occurrence
of "pawwork.global.dat:language" (the four localStorage.setItem usages) with
LANGUAGE_KEY so tests will break loudly if the app renames the key.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fdd812b-8573-400b-b107-918270dc2931

📥 Commits

Reviewing files that changed from the base of the PR and between 7fc344a and 3e55c4a.

📒 Files selected for processing (3)
  • packages/app/e2e/release-notes/release-notes-toast.spec.ts
  • packages/app/src/context/highlights.tsx
  • packages/ui/src/components/toast.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/app/src/context/highlights.tsx
  • packages/ui/src/components/toast.tsx

Adds an e2e that clicks the Full release notes action and asserts the
GitHub release URL is opened (via a stubbed window.open that captures
calls) and that the version advances in highlights.v1. Guards three
regressions in one test: (1) URL stays anchored on platform.version
with the v prefix, (2) action click marks the version as seen via the
same onDismiss path used by close/Escape, (3) the toast actually
dismisses on action click.
@Astro-Han
Copy link
Copy Markdown
Owner Author

P2 (action click coverage) addressed in 9399c5d. Added an e2e that clicks the Full release notes action with a stubbed window.open and asserts (1) the captured URL equals `https://github.com/Astro-Han/pawwork/releases/tag/v2026.5.7\`, (2) highlights.v1 advances via the same onDismiss path used by close/Escape, (3) the toast dismisses. 10/10 e2e local.

P3 (docs/DESIGN.md) already addressed in #issuecomment-4396407795: that file is repo-local via .git/info/exclude, never enters PR diff.

@Astro-Han
Copy link
Copy Markdown
Owner Author

Nitpick addressed in ee020cf. Extracted LANGUAGE_KEY = "pawwork.global.dat:language" next to HIGHLIGHTS_KEY and replaced all four inline occurrences.

@Astro-Han Astro-Han merged commit 11dcbf1 into dev May 7, 2026
20 checks passed
@Astro-Han Astro-Han deleted the claude/release-notes-toast branch May 7, 2026 11:19
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.

[Feature] Replace release notes dialog with persistent subtle toast

1 participant