feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from Settings → Dashboard to the Claude Code tab's AgentHub sub-tab (ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub. A home that is or contains the OS home is refused (EBADHOME). Settings persistence: dashboard-saved settings go to ~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin cache an update wipes. Load order: real env > the checkout's .env > that file. The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put. C1: codex-usage.js reads Codex rollout journals into sessions (new column tool) and token_usage, tracked per file in codex_rollouts; forks deduped by total vector, sub-agent threads folded into the root; pricing-sync also prices OpenAI models; PricingSettings with a vendor filter. C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on a click, through a stable forwarder; Codex trust is never written. The hook route creates tool='codex' sessions and folds sub-agent hooks into the root. D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab; Gemini pricing. Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=, clear-data covering the new tables, Codex child-thread events moving no status, hooks owning status only while current, EFOREIGN for a forwarder that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE honoured from .env. Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
This commit is contained in:
@@ -46,6 +46,27 @@ const codexConfigMock = vi.hoisted(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const codexHomeMock = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
}));
|
||||
|
||||
const codexHooksMock = vi.hoisted(() => ({
|
||||
get: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
supported: true,
|
||||
readable: true,
|
||||
path: "/home/.codex/hooks.json",
|
||||
script: "/home/.codex/agenthub-hook.sh",
|
||||
installed: false,
|
||||
trusted: false,
|
||||
events: {},
|
||||
})
|
||||
),
|
||||
install: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
@@ -53,6 +74,11 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
api: {
|
||||
...(actual.api as Record<string, unknown>),
|
||||
codexConfig: codexConfigMock,
|
||||
settings: {
|
||||
...((actual.api as { settings: Record<string, unknown> }).settings ?? {}),
|
||||
codexHome: codexHomeMock,
|
||||
codexHooks: codexHooksMock,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -97,6 +123,33 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("CodexConfig", () => {
|
||||
it("edits CODEX_HOME on the AgentHub sub-tab and reports lanes whose router layer failed", async () => {
|
||||
stubCodexApi({});
|
||||
codexHomeMock.get.mockResolvedValue({ codex_home: "/home/.codex", env_owned: false });
|
||||
codexHomeMock.set.mockResolvedValue({
|
||||
ok: true,
|
||||
codex_home: "/work/.codex",
|
||||
env_owned: false,
|
||||
routers: { updated: 1, failed: ["4 (api): EDIR"] },
|
||||
});
|
||||
render(<CodexConfig />);
|
||||
await settle();
|
||||
const overviewCalls = codexConfigMock.overview.mock.calls.length;
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: /agenthub/i }));
|
||||
const input = await screen.findByDisplayValue("/home/.codex");
|
||||
expect(await screen.findByText("Codex hooks")).toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText(/search/i)).not.toBeInTheDocument();
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "/work/.codex");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
expect(codexHomeMock.set).toHaveBeenCalledWith("/work/.codex");
|
||||
expect(await screen.findByText(/4 \(api\): EDIR/)).toBeInTheDocument();
|
||||
// Every list lives under CODEX_HOME, so a new one re-reads them.
|
||||
expect(codexConfigMock.overview.mock.calls.length).toBeGreaterThan(overviewCalls);
|
||||
});
|
||||
|
||||
it("offers create on the authored surfaces and withholds it on the CLI-owned ones", async () => {
|
||||
stubCodexApi({ skills: [], mcp: { servers: [] } });
|
||||
render(<CodexConfig />);
|
||||
|
||||
@@ -1474,6 +1474,35 @@ 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"
|
||||
@@ -6867,7 +6896,7 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
<p
|
||||
class="text-xs text-fg-muted"
|
||||
>
|
||||
Manage pricing, notifications, data, and hooks
|
||||
Manage appearance, AI tools, notifications and data
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6951,7 +6980,7 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
class="flex items-center gap-1.5 overflow-x-auto no-scrollbar flex-1"
|
||||
>
|
||||
<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"
|
||||
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 bg-accent/15 border-accent/30 text-accent"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -7037,65 +7066,6 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
</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 bg-accent/15 border-accent/30 text-accent"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-dollar-sign 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"
|
||||
>
|
||||
<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>
|
||||
<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-plug 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 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
|
||||
</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"
|
||||
@@ -7134,28 +7104,6 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
</svg>
|
||||
Lanes
|
||||
</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-folder-open 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="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
|
||||
</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"
|
||||
@@ -8265,308 +8213,6 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
</div>
|
||||
</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"
|
||||
/>
|
||||
</table>
|
||||
</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="lanes"
|
||||
@@ -8654,73 +8300,6 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<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
|
||||
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>
|
||||
<p
|
||||
class="text-xs text-fg-muted"
|
||||
>
|
||||
Current:
|
||||
|
||||
<code
|
||||
class="text-fg-secondary"
|
||||
>
|
||||
/home/test/.claude
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="scroll-mt-24"
|
||||
id="import"
|
||||
|
||||
@@ -590,6 +590,36 @@ 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.
|
||||
describe("Claude Code's AgentHub settings", () => {
|
||||
it("shows pricing, hooks and CLAUDE_HOME on the AgentHub sub-tab only", async () => {
|
||||
const dashboard = render(
|
||||
<MemoryRouter initialEntries={["/settings"]}>
|
||||
<Settings />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
for (const id of ["pricing", "hooks", "claude-home"]) {
|
||||
expect(dashboard.container.querySelector(`section#${id}`)).toBeNull();
|
||||
}
|
||||
dashboard.unmount();
|
||||
|
||||
const { container } = render(
|
||||
<MemoryRouter initialEntries={["/cc-config"]}>
|
||||
<CcConfig />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
expect(container.querySelector("section#pricing")).toBeNull();
|
||||
fireEvent.click(screen.getByTestId("cc-tab-agenthub"));
|
||||
await settle();
|
||||
for (const id of ["pricing", "hooks", "claude-home"]) {
|
||||
expect(container.querySelector(`section#${id}`)).not.toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// The page opens on Overview; the plugin cards live behind the Plugins tab.
|
||||
async function openPluginsTab() {
|
||||
render(
|
||||
|
||||
Reference in New Issue
Block a user