Skip to content

Chat UI: message list, message bubble, and typing indicator components #6515

Description

@JSONbored

Context

The miner dashboard redesign adds a persistent chat rail to apps/loopover-miner-ui (mounted once in apps/loopover-miner-ui/src/routes/__root.tsx so it survives route navigation — see the chat rail shell issue for that mounting work). Before the message list, message bubble, and typing indicator that rail needs can be built, this issue delivers them as three standalone, backend-agnostic components, tested only against fixture data — not yet wired to a real backend.

apps/loopover-miner-ui already consumes @loopover/ui-kit as a first-class npm workspace dependency (apps/loopover-miner-ui/package.json:18, "@loopover/ui-kit": ">=0.1.0 <2.0.0") and already imports components straight from the package (e.g. apps/loopover-miner-ui/src/routes/portfolio.tsx:4-6 imports Button/Card/Table from @loopover/ui-kit/components/*). The two primitives this work needs already exist there, unused by any chat-shaped UI today:

  • packages/loopover-ui-kit/src/components/scroll-area.tsx — the scrollable-viewport primitive the message list must be built on.
  • packages/loopover-ui-kit/src/components/avatar.tsxAvatar/AvatarImage/AvatarFallback, the per-message avatar primitive.

#6244's audit (landed as apps/loopover-ui/src/chat-ui-primitives-audit.md via PR #6474) confirmed both are "reusable as-is" for chat, and also confirmed the two things this issue must build are entirely absent from the codebase today: "a chat-bubble/message-list component; a typing/in-progress indicator (no dot-pulse or inline 'other side is composing' affordance — Spinner only signals whole-panel loading)."

That Spinner lives in apps/loopover-ui/src/components/site/state-views.tsx alongside LoadingState/EmptyState/ErrorState/StateBoundary — today it's app-local to apps/loopover-ui, not importable from apps/loopover-miner-ui. #6244 also confirmed StateBoundary is reusable as-is once it exists somewhere both apps can reach; per the redesign synthesis, it must be ported into @loopover/ui-kit proper before any chat component that renders its own async state is built, since the message list is exactly that component. That port is a separate, prerequisite issue (the StateBoundary ui-kit port issue) — this issue is blocked by it.

packages/loopover-ui-kit/src/theme.css already defines the palette (oklch, "Lovable Lime") both apps consume via @import "@loopover/ui-kit/theme.css" (apps/loopover-miner-ui/src/styles.css:5), including --success/--warning/--danger, --chart-1..5, --sidebar-*, and the .dark-class dark-mode mechanism (theme.css:17) — the tokens for role-differentiated bubble colors already exist and should be reused, not invented.

Requirements

⚠️ Read this before starting. These are new files under apps/loopover-miner-ui/src/components/chat/ — not in packages/loopover-ui-kit (that package holds generic shadcn/Radix primitives, not this app's chat composites) and not in apps/loopover-ui (that's the separate maintainer-chat scope, #6230). A PR that adds these components to either of those other locations does NOT resolve this issue.

  • Create exactly three new component files:
    • apps/loopover-miner-ui/src/components/chat/message-list.tsx exporting MessageList
    • apps/loopover-miner-ui/src/components/chat/message-bubble.tsx exporting MessageBubble
    • apps/loopover-miner-ui/src/components/chat/typing-indicator.tsx exporting TypingIndicator
  • MessageList must render its scrollable viewport using ScrollArea imported from @loopover/ui-kit/components/scroll-area — not a raw overflow-y-auto div and not a third-party virtualization library.
  • MessageList must wrap its rendered content in StateBoundary imported from @loopover/ui-kit/components/state-views (the location the StateBoundary ui-kit port issue creates), covering its own loading/empty/error states for the message array it's given.
    • Do not import StateBoundary/LoadingState/EmptyState/ErrorState from apps/loopover-ui/src/components/site/state-views.tsx — that path is not reachable from apps/loopover-miner-ui.
    • Do not copy-paste state-views.tsx's contents into apps/loopover-miner-ui as a local duplicate. If the ui-kit port hasn't landed yet, this issue is blocked and should wait for it rather than working around it with a duplicate file.
  • MessageBubble must render its avatar using Avatar/AvatarImage/AvatarFallback imported from @loopover/ui-kit/components/avatar, a role-colored bubble background built from existing CSS custom properties already defined in packages/loopover-ui-kit/src/theme.css (no new hardcoded hex/oklch color literals), and a rendered timestamp.
  • TypingIndicator must be visually and semantically distinct from the existing bare Spinner (apps/loopover-ui/src/components/site/state-views.tsx) — an animated composing affordance (e.g. a dot-pulse), not a reused/re-styled spinner — and must expose an aria-label (or equivalent accessible name) that describes composing/typing, not loading, per research: audit ui-kit for existing message/chat-adjacent UI primitives #6244's finding that Spinner only communicates panel-loading today.
  • Define a shared fixture message shape (id, role, content, timestamp, and whatever optional avatar fields MessageBubble needs) and a fixtures module under apps/loopover-miner-ui/src/components/chat/ supplying representative sample arrays: empty, a single message, a multi-turn exchange, and at least one long-content edge case.
  • None of the three components may perform a network call, open a WebSocket/EventSource, or otherwise depend on a live backend — every prop they take is data (a message array, a boolean composing flag, etc.), supplied by fixtures in this issue and by a real data source only in later, separately-scoped issues.
  • Reuse the existing .dark-class dark-mode mechanism (no component-local light/dark branching, no new theme toggle logic — that's out of scope here).

Deliverables

  • apps/loopover-miner-ui/src/components/chat/message-list.tsx
  • apps/loopover-miner-ui/src/components/chat/message-bubble.tsx
  • apps/loopover-miner-ui/src/components/chat/typing-indicator.tsx
  • apps/loopover-miner-ui/src/components/chat/fixtures.ts (or .tsx, if it also needs to render JSX) with the sample message arrays described above
  • Component tests for all three, exercising the fixture-driven loading/empty/error/populated states of MessageList, the avatar-image-vs-fallback and role-color branches of MessageBubble, and the accessible-name/visual-state of TypingIndicator

Test Coverage Requirements

apps/loopover-miner-ui is under apps/**, which is outside Codecov's coverage.include — only src/** (the ORB Worker app) is measured by Codecov's patch-coverage gate (per this repo's CLAUDE.md). Codecov's codecov/patch check will not score these files, and that's expected, not a gap to work around — don't add these paths to coverage.include as part of this issue.

That said, the local gate (npm run test:ci) still runs and still needs to pass, and this codebase's own testing discipline still applies regardless of what Codecov scores: write tests that exercise every branch of the new components, not just a happy path —

  • MessageList's three StateBoundary branches (loading, empty, error) plus the populated-list render path
  • MessageBubble's avatar-image-present vs. avatar-fallback branch, and each role's color branch
  • TypingIndicator's rendered/hidden (or composing=true/false) branches and its accessible name

A regression test isn't applicable here (no existing bug being fixed) — the invariant/branch tests above are the equivalent bar for new components per this repo's house convention.

Expected Outcome

apps/loopover-miner-ui gains three new, independently tested, backend-agnostic chat UI components — a message list built on the shared ScrollArea primitive with real loading/empty/error handling via the newly-shared StateBoundary, a message bubble pairing avatar + role color + timestamp for the first time in this codebase, and a typing indicator that reads as "someone is composing" rather than "this panel is loading." The chat rail shell issue can compose these directly once it lands, without needing to build any of this scaffolding itself.

Links & Resources

  • research: audit ui-kit for existing message/chat-adjacent UI primitives #6244 / PR docs(ui): audit ui-kit for existing chat-adjacent UI primitives #6474apps/loopover-ui/src/chat-ui-primitives-audit.md, the audit this issue's scope is drawn from (confirms scroll-area.tsx/avatar.tsx reusable as-is, StateBoundary reusable once ported, and that message-list/bubble/typing-indicator are entirely new builds)
  • packages/loopover-ui-kit/src/components/scroll-area.tsx
  • packages/loopover-ui-kit/src/components/avatar.tsx
  • apps/loopover-ui/src/components/site/state-views.tsx — current (app-local) home of LoadingState/EmptyState/ErrorState/StateBoundary/Spinner
  • packages/loopover-ui-kit/src/theme.css — token source for role-colored bubbles and the .dark-class mechanism
  • apps/loopover-miner-ui/src/routes/__root.tsx — where the future chat rail shell will mount and consume these components
  • apps/loopover-miner-ui/package.json:18 — existing @loopover/ui-kit workspace dependency
  • The StateBoundary ui-kit port issue — hard blocker for this issue
  • The chat rail shell / __root.tsx mount issue — downstream consumer of these components (not a blocker)
  • Spec: conversational chat interface for loopover (Lovable/Cursor-style) #6230 — the separate maintainer-chat scope this issue's components are not part of

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