From 5e0a9b526bd2588c907284531dbf4c3fac9b38e9 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 16 Oct 2026 10:58:14 +0700 Subject: [PATCH] ui(settings): Quit/Uninstall as real buttons with icons and a busy spinner; code in row descriptions; dimmed unavailable rows; smoother switch --- client/src/components/Switch.tsx | 9 +- .../components/settings/AutostartSettings.tsx | 21 ++- .../src/components/settings/SettingsRow.tsx | 35 ++++- .../screens.snapshot.test.tsx.snap | 131 +++++++++++++----- 4 files changed, 151 insertions(+), 45 deletions(-) diff --git a/client/src/components/Switch.tsx b/client/src/components/Switch.tsx index 1dfb06ae..c7d4f7c9 100644 --- a/client/src/components/Switch.tsx +++ b/client/src/components/Switch.tsx @@ -33,13 +33,16 @@ export function Switch({ // On: the solid accent (white knob clears 3:1 in every theme). Off: a // border-light track with an inset fg-muted/80 ring (≥3:1 in every theme), so the control's edge // is visible on any surface (surface-4 alone was ~1:1 on a light card). - className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50 ${ - checked ? "bg-accent-solid" : "bg-border-light ring-1 ring-inset ring-fg-muted/80" + // Hover darkens the track (enabled only), so it reads as clickable. + className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-40 ${ + checked + ? "bg-accent-solid enabled:hover:bg-accent-solid-hover" + : "bg-border-light ring-1 ring-inset ring-fg-muted/80 enabled:hover:ring-fg-secondary" }`} > diff --git a/client/src/components/settings/AutostartSettings.tsx b/client/src/components/settings/AutostartSettings.tsx index 9eb61636..59dee2c7 100644 --- a/client/src/components/settings/AutostartSettings.tsx +++ b/client/src/components/settings/AutostartSettings.tsx @@ -9,17 +9,24 @@ * boot/login while the switch is on. Between them, the status-area icon switch * (`GET/PUT /api/settings/tray`, on by default — server/lib/tray.js). Last, * **Uninstall AgentHub**: `POST /api/settings/uninstall` removes AgentHub from - * the machine, database included (scripts/uninstall.js). + * the machine, database included (scripts/uninstall.js). Quit is a plain + * button and Uninstall a danger one, both the same width with an icon, and + * each spins while its request runs. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { Loader2, Power, Trash2 } from "lucide-react"; import { api, type AutostartStatus, type TrayStatus } from "../../lib/api"; import { toast } from "../../lib/toast"; import { confirmDialog } from "../../lib/confirm"; import { SettingsRow, SettingsSection, SettingsSwitchRow } from "./SettingsRow"; +// The two action buttons share one width, so they line up under the switches. +const ACTION = "min-w-[6.5rem] justify-center text-xs"; +const ICON = "h-3.5 w-3.5 shrink-0"; + export function AutostartSettings() { const { t } = useTranslation("settings"); const [status, setStatus] = useState(null); @@ -126,9 +133,11 @@ export function AutostartSettings() { type="button" data-testid="quit-agenthub" disabled={quitting} + aria-busy={quitting} onClick={() => void quit()} - className="btn-ghost text-xs text-status-danger disabled:opacity-40" + className={`btn-secondary ${ACTION}`} > + {quitting ? : } {t("quit.button")} } @@ -141,9 +150,15 @@ export function AutostartSettings() { type="button" data-testid="uninstall-agenthub" disabled={uninstalling} + aria-busy={uninstalling} onClick={() => void uninstall()} - className="btn-ghost text-xs text-status-danger disabled:opacity-40" + className={`btn-danger ${ACTION}`} > + {uninstalling ? ( + + ) : ( + + )} {t("uninstall.button")} } diff --git a/client/src/components/settings/SettingsRow.tsx b/client/src/components/settings/SettingsRow.tsx index 8e32f5ff..597d5fed 100644 --- a/client/src/components/settings/SettingsRow.tsx +++ b/client/src/components/settings/SettingsRow.tsx @@ -4,7 +4,8 @@ * switch row (`SettingsSwitchRow`) and a segmented radio group * (`SettingsSegmented`, arrow keys + one roving tab stop). Rows divide * themselves with a hairline, none after the last, so a section is just its - * rows in order. + * rows in order. A string description shows its `backticked` words as code, + * and a `disabled` row dims its label, so an unavailable control reads as one. * @author Nguyễn Ngọc Trí Vĩ */ @@ -32,30 +33,55 @@ export function SettingsSection({ ); } +/** `text` with every `backticked` run drawn as inline code (a command to type). */ +export function withCode(text: string): ReactNode { + const parts = text.split(/`([^`]+)`/); + if (parts.length === 1) return text; + return parts.map((p, i) => + i % 2 ? ( + + {p} + + ) : ( + p + ) + ); +} + export function SettingsRow({ label, description, control, alignTop = false, + disabled = false, id, }: { label: ReactNode; description?: ReactNode; control: ReactNode; alignTop?: boolean; + /** The control cannot be used here: the label is dimmed with it. */ + disabled?: boolean; /** A deep-link anchor (`#id`) for one row inside a grouped section. */ id?: string; }) { return (
-

{label}

- {description &&

{description}

} +

{label}

+ {description && ( +

+ {typeof description === "string" ? withCode(description) : description} +

+ )}
{control}
@@ -82,6 +108,7 @@ export function SettingsSwitchRow({ id={id} label={label} description={description} + disabled={disabled} control={} /> ); diff --git a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap index afb9931e..8caeafbd 100644 --- a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap +++ b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap @@ -7022,7 +7022,7 @@ exports[`screen snapshots > Settings — about 1`] = ` class="mt-2" >
Settings — about 1`] = ` Start AgentHub automatically

Run the dashboard as a systemd --user service, so it starts at boot without Claude Code being opened. Turning it off keeps the running server up.

@@ -7044,19 +7044,19 @@ exports[`screen snapshots > Settings — about 1`] = `
Settings — about 1`] = ` Show AgentHub icon in the status area

An AgentHub icon beside the clock (top bar, menu bar or notification area) to open the dashboard, quit it, and start it again after a quit.

@@ -7078,19 +7078,19 @@ exports[`screen snapshots > Settings — about 1`] = `
Settings — about 1`] = ` Quit AgentHub

- Stop the dashboard so nothing runs in the background. Opening Claude Code no longer starts it; run \`agenthub start\` to bring it back. + Stop the dashboard so nothing runs in the background. Opening Claude Code no longer starts it; run + + agenthub start + + to bring it back.

Settings — about 1`] = ` Uninstall AgentHub

Remove AgentHub from this computer: the dashboard, its database and settings, its hooks in Claude Code, Codex and Antigravity, and the plugin. Your conversations, workspace folders and repositories are kept.

@@ -7139,10 +7165,45 @@ exports[`screen snapshots > Settings — about 1`] = ` class="shrink-0" >
@@ -7162,7 +7223,7 @@ exports[`screen snapshots > Settings — about 1`] = ` class="mt-2" >
Settings — about 1`] = ` Release

Settings — about 1`] = `

Settings — about 1`] = `
Settings — about 1`] = `
Settings — about 1`] = `
Settings — about 1`] = `
Settings — about 1`] = ` Live connections

Browser tabs and tools receiving live updates

@@ -7325,7 +7386,7 @@ exports[`screen snapshots > Settings — about 1`] = ` class="mt-2" >
Settings — about 1`] = ` class="mt-2" >
Settings — about 1`] = ` Database file

Settings — about 1`] = `

Settings — about 1`] = `

API keys and tokens of known shapes, bearer headers, private keys and password=/TOKEN= values in event payloads and summaries become [redacted]. The transcripts on disk are the CLIs' own files and are not changed.

@@ -8982,13 +9043,13 @@ exit 0
@@ -9739,7 +9800,7 @@ exports[`screen snapshots > Settings — notifications 1`] = ` class="min-w-0 flex-1" >
Settings — notifications 1`] = `