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:
@@ -8,7 +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".
|
||||
* 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.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -17,6 +20,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import {
|
||||
Boxes,
|
||||
Gauge,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Sparkles,
|
||||
@@ -51,6 +55,9 @@ import { ToastNotice, type Toast } from "../components/config/ToastNotice";
|
||||
import { ScopeBadge } from "../components/config/ScopeBadge";
|
||||
import { ConfigTabBar } from "../components/config/ConfigTabBar";
|
||||
import { ProjectMemoryModal } from "../components/config/ProjectMemoryModal";
|
||||
import { HomeDirSetting } from "../components/settings/HomeDirSetting";
|
||||
import { PricingSettings } from "../components/settings/PricingSettings";
|
||||
import { CodexHooksSettings } from "../components/settings/CodexHooksSettings";
|
||||
import {
|
||||
Empty,
|
||||
SkeletonRows,
|
||||
@@ -75,7 +82,16 @@ import type {
|
||||
CodexSkill,
|
||||
} from "../lib/api";
|
||||
|
||||
type TabKey = "overview" | "skills" | "agents" | "rules" | "memory" | "config" | "mcp" | "plugins";
|
||||
type TabKey =
|
||||
| "overview"
|
||||
| "skills"
|
||||
| "agents"
|
||||
| "rules"
|
||||
| "memory"
|
||||
| "config"
|
||||
| "mcp"
|
||||
| "plugins"
|
||||
| "agenthub";
|
||||
|
||||
interface TabDef {
|
||||
key: TabKey;
|
||||
@@ -92,6 +108,7 @@ 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" {
|
||||
@@ -141,7 +158,12 @@ interface NamedArtifactItem {
|
||||
|
||||
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.
|
||||
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);
|
||||
const [scope, setScope] = useState<CodexScope>("all");
|
||||
const [data, setData] = useState<PageState>(EMPTY_STATE);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -361,7 +383,10 @@ export function CodexConfig() {
|
||||
lastUpdated={lastUpdated}
|
||||
scope={scope}
|
||||
onScopeChange={setScope}
|
||||
onRefresh={fetchAll}
|
||||
onRefresh={() => {
|
||||
void fetchAll();
|
||||
setAgenthubKey((k) => k + 1);
|
||||
}}
|
||||
onOpenBackups={() => setBackupsOpen(true)}
|
||||
wsConnected={wsConnected}
|
||||
/>
|
||||
@@ -375,67 +400,101 @@ export function CodexConfig() {
|
||||
|
||||
<Tabs current={tab} onSelect={setTab} counts={data.overview?.counts} />
|
||||
|
||||
<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 && (
|
||||
{/* 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 key={agenthubKey} className="space-y-8">
|
||||
<PricingSettings vendor="openai" id="codex-pricing" />
|
||||
<CodexHooksSettings key={hooksKey} />
|
||||
<HomeDirSetting
|
||||
id="codex-home"
|
||||
title={tSettings("codexHome.title")}
|
||||
description={tSettings("codexHome.description")}
|
||||
note={tSettings("codexHome.restartNote")}
|
||||
placeholder={tSettings("codexHome.placeholder")}
|
||||
load={() =>
|
||||
api.settings.codexHome
|
||||
.get()
|
||||
.then((r) => ({ value: r.codex_home, envOwned: r.env_owned }))
|
||||
}
|
||||
save={(p) =>
|
||||
api.settings.codexHome.set(p).then((r) => {
|
||||
void fetchAll();
|
||||
setHooksKey((k) => k + 1);
|
||||
return {
|
||||
value: r.codex_home,
|
||||
envOwned: r.env_owned,
|
||||
warning: r.routers.failed.length
|
||||
? tSettings("codexHome.routersFailed", { lanes: r.routers.failed.join(", ") })
|
||||
: null,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</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
|
||||
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)}
|
||||
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("list.newButton")}
|
||||
{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>
|
||||
)}
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{backupsOpen && (
|
||||
<BackupsModal roots={data.overview?.roots} onClose={() => setBackupsOpen(false)} />
|
||||
|
||||
Reference in New Issue
Block a user