fix(chat): keep the first-send loader inside the chat panel

Merged
VI
2d ago
+20-8

What is this contribution about?

Sending the first message from a destination (open Studio, open the chat, send) replaced the whole area right of the sidebar with the panel loader for about 350ms, then put the layout back. Only the chat panel should load.

The send mints a thread and adds ?thread= to the URL. Two things then suspended above the chat panel's own MainPanelBoundary:

  1. ActiveTaskProvider called use(conn.ready). Its comment said the side panel's boundary caught this, but the provider wraps the whole ChatLayout, so the suspension reached the route-level boundary and blanked both panels.
  2. useTaskMetadata always ran its suspense query, even when the thread row was already in the local store (the local row wins anyway). A thread created a moment ago has no cached query, so the topbar, the tab bar and the main panel content suspended on a COLLECTION_THREADS_GET they didn't need.

Changes:

  • ChatStreamContextValue gets a ready promise. ActiveTaskProvider no longer suspends and passes conn.ready; the threadless and native terminal providers pass an already-settled one.
  • ChatSidePanel suspends on ready inside its own boundary.
  • The autosend effect waits until the first page has loaded, since an existing thread looks empty before that.
  • useTaskMetadata seeds its query with the local row through initialData (partial /watch rows are skipped, as in useEnsureTask).

Trade-off: a main-panel affordance that sends a message (for example the visual editor prompt) can now render during the roughly 300ms before the chat history loads. Before, nothing rendered in that window.

How did you verify your code works?

  • bun run verify: 10960 pass, 0 fail. tsc --noEmit on apps/web is clean.
  • In a local dev build I attached a MutationObserver that logs every [data-testid=panel-loading] element with its position and width, then ran open chat → type → Enter:
    • On main: after the send, a loader 1491px wide (the full area right of the sidebar) for about 350ms.
    • With only the provider change: the full-width loader still showed for about 30ms. Temporary labeled Suspense boundaries showed the thread topbar and the main content outlet were suspending too, which led to the useTaskMetadata change.
    • With both changes: only the chat panel's loader (487px) for about 300ms. The topbar and main content no longer suspend, and the autosent message is sent and shown.

Screenshots/Demonstration

No screenshots: the defect is a flash of about 350ms, and a still image shows nothing. The observer measurements above are the evidence.

How to Test

  1. Open an organization's Home with the chat closed.
  2. Open the chat, type a message, and press Enter.
  3. Expected: the main panel and the topbar stay in place; at most the chat panel shows a spinner briefly before the conversation appears.

Review Checklist

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

Summary by cubic

Fixes the first-send loading state so only the chat panel shows a spinner, instead of the whole area right of the sidebar flashing a loader for roughly 350ms.

  • Moved suspense off ActiveTaskProvider (which wraps the entire chat layout) into the chat panel's own boundary via a new ready promise on the chat stream context.
  • Delayed autosend until the first page of messages loads, so an existing thread does not look empty before then.
  • useTaskMetadata now seeds its query from a non-partial local row, avoiding a needless suspension on a thread that was just created locally.

Trade-off: a main-panel affordance that sends a message (for example the visual editor prompt) can now render during the ~300ms before chat history loads; previously nothing rendered in that window.