From 3324d63ab1be9dce68cf557c33f16894c9f7c585 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 7 Aug 2026 01:05:05 -0700 Subject: [PATCH] [eric] release: What's New card + endpoint serve the same story as Help and GitHub, header punctuation fixed --- backend/apps/help/bundle.py | 16 ++++ backend/apps/help/changelog.py | 2 +- backend/tests/test_changelog.py | 6 ++ .../src/app/components/Layout/AppShell.tsx | 3 + .../app/components/Layout/WhatsNewCard.tsx | 81 +++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/components/Layout/WhatsNewCard.tsx diff --git a/backend/apps/help/bundle.py b/backend/apps/help/bundle.py index 48e7caf1..07a19415 100644 --- a/backend/apps/help/bundle.py +++ b/backend/apps/help/bundle.py @@ -170,6 +170,22 @@ async def get_help_knowledge() -> HelpKnowledgeResponse: return build_knowledge_response() +@help_app.router.get("/whats-new") +def whats_new() -> dict: + """The release story for the in-app What's New card. Same words the Help agent and the GitHub + body get, so a user never reads two different accounts of the same release.""" + from backend.apps.help.changelog import as_markdown, latest_release, release_notes + from backend.apps.service.version import APP_VERSION + note = release_notes(APP_VERSION) or latest_release() + return { + "version": note.version, + "headline": note.headline, + "highlights": note.highlights, + "fixes": note.fixes, + "markdown": as_markdown(note), + } + + @help_app.router.post("/bundle") @typechecked async def build_bundle(body: BundleRequest) -> dict: diff --git a/backend/apps/help/changelog.py b/backend/apps/help/changelog.py index 1f557afc..c2e7281a 100644 --- a/backend/apps/help/changelog.py +++ b/backend/apps/help/changelog.py @@ -64,7 +64,7 @@ def latest_release() -> ReleaseNote: @typechecked def as_markdown(note: ReleaseNote) -> str: """The GitHub release body; identical words to the in-app card, so nobody reads two stories.""" - lines = [f"## {note.version} — {note.headline}", ""] + lines = [f"## {note.version}: {note.headline}", ""] if note.highlights: lines.append("### New") lines += [f"- {h}" for h in note.highlights] diff --git a/backend/tests/test_changelog.py b/backend/tests/test_changelog.py index 4140c27b..54bf9e4b 100644 --- a/backend/tests/test_changelog.py +++ b/backend/tests/test_changelog.py @@ -21,6 +21,12 @@ def test_notes_are_written_for_users_not_committers(): assert "—" not in line and "–" not in line, "house style: no em/en dashes" +def test_no_em_dashes_anywhere_in_a_release_body(): + # House rule, and the header was the one place the earlier test did not look. + md = as_markdown(latest_release()) + assert "\u2014" not in md and "\u2013" not in md, "release bodies use plain punctuation" + + def test_markdown_body_carries_the_same_words_as_the_app(): note = latest_release() md = as_markdown(note) diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index 1cbdc6f9..742003d5 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -25,6 +25,7 @@ import { ackRun, runWorkflowNow } from '@/shared/state/workflowsSlice'; import { setPendingBrowserUrl } from '@/shared/state/tempStateSlice'; import { fetchOutputs } from '@/shared/state/outputsSlice'; import UpdateReadyPill from '@/app/components/Layout/UpdateReadyPill'; +import WhatsNewCard from '@/app/components/Layout/WhatsNewCard'; import ShareRequestHost from '@/app/components/share/ShareRequestHost'; import CardContextMenu from '@/app/pages/Dashboard/desktop/CardContextMenu'; import { findBrowserByWebContentsId } from '@/shared/browserRegistry'; @@ -590,6 +591,8 @@ const AppShell: React.FC = () => { {/* Shell-global right-click host (portals to body): chat surfaces render on non-dashboard routes too, so the menu can't live inside DashboardCanvas. */} + + ); diff --git a/frontend/src/app/components/Layout/WhatsNewCard.tsx b/frontend/src/app/components/Layout/WhatsNewCard.tsx new file mode 100644 index 00000000..a366aac0 --- /dev/null +++ b/frontend/src/app/components/Layout/WhatsNewCard.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Fade from '@mui/material/Fade'; +import { API_BASE } from '@/shared/config'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; + +// Shown ONCE per version, right after an update: what actually changed, in the same words the Help +// agent and the GitHub release body carry. A release that ships with no story is a bug the backend +// test catches; a user who never hears about the fix is the bug this card catches. +interface WhatsNew { + version: string; + headline: string; + highlights: string[]; + fixes: string[]; +} + +const SEEN_KEY = 'openswarm.whatsNew.seenVersion'; + +export default function WhatsNewCard(): React.ReactElement | null { + const c = useClaudeTokens(); + const [note, setNote] = React.useState(null); + + React.useEffect(() => { + let cancelled = false; + fetch(`${API_BASE}/help/whats-new`) + .then((r) => (r.ok ? r.json() : null)) + .then((data: WhatsNew | null) => { + if (cancelled || !data?.version) return; + let seen: string | null = null; + try { seen = window.localStorage.getItem(SEEN_KEY); } catch { /* private mode */ } + if (seen === data.version) return; + setNote(data); + }) + .catch(() => {}); + return () => { cancelled = true; }; + }, []); + + const dismiss = React.useCallback(() => { + if (note) { + try { window.localStorage.setItem(SEEN_KEY, note.version); } catch { /* private mode */ } + } + setNote(null); + }, [note]); + + if (!note) return null; + const lines = [...note.highlights.map((t) => ({ t, kind: 'new' })), ...note.fixes.map((t) => ({ t, kind: 'fixed' }))]; + + return ( + + + + {`What's new in ${note.version}`} + + + {note.headline} + + + {lines.slice(0, 5).map((l) => ( + + {l.t} + + ))} + + + + + + + ); +}