fix(canvas/plugins): explicit loading state for Plugins tab + CI regression guards #3132
Reference in New Issue
Block a user
Delete Branch "fix/plugins-tab-loading-state"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The workspace Plugins tab (
canvas/.../SkillsTab.tsx) rendered its empty state ("0 installed", "Registry returned 0 plugins") while the installed-plugins and registry fetches were still in flight, so it looked broken until data arrived a second later (user-reported). This adds explicit loading states and CI guards so the class can't silently regress.Bug 1 — no loading state (fixed)
installedLoadingflag + a reusablePluginSkeletonRowsanimated skeleton (motion-safe:animate-pulse,role=status/aria-busy).loading/empty/errorstates are now cleanly distinguished for every async section.Bug 2 — registry not shown in Install dialog (root cause)
The registry data path (
GET /plugins→ render) and its loading/error/empty states already exist onmain. The literal "no registry available" string the user saw does not exist in the current code — it was an older deployed tenant image. BackendListRegistryreads the platform host'spluginsDir(populated byclone-manifest.sh), independent of the tenant box, so the data path is correct. The remaining "empty-during-load" flash is resolved by Bug 1's fix. A test pins that the dialog lists the registry (name + version + description) when/pluginsreturns entries, alongside the existing install-by-URL path.CI regression guards
SkillsTab.loadingState.test.tsx(4 cases): skeleton while pending (not empty); rows on resolve-with-data; empty/compact pill on resolve-empty; install dialog lists registry entries when/pluginsreturns data.TestPluginInstallLifecycle_Staging: registry non-empty → install plugin on a SaaS workspace →ListInstalledreturns it (guards CP #3125 EIC readback) → workspace back online+routable + serves A2A after the install restart (guards the #159 mgmt-MCP self-heal). Wired as a fail-loud advisory jobE2E Staging Plugin Install Lifecycleine2e-staging-saas.yml.Gating note
The new e2e job runs + fails loud but is not yet branch-protection required. To gate it (owner action), add
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycleto.gitea/required-contexts.txtand tobranch_protections/mainstatus_check_contexts — same two-step the platform-boot gate documents. The job comment records this.Tests
SkillsTab.compactEmpty+ skill tests: 36/36 pass. Full canvas suite: 3472/3472 tests pass (2 suite files error locally only — missing@novnc/novncoptional dep in local node_modules, unrelated to this change; green in CI).tsc --noEmit: zero new errors in SkillsTab (repo baseline has 229 pre-existing test-file TS errors, unchanged).go vet -tags staging_e2e ./internal/staginge2e/: clean. Test skips correctly withoutSTAGING_E2E=1.Deploy / verify
Tenant UI ships via the platform-tenant image build → a tenant must be re-provisioned/redeployed to pick up the canvas change. Verify on a fresh tenant: open a workspace → Plugins tab → on first paint the installed list + Install-dialog registry show animated skeletons, resolving to data/empty/error cleanly (no "0 installed" flash).
🤖 Generated with Claude Code
The workspace Plugins tab (SkillsTab) rendered its EMPTY state ("0 installed", "Registry returned 0 plugins") while the installed-plugins and registry fetches were still in flight, so the tab looked broken until data arrived a second later (user-reported). Bug 1 (loading state): add an `installedLoading` flag + a `PluginSkeletonRows` animated skeleton (motion-safe, aria-busy / role=status) so each async section shows an explicit loading affordance while pending, and only falls back to the empty state after the fetch resolves with zero results. The installed section had no loading state at all (it painted the "0 installed" header during load); the registry section's plain "Loading registry…" text is upgraded to the same skeleton. Bug 2 (registry not shown in the Install dialog): the registry data path (GET /plugins → render) and loading/error/empty states already exist on main (the "no registry available" string the user saw is from an older deployed image). Confirmed the dialog lists the registry; the remaining "empty-during-load" flash is resolved by Bug 1's fix. Added test coverage pinning that the dialog shows the registry list when /plugins returns entries. CI regression guards: - canvas vitest (SkillsTab.loadingState.test.tsx): asserts skeleton while pending (not empty), rows on resolve-with-data, empty/compact pill on resolve-empty, and the install dialog lists registry entries when /plugins returns data. - workspace-server staging e2e (TestPluginInstallLifecycle_Staging): registry non-empty → install plugin on a SaaS workspace → ListInstalled returns it (guards CP #3125 EIC readback) → workspace back online+routable + serves A2A after the install restart (guards the #159 mgmt-MCP self-heal). Wired as a fail-loud (advisory) job in e2e-staging-saas.yml; to gate it, add the context to required-contexts.txt + branch protection (owner action, noted in the job comment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Reviewed: SkillsTab adds installedLoading + PluginSkeletonRows (animated, aria-busy) so loading is distinct from empty — fixes the '0 installed / no registry' flash; registry data path was already correct (the flash was the cause). vitest covers loading/data/empty + registry-list. New e2e (TestPluginInstallLifecycle_Staging) guards #3125 (ListInstalled SaaS) + #159 (stays online post-install), fail-loud, bp-required: pending #3133. LGTM.
Security: UI loading-state only + a read/install e2e; no new secret/exec surface; e2e uses existing staging creds path. LGTM.
Fresh current-head review for molecule-core#3132 @
0a5bfd99dc.APPROVED.
I reviewed the Plugins tab loading-state change, the vitest regression coverage, and the staging plugin lifecycle guard.
Correctness: installed plugins now track an explicit in-flight state and render skeleton rows before the first installed fetch resolves, so the UI no longer paints the "0 installed" empty affordance during initial loading. The registry path similarly renders a skeleton while /plugins is pending and only shows the zero-registry banner after an actual empty response. Registry rows still render with name/version/description once returned.
Robustness: the loading flag is set around loadInstalled and cleared in finally with the mounted guard, preserving the existing installedLoaded empty-state semantics. The tests cover pending installed load, resolved empty, resolved installed data, pending registry, registry-with-data display, and resolved-empty registry.
Security/performance/readability: no auth/secret surface changes in the canvas path; the added staging e2e uses existing staging harness/admin cleanup and is isolated behind staging_e2e. The UI change is small and readable.
CI/gates: existing code-review and security approvals are present; this approval is intended to satisfy qa-review/approved for the current head.
5-axis review on current head
0a5bfd99dc: APPROVED.Correctness: The Plugins tab now has an explicit installed-plugins loading state, so initial in-flight
/workspaces/:id/pluginsno longer renders as a genuine empty state. Registry loading similarly renders skeleton rows until/pluginsresolves, then shows registry rows or the empty-registry banner only after resolution. The new vitest covers the regression paths: installed pending vs empty, installed data, registry pending vs populated, and registry empty-after-resolve.Robustness: The installed loading flag is set before each fetch and cleared in
finally, guarded bymountedRef; registry loading/error behavior remains separate and retryable. The staging plugin lifecycle E2E exercises the server-side registry non-empty/readback/restart-online path without changing production code.Security: UI-only loading-state changes do not widen auth, plugin install permissions, CSP, or secret handling. The staging E2E uses the existing admin-token secret and tenant admin paths in the established staging_e2e pattern.
Performance: Skeleton rendering is trivial; no extra polling or hot-path work added to the canvas. The new live E2E is advisory/pending-required as documented, not part of branch protection in this PR.
Readability: The loading-state branch and
PluginSkeletonRowshelper are clear and localized. The regression test is direct and documents the user-visible failure mode it protects.