[eric] release: What's New card + endpoint serve the same story as Help and GitHub, header punctuation fixed

This commit is contained in:
ciregenz
2026-08-07 01:05:05 -07:00
parent a7fd7168ec
commit 3324d63ab1
5 changed files with 107 additions and 1 deletions
+16
View File
@@ -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:
+1 -1
View File
@@ -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]
+6
View File
@@ -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)
@@ -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. */}
<CardContextMenu />
<WhatsNewCard />
</Box>
);
@@ -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<WhatsNew | null>(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 (
<Fade in timeout={{ enter: 260, exit: 200 }}>
<Box
data-select-type="whats-new"
sx={{
position: 'fixed', bottom: 24, right: 24, zIndex: 1450, width: 380, maxWidth: '90vw',
bgcolor: c.bg.surface, border: `1px solid ${c.border.medium}`, borderRadius: '14px',
boxShadow: '0 18px 44px rgba(0,0,0,0.28)', p: 2,
}}
>
<Typography sx={{ fontSize: c.font.size.sm, color: c.text.muted, mb: 0.25 }}>
{`What's new in ${note.version}`}
</Typography>
<Typography sx={{ fontSize: c.font.size.base, fontWeight: 600, color: c.text.primary, mb: 1.25 }}>
{note.headline}
</Typography>
<Box component="ul" sx={{ m: 0, pl: 2, display: 'flex', flexDirection: 'column', gap: 0.75 }}>
{lines.slice(0, 5).map((l) => (
<Typography key={l.t} component="li" sx={{ fontSize: c.font.size.sm, color: c.text.secondary, lineHeight: 1.5 }}>
{l.t}
</Typography>
))}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1.5 }}>
<Button size="small" onClick={dismiss} sx={{ color: c.accent.primary, fontWeight: 600, textTransform: 'none' }}>
Got it
</Button>
</Box>
</Box>
</Fade>
);
}