feat: model lists and prices that follow each AI tool, a Settings tab per tool, live lane status (#40)
Pricing: the LiteLLM sync now adds every current Claude / GPT (>= 5) /
Gemini (>= 3) chat model before it is used and keeps its own rows
(model_pricing.source = 'sync', the Auto badge) at the published rate. A
user's row (any PUT) and a deleted pattern (model_pricing_dismissed) are
never touched; a deleted built-in row is not re-seeded at the next start; a
reset re-applies the list at once; retired Claude models are priced only
once used.
Models: GET /api/ai-tools/:id/models reads each tool's own list (Codex's
models_cache.json, Claude's aliases named after the newest version plus
priced versions, a catalog tool's vendor rows plus the models its sessions
used). A catalog `model: {flag, vendor?}` field passes `<flag> <id>`
(validated: never a flag or shell text); a per-tool default model starts
each run, and a resume keeps the session's own model.
Settings: every installed, enabled catalog tool without its own tab gets
one (AiToolConfig.tsx): path, homepage, Update, sessions/cost/lanes,
default model, vendor pricing, and the config files its release catalog
lists, edited over the opened sha, syntax-checked, backed up, written
atomically; credential-looking names refused. Antigravity keeps its hooks
card there.
Lanes: a just-started run no longer reads "dead" (age counts the run's
start while the old session is still bound); a second dashboard on the
same database re-broadcasts the lanes the other changed (PRAGMA
data_version, every 2 s); a lane start with a bad model answers 400.
DB: every migration runs under a cross-process lock (<db>-migrate.lock), so
two dashboards started at once on a fresh database both come up.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file SettingsHub.tsx
|
||||
* @description Tabbed shell for /settings that hosts the dashboard's own settings plus one configuration panel per AI tool (Claude Code, Codex and Antigravity) plus the AI Router profiles they can be pointed at and the git logins dashboard fetch/pull/push use, with the active tab kept in the `?tool=` query param. The **AI Tools** button at the bar's right edge opens `AiToolsModal` (also opened by `/settings#ai-tools`, the Add-lane modal's link); a tool's tab is hidden while that tool is disabled there.
|
||||
* @description Tabbed shell for /settings that hosts the dashboard's own settings plus one configuration panel per AI tool (Claude Code and Codex have their own; every other installed, enabled catalog tool gets the generic `AiToolConfig` tab, Antigravity's with its hooks card) plus the AI Router profiles they can be pointed at and the git logins dashboard fetch/pull/push use, with the active tab kept in the `?tool=` query param. The **AI Tools** button at the bar's right edge opens `AiToolsModal` (also opened by `/settings#ai-tools`, the Add-lane modal's link); a tool's tab is hidden while that tool is disabled there.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import { useCallback, useEffect, useState, type ComponentType } from "react";
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Boxes,
|
||||
KeyRound,
|
||||
Orbit,
|
||||
Puzzle,
|
||||
Router,
|
||||
Settings as SettingsIcon,
|
||||
SquareTerminal,
|
||||
@@ -19,7 +20,7 @@ import { PageTabs } from "../components/PageTabs";
|
||||
import { Settings } from "./Settings";
|
||||
import { CcConfig } from "./CcConfig";
|
||||
import { CodexConfig } from "./CodexConfig";
|
||||
import { AntigravityConfig } from "./AntigravityConfig";
|
||||
import { AiToolConfig } from "./AiToolConfig";
|
||||
import { AiRouterConfig } from "./AiRouterConfig";
|
||||
import { GitCredentialsConfig } from "./GitCredentialsConfig";
|
||||
import { useAiTools } from "../hooks/useAiTools";
|
||||
@@ -27,9 +28,13 @@ import { AiToolsModal } from "../components/settings/AiToolsModal";
|
||||
|
||||
interface ToolTab {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
labelKey?: string;
|
||||
/** A catalog tool's own label, used instead of `labelKey`. */
|
||||
label?: string;
|
||||
Icon: ComponentType<{ className?: string }>;
|
||||
Panel: ComponentType;
|
||||
Panel?: ComponentType;
|
||||
/** Set on a generic catalog-tool tab: rendered as `<AiToolConfig toolId>`. */
|
||||
toolId?: string;
|
||||
/** The AI Tools catalog id this tab configures; hidden while it is disabled. */
|
||||
aiTool?: string;
|
||||
}
|
||||
@@ -58,13 +63,6 @@ const TOOL_TABS: ToolTab[] = [
|
||||
Panel: CodexConfig,
|
||||
aiTool: "codex",
|
||||
},
|
||||
{
|
||||
id: "antigravity",
|
||||
labelKey: "tools.antigravity",
|
||||
Icon: Orbit,
|
||||
Panel: AntigravityConfig,
|
||||
aiTool: "antigravity",
|
||||
},
|
||||
{ id: "ai-router", labelKey: "tools.aiRouter", Icon: Router, Panel: AiRouterConfig },
|
||||
{
|
||||
id: "git-credentials",
|
||||
@@ -83,9 +81,20 @@ export function SettingsHub() {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const requested = params.get("tool");
|
||||
const aiTools = useAiTools();
|
||||
const tabs = TOOL_TABS.filter(
|
||||
const fixed = TOOL_TABS.filter(
|
||||
(tab) => !tab.aiTool || aiTools.byId(tab.aiTool)?.enabled !== false
|
||||
);
|
||||
// Every other installed, enabled tool, after Codex; a fixed tab's id wins.
|
||||
const catalogTabs: ToolTab[] = aiTools.tools
|
||||
.filter((x) => x.installed && x.enabled && !x.launcher && !fixed.some((f) => f.id === x.id))
|
||||
.map((x) => ({
|
||||
id: x.id,
|
||||
label: x.label,
|
||||
Icon: x.id === "antigravity" ? Orbit : Puzzle,
|
||||
toolId: x.id,
|
||||
}));
|
||||
const at = fixed.findIndex((tab) => tab.id === "ai-router");
|
||||
const tabs = [...fixed.slice(0, at), ...catalogTabs, ...fixed.slice(at)];
|
||||
const active = tabs.find((tab) => tab.id === requested) ?? DASHBOARD_TAB;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@@ -107,10 +116,10 @@ export function SettingsHub() {
|
||||
ariaLabel={t("tools.label", "Tools")}
|
||||
activeId={active.id}
|
||||
onSelect={(id) => setParams(id === DASHBOARD_TAB.id ? {} : { tool: id }, { replace: true })}
|
||||
tabs={tabs.map(({ id, labelKey, Icon }) => ({
|
||||
tabs={tabs.map(({ id, labelKey, label, Icon }) => ({
|
||||
id,
|
||||
Icon,
|
||||
label: t(labelKey),
|
||||
label: label ?? t(labelKey ?? id),
|
||||
}))}
|
||||
actions={
|
||||
<button
|
||||
@@ -125,7 +134,11 @@ export function SettingsHub() {
|
||||
}
|
||||
/>
|
||||
|
||||
<active.Panel />
|
||||
{active.toolId ? (
|
||||
<AiToolConfig key={active.toolId} toolId={active.toolId} />
|
||||
) : (
|
||||
active.Panel && <active.Panel />
|
||||
)}
|
||||
{aiToolsOpen && <AiToolsModal onClose={closeAiTools} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user