oxios 1.23.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { Loader2, Sparkles } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { deriveCurrentActivity, describeLiveActivity } from '@/lib/live-activity'
import { cn } from '@/lib/utils'
import { useChatStore } from '@/stores/chat'

/**
 * RFC-015 §4.3 — LiveActivityBar.
 *
 * Replaces the legacy 3-dot typing indicator with a header that reflects
 * the single most recent in-flight activity for the assistant turn.
 * Instead of a generic "Thinking…" the bar now shows a sentence-level
 * description of what is actually happening:
 *
 *   thinking      → pulse + "Thinking…"
 *   tool_running  → spinner + "Searching the web · rust async"
 *                             "Reading file · …/src/main.rs"
 *                             "Running command · cargo build"
 *   reasoning     → pulse  + "Reasoning…"
 *
 * Activity cards below remain as the historical timeline (see
 * `ActivityTimeline`). The bar fades out the moment the assistant starts
 * streaming text, so the typewriter takes over.
 *
 * Mounted only while an assistant turn is being built — i.e. `isStreaming`
 * is true AND the most recent message in the store is an assistant
 * message. This matches the live-activity UX table in the design doc.
 */
export function LiveActivityBar() {
  const { t } = useTranslation()
  const isStreaming = useChatStore((s) => s.isStreaming)
  const last = useChatStore((s) => s.messages.at(-1))

  if (!isStreaming || last?.role !== 'assistant') return null

  const descriptor = deriveCurrentActivity(last.activities)
  const streamingTextStarted = (last.content ?? '').trim().length > 0
  const { label, detail } = describeLiveActivity(descriptor, t)

  return (
    <div
      className={cn(
        'flex my-1.5 animate-fade-in-up transition-opacity duration-300',
        streamingTextStarted && 'opacity-0 pointer-events-none',
      )}
      aria-live="polite"
      data-state={streamingTextStarted ? 'fading' : 'live'}
    >
      <div className="max-w-[80%]">
        <div className="rounded-lg px-4 py-2.5 bg-muted">
          <div className="flex items-center gap-2 text-sm text-muted-foreground">
            {descriptor.kind === 'tool_running' ? (
              <Loader2 className="h-3.5 w-3.5 animate-spin shrink-0" aria-hidden />
            ) : descriptor.kind === 'reasoning' ? (
              <Sparkles className="h-3.5 w-3.5 animate-pulse shrink-0" aria-hidden />
            ) : (
              <span
                className="h-2 w-2 rounded-full bg-muted-foreground/60 animate-pulse shrink-0"
                aria-hidden
              />
            )}
            <span className="truncate">{label}</span>
            {detail && (
              <>
                <span className="text-muted-foreground/40 shrink-0">·</span>
                <span className="text-muted-foreground/70 truncate max-w-[40ch]">{detail}</span>
              </>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}