import * as React from 'react'
import { useConfig } from '@/state/config'
import { ipc } from '@/lib/api/desktop'
import { asEnvelope } from '@/lib/error'
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
import { isTauriHost } from '@/lib/api/desktop'
import type {
DesktopErrorEnvelope,
DesktopStatusResponse,
} from '@/lib/types/desktop'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { ErrorBanner } from '@/components/error-banner'
import { CheckCircle2, Loader2, RefreshCw, XCircle } from 'lucide-react'
type MemoryStats = {
total: number
wakeup_ready: number
pending_review: number
by_type: Record<string, number>
}
export function StatusPage() {
const cfg = useConfig()
const [status, setStatus] = React.useState<DesktopStatusResponse | null>(null)
const [stats, setStats] = React.useState<MemoryStats | null>(null)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState<DesktopErrorEnvelope | null>(null)
const load = React.useCallback(async () => {
if (!cfg.configPath.trim() || !cfg.cwd.trim()) return
setLoading(true)
setError(null)
try {
const res = await ipc.collectStatus({
config_path: cfg.configPath,
vault_root_override: cfg.vaultRoot.trim() || null,
cwd: cfg.cwd,
})
setStatus(res)
} catch (err) {
setError(asEnvelope(err))
} finally {
setLoading(false)
}
}, [cfg.configPath, cfg.cwd, cfg.vaultRoot])
React.useEffect(() => {
void load()
if (isTauriHost()) {
tauriInvoke<MemoryStats>('desktop_memory_stats')
.then(setStats)
.catch(() => {})
}
}, [load])
return (
<div className="space-y-4">
{error && <ErrorBanner envelope={error} />}
{stats && (
<div className="grid gap-3 sm:grid-cols-3">
<Card>
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold">{stats.wakeup_ready}</p>
<p className="text-xs text-muted-foreground">活跃记忆</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold">{stats.pending_review}</p>
<p className="text-xs text-muted-foreground">待审核</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold">{stats.total}</p>
<p className="text-xs text-muted-foreground">总记录</p>
</CardContent>
</Card>
</div>
)}
{stats && Object.keys(stats.by_type).length > 0 && (
<Card>
<CardContent className="p-4">
<div className="flex flex-wrap gap-2">
{Object.entries(stats.by_type)
.sort(([, a], [, b]) => b - a)
.map(([type, count]) => (
<Badge key={type} variant="secondary" className="text-xs">
{type}: {count}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">系统状态</CardTitle>
<CardDescription>
配置、Vault、会话来源与 MCP 注册情况一览。
</CardDescription>
</div>
<Button size="sm" variant="outline" onClick={load} disabled={loading}>
{loading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
刷新
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{!status && !loading && (
<p className="text-sm text-muted-foreground">
填写 config_path 与 cwd 后自动加载。
</p>
)}
{status && (
<>
<div className="grid gap-2 sm:grid-cols-2">
<StatusRow label="config_path 存在" ok={status.config_exists} />
<StatusRow label="cwd 存在" ok={status.cwd_exists} detail={status.cwd} />
<StatusRow
label="Vault 可访问"
ok={status.vault_available}
detail={status.vault_root ?? undefined}
/>
<StatusRow
label="会话来源就绪"
ok={status.session_sources_available}
/>
<StatusRow
label="MCP 配置文件检测"
ok={status.mcp_config_detected}
/>
<StatusRow
label="Claude 注册 Spool MCP"
ok={status.claude_mcp_registered}
/>
<StatusRow
label="Codex 注册 Spool MCP"
ok={status.codex_mcp_registered}
/>
</div>
<Separator />
<div className="space-y-3">
<div>
<p className="text-xs font-medium text-muted-foreground">
spool-mcp 可执行路径建议
</p>
<pre className="mt-1 overflow-auto rounded-md border bg-muted/30 p-2 text-xs">
{status.spool_mcp_command}
</pre>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">
Claude MCP 配置片段
</p>
<pre className="mt-1 overflow-auto rounded-md border bg-muted/30 p-2 text-xs">
{status.claude_mcp_snippet}
</pre>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">
Codex MCP 配置片段
</p>
<pre className="mt-1 overflow-auto rounded-md border bg-muted/30 p-2 text-xs">
{status.codex_mcp_snippet}
</pre>
</div>
</div>
{status.recent_enhancement && (
<>
<Separator />
<div>
<p className="text-xs font-medium text-muted-foreground">
最近一次 Prompt 优化 trace
</p>
<pre className="mt-1 max-h-[300px] overflow-auto rounded-md border bg-muted/30 p-2 text-xs">
{JSON.stringify(status.recent_enhancement, null, 2)}
</pre>
</div>
</>
)}
</>
)}
</CardContent>
</Card>
</div>
)
}
function StatusRow({
label,
ok,
detail,
}: {
label: string
ok: boolean
detail?: string
}) {
return (
<div className="flex items-start gap-2 rounded-md border bg-muted/10 px-3 py-2">
{ok ? (
<CheckCircle2 className="mt-0.5 h-4 w-4 text-emerald-500" />
) : (
<XCircle className="mt-0.5 h-4 w-4 text-destructive" />
)}
<div className="min-w-0 flex-1">
<p className="text-sm">{label}</p>
{detail && (
<p className="truncate text-xs text-muted-foreground" title={detail}>
{detail}
</p>
)}
</div>
<Badge variant={ok ? 'default' : 'muted'} className="shrink-0 text-[10px]">
{ok ? 'OK' : 'N/A'}
</Badge>
</div>
)
}