Skip to content

Chat rail: persistent collapsible shell mounted in __root.tsx #6513

Description

@JSONbored

Context

apps/loopover-miner-ui/src/routes/__root.tsx (lines 8-39) currently renders a fixed <header> (uppercase mono kicker "LoopOver Miner" + <h1>Local dashboard</h1> + a 4-link nav for Overview/Run history/Portfolio/Ledgers), a <main className="max-w-5xl"> wrapping the routed <Outlet/>, and a <GrafanaFooterLink/>. All four routes (index.tsx, run-history.tsx, portfolio.tsx, ledgers.tsx) render inside that single <Outlet/> and are swapped in/out by TanStack Router on navigation — there is currently no persistent UI element that survives a route change other than the header itself.

The miner dashboard redesign keeps this dashboard-first shape: the routed pages stay the primary content area (restyled in place by separate, later issues), and a persistent chat rail is added alongside them rather than replacing navigation. Because a component rendered inside a route file gets unmounted/remounted on every navigation, the rail has to be mounted exactly once, in __root.tsx itself, so its open/closed state and (eventually) its conversation state survive moving between routes.

@loopover/ui-kit (packages/loopover-ui-kit/src/components/) already ships a full sidebar.tsx component and a hooks/use-mobile.tsx breakpoint hook, confirmed unused anywhere in apps/loopover-miner-ui today. sidebar.tsx's own mobile behavior — collapsing to a Sheet-based slide-over below use-mobile.tsx's breakpoint instead of staying docked — is the exact mechanism this rail must reuse for its own narrow-viewport behavior, so the app doesn't end up with two different "how do we collapse a side panel on mobile" implementations.

This issue is scoped as a pure structural shell: no chat message list, no composer, no streaming, no backend call of any kind. The recently-merged ui-kit chat-primitives audit (apps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PR #6474) found that state-views.tsx's LoadingState/EmptyState/ErrorState/StateBoundary need to be ported from apps/loopover-ui into @loopover/ui-kit proper before any chat component that renders its own async state (the eventual message list) is built — that porting requirement does not apply here, since this rail renders no async state and consumes none of those primitives. This issue has no dependency on that port and can proceed independently of it.

Requirements

⚠️ Read this before starting. This issue is a structural shell only. Do not build a message composer, a message list/scrollback, a typing indicator, streaming text rendering, or any fetch/EventSource/backend call — those are separate, later issues layered on top of this one. Do not implement the narrow-viewport behavior as a new custom modal, a new matchMedia/CSS-only breakpoint, or any mobile pattern other than packages/loopover-ui-kit/src/components/sidebar.tsx's existing Sheet-based slide-over, gated by packages/loopover-ui-kit/src/hooks/use-mobile.tsx. A PR that adds a working composer, wires any endpoint, invents a second mobile-collapse mechanism, or defines its own breakpoint constant instead of reusing use-mobile.tsx's does not resolve this issue and will be closed.

  • Add a new rail component under apps/loopover-miner-ui/src/components/ (e.g. apps/loopover-miner-ui/src/components/chat-rail.tsx, or a small set of files in a chat-rail/ subdirectory) — apps/loopover-miner-ui/src currently has no local components/ directory, so this is new.
  • Mount the rail exactly once inside apps/loopover-miner-ui/src/routes/__root.tsx, alongside the existing <header>/<main className="max-w-5xl">/<Outlet/>/<GrafanaFooterLink/>. It must not be mounted inside, imported by, or duplicated across any of the four route files (index.tsx, run-history.tsx, portfolio.tsx, ledgers.tsx) — mounting it per-route would defeat the entire point of surviving navigation.
  • On wide viewports the rail is docked at ~380px wide, rendered beside (not overlapping) the existing routed content — this requires restructuring __root.tsx's layout container into a row that holds the main content area and the rail side by side, without changing the visual output of the four existing routes.
  • Provide a visible collapse/expand toggle. Collapsing must not unmount the rail's own state (so re-expanding doesn't lose it later, once real content exists) — it only changes whether the ~380px panel is visually docked/visible.
  • Collapse/expand state must live in __root.tsx (or a hook/context it owns), not in a route file, so it persists across client-side navigation between the four routes. It does not need to persist across a full page reload — no localStorage/sessionStorage requirement for this issue; in-memory state scoped to the __root.tsx mount is sufficient.
  • Below use-mobile.tsx's existing breakpoint, the rail renders as a slide-over/sheet overlay instead of the docked ~380px panel, using the same Sheet component and use-mobile.tsx hook that sidebar.tsx already uses for its own mobile mode. Import and reuse that hook directly rather than re-deriving the breakpoint value.
  • Content is static placeholder only: e.g. a header label ("Chat" or similar) and a single placeholder/empty-state line. No <textarea>/<input> submit handling, no message bubbles, no scrollback.
  • No new usage of @loopover/ui-kit's scroll-area.tsx, avatar.tsx, or state-views.tsx primitives in this PR — those belong to the follow-up message-list/composer work, not this shell.
  • No TanStack Router route-tree changes — this is a __root.tsx + new-component change only.
  • No config flag is needed for this issue. The config-gated piece described in the wider chat design is action-dispatch specifically (a separate, later issue); this shell carries no privileged or write behavior, so it always renders.

Deliverables

  • apps/loopover-miner-ui/src/components/chat-rail.tsx (or equivalent small set of files) — the new rail shell component
  • apps/loopover-miner-ui/src/routes/__root.tsx — updated to mount the rail once, in a layout that places it beside the existing <main>/<Outlet/> without altering the four routes' own content
  • Collapse/expand toggle control wired to component/context-local state that survives client-side route navigation
  • Narrow-viewport slide-over behavior reusing sidebar.tsx's Sheet + use-mobile.tsx mechanism
  • Tests covering the behavior below

Test Coverage Requirements

This change is entirely under apps/loopover-miner-ui/**, which is outside Codecov's coverage.include (only src/** is measured — see the repo's contributing skill/reference doc), so this PR will not be gated on a Codecov patch-coverage percentage. That does not mean untested: npm run test:ci still runs the full local gate including this app's existing tests, and this PR must add real tests for the new behavior, following this app's existing test conventions. At minimum, cover:

  • The rail renders once inside __root.tsx and its open/closed state does not reset when simulating a client-side navigation between two of the four routes (e.g. Overview → Portfolio).
  • The collapse/expand toggle correctly flips the rail's docked/collapsed rendered state (and any aria-expanded/aria-hidden-equivalent attributes used).
  • Below use-mobile.tsx's mobile breakpoint, the rail renders via the Sheet/slide-over path rather than the docked ~380px panel (assert the same mechanism, not a re-implementation).
  • The four existing routes' own rendered output is unchanged by the __root.tsx restructuring — a regression check that this is a layout-only addition.

Expected Outcome

After this ships, apps/loopover-miner-ui has a persistent ~380px chat rail visible from every route (Overview/Run history/Portfolio/Ledgers) that survives navigation without being re-mounted per page, is collapsible/expandable via a visible control, and correctly degrades to a slide-over sheet on narrow viewports using the exact mechanism sidebar.tsx already ships elsewhere in the design system — with placeholder-only content, no message list, composer, or backend wiring of any kind. The four existing routes render exactly as they did before. This becomes the mount point that the chat message-list-and-composer issue, the chat streaming-renderer issue, and (once their own config flag and endpoint wiring are decided) the chat action-dispatch scaffolding issue and its three action-family children build inside, without any of them needing to touch __root.tsx's mounting logic again.

Links & Resources

  • apps/loopover-miner-ui/src/routes/__root.tsx (lines 8-39) — mount point for the rail
  • apps/loopover-miner-ui/src/routes/index.tsx, run-history.tsx, portfolio.tsx, ledgers.tsx — must render visually unchanged
  • packages/loopover-ui-kit/src/components/sidebar.tsx — source of the mobile-sheet mechanism to reuse
  • packages/loopover-ui-kit/src/hooks/use-mobile.tsx — breakpoint hook to reuse, not reimplement
  • apps/loopover-miner-ui/src/styles.css — already imports @loopover/ui-kit/theme.css, so no new token wiring is needed for this shell
  • The ui-kit chat-primitives audit (apps/loopover-ui/src/chat-ui-primitives-audit.md, merged via PR docs(ui): audit ui-kit for existing chat-adjacent UI primitives #6474) — confirms scroll-area.tsx/avatar.tsx/state-views.tsx as reusable building blocks for the next chat issues, not needed here
  • The chat message-list-and-composer issue, the chat streaming-renderer issue, and the chat action-dispatch scaffolding issue (plus its three action-family children) all build on top of this rail shell but are separate, later issues — none of their scope belongs in this PR
  • The separate maintainer-chat scoping issue (ORB's read-only maintainer chat) is unrelated scope with its own open audience/backend/placement questions — nothing in this issue should reference or share code with it

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:featureGittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.help wantedExtra attention is needed

    Projects

    Status
    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions