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:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @file HomeDirSetting.tsx
|
||||
* @description One editable home directory (CLAUDE_HOME in Claude Code →
|
||||
* AgentHub, CODEX_HOME in Codex → AgentHub): a path field with Save, the
|
||||
* server's refusal inline, the value in effect, and a warning when the real
|
||||
* environment sets the variable (the environment wins at the next start, so a
|
||||
* saved value would not survive a restart). The server validates and persists;
|
||||
* this component only loads, submits and reports.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertTriangle, FolderOpen } from "lucide-react";
|
||||
|
||||
export interface HomeDirValue {
|
||||
value: string;
|
||||
envOwned: boolean;
|
||||
/** Shown after a save that succeeded only in part (e.g. a lane not updated). */
|
||||
warning?: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** `<section id>` — a deep-link target. */
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
/** Optional second line under the description. */
|
||||
note?: string;
|
||||
placeholder: string;
|
||||
load: () => Promise<HomeDirValue>;
|
||||
save: (path: string) => Promise<HomeDirValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An editable home directory with its own load/save cycle.
|
||||
* @returns The section, or its skeleton-free shell while loading.
|
||||
*/
|
||||
export function HomeDirSetting({ id, title, description, note, placeholder, load, save }: Props) {
|
||||
const { t } = useTranslation("settings");
|
||||
const [current, setCurrent] = useState<HomeDirValue | null>(null);
|
||||
const [input, setInput] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
load()
|
||||
.then((v) => {
|
||||
if (!live) return;
|
||||
setCurrent(v);
|
||||
setInput(v.value);
|
||||
})
|
||||
.catch((err) => live && setError(err instanceof Error ? err.message : String(err)));
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
// `load` is a stable api call per mount; re-running on identity would loop.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!current || input === current.value) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const next = await save(input);
|
||||
setCurrent(next);
|
||||
setInput(next.value);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("homeDir.saveFailed"));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id={id} className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<FolderOpen className="w-4 h-4 text-fg-muted" />
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-xs text-fg-muted mb-1">{description}</p>
|
||||
{note && <p className="text-[11px] text-fg-muted italic mb-4 leading-snug">{note}</p>}
|
||||
|
||||
<div className={`card p-5 space-y-4 ${note ? "" : "mt-3"}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
aria-label={title}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
className="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={placeholder}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || !current || input === current.value}
|
||||
className="btn-primary px-4 py-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? t("homeDir.saving") : t("homeDir.save")}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-xs text-status-danger">{error}</p>}
|
||||
{current && (
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t("homeDir.current")} <code className="text-fg-secondary">{current.value}</code>
|
||||
</p>
|
||||
)}
|
||||
{current?.envOwned && (
|
||||
<p className="text-xs text-status-warning flex items-start gap-1.5">
|
||||
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0 mt-px" />
|
||||
{t("homeDir.envOwned")}
|
||||
</p>
|
||||
)}
|
||||
{current?.warning && (
|
||||
<p className="text-xs text-status-warning flex items-start gap-1.5">
|
||||
<AlertTriangle className="w-3.5 h-3.5 flex-shrink-0 mt-px" />
|
||||
{current.warning}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user