mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-26 19:44:51 +02:00
[eric] release: What's New card + endpoint serve the same story as Help and GitHub, header punctuation fixed
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user