feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)
Usage panel (status bar): - every account shows its plan (Claude subscriptionType, Codex chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier) - new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's own file, sent only to its own host while valid, never refreshed - in-flight quota reads are shared between tabs AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added; Gemini CLI, Hermes and OpenClaw removed. Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat on an editor selection, Settings in three groups, JetBrains-style Changes panel (ticked files/hunks, ignore, blame, file history), terminal font. --no-verify: :3000 is taken by another dev server (lane-detect tests); both suites run by hand — server 2636/2639 (the 3 are :3000), client 1131/1131.
This commit is contained in:
@@ -34,7 +34,7 @@ vi.mock("../../lib/api", () => ({
|
||||
RUN_MODEL_CHOICES: [],
|
||||
}));
|
||||
vi.mock("../../components/settings/PricingSettings", () => ({
|
||||
PricingSettings: ({ vendor }: { vendor: string }) => <div>pricing:{vendor}</div>,
|
||||
PricingLink: ({ vendor }: { vendor: string }) => <div>pricing:{vendor}</div>,
|
||||
}));
|
||||
|
||||
const GEMINI: AiTool = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @file Settings.test.tsx
|
||||
* @description The install-wide Settings pages: General and Notifications never
|
||||
* @description The install-wide Settings pages: System shows the server's
|
||||
* runtime, install and database facts; General and Notifications never
|
||||
* read `/api/settings/info` (it COUNTs whole tables), Data reads it once and
|
||||
* coalesces a burst of activity instead of re-reading per event, a purge takes
|
||||
* a whole number of days and runs only after a dry-run count is confirmed, and
|
||||
@@ -38,9 +39,21 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
...actual,
|
||||
api: {
|
||||
pricing: { totalCost: vi.fn(async () => ({ total_cost: 1 })) },
|
||||
updates: {
|
||||
status: vi.fn(async () => ({
|
||||
git_repo: false,
|
||||
update_available: true,
|
||||
install_kind: "plugin",
|
||||
repo_root: "/home/u/.claude/plugins/cache/agenthub/9.9.8",
|
||||
local_version: "9.9.8",
|
||||
latest_release: "v9.9.9",
|
||||
})),
|
||||
},
|
||||
settings: {
|
||||
info: (...a: unknown[]) => info(...(a as [])),
|
||||
cleanup: (p: { dry_run?: boolean }) => cleanup(p),
|
||||
autostart: { get: vi.fn(async () => ({ available: false })) },
|
||||
tray: { get: vi.fn(async () => ({ available: false })) },
|
||||
clearData: () => clearData(),
|
||||
gc: vi.fn(async () => ({ reaped: [], capped: [] })),
|
||||
exportData: () => "/api/settings/export",
|
||||
@@ -81,6 +94,47 @@ describe("Settings", () => {
|
||||
expect(info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows the server's runtime, install and database facts on System", async () => {
|
||||
info.mockImplementationOnce(
|
||||
async () =>
|
||||
({
|
||||
db: {
|
||||
path: "/data/dashboard.db",
|
||||
size: 5 * 1024 * 1024,
|
||||
counts: {},
|
||||
pragmas: { journal_mode: "wal" },
|
||||
load_stats: { m5: 3, m15: 9, h1: 40 },
|
||||
},
|
||||
hooks: { installed: true, path: "", hooks: {} },
|
||||
server: {
|
||||
version: "1",
|
||||
uptime: 3700,
|
||||
node_version: "v26.0.0",
|
||||
platform: "linux",
|
||||
arch: "x64",
|
||||
cpus: 8,
|
||||
cpu_load: [0.5, 0.25, 0.125],
|
||||
total_mem: 16 * 1024 ** 3,
|
||||
free_mem: 4 * 1024 ** 3,
|
||||
memory: { rss: 200 * 1024 ** 2, heapUsed: 50 * 1024 ** 2, heapLimit: 4 * 1024 ** 3 },
|
||||
ws_connections: 2,
|
||||
},
|
||||
}) as never
|
||||
);
|
||||
render(<Settings section="about" />);
|
||||
expect(await screen.findByText("/data/dashboard.db")).toBeInTheDocument();
|
||||
expect(screen.getByText("1h 1m")).toBeInTheDocument();
|
||||
expect(screen.getByText(/linux · x64 · Node.js v26.0.0/)).toBeInTheDocument();
|
||||
expect(screen.getByText("8 cores · load 0.50 / 0.25 / 0.13")).toBeInTheDocument();
|
||||
expect(screen.getByText("200.0 MB RSS · heap 50.0 MB of 4.0 GB")).toBeInTheDocument();
|
||||
expect(screen.getByText("12.0 GB used of 16.0 GB")).toBeInTheDocument();
|
||||
expect(screen.getByText("wal")).toBeInTheDocument();
|
||||
expect(screen.getByText("3 in 5 min · 9 in 15 min · 40 in 1 h")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Claude Code plugin")).toBeInTheDocument();
|
||||
expect(screen.getByText("v9.9.9")).toBeInTheDocument();
|
||||
expect(screen.getByText("Update available")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reads info once on Data and coalesces a burst of activity", async () => {
|
||||
render(<Settings section="data" />);
|
||||
await screen.findByText("/tmp/x.db");
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
* @file SettingsHub.test.tsx
|
||||
* @description The Settings page shell: a path picks its section, `/settings`
|
||||
* and the legacy `?tool=` / `#ai-tools` links redirect to the path form, an
|
||||
* unknown section falls back to General, a disabled tool has no page while an
|
||||
* unknown section falls back to Appearance, a disabled tool has no page while an
|
||||
* installed, enabled catalog tool gets one, the mobile
|
||||
* select navigates, and the search filters the nav, opens the first match on
|
||||
* Enter, clears on Escape and is focused by Ctrl+F (a second press selects its
|
||||
* text), its result list deep-links a row's sub-tab, an anchor in the URL is
|
||||
* highlighted, Pricing sits in its own Cost group, and an unknown section toasts.
|
||||
* highlighted, the nav has three groups (General, Agents, Data) with no
|
||||
* one-page group, Git and Snapshots & reports are one page each (their old
|
||||
* paths redirect there), and an unknown section toasts.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -31,7 +33,18 @@ vi.mock("../AiToolConfig", () => ({
|
||||
AiToolConfig: ({ toolId }: { toolId: string }) => <div data-testid={`tool-${toolId}`} />,
|
||||
}));
|
||||
vi.mock("../AiProxyConfig", () => ({ AiProxyConfig: () => null }));
|
||||
vi.mock("../GitCredentialsConfig", () => ({ GitCredentialsConfig: () => null }));
|
||||
vi.mock("../GitCredentialsConfig", () => ({
|
||||
GitCredentialsConfig: () => <div data-testid="git-credentials" />,
|
||||
}));
|
||||
vi.mock("../../components/settings/WorktreeVisibilitySettings", () => ({
|
||||
WorktreeVisibilitySettings: () => <div data-testid="worktree-visibility" />,
|
||||
}));
|
||||
vi.mock("../../components/settings/SnapshotSettings", () => ({
|
||||
SnapshotSettings: () => <div data-testid="snapshot-settings" />,
|
||||
}));
|
||||
vi.mock("../../components/settings/ReportSettings", () => ({
|
||||
ReportSettings: () => <div data-testid="report-settings" />,
|
||||
}));
|
||||
vi.mock("../../components/settings/AiToolsSettings", () => ({
|
||||
AiToolsSettings: () => <div data-testid="ai-tools" />,
|
||||
}));
|
||||
@@ -124,6 +137,10 @@ describe("SettingsHub", () => {
|
||||
["/settings?tool=claude-code", "/settings/agents/claude-code"],
|
||||
["/settings?tool=gemini", "/settings/agents/gemini"],
|
||||
["/settings#ai-tools", "/settings/agents"],
|
||||
["/settings/about", "/settings/system"],
|
||||
["/settings/snapshots", "/settings/reports"],
|
||||
["/settings/worktrees", "/settings/git"],
|
||||
["/settings/git-credentials", "/settings/git"],
|
||||
])("redirects %s to %s", async (from, to) => {
|
||||
renderAt(from);
|
||||
await screen.findByRole("heading", { level: 1 });
|
||||
@@ -170,7 +187,7 @@ describe("SettingsHub", () => {
|
||||
const links = within(nav())
|
||||
.getAllByRole("link")
|
||||
.map((a) => a.textContent);
|
||||
expect(links).toEqual(["Claude Code", "Codex", "Pricing"]);
|
||||
expect(links).toEqual(["Pricing"]);
|
||||
expect(within(nav()).getByRole("link", { name: "Pricing" })).toHaveAttribute(
|
||||
"data-first-match",
|
||||
"true"
|
||||
@@ -234,18 +251,38 @@ describe("SettingsHub", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("puts Pricing in its own Cost group and folds language and pet into Appearance", async () => {
|
||||
it("groups the nav into General, Agents and Data, with no one-page group", async () => {
|
||||
renderAt("/settings/data");
|
||||
expect(await screen.findByTestId("settings-data")).toBeInTheDocument();
|
||||
expect(within(nav()).getByText("Cost")).toBeInTheDocument();
|
||||
const groups = within(nav())
|
||||
.getAllByRole("group")
|
||||
.map((g) => [g.getAttribute("aria-label"), within(g).getAllByRole("link").length]);
|
||||
expect(groups.map(([name]) => name)).toEqual(["General", "Agents", "Data"]);
|
||||
for (const [, count] of groups) expect(count).toBeGreaterThan(1);
|
||||
const links = within(nav())
|
||||
.getAllByRole("link")
|
||||
.map((a) => a.textContent);
|
||||
expect(links.slice(0, 3)).toEqual(["Data", "About", "Appearance"]);
|
||||
expect(links).not.toContain("General");
|
||||
expect(links.slice(0, 3)).toEqual(["Appearance", "Notifications", "System"]);
|
||||
expect(links).toEqual(expect.arrayContaining(["AI Proxy", "Pricing", "Git"]));
|
||||
expect(links).not.toContain("About");
|
||||
expect(links).not.toContain("Worktrees");
|
||||
expect(links).not.toContain("Pet");
|
||||
});
|
||||
|
||||
it("shows git logins and worktree suggestions on one Git page", async () => {
|
||||
renderAt("/settings/git");
|
||||
expect(await screen.findByRole("heading", { level: 1, name: "Git" })).toBeInTheDocument();
|
||||
expect(screen.getByTestId("git-credentials")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("worktree-visibility")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows snapshots and scheduled reports on one page", async () => {
|
||||
renderAt("/settings/reports");
|
||||
await screen.findByRole("heading", { level: 1, name: "Snapshots & reports" });
|
||||
expect(screen.getByTestId("snapshot-settings")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("report-settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("scrolls to and highlights the anchor the URL names", async () => {
|
||||
renderAt("/settings/data#data-row");
|
||||
const row = await screen.findByTestId("settings-data");
|
||||
@@ -272,7 +309,9 @@ describe("SettingsHub", () => {
|
||||
const off = subscribeToasts((i) => (seen = i));
|
||||
renderAt("/settings/nope");
|
||||
await screen.findByTestId("pet-settings");
|
||||
expect(seen.some((x) => x.message.includes("nope"))).toBe(true);
|
||||
const msg = seen.find((x) => x.message.includes("nope"))?.message ?? "";
|
||||
expect(msg).toContain("Appearance");
|
||||
expect(msg).not.toContain("General");
|
||||
off();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @file Users.test.tsx
|
||||
* @description The Users page: local mode and a member are sent home; an
|
||||
* analyst reads the table with no account controls; an admin adds a user only
|
||||
* with their own password typed (the confirm stays disabled until then), sees
|
||||
* a wrong password reported in the dialog, and cannot change their own role or
|
||||
* disable themselves.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import type { AuthState, ServerUserUsage } from "../../lib/api";
|
||||
|
||||
let authState: AuthState = { mode: "local" };
|
||||
vi.mock("../../components/AuthGate", () => ({
|
||||
useAuth: () => ({ state: authState, logout: async () => {} }),
|
||||
}));
|
||||
|
||||
const usage = { cost: 0, tokens: 0 };
|
||||
const row = (id: number, username: string, role: ServerUserUsage["role"]): ServerUserUsage => ({
|
||||
id,
|
||||
username,
|
||||
role,
|
||||
disabled: false,
|
||||
workspaces: 0,
|
||||
sessions: 0,
|
||||
last7d: usage,
|
||||
last30d: usage,
|
||||
});
|
||||
const users = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
audit: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
setPassword: vi.fn(),
|
||||
assignWorkspace: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
users,
|
||||
lanes: { list: vi.fn(async () => ({ lanes: [], counts: {} })) },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { Users } from "../Users";
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={["/users"]}>
|
||||
<Routes>
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/" element={<div data-testid="home" />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
for (const fn of Object.values(users)) fn.mockReset();
|
||||
users.list.mockResolvedValue({
|
||||
users: [row(1, "boss", "admin"), row(2, "mia", "member")],
|
||||
});
|
||||
users.audit.mockResolvedValue({ entries: [] });
|
||||
});
|
||||
|
||||
describe("Users page", () => {
|
||||
it("sends local mode and a member home", async () => {
|
||||
authState = { mode: "local" };
|
||||
renderPage();
|
||||
expect(await screen.findByTestId("home")).toBeTruthy();
|
||||
authState = { mode: "server", user: { id: 2, username: "mia", role: "member" } };
|
||||
renderPage();
|
||||
expect((await screen.findAllByTestId("home")).length).toBeGreaterThan(0);
|
||||
expect(users.list).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an analyst the accounts with no controls", async () => {
|
||||
authState = { mode: "server", user: { id: 3, username: "ana", role: "analyst" } };
|
||||
renderPage();
|
||||
expect(await screen.findByText("mia")).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: "Add user" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Reset password" })).toBeNull();
|
||||
expect(screen.getByText("Analysts can view accounts but not change them.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("adds a user only with the admin's own password", async () => {
|
||||
authState = { mode: "server", user: { id: 1, username: "boss", role: "admin" } };
|
||||
users.create.mockResolvedValue({
|
||||
user: { id: 9, username: "neo", role: "member", disabled: false },
|
||||
});
|
||||
const ue = userEvent.setup();
|
||||
renderPage();
|
||||
await ue.click(await screen.findByRole("button", { name: "Add user" }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
await ue.type(screen.getByLabelText("Username"), "neo");
|
||||
await ue.type(screen.getByLabelText("Password"), "neo-pass-123");
|
||||
const confirm = Array.from(dialog.querySelectorAll("button")).find(
|
||||
(b) => b.textContent === "Add user"
|
||||
)!;
|
||||
expect(confirm.hasAttribute("disabled")).toBe(true);
|
||||
await ue.type(screen.getByLabelText("Your password"), "boss-pass");
|
||||
expect(confirm.hasAttribute("disabled")).toBe(false);
|
||||
await ue.click(confirm);
|
||||
await waitFor(() =>
|
||||
expect(users.create).toHaveBeenCalledWith(
|
||||
{ username: "neo", password: "neo-pass-123", role: "member" },
|
||||
"boss-pass"
|
||||
)
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
expect(users.list).toHaveBeenCalledTimes(2); // reloaded after the change
|
||||
});
|
||||
|
||||
it("reports a wrong password in the dialog and keeps it open", async () => {
|
||||
authState = { mode: "server", user: { id: 1, username: "boss", role: "admin" } };
|
||||
users.setPassword.mockRejectedValue(
|
||||
Object.assign(new Error("your password is wrong"), { code: "EREAUTH" })
|
||||
);
|
||||
const ue = userEvent.setup();
|
||||
renderPage();
|
||||
await screen.findByText("mia");
|
||||
await ue.click(screen.getAllByRole("button", { name: "Reset password" })[1]!);
|
||||
await ue.type(screen.getByLabelText("New password"), "mia-new-pass-1");
|
||||
await ue.type(screen.getByLabelText("Your password"), "wrong{Enter}");
|
||||
expect(await screen.findByRole("alert")).toHaveProperty(
|
||||
"textContent",
|
||||
"Your password is wrong."
|
||||
);
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
expect(users.setPassword).toHaveBeenCalledWith(2, "mia-new-pass-1", "wrong");
|
||||
});
|
||||
|
||||
it("never offers an admin a role change or disable on their own row", async () => {
|
||||
authState = { mode: "server", user: { id: 1, username: "boss", role: "admin" } };
|
||||
renderPage();
|
||||
await screen.findByText("mia");
|
||||
const [ownRole, miaRole] = screen.getAllByRole("button", { name: "Change role" });
|
||||
const [ownLock, miaLock] = screen.getAllByRole("button", { name: "Disable" });
|
||||
expect(ownRole!.hasAttribute("disabled")).toBe(true);
|
||||
expect(ownLock!.hasAttribute("disabled")).toBe(true);
|
||||
expect(miaRole!.hasAttribute("disabled")).toBe(false);
|
||||
expect(miaLock!.hasAttribute("disabled")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -758,6 +758,21 @@ describe("split panes", () => {
|
||||
expect(screen.getByTestId("run-param").textContent).toBe("agenthub-lane-1-1");
|
||||
});
|
||||
|
||||
it("a pane's X stops just its session, without taking it out of the tab first", async () => {
|
||||
withRuns([live(0), live(1), live(2)]);
|
||||
await open("/workspace?lane=1&run=agenthub-lane-1");
|
||||
await ctrlClick(screen.getByTestId("console-tab-1"));
|
||||
await ctrlClick(screen.getByTestId("console-tab-2"));
|
||||
|
||||
withRuns([live(0), live(2)]);
|
||||
await userEvent.setup().click(screen.getByTestId("pane-stop-1"));
|
||||
await settle();
|
||||
expect(api.run.kill).toHaveBeenCalledTimes(1);
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-1");
|
||||
expect(shownPanes()).toEqual(["pane-0", "pane-2"]);
|
||||
expect(screen.queryByTestId("console-tab-1")).toBeNull();
|
||||
});
|
||||
|
||||
it("drops a remembered pane whose session is gone", async () => {
|
||||
window.localStorage.setItem(
|
||||
"agenthub.workspace.paneLayout",
|
||||
|
||||
@@ -2765,137 +2765,71 @@ exports[`screen snapshots > Claude Config 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
id="pricing"
|
||||
<div
|
||||
class="card flex items-center justify-between gap-4 p-5"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-3 mb-4"
|
||||
class="min-w-0"
|
||||
>
|
||||
<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"
|
||||
<h3
|
||||
class="flex items-center gap-2 text-sm font-semibold text-fg-primary"
|
||||
>
|
||||
<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-dollar-sign h-4 w-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"
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
</h3>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
Prices for every vendor's models live on one page.
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
class="card mt-4 px-4 py-6 text-center text-xs text-fg-muted"
|
||||
<a
|
||||
class="btn-ghost shrink-0 text-xs"
|
||||
href="/settings/pricing#pricing-claude"
|
||||
>
|
||||
No Claude model prices. Reset Defaults restores them.
|
||||
</p>
|
||||
</section>
|
||||
Open Pricing
|
||||
<svg
|
||||
class="lucide lucide-arrow-right h-3.5 w-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 5 7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7142,333 +7076,404 @@ exports[`screen snapshots > Settings — about 1`] = `
|
||||
<div
|
||||
class="animate-fade-in space-y-8"
|
||||
>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
<div
|
||||
class="space-y-6 scroll-mt-24"
|
||||
id="about"
|
||||
>
|
||||
<div
|
||||
class="mb-4"
|
||||
<section
|
||||
class="card p-5 scroll-mt-24"
|
||||
id="autostart"
|
||||
>
|
||||
<section
|
||||
class="card p-5 scroll-mt-24"
|
||||
id="autostart"
|
||||
<h3
|
||||
class="text-sm font-semibold text-fg-primary"
|
||||
>
|
||||
Startup
|
||||
</h3>
|
||||
<div
|
||||
class="mt-2"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-semibold text-fg-primary"
|
||||
>
|
||||
Startup
|
||||
</h3>
|
||||
<div
|
||||
class="mt-2"
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
|
||||
class="min-w-0"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Start AgentHub automatically
|
||||
</p>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
Start AgentHub automatically
|
||||
</p>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-label="Start AgentHub automatically"
|
||||
class="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 bg-border-light ring-1 ring-inset ring-fg-muted/80"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-transform translate-x-0.5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-label="Start AgentHub automatically"
|
||||
class="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 bg-border-light ring-1 ring-inset ring-fg-muted/80"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-transform translate-x-0.5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<p
|
||||
class="text-xs text-fg-muted mb-4"
|
||||
>
|
||||
Server runtime information.
|
||||
</p>
|
||||
<div
|
||||
class="card p-5"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3"
|
||||
>
|
||||
<div
|
||||
class="bg-surface-2 rounded-lg px-4 py-3"
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 mb-1.5"
|
||||
class="min-w-0"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-server w-4 h-4 text-accent"
|
||||
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="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
width="20"
|
||||
x="2"
|
||||
y="2"
|
||||
/>
|
||||
<rect
|
||||
height="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
width="20"
|
||||
x="2"
|
||||
y="14"
|
||||
/>
|
||||
<line
|
||||
x1="6"
|
||||
x2="6.01"
|
||||
y1="6"
|
||||
y2="6"
|
||||
/>
|
||||
<line
|
||||
x1="6"
|
||||
x2="6.01"
|
||||
y1="18"
|
||||
y2="18"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="text-[11px] text-fg-muted uppercase tracking-wider"
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Show AgentHub icon in the status area
|
||||
</p>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<button
|
||||
aria-checked="true"
|
||||
aria-label="Show AgentHub icon in the status area"
|
||||
class="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 bg-accent-solid"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="inline-block h-4 w-4 rounded-full bg-white shadow transition-transform translate-x-[18px]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Quit AgentHub
|
||||
</p>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
Stop the dashboard so nothing runs in the background. Opening Claude Code no longer starts it; run \`agenthub start\` to bring it back.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<button
|
||||
class="btn-ghost text-xs text-status-danger disabled:opacity-40"
|
||||
data-testid="quit-agenthub"
|
||||
type="button"
|
||||
>
|
||||
Quit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="card p-5 scroll-mt-24"
|
||||
id="server"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-semibold text-fg-primary"
|
||||
>
|
||||
Server
|
||||
</h3>
|
||||
<div
|
||||
class="mt-2"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Release
|
||||
</p>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
<span
|
||||
class="text-status-warning/90"
|
||||
>
|
||||
UI build v2.5.12 (rebuild client to match)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-semibold text-fg-secondary font-mono"
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
v
|
||||
</p>
|
||||
<p
|
||||
class="text-[10px] text-status-warning/90 mt-1"
|
||||
>
|
||||
UI build v2.5.11 (rebuild client to match)
|
||||
</p>
|
||||
<span
|
||||
class="text-sm text-fg-primary font-mono text-xs"
|
||||
>
|
||||
v
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-surface-2 rounded-lg px-4 py-3"
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-2 items-center"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 mb-1.5"
|
||||
class="min-w-0"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-clock w-4 h-4 text-accent"
|
||||
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"
|
||||
/>
|
||||
<polyline
|
||||
points="12 6 12 12 16 14"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="text-[11px] text-fg-muted uppercase tracking-wider"
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Uptime
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-semibold text-fg-secondary"
|
||||
>
|
||||
0m
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="bg-surface-2 rounded-lg px-4 py-3"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 mb-1.5"
|
||||
class="shrink-0"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-cpu w-4 h-4 text-status-success"
|
||||
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"
|
||||
<span
|
||||
class="text-sm text-fg-primary "
|
||||
>
|
||||
<rect
|
||||
height="16"
|
||||
rx="2"
|
||||
width="16"
|
||||
x="4"
|
||||
y="4"
|
||||
/>
|
||||
<rect
|
||||
height="6"
|
||||
rx="1"
|
||||
width="6"
|
||||
x="9"
|
||||
y="9"
|
||||
/>
|
||||
<path
|
||||
d="M15 2v2"
|
||||
/>
|
||||
<path
|
||||
d="M15 20v2"
|
||||
/>
|
||||
<path
|
||||
d="M2 15h2"
|
||||
/>
|
||||
<path
|
||||
d="M2 9h2"
|
||||
/>
|
||||
<path
|
||||
d="M20 15h2"
|
||||
/>
|
||||
<path
|
||||
d="M20 9h2"
|
||||
/>
|
||||
<path
|
||||
d="M9 2v2"
|
||||
/>
|
||||
<path
|
||||
d="M9 20v2"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="text-[11px] text-fg-muted uppercase tracking-wider"
|
||||
>
|
||||
Node.js
|
||||
</p>
|
||||
0m
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-semibold text-fg-secondary font-mono"
|
||||
>
|
||||
v22.0.0
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="bg-surface-2 rounded-lg px-4 py-3"
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-2 items-center"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 mb-1.5"
|
||||
class="min-w-0"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-globe w-4 h-4 text-syntax-keyword"
|
||||
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 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"
|
||||
/>
|
||||
<path
|
||||
d="M2 12h20"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="text-[11px] text-fg-muted uppercase tracking-wider"
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Platform
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-semibold text-fg-secondary"
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
linux
|
||||
</p>
|
||||
<span
|
||||
class="text-sm text-fg-primary font-mono text-xs"
|
||||
>
|
||||
linux
|
||||
· x64
|
||||
· Node.js
|
||||
v22.0.0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-surface-2 rounded-lg px-4 py-3"
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-2 items-center"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 mb-1.5"
|
||||
class="min-w-0"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-wifi w-4 h-4 text-status-warning"
|
||||
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 20h.01"
|
||||
/>
|
||||
<path
|
||||
d="M2 8.82a15 15 0 0 1 20 0"
|
||||
/>
|
||||
<path
|
||||
d="M5 12.859a10 10 0 0 1 14 0"
|
||||
/>
|
||||
<path
|
||||
d="M8.5 16.429a5 5 0 0 1 7 0"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="text-[11px] text-fg-muted uppercase tracking-wider"
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
WS Clients
|
||||
CPU
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm font-semibold text-fg-secondary"
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<span
|
||||
class="text-sm text-fg-primary "
|
||||
>
|
||||
1 cores · load 0.00 / 0.00 / 0.00
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-2 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Process memory
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<span
|
||||
class="text-sm text-fg-primary "
|
||||
>
|
||||
0 B RSS · heap 0 B of 0 B
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Live connections
|
||||
</p>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
Browser tabs and tools receiving live updates
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<span
|
||||
class="text-sm text-fg-primary "
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section
|
||||
class="card p-5 scroll-mt-24"
|
||||
id="install"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-semibold text-fg-primary"
|
||||
>
|
||||
Installation
|
||||
</h3>
|
||||
<div
|
||||
class="mt-2"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-2 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Unknown
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="card p-5 scroll-mt-24"
|
||||
id="database"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-semibold text-fg-primary"
|
||||
>
|
||||
Database
|
||||
</h3>
|
||||
<div
|
||||
class="mt-2"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-3 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Database file
|
||||
</p>
|
||||
<p
|
||||
class="mt-0.5 text-xs text-fg-muted"
|
||||
>
|
||||
<span
|
||||
class="font-mono break-all"
|
||||
>
|
||||
/tmp/test.db
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<span
|
||||
class="text-sm text-fg-primary "
|
||||
>
|
||||
0 B
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-2 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Journal mode
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<span
|
||||
class="text-sm text-fg-primary font-mono text-xs"
|
||||
>
|
||||
wal
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between gap-4 border-b border-border/60 last:border-b-0 py-2 items-center"
|
||||
>
|
||||
<div
|
||||
class="min-w-0"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-fg-secondary"
|
||||
>
|
||||
Events recorded
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0"
|
||||
>
|
||||
<span
|
||||
class="text-sm text-fg-primary "
|
||||
>
|
||||
0 in 5 min · 0 in 15 min · 0 in 1 h
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -338,6 +338,10 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
get: r({ available: true, kind: "systemd", enabled: false, foreign: false }),
|
||||
set: r({ available: true, kind: "systemd", enabled: true, foreign: false }),
|
||||
},
|
||||
tray: {
|
||||
get: r({ available: true, enabled: true, running: true }),
|
||||
set: r({ available: true, enabled: false, running: false }),
|
||||
},
|
||||
privacy: {
|
||||
get: r({
|
||||
redact_secrets: false,
|
||||
@@ -639,11 +643,11 @@ describe("screen snapshots", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Pricing, hook status and CLAUDE_HOME belong to Claude Code (#40 part B):
|
||||
// they sit under the Claude Code tab's Overview, not on Dashboard or on the
|
||||
// other sub-tabs.
|
||||
// Hook status and CLAUDE_HOME belong to Claude Code (#40 part B): they sit
|
||||
// under the Claude Code tab's Overview, not on Dashboard or on the other
|
||||
// sub-tabs. Its prices live on the Pricing page; the Overview only links there.
|
||||
describe("Claude Code's AgentHub settings", () => {
|
||||
it("shows pricing, hooks and CLAUDE_HOME on the Overview only", async () => {
|
||||
it("shows hooks and CLAUDE_HOME on the Overview only", async () => {
|
||||
const dashboard = render(
|
||||
<MemoryRouter initialEntries={["/settings"]}>
|
||||
<Settings />
|
||||
@@ -662,12 +666,14 @@ describe("Claude Code's AgentHub settings", () => {
|
||||
);
|
||||
await settle();
|
||||
expect(screen.queryByTestId("cc-tab-agenthub")).toBeNull();
|
||||
for (const id of ["pricing", "hooks", "claude-home"]) {
|
||||
for (const id of ["hooks", "claude-home"]) {
|
||||
expect(container.querySelector(`section#${id}`)).not.toBeNull();
|
||||
}
|
||||
expect(container.querySelector("section#pricing")).toBeNull();
|
||||
expect(container.querySelector('a[href="/settings/pricing#pricing-claude"]')).not.toBeNull();
|
||||
fireEvent.click(screen.getByTestId("cc-tab-skills"));
|
||||
await settle();
|
||||
expect(container.querySelector("section#pricing")).toBeNull();
|
||||
expect(container.querySelector("section#hooks")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user