UI: nav tabs (Workspaces / + New / Providers) don't re-render the view on switch #1

Closed
opened 2026-05-11 22:28:17 +02:00 by lz · 0 comments
Owner

Symptom

Clicking the Providers tab from Workspaces correctly switches to the providers view. Clicking Workspaces afterwards leaves the providers view on screen — the workspaces list never renders. Same problem in the other direction with + New. The active-tab highlight in the nav does move, so the click handler fires; only the view body fails to repaint.

Reproduce

  1. Open http://127.0.0.1:3001/ on a logged-in session.
  2. Land on Workspaces (default view).
  3. Click Providers → providers view renders correctly.
  4. Click Workspaces → nav highlight moves back, but the providers view stays on screen.
  5. Click + New → same: nav moves, view doesn't.

Likely cause

nexus/web/src/views/workers.ts (post-redesign, diff-based renderer) caches module-level state across calls:

let shellRoot: HTMLElement | null = null;
// ...
if (shellRoot !== root || !listEl) { /* build shell */ }

The intent was to keep shellRoot stable across the 10s poll so the section header isn't rewritten and so per-workspace cards survive in cardSlots. But when the user navigates Providers → Workspaces, renderConnections has already done root.innerHTML = … on the same appEl() and discarded the workspaces shell. On return, shellRoot === root is still true (same DOM node), listEl is still a reference but its element was detached by the providers render, so the early-return path skips rebuilding the shell — yet listEl no longer lives inside root. Net effect: the section header / list container exist as orphan nodes in memory, the visible DOM still shows providers, and the diff loop appends new cards into a detached tree.

The same trap may apply to cardSlots (entries still satisfy slot.el.isConnected === false after navigation away and back, depending on which check runs first). Need to verify against the actual paths.

Fix sketch

Treat any view-switch as a "drop my cached state". Either:

  • In main.ts setView, reset module-level state in the views that have caches (workers.ts exports a resetWorkersView() hook). Simple, explicit.
  • Or: at the top of renderWorkers, check shellRoot && shellRoot.parentElement === root.parentElement && listEl?.isConnected. If any fail, drop the cache and rebuild the shell. Slightly more defensive.

Same audit needed for nexus/web/src/views/sessions.ts which keeps a WeakMap<HTMLElement, MountState> — the WeakMap key is the mount node, which is freed when navigation discards it, so this one should self-clean. Worth verifying.

Reference

  • Files involved: nexus/web/src/views/workers.ts, nexus/web/src/main.ts, nexus/web/src/views/sessions.ts.
  • Triggered by the "UI Redesign: Workspaces + Empty-Container Spawn + Flicker Fix" plan.
## Symptom Clicking the **Providers** tab from **Workspaces** correctly switches to the providers view. Clicking **Workspaces** afterwards leaves the providers view on screen — the workspaces list never renders. Same problem in the other direction with **+ New**. The active-tab highlight in the nav *does* move, so the click handler fires; only the view body fails to repaint. ## Reproduce 1. Open http://127.0.0.1:3001/ on a logged-in session. 2. Land on **Workspaces** (default view). 3. Click **Providers** → providers view renders correctly. 4. Click **Workspaces** → nav highlight moves back, but the providers view stays on screen. 5. Click **+ New** → same: nav moves, view doesn't. ## Likely cause `nexus/web/src/views/workers.ts` (post-redesign, diff-based renderer) caches module-level state across calls: ```ts let shellRoot: HTMLElement | null = null; // ... if (shellRoot !== root || !listEl) { /* build shell */ } ``` The intent was to keep `shellRoot` stable across the 10s poll so the section header isn't rewritten and so per-workspace cards survive in `cardSlots`. But when the user navigates **Providers → Workspaces**, `renderConnections` has already done `root.innerHTML = …` on the same `appEl()` and discarded the workspaces shell. On return, `shellRoot === root` is still true (same DOM node), `listEl` is still a reference but its element was detached by the providers render, so the early-return path skips rebuilding the shell — yet `listEl` no longer lives inside `root`. Net effect: the section header / list container exist as orphan nodes in memory, the visible DOM still shows providers, and the diff loop appends new cards into a detached tree. The same trap may apply to `cardSlots` (entries still satisfy `slot.el.isConnected === false` after navigation away and back, depending on which check runs first). Need to verify against the actual paths. ## Fix sketch Treat any view-switch as a "drop my cached state". Either: - In `main.ts` `setView`, reset module-level state in the views that have caches (`workers.ts` exports a `resetWorkersView()` hook). Simple, explicit. - Or: at the top of `renderWorkers`, check `shellRoot && shellRoot.parentElement === root.parentElement && listEl?.isConnected`. If any fail, drop the cache and rebuild the shell. Slightly more defensive. Same audit needed for `nexus/web/src/views/sessions.ts` which keeps a `WeakMap<HTMLElement, MountState>` — the WeakMap key is the mount node, which *is* freed when navigation discards it, so this one should self-clean. Worth verifying. ## Reference - Files involved: `nexus/web/src/views/workers.ts`, `nexus/web/src/main.ts`, `nexus/web/src/views/sessions.ts`. - Triggered by the "UI Redesign: Workspaces + Empty-Container Spawn + Flicker Fix" plan.
lz closed this issue 2026-05-11 23:47:20 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus#1
No description provided.