Use Nova integration cards for desktop tools

This commit is contained in:
Sreeram Sreedhar 2026-06-23 18:55:03 -07:00
parent 1729cc4407
commit 10e18fc9b8
2 changed files with 130 additions and 83 deletions

View file

@ -9,6 +9,7 @@ import {
CardTitle,
} from "@ui/components/card"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
@ -38,10 +39,14 @@ import {
disconnectDesktopTool,
type DesktopToolId,
type DesktopToolPreview,
type DesktopToolStatus,
previewConnectDesktopTool,
previewDisconnectDesktopTool,
} from "@/lib/tools"
import {
sortDesktopToolCards,
toDesktopToolCard,
type DesktopToolCard,
} from "@/lib/tool-catalog"
type AppInfo = {
name: string
@ -62,7 +67,7 @@ export default function SettingsPage() {
const [smfsBusy, setSmfsBusy] = useState<string | null>(null)
const [smfsLogs, setSmfsLogs] = useState<string | null>(null)
const [smfsProfile, setSmfsProfile] = useState<SmfsProfile | null>(null)
const [tools, setTools] = useState<DesktopToolStatus[]>([])
const [tools, setTools] = useState<DesktopToolCard[]>([])
const [toolsError, setToolsError] = useState<string | null>(null)
const [toolsBusy, setToolsBusy] = useState<string | null>(null)
const [toolPreview, setToolPreview] = useState<DesktopToolPreview | null>(
@ -81,7 +86,8 @@ export default function SettingsPage() {
const refreshTools = useCallback(async () => {
setToolsError(null)
try {
setTools(await detectDesktopTools())
const statuses = await detectDesktopTools()
setTools(sortDesktopToolCards(statuses.map(toDesktopToolCard)))
} catch (err) {
setToolsError(formatError(err, "Could not detect tools"))
}
@ -474,54 +480,17 @@ export default function SettingsPage() {
Refresh
</Button>
</div>
<div className="space-y-3">
<div className="grid gap-3 md:grid-cols-3">
{tools.map((tool) => (
<div
<SettingsToolCard
key={tool.id}
className="rounded-md border border-border bg-muted/10 p-3"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<p className="font-medium">{tool.name}</p>
<span className="rounded-full border border-border px-2 py-0.5 text-[11px] text-muted-foreground">
{tool.connected
? "Connected"
: tool.detected
? "Detected"
: "Not detected"}
</span>
</div>
<p className="text-muted-foreground text-xs">
{tool.detail}
</p>
<p
className="truncate font-mono text-muted-foreground text-xs"
title={tool.configPath}
>
{tool.configPath}
</p>
</div>
<div className="flex shrink-0 gap-2">
<Button
type="button"
variant="outline"
disabled={toolsBusy !== null}
onClick={() => previewToolAction(tool.id, "connect")}
>
Preview
</Button>
<Button
type="button"
variant="outline"
disabled={toolsBusy !== null || !tool.connected}
onClick={() => previewToolAction(tool.id, "disconnect")}
>
Disconnect
</Button>
</div>
</div>
</div>
tool={tool}
busy={toolsBusy !== null}
onPreviewConnect={() => previewToolAction(tool.id, "connect")}
onPreviewDisconnect={() =>
previewToolAction(tool.id, "disconnect")
}
/>
))}
</div>
{toolsError ? (
@ -600,6 +569,82 @@ const SHORTCUT_OPTIONS = [
{ label: "⌘ ⌥ M", accelerator: "CommandOrControl+Option+M" },
] as const
function SettingsToolCard({
tool,
busy,
onPreviewConnect,
onPreviewDisconnect,
}: {
tool: DesktopToolCard
busy: boolean
onPreviewConnect: () => void
onPreviewDisconnect: () => void
}) {
const Icon = tool.icon
return (
<div
className={cn(
"group relative flex min-h-[230px] flex-col rounded-xl border border-[#0D121A] bg-[#080B0F] p-4 pt-14 text-left transition-all duration-300 hover:border-[#3374FF]/50 hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:200%_auto] hover:bg-[center_top_1rem] hover:bg-no-repeat",
)}
>
<span className="absolute top-3 left-3">
<SettingsToolStatusPill tool={tool} />
</span>
<span className="absolute top-2 right-2 text-[#8BC6FF] opacity-60 transition-opacity group-hover:opacity-100">
<Icon className="size-8" />
</span>
<h3 className="text-sm font-medium text-white">{tool.name}</h3>
<p className="mt-0.5 line-clamp-2 text-[#8B8B8B] text-xs leading-relaxed">
{tool.description}
</p>
<div className="mt-4 min-w-0 space-y-2">
<p
className="truncate font-mono text-[#737373] text-[11px]"
title={tool.configPath}
>
{tool.configPath}
</p>
<p className="line-clamp-2 text-[#737373] text-xs">{tool.detail}</p>
</div>
<div className="mt-auto flex gap-2 pt-4">
<Button
type="button"
variant="outline"
disabled={busy}
onClick={onPreviewConnect}
className="h-8 flex-1 rounded-lg border-white/[0.08] bg-white/[0.03] text-xs hover:bg-white/[0.06]"
>
Preview
</Button>
<Button
type="button"
variant="outline"
disabled={busy || !tool.connected}
onClick={onPreviewDisconnect}
className="h-8 flex-1 rounded-lg border-white/[0.08] bg-white/[0.03] text-xs hover:bg-white/[0.06]"
>
Disconnect
</Button>
</div>
</div>
)
}
function SettingsToolStatusPill({ tool }: { tool: DesktopToolCard }) {
if (tool.connected) {
return (
<span className="inline-block rounded-full bg-[#00AC3F]/10 px-1.5 py-0.5 font-medium text-[#00AC3F] text-[10px]">
Connected
</span>
)
}
return (
<span className="inline-block rounded-full bg-[#737373]/10 px-1.5 py-0.5 font-medium text-[#737373] text-[10px]">
{tool.detected ? "Detected" : "Not detected"}
</span>
)
}
function Row({ label, value }: { label: string; value: string }) {
return (
<div className="grid grid-cols-[8rem_minmax(0,1fr)] gap-3">

View file

@ -502,27 +502,25 @@ function ToolCard({
onPreview: () => void
}) {
const Icon = tool.icon
const canPreview = !busy && !tool.connected
return (
<div className="flex min-h-[280px] flex-col rounded-2xl border border-white/[0.07] bg-[#0B1018]/70 p-4 shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.45)]">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[#00173C] text-[#8BC6FF] ring-1 ring-[#2261CA33]">
<Icon className="size-5" />
</span>
<div className="min-w-0">
<p className="truncate font-medium text-white">{tool.name}</p>
<p className="text-[#737373] text-xs">{toolStatusLabel(tool)}</p>
</div>
</div>
{tool.connected ? (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-400/10 px-2 py-1 text-[11px] text-emerald-300">
<span className="size-1.5 rounded-full bg-emerald-300" />
Active
</span>
) : null}
</div>
<p className="mt-4 line-clamp-2 text-[#A1A1AA] text-sm leading-5">
{tool.tagline}
<button
type="button"
onClick={canPreview ? onPreview : undefined}
disabled={busy}
className={cn(
"group relative flex min-h-[250px] w-full cursor-pointer flex-col rounded-xl border border-[#0D121A] bg-[#080B0F] p-4 pt-14 text-left transition-all duration-300 hover:border-[#3374FF]/50 hover:bg-[url('/onboarding/bg-gradient-1.png')] hover:bg-[length:200%_auto] hover:bg-[center_top_1rem] hover:bg-no-repeat disabled:cursor-default disabled:opacity-80",
)}
>
<span className="absolute top-3 left-3">
<ToolStatusPill tool={tool} />
</span>
<span className="absolute top-2 right-2 text-[#8BC6FF] opacity-60 transition-opacity group-hover:opacity-100">
<Icon className="size-8" />
</span>
<h3 className="text-sm font-medium text-white">{tool.name}</h3>
<p className="mt-0.5 line-clamp-2 text-[#8B8B8B] text-xs leading-relaxed">
{tool.description}
</p>
<div className="mt-4 grid gap-2 text-xs">
<InfoRow label="Config" value={tool.configPath} />
@ -537,12 +535,7 @@ function ToolCard({
<p className="mt-4 text-[#737373] text-xs leading-5">
{tool.restartHint}
</p>
<button
type="button"
onClick={onPreview}
disabled={busy || tool.connected}
className="mt-auto inline-flex h-9 items-center justify-center rounded-xl bg-white px-3 font-medium text-[#05080D] text-sm disabled:cursor-default disabled:bg-white/[0.08] disabled:text-[#737373]"
>
<span className="mt-auto inline-flex h-9 items-center justify-center rounded-xl bg-white px-3 font-medium text-[#05080D] text-sm transition-transform group-hover:scale-[1.01] group-disabled:bg-white/[0.08] group-disabled:text-[#737373] group-disabled:group-hover:scale-100">
{tool.connected
? "Connected"
: busy
@ -550,8 +543,23 @@ function ToolCard({
: tool.primaryAction === "connect"
? "Connect"
: "Set up"}
</button>
</div>
</span>
</button>
)
}
function ToolStatusPill({ tool }: { tool: DesktopToolCard }) {
if (tool.connected) {
return (
<span className="inline-block rounded-full bg-[#00AC3F]/10 px-1.5 py-0.5 font-medium text-[#00AC3F] text-[10px]">
Connected
</span>
)
}
return (
<span className="inline-block rounded-full bg-[#737373]/10 px-1.5 py-0.5 font-medium text-[#737373] text-[10px]">
{tool.detected ? "Detected" : "Not detected"}
</span>
)
}
@ -775,12 +783,6 @@ function NovaBackground() {
)
}
function toolStatusLabel(tool: DesktopToolCard) {
if (tool.connected) return "Connected"
if (tool.detected) return "Detected"
return "Not detected"
}
function formatSmfsState(state?: SmfsStatus["state"]) {
switch (state) {
case "degraded":