feat(ui): AI Tools modal, one-button lane tool picker, AgentHub settings in Overview, markdown preview
- AI Tools moves from a Settings section into a modal behind a button at the Settings tab bar's right edge (AiToolsModal, `actions` on PageTabs, /settings#ai-tools); rows get an enable switch, Default badge, and an Update button for every tool whose catalog names an updater (no version lookup on open); not-installed tools fold behind a disclosure, a finished job's log folds to one line; shared AiToolIcon - codex updates through its own `codex update`, openclaw/hermes gain `update --yes`; update-all runs every installed updater and skips an npm one only when npm does not manage the binary - Add-lane tool picker is one button showing the chosen tool that opens the enabled tools as buttons - Claude Code / Codex AgentHub sub-tab folded into Overview (home editor, hooks, pricing); the path list drops CLAUDE_HOME/CODEX_HOME; HomeDirSetting shows the saved value only while the field differs - Preview/Raw toggle for markdown files in the config FileViewer and the Changes panel's whole-file view (MarkdownPreview: react-markdown + remark-gfm, raw HTML never run, frontmatter as a code block) - docs and the CLAUDE.md marker updated
This commit is contained in:
@@ -7,8 +7,9 @@
|
||||
* low-risk text-file surfaces (skills, agents, commands, output styles,
|
||||
* CLAUDE.md memory, and per-project file-based memory files). Plugins, MCP,
|
||||
* hooks-in-settings, and settings.json files stay read-only - those have
|
||||
* concurrent-write races with the live CLI. Its AgentHub sub-tab holds the
|
||||
* dashboard's own Claude Code settings (pricing, hooks, CLAUDE_HOME).
|
||||
* concurrent-write races with the live CLI. Its Overview also holds the
|
||||
* dashboard's own Claude Code settings (CLAUDE_HOME — editable, so the path
|
||||
* list leaves it out — hooks, pricing: `ClaudeAgentHubSettings`).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
@@ -59,7 +60,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import {
|
||||
Boxes,
|
||||
Gauge,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Sparkles,
|
||||
@@ -181,8 +181,7 @@ type TabKey =
|
||||
| "hooks"
|
||||
| "keybindings"
|
||||
| "settings"
|
||||
| "memory"
|
||||
| "agenthub";
|
||||
| "memory";
|
||||
|
||||
interface TabDef {
|
||||
key: TabKey;
|
||||
@@ -203,7 +202,6 @@ const TABS: TabDef[] = [
|
||||
{ key: "keybindings", icon: Keyboard, i18nKey: "tabs.keybindings" },
|
||||
{ key: "settings", icon: SettingsIcon, i18nKey: "tabs.settings" },
|
||||
{ key: "outputStyles", icon: Palette, i18nKey: "tabs.outputStyles" },
|
||||
{ key: "agenthub", icon: Gauge, i18nKey: "tabs.agenthub" },
|
||||
];
|
||||
|
||||
interface PageState {
|
||||
@@ -243,7 +241,7 @@ const EMPTY_STATE: PageState = {
|
||||
export function CcConfig() {
|
||||
const { t } = useTranslation("ccConfig");
|
||||
const [tab, setTab] = useState<TabKey>("overview");
|
||||
// Bumped by Refresh: the AgentHub sub-tab loads on mount, so a new key reloads it.
|
||||
// Bumped by Refresh: the AgentHub cards load on mount, so a new key reloads them.
|
||||
const [agenthubKey, setAgenthubKey] = useState(0);
|
||||
const [scope, setScope] = useState<CcScope>("all");
|
||||
const [data, setData] = useState<PageState>(EMPTY_STATE);
|
||||
@@ -530,72 +528,72 @@ export function CcConfig() {
|
||||
|
||||
<Tabs current={tab} onSelect={setTab} counts={data.overview?.counts} />
|
||||
|
||||
{/* AgentHub's own Claude Code settings are cards of their own, not a
|
||||
list over this tab's search box and scope. */}
|
||||
{tab === "agenthub" ? (
|
||||
<ClaudeAgentHubSettings key={agenthubKey} onHomeChanged={() => void fetchAll()} />
|
||||
) : (
|
||||
<div className="rounded-xl border border-border bg-surface-1">
|
||||
{tab !== "overview" && (
|
||||
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
|
||||
<Search className="w-4 h-4 text-fg-muted" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t("common.search")}
|
||||
className="h-7 bg-transparent text-sm text-fg-primary placeholder:text-fg-muted focus:outline-none flex-1"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch("")}
|
||||
title={t("common.clearSearch")}
|
||||
aria-label={t("common.clearSearch")}
|
||||
className="h-7 w-7 flex-shrink-0 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{isMutable(tab) && tab !== "memory" && (
|
||||
<button
|
||||
onClick={() => openCreate(tabToArtifactType(tab))}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("edit.newButton")}
|
||||
</button>
|
||||
)}
|
||||
{/* The memory tab's own "new" is a different thing: the two
|
||||
<div className="rounded-xl border border-border bg-surface-1">
|
||||
{tab !== "overview" && (
|
||||
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
|
||||
<Search className="w-4 h-4 text-fg-muted" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t("common.search")}
|
||||
className="h-7 bg-transparent text-sm text-fg-primary placeholder:text-fg-muted focus:outline-none flex-1"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch("")}
|
||||
title={t("common.clearSearch")}
|
||||
aria-label={t("common.clearSearch")}
|
||||
className="h-7 w-7 flex-shrink-0 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{isMutable(tab) && tab !== "memory" && (
|
||||
<button
|
||||
onClick={() => openCreate(tabToArtifactType(tab))}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("edit.newButton")}
|
||||
</button>
|
||||
)}
|
||||
{/* The memory tab's own "new" is a different thing: the two
|
||||
CLAUDE.md cards below are this machine's user + current-project
|
||||
files, while this creates one in ANOTHER project's directory. */}
|
||||
{tab === "memory" && (
|
||||
<button
|
||||
onClick={() => setProjectMemoryOpen(true)}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("projectMemory.newButton")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<TabPanel
|
||||
tab={tab}
|
||||
data={data}
|
||||
search={search}
|
||||
onOpenFile={openViewer}
|
||||
onEdit={openEdit}
|
||||
onDelete={openDelete}
|
||||
onCreateMemory={(s) => openCreate("memory", s)}
|
||||
onEditAuto={openEditAuto}
|
||||
onDeleteAuto={openDeleteAuto}
|
||||
onCreateAuto={openCreateAuto}
|
||||
onChanged={fetchAll}
|
||||
onToast={setToast}
|
||||
/>
|
||||
{tab === "memory" && (
|
||||
<button
|
||||
onClick={() => setProjectMemoryOpen(true)}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("projectMemory.newButton")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<TabPanel
|
||||
tab={tab}
|
||||
data={data}
|
||||
search={search}
|
||||
onOpenFile={openViewer}
|
||||
onEdit={openEdit}
|
||||
onDelete={openDelete}
|
||||
onCreateMemory={(s) => openCreate("memory", s)}
|
||||
onEditAuto={openEditAuto}
|
||||
onDeleteAuto={openDeleteAuto}
|
||||
onCreateAuto={openCreateAuto}
|
||||
onChanged={fetchAll}
|
||||
onToast={setToast}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AgentHub's own Claude Code settings follow the overview, outside the
|
||||
list's search box and scope. */}
|
||||
{tab === "overview" && (
|
||||
<ClaudeAgentHubSettings key={agenthubKey} onHomeChanged={() => void fetchAll()} />
|
||||
)}
|
||||
|
||||
{viewer && <FileViewer state={viewer} onClose={() => setViewer(null)} />}
|
||||
@@ -1069,12 +1067,6 @@ function OverviewPanel({ overview }: { overview: CcOverview | null }) {
|
||||
{t("overview.rootsTitle")}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<RootRow
|
||||
icon={FolderTree}
|
||||
tone="sky"
|
||||
label={t("overview.claudeHome")}
|
||||
value={roots.claudeHome}
|
||||
/>
|
||||
<RootRow
|
||||
icon={FolderTree}
|
||||
tone="emerald"
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
* CLI-owned and only offer add/remove, mirroring the Claude Code panel's
|
||||
* split between editable artifacts and CLI-driven surfaces. Codex has no
|
||||
* "project-local" scope and no per-project file-based memory - its scope
|
||||
* union is exactly "user" | "project". Its **AgentHub** sub-tab holds the
|
||||
* dashboard's own Codex settings: Model Pricing for OpenAI models (the shared
|
||||
* `PricingSettings` table), AgentHub's Codex hooks (`CodexHooksSettings`)
|
||||
* and CODEX_HOME.
|
||||
* union is exactly "user" | "project". Its **Overview** also holds the
|
||||
* dashboard's own Codex settings: CODEX_HOME (editable, so the overview's path
|
||||
* list leaves it out), AgentHub's Codex hooks (`CodexHooksSettings`) and Model
|
||||
* Pricing for OpenAI models (the shared `PricingSettings` table).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import {
|
||||
Boxes,
|
||||
Gauge,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Sparkles,
|
||||
@@ -82,16 +81,7 @@ import type {
|
||||
CodexSkill,
|
||||
} from "../lib/api";
|
||||
|
||||
type TabKey =
|
||||
| "overview"
|
||||
| "skills"
|
||||
| "agents"
|
||||
| "rules"
|
||||
| "memory"
|
||||
| "config"
|
||||
| "mcp"
|
||||
| "plugins"
|
||||
| "agenthub";
|
||||
type TabKey = "overview" | "skills" | "agents" | "rules" | "memory" | "config" | "mcp" | "plugins";
|
||||
|
||||
interface TabDef {
|
||||
key: TabKey;
|
||||
@@ -108,7 +98,6 @@ const TABS: TabDef[] = [
|
||||
{ key: "config", icon: SettingsIcon, i18nKey: "tabs.config" },
|
||||
{ key: "mcp", icon: Server, i18nKey: "tabs.mcp" },
|
||||
{ key: "plugins", icon: PlugZap, i18nKey: "tabs.plugins" },
|
||||
{ key: "agenthub", icon: Gauge, i18nKey: "tabs.agenthub" },
|
||||
];
|
||||
|
||||
function isNamedArtifact(tab: TabKey): tab is "skills" | "agents" | "rules" {
|
||||
@@ -160,7 +149,7 @@ export function CodexConfig() {
|
||||
const { t } = useTranslation("codexConfig");
|
||||
const { t: tSettings } = useTranslation("settings");
|
||||
const [tab, setTab] = useState<TabKey>("overview");
|
||||
// Bumped by Refresh: the AgentHub sub-tab loads on mount, so a new key reloads it.
|
||||
// Bumped by Refresh: the AgentHub cards load on mount, so a new key reloads them.
|
||||
const [agenthubKey, setAgenthubKey] = useState(0);
|
||||
// Bumped after a CODEX_HOME save: the hooks card reads the new home's hooks.json.
|
||||
const [hooksKey, setHooksKey] = useState(0);
|
||||
@@ -400,12 +389,72 @@ export function CodexConfig() {
|
||||
|
||||
<Tabs current={tab} onSelect={setTab} counts={data.overview?.counts} />
|
||||
|
||||
{/* AgentHub's own Codex settings are cards of their own, not a list. A
|
||||
new CODEX_HOME re-reads every list, since they all live under it. */}
|
||||
{tab === "agenthub" ? (
|
||||
<div className="rounded-xl border border-border bg-surface-1">
|
||||
{tab !== "overview" && tab !== "memory" && tab !== "config" && (
|
||||
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
|
||||
<Search className="w-4 h-4 text-fg-muted" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t("common.search")}
|
||||
className="h-7 bg-transparent text-sm text-fg-primary placeholder:text-fg-muted focus:outline-none flex-1"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch("")}
|
||||
title={t("common.clearSearch")}
|
||||
aria-label={t("common.clearSearch")}
|
||||
className="h-7 w-7 flex-shrink-0 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{isNamedArtifact(tab) && (
|
||||
<button
|
||||
onClick={() => openCreate(tab)}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("list.newButton")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* The memory tab hides the search bar, but still needs its own
|
||||
"new": a CLAUDE.md-style AGENTS.md in ANOTHER project's dir, not
|
||||
the user/project files listed below. */}
|
||||
{tab === "memory" && (
|
||||
<div className="flex items-center justify-end border-b border-border px-4 py-2.5">
|
||||
<button
|
||||
onClick={() => setProjectMemoryOpen(true)}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("projectMemory.newButton")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<TabPanel
|
||||
tab={tab}
|
||||
data={data}
|
||||
search={search}
|
||||
cliError={cliError}
|
||||
onOpenFile={openViewer}
|
||||
onEdit={openEdit}
|
||||
onDelete={openDelete}
|
||||
onCreateScopeFile={openCreate}
|
||||
onChanged={fetchAll}
|
||||
onToast={setToast}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AgentHub's own Codex settings follow the overview. A new CODEX_HOME
|
||||
re-reads every list, since they all live under it. */}
|
||||
{tab === "overview" && (
|
||||
<div key={agenthubKey} className="space-y-8">
|
||||
<PricingSettings vendor="openai" id="codex-pricing" />
|
||||
<CodexHooksSettings key={hooksKey} />
|
||||
<HomeDirSetting
|
||||
id="codex-home"
|
||||
title={tSettings("codexHome.title")}
|
||||
@@ -431,68 +480,8 @@ export function CodexConfig() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-border bg-surface-1">
|
||||
{tab !== "overview" && tab !== "memory" && tab !== "config" && (
|
||||
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
|
||||
<Search className="w-4 h-4 text-fg-muted" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t("common.search")}
|
||||
className="h-7 bg-transparent text-sm text-fg-primary placeholder:text-fg-muted focus:outline-none flex-1"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch("")}
|
||||
title={t("common.clearSearch")}
|
||||
aria-label={t("common.clearSearch")}
|
||||
className="h-7 w-7 flex-shrink-0 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{isNamedArtifact(tab) && (
|
||||
<button
|
||||
onClick={() => openCreate(tab)}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("list.newButton")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* The memory tab hides the search bar, but still needs its own
|
||||
"new": a CLAUDE.md-style AGENTS.md in ANOTHER project's dir, not
|
||||
the user/project files listed below. */}
|
||||
{tab === "memory" && (
|
||||
<div className="flex items-center justify-end border-b border-border px-4 py-2.5">
|
||||
<button
|
||||
onClick={() => setProjectMemoryOpen(true)}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("projectMemory.newButton")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<TabPanel
|
||||
tab={tab}
|
||||
data={data}
|
||||
search={search}
|
||||
cliError={cliError}
|
||||
onOpenFile={openViewer}
|
||||
onEdit={openEdit}
|
||||
onDelete={openDelete}
|
||||
onCreateScopeFile={openCreate}
|
||||
onChanged={fetchAll}
|
||||
onToast={setToast}
|
||||
/>
|
||||
</div>
|
||||
<CodexHooksSettings key={hooksKey} />
|
||||
<PricingSettings vendor="openai" id="codex-pricing" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -829,7 +818,6 @@ function OverviewPanel({ overview }: { overview: CodexOverview | null }) {
|
||||
{t("overview.rootsTitle")}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<RootRow icon={FolderTree} label={t("overview.codexHome")} value={roots.codexHome} />
|
||||
<RootRow
|
||||
icon={FolderTree}
|
||||
label={t("overview.projectCodexDir")}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file Settings.tsx
|
||||
* @description Settings → Dashboard: the install-wide settings — total cost, appearance, AI Tools, lanes housekeeping, import, remote sources, Sagi, notifications, alerts, data management and system information. Per-tool settings live in that tool's own tab (Claude Code's pricing, hooks and CLAUDE_HOME: `components/settings/ClaudeAgentHubSettings.tsx`).
|
||||
* @description Settings → Dashboard: the install-wide settings — total cost, appearance, lanes housekeeping, import, remote sources, Sagi, notifications, alerts, data management and system information. Per-tool settings live in that tool's own tab (Claude Code's pricing, hooks and CLAUDE_HOME: `components/settings/ClaudeAgentHubSettings.tsx`). AI Tools is a modal opened from the tab bar (`SettingsHub.tsx`).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
@@ -60,7 +60,6 @@ import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
DollarSign,
|
||||
Palette,
|
||||
Bot,
|
||||
RefreshCw,
|
||||
Database,
|
||||
HardDrive,
|
||||
@@ -112,7 +111,6 @@ import { RemoteSources } from "../components/RemoteSources";
|
||||
import { Skeleton } from "../components/Skeleton";
|
||||
import { AlertsNotifications } from "../components/AlertsNotifications";
|
||||
import { AppearanceSettings } from "../components/settings/AppearanceSettings";
|
||||
import { AiToolsSettings } from "../components/settings/AiToolsSettings";
|
||||
import type { WSMessage } from "../lib/types";
|
||||
|
||||
// In-page navigation for the (dense) Settings screen. Each entry maps to a
|
||||
@@ -124,7 +122,6 @@ const SETTINGS_SECTIONS: {
|
||||
Icon: typeof DollarSign;
|
||||
}[] = [
|
||||
{ id: "appearance", labelKey: "appearance.title", Icon: Palette },
|
||||
{ id: "ai-tools", labelKey: "aiTools.title", Icon: Bot },
|
||||
{ id: "lanes", labelKey: "lanesSection.title", Icon: GitBranch },
|
||||
{ id: "import", labelKey: "import.title", fallback: "Import", Icon: History },
|
||||
{
|
||||
@@ -626,8 +623,6 @@ export function Settings() {
|
||||
|
||||
<AppearanceSettings />
|
||||
|
||||
<AiToolsSettings />
|
||||
|
||||
{/* ─── LANES ─── */}
|
||||
<section id="lanes" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* @file SettingsHub.tsx
|
||||
* @description Tabbed shell for /settings that hosts the dashboard's own settings plus one configuration panel per AI tool (Claude Code, Codex and Antigravity) plus the AI Router profiles they can be pointed at and the git logins dashboard fetch/pull/push use, with the active tab kept in the `?tool=` query param. A tool's tab is hidden while that tool is disabled in Settings → AI Tools.
|
||||
* @description Tabbed shell for /settings that hosts the dashboard's own settings plus one configuration panel per AI tool (Claude Code, Codex and Antigravity) plus the AI Router profiles they can be pointed at and the git logins dashboard fetch/pull/push use, with the active tab kept in the `?tool=` query param. The **AI Tools** button at the bar's right edge opens `AiToolsModal` (also opened by `/settings#ai-tools`, the Add-lane modal's link); a tool's tab is hidden while that tool is disabled there.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import type { ComponentType } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useCallback, useEffect, useState, type ComponentType } from "react";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Bot,
|
||||
Boxes,
|
||||
KeyRound,
|
||||
Orbit,
|
||||
@@ -22,6 +23,7 @@ import { AntigravityConfig } from "./AntigravityConfig";
|
||||
import { AiRouterConfig } from "./AiRouterConfig";
|
||||
import { GitCredentialsConfig } from "./GitCredentialsConfig";
|
||||
import { useAiTools } from "../hooks/useAiTools";
|
||||
import { AiToolsModal } from "../components/settings/AiToolsModal";
|
||||
|
||||
interface ToolTab {
|
||||
id: string;
|
||||
@@ -85,6 +87,19 @@ export function SettingsHub() {
|
||||
(tab) => !tab.aiTool || aiTools.byId(tab.aiTool)?.enabled !== false
|
||||
);
|
||||
const active = tabs.find((tab) => tab.id === requested) ?? DASHBOARD_TAB;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [aiToolsOpen, setAiToolsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.hash === "#ai-tools") setAiToolsOpen(true);
|
||||
}, [location.hash]);
|
||||
|
||||
const closeAiTools = useCallback(() => {
|
||||
setAiToolsOpen(false);
|
||||
// Drop the deep link, or a reload would open the modal again.
|
||||
if (location.hash) navigate({ search: location.search }, { replace: true });
|
||||
}, [location.hash, location.search, navigate]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -97,9 +112,21 @@ export function SettingsHub() {
|
||||
Icon,
|
||||
label: t(labelKey),
|
||||
}))}
|
||||
actions={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAiToolsOpen(true)}
|
||||
aria-haspopup="dialog"
|
||||
className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-lg border border-border px-3 py-1.5 text-xs text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg-primary"
|
||||
>
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
{t("aiTools.title")}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<active.Panel />
|
||||
{aiToolsOpen && <AiToolsModal onClose={closeAiTools} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("CodexConfig", () => {
|
||||
it("edits CODEX_HOME on the AgentHub sub-tab and reports lanes whose router layer failed", async () => {
|
||||
it("edits CODEX_HOME on the Overview and reports lanes whose router layer failed", async () => {
|
||||
stubCodexApi({});
|
||||
codexHomeMock.get.mockResolvedValue({ codex_home: "/home/.codex", env_owned: false });
|
||||
codexHomeMock.set.mockResolvedValue({
|
||||
@@ -136,7 +136,8 @@ describe("CodexConfig", () => {
|
||||
await settle();
|
||||
const overviewCalls = codexConfigMock.overview.mock.calls.length;
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: /agenthub/i }));
|
||||
// No AgentHub sub-tab: its cards sit under the Overview, the page's first tab.
|
||||
expect(screen.queryByRole("tab", { name: /agenthub/i })).toBeNull();
|
||||
const input = await screen.findByDisplayValue("/home/.codex");
|
||||
expect(await screen.findByText("Codex hooks")).toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText(/search/i)).not.toBeInTheDocument();
|
||||
|
||||
@@ -58,6 +58,7 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
aiTools: { iconUrl: (id: string) => `/api/ai-tools/${id}/icon` },
|
||||
lanes: {
|
||||
list: vi.fn().mockImplementation(async () => {
|
||||
recordCall("GET", "/api/lanes");
|
||||
|
||||
@@ -1474,35 +1474,6 @@ exports[`screen snapshots > Claude Config 1`] = `
|
||||
0
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-selected="false"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors flex-shrink-0 whitespace-nowrap text-fg-secondary hover:text-fg-primary hover:bg-surface-3 border border-transparent"
|
||||
data-testid="cc-tab-agenthub"
|
||||
role="tab"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-gauge w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m12 14 4-4"
|
||||
/>
|
||||
<path
|
||||
d="M3.34 19a10 10 0 1 1 17.32 0"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
AgentHub
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 top-0 bottom-0 w-12 rounded-r-xl bg-gradient-to-l from-surface-1 to-transparent transition-opacity z-10 opacity-0"
|
||||
@@ -1526,86 +1497,6 @@ exports[`screen snapshots > Claude Config 1`] = `
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-2"
|
||||
>
|
||||
<div
|
||||
class="relative flex items-center gap-2.5 rounded-lg border border-border bg-surface-2 px-3 py-2 min-w-0 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="absolute left-0 top-0 bottom-0 w-1 bg-sky-500/40"
|
||||
/>
|
||||
<span
|
||||
class="w-7 h-7 rounded-md bg-sky-500/10 flex items-center justify-center flex-shrink-0 ml-1.5"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-folder-tree w-3.5 h-3.5 text-sky-700 dark:text-sky-300"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"
|
||||
/>
|
||||
<path
|
||||
d="M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"
|
||||
/>
|
||||
<path
|
||||
d="M3 5a2 2 0 0 0 2 2h3"
|
||||
/>
|
||||
<path
|
||||
d="M3 3v13a2 2 0 0 0 2 2h3"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="min-w-0 flex-1"
|
||||
>
|
||||
<div
|
||||
class="text-[10px] font-semibold uppercase tracking-wider text-fg-secondary"
|
||||
>
|
||||
Claude home
|
||||
</div>
|
||||
<div
|
||||
class="font-mono text-[11px] text-fg-secondary truncate"
|
||||
>
|
||||
/home/test/.claude
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-[10px] font-medium px-1.5 py-1 rounded border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1 flex-shrink-0"
|
||||
title="Copy path"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-copy w-3 h-3"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
height="14"
|
||||
rx="2"
|
||||
ry="2"
|
||||
width="14"
|
||||
x="8"
|
||||
y="8"
|
||||
/>
|
||||
<path
|
||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex items-center gap-2.5 rounded-lg border border-border bg-surface-2 px-3 py-2 min-w-0 overflow-hidden"
|
||||
>
|
||||
@@ -2602,6 +2493,378 @@ exports[`screen snapshots > Claude Config 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="space-y-8"
|
||||
>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
id="claude-home"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-folder-open w-4 h-4 text-fg-muted"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"
|
||||
/>
|
||||
</svg>
|
||||
CLAUDE_HOME
|
||||
</h3>
|
||||
<p
|
||||
class="text-xs text-fg-muted mb-1"
|
||||
>
|
||||
Root directory for Claude Code session data. Changing this updates the scan path for transcripts and imports immediately.
|
||||
</p>
|
||||
<p
|
||||
class="text-[11px] text-fg-muted italic mb-4 leading-snug"
|
||||
>
|
||||
Informational: Cursor sessions count here too — Cursor happens to use the same ~/.claude paths as Claude Code (locally and on synced remotes).
|
||||
</p>
|
||||
<div
|
||||
class="card p-5 space-y-4 "
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<input
|
||||
aria-label="CLAUDE_HOME"
|
||||
class="flex-1 bg-surface-4 border border-surface-3 rounded-lg px-3 py-2 text-sm text-fg-secondary font-mono focus:outline-none focus:border-violet-500/50"
|
||||
placeholder="/path/to/claude-home"
|
||||
type="text"
|
||||
value="/home/test/.claude"
|
||||
/>
|
||||
<button
|
||||
class="btn-primary px-4 py-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled=""
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
id="hooks"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-plug w-4 h-4 text-fg-muted"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 22v-5"
|
||||
/>
|
||||
<path
|
||||
d="M9 8V2"
|
||||
/>
|
||||
<path
|
||||
d="M15 8V2"
|
||||
/>
|
||||
<path
|
||||
d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"
|
||||
/>
|
||||
</svg>
|
||||
Hook Configuration
|
||||
</h3>
|
||||
<p
|
||||
class="text-xs text-fg-muted mb-4"
|
||||
>
|
||||
Claude Code hooks forward events to the dashboard in real time.
|
||||
</p>
|
||||
<div
|
||||
class="card p-5 space-y-4"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-wrap gap-3"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-xs font-medium text-status-success bg-status-success/10 border border-status-success/20 px-2.5 py-1 rounded-full"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-circle-check-big w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.801 10A10 10 0 1 1 17 3.335"
|
||||
/>
|
||||
<path
|
||||
d="m9 11 3 3L22 4"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
All hooks installed
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="btn-ghost text-xs disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-rotate-ccw w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
|
||||
/>
|
||||
<path
|
||||
d="M3 3v5h5"
|
||||
/>
|
||||
</svg>
|
||||
Reinstall Hooks
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2"
|
||||
/>
|
||||
<p
|
||||
class="text-[11px] text-fg-muted font-mono truncate"
|
||||
>
|
||||
/tmp/settings.json
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
id="pricing"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-3 mb-4"
|
||||
>
|
||||
<div>
|
||||
<h3
|
||||
class="text-sm font-medium text-fg-secondary flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-dollar-sign w-4 h-4 text-fg-muted"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="2"
|
||||
y2="22"
|
||||
/>
|
||||
<path
|
||||
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
|
||||
/>
|
||||
</svg>
|
||||
Model Pricing
|
||||
<button
|
||||
aria-label="How model pricing works"
|
||||
class="inline-flex items-center justify-center rounded-full p-0.5 text-fg-muted hover:text-fg-secondary focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-info w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
/>
|
||||
<path
|
||||
d="M12 16v-4"
|
||||
/>
|
||||
<path
|
||||
d="M12 8h.01"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</h3>
|
||||
<p
|
||||
class="text-xs text-fg-muted mt-0.5"
|
||||
>
|
||||
Rates per million tokens (USD). Use % as wildcard in patterns. Some models have a time-limited introductory rate (shown in violet); usage on or before its cutoff date is billed at that rate, and usage after it at the standard rate.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<button
|
||||
class="text-xs px-3 py-1.5 rounded-md transition-colors disabled:opacity-50 inline-flex items-center gap-1.5 text-fg-secondary hover:text-fg-primary hover:bg-surface-4"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-rotate-ccw w-3 h-3"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
|
||||
/>
|
||||
<path
|
||||
d="M3 3v5h5"
|
||||
/>
|
||||
</svg>
|
||||
Reset Defaults
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary text-xs disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-plus w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 12h14"
|
||||
/>
|
||||
<path
|
||||
d="M12 5v14"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
Add Model
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="card overflow-x-auto mt-4"
|
||||
>
|
||||
<table
|
||||
class="w-full min-w-[1000px]"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="border-b border-border text-left"
|
||||
>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider"
|
||||
>
|
||||
Pattern
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider"
|
||||
>
|
||||
Model
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
|
||||
>
|
||||
Input
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
|
||||
>
|
||||
Output
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
|
||||
>
|
||||
Cache Read
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
|
||||
>
|
||||
Cache Write (5m)
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
|
||||
>
|
||||
Cache Write (1h)
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
|
||||
>
|
||||
Fast Input
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right"
|
||||
>
|
||||
Fast Output
|
||||
</th>
|
||||
<th
|
||||
class="w-24 px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-border"
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
class="px-4 py-6 text-center text-xs text-fg-muted"
|
||||
colspan="10"
|
||||
>
|
||||
No Claude model prices. Reset Defaults restores them.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -7025,47 +7288,6 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
</svg>
|
||||
Appearance
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-2.5 py-1.5 rounded-lg border transition-colors flex-shrink-0 border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-bot w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 8V4H8"
|
||||
/>
|
||||
<rect
|
||||
height="12"
|
||||
rx="2"
|
||||
width="16"
|
||||
x="4"
|
||||
y="8"
|
||||
/>
|
||||
<path
|
||||
d="M2 14h2"
|
||||
/>
|
||||
<path
|
||||
d="M20 14h2"
|
||||
/>
|
||||
<path
|
||||
d="M15 13v2"
|
||||
/>
|
||||
<path
|
||||
d="M9 13v2"
|
||||
/>
|
||||
</svg>
|
||||
AI Tools
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-2.5 py-1.5 rounded-lg border transition-colors flex-shrink-0 border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
type="button"
|
||||
@@ -8078,141 +8300,6 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
id="ai-tools"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-bot w-4 h-4 text-fg-muted"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 8V4H8"
|
||||
/>
|
||||
<rect
|
||||
height="12"
|
||||
rx="2"
|
||||
width="16"
|
||||
x="4"
|
||||
y="8"
|
||||
/>
|
||||
<path
|
||||
d="M2 14h2"
|
||||
/>
|
||||
<path
|
||||
d="M20 14h2"
|
||||
/>
|
||||
<path
|
||||
d="M15 13v2"
|
||||
/>
|
||||
<path
|
||||
d="M9 13v2"
|
||||
/>
|
||||
</svg>
|
||||
AI Tools
|
||||
</h3>
|
||||
<p
|
||||
class="text-xs text-fg-muted mb-4"
|
||||
>
|
||||
AI coding CLIs AgentHub can run in a lane. Detected on this machine's PATH; a disabled tool is hidden from Add lane.
|
||||
</p>
|
||||
<div
|
||||
class="card p-5 space-y-5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 mb-2"
|
||||
>
|
||||
<p
|
||||
class="text-xs font-medium text-fg-secondary"
|
||||
>
|
||||
Installed (0 detected)
|
||||
</p>
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs text-fg-secondary hover:bg-surface-3 disabled:opacity-50"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-download w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
|
||||
/>
|
||||
<polyline
|
||||
points="7 10 12 15 17 10"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="15"
|
||||
y2="3"
|
||||
/>
|
||||
</svg>
|
||||
Update tools
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs text-fg-secondary hover:bg-surface-3 disabled:opacity-50"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-refresh-cw w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"
|
||||
/>
|
||||
<path
|
||||
d="M21 3v5h-5"
|
||||
/>
|
||||
<path
|
||||
d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"
|
||||
/>
|
||||
<path
|
||||
d="M8 16H3v5"
|
||||
/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="divide-y divide-border rounded-md border border-border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
id="lanes"
|
||||
|
||||
@@ -591,9 +591,10 @@ describe("screen snapshots", () => {
|
||||
});
|
||||
|
||||
// Pricing, hook status and CLAUDE_HOME belong to Claude Code (#40 part B):
|
||||
// they live behind the Claude Code tab's AgentHub sub-tab, not on Dashboard.
|
||||
// they sit under the Claude Code tab's Overview, not on Dashboard or on the
|
||||
// other sub-tabs.
|
||||
describe("Claude Code's AgentHub settings", () => {
|
||||
it("shows pricing, hooks and CLAUDE_HOME on the AgentHub sub-tab only", async () => {
|
||||
it("shows pricing, hooks and CLAUDE_HOME on the Overview only", async () => {
|
||||
const dashboard = render(
|
||||
<MemoryRouter initialEntries={["/settings"]}>
|
||||
<Settings />
|
||||
@@ -611,12 +612,13 @@ describe("Claude Code's AgentHub settings", () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
expect(container.querySelector("section#pricing")).toBeNull();
|
||||
fireEvent.click(screen.getByTestId("cc-tab-agenthub"));
|
||||
await settle();
|
||||
expect(screen.queryByTestId("cc-tab-agenthub")).toBeNull();
|
||||
for (const id of ["pricing", "hooks", "claude-home"]) {
|
||||
expect(container.querySelector(`section#${id}`)).not.toBeNull();
|
||||
}
|
||||
fireEvent.click(screen.getByTestId("cc-tab-skills"));
|
||||
await settle();
|
||||
expect(container.querySelector("section#pricing")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user