oxios 1.10.1

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { AlertTriangle, ArrowRight, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog'
import { Separator } from '@/components/ui/separator'

export interface ConfigDiff {
  /** Dotted config path, e.g. `exec.allowed_commands`. */
  path: string
  /** Previous value (serialised). */
  before: unknown
  /** New value (serialised). */
  after: unknown
  /** True if the change is applied immediately. */
  hotReload: boolean
  /** Optional restart scope (kernel / gateway / etc.). */
  scope?: string
}

interface DiffPreviewProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  diffs: ConfigDiff[]
  onConfirm: () => void
  isPending?: boolean
  /** Optional lookup: dotted config path → human-readable label. */
  labelForPath?: (path: string) => string | undefined
}

/**
 * Modal shown right before saving changes. Lists every changed field
 * with old → new, plus a callout for fields that need a daemon restart.
 */
export function DiffPreview({
  open,
  onOpenChange,
  diffs,
  onConfirm,
  isPending,
  labelForPath,
}: DiffPreviewProps) {
  const { t } = useTranslation()

  const resolveLabel = (path: string): string | undefined => {
    const key = labelForPath?.(path)
    if (!key) return undefined
    const translated = t(key)
    // i18next returns the key itself when the entry is missing.
    return translated === key ? undefined : translated
  }

  const restartRequired = diffs.filter((d) => !d.hotReload)
  const hotReload = diffs.filter((d) => d.hotReload)

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-2xl">
        <DialogHeader>
          <DialogTitle>{t('settings.confirmChanges')}</DialogTitle>
          <DialogDescription>
            {t('settings.confirmChangesDescription', { count: diffs.length })}
          </DialogDescription>
        </DialogHeader>

        <div className="my-4 max-h-80 space-y-2 overflow-y-auto pr-1" data-testid="diff-list">
          {diffs.length === 0 ? (
            <p className="text-sm text-muted-foreground py-6 text-center">
              {t('settings.noChanges')}
            </p>
          ) : (
            diffs.map((diff) => <DiffRow key={diff.path} diff={diff} resolveLabel={resolveLabel} />)
          )}
        </div>

        {restartRequired.length > 0 && (
          <>
            <Separator />
            <div className="mt-4 rounded-lg border border-warning-subtle p-3 text-sm">
              <p className="font-medium flex items-center gap-2 text-warning">
                <AlertTriangle className="h-4 w-4" />
                {t('settings.restartRequiredWarning', { count: restartRequired.length })}
              </p>
              <ul className="mt-2 list-disc list-inside text-xs text-warning/90 space-y-0.5">
                {restartRequired.map((d) => (
                  <li key={d.path}>
                    <code className="font-mono">{resolveLabel(d.path) ?? d.path}</code>
                  </li>
                ))}
              </ul>
            </div>
          </>
        )}

        {hotReload.length > 0 && (
          <div className="text-xs text-muted-foreground">
            <p className="flex items-center gap-1.5">
              <Check className="h-3 w-3 text-success" />
              {t('settings.appliedImmediately', { count: hotReload.length })}
            </p>
          </div>
        )}

        <DialogFooter className="mt-4 flex justify-end gap-2">
          <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>
            {t('common.cancel')}
          </Button>
          <Button
            onClick={onConfirm}
            disabled={isPending || diffs.length === 0}
            data-testid="confirm-save"
          >
            {t('common.save')}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

function DiffRow({
  diff,
  resolveLabel,
}: {
  diff: ConfigDiff
  resolveLabel: (path: string) => string | undefined
}) {
  const { t } = useTranslation()
  const label = resolveLabel(diff.path)
  return (
    <div className="flex flex-col gap-0.5 rounded-md border bg-muted/30 px-3 py-2">
      <div className="flex items-center justify-between">
        <div className="flex flex-col">
          {label ? (
            <>
              <span className="text-sm font-medium">{label}</span>
              <code className="font-mono text-2xs text-muted-foreground">{diff.path}</code>
            </>
          ) : (
            <code className="font-mono text-xs text-muted-foreground">{diff.path}</code>
          )}
        </div>
        {diff.hotReload ? (
          <span className="text-2xs uppercase tracking-wider text-success">
            {t('settings.hotReload')}
          </span>
        ) : (
          <span className="text-2xs uppercase tracking-wider text-warning flex items-center gap-1">
            <AlertTriangle className="h-2.5 w-2.5" />
            {t('settings.requiresRestart')}
          </span>
        )}
      </div>
      <div className="flex items-center gap-2 text-xs">
        <ValuePreview value={diff.before} />
        <ArrowRight className="h-3 w-3 text-muted-foreground shrink-0" />
        <ValuePreview value={diff.after} />
      </div>
    </div>
  )
}

function ValuePreview({ value }: { value: unknown }) {
  const formatted =
    value === null || value === undefined
      ? '∅'
      : Array.isArray(value)
        ? `[${value.map((v) => JSON.stringify(v)).join(', ')}]`
        : typeof value === 'object'
          ? JSON.stringify(value)
          : String(value)
  return (
    <span className="font-mono truncate max-w-[260px] inline-block" title={formatted}>
      {formatted}
    </span>
  )
}