oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { useQuery } from '@tanstack/react-query'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { ArrowLeft, Clock, FolderKanban, MessageSquare } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { ErrorState } from '@/components/shared/error-state'
import { LoadingCards } from '@/components/shared/loading'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { useProjects } from '@/hooks/use-projects'
import { api } from '@/lib/api-client'
import type { SessionDetail } from '@/types'

export const Route = createFileRoute('/sessions/$sessionId')({
  component: SessionDetailPage,
})

function ProjectSelector({
  currentProjectId,
}: {
  sessionId?: string
  currentProjectId: string | null
}) {
  const { t } = useTranslation()
  const { data: projectsData } = useProjects()
  const projects = Array.isArray(projectsData?.items) ? projectsData.items : []
  const currentProject = projects.find((p) => p.id === currentProjectId)

  return (
    <div className="flex items-center gap-2">
      <FolderKanban className="h-4 w-4 text-muted-foreground shrink-0" />
      {currentProjectId && currentProject ? (
        <span className="text-sm">
          {currentProject.emoji ?? '📦'} <span className="font-medium">{currentProject.name}</span>
        </span>
      ) : (
        <span className="text-xs text-muted-foreground">
          {t('sessions.noProject', '— No project')}
        </span>
      )}
    </div>
  )
}

function SessionDetailPage() {
  const { t } = useTranslation()
  const { sessionId } = Route.useParams()
  const navigate = useNavigate()

  const {
    data: session,
    isLoading,
    isError,
    refetch,
  } = useQuery({
    queryKey: ['session', sessionId],
    queryFn: () => api.get<SessionDetail>(`/api/sessions/${sessionId}`),
  })

  if (isLoading) return <LoadingCards count={3} />
  if (isError) return <ErrorState onRetry={() => refetch()} />
  if (!session) return <p className="text-muted-foreground">{t('sessions.notFound')}</p>

  // Build interleaved messages from user_messages and agent_responses
  const messages: { role: 'user' | 'assistant'; content: string }[] = []
  const userMsgs: { content: string }[] = Array.isArray(session.user_messages)
    ? session.user_messages
    : []
  const agentMsgs: { content: string }[] = Array.isArray(session.agent_responses)
    ? session.agent_responses
    : []
  const maxLen = Math.max(userMsgs.length, agentMsgs.length)
  for (let i = 0; i < maxLen; i++) {
    const userMsg = userMsgs[i]
    const agentMsg = agentMsgs[i]
    if (userMsg != null) messages.push({ role: 'user', content: userMsg.content })
    if (agentMsg) messages.push({ role: 'assistant', content: agentMsg.content ?? '' })
  }

  const details = [
    { label: t('sessions.sessionId'), value: session.id },
    { label: 'User ID', value: session.user_id ?? '—' },
    { label: t('seeds.seed'), value: session.active_seed_id ?? '—' },
    { label: t('sessions.messages'), value: messages.length },
    { label: t('sessions.createdAt'), value: new Date(session.created_at).toLocaleString() },
    {
      label: t('sessions.updated'),
      value: session.updated_at ? new Date(session.updated_at).toLocaleString() : '—',
    },
  ]

  return (
    <div className="space-y-6">
      <div className="flex items-center gap-4">
        <Button
          variant="ghost"
          size="icon"
          onClick={() => navigate({ to: '/sessions' })}
          aria-label={t('common.back')}
        >
          <ArrowLeft className="h-4 w-4" />
        </Button>
        <div className="flex-1">
          <h1 className="text-2xl font-bold flex items-center gap-2">
            <Clock className="h-6 w-6" /> {t('sessions.sessionDetail')}
          </h1>
          <p className="text-muted-foreground font-mono text-xs">{sessionId}</p>
        </div>
      </div>

      <Card>
        <CardHeader>
          <CardTitle>{t('sessions.sessionInfo')}</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="grid gap-3 md:grid-cols-2">
            {/* Project row */}
            <div className="flex items-center justify-between rounded-lg border p-3 bg-muted/20">
              <span className="text-sm text-muted-foreground flex items-center gap-1">
                <FolderKanban className="h-3 w-3" />
                {t('sessions.project', 'Project')}
              </span>
              <ProjectSelector currentProjectId={(session as any).project_id ?? null} />
            </div>
            {details.map((d) => (
              <div
                key={d.label}
                className="flex items-center justify-between rounded-lg border p-3"
              >
                <span className="text-sm text-muted-foreground">{d.label}</span>
                <span className="text-sm font-medium">{d.value}</span>
              </div>
            ))}
          </div>
        </CardContent>
      </Card>

      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <MessageSquare className="h-4 w-4" /> {t('sessions.messages')}
          </CardTitle>
        </CardHeader>
        <CardContent>
          {messages.length > 0 ? (
            <div className="space-y-3">
              {messages.map((msg, i) => (
                <div key={`msg-${i}`} className="flex gap-3">
                  <Badge
                    variant={msg.role === 'user' ? 'default' : 'secondary'}
                    className="shrink-0 h-6"
                  >
                    {msg.role === 'user' ? t('chat.user') : t('chat.assistant')}
                  </Badge>
                  <div className="flex-1 rounded-lg bg-muted p-3">
                    <p className="text-sm whitespace-pre-wrap">{msg.content}</p>
                  </div>
                </div>
              ))}
            </div>
          ) : (
            <p className="text-sm text-muted-foreground">{t('sessions.noMessages')}</p>
          )}
        </CardContent>
      </Card>
    </div>
  )
}