oxios 1.10.1

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { createFileRoute } from '@tanstack/react-router'
import { FolderPlus, RefreshCw, Sparkles, Trash2, Wrench } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { CreateMountDialog } from '@/components/mount/create-mount-dialog'
import { EmptyState } from '@/components/shared/empty-state'
import { ErrorState } from '@/components/shared/error-state'
import { LoadingCards } from '@/components/shared/loading'
import { RefreshButton } from '@/components/shared/refresh-button'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useDeleteMount, useMounts, useRescanMount } from '@/hooks/use-mounts'
import type { Mount } from '@/types'

export const Route = createFileRoute('/mounts/')({ component: MountsPage })

function MountsPage() {
  const { t } = useTranslation()
  const [search, setSearch] = useState('')
  const [showCreate, setShowCreate] = useState(false)

  const { data, isLoading, isError, refetch } = useMounts(search || undefined)
  const deleteMount = useDeleteMount()
  const rescanMount = useRescanMount()

  const mounts = Array.isArray(data?.items) ? data.items : []

  const handleDelete = async (mount: Mount) => {
    try {
      await deleteMount.mutateAsync(mount.id)
      toast.success(t('mounts.deleted', 'Mount가 삭제되었습니다'))
    } catch (err) {
      toast.error(err instanceof Error ? err.message : t('mounts.deleteFailed', '삭제 실패'))
    }
  }

  const handleRescan = async (mount: Mount) => {
    try {
      await rescanMount.mutateAsync(mount.id)
      toast.success(t('mounts.rescanned', 'Mount가 갱신되었습니다'))
    } catch (err) {
      toast.error(err instanceof Error ? err.message : t('mounts.rescanFailed', '갱신 실패'))
    }
  }

  return (
    <div className="space-y-4">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold">{t('mounts.title', 'Mounts')}</h1>
          <p className="text-muted-foreground text-sm">
            {t('mounts.desc', '경로 별칭. 이름을 언급하면 자동으로 컨텍스트에 주입됩니다.')}
          </p>
        </div>
        <Button onClick={() => setShowCreate(true)}>
          <FolderPlus className="h-4 w-4 mr-2" />
          {t('mounts.create', 'Mount 만들기')}
        </Button>
      </div>

      {/* Search */}
      <div className="flex items-center gap-2">
        <Input
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder={t('mounts.searchPlaceholder', '이름, 설명, 언어로 검색...')}
          className="max-w-xs"
        />
        <RefreshButton onClick={() => refetch()} />
      </div>

      {/* Content */}
      {isLoading ? (
        <LoadingCards />
      ) : isError ? (
        <ErrorState onRetry={() => refetch()} />
      ) : mounts.length === 0 ? (
        <EmptyState
          icon={<FolderPlus className="h-8 w-8" />}
          title={t('mounts.empty', 'Mount가 없습니다')}
          description={t(
            'mounts.emptyDesc',
            'Mount를 만들어 경로에 이름을 붙이세요. 에이전트가 자동으로 설명을 채웁니다.',
          )}
          action={
            <Button onClick={() => setShowCreate(true)}>
              <FolderPlus className="h-4 w-4 mr-2" />
              {t('mounts.create', 'Mount 만들기')}
            </Button>
          }
        />
      ) : (
        <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
          {mounts.map((mount) => (
            <div
              key={mount.id}
              className="group relative rounded-lg border bg-card p-4 transition-all hover:shadow-sm"
            >
              {/* Action buttons: rescan + delete (top-right) */}
              <div className="absolute right-2 top-2 flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-7 w-7"
                  onClick={() => handleRescan(mount)}
                  aria-label={t('mounts.rescan', '갱신')}
                >
                  <RefreshCw className="h-3.5 w-3.5" />
                </Button>
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-7 w-7"
                  onClick={() => handleDelete(mount)}
                  aria-label={t('common.delete', '삭제')}
                >
                  <Trash2 className="h-3.5 w-3.5" />
                </Button>
              </div>

              {/* Name */}
              <div className="mb-2 flex items-center gap-2">
                {mount.source === 'auto_promoted' ? (
                  <Sparkles className="h-4 w-4 text-violet-500" />
                ) : (
                  <Wrench className="h-4 w-4" />
                )}
                <h3 className="font-semibold truncate">{mount.name}</h3>
                {mount.source === 'auto_promoted' && (
                  <span className="rounded-full bg-violet-500/10 px-2 py-0.5 text-xs text-violet-600">
                    {t('mounts.autoPromoted', '자동 생성')}
                  </span>
                )}
                {mount.enrichment_pending && (
                  <span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-xs text-amber-600">
                    {t('mounts.needsRefresh', '갱신 필요')}
                  </span>
                )}
              </div>

              {/* Path */}
              <p className="mb-2 text-xs text-muted-foreground truncate font-mono">
                {mount.paths[0] ?? '(no path)'}
              </p>

              {/* Auto-description */}
              {mount.auto_description && (
                <p className="mb-2 text-sm text-muted-foreground line-clamp-2">
                  {mount.auto_description}
                </p>
              )}

              {/* Languages + stack */}
              <div className="flex flex-wrap gap-1">
                {mount.auto_meta.languages.map((lang) => (
                  <span
                    key={lang}
                    className="rounded bg-primary/10 px-1.5 py-0.5 text-xs text-primary"
                  >
                    {lang}
                  </span>
                ))}
                {mount.auto_meta.stack.slice(0, 4).map((s) => (
                  <span
                    key={s}
                    className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground"
                  >
                    {s}
                  </span>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Create dialog */}
      <CreateMountDialog open={showCreate} onOpenChange={setShowCreate} />
    </div>
  )
}