Files
roboco/panel/src/components/settings/self-hosted-section.tsx
T
roboco-app[bot]GitHubroboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>Frontend Developer 1Frontend DocumenterFrontend Developer 2Renn FRenzo F
9d39005c58 [73275ff0] Panel consistency & UX wave: forms audit, command palette, kanban merge, responsiveness (#694)
* [170c9578] Frontend: Panel consistency & UX wave (forms audit, command palette, kanban merge, responsiveness) (#688)

* [f1957610] Stream1-A: Project form sync (#667)

* [f1957610] feat(panel): expose codegen_command in create-project dialog

Add the Codegen Command input to create-project-dialog.tsx, mirroring
the field already present in edit-project-dialog.tsx. All other
fields named in this task (git_provider, github_installation_id,
environments, protected_branches, video_engine_enabled,
monthly_budget_usd with gt=0 client validation, sandbox_extensions)
were already implemented on this branch's base by prior work, and the
ProjectCreate/ProjectUpdate types in types/index.ts already match the
backend ProjectCreateRequest/ProjectUpdateRequest schemas exactly --
no further changes were needed there.

* [f1957610] docs(forms): add project-fields-audit reference for future field consistency

Create a living audit of which project configuration fields are exposed in the create vs. edit dialogs, mapping to the backend ProjectCreateRequest/ProjectUpdateRequest schemas. This serves as a future reference to prevent field-sync gaps and documents the intentional asymmetry (create focuses on git setup, edit adds autonomy/maintenance toggles). Includes a checklist for adding new project fields in the future.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [515697f4] feat(panel): settings save feedback + forms-audit.md living reference (#669)

Add per-toggle confirmation toasts to the four Settings-page prefs
(notifications, sound, auto refresh, refresh interval) so an immediate
write is never indistinguishable from a silent failure. These prefs
stay on the already-shipped client-persisted useUIStore pattern
(CHANGELOG.md "Settings preferences persist as real client prefs
instead of 422-ing as theater") rather than settingsApi, since the
backend _VALIDATORS allowlist deliberately excludes them and the
parent task scoped this stream as needing no backend schema changes.

Check in docs/forms-audit.md: a living form x field x verdict table
covering Stream1-A (project dialogs), Stream1-B (task dialogs), and
this settings work, with a header note that future backend schema
changes require a row update. Fixes the project-slug help text
(now correctly says letters/numbers/hyphens, not just hyphens).

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [80a215a4] Stream2-A: Command palette component (#670)

* [80a215a4] feat(panel): Cmd+K command palette component

Radix Dialog + combobox pattern searching tasks/agents/projects/pages,
localStorage recents under roboco-cmd-recents, keyboard nav (arrows/
Enter/Escape), mounted globally in the dashboard layout.

* [80a215a4] fix(panel): restore fields dropped from ui-store.ts by prior merge

Stream1-C's merge stripped notificationsEnabled, soundEnabled,
autoRefresh, refreshIntervalSeconds, a2aContextOpen, quickActionIds,
productsView, and projectsView from the shared UI store, breaking
typecheck for settings/quick-actions/products/projects/a2a/notification
consumers repo-wide. Restored per already-committed tests + consumers.

* [80a215a4] docs(panel): add command palette reference guide

Documents the global Cmd+K search feature: usage (keyboard shortcuts, search categories, recents), architecture (CommandPalette component, useCommandPalette hook, fuzzy-match and recents helpers), data flow, and verification against live API data.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [cdc371d1] Stream4-A: Responsiveness audit and fix — wide-content pages (#671)

* [cdc371d1] fix(panel): bump Button sm size to 36px touch-target floor

Button's size="sm" variant was h-8 (32px), used as literal row-action
buttons on the overview page's CEO Approval/PR Review queues and other
controls across settings/metrics/agents/a2a. Bump to h-9 (36px) to meet
the touch-target floor everywhere at once, keeping the smaller
horizontal padding/gap intact for visual density.

* [cdc371d1] fix(panel): make AlertDialog scroll its body at short viewport heights

AlertDialogContent lacked the max-h-[85vh]/overflow-y-auto that the
sibling DialogContent already has, and AlertDialogFooter lacked
DialogFooter's sticky bottom-0 pinning. A tall description at a short
viewport height (mobile landscape) could clip the action buttons off
screen with no way to reach them. Affects the settings page's
GitHubAppCredentialsCard/FeatureFlagsCard confirm dialogs (and every
other AlertDialog app-wide). Ports DialogContent's already-solved
scroll pattern onto AlertDialogContent/Footer.

* [cdc371d1] fix(panel): wrap Scorecards Members table in ResponsiveTable

metrics/scorecards-tab.tsx's 9-column Members table was a bare
&lt;Table&gt; with no mobile-card fallback, unlike its sibling tables in
the same file (Rework, SpawnWaste) and sessions-table.tsx, which
already use the established ResponsiveTable wrapper. Add a MemberCard
component and wrap the table so it stacks as cards below md instead of
forcing a cramped in-card horizontal scroll on a 375px viewport.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

* [b25fca69] Stream2-B: Header integration for command palette (#675)

* [b25fca69] Wire header search into Stream2-A command palette: click trigger via useUIStore.setCommandPaletteOpen, remove disabled input and Coming Soon tooltip remnants

* [b25fca69] Wire header search into Stream2-A command palette: click trigger via useUIStore.setCommandPaletteOpen, remove disabled input and Coming Soon tooltip remnants

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

* [e4ce5b9a] Stream3-A: Tasks page List|Kanban tabs + kanban embed (#674)

* [e4ce5b9a] feat(tasks): add List|Kanban tabs to tasks page sharing URL filter state

Add top-level List|Kanban Tabs above the tasks page filter bar. List
tab renders the existing TaskFilters+TaskTable unchanged; Kanban tab
embeds the existing DevKanban/QaKanban/PrReviewKanban/PmKanban views
via nested sub-tabs (dev/qa/pr-review/pm), mirroring the standalone
/kanban page's own tab styling (tooltip-wrapped triggers, pickTab
helper). Both tabs read/write `tab`/`view` query params through the
page's existing updateParams pattern, so all filters persist across
tab switches. The four kanban view wrappers gain an optional
controlled team/onTeamChange pair so the team filter is shared
bidirectionally with the List tab's team filter, while staying
backward compatible (uncontrolled, initialTeam-only) for the
standalone /kanban route. KanbanBoard's dnd-kit drag-and-drop and
mobile single-column navigation are untouched.

* [e4ce5b9a] docs(tasks): add tasks-page-tabs.md documenting List|Kanban tab structure and shared filter state

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [18c52802] feat(panel): redirect /kanban to Tasks kanban tab, remove sidebar entry, swap bottom tab bar to Agents (#679)

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

* Stream4-B: Responsiveness fixes — remaining dashboard pages (#676)

* [fee25542] fix(a11y): bump sub-36px icon-sm touch targets to 36px on remaining pages

Both kb-search-bar.tsx (Knowledge Base search clear button) and
self-hosted-section.tsx (Settings token show/hide button) used
Button size="icon-sm" (32px) for an absolutely-positioned input
adornment, below the 36px minimum touch-target size. Bumped both
to size="icon" (36px, matching the 36px input height) and adjusted
the absolute-position offset so the button still sits fully inside
each input's existing right padding reservation.

Audited every remaining dashboard page (everything Stream4-A's
wide-content/table fixes didn't already cover): no un-wrapped wide
tables remain (every <Table> already rides ResponsiveTable), and
every DialogContent across the repo already inherits or supplies
max-h-[*vh] + overflow-y-auto, so dialogs stay usable at small
viewport heights.

* [fee25542] fix(a11y): re-land sub-36px touch target and overflow fixes after sync_branch reset them again

Re-applies the fda2ac0c fix content a third time -- sync_branch's
rebase+force-push reset the branch and working tree back to the stale
f9f45d9f ref (the round-1-only state) instead of preserving the local
commits ahead of it, discarding the round-2 fix yet again.

- quick-actions-card.tsx (Overview dashboard customize dialog): reorder
  arrows drop the h-6 w-6 override, falling back to Button's 36px icon
  default
- agent-card.tsx (Agents page grid): DM / dedicated-chat / actions-menu
  icon buttons drop their h-6 w-6 override, now 36px
- product-card-grid.tsx / project-card-grid.tsx (Products/Projects card
  view): edit/external-link icon buttons drop h-6 w-6, now 36px
- environment-ladder-editor.tsx (Edit Project dialog): move-up/move-down/
  remove-rung icon buttons drop their h-6/h-8 overrides, now 36px; the
  per-rung row now scrolls horizontally within its own bordered box
  (overflow-x-auto + min-w-max) instead of overflowing at 375px now
  that the icon buttons are back to full width
- acceptance-criteria-editor.tsx / dependency-selector.tsx (task create/
  edit dialogs): remove-row icon buttons drop their h-6/h-8 overrides,
  now 36px
- tab-commits.tsx: "Linked Commits" header's fixed 3-column grid now
  stacks to one column below sm, and the branch/PR badge row scrolls
  horizontally in its own container instead of the page

panel lint + tsc --noEmit are both clean.

* [fee25542] fix(a11y): bump tab-commits.tsx delete-commit button to 36px touch target

The per-commit unlink button used className="h-7 w-7" (28px), the one
sub-36px target the prior re-land commits (fda2ac0c/8e00303f/d8408c77/
34ea8fe1/e6bee7c8) didn't cover -- their content only fixed the header
grid-stack overflow in this file, not this button. Bumped to h-9 w-9
(36px) matching the Button component's own size="icon" default used
everywhere else in this fix series, and bumped the icon from h-3 w-3
to h-3.5 w-3.5 to stay visually proportional at the larger target.

panel-gate (lint + tsc --noEmit + vitest) is green.

* [fee25542] fix(a11y): remove trailing narrative JSX comments in self-hosted-section.tsx

Removes the 9 {/* ... */} comments flagged by the conventions validator's
no_inline_comments rule (F-8db499ac) — Header, Base URL input, Auth token
input, Test Connection button, inline result badge, and the three empty-
state section markers. Each block is already self-evident from its JSX
composition (distinct Input/Button/Badge groupings and conditional guards
showNoUrlState/showErrorState/showNoModelsState/showModelList), so no
docstring/JSDoc replacement is needed. Pure deletion, no behavior change.

The two remaining open findings (F-8a39564c, F-4308219d) allege the
touch-target/overflow fixes across 8 files are missing from this branch --
re-verified via roboco_git_log(branch=<task branch>), which reads the real
ref directly, that the branch tip is c629c3e1 and already contains those
fixes (h-9 w-9 delete button + overflow-x-auto header row in tab-commits.tsx,
overflow-x-auto rung rows in environment-ladder-editor.tsx, no shrunk
icon-sm/h-6/h-7 overrides left in the other 6 files), confirmed by reading
every file on disk in this worktree. No code change needed for those two;
resolved via verification evidence instead of a 9th re-land.

pnpm lint + pnpm typecheck both clean.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

* [1ff154da] restore(panel): re-add AutoRefreshDriver and ScrollJumpButtons to dashboard layout (#691)

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

* [6ee71578] Add sequence field to task dialogs; fix pr_gate forms-audit.md findings (#693)

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

---------

Co-authored-by: roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [73275ff0] fix(panel): review follow-ups — budget help text, touch-visible unlink, docs consolidation

- The task budget help text and validation toast said empty falls back
  to a task-type default; that default table was removed (null = no
  cap) — both strings now say so, and the forms audit row documents
  the correction instead of claiming ok over a stale label.
- The per-commit unlink button was hover-revealed only, invisible on
  touch devices and to keyboard focus; it now also reveals on
  focus-visible and coarse pointers.
- The forms audit moves from the docs root into the governed
  docs/frontend/forms/ tree, cross-linked both ways with the project
  fields reference it overlapped, and both are registered in the
  frontend docs index; the tasks-page-tabs doc's standalone-reuse
  rationale now states the /kanban redirect reality.
- The two 20px tree-expand chevrons in the tasks table are left as-is
  deliberately: explicit dense-row overrides, where a 36px target
  would break table density.

---------

Co-authored-by: roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Renzo F <45401804+rennf93@users.noreply.github.com>
2026-07-24 19:05:29 +00:00

384 lines
13 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import {
useSelfHostedConfig,
useSetSelfHostedConfig,
useTestSelfHosted,
useSelfHostedModels,
useRefreshSelfHostedModels,
} from "@/hooks/use-providers";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import {
AlertTriangle,
CheckCircle2,
Eye,
EyeOff,
RefreshCw,
Server,
XCircle,
} from "lucide-react";
import { toast } from "sonner";
import type { SelfHostedTestResult } from "@/lib/api/providers";
// Relative-time helper (no date-fns dependency).
function relativeTime(isoDate: string | null): string {
if (!isoDate) return "never";
const diff = Date.now() - new Date(isoDate).getTime();
const s = Math.floor(diff / 1000);
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
return `${Math.floor(h / 24)}d ago`;
}
function errMsg(e: unknown): string {
return e instanceof Error ? e.message : "Unknown error";
}
/** Props exposed to the parent (ai-routing-card) so it can read test status. */
export interface SelfHostedSectionProps {
/** Called whenever a successful connection test completes. */
onTestSuccess?: (modelCount: number) => void;
/** The last known test result — driven by parent if the parent stores it. */
testResult?: SelfHostedTestResult | null;
/** Called when the user clicks "Test Connection" and we get any result. */
onTestResult?: (result: SelfHostedTestResult) => void;
}
export function SelfHostedSection({
onTestSuccess,
testResult,
onTestResult,
}: SelfHostedSectionProps) {
const { data: config } = useSelfHostedConfig();
const { data: models = [] } = useSelfHostedModels();
const saveConfig = useSetSelfHostedConfig();
const testConnection = useTestSelfHosted();
const refreshModels = useRefreshSelfHostedModels();
// Local form state
const [baseUrl, setBaseUrl] = useState("");
const [authToken, setAuthToken] = useState("");
const [showToken, setShowToken] = useState(false);
// Timestamps for "Last refreshed" label
const [lastRefreshed, setLastRefreshed] = useState<string | null>(null);
const hasSavedUrl = !!config?.base_url;
// ---- Save handler --------------------------------------------------------
const handleSave = async () => {
const url = baseUrl.trim();
if (!url) {
toast.error("Enter a base URL first");
return;
}
try {
await saveConfig.mutateAsync({
base_url: url,
...(authToken ? { auth_token: authToken } : {}),
});
toast.success("Self-hosted config saved");
setBaseUrl("");
setAuthToken("");
} catch (e) {
toast.error("Save failed: " + errMsg(e));
}
};
// ---- Test Connection handler ---------------------------------------------
const handleTest = async () => {
try {
const result = await testConnection.mutateAsync();
onTestResult?.(result);
if (result.ok) {
onTestSuccess?.(result.model_count ?? 0);
setLastRefreshed(new Date().toISOString());
const count = result.model_count ?? 0;
toast.success(
`Connected — ${count} model${count === 1 ? "" : "s"} available`,
);
} else {
toast.error(`Connection failed: ${result.error ?? "unknown error"}`);
}
} catch (e) {
toast.error("Test failed: " + errMsg(e));
}
};
// ---- Refresh Models handler ----------------------------------------------
const handleRefresh = useCallback(async () => {
try {
await refreshModels.mutateAsync();
setLastRefreshed(new Date().toISOString());
toast.success("Model list refreshed");
} catch (e) {
toast.error("Refresh failed: " + errMsg(e));
}
}, [refreshModels]);
// ---- Retry handler (from error empty state) ------------------------------
const handleRetry = () => handleTest();
// Determine which empty state to show, if any.
const showNoUrlState = !hasSavedUrl;
const showErrorState =
hasSavedUrl && testResult?.ok === false && !showNoUrlState;
const showNoModelsState =
hasSavedUrl &&
testResult?.ok === true &&
models.length === 0 &&
!showNoUrlState;
const showModelList =
hasSavedUrl && testResult?.ok === true && models.length > 0;
return (
<section className="space-y-4">
<div className="flex items-center gap-2">
<Server className="h-4 w-4 text-muted-foreground" />
<HelpTip label="Routes agents to any locally-run OpenAI-compatible endpoint (Ollama, vLLM, LM Studio) instead of a cloud provider.">
<Label className="text-sm font-medium">Self-Hosted LLM</Label>
</HelpTip>
{testResult?.ok === true && (
<HelpTip label="The last Test Connection call succeeded — the model list below reflects this endpoint.">
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
<CheckCircle2 className="h-3 w-3" /> connected
</Badge>
</HelpTip>
)}
{testResult?.ok === false && (
<HelpTip label="The last Test Connection call failed — see the error detail below.">
<Badge className="bg-red-500/10 text-red-600 border-0">
<XCircle className="h-3 w-3" /> error
</Badge>
</HelpTip>
)}
</div>
<div className="space-y-1">
<HelpTip label="Any OpenAI-compatible endpoint — e.g. Ollama, vLLM, LM Studio.">
<Label className="text-xs text-muted-foreground">Base URL</Label>
</HelpTip>
<div className="flex gap-2">
<Input
type="text"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder={
hasSavedUrl
? (config.base_url ?? "http://localhost:11434")
: "http://localhost:11434"
}
className="font-mono text-sm"
/>
</div>
</div>
<div className="space-y-1">
<HelpTip label="Stored encrypted server-side; never displayed once saved.">
<Label className="text-xs text-muted-foreground">
Auth token{" "}
<span className="text-muted-foreground/60">(optional)</span>
</Label>
</HelpTip>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
type={showToken ? "text" : "password"}
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
placeholder={
config?.has_token
? "•••••••••••• (leave blank to keep)"
: "sk-… or Bearer token"
}
className="pr-10"
/>
<HelpTip label={showToken ? "Hide token" : "Show token"}>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowToken((v) => !v)}
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</HelpTip>
</div>
<HelpTip label="Saves the Base URL above together with any token typed here, in one combined config — not just this token.">
<Button onClick={handleSave} disabled={saveConfig.isPending}>
{saveConfig.isPending ? "Saving…" : "Save"}
</Button>
</HelpTip>
</div>
{config?.has_token && (
<p className="text-xs text-muted-foreground">
A token is stored. Type a new value to update it.
</p>
)}
{!config?.has_token && (
<p className="text-xs text-muted-foreground">
Stored Fernet-encrypted server-side; never returned by the API.
</p>
)}
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
onClick={handleTest}
disabled={!hasSavedUrl || testConnection.isPending}
>
{testConnection.isPending ? (
<>
<RefreshCw className="mr-1.5 h-3.5 w-3.5 animate-spin" />
Testing
</>
) : (
"Test Connection"
)}
</Button>
{testResult?.ok === true && (
<Badge className="bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-0 px-3 py-1 text-xs">
<CheckCircle2 className="h-3.5 w-3.5" />
Connected &mdash; {testResult.model_count ?? 0} model
{(testResult.model_count ?? 0) === 1 ? "" : "s"} available
</Badge>
)}
{testResult?.ok === false && (
<Badge className="bg-red-500/10 text-red-700 dark:text-red-400 border-0 px-3 py-1 text-xs">
<XCircle className="h-3.5 w-3.5" />
{testResult.error ?? "Connection failed"}
</Badge>
)}
</div>
{showNoUrlState && (
<div className="rounded-md border border-dashed p-4 text-sm text-muted-foreground">
<p className="font-medium">No base URL configured</p>
<p className="mt-1 text-xs">
Enter the URL of your self-hosted LLM endpoint (e.g.{" "}
<code>http://localhost:11434</code> for Ollama) and click{" "}
<strong>Save</strong>. Then click <strong>Test Connection</strong>{" "}
to verify and discover available models.
</p>
</div>
)}
{showErrorState && (
<div className="rounded-md border border-red-200 bg-red-50 p-4 dark:border-red-900/40 dark:bg-red-950/20">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-600" />
<div className="flex-1 space-y-1 text-sm">
<p className="font-medium text-red-700 dark:text-red-400">
Last test failed
</p>
<p className="text-xs text-red-600 dark:text-red-500">
{testResult?.error ?? "Unknown error"}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRetry}
disabled={testConnection.isPending}
>
Retry
</Button>
</div>
</div>
)}
{showNoModelsState && (
<div className="rounded-md border border-amber-200 bg-amber-50 p-4 dark:border-amber-900/40 dark:bg-amber-950/20">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
<p className="text-sm text-amber-700 dark:text-amber-400">
Connected but no models found. Pull a model first (e.g.{" "}
<code className="font-mono">ollama pull llama3.2</code>) then
click <strong>Refresh Models</strong>.
</p>
</div>
</div>
)}
{showModelList && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">
Last refreshed:{" "}
<HelpTip
label={
lastRefreshed
? new Date(lastRefreshed).toLocaleString()
: undefined
}
>
<span className="font-medium">
{relativeTime(lastRefreshed)}
</span>
</HelpTip>
</p>
<HelpTip label="Re-queries the endpoint's model list only — doesn't re-run the connection test above.">
<Button
variant="ghost"
size="sm"
onClick={handleRefresh}
disabled={refreshModels.isPending}
>
{refreshModels.isPending ? (
<>
<RefreshCw className="mr-1.5 h-3 w-3 animate-spin" />
Refreshing
</>
) : (
<>
<RefreshCw className="mr-1.5 h-3 w-3" />
Refresh Models
</>
)}
</Button>
</HelpTip>
</div>
<div className="divide-y rounded-md border">
{models.map((m) => (
<div
key={m.model_name}
className="flex items-center justify-between px-3 py-2"
>
<div>
<span className="text-sm font-medium">{m.display_name}</span>
<span className="ml-2 font-mono text-xs text-muted-foreground">
{m.model_name}
</span>
</div>
<HelpTip label="Found by probing the endpoint's model list — not manually added.">
<Badge variant="secondary" className="text-xs">
auto-discovered
</Badge>
</HelpTip>
</div>
))}
</div>
</div>
)}
</section>
);
}