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:
@@ -7,7 +7,8 @@
|
||||
* 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.
|
||||
* concurrent-write races with the live CLI. Its AgentHub sub-tab holds the
|
||||
* dashboard's own Claude Code settings (pricing, hooks, CLAUDE_HOME).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
@@ -58,6 +59,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import {
|
||||
Boxes,
|
||||
Gauge,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Sparkles,
|
||||
@@ -110,6 +112,7 @@ import { MdItemList } from "../components/config/MdItemList";
|
||||
import { ScopeBadge } from "../components/config/ScopeBadge";
|
||||
import { ConfigTabBar } from "../components/config/ConfigTabBar";
|
||||
import { ProjectMemoryModal } from "../components/config/ProjectMemoryModal";
|
||||
import { ClaudeAgentHubSettings } from "../components/settings/ClaudeAgentHubSettings";
|
||||
import {
|
||||
Empty,
|
||||
SkeletonRows,
|
||||
@@ -178,7 +181,8 @@ type TabKey =
|
||||
| "hooks"
|
||||
| "keybindings"
|
||||
| "settings"
|
||||
| "memory";
|
||||
| "memory"
|
||||
| "agenthub";
|
||||
|
||||
interface TabDef {
|
||||
key: TabKey;
|
||||
@@ -199,6 +203,7 @@ 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 {
|
||||
@@ -238,6 +243,8 @@ 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.
|
||||
const [agenthubKey, setAgenthubKey] = useState(0);
|
||||
const [scope, setScope] = useState<CcScope>("all");
|
||||
const [data, setData] = useState<PageState>(EMPTY_STATE);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -505,7 +512,10 @@ export function CcConfig() {
|
||||
lastUpdated={lastUpdated}
|
||||
scope={scope}
|
||||
onScopeChange={setScope}
|
||||
onRefresh={fetchAll}
|
||||
onRefresh={() => {
|
||||
void fetchAll();
|
||||
setAgenthubKey((k) => k + 1);
|
||||
}}
|
||||
onOpenBackups={() => setBackupsOpen(true)}
|
||||
onOpenProfiles={() => setProfilesOpen(true)}
|
||||
wsConnected={wsConnected}
|
||||
@@ -520,67 +530,73 @@ export function CcConfig() {
|
||||
|
||||
<Tabs current={tab} onSelect={setTab} counts={data.overview?.counts} />
|
||||
|
||||
<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
|
||||
{/* 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
|
||||
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>
|
||||
)}
|
||||
{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 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>
|
||||
)}
|
||||
|
||||
{viewer && <FileViewer state={viewer} onClose={() => setViewer(null)} />}
|
||||
{editor && (
|
||||
|
||||
Reference in New Issue
Block a user