Skip to content

fix(webui): show a version on Hub cards for not-yet-installed agents - #3878

Merged
itomek merged 4 commits into
mainfrom
autofix/issue-2970-card
Sep 24, 2026
Merged

itomek merged 4 commits into
mainfrom
autofix/issue-2970-card

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Browsing the Agent Hub, an agent you have not installed yet shows no version anywhere — not on its card, not in its Details panel — even though the catalog already knows which version it would install you. After this change every Hub card carries a version badge: an installed card shows the version you have, a catalog card shows the version you would get, and Details shows a Version row in both cases.

One correction to the latest reopen on the issue: the installed card badge does already work on current main after #3819. A HubPage test driven by the real catalog payload renders v0.6.0 for the installed Email agent with none of this PR's source changes applied — that test ships here as a regression pin. The path that was genuinely dead is the not-yet-installed one.

Refs #2970

Test plan

  • npx vitest run in src/gaia/apps/webui — 349/349 pass (the two new not-yet-installed cases fail without this PR's source change; verified by stashing it)
  • npx tsc --noEmit — clean
  • Real app, before installing: build the webui, run python -m gaia.ui.server, open the Agent Hub — an uninstalled catalog agent's card shows a v<x.y.z> badge, and its Details panel shows a Version row
  • Real app, after installing the Email agent: its installed card shows v0.6.0 and Details still shows Version 0.6.0

⚠️ Needs manual validation — this is frontend-only code, so the automated checks here (frontend suite + typecheck) prove the render logic but cannot exercise a live catalog. A maintainer should run the two real-app steps above before merging. This issue has been closed twice on a green suite while the feature was dead, so please do not take the checkmarks alone.

🔍 Technical details

Root cause. The Available lane passes raw GET /api/agents/catalog entries straight to AgentHubCard / AgentDetailModal (HubPage.tsx:236), with no mergeCatalogStatus pass. Both components read agent.version — a key the backend never sends and that only mergeCatalogStatus writes, only for installed agents (agentHub.ts:74, added by #3819). So for every catalog card the badge condition and the modal's Version row were unreachable, while latest_version sat unused on the object.

Change.

  • utils/agentHub.ts — new displayVersion(agent) returning installed_version ?? version ?? latest_version, i.e. derived from what the wire actually carries rather than from a post-merge field. Neither surface now depends on the merge having run.
  • AgentHubCard.tsx:95 — const version = isAvailable ? agent.latest_version : displayVersion(agent), and the badge drops its !isAvailable gate. The split keeps the semantics honest: a catalog card advertises the version on offer, never an installed one.
  • AgentDetailModal.tsx:44 — version from the helper, used by the Version row, the hasDetails gate that decides whether the DETAILS section renders at all, and the #2965 "measured on vX (current: vY)" scorecard line.

Verification. Fixtures use the exact wire shape (installed_version / latest_version, never version), which is the mock-validity gap that let this through twice:

  • HubPage.test.tsx — three cases through the real component tree: installed card badge (passes pre-change, pins fix(webui): make the Hub Details modal actually show an agent's version #3819), available card badge and available Details Version row (both fail pre-change, confirmed by stashing the source diff).
  • agentHub.test.ts — four displayVersion precedence cases.
  • Full webui suite 349/349, tsc --noEmit clean.
  • Python lint/pytest not run: zero Python files touched, and this environment has no pytest installed. util/lint.py --all reports the same two pre-existing failures (Import Validation, Bandit) with and without this branch.

Relation to #3820. That PR predates #3819 and now overlaps it on three of its five files. This branch is the non-overlapping remainder rewritten against current main; #3820 can be closed.

Browsing the Agent Hub showed no version anywhere for an agent you have
not installed yet — not on its card, not in its Details panel — even
though the catalog sends `latest_version` for exactly that case. The
Available lane renders raw catalog entries, and both surfaces read
`version`, a key only `mergeCatalogStatus` writes and only for installed
agents, so the branch was dead for every catalog card.

A shared `displayVersion` helper now derives the value from the fields
the wire actually carries (`installed_version` -> `version` ->
`latest_version`), and the card badge drops its installed-only gate:
installed cards show what you have, catalog cards show what you'd get.
Deriving it from the wire instead of a post-merge field also means
neither surface depends on the merge pass having run.

The installed-card path #3819 fixed keeps working and now has a HubPage
test pinning it, built from the real wire shape rather than a hand-set
`version`.

Refs #2970
@itomek
itomek marked this pull request as ready for review September 22, 2026 00:17
@itomek itomek added the ready_for_ci Run CI workflows on draft PR without requesting review label Sep 22, 2026
@itomek

itomek commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Revived this and got a real test run on it — the full Agent UI suite passes on the rebased branch. It is now out of draft with ready_for_ci, and #3820 is closed as superseded.

What still needs a human: the live-app check. Nothing automated here can open the Hub with an agent not installed and look at the card, so the two unchecked boxes in the test plan are the merge gate.

🔍 Technical details

Reproduced (executed). gh pr update-branch 3878 (now at c2f3477, mergeable onto main @ 158e2a7), then dispatched test_electron.yml on the branch because the automatic runs all died to a concurrency race — the synchronize and ready_for_review events landed in the same second, so every real job was either cancelled or evaluated while the PR was still a draft. Dispatched run: https://github.com/amd/gaia/actions/runs/35671864089 — all 9 jobs green, Test Agent UI Components (Vitest) → Test Files 48 passed (48), Tests 392 passed (392).

Shape matches (static, not executed) against origin/main @ 158e2a7:

  • Installed lane: HubPage.tsx:227 runs mergeCatalogStatus, which sets version: cat.installed_version (agentHub.ts, from fix(webui): make the Hub Details modal actually show an agent's version #3819) — so the installed card badge (AgentHubCard.tsx:169) and the modal's Version row (AgentDetailModal.tsx:108) both have a value today. The reopen that claimed the installed card was dead was reading HubPage.tsx for "version"; the badge lives in AgentHubCard.
  • Available lane: HubPage.tsx:236 builds it from raw catalog entries with no merge, so version is undefined — and AgentHubCard.tsx:169 additionally gates the badge on !isAvailable. Both surfaces are therefore blank for an uninstalled agent, which is what this PR fixes.
  • catalog.py:477-478 does send latest_version for catalog entries, so the value this PR reads is really on the wire.

@github-actions

Copy link
Copy Markdown
Contributor Author

Verdict: Approve with suggestions

Hub cards for agents you haven't installed finally show a version, and the fix is built on the fields the backend actually sends rather than one the UI invented — which is why this bug survived two "fixed" attempts. Tests pin the real wire shape, and the evidence run confirms the behaviour against a live catalog.

One thing worth a second look before merge: the new "which version do I show?" helper falls back to the catalog's latest when it can't find an installed version. For an agent that's present locally but wasn't installed through the Hub (a pip-installed agent wheel, or a dev checkout), its Installed card will now badge the newest version in the catalog as if you already had it — where today it just shows nothing. A one-line change keeps the badge blank in that case instead of stating a version the user doesn't have. Not a blocker in my view — every other path is strictly better than the blank badge it replaces — but it's the one place this PR can say something untrue.

Real-world evidence

Present and matched to the surface — evidence-bundle.md exercised the two HTTP routes that feed these cards against a live backend, plus the PR's own helper over those real responses.

$ curl -s "http://127.0.0.1:4200/api/agents/catalog"          # HTTP 200
version-ish keys: ['eval_score_version', 'installed_version', 'latest_version']
{"id": "email", "status": "available", "source": "hub", "version": null, "installed_version": null, "latest_version": "0.6.0"}
$ node --experimental-strip-types before_after.mjs
email        tab=Installed before=v0.6.0     after=v0.6.0
agent-ui     tab=Available before=(no badge) after=v0.24.1
gaia         tab=Available before=(no badge) after=v0.2.0
terminal-hub tab=Available before=(no badge) after=v0.24.1

A post-install check (installed_version: "0.6.0") and gaia hub list agreed with the API, and three adjacent agent routes were spot-checked with no change. The rendered screenshot is deferred to the strix-halo lane — the no-inference runner can't produce it. That's the expected CI-lane split and I'm not blocking on it, but since this is a pixel-level change, the PR's two manual real-app steps are still the real proof. My verdict rests on the route/helper evidence above plus static review for the rendering itself.

🔍 Technical details

🟡 displayVersion can badge an installed card with a version the user doesn't have (utils/agentHub.ts:33)

AgentHubCard.tsx:96 deliberately splits catalog vs. installed (isAvailable ? agent.latest_version : displayVersion(agent)) so an installed card never advertises the version on offer — but displayVersion ends in ?? agent.latest_version, which defeats that split for exactly the case it guards.

Reachable path: merge_with_registry marks an agent installed with installed_version: null whenever it's in the registry but has no Hub install sentinel (catalog.py:450) — i.e. an agent registered from a gaia.agent entry point (registry.py:779, the direction #1102 is heading) or an editable dev install. mergeCatalogStatus then yields version: undefined, latest_version: "0.2.0", and both the card badge and the modal's Version row render v0.2.0. AgentDetailModal.tsx:44 has the same exposure with no split at all.

Gate the fallback on status so only a not-yet-installed entry can show the offered version:

export function displayVersion(agent: AgentInfo): string | undefined {
    const installed = agent.installed_version ?? agent.version;
    if (installed) return installed;
    // Never badge an installed card with a version the user doesn't have —
    // ``latest_version`` is what's on offer, not what's on disk.
    const isInstalled = agent.status === 'installed' || agent.status === 'update_available';
    return isInstalled ? undefined : agent.latest_version;
}

With that, AgentHubCard.tsx:96 can drop its ternary and call displayVersion(agent) on both tabs — one rule, one place, and the modal stops diverging from the card.

🟢 Nits

  • AgentDetailModal.tsx:214 — for an available agent, the scorecard line now reads measured on v0.5.0 (current: v0.6.0), where "current" is a version the reader has never installed. "latest" would be truer on that tab. (≤50 words, no suggestion — wording call is yours.)
  • utils/agentHub.ts:34 — the installed_version branch is dead in both call sites today (the Installed grid only ever sees post-mergeCatalogStatus entries, which don't carry it; the Available grid only sees status: 'available'). Harmless and correctly unit-tested, just worth knowing it's defensive rather than load-bearing.

Strengths

  • The fixtures are the fix. HubPage.test.tsx:94 and agentHub.test.ts:78 use only keys the backend really sends (installed_version / latest_version, never version) — this is precisely the mock-validity gap CLAUDE.md calls out, and it's what let fix(ui): email agent Details panel shows no version, empty section #2970 be closed twice on a green suite.
  • Testing through the real component tree (badge and the Details Version row) rather than only the helper, with the pre-change failure confirmed by stashing the source diff.
  • The PR description is honest about what the automated checks can and can't prove, and explicitly asks for the two real-app steps instead of treating checkmarks as sign-off.

Resolves the conflict with main's removal of the never-sent `compatibility`
field (#3846) and tightens the version badge so it can't advertise a version
the user doesn't actually have.

Main deleted `compatLevel`/`compatLabel` and the compatibility dot along with
the dead wire field, so this branch's imports and locals for them go too. The
version badge itself keeps this branch's behaviour: it now renders on both
tabs, derived from the fields the catalog really sends.

The badge rule also gained a guard. An agent installed from a registry entry
point or an editable dev checkout reports no installed version, and the old
chain fell through to the catalog's `latest_version` — badging a card with a
version that isn't on disk. Those cards now show no badge at all, while a
not-yet-installed catalog card still shows what's on offer. Both the card and
the details modal read the single helper, so they can't drift apart, and the
modal's scorecard line says "latest" instead of "current" for an agent the
reader hasn't installed.
@itomek

itomek commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto main and fixed the case you flagged: a Hub card could show a version the user doesn't actually have.

An agent installed from a registry entry point or an editable dev checkout reports no installed version, and the old fallback chain dropped through to the catalog's latest — so the card badged a version that was never on disk. Those cards now show no badge at all. A not-yet-installed catalog card still shows the version on offer, unchanged.

The card and the details modal now both read one helper instead of each deciding for themselves, so they can't drift apart again. The modal's scorecard line also says "latest" rather than "current" when the reader hasn't installed the agent.

Merging main was the other half of this: main removed the compatibility dot, so this branch's leftovers for it are gone too.

Verified in src/gaia/apps/webui: 393 tests across 48 files pass, and the typecheck is clean.

🔍 Technical details

displayVersion in src/gaia/apps/webui/src/utils/agentHub.ts no longer ends in ?? agent.latest_version:

export function displayVersion(agent: AgentInfo): string | undefined {
    const installed = agent.installed_version ?? agent.version;
    if (installed) return installed;
    // Never badge an installed card with a version the user doesn't have —
    // ``latest_version`` is what's on offer, not what's on disk.
    return isInstalledStatus(agent) ? undefined : agent.latest_version;
}

The status === 'installed' || status === 'update_available' test is factored out as an exported isInstalledStatus, since the modal needs the same predicate for its "current"/"latest" wording.

AgentHubCard.tsx drops the isAvailable ? agent.latest_version : displayVersion(agent) ternary and calls displayVersion(agent) on both tabs. That's safe because splitAvailable only ever feeds the Available tab entries with status === 'available', which take the latest_version branch — identical to the old ternary.

Three new cases in src/gaia/apps/webui/src/utils/__tests__/agentHub.test.ts pin it: installed and update_available with a null installed version return undefined, and available still returns latest_version.

Conflict resolution. Conflicts were in AgentHubCard.tsx and agentHub.test.ts, both against #3846, which deleted the never-emitted compatibility field. Resolved by taking main's deletion (dropping the compatLevel/compatLabel imports, locals and tests) and keeping this branch's version-badge work. HubPage.test.tsx auto-merged but kept a compatibility fixture key that no longer typechecks — removed.

Commands run:

npx tsc --noEmit                       # clean
npx vitest run                         # Test Files 48 passed (48) / Tests 393 passed (393)

@itomek

itomek commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Verified in the real app on Linux: an Agent Hub card for an agent you have not installed now shows its version badge, and its Details panel now has a Version row. Before this change both were blank — the card showed only the security-tier and language badges, and the Details panel had no Details section at all.

Checked against the live /api/agents/catalog on a machine where two entries are genuinely uninstalled (GAIA Agent UI and GAIA Terminal Hub, both status: available, latest_version: 0.24.1, installed_version: null). Both render v0.24.1 on the card; opening Details on GAIA Agent UI shows Version 0.24.1. Installed agents are unaffected — Email still badges v0.6.0, its own installed version, not the catalog's latest.

🔍 Technical details

Built the frontend from this branch (head 30db9e80) and served it from the same checkout, with PYTHONPATH pinned so the backend ran this branch's code rather than the editable install:

npm --prefix src/gaia/apps/webui install && npm --prefix src/gaia/apps/webui run build
PYTHONPATH=<worktree>/src python -m gaia.ui.server --port 4200 --host 127.0.0.1
# gaia.__file__ -> <worktree>/src/gaia/__init__.py

Catalog payload from GET /api/agents/catalog:

id            status            latest_version  installed_version  source
agent-ui      available         0.24.1          null               hub
terminal-hub  available         0.24.1          null               hub
email         installed         0.6.0           0.6.0              installed
gaia          installed         0.2.0           null               installed

Before/after was captured by reverting only the three changed source files to main in the same checkout, rebuilding, and re-driving the same page — so the only variable is the diff.

The reason the old code came up blank is in the payload above: the catalog never sends version for an uninstalled entry, only latest_version. version is populated later by mergeCatalogStatus, so the card and modal were reading a field that does not exist yet on a raw catalog entry. displayVersion() now reads installed_version first and falls back to latest_version only when the agent is not installed, which is why the Email badge stays on the version actually on disk.

@itomek itomek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback is gated on status now, so an installed agent with no recorded version shows a blank badge rather than the catalog's latest. Verified in the real app: an uninstalled Hub card renders its version badge and Details shows a Version row, with the before/after taken in the same checkout. Full suite green on the rebased branch.

@itomek
itomek added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit f557930 Sep 24, 2026
44 checks passed
@itomek
itomek deleted the autofix/issue-2970-card branch September 24, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready_for_ci Run CI workflows on draft PR without requesting review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant