fix(web): keep loading states on the widget that is loading

Merged
VI
2d ago
+208-170

What is this contribution about?

Follow-up to #7995. An audit of every suspending read (useSuspenseQuery, use(), suspense SDK hooks, lazy components) against its nearest boundary turned up more places where a user action makes a small piece of UI suspend and a boundary far above it blanks a whole page, panel, or both chat and main panels. Each fix puts the loading state on the part that is actually waiting, or removes the wait when the data is optional.

Monitoring

  • Relative ranges (now-24h … now) were resolved against a fresh Date on every render. Each navigation, including every keystroke in the search box, produced a new audit logs query key and re-suspended the dashboard; the search input lost focus mid-typing. expressionToDate now takes an optional now, and the dashboard pins it per picked range.
  • The audit table has its own boundary (table skeleton), so a filter change no longer hides the toolbar.
  • The connection filter's search term is deferred, so typing keeps the current options instead of suspending the page around the open popover.

Chat layout (these escaped to the boundary around the whole ChatLayout, blanking both panels)

  • Threads menu rows and chat search avatars read their agent with useVirtualMCPNonBlocking. Opening the menu, loading more, or typing a search no longer blanks the layout.
  • The tab bar's dev-server MCP client is non-blocking and only connects once the dev server is running. On desktop, it suspended the tab bar when the sandbox came up.
  • The composer's tools popover and the next-action chip read their client without suspending. The popover only needs it when open; the chip only for studio-pack agents.
  • useMCPClientNonBlocking takes an enabled flag; useMCPClientOptional had no callers left and is removed.
  • The Develop/Live toggle prefetches the counterpart agent (prefetchVirtualMCP) in parallel with the thread create, so the flip doesn't land on a session-wide fallback.

Connections and settings

  • The connection page followed siblings[0], which is ordered by updated_at, so enabling, disabling or saving an instance swapped the page onto another client and re-suspended all of it. The primary connection is now pinned once picked.
  • Each app preview in the connection's UI tab has its own skeleton.
  • Switching a tool's instance runs in a transition, so the current instance stays on screen while the next loads.
  • The role editor's MCP tab has its own spinner instead of replacing the editor, its Save/Cancel and its tabs.
  • The binding selector reads its selected connection with the new useConnectionNonBlocking, so inline-installing a connection doesn't blank the form.
  • Fixes a cache-shape mismatch: the tool-set selector wrote bare connection items into the key useConnection reads as { item }. It now uses the shared collectionItemQueryOptions.

Automations and reports

  • The automation Runs tab has its own boundary and reads members without suspending, so opening it keeps the back button and Settings/Runs switcher.
  • Automation list rows read their agent without suspending.
  • Reports companion cards defer the schema and saved state their queries are keyed on, so connecting or disconnecting a source keeps the cards up instead of reverting the section to skeletons.

Checked and left alone: route changes (sidebar, settings navigation) already show the loader only in the page area, and most dialogs, popovers and message parts already have their own boundaries.

How did you verify your code works?

  • bun run verify: 11000 pass, 0 fail. bun run check passes.
  • Ran the app locally against an isolated dev stack with a MutationObserver logging every [data-testid=panel-loading] (position and size) and skeleton:
    • Monitoring › Audit, typing in the search box: on main the content was replaced by skeletons and document.activeElement became BODY after typing. With this branch, no loader appears and focus stays in the input.
    • Automation › Runs tab (built-in Reviewer): no loader on switch.
    • Role editor › MCP Permissions: no loader replacing the editor; Save/Cancel stay.
    • Chat panel open/close, Board, Library, settings navigation, Monitor tab switches, threads menu, inbox, org switcher and New Project: loaders only in their own area (unchanged).
  • Not exercised at runtime (the local org had no AI provider, connections or threads): the composer popovers, the connection page, the tool detail page, reports companions, the desktop dev-server tab bar, and the Develop/Live toggle. Those changes are covered by reading the code paths and by the typecheck.

Screenshots/Demonstration

No screenshots: the defects are brief fallbacks and focus loss, which a still image doesn't show. The observer results above are the evidence.

How to Test

  1. Settings › Monitor › Audit: type in the search box. The table and toolbar stay, and the input keeps focus.
  2. Open a connection with several instances and toggle one: the page stays; only parts with new data update.
  3. Open the chat's threads menu with threads from several agents: the layout stays; avatars fill in.
  4. Open an automation and switch to Runs: the header and switcher stay.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

Summary by cubic

Stops user-triggered suspense fallbacks from blanking whole pages and panels by placing loading states on the exact widget that's waiting, or deferring optional reads that don't need to block. Typing in the monitoring audit search previously re-suspended the dashboard and dropped focus, toggling or saving a connection swapped the page to a different client, and optional chat reads blanked the entire layout.

Loading states scoped to the waiting UI

  • Monitoring: audit range expressions are pinned per picked range so keystrokes no longer change the query key; the audit table got its own skeleton boundary and the connection filter's search term is deferred.
  • Chat: thread rows, search avatars, the composer popover, the next-action chip, and the dev-server client now read their data without suspending above the layout.
  • Connections: the displayed connection is pinned instead of following siblings[0], app previews have their own skeletons, tool instance switches run in a transition, and the role editor's MCP tab and binding selector have scoped fallbacks.
  • Automations: the Runs tab and list rows no longer suspend the enclosing page; reports companion cards defer the schema and saved state their queries depend on.

Hook and cache changes

  • useMCPClientNonBlocking gained an enabled flag; useMCPClientOptional was removed as unused.
  • The Develop/Live toggle now prefetches the counterpart agent so the flip lands on a cache hit.
  • The tool-set selector writes connection items in the same { item } cache shape useConnection reads.