Settings: a section per plugin, and call the folder Spam

Plugin settings pages were stacked at the bottom of Settings -> Accounts, which
put antispam's thresholds and lists under a heading about mail accounts. Each
`settings.page` now gets its own entry in the Settings nav: "Antispam filtering"
and "Mail filters".

Slot gains an optional `title` for this — the host needs a label when it gives a
slot an area of its own, and falls back to the plugin name. PluginSlot gains an
optional `plugin` filter so a host can render one plugin's bundle rather than
every binding for a slot name.

Wording: the app calls that folder Spam (i18n `nav.spam`), so the plugin's copy
now does too — "filed into Spam", "Move to Spam", "marked as spam". The IMAP
special-use stays `junk`, which is what the protocol calls it; only what the
user reads changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
2026-09-17 08:19:11 +02:00
co-authored by Claude Opus 5
parent fe471dbf01
commit 6414504f6e
11 changed files with 71 additions and 21 deletions
+5 -4
View File
@@ -96,7 +96,10 @@ def mount_routers(app: FastAPI) -> None:
def _slot_dict(slot: Slot) -> dict[str, str]:
return {"name": slot.name, "bundle": slot.bundle}
d = {"name": slot.name, "bundle": slot.bundle}
if slot.title:
d["title"] = slot.title
return d
def _page_dict(page: Page) -> dict[str, Any]:
@@ -120,9 +123,7 @@ def _manifest_dict(plugin: Plugin, manifest: UiManifest) -> dict[str, Any]:
def build_ui_manifest() -> dict[str, Any]:
"""Aggregate every plugin's UI manifest for the frontend loader (SPEC §11.3)."""
plugins = [
_manifest_dict(p, p.ui_manifest) for p in get_plugins() if p.ui_manifest is not None
]
plugins = [_manifest_dict(p, p.ui_manifest) for p in get_plugins() if p.ui_manifest is not None]
return {"plugins": plugins}
+4
View File
@@ -38,6 +38,10 @@ class Slot:
name: str
bundle: str # path served by the plugin router, resolved by the frontend loader
# Human label for slots the host renders as a named area of its own — today
# `settings.page`, which becomes a section in Settings rather than being
# stacked under someone else's. Falls back to the plugin's name.
title: str | None = None
def __post_init__(self) -> None:
if self.name not in SLOT_NAMES: