Implements the user-facing half of #10, on top of the already-merged signed-catalog core (#61/#65). Follows the final design from the 2026-07-12 "Decision + final design" comment on #10.
What's here
bcc_core.py (pure, GUI-free, per the design comment):
CATALOG_CATEGORY_GROUPS / catalog_category_group() — collapses the raw 20-value taxonomy to the 7 UI chips (Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other). Unknown categories fall back to Other rather than dropping the entry.
format_freshness_hint() — last_release ISO date → "Last updated N months/years ago"; "" when missing/unparseable/in the future.
first_unfilled_focus_target() — finds the first <PLACEHOLDER> arg or blank env_required key so the GUI can focus it right after Add.
load_bundled_catalog_entries() — reads+verifies+validates the bundled catalog.json/.sig pair; returns the servers list or an empty list on any failure (missing file, bad signature, invalid schema). This is the load-bearing guarantee behind the dialog's "Catalog unavailable" empty state — never a half-trusted list.
Bug fix in already-merged code:catalog_entry_to_paste_json() only copied config.env, ignoring env_required — but in the shipped seed data, env_required is where 9 of 19 entries (postgres, github, notion, obsidian, brave-search, tavily, home-assistant, n8n, grafana) actually declare their secret var names; config.env is essentially unused for that purpose. Clicking Add on any of these would have silently produced a server with no env field at all for the user to fill in. Fixed to seed env_required keys as empty-string placeholders unless config.env already sets them (never overwrites a real value, still never ships a real secret — validate_catalog already enforces env_required values must be "").
bcc.py:
BrowseCatalogDialog — search box, category chips, list (display/description/category/★/✓ official), detail pane (description, notes, homepage link, freshness hint, and the exact command rendered verbatim). Per-tier action: Add (basic) routes through the existing _import_server path so the duplicate-name prompt (#8) applies for free; Open setup docs (link-only) calls QDesktopServices.openUrl and never auto-adds, and doesn't close the dialog.
Rendering safety: every catalog-derived string goes through a plain_label() helper (Qt.PlainText + html.escape, matching catalog_console.py's existing approach) or an inherently-plain QPlainTextEdit for the command preview — catalog.json takes community PRs, so every field is attacker-influenceable even though it's signed.
"Browse catalog…" button next to Paste JSON…
ServerEditor.focus_target() — selects the first unfilled arg line, or opens the env-table cell editor on the first blank env row, after a catalog Add.
Save-time placeholder guard: warns (Yes/No, defaults No) if any server still has an unfilled <PLACEHOLDER> — never blocks a deliberate save, never lets one slip through unnoticed.
bcc.spec: added data/catalog.json.sig to datas — it was missing, so a frozen build would have shipped a catalog.json with no matching .sig for resolve_catalog() to verify against, meaning the bundled catalog would always fail signature verification in a real build.
Tests
+82 tests in tests/test_core.py: category-group mapping (all 19 taxonomy values + unknown fallback + case-insensitivity), catalog search/filter, freshness-hint formatting (including the 23-vs-24-month rounding boundary), first_unfilled_focus_target, the env_required fix (incl. a regression against the real shipped postgres entry), and load_bundled_catalog_entries under valid/tampered-byte/wrong-key/missing-file/invalid-but-signed conditions, plus an end-to-end check against the real data/catalog.json + .sig (19 entries, signature verifies).
Status: 374 passed, 1 skipped (pre-existing). ruff check . and ruff format --check . both clean.
What I could not verify
PySide6 cannot import in this build sandbox (ImportError: libEGL.so.1, no system GL libs, no root to install them). So BrowseCatalogDialog, ServerEditor.focus_target, and MainWindow.browse_catalog are not exercised at runtime — only py_compile/ast.parse-checked and reviewed by hand. All decision logic they depend on (filtering, grouping, freshness, focus-target selection, catalog loading) is pushed into bcc_core.py and is unit-tested. Please eyeball the dialog live before shipping, especially:
The two-line QListWidgetItem row text (display\ndescription · group) — I couldn't confirm Qt renders the embedded \n the way I expect across platforms; worth a visual check.
ServerEditor.focus_target's QTextCursor line-selection for the args page, and the env table's editItem() call opening the inline editor as expected.
Overall dialog sizing/splitter proportions (880×560, list 360 / detail 480).
Out of scope (per the task)
Remote fetch/cache (#61 follow-up), and no changes to catalog_console.py, catalog_review.py, scripts/, workflows, or data/catalog.json itself.
Implements the user-facing half of #10, on top of the already-merged signed-catalog core (#61/#65). Follows the final design from the 2026-07-12 "Decision + final design" comment on #10.
## What's here
**`bcc_core.py`** (pure, GUI-free, per the design comment):
- `CATALOG_CATEGORY_GROUPS` / `catalog_category_group()` — collapses the raw 20-value taxonomy to the 7 UI chips (Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other). Unknown categories fall back to Other rather than dropping the entry.
- `catalog_entry_matches_query()` / `filter_catalog_entries()` / `catalog_entries_in_group()` — search-as-you-type (id/display/description/category) + category-chip filtering.
- `format_freshness_hint()` — `last_release` ISO date → "Last updated N months/years ago"; `""` when missing/unparseable/in the future.
- `first_unfilled_focus_target()` — finds the first `<PLACEHOLDER>` arg or blank `env_required` key so the GUI can focus it right after Add.
- `load_bundled_catalog_entries()` — reads+verifies+validates the bundled `catalog.json`/`.sig` pair; returns the servers list or an **empty list on any failure** (missing file, bad signature, invalid schema). This is the load-bearing guarantee behind the dialog's "Catalog unavailable" empty state — never a half-trusted list.
**Bug fix in already-merged code:** `catalog_entry_to_paste_json()` only copied `config.env`, ignoring `env_required` — but in the shipped seed data, `env_required` is where 9 of 19 entries (postgres, github, notion, obsidian, brave-search, tavily, home-assistant, n8n, grafana) actually declare their secret var names; `config.env` is essentially unused for that purpose. Clicking Add on any of these would have silently produced a server with **no env field at all** for the user to fill in. Fixed to seed `env_required` keys as empty-string placeholders unless `config.env` already sets them (never overwrites a real value, still never ships a real secret — `validate_catalog` already enforces `env_required` values must be `""`).
**`bcc.py`**:
- `BrowseCatalogDialog` — search box, category chips, list (display/description/category/★/✓ official), detail pane (description, notes, homepage link, freshness hint, and the **exact command rendered verbatim**). Per-tier action: `Add` (basic) routes through the existing `_import_server` path so the duplicate-name prompt (#8) applies for free; `Open setup docs` (link-only) calls `QDesktopServices.openUrl` and never auto-adds, and doesn't close the dialog.
- **Rendering safety**: every catalog-derived string goes through a `plain_label()` helper (`Qt.PlainText` + `html.escape`, matching `catalog_console.py`'s existing approach) or an inherently-plain `QPlainTextEdit` for the command preview — `catalog.json` takes community PRs, so every field is attacker-influenceable even though it's signed.
- "Browse catalog…" button next to Paste JSON…
- `ServerEditor.focus_target()` — selects the first unfilled arg line, or opens the env-table cell editor on the first blank env row, after a catalog Add.
- Save-time placeholder guard: warns (Yes/No, defaults No) if any server still has an unfilled `<PLACEHOLDER>` — never blocks a deliberate save, never lets one slip through unnoticed.
**`bcc.spec`**: added `data/catalog.json.sig` to `datas` — it was missing, so a frozen build would have shipped a `catalog.json` with no matching `.sig` for `resolve_catalog()` to verify against, meaning the bundled catalog would always fail signature verification in a real build.
## Tests
+82 tests in `tests/test_core.py`: category-group mapping (all 19 taxonomy values + unknown fallback + case-insensitivity), catalog search/filter, freshness-hint formatting (including the 23-vs-24-month rounding boundary), `first_unfilled_focus_target`, the `env_required` fix (incl. a regression against the real shipped `postgres` entry), and `load_bundled_catalog_entries` under valid/tampered-byte/wrong-key/missing-file/invalid-but-signed conditions, plus an end-to-end check against the real `data/catalog.json` + `.sig` (19 entries, signature verifies).
**Status: 374 passed, 1 skipped (pre-existing). `ruff check .` and `ruff format --check .` both clean.**
## What I could not verify
PySide6 cannot import in this build sandbox (`ImportError: libEGL.so.1`, no system GL libs, no root to install them). So `BrowseCatalogDialog`, `ServerEditor.focus_target`, and `MainWindow.browse_catalog` are **not exercised at runtime** — only `py_compile`/`ast.parse`-checked and reviewed by hand. All decision logic they depend on (filtering, grouping, freshness, focus-target selection, catalog loading) is pushed into `bcc_core.py` and is unit-tested. Please eyeball the dialog live before shipping, especially:
- The two-line `QListWidgetItem` row text (`display\ndescription · group`) — I couldn't confirm Qt renders the embedded `\n` the way I expect across platforms; worth a visual check.
- `ServerEditor.focus_target`'s `QTextCursor` line-selection for the args page, and the `env` table's `editItem()` call opening the inline editor as expected.
- Overall dialog sizing/splitter proportions (880×560, list 360 / detail 480).
## Out of scope (per the task)
Remote fetch/cache (#61 follow-up), and no changes to `catalog_console.py`, `catalog_review.py`, `scripts/`, workflows, or `data/catalog.json` itself.
Adds the user-facing half of the server catalog: a searchable, signed
catalog browser wired to the existing paste/import path.
bcc_core.py (pure, GUI-free, per the design comment on #10):
- CATALOG_CATEGORY_GROUPS / catalog_category_group(): collapses the
raw taxonomy to the 7 UI chips (unknown categories fall back to
Other, never drop an entry).
- catalog_entry_matches_query() / filter_catalog_entries() /
catalog_entries_in_group(): search-as-you-type + category chip
filtering over catalog entries.
- format_freshness_hint(): last_release ISO date -> "Last updated N
months/years ago", '' when missing/unparseable/future.
- first_unfilled_focus_target(): finds the first <PLACEHOLDER> arg or
blank env_required key so the GUI can focus it after Add.
- load_bundled_catalog_entries(): reads+verifies+validates the
bundled catalog.json/.sig pair, returns servers or an EMPTY list on
ANY failure -- the load-bearing guarantee behind the dialog's empty
state.
- Bug fix: catalog_entry_to_paste_json() only copied config.env,
ignoring env_required -- the field where 9 of the 19 seed entries
(postgres, github, notion, obsidian, brave-search, tavily,
home-assistant, n8n, grafana) actually declare their secret var
names. Add would have silently added these servers with no env
field for the user to fill in. Now env_required keys are seeded as
empty-string placeholders unless config.env already sets them.
bcc.py:
- BrowseCatalogDialog: search box, category chips, list, detail pane
(description/notes/homepage/freshness hint/verbatim command
preview), per-tier action (Add for basic, Open setup docs for
link-only). Every catalog-derived string goes through plain_label()
(Qt.PlainText + html.escape, matching catalog_console.py's
approach) or an inherently-plain QPlainTextEdit for the command
preview -- catalog.json takes community PRs, so every field is
attacker-influenceable.
- "Browse catalog…" button next to Paste JSON.
- ServerEditor.focus_target(): selects the first unfilled arg line or
opens the env-table cell editor for the first blank env row.
- Save-time placeholder guard: warns (Yes/No, defaults No) when any
server still has an unfilled <PLACEHOLDER>; never blocks a
deliberate save, never saves one unnoticed.
bcc.spec: bundle data/catalog.json.sig alongside catalog.json -- the
signature file was missing from datas, so a frozen build would have
had a catalog.json with no matching .sig for resolve_catalog() to
verify against.
tests/test_core.py: +82 tests covering category-group mapping,
catalog search/filter, freshness-hint formatting (including the
month/year rounding boundary), first_unfilled_focus_target, the
catalog_entry_to_paste_json env_required fix (incl. a regression
against the real shipped postgres entry), and
load_bundled_catalog_entries under valid/tampered/wrong-key/missing-
file/invalid-but-signed conditions plus an end-to-end check against
the real data/catalog.json + .sig (19 entries).
GUI code (BrowseCatalogDialog, ServerEditor.focus_target,
MainWindow.browse_catalog) is unavoidably untested here -- PySide6
cannot import in this sandbox (missing libEGL/system GL libs) -- but
all the logic it depends on is pushed into bcc_core.py and covered
above.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements the user-facing half of #10, on top of the already-merged signed-catalog core (#61/#65). Follows the final design from the 2026-07-12 "Decision + final design" comment on #10.
What's here
bcc_core.py(pure, GUI-free, per the design comment):CATALOG_CATEGORY_GROUPS/catalog_category_group()— collapses the raw 20-value taxonomy to the 7 UI chips (Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other). Unknown categories fall back to Other rather than dropping the entry.catalog_entry_matches_query()/filter_catalog_entries()/catalog_entries_in_group()— search-as-you-type (id/display/description/category) + category-chip filtering.format_freshness_hint()—last_releaseISO date → "Last updated N months/years ago";""when missing/unparseable/in the future.first_unfilled_focus_target()— finds the first<PLACEHOLDER>arg or blankenv_requiredkey so the GUI can focus it right after Add.load_bundled_catalog_entries()— reads+verifies+validates the bundledcatalog.json/.sigpair; returns the servers list or an empty list on any failure (missing file, bad signature, invalid schema). This is the load-bearing guarantee behind the dialog's "Catalog unavailable" empty state — never a half-trusted list.Bug fix in already-merged code:
catalog_entry_to_paste_json()only copiedconfig.env, ignoringenv_required— but in the shipped seed data,env_requiredis where 9 of 19 entries (postgres, github, notion, obsidian, brave-search, tavily, home-assistant, n8n, grafana) actually declare their secret var names;config.envis essentially unused for that purpose. Clicking Add on any of these would have silently produced a server with no env field at all for the user to fill in. Fixed to seedenv_requiredkeys as empty-string placeholders unlessconfig.envalready sets them (never overwrites a real value, still never ships a real secret —validate_catalogalready enforcesenv_requiredvalues must be"").bcc.py:BrowseCatalogDialog— search box, category chips, list (display/description/category/★/✓ official), detail pane (description, notes, homepage link, freshness hint, and the exact command rendered verbatim). Per-tier action:Add(basic) routes through the existing_import_serverpath so the duplicate-name prompt (#8) applies for free;Open setup docs(link-only) callsQDesktopServices.openUrland never auto-adds, and doesn't close the dialog.plain_label()helper (Qt.PlainText+html.escape, matchingcatalog_console.py's existing approach) or an inherently-plainQPlainTextEditfor the command preview —catalog.jsontakes community PRs, so every field is attacker-influenceable even though it's signed.ServerEditor.focus_target()— selects the first unfilled arg line, or opens the env-table cell editor on the first blank env row, after a catalog Add.<PLACEHOLDER>— never blocks a deliberate save, never lets one slip through unnoticed.bcc.spec: addeddata/catalog.json.sigtodatas— it was missing, so a frozen build would have shipped acatalog.jsonwith no matching.sigforresolve_catalog()to verify against, meaning the bundled catalog would always fail signature verification in a real build.Tests
+82 tests in
tests/test_core.py: category-group mapping (all 19 taxonomy values + unknown fallback + case-insensitivity), catalog search/filter, freshness-hint formatting (including the 23-vs-24-month rounding boundary),first_unfilled_focus_target, theenv_requiredfix (incl. a regression against the real shippedpostgresentry), andload_bundled_catalog_entriesunder valid/tampered-byte/wrong-key/missing-file/invalid-but-signed conditions, plus an end-to-end check against the realdata/catalog.json+.sig(19 entries, signature verifies).Status: 374 passed, 1 skipped (pre-existing).
ruff check .andruff format --check .both clean.What I could not verify
PySide6 cannot import in this build sandbox (
ImportError: libEGL.so.1, no system GL libs, no root to install them). SoBrowseCatalogDialog,ServerEditor.focus_target, andMainWindow.browse_catalogare not exercised at runtime — onlypy_compile/ast.parse-checked and reviewed by hand. All decision logic they depend on (filtering, grouping, freshness, focus-target selection, catalog loading) is pushed intobcc_core.pyand is unit-tested. Please eyeball the dialog live before shipping, especially:QListWidgetItemrow text (display\ndescription · group) — I couldn't confirm Qt renders the embedded\nthe way I expect across platforms; worth a visual check.ServerEditor.focus_target'sQTextCursorline-selection for the args page, and theenvtable'seditItem()call opening the inline editor as expected.Out of scope (per the task)
Remote fetch/cache (#61 follow-up), and no changes to
catalog_console.py,catalog_review.py,scripts/, workflows, ordata/catalog.jsonitself.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.