diff --git a/web/src/App.tsx b/web/src/App.tsx index 4f7f41898f..65a9ede4a5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -558,35 +558,9 @@ export default function App() { /> - {/* - Persistent chat host: always mounted when `hermes dashboard - --tui` is active, visibility toggled by route. Keeping the - tree alive preserves the xterm instance, its WebSocket, and - the PTY child that backs the TUI session โ€” so navigating to - another tab and returning lands the user in the same - conversation instead of spawning a fresh session. - - The host sits alongside (not inside one) because - React Router unmounts route elements on path change, which - is exactly the destructive lifecycle we're avoiding. - - Trade-off worth knowing about: while hidden, ChatPage still - holds a PTY child + WebSocket + xterm instance for the - dashboard's full lifetime. The WS keeps delivering bytes - and xterm keeps parsing them into a display:none host - (cheap โ€” no paint work, but not free). If this becomes a - resource problem we can pause `term.write` when !isActive - or idle-disconnect after N minutes hidden; neither is - shipped today. - */} {embeddedChat && !chatOverriddenByPlugin && (pluginsLoading ? ( - // Direct /chat deep-link: plugin manifests haven't resolved - // yet, so we can't tell if a plugin is going to claim this - // route. Show a lightweight placeholder instead of a - // blank page. Typical wait is <50ms; worst case is the - // 2s plugin-registration safety timeout. isChatRoute ? (
hides itself when a CSS bg is set // so the two don't double-darken. CSS var fallbacks keep the // default behaviour unchanged when no theme customises these. - mixBlendMode: "var(--component-backdrop-filler-blend-mode, difference)", + mixBlendMode: + "var(--component-backdrop-filler-blend-mode, difference)", opacity: "var(--component-backdrop-filler-opacity, 0.033)", backgroundImage: "var(--theme-asset-bg)", backgroundSize: "var(--component-backdrop-background-size, cover)", - backgroundPosition: "var(--component-backdrop-background-position, center)", + backgroundPosition: + "var(--component-backdrop-background-position, center)", } as unknown as React.CSSProperties } > - {/* Default filler image only renders when no theme-asset-bg is - set. Themes that provide their own `assets.bg` override the -
's backgroundImage above, so hiding the in that - case prevents the two from compositing incorrectly. */} - {/* Show the *current* language's flag โ€” tooltip advertises the click action */} {locale === "en" ? "๐Ÿ‡ฌ๐Ÿ‡ง" : "๐Ÿ‡จ๐Ÿ‡ณ"} diff --git a/web/src/components/ModelInfoCard.tsx b/web/src/components/ModelInfoCard.tsx index 1a78710e90..b398d2b82a 100644 --- a/web/src/components/ModelInfoCard.tsx +++ b/web/src/components/ModelInfoCard.tsx @@ -1,12 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { - Brain, - Eye, - Gauge, - Lightbulb, - Wrench, - Loader2, -} from "lucide-react"; +import { Brain, Eye, Gauge, Lightbulb, Wrench, Loader2 } from "lucide-react"; import { api } from "@/lib/api"; import type { ModelInfoResponse } from "@/lib/api"; import { formatTokenCount } from "@/lib/format"; @@ -18,7 +11,10 @@ interface ModelInfoCardProps { refreshKey?: number; } -export function ModelInfoCard({ currentModel, refreshKey = 0 }: ModelInfoCardProps) { +export function ModelInfoCard({ + currentModel, + refreshKey = 0, +}: ModelInfoCardProps) { const [info, setInfo] = useState(null); const [loading, setLoading] = useState(false); const lastFetchKeyRef = useRef(""); @@ -53,7 +49,6 @@ export function ModelInfoCard({ currentModel, refreshKey = 0 }: ModelInfoCardPro return (
- {/* Context window */}
@@ -68,12 +63,13 @@ export function ModelInfoCard({ currentModel, refreshKey = 0 }: ModelInfoCardPro (override โ€” auto: {formatTokenCount(info.auto_context_length)}) ) : ( - auto-detected + + auto-detected + )}
- {/* Max output */} {hasCaps && caps.max_output_tokens && caps.max_output_tokens > 0 && (
@@ -86,7 +82,6 @@ export function ModelInfoCard({ currentModel, refreshKey = 0 }: ModelInfoCardPro
)} - {/* Capability badges */} {hasCaps && (
{caps.supports_tools && ( diff --git a/web/src/components/OAuthLoginModal.tsx b/web/src/components/OAuthLoginModal.tsx index 8a85d79abd..67c3d885f5 100644 --- a/web/src/components/OAuthLoginModal.tsx +++ b/web/src/components/OAuthLoginModal.tsx @@ -21,11 +21,7 @@ type Phase = | "approved" | "error"; -export function OAuthLoginModal({ - provider, - onClose, - onSuccess, -}: Props) { +export function OAuthLoginModal({ provider, onClose, onSuccess }: Props) { const [phase, setPhase] = useState("starting"); const [start, setStart] = useState(null); const [pkceCode, setPkceCode] = useState(""); @@ -202,7 +198,6 @@ export function OAuthLoginModal({ )}
- {/* โ”€โ”€ starting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} {phase === "starting" && (
@@ -210,7 +205,6 @@ export function OAuthLoginModal({
)} - {/* โ”€โ”€ PKCE: paste code โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} {start?.flow === "pkce" && phase === "awaiting_user" && ( <>
    @@ -250,7 +244,6 @@ export function OAuthLoginModal({ )} - {/* โ”€โ”€ PKCE: submitting exchange โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} {phase === "submitting" && (
    @@ -258,7 +251,6 @@ export function OAuthLoginModal({
    )} - {/* โ”€โ”€ Device code: show code + URL, polling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} {start?.flow === "device_code" && phase === "polling" && ( <>

    @@ -309,7 +301,6 @@ export function OAuthLoginModal({ )} - {/* โ”€โ”€ approved โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} {phase === "approved" && (

    @@ -317,7 +308,6 @@ export function OAuthLoginModal({
    )} - {/* โ”€โ”€ error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} {phase === "error" && ( <>
    diff --git a/web/src/components/OAuthProvidersCard.tsx b/web/src/components/OAuthProvidersCard.tsx index c7faadb87a..842be55778 100644 --- a/web/src/components/OAuthProvidersCard.tsx +++ b/web/src/components/OAuthProvidersCard.tsx @@ -1,8 +1,22 @@ import { useEffect, useState, useCallback, useRef } from "react"; -import { ShieldCheck, ShieldOff, ExternalLink, RefreshCw, LogOut, Terminal, LogIn } from "lucide-react"; +import { + ShieldCheck, + ShieldOff, + ExternalLink, + RefreshCw, + LogOut, + Terminal, + LogIn, +} from "lucide-react"; import { api, type OAuthProvider } from "@/lib/api"; import { Button, CopyButton } from "@nous-research/ui"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Badge } from "@nous-research/ui"; import { OAuthLoginModal } from "@/components/OAuthLoginModal"; import { useI18n } from "@/i18n"; @@ -12,7 +26,10 @@ interface Props { onSuccess?: (msg: string) => void; } -function formatExpiresAt(expiresAt: string | null | undefined, expiresInTemplate: string): string | null { +function formatExpiresAt( + expiresAt: string | null | undefined, + expiresInTemplate: string, +): string | null { if (!expiresAt) return null; try { const dt = new Date(expiresAt); @@ -70,7 +87,8 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) { } }; - const connectedCount = providers?.filter((p) => p.status.logged_in).length ?? 0; + const connectedCount = + providers?.filter((p) => p.status.logged_in).length ?? 0; const totalCount = providers?.length ?? 0; return ( @@ -79,19 +97,25 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
    - {t.oauth.providerLogins} + + {t.oauth.providerLogins} +
    - {t.oauth.description.replace("{connected}", String(connectedCount)).replace("{total}", String(totalCount))} + {t.oauth.description + .replace("{connected}", String(connectedCount)) + .replace("{total}", String(totalCount))} @@ -107,14 +131,16 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) { )}
    {providers?.map((p) => { - const expiresLabel = formatExpiresAt(p.status.expires_at, t.oauth.expiresIn); + const expiresLabel = formatExpiresAt( + p.status.expires_at, + t.oauth.expiresIn, + ); const isBusy = busyId === p.id; return (
    - {/* Left: status icon + name + source */}
    {p.status.logged_in ? ( @@ -124,7 +150,10 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
    {p.name} - + {t.oauth.flowLabels[p.flow]} {p.status.logged_in && ( @@ -145,11 +174,12 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
    {p.status.logged_in && p.status.token_preview && ( - token{" "} + token {p.status.token_preview} {p.status.source_label && ( - {" "}ยท {p.status.source_label} + {" "} + ยท {p.status.source_label} )} @@ -170,7 +200,7 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) { )}
    - {/* Right: action buttons */} +
    {p.docs_url && ( )} {!p.status.logged_in && p.flow !== "external" && ( - )} diff --git a/web/src/pages/AnalyticsPage.tsx b/web/src/pages/AnalyticsPage.tsx index b22fb6f751..bce6cb4599 100644 --- a/web/src/pages/AnalyticsPage.tsx +++ b/web/src/pages/AnalyticsPage.tsx @@ -1,13 +1,12 @@ import { useCallback, useEffect, useLayoutEffect, useState } from "react"; -import { - BarChart3, - Brain, - Cpu, - RefreshCw, - TrendingUp, -} from "lucide-react"; +import { BarChart3, Brain, Cpu, RefreshCw, TrendingUp } from "lucide-react"; import { api } from "@/lib/api"; -import type { AnalyticsResponse, AnalyticsDailyEntry, AnalyticsModelEntry, AnalyticsSkillEntry } from "@/lib/api"; +import type { + AnalyticsResponse, + AnalyticsDailyEntry, + AnalyticsModelEntry, + AnalyticsSkillEntry, +} from "@/lib/api"; import { timeAgo } from "@/lib/utils"; import { Button, Stats } from "@nous-research/ui"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -43,16 +42,21 @@ function TokenBarChart({ daily }: { daily: AnalyticsDailyEntry[] }) { const { t } = useI18n(); if (daily.length === 0) return null; - const maxTokens = Math.max(...daily.map((d) => d.input_tokens + d.output_tokens), 1); + const maxTokens = Math.max( + ...daily.map((d) => d.input_tokens + d.output_tokens), + 1, + ); return (
    - {t.analytics.dailyTokenUsage} + + {t.analytics.dailyTokenUsage} +
    -
    +
    {t.analytics.input} @@ -64,47 +68,63 @@ function TokenBarChart({ daily }: { daily: AnalyticsDailyEntry[] }) {
    -
    +
    {daily.map((d) => { const total = d.input_tokens + d.output_tokens; - const inputH = Math.round((d.input_tokens / maxTokens) * CHART_HEIGHT_PX); - const outputH = Math.round((d.output_tokens / maxTokens) * CHART_HEIGHT_PX); + const inputH = Math.round( + (d.input_tokens / maxTokens) * CHART_HEIGHT_PX, + ); + const outputH = Math.round( + (d.output_tokens / maxTokens) * CHART_HEIGHT_PX, + ); return (
    - {/* Tooltip */}
    {formatDate(d.day)}
    -
    {t.analytics.input}: {formatTokens(d.input_tokens)}
    -
    {t.analytics.output}: {formatTokens(d.output_tokens)}
    -
    {t.analytics.total}: {formatTokens(total)}
    +
    + {t.analytics.input}: {formatTokens(d.input_tokens)} +
    +
    + {t.analytics.output}: {formatTokens(d.output_tokens)} +
    +
    + {t.analytics.total}: {formatTokens(total)} +
    - {/* Input bar */} +
    0 ? 1 : 0) }} /> - {/* Output bar */} +
    0 ? 1 : 0) }} + style={{ + height: Math.max(outputH, d.output_tokens > 0 ? 1 : 0), + }} />
    ); })}
    - {/* X-axis labels */} +
    {daily.length > 0 ? formatDate(daily[0].day) : ""} {daily.length > 2 && ( {formatDate(daily[Math.floor(daily.length / 2)].day)} )} - {daily.length > 1 ? formatDate(daily[daily.length - 1].day) : ""} + + {daily.length > 1 ? formatDate(daily[daily.length - 1].day) : ""} +
    @@ -122,7 +142,9 @@ function DailyTable({ daily }: { daily: AnalyticsDailyEntry[] }) {
    - {t.analytics.dailyBreakdown} + + {t.analytics.dailyBreakdown} +
    @@ -130,23 +152,42 @@ function DailyTable({ daily }: { daily: AnalyticsDailyEntry[] }) { - - - - + + + + {sorted.map((d) => { return ( - - - + + + ); @@ -164,7 +205,8 @@ function ModelTable({ models }: { models: AnalyticsModelEntry[] }) { if (models.length === 0) return null; const sorted = [...models].sort( - (a, b) => b.input_tokens + b.output_tokens - (a.input_tokens + a.output_tokens), + (a, b) => + b.input_tokens + b.output_tokens - (a.input_tokens + a.output_tokens), ); return ( @@ -172,7 +214,9 @@ function ModelTable({ models }: { models: AnalyticsModelEntry[] }) {
    - {t.analytics.perModelBreakdown} + + {t.analytics.perModelBreakdown} +
    @@ -180,22 +224,37 @@ function ModelTable({ models }: { models: AnalyticsModelEntry[] }) {
    {t.analytics.date}{t.sessions.title}{t.analytics.input}{t.analytics.output} + {t.analytics.date} + + {t.sessions.title} + + {t.analytics.input} + + {t.analytics.output} +
    {formatDate(d.day)}{d.sessions}
    + {formatDate(d.day)} + + {d.sessions} + - {formatTokens(d.input_tokens)} + + {formatTokens(d.input_tokens)} + - {formatTokens(d.output_tokens)} + + {formatTokens(d.output_tokens)} +
    - - - + + + {sorted.map((m) => ( - + - + ))} @@ -224,21 +283,38 @@ function SkillTable({ skills }: { skills: AnalyticsSkillEntry[] }) {
    {t.analytics.model}{t.sessions.title}{t.analytics.tokens} + {t.analytics.model} + + {t.sessions.title} + + {t.analytics.tokens} +
    {m.model} {m.sessions} + {m.sessions} + - {formatTokens(m.input_tokens)} + + {formatTokens(m.input_tokens)} + {" / "} - {formatTokens(m.output_tokens)} + + {formatTokens(m.output_tokens)} +
    - - - - - + + + + + {skills.map((skill) => ( - + - - + +
    {t.analytics.skill}{t.analytics.loads}{t.analytics.edits}{t.analytics.total}{t.analytics.lastUsed} + {t.analytics.skill} + + {t.analytics.loads} + + {t.analytics.edits} + + {t.analytics.total} + + {t.analytics.lastUsed} +
    {skill.skill} {skill.view_count}{skill.manage_count} + {skill.view_count} + + {skill.manage_count} + {skill.total_count} {skill.last_used_at ? timeAgo(skill.last_used_at) : "โ€”"} @@ -338,7 +414,6 @@ export default function AnalyticsPage() { {data && ( <> - {/* Summary stats + bar chart side-by-side on lg+ */}
    @@ -377,24 +452,28 @@ export default function AnalyticsPage() {
    - {/* Tables */} )} - {data && data.daily.length === 0 && data.by_model.length === 0 && data.skills.top_skills.length === 0 && ( - - -
    - -

    {t.analytics.noUsageData}

    -

    {t.analytics.startSession}

    -
    -
    -
    - )} + {data && + data.daily.length === 0 && + data.by_model.length === 0 && + data.skills.top_skills.length === 0 && ( + + +
    + +

    {t.analytics.noUsageData}

    +

    + {t.analytics.startSession} +

    +
    +
    +
    + )} ); diff --git a/web/src/pages/ConfigPage.tsx b/web/src/pages/ConfigPage.tsx index 273a1c4ca7..7b074cc367 100644 --- a/web/src/pages/ConfigPage.tsx +++ b/web/src/pages/ConfigPage.tsx @@ -45,7 +45,10 @@ import { PluginSlot } from "@/plugins"; /* Helpers */ /* ------------------------------------------------------------------ */ -const CATEGORY_ICONS: Record> = { +const CATEGORY_ICONS: Record< + string, + React.ComponentType<{ className?: string }> +> = { general: Settings, agent: Bot, terminal: Monitor, @@ -63,7 +66,13 @@ const CATEGORY_ICONS: Record auxiliary: Wrench, }; -function CategoryIcon({ category, className }: { category: string; className?: string }) { +function CategoryIcon({ + category, + className, +}: { + category: string; + className?: string; +}) { const Icon = CATEGORY_ICONS[category] ?? FileQuestion; return ; } @@ -74,9 +83,14 @@ function CategoryIcon({ category, className }: { category: string; className?: s export default function ConfigPage() { const [config, setConfig] = useState | null>(null); - const [schema, setSchema] = useState> | null>(null); + const [schema, setSchema] = useState + > | null>(null); const [categoryOrder, setCategoryOrder] = useState([]); - const [defaults, setDefaults] = useState | null>(null); + const [defaults, setDefaults] = useState | null>( + null, + ); const [saving, setSaving] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [yamlMode, setYamlMode] = useState(false); @@ -124,7 +138,10 @@ export default function ConfigPage() { } useEffect(() => { - api.getConfig().then(setConfig).catch(() => {}); + api + .getConfig() + .then(setConfig) + .catch(() => {}); api .getSchema() .then((resp) => { @@ -132,7 +149,10 @@ export default function ConfigPage() { setCategoryOrder(resp.category_order ?? []); }) .catch(() => {}); - api.getDefaults().then(setDefaults).catch(() => {}); + api + .getDefaults() + .then(setDefaults) + .catch(() => {}); }, []); // Set active category when categories load @@ -157,7 +177,11 @@ export default function ConfigPage() { /* ---- Categories ---- */ const categories = useMemo(() => { if (!schema) return []; - const allCats = [...new Set(Object.values(schema).map((s) => String(s.category ?? "general")))]; + const allCats = [ + ...new Set( + Object.values(schema).map((s) => String(s.category ?? "general")), + ), + ]; const ordered = categoryOrder.filter((c) => allCats.includes(c)); const extra = allCats.filter((c) => !categoryOrder.includes(c)).sort(); return [...ordered, ...extra]; @@ -186,8 +210,12 @@ export default function ConfigPage() { return ( key.toLowerCase().includes(lowerSearch) || humanLabel.toLowerCase().includes(lowerSearch) || - String(s.category ?? "").toLowerCase().includes(lowerSearch) || - String(s.description ?? "").toLowerCase().includes(lowerSearch) + String(s.category ?? "") + .toLowerCase() + .includes(lowerSearch) || + String(s.description ?? "") + .toLowerCase() + .includes(lowerSearch) ); }); }, [isSearching, lowerSearch, schema]); @@ -196,7 +224,7 @@ export default function ConfigPage() { const activeFields = useMemo(() => { if (!schema || isSearching) return []; return Object.entries(schema).filter( - ([, s]) => String(s.category ?? "general") === activeCategory + ([, s]) => String(s.category ?? "general") === activeCategory, ); }, [schema, activeCategory, isSearching]); @@ -219,7 +247,10 @@ export default function ConfigPage() { try { await api.saveConfigRaw(yamlText); showToast(t.config.yamlConfigSaved, "success"); - api.getConfig().then(setConfig).catch(() => {}); + api + .getConfig() + .then(setConfig) + .catch(() => {}); } catch (e) { showToast(`${t.config.failedToSaveYaml}: ${e}`, "error"); } finally { @@ -247,12 +278,17 @@ export default function ConfigPage() { next = setNestedValue(next, key, getNestedValue(defaults, key)); } setConfig(next); - showToast(t.config.resetScopeToast.replace("{scope}", scopeLabel), "success"); + showToast( + t.config.resetScopeToast.replace("{scope}", scopeLabel), + "success", + ); }; const handleExport = () => { if (!config) return; - const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" }); + const blob = new Blob([JSON.stringify(config, null, 2)], { + type: "application/json", + }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; @@ -287,7 +323,10 @@ export default function ConfigPage() { } /* ---- Render field list (shared between search & normal) ---- */ - const renderFields = (fields: [string, Record][], showCategory = false) => { + const renderFields = ( + fields: [string, Record][], + showCategory = false, + ) => { let lastSection = ""; let lastCat = ""; return fields.map(([key, s]) => { @@ -295,7 +334,11 @@ export default function ConfigPage() { const section = parts.length > 1 ? parts[0] : ""; const cat = String(s.category ?? "general"); const showCatBadge = showCategory && cat !== lastCat; - const showSection = !showCategory && section && section !== lastSection && section !== activeCategory; + const showSection = + !showCategory && + section && + section !== lastSection && + section !== activeCategory; lastSection = section; lastCat = cat; @@ -303,7 +346,10 @@ export default function ConfigPage() {
    {showCatBadge && (
    - + {prettyCategoryName(cat)} @@ -336,7 +382,6 @@ export default function ConfigPage() { - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Header Bar โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */}
    @@ -345,24 +390,52 @@ export default function ConfigPage() {
    - - - - {!yamlMode && (() => { - const resetScopeLabel = isSearching - ? t.config.searchResults - : prettyCategoryName(activeCategory); - const resetTitle = t.config.resetScopeTooltip.replace("{scope}", resetScopeLabel); - return ( - - ); - })()} + + {!yamlMode && + (() => { + const resetScopeLabel = isSearching + ? t.config.searchResults + : prettyCategoryName(activeCategory); + const resetTitle = t.config.resetScopeTooltip.replace( + "{scope}", + resetScopeLabel, + ); + return ( + + ); + })()}
    @@ -375,7 +448,11 @@ export default function ConfigPage() { {yamlMode ? ( - ) : ( @@ -386,7 +463,6 @@ export default function ConfigPage() {
    - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• YAML Mode โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} {yamlMode ? ( @@ -411,13 +487,10 @@ export default function ConfigPage() { ) : ( - /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Form Mode โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */
    - {/* ---- Filter panel ---- */} - {/* ---- Content ---- */}
    {isSearching ? ( - /* Search results */
    @@ -485,7 +559,11 @@ export default function ConfigPage() { {t.config.searchResults} - {searchMatchedFields.length} {t.config.fields.replace("{s}", searchMatchedFields.length !== 1 ? "s" : "")} + {searchMatchedFields.length}{" "} + {t.config.fields.replace( + "{s}", + searchMatchedFields.length !== 1 ? "s" : "", + )}
    @@ -505,11 +583,18 @@ export default function ConfigPage() {
    - + {prettyCategoryName(activeCategory)} - {activeFields.length} {t.config.fields.replace("{s}", activeFields.length !== 1 ? "s" : "")} + {activeFields.length}{" "} + {t.config.fields.replace( + "{s}", + activeFields.length !== 1 ? "s" : "", + )}
    diff --git a/web/src/pages/EnvPage.tsx b/web/src/pages/EnvPage.tsx index a2ac215a52..429a112faf 100644 --- a/web/src/pages/EnvPage.tsx +++ b/web/src/pages/EnvPage.tsx @@ -22,7 +22,13 @@ import { useConfirmDelete } from "@/hooks/useConfirmDelete"; import { useToast } from "@/hooks/useToast"; import { OAuthProvidersCard } from "@/components/OAuthProvidersCard"; import { Button } from "@nous-research/ui"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Badge } from "@nous-research/ui"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -36,25 +42,25 @@ import { PluginSlot } from "@/plugins"; /** Map env-var key prefixes to a human-friendly provider name + ordering. */ const PROVIDER_GROUPS: { prefix: string; name: string; priority: number }[] = [ // Nous Portal first - { prefix: "NOUS_", name: "Nous Portal", priority: 0 }, + { prefix: "NOUS_", name: "Nous Portal", priority: 0 }, // Then alphabetical by display name - { prefix: "ANTHROPIC_", name: "Anthropic", priority: 1 }, - { prefix: "DASHSCOPE_", name: "DashScope (Qwen)", priority: 2 }, - { prefix: "HERMES_QWEN_", name: "DashScope (Qwen)", priority: 2 }, - { prefix: "DEEPSEEK_", name: "DeepSeek", priority: 3 }, - { prefix: "GOOGLE_", name: "Gemini", priority: 4 }, - { prefix: "GEMINI_", name: "Gemini", priority: 4 }, - { prefix: "GLM_", name: "GLM / Z.AI", priority: 5 }, - { prefix: "ZAI_", name: "GLM / Z.AI", priority: 5 }, - { prefix: "Z_AI_", name: "GLM / Z.AI", priority: 5 }, - { prefix: "HF_", name: "Hugging Face", priority: 6 }, - { prefix: "KIMI_", name: "Kimi / Moonshot", priority: 7 }, - { prefix: "MINIMAX_CN_", name: "MiniMax (China)", priority: 9 }, - { prefix: "MINIMAX_", name: "MiniMax", priority: 8 }, - { prefix: "OPENCODE_GO_", name: "OpenCode Go", priority: 10 }, - { prefix: "OPENCODE_ZEN_", name: "OpenCode Zen", priority: 11 }, - { prefix: "OPENROUTER_", name: "OpenRouter", priority: 12 }, - { prefix: "XIAOMI_", name: "Xiaomi MiMo", priority: 13 }, + { prefix: "ANTHROPIC_", name: "Anthropic", priority: 1 }, + { prefix: "DASHSCOPE_", name: "DashScope (Qwen)", priority: 2 }, + { prefix: "HERMES_QWEN_", name: "DashScope (Qwen)", priority: 2 }, + { prefix: "DEEPSEEK_", name: "DeepSeek", priority: 3 }, + { prefix: "GOOGLE_", name: "Gemini", priority: 4 }, + { prefix: "GEMINI_", name: "Gemini", priority: 4 }, + { prefix: "GLM_", name: "GLM / Z.AI", priority: 5 }, + { prefix: "ZAI_", name: "GLM / Z.AI", priority: 5 }, + { prefix: "Z_AI_", name: "GLM / Z.AI", priority: 5 }, + { prefix: "HF_", name: "Hugging Face", priority: 6 }, + { prefix: "KIMI_", name: "Kimi / Moonshot", priority: 7 }, + { prefix: "MINIMAX_CN_", name: "MiniMax (China)", priority: 9 }, + { prefix: "MINIMAX_", name: "MiniMax", priority: 8 }, + { prefix: "OPENCODE_GO_", name: "OpenCode Go", priority: 10 }, + { prefix: "OPENCODE_ZEN_", name: "OpenCode Zen", priority: 11 }, + { prefix: "OPENROUTER_", name: "OpenRouter", priority: 12 }, + { prefix: "XIAOMI_", name: "Xiaomi MiMo", priority: 13 }, ]; function getProviderGroup(key: string): string { @@ -117,25 +123,38 @@ function EnvVarRow({ const { t } = useI18n(); const isEditing = edits[varKey] !== undefined; const isRevealed = !!revealed[varKey]; - const displayValue = isRevealed ? revealed[varKey] : (info.redacted_value ?? "---"); + const displayValue = isRevealed + ? revealed[varKey] + : (info.redacted_value ?? "---"); // Compact inline row for unset, non-editing keys (used inside provider groups) if (compact && !info.is_set && !isEditing) { return (
    - {varKey} - {info.description} + + {varKey} + + + {info.description} +
    {info.url && ( - + {t.env.getKey} )} -
    @@ -148,18 +167,29 @@ function EnvVarRow({ return (
    - - {info.description} + + + {info.description} +
    {info.url && ( - + {t.env.getKey} )} -
    @@ -178,8 +208,12 @@ function EnvVarRow({
    {info.url && ( - + {t.env.getKey} )} @@ -190,35 +224,57 @@ function EnvVarRow({ {info.tools.length > 0 && (
    {info.tools.map((tool) => ( - {tool} + + {tool} + ))}
    )} {!isEditing && (
    -
    +
    {info.is_set ? displayValue : "---"}
    {info.is_set && ( - )} - {info.is_set && ( - )} @@ -227,12 +283,28 @@ function EnvVarRow({ {isEditing && (
    - setEdits((prev) => ({ ...prev, [varKey]: e.target.value }))} - placeholder={info.is_set ? t.env.replaceCurrentValue.replace("{preview}", info.redacted_value ?? "---") : t.env.enterValue} - className="flex-1 font-mono-ui text-xs" /> - - {/* Expanded content */} {expanded && (
    - {/* API keys first (most important) */} {apiKeys.map(([key, info]) => ( ))} - {/* Base URLs (secondary) */} + {baseUrls.map(([key, info]) => ( ))} - {/* Anything else */} + {other.map(([key, info]) => ( ))} @@ -365,7 +483,10 @@ export default function EnvPage() { const { t } = useI18n(); useEffect(() => { - api.getEnvVars().then(setVars).catch(() => {}); + api + .getEnvVars() + .then(setVars) + .catch(() => {}); }, []); const handleSave = async (key: string) => { @@ -378,12 +499,24 @@ export default function EnvPage() { prev ? { ...prev, - [key]: { ...prev[key], is_set: true, redacted_value: value.slice(0, 4) + "..." + value.slice(-4) }, + [key]: { + ...prev[key], + is_set: true, + redacted_value: value.slice(0, 4) + "..." + value.slice(-4), + }, } : prev, ); - setEdits((prev) => { const n = { ...prev }; delete n[key]; return n; }); - setRevealed((prev) => { const n = { ...prev }; delete n[key]; return n; }); + setEdits((prev) => { + const n = { ...prev }; + delete n[key]; + return n; + }); + setRevealed((prev) => { + const n = { ...prev }; + delete n[key]; + return n; + }); showToast(`${key} ${t.common.save.toLowerCase()}d`, "success"); } catch (e) { showToast(`${t.config.failedToSave} ${key}: ${e}`, "error"); @@ -400,11 +533,22 @@ export default function EnvPage() { await api.deleteEnvVar(key); setVars((prev) => prev - ? { ...prev, [key]: { ...prev[key], is_set: false, redacted_value: null } } + ? { + ...prev, + [key]: { ...prev[key], is_set: false, redacted_value: null }, + } : prev, ); - setEdits((prev) => { const n = { ...prev }; delete n[key]; return n; }); - setRevealed((prev) => { const n = { ...prev }; delete n[key]; return n; }); + setEdits((prev) => { + const n = { ...prev }; + delete n[key]; + return n; + }); + setRevealed((prev) => { + const n = { ...prev }; + delete n[key]; + return n; + }); showToast(`${key} ${t.common.removed}`, "success"); } catch (e) { showToast(`${t.common.failedToRemove} ${key}: ${e}`, "error"); @@ -419,7 +563,11 @@ export default function EnvPage() { const handleReveal = async (key: string) => { if (revealed[key]) { - setRevealed((prev) => { const n = { ...prev }; delete n[key]; return n; }); + setRevealed((prev) => { + const n = { ...prev }; + delete n[key]; + return n; + }); return; } try { @@ -431,7 +579,11 @@ export default function EnvPage() { }; const cancelEdit = (key: string) => { - setEdits((prev) => { const n = { ...prev }; delete n[key]; return n; }); + setEdits((prev) => { + const n = { ...prev }; + delete n[key]; + return n; + }); }; /* ---- Build provider groups ---- */ @@ -439,7 +591,8 @@ export default function EnvPage() { if (!vars) return { providerGroups: [], nonProviderGrouped: [] }; const providerEntries = Object.entries(vars).filter( - ([, info]) => info.category === "provider" && (showAdvanced || !info.advanced), + ([, info]) => + info.category === "provider" && (showAdvanced || !info.advanced), ); // Group by provider @@ -498,9 +651,7 @@ export default function EnvPage() { const pendingClearKey = keyClear.pendingId; const pendingKeyDescription = - pendingClearKey && vars - ? vars[pendingClearKey]?.description - : undefined; + pendingClearKey && vars ? vars[pendingClearKey]?.description : undefined; return (
    @@ -534,13 +685,11 @@ export default function EnvPage() {
    - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• OAuth Logins โ•โ• */} showToast(msg, "error")} onSuccess={(msg) => showToast(msg, "success")} /> - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• LLM Providers (grouped) โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */}
    @@ -548,7 +697,9 @@ export default function EnvPage() { {t.env.llmProviders}
    - {t.env.providersConfigured.replace("{configured}", String(configuredProviders)).replace("{total}", String(totalProviders))} + {t.env.providersConfigured + .replace("{configured}", String(configuredProviders)) + .replace("{total}", String(totalProviders))}
    @@ -557,53 +708,82 @@ export default function EnvPage() { ))}
    - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Other categories (flat) โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} - {nonProviderGrouped.map(({ label, icon: Icon, setEntries, unsetEntries, totalEntries, category }) => { - if (totalEntries === 0) return null; + {nonProviderGrouped.map( + ({ + label, + icon: Icon, + setEntries, + unsetEntries, + totalEntries, + category, + }) => { + if (totalEntries === 0) return null; - return ( - - -
    - - {label} -
    - - {setEntries.length} {t.common.of} {totalEntries} {t.common.configured} - -
    + return ( + + +
    + + {label} +
    + + {setEntries.length} {t.common.of} {totalEntries}{" "} + {t.common.configured} + +
    - - {setEntries.map(([key, info]) => ( - - ))} + + {setEntries.map(([key, info]) => ( + + ))} - {unsetEntries.length > 0 && ( - - )} - -
    - ); - })} + {unsetEntries.length > 0 && ( + + )} + +
    + ); + }, + )}
    ); @@ -648,20 +828,33 @@ function CollapsibleUnset({ className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer pt-1" onClick={() => setCollapsed(!collapsed)} > - {collapsed - ? - : } - {t.env.notConfigured.replace("{count}", String(unsetEntries.length))} + {collapsed ? ( + + ) : ( + + )} + + {t.env.notConfigured.replace("{count}", String(unsetEntries.length))} + - {!collapsed && unsetEntries.map(([key, info]) => ( - - ))} + {!collapsed && + unsetEntries.map(([key, info]) => ( + + ))} ); } diff --git a/web/src/pages/LogsPage.tsx b/web/src/pages/LogsPage.tsx index 47b5a96f75..18da4a0877 100644 --- a/web/src/pages/LogsPage.tsx +++ b/web/src/pages/LogsPage.tsx @@ -1,4 +1,10 @@ -import { useEffect, useLayoutEffect, useState, useCallback, useRef } from "react"; +import { + useEffect, + useLayoutEffect, + useState, + useCallback, + useRef, +} from "react"; import { FileText, RefreshCw } from "lucide-react"; import { api } from "@/lib/api"; import { Button } from "@nous-research/ui"; @@ -141,18 +147,25 @@ export default function LogsPage() { return (
    - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Filter toolbar โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */}
    - + - + @@ -177,7 +190,6 @@ export default function LogsPage() {
    - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Log viewer โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */} diff --git a/web/src/pages/SessionsPage.tsx b/web/src/pages/SessionsPage.tsx index 37f545978e..da8a610969 100644 --- a/web/src/pages/SessionsPage.tsx +++ b/web/src/pages/SessionsPage.tsx @@ -497,7 +497,10 @@ export default function SessionsPage() { useEffect(() => { const loadOverview = () => { - api.getStatus().then(setStatus).catch(() => {}); + api + .getStatus() + .then(setStatus) + .catch(() => {}); api .getSessions(50) .then((r) => setOverviewSessions(r.sessions)) @@ -551,7 +554,12 @@ export default function SessionsPage() { throw new Error("delete failed"); } }, - [expandedId, showToast, t.sessions.sessionDeleted, t.sessions.failedToDelete], + [ + expandedId, + showToast, + t.sessions.sessionDeleted, + t.sessions.failedToDelete, + ], ), }); @@ -800,7 +808,6 @@ export default function SessionsPage() { ))}
    - {/* Pagination โ€” hidden during search */} {!searchResults && total > PAGE_SIZE && (
    diff --git a/web/src/pages/SkillsPage.tsx b/web/src/pages/SkillsPage.tsx index 272e781278..a5a07470de 100644 --- a/web/src/pages/SkillsPage.tsx +++ b/web/src/pages/SkillsPage.tsx @@ -221,15 +221,7 @@ export default function SkillsPage() { setAfterTitle(null); setEnd(null); }; - }, [ - enabledCount, - loading, - search, - setAfterTitle, - setEnd, - skills.length, - t, - ]); + }, [enabledCount, loading, search, setAfterTitle, setEnd, skills.length, t]); const filteredToolsets = useMemo(() => { return toolsets.filter( @@ -255,13 +247,8 @@ export default function SkillsPage() { - {/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Filter panel + Content โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */}
    - {/* ---- Filter panel ---- */} -