From 9d8fe2e5451b36f31313310abaeb623ab6dcbc77 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 7 Aug 2026 00:45:06 -0700 Subject: [PATCH] [eric] release: one changelog source feeds the Help agent, the GitHub body, and the in-app card --- backend/apps/help/changelog.py | 90 +++++++++++++++++++++++++++++++++ backend/apps/help/knowledge.py | 8 +++ backend/tests/test_changelog.py | 43 ++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 backend/apps/help/changelog.py create mode 100644 backend/tests/test_changelog.py diff --git a/backend/apps/help/changelog.py b/backend/apps/help/changelog.py new file mode 100644 index 00000000..1f557afc --- /dev/null +++ b/backend/apps/help/changelog.py @@ -0,0 +1,90 @@ +"""The release story, in one place, for three surfaces: the in-app What's New card, the GitHub +release body, and the Help agent's context. One source means the agent can never answer from a +stale picture of the app, and a release can never ship with no story.""" + +from typing import Dict, List + +from pydantic import BaseModel, ConfigDict +from typeguard import typechecked + + +class ReleaseNote(BaseModel): + model_config = ConfigDict(validate_assignment=True) + version: str + headline: str + # User-facing lines only: what changed for the person using the app, not the diff. + highlights: List[str] + fixes: List[str] + + +P_RELEASES: List[ReleaseNote] = [ + ReleaseNote( + version="1.7.5", + headline="Fewer dead ends, and the app tells us when something breaks.", + highlights=[ + "Scrolling inside any panel, chat, or browser stays in that panel instead of dragging the canvas.", + "Clicking the Marketplace window frames it like every other window, and camera moves land softly instead of snapping.", + "While a browser agent works you see the live mini browser only, not the same action log twice.", + ], + fixes=[ + "Dictation lands in the field you started in, even if you click elsewhere while it is still transcribing.", + "The first message after opening a chat reuses the warmed-up connection, so it answers sooner.", + "A provider hiccup that fixes itself no longer shows a scary reconnect card.", + "Crashes, freezes, and runaway memory now report themselves, so bugs get diagnosed instead of guessed at.", + ], + ), + ReleaseNote( + version="1.7.4", + headline="Chats survive a hiccup instead of stopping.", + highlights=[ + "A dropped local connection retries and resumes the same answer instead of failing the message.", + "The spawn composer steps aside when a window is open.", + ], + fixes=[ + "App previews reconnect on their own after a backend restart.", + "Dictation cue sounds default to a level you can actually hear.", + ], + ), +] + + +@typechecked +def release_notes(version: str) -> ReleaseNote | None: + for note in P_RELEASES: + if note.version == version: + return note + return None + + +@typechecked +def latest_release() -> ReleaseNote: + return P_RELEASES[0] + + +@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}", ""] + if note.highlights: + lines.append("### New") + lines += [f"- {h}" for h in note.highlights] + lines.append("") + if note.fixes: + lines.append("### Fixed") + lines += [f"- {f}" for f in note.fixes] + return "\n".join(lines).strip() + + +@typechecked +def help_context_block(app_version: str) -> str: + """What the Help agent must know about what just changed, so "what's new" is never stale.""" + note = release_notes(app_version) or latest_release() + body = [f"Version {note.version}: {note.headline}"] + body += [f"- new: {h}" for h in note.highlights] + body += [f"- fixed: {f}" for f in note.fixes] + return "\n".join(body) + + +@typechecked +def all_versions() -> Dict[str, str]: + return {n.version: n.headline for n in P_RELEASES} diff --git a/backend/apps/help/knowledge.py b/backend/apps/help/knowledge.py index 78db0d59..10778761 100644 --- a/backend/apps/help/knowledge.py +++ b/backend/apps/help/knowledge.py @@ -136,6 +136,9 @@ def p_issues_block() -> str: @typechecked +from backend.apps.help.changelog import help_context_block + + def build_system_prompt(shortcuts: List[HelpShortcut], app_version: str) -> str: shortcut_lines = "\n".join(f"- {s.keys}: {s.action}" for s in shortcuts) os_name = "macOS" if IS_MAC else platform.system() @@ -158,6 +161,11 @@ def build_system_prompt(shortcuts: List[HelpShortcut], app_version: str) -> str: shortcut_lines, "", "", + "", + "What actually changed in this build. Answer \"what's new\" from THIS, never from memory.", + help_context_block(app_version), + "", + "", "", "The complete list of issues shipped with this build. You cannot see live bug reports.", p_issues_block(), diff --git a/backend/tests/test_changelog.py b/backend/tests/test_changelog.py new file mode 100644 index 00000000..4140c27b --- /dev/null +++ b/backend/tests/test_changelog.py @@ -0,0 +1,43 @@ +"""One release story, three surfaces. A release with no story, or a Help agent answering from a +stale picture of the app, are both bugs this pins shut.""" + +from backend.apps.help.changelog import ( + all_versions, as_markdown, help_context_block, latest_release, release_notes, +) +from backend.apps.service.version import APP_VERSION + + +def test_the_shipping_version_has_a_story(): + note = release_notes(APP_VERSION) + assert note is not None, f"{APP_VERSION} ships with no release notes; write them before tagging" + assert note.headline and (note.highlights or note.fixes) + + +def test_notes_are_written_for_users_not_committers(): + for note in (latest_release(),): + for line in note.highlights + note.fixes: + assert not line.startswith("["), "no commit-style prefixes" + assert "commit" not in line.lower() and "refactor" not in line.lower() + assert "—" not in line and "–" not in line, "house style: no em/en dashes" + + +def test_markdown_body_carries_the_same_words_as_the_app(): + note = latest_release() + md = as_markdown(note) + assert note.headline in md + for line in note.highlights + note.fixes: + assert line in md, "the GitHub body must not drift from the in-app card" + + +def test_help_context_names_the_version_and_its_changes(): + block = help_context_block(APP_VERSION) + assert APP_VERSION in block + note = release_notes(APP_VERSION) + assert note is not None + assert note.highlights[0] in block + + +def test_unknown_version_falls_back_to_the_latest_story_not_silence(): + assert release_notes("0.0.0") is None + assert latest_release().version in help_context_block("0.0.0") + assert len(all_versions()) >= 2