feat(settings): host Claude config as a tab in Settings
Settings becomes a tabbed shell (SettingsHub) with one tab per AI tool: Dashboard (existing settings), Claude Code (CcConfig), plus Codex and Cursor placeholders. Active tab lives in the ?tool= query param, and the old /cc-config route redirects to /settings?tool=claude-code. The standalone sidebar entry is dropped. Adding a tool is one entry in TOOL_TABS plus its i18n label.
This commit is contained in:
+7
-6
@@ -43,8 +43,7 @@
|
||||
* - `./pages/SessionDetail`
|
||||
* - `./pages/ActivityFeed`
|
||||
* - `./pages/Workflows`
|
||||
* - `./pages/Settings`
|
||||
* - `./pages/CcConfig`
|
||||
* - `./pages/SettingsHub`
|
||||
* - `./pages/Workspace`
|
||||
*
|
||||
* ## Testing pointers
|
||||
@@ -68,8 +67,7 @@ import { Sessions } from "./pages/Sessions";
|
||||
import { SessionDetail } from "./pages/SessionDetail";
|
||||
import { ActivityFeed } from "./pages/ActivityFeed";
|
||||
import { Workflows } from "./pages/Workflows";
|
||||
import { Settings } from "./pages/Settings";
|
||||
import { CcConfig } from "./pages/CcConfig";
|
||||
import { SettingsHub } from "./pages/SettingsHub";
|
||||
import { Workspace } from "./pages/Workspace";
|
||||
import { NotFound } from "./pages/NotFound";
|
||||
import { useWebSocket } from "./hooks/useWebSocket";
|
||||
@@ -102,9 +100,12 @@ export default function App() {
|
||||
<Route path="activity" element={<ActivityFeed />} />
|
||||
<Route path="workflows" element={<Workflows />} />
|
||||
<Route path="lanes" element={<Navigate to="/run" replace />} />
|
||||
<Route path="cc-config" element={<CcConfig />} />
|
||||
<Route
|
||||
path="cc-config"
|
||||
element={<Navigate to="/settings?tool=claude-code" replace />}
|
||||
/>
|
||||
<Route path="run" element={<Workspace />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="settings" element={<SettingsHub />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -58,7 +58,6 @@ import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
Workflow,
|
||||
Boxes,
|
||||
Play,
|
||||
Settings,
|
||||
Wifi,
|
||||
@@ -91,7 +90,6 @@ const NAV_KEYS = [
|
||||
{ to: "/sessions", icon: FolderOpen, key: "nav:sessions" },
|
||||
{ to: "/activity", icon: Activity, key: "nav:activityFeed" },
|
||||
{ to: "/workflows", icon: Workflow, key: "nav:workflows" },
|
||||
{ to: "/cc-config", icon: Boxes, key: "nav:ccConfig" },
|
||||
{ to: "/run", icon: Play, key: "nav:workspace" },
|
||||
{ to: "/settings", icon: Settings, key: "nav:settings" },
|
||||
] as const;
|
||||
|
||||
@@ -440,5 +440,15 @@
|
||||
"removeKeep": "Remove (keep data)",
|
||||
"remoteSourceBadgeTitle": "Collected from a remote machine"
|
||||
},
|
||||
"cursorPathsNote": "Informational: Cursor sessions count here too — Cursor happens to use the same ~/.claude paths as Claude Code (locally and on synced remotes)."
|
||||
"cursorPathsNote": "Informational: Cursor sessions count here too — Cursor happens to use the same ~/.claude paths as Claude Code (locally and on synced remotes).",
|
||||
"tools": {
|
||||
"label": "Tools",
|
||||
"dashboard": "Dashboard",
|
||||
"claudeCode": "Claude Code",
|
||||
"codex": "Codex",
|
||||
"cursor": "Cursor",
|
||||
"soonBadge": "soon",
|
||||
"comingSoon": "{{tool}} is not integrated yet",
|
||||
"comingSoonDesc": "Its configuration will show up here once support lands."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,5 +440,15 @@
|
||||
"removeKeep": "Xóa (giữ dữ liệu)",
|
||||
"remoteSourceBadgeTitle": "Được thu thập từ một máy từ xa"
|
||||
},
|
||||
"cursorPathsNote": "Chỉ mang tính thông tin: phiên Cursor cũng được tính — Cursor tình cờ dùng cùng đường dẫn ~/.claude với Claude Code (cục bộ và trên máy từ xa đã đồng bộ)."
|
||||
"cursorPathsNote": "Chỉ mang tính thông tin: phiên Cursor cũng được tính — Cursor tình cờ dùng cùng đường dẫn ~/.claude với Claude Code (cục bộ và trên máy từ xa đã đồng bộ).",
|
||||
"tools": {
|
||||
"label": "Công cụ",
|
||||
"dashboard": "Dashboard",
|
||||
"claudeCode": "Claude Code",
|
||||
"codex": "Codex",
|
||||
"cursor": "Cursor",
|
||||
"soonBadge": "sắp có",
|
||||
"comingSoon": "{{tool}} chưa được tích hợp",
|
||||
"comingSoonDesc": "Cấu hình của công cụ này sẽ hiện ở đây khi có hỗ trợ."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @file SettingsHub.tsx
|
||||
* @description Tabbed shell for /settings that hosts the dashboard's own settings plus per-AI-tool configuration panels (Claude Code today, Codex/Cursor reserved), with the active tab kept in the `?tool=` query param.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import type { ComponentType } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Boxes, Settings as SettingsIcon, SquareTerminal, MousePointerClick } from "lucide-react";
|
||||
import { Settings } from "./Settings";
|
||||
import { CcConfig } from "./CcConfig";
|
||||
|
||||
interface ToolTab {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
Icon: ComponentType<{ className?: string }>;
|
||||
/** Omit for tools that are not integrated yet - the tab renders a placeholder. */
|
||||
Panel?: ComponentType;
|
||||
}
|
||||
|
||||
const DASHBOARD_TAB: ToolTab = {
|
||||
id: "dashboard",
|
||||
labelKey: "tools.dashboard",
|
||||
Icon: SettingsIcon,
|
||||
Panel: Settings,
|
||||
};
|
||||
|
||||
/** Add a tool here to give it a tab; that is the whole extension point. */
|
||||
const TOOL_TABS: ToolTab[] = [
|
||||
DASHBOARD_TAB,
|
||||
{ id: "claude-code", labelKey: "tools.claudeCode", Icon: Boxes, Panel: CcConfig },
|
||||
{ id: "codex", labelKey: "tools.codex", Icon: SquareTerminal },
|
||||
{ id: "cursor", labelKey: "tools.cursor", Icon: MousePointerClick },
|
||||
];
|
||||
|
||||
/**
|
||||
* Settings page shell with one tab per configuration surface.
|
||||
* @returns The tab bar plus the active tab's panel.
|
||||
*/
|
||||
export function SettingsHub() {
|
||||
const { t } = useTranslation("settings");
|
||||
const [params, setParams] = useSearchParams();
|
||||
const requested = params.get("tool");
|
||||
const active = TOOL_TABS.find((tab) => tab.id === requested) ?? DASHBOARD_TAB;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={t("tools.label", "Tools")}
|
||||
className="flex items-center gap-1.5 overflow-x-auto no-scrollbar border-b border-border/60 pb-2"
|
||||
>
|
||||
{TOOL_TABS.map(({ id, labelKey, Icon, Panel }) => {
|
||||
const isActive = id === active.id;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
onClick={() =>
|
||||
setParams(id === DASHBOARD_TAB.id ? {} : { tool: id }, { replace: true })
|
||||
}
|
||||
className={`inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-3 py-1.5 rounded-lg border transition-colors flex-shrink-0 ${
|
||||
isActive
|
||||
? "bg-accent/15 border-accent/30 text-accent"
|
||||
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
{t(labelKey)}
|
||||
{!Panel && (
|
||||
<span className="text-[10px] text-fg-muted">{t("tools.soonBadge", "soon")}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{active.Panel ? (
|
||||
<active.Panel />
|
||||
) : (
|
||||
<div className="card p-8 text-center space-y-1">
|
||||
<p className="text-sm font-medium text-fg-primary">
|
||||
{t("tools.comingSoon", { tool: t(active.labelKey) })}
|
||||
</p>
|
||||
<p className="text-xs text-fg-muted">{t("tools.comingSoonDesc")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user