@@ -579,8 +585,9 @@ export default function App() {
or idle-disconnect after N minutes hidden; neither is
shipped today.
*/}
- {embeddedChat && !chatOverriddenByPlugin && (
- pluginsLoading ? (
+ {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
@@ -593,7 +600,10 @@ export default function App() {
aria-live="polite"
>
-
+
Loading chat…
@@ -609,8 +619,7 @@ export default function App() {
>
- )
- )}
+ ))}
diff --git a/web/src/components/ChatSidebar.tsx b/web/src/components/ChatSidebar.tsx
index 6bfac9cfac..05f07a337c 100644
--- a/web/src/components/ChatSidebar.tsx
+++ b/web/src/components/ChatSidebar.tsx
@@ -23,8 +23,8 @@
* terminal pane keeps working unimpaired.
*/
+import { Button } from "@nous-research/ui";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ModelPickerDialog } from "@/components/ModelPickerDialog";
@@ -337,12 +337,11 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) {
{error && (
}
>
-
reconnect
)}
diff --git a/web/src/components/ModelPickerDialog.tsx b/web/src/components/ModelPickerDialog.tsx
index d30fb8dd6c..81bb5abf59 100644
--- a/web/src/components/ModelPickerDialog.tsx
+++ b/web/src/components/ModelPickerDialog.tsx
@@ -1,4 +1,4 @@
-import { Button } from "@/components/ui/button";
+import { Button } from "@nous-research/ui";
import { Input } from "@/components/ui/input";
import type { GatewayClient } from "@/lib/gatewayClient";
import { Check, Loader2, Search, X } from "lucide-react";
@@ -222,10 +222,10 @@ export function ModelPickerDialog({ gw, sessionId, onClose, onSubmit }: Props) {
-
diff --git a/web/src/components/OAuthLoginModal.tsx b/web/src/components/OAuthLoginModal.tsx
index 66c78139ef..a6cd2ca299 100644
--- a/web/src/components/OAuthLoginModal.tsx
+++ b/web/src/components/OAuthLoginModal.tsx
@@ -1,8 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { ExternalLink, Copy, X, Check, Loader2 } from "lucide-react";
-import { H2 } from "@nous-research/ui";
+import { Button, H2 } from "@nous-research/ui";
import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api";
-import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useI18n } from "@/i18n";
@@ -254,7 +253,6 @@ export function OAuthLoginModal({
{t.oauth.submitCode}
@@ -289,8 +287,7 @@ export function OAuthLoginModal({
}
handleCopyUserCode(
(
@@ -301,12 +298,12 @@ export function OAuthLoginModal({
).user_code,
)
}
- className="text-xs"
+ className="!p-2 aspect-square"
>
{codeCopied ? (
-
+
) : (
-
+
)}
@@ -348,11 +345,10 @@ export function OAuthLoginModal({
{errorMsg || t.oauth.loginFailed}
-
+
{t.common.close}
{
if (start?.session_id) {
api.cancelOAuthSession(start.session_id).catch(() => {});
diff --git a/web/src/components/OAuthProvidersCard.tsx b/web/src/components/OAuthProvidersCard.tsx
index 940848787d..70672908b0 100644
--- a/web/src/components/OAuthProvidersCard.tsx
+++ b/web/src/components/OAuthProvidersCard.tsx
@@ -1,8 +1,8 @@
import { useEffect, useState, useCallback, useRef } from "react";
import { ShieldCheck, ShieldOff, Copy, ExternalLink, RefreshCw, LogOut, Terminal, LogIn } from "lucide-react";
import { api, type OAuthProvider } from "@/lib/api";
+import { Button } from "@nous-research/ui";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { OAuthLoginModal } from "@/components/OAuthLoginModal";
import { useI18n } from "@/i18n";
@@ -94,13 +94,11 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
{t.oauth.providerLogins}
}
>
-
{t.common.refresh}
@@ -194,53 +192,42 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
className="inline-flex"
title={`Open ${p.name} docs`}
>
-
+
)}
{!p.status.logged_in && p.flow !== "external" && (
setLoginFor(p)}
- className="text-xs h-7"
+ prefix={}
>
-
{t.oauth.login}
)}
{!p.status.logged_in && (
handleCopy(p)}
- className="text-xs h-7"
title={t.oauth.copyCliCommand}
+ prefix={copiedId === p.id ? undefined : }
>
- {copiedId === p.id ? (
- <>{t.oauth.copied}>
- ) : (
- <>
-
- {t.oauth.cli}
- >
- )}
+ {copiedId === p.id ? t.oauth.copied : t.oauth.cli}
)}
{p.status.logged_in && p.flow !== "external" && (
handleDisconnect(p)}
disabled={isBusy}
- className="text-xs h-7"
+ prefix={
+ isBusy ? (
+
+ ) : (
+
+ )
+ }
>
- {isBusy ? (
-
- ) : (
-
- )}
{t.oauth.disconnect}
)}
diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx
deleted file mode 100644
index 8f2f272069..0000000000
--- a/web/src/components/ui/button.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils";
-
-export const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap font-mondwest text-xs tracking-[0.1em] uppercase transition-colors cursor-pointer"
- + " disabled:pointer-events-none disabled:opacity-50",
- {
- variants: {
- variant: {
- default: "bg-foreground/90 text-background hover:bg-foreground",
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
- outline: "border border-border bg-transparent hover:bg-foreground/10 hover:text-foreground",
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
- ghost: "hover:bg-foreground/10 hover:text-foreground",
- link: "text-foreground underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2",
- sm: "h-8 px-3 text-[0.65rem]",
- lg: "h-10 px-8",
- icon: "h-9 w-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-);
-
-export function Button({
- className,
- variant,
- size,
- ...props
-}: React.ButtonHTMLAttributes & VariantProps) {
- return ;
-}
diff --git a/web/src/components/ui/confirm-dialog.tsx b/web/src/components/ui/confirm-dialog.tsx
index 48e58264f8..6e735383c3 100644
--- a/web/src/components/ui/confirm-dialog.tsx
+++ b/web/src/components/ui/confirm-dialog.tsx
@@ -1,8 +1,8 @@
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { AlertTriangle } from "lucide-react";
+import { Button } from "@nous-research/ui";
import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
export function ConfirmDialog({
cancelLabel = "Cancel",
@@ -101,8 +101,7 @@ export function ConfirmDialog({
@@ -111,10 +110,9 @@ export function ConfirmDialog({
{loading ? "…" : confirmLabel}
diff --git a/web/src/pages/AnalyticsPage.tsx b/web/src/pages/AnalyticsPage.tsx
index 63dd15e4a3..e0574782d0 100644
--- a/web/src/pages/AnalyticsPage.tsx
+++ b/web/src/pages/AnalyticsPage.tsx
@@ -10,9 +10,9 @@ import {
import { api } from "@/lib/api";
import type { AnalyticsResponse, AnalyticsDailyEntry, AnalyticsModelEntry, AnalyticsSkillEntry } from "@/lib/api";
import { timeAgo } from "@/lib/utils";
+import { Button } from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { usePageHeader } from "@/contexts/usePageHeader";
import { useI18n } from "@/i18n";
import { PluginSlot } from "@/plugins";
@@ -317,9 +317,7 @@ export default function AnalyticsPage() {
setDays(p.days)}
>
{p.label}
@@ -328,13 +326,11 @@ export default function AnalyticsPage() {
}
>
-
{t.common.refresh}
,
diff --git a/web/src/pages/ConfigPage.tsx b/web/src/pages/ConfigPage.tsx
index 8705ac4c5d..0e943decfb 100644
--- a/web/src/pages/ConfigPage.tsx
+++ b/web/src/pages/ConfigPage.tsx
@@ -33,8 +33,8 @@ import { getNestedValue, setNestedValue } from "@/lib/nested";
import { useToast } from "@/hooks/useToast";
import { Toast } from "@/components/Toast";
import { AutoField } from "@/components/AutoField";
+import { Button } from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { useI18n } from "@/i18n";
@@ -345,10 +345,10 @@ export default function ConfigPage() {
-
+
- fileInputRef.current?.click()} title={t.config.importConfig} aria-label={t.config.importConfig}>
+ fileInputRef.current?.click()} title={t.config.importConfig} aria-label={t.config.importConfig} className="!p-2 aspect-square">
@@ -358,7 +358,7 @@ export default function ConfigPage() {
: prettyCategoryName(activeCategory);
const resetTitle = t.config.resetScopeTooltip.replace("{scope}", resetScopeLabel);
return (
-
+
);
@@ -367,32 +367,19 @@ export default function ConfigPage() {
setYamlMode(!yamlMode)}
- className="gap-1.5"
+ prefix={yamlMode ? : }
>
- {yamlMode ? (
- <>
-
- {t.common.form}
- >
- ) : (
- <>
-
- YAML
- >
- )}
+ {yamlMode ? t.common.form : "YAML"}
{yamlMode ? (
-
-
+ }>
{yamlSaving ? t.common.saving : t.common.save}
) : (
-
-
+ }>
{saving ? t.common.saving : t.common.save}
)}
diff --git a/web/src/pages/CronPage.tsx b/web/src/pages/CronPage.tsx
index 63478fa74d..569e4559e7 100644
--- a/web/src/pages/CronPage.tsx
+++ b/web/src/pages/CronPage.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { Clock, Pause, Play, Plus, Trash2, Zap } from "lucide-react";
-import { H2 } from "@nous-research/ui";
+import { Button, H2 } from "@nous-research/ui";
import { api } from "@/lib/api";
import type { CronJob } from "@/lib/api";
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
@@ -9,7 +9,6 @@ import { useConfirmDelete } from "@/hooks/useConfirmDelete";
import { Toast } from "@/components/Toast";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectOption } from "@/components/ui/select";
@@ -166,7 +165,6 @@ export default function CronPage() {
loading={jobDelete.isDeleting}
/>
- {/* Create new job form */}
@@ -237,9 +235,9 @@ export default function CronPage() {
}
className="w-full"
>
-
{creating ? t.common.creating : t.common.create}
@@ -248,7 +246,6 @@ export default function CronPage() {
- {/* Jobs list */}
(
- {/* Info */}
@@ -306,16 +302,15 @@ export default function CronPage() {
)}
- {/* Actions */}
handlePauseResume(job)}
+ className="!p-2 aspect-square"
>
{job.state === "paused" ? (
@@ -325,21 +320,21 @@ export default function CronPage() {
handleTrigger(job)}
+ className="!p-2 aspect-square"
>
jobDelete.requestDelete(job.id)}
+ className="!p-2 aspect-square"
>
@@ -348,6 +343,7 @@ export default function CronPage() {
))}
+
);
diff --git a/web/src/pages/DocsPage.tsx b/web/src/pages/DocsPage.tsx
index 2e1a6491fa..95ef2718f7 100644
--- a/web/src/pages/DocsPage.tsx
+++ b/web/src/pages/DocsPage.tsx
@@ -2,12 +2,19 @@ import { useLayoutEffect } from "react";
import { ExternalLink } from "lucide-react";
import { useI18n } from "@/i18n";
import { usePageHeader } from "@/contexts/usePageHeader";
-import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { PluginSlot } from "@/plugins";
export const HERMES_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/";
+const DS_BUTTON_OUTLINED_LINK_CN = cn(
+ "group relative inline-grid grid-cols-[auto_1fr_auto] items-center",
+ "px-[.9em_.75em] py-[1.25em] gap-2",
+ "leading-0 font-bold tracking-[0.2em] uppercase",
+ "text-midground bg-transparent shadow-midground",
+ "shadow-[inset_-1px_-1px_0_0_#00000080,inset_1px_1px_0_0_#ffffff80]",
+);
+
export default function DocsPage() {
const { t } = useI18n();
const { setEnd } = usePageHeader();
@@ -18,12 +25,9 @@ export default function DocsPage() {
href={HERMES_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
- className={cn(
- buttonVariants({ variant: "outline", size: "sm" }),
- "h-7 text-xs",
- )}
+ className={DS_BUTTON_OUTLINED_LINK_CN}
>
-
+
{t.app.openDocumentation}
,
);
diff --git a/web/src/pages/EnvPage.tsx b/web/src/pages/EnvPage.tsx
index 7ece6912e5..c5bf1b61ce 100644
--- a/web/src/pages/EnvPage.tsx
+++ b/web/src/pages/EnvPage.tsx
@@ -21,9 +21,9 @@ import { Toast } from "@/components/Toast";
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 { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useI18n } from "@/i18n";
@@ -134,9 +134,8 @@ function EnvVarRow({
{t.env.getKey}
)}
- }
onClick={() => setEdits((prev) => ({ ...prev, [varKey]: "" }))}>
-
{t.common.set}
@@ -159,9 +158,8 @@ function EnvVarRow({
{t.env.getKey}
)}
- }
onClick={() => setEdits((prev) => ({ ...prev, [varKey]: "" }))}>
-
{t.common.set}
@@ -206,26 +204,25 @@ function EnvVarRow({
{info.is_set && (
- onReveal(varKey)}
+ onReveal(varKey)}
title={isRevealed ? t.env.hideValue : t.env.showValue}
- aria-label={isRevealed ? `Hide ${varKey}` : `Reveal ${varKey}`}>
+ aria-label={isRevealed ? `Hide ${varKey}` : `Reveal ${varKey}`}
+ className="!p-2 aspect-square">
{isRevealed
?
: }
)}
- }
onClick={() => setEdits((prev) => ({ ...prev, [varKey]: "" }))}>
-
{info.is_set ? t.common.replace : t.common.set}
{info.is_set && (
- }
+ className="text-destructive hover:!text-destructive"
onClick={() => onClear(varKey)} disabled={saving === varKey || clearDialogOpen}>
-
{saving === varKey ? "..." : t.common.clear}
)}
@@ -238,13 +235,12 @@ function EnvVarRow({
onChange={(e) => 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" />
- onSave(varKey)}
+ onSave(varKey)} prefix={}
disabled={saving === varKey || !edits[varKey]}>
-
{saving === varKey ? "..." : t.common.save}
- onCancelEdit(varKey)}>
- {t.common.cancel}
+ } onClick={() => onCancelEdit(varKey)}>
+ {t.common.cancel}
)}
@@ -537,7 +533,7 @@ export default function EnvPage() {
{t.env.changesNote}
- setShowAdvanced(!showAdvanced)}>
+ setShowAdvanced(!showAdvanced)}>
{showAdvanced ? t.env.hideAdvanced : t.env.showAdvanced}
diff --git a/web/src/pages/LogsPage.tsx b/web/src/pages/LogsPage.tsx
index b6e6905837..fed5d607a2 100644
--- a/web/src/pages/LogsPage.tsx
+++ b/web/src/pages/LogsPage.tsx
@@ -1,8 +1,8 @@
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";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
@@ -101,13 +101,11 @@ export default function LogsPage() {
}
>
-
{t.common.refresh}
,
diff --git a/web/src/pages/SessionsPage.tsx b/web/src/pages/SessionsPage.tsx
index 48fbf7dfb0..75514faea3 100644
--- a/web/src/pages/SessionsPage.tsx
+++ b/web/src/pages/SessionsPage.tsx
@@ -36,8 +36,8 @@ import { timeAgo } from "@/lib/utils";
import { Markdown } from "@/components/Markdown";
import { PlatformsCard } from "@/components/PlatformsCard";
import { Toast } from "@/components/Toast";
+import { Button } from "@nous-research/ui";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
import { useConfirmDelete } from "@/hooks/useConfirmDelete";
@@ -356,9 +356,8 @@ function SessionRow({
{resumeInChatEnabled && (
{
@@ -370,9 +369,8 @@ function SessionRow({
)}
{
e.stopPropagation();
@@ -808,9 +806,8 @@ export default function SessionsPage() {
setPage((p) => p - 1)}
aria-label={t.sessions.previousPage}
@@ -822,9 +819,8 @@ export default function SessionsPage() {
{Math.ceil(total / PAGE_SIZE)}
= total}
onClick={() => setPage((p) => p + 1)}
aria-label={t.sessions.nextPage}
diff --git a/web/src/plugins/registry.ts b/web/src/plugins/registry.ts
index 08a5c99902..7c7186ef09 100644
--- a/web/src/plugins/registry.ts
+++ b/web/src/plugins/registry.ts
@@ -19,9 +19,9 @@ import React, {
} from "react";
import { api, fetchJSON } from "@/lib/api";
import { cn, timeAgo, isoTimeAgo } from "@/lib/utils";
+import { Button } from "@nous-research/ui";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectOption } from "@/components/ui/select";