open-swe/ui/src/routes/admin.tsx
Johannes du Plessis 876836bdfe
Normalize frontend typography + enable font smoothing (#1546)
Lighter, more consistent type across the dashboard and agent UI:
unify title sizes, drop semibold/bold in chrome to medium, harmonize
stray text-sm to text-xs, normalize chat markdown headings, and enable
antialiased font smoothing (fixes heavy text on dark backgrounds).

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-16 14:56:11 -07:00

733 lines
23 KiB
TypeScript

import { Link, Navigate, createFileRoute } from "@tanstack/react-router"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { CaretRightIcon } from "@phosphor-icons/react"
import { useEffect, useMemo, useState } from "react"
import type {
DatadogConnectBody,
LangSmithConnectBody,
ModelOption,
TeamSettings,
UserMapping,
} from "@/lib/api"
import { AppShell, SettingsRow, SettingsSection } from "@/components/AppShell"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Skeleton } from "@/components/ui/skeleton"
import { api } from "@/lib/api"
import { useSession } from "@/lib/session"
export const Route = createFileRoute("/admin")({ component: AdminPage })
function AdminPage() {
const session = useSession()
const options = useQuery({
queryKey: ["options"],
queryFn: api.options,
enabled: !!session.data?.is_admin,
})
if (session.isLoading) {
return (
<main className="p-6">
<Skeleton className="h-64 w-full" />
</main>
)
}
if (!session.data) return <Navigate to="/login" />
if (!session.data.is_admin) return <Navigate to="/my-settings" />
return (
<AppShell
user={session.data}
title="Admin"
description="Workspace-wide defaults and user mappings."
>
<GlobalDefaultsSection models={options.data?.models ?? []} />
<TriggerReviewSection />
<SettingsSection title="Evals">
<Link
to="/admin/evals"
className="flex items-center justify-between gap-6 px-4 py-3 hover:bg-muted/40"
>
<div className="flex flex-col gap-0.5">
<span className="text-xs font-medium text-foreground">
Reviewer eval
</span>
<span className="text-xs text-muted-foreground">
Run the offline reviewer benchmark and watch its output stream
live.
</span>
</div>
<CaretRightIcon className="size-3.5 shrink-0 text-muted-foreground" />
</Link>
</SettingsSection>
<ObservabilityCredentialsSection />
<UserMappingsSection enabled={!!session.data.is_admin} />
</AppShell>
)
}
const PR_URL_RE = /^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/
function TriggerReviewSection() {
const [url, setUrl] = useState("")
const [error, setError] = useState<string | null>(null)
const [message, setMessage] = useState<string | null>(null)
const parsed = useMemo(() => {
const match = PR_URL_RE.exec(url.trim())
if (!match) return null
const [, owner, repo, number] = match
if (!owner || !repo || !number) return null
return { owner, repo, number: Number(number) }
}, [url])
const trigger = useMutation({
mutationFn: () => {
if (!parsed) throw new Error("invalid PR URL")
return api.reReview(parsed.owner, parsed.repo, parsed.number)
},
onSuccess: (result) => {
setError(null)
setMessage(
result.queued
? "Review queued — a run is already in progress on this PR."
: "Review started."
)
},
onError: (e: Error) => {
setMessage(null)
setError(e.message)
},
})
return (
<SettingsSection
title="Trigger a review"
description="Manually start an Open SWE Review run on a pull request. The repository must be enabled for review."
>
<div className="flex flex-col gap-2 p-4">
<div className="flex items-center gap-2">
<Input
className="flex-1"
placeholder="https://github.com/owner/repo/pull/123"
value={url}
onChange={(e) => {
setUrl(e.target.value)
setMessage(null)
setError(null)
}}
/>
<Button
size="sm"
onClick={() => trigger.mutate()}
disabled={!parsed || trigger.isPending}
>
{trigger.isPending ? "Starting…" : "Start review"}
</Button>
</div>
{url.trim() && !parsed && (
<p className="text-xs text-muted-foreground">
Enter a full PR URL like https://github.com/owner/repo/pull/123
</p>
)}
{message && parsed && (
<p className="text-xs text-muted-foreground">
{message}{" "}
<Link
to="/agents/reviews/$owner/$repo/$number"
params={{
owner: parsed.owner,
repo: parsed.repo,
number: String(parsed.number),
}}
className="underline hover:text-foreground"
>
View review
</Link>
</p>
)}
{error && <p className="text-xs text-destructive">{error}</p>}
</div>
</SettingsSection>
)
}
const PAGE_SIZE = 20
function UserMappingsSection({ enabled }: { enabled: boolean }) {
const [error, setError] = useState<string | null>(null)
const [page, setPage] = useState(1)
const mappings = useQuery({
queryKey: ["adminUserMappings", page],
queryFn: () => api.adminListUserMappings(page, PAGE_SIZE),
enabled,
})
const total = mappings.data?.total ?? 0
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE))
useEffect(() => {
if (!mappings.isFetching && page > pageCount) {
setPage(pageCount)
}
}, [mappings.isFetching, page, pageCount])
const remove = useMutation({
mutationFn: (gh: string) => api.adminDeleteUserMapping(gh),
onSuccess: () => void mappings.refetch(),
onError: (e: Error) => setError(e.message),
})
const items = mappings.data?.items ?? []
return (
<SettingsSection
title="User mappings"
description="Mappings are created when users connect Slack from settings. Admins can remove stale mappings here."
>
<div className="flex flex-col gap-3 p-4">
{error && <span className="text-xs text-destructive">{error}</span>}
<div className="flex flex-col gap-0.5">
{mappings.isLoading ? (
<Skeleton className="h-32" />
) : !items.length ? (
<p className="text-xs text-muted-foreground">No mappings yet.</p>
) : (
items.map((m: UserMapping) => (
<div
key={m.github_login}
className="flex items-center justify-between gap-2 border-b border-border py-1.5 text-xs last:border-b-0"
>
<div className="flex min-w-0 flex-col">
<span className="truncate font-medium">{m.github_login}</span>
<span className="truncate text-xs text-muted-foreground">
{m.work_email}
{m.slack_user_id ? ` · ${m.slack_user_id}` : ""}
{m.source ? ` · ${m.source}` : ""}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => remove.mutate(m.github_login)}
disabled={remove.isPending}
>
Remove
</Button>
</div>
))
)}
</div>
{total > PAGE_SIZE && (
<div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
<span>
{total} mapping{total === 1 ? "" : "s"} · page {page} of{" "}
{pageCount}
</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1 || mappings.isFetching}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(pageCount, p + 1))}
disabled={page >= pageCount || mappings.isFetching}
>
Next
</Button>
</div>
</div>
)}
</div>
</SettingsSection>
)
}
function ObservabilityCredentialsSection() {
const qc = useQueryClient()
const creds = useQuery({
queryKey: ["teamCredentials"],
queryFn: api.getTeamCredentials,
})
const [error, setError] = useState<string | null>(null)
const [ddSite, setDdSite] = useState("datadoghq.com")
const [ddApiKey, setDdApiKey] = useState("")
const [ddAppKey, setDdAppKey] = useState("")
const [lsApiKey, setLsApiKey] = useState("")
const [lsEndpoint, setLsEndpoint] = useState("")
const onError = (e: Error) => setError(e.message)
const onSuccess = (
saved: Awaited<ReturnType<typeof api.getTeamCredentials>>
) => {
qc.setQueryData(["teamCredentials"], saved)
setError(null)
}
const connectDd = useMutation({
mutationFn: (body: DatadogConnectBody) => api.connectDatadog(body),
onSuccess: (saved) => {
onSuccess(saved)
setDdApiKey("")
setDdAppKey("")
},
onError,
})
const disconnectDd = useMutation({
mutationFn: () => api.disconnectDatadog(),
onSuccess,
onError,
})
const connectLs = useMutation({
mutationFn: (body: LangSmithConnectBody) => api.connectLangSmith(body),
onSuccess: (saved) => {
onSuccess(saved)
setLsApiKey("")
},
onError,
})
const disconnectLs = useMutation({
mutationFn: () => api.disconnectLangSmith(),
onSuccess,
onError,
})
const datadog = creds.data?.datadog
const langsmith = creds.data?.langsmith
const busy = creds.isLoading
return (
<SettingsSection
title="Observability credentials"
description="Team-wide Datadog and LangSmith credentials. Stored encrypted server-side and never exposed to the sandbox. Connecting enables read-only observability tools for agent runs."
>
<div className="divide-y divide-border">
<SettingsRow
label="Datadog"
description={
datadog?.connected
? `Connected · ${datadog.site ?? ""} · key ••••${datadog.api_key_last4 ?? ""}`
: "Connect Datadog to enable read-only metrics, logs, traces, and monitor tools."
}
control={
datadog?.connected ? (
<Button
variant="outline"
size="sm"
onClick={() => disconnectDd.mutate()}
disabled={disconnectDd.isPending}
>
Disconnect
</Button>
) : (
<div className="flex flex-col items-end gap-2">
<Input
className="w-56"
placeholder="datadoghq.com"
value={ddSite}
onChange={(e) => setDdSite(e.target.value)}
disabled={busy}
/>
<Input
className="w-56"
placeholder="API key"
type="password"
value={ddApiKey}
onChange={(e) => setDdApiKey(e.target.value)}
disabled={busy}
/>
<Input
className="w-56"
placeholder="Application key"
type="password"
value={ddAppKey}
onChange={(e) => setDdAppKey(e.target.value)}
disabled={busy}
/>
<Button
size="sm"
onClick={() =>
connectDd.mutate({
site: ddSite.trim(),
api_key: ddApiKey.trim(),
app_key: ddAppKey.trim(),
})
}
disabled={
connectDd.isPending ||
!ddSite.trim() ||
!ddApiKey.trim() ||
!ddAppKey.trim()
}
>
Connect
</Button>
</div>
)
}
/>
<SettingsRow
label="LangSmith"
description={
langsmith?.connected
? `Connected · key ••••${langsmith.api_key_last4 ?? ""}${langsmith.endpoint ? ` · ${langsmith.endpoint}` : ""}`
: "Connect LangSmith to enable read-only trace and run lookup tools."
}
control={
langsmith?.connected ? (
<Button
variant="outline"
size="sm"
onClick={() => disconnectLs.mutate()}
disabled={disconnectLs.isPending}
>
Disconnect
</Button>
) : (
<div className="flex flex-col items-end gap-2">
<Input
className="w-56"
placeholder="API key"
type="password"
value={lsApiKey}
onChange={(e) => setLsApiKey(e.target.value)}
disabled={busy}
/>
<Input
className="w-56"
placeholder="Endpoint (optional)"
value={lsEndpoint}
onChange={(e) => setLsEndpoint(e.target.value)}
disabled={busy}
/>
<Button
size="sm"
onClick={() =>
connectLs.mutate({
api_key: lsApiKey.trim(),
endpoint: lsEndpoint.trim() || null,
})
}
disabled={connectLs.isPending || !lsApiKey.trim()}
>
Connect
</Button>
</div>
)
}
/>
</div>
{error && <p className="px-4 pb-3 text-xs text-destructive">{error}</p>}
</SettingsSection>
)
}
function GlobalDefaultsSection({ models }: { models: Array<ModelOption> }) {
const qc = useQueryClient()
const settings = useQuery({
queryKey: ["teamSettings"],
queryFn: api.getTeamSettings,
})
const [error, setError] = useState<string | null>(null)
const [defaultRepoDraft, setDefaultRepoDraft] = useState("")
useEffect(() => {
setDefaultRepoDraft(settings.data?.default_repo ?? "")
}, [settings.data?.default_repo])
const save = useMutation({
mutationFn: (body: TeamSettings) => api.saveTeamSettings(body),
onSuccess: (saved) => {
qc.setQueryData(["teamSettings"], saved)
setError(null)
},
onError: (e: Error) => setError(e.message),
})
return (
<SettingsSection
title="Global defaults"
description="Workspace-wide model defaults. Per-user Cloud Agent selections override the agent defaults."
>
<div className="divide-y divide-border">
<RolePicker
label="Open SWE Agent"
description="Model used for code-writing runs triggered from Slack, Linear, GitHub, and the Open SWE Agent."
models={models}
model={settings.data?.default_agent_model ?? null}
effort={settings.data?.default_agent_reasoning_effort ?? null}
onChange={(model, effort) =>
settings.data &&
save.mutate({
...settings.data,
default_agent_model: model,
default_agent_reasoning_effort: effort,
})
}
disabled={!settings.data || save.isPending}
/>
<RolePicker
label="Open SWE Agent subagents"
description="Model used by delegated main-agent tasks."
models={models}
model={settings.data?.default_agent_subagent_model ?? null}
effort={
settings.data?.default_agent_subagent_reasoning_effort ?? null
}
onChange={(model, effort) =>
settings.data &&
save.mutate({
...settings.data,
default_agent_subagent_model: model,
default_agent_subagent_reasoning_effort: effort,
})
}
disabled={!settings.data || save.isPending}
/>
<SettingsRow
label="Default Repository"
description="Global fallback used when a run has no explicit repo and the user has no profile default. Use owner/repo."
control={
<Input
className="w-56"
placeholder="owner/repo"
value={defaultRepoDraft}
onChange={(e) => setDefaultRepoDraft(e.target.value)}
onBlur={() =>
settings.data &&
save.mutate({
...settings.data,
default_repo: defaultRepoDraft.trim() || null,
})
}
disabled={!settings.data || save.isPending}
/>
}
/>
<RolePicker
label="Open SWE Reviewer"
description="Model used for PR review runs."
models={models}
model={settings.data?.default_reviewer_model ?? null}
effort={settings.data?.default_reviewer_reasoning_effort ?? null}
onChange={(model, effort) =>
settings.data &&
save.mutate({
...settings.data,
default_reviewer_model: model,
default_reviewer_reasoning_effort: effort,
})
}
disabled={!settings.data || save.isPending}
/>
<RolePicker
label="Open SWE Reviewer subagents"
description="Model used by delegated reviewer tasks."
models={models}
model={settings.data?.default_reviewer_subagent_model ?? null}
effort={
settings.data?.default_reviewer_subagent_reasoning_effort ?? null
}
onChange={(model, effort) =>
settings.data &&
save.mutate({
...settings.data,
default_reviewer_subagent_model: model,
default_reviewer_subagent_reasoning_effort: effort,
})
}
disabled={!settings.data || save.isPending}
/>
<RolePicker
label="Open SWE Review Diff Grouping"
description="Model used for the review's 'AI sorted' view that groups changed files into a logical walkthrough. Inherits the Reviewer subagent default when unset."
models={models}
model={settings.data?.default_grouping_model ?? null}
effort={settings.data?.default_grouping_reasoning_effort ?? null}
inheritLabel="Reviewer subagent default"
onInherit={() =>
settings.data &&
save.mutate({
...settings.data,
default_grouping_model: null,
default_grouping_reasoning_effort: null,
})
}
onChange={(model, effort) =>
settings.data &&
save.mutate({
...settings.data,
default_grouping_model: model,
default_grouping_reasoning_effort: effort,
})
}
disabled={!settings.data || save.isPending}
/>
<RolePicker
label="Open SWE Review Chat"
description="Model used by the 'chat with this PR' assistant on the review page. Inherits the Agent default when unset."
models={models}
model={settings.data?.default_chat_model ?? null}
effort={settings.data?.default_chat_reasoning_effort ?? null}
inheritLabel="Agent default"
onInherit={() =>
settings.data &&
save.mutate({
...settings.data,
default_chat_model: null,
default_chat_reasoning_effort: null,
})
}
onChange={(model, effort) =>
settings.data &&
save.mutate({
...settings.data,
default_chat_model: model,
default_chat_reasoning_effort: effort,
})
}
disabled={!settings.data || save.isPending}
/>
</div>
{error && <p className="px-4 pb-3 text-xs text-destructive">{error}</p>}
</SettingsSection>
)
}
interface RolePickerProps {
label: string
description: string
models: Array<ModelOption>
model: string | null
effort: string | null
onChange: (model: string, effort: string) => void
disabled: boolean
/**
* When set, the model dropdown gains a leading "inherit" option with this
* label. Selecting it calls {@link onInherit} (clearing the override); an
* unset `model` renders as this option.
*/
inheritLabel?: string
onInherit?: () => void
}
const INHERIT_VALUE = "__inherit__"
function RolePicker({
label,
description,
models,
model,
effort,
onChange,
disabled,
inheritLabel,
onInherit,
}: RolePickerProps) {
const inheritFallback = inheritLabel ? INHERIT_VALUE : ""
const [localModel, setLocalModel] = useState<string>(model ?? inheritFallback)
const [localEffort, setLocalEffort] = useState<string>(effort ?? "")
useEffect(() => {
setLocalModel(model ?? inheritFallback)
setLocalEffort(effort ?? "")
}, [model, effort, inheritFallback])
const isInherit = localModel === INHERIT_VALUE
const selectedModel = models.find((m) => m.id === localModel)
const availableEfforts = selectedModel?.efforts ?? []
const handleModelChange = (value: string | null) => {
if (!value) return
if (value === INHERIT_VALUE) {
setLocalModel(INHERIT_VALUE)
setLocalEffort("")
onInherit?.()
return
}
const nextModel = models.find((m) => m.id === value)
if (!nextModel) return
const nextEffort = nextModel.efforts.includes(localEffort)
? localEffort
: nextModel.default_effort
setLocalModel(value)
setLocalEffort(nextEffort)
onChange(value, nextEffort)
}
const handleEffortChange = (value: string | null) => {
if (!value || !localModel || isInherit) return
setLocalEffort(value)
onChange(localModel, value)
}
return (
<SettingsRow
label={label}
description={description}
control={
<div className="flex items-center gap-2">
<Select
value={localModel}
onValueChange={handleModelChange}
disabled={disabled}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
{inheritLabel && (
<SelectItem value={INHERIT_VALUE}>{inheritLabel}</SelectItem>
)}
{models.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={localEffort}
onValueChange={handleEffortChange}
disabled={disabled || !localModel || isInherit}
>
<SelectTrigger className="w-28">
<SelectValue placeholder="effort" />
</SelectTrigger>
<SelectContent>
{availableEfforts.map((e) => (
<SelectItem key={e} value={e}>
{e}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
}
/>
)
}