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:
2026-08-26 09:01:00 +07:00
parent 64fc713440
commit 341197aac0
7 changed files with 127 additions and 15 deletions
+5 -4
View File
@@ -448,9 +448,9 @@ graph TB
Detail["/sessions/:id"]
Feed["/activity"]
Workflows["/workflows"]
CcConfig["/cc-config"]
CcConfig["/cc-config (redirect)"]
Run["/run"]
Settings["/settings"]
Settings["/settings (tabs: dashboard | claude-code | ...)"]
NF["/* (NotFound)"]
Root --> Dashboard & Sessions & Detail & Feed & Workflows & CcConfig & Run & Settings & NF
@@ -476,9 +476,10 @@ function App() {
<Route path="sessions/:id" element={<SessionDetail />} />
<Route path="activity" element={<ActivityFeed />} />
<Route path="workflows" element={<Workflows />} />
<Route path="cc-config" element={<CcConfig />} />
{/* legacy path - Claude config now lives in a Settings tab */}
<Route path="cc-config" element={<Navigate to="/settings?tool=claude-code" replace />} />
<Route path="run" element={<Run />} />
<Route path="settings" element={<Settings />} />
<Route path="settings" element={<SettingsHub />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
+7 -6
View File
@@ -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>
-2
View File
@@ -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;
+11 -1
View File
@@ -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."
}
}
+11 -1
View File
@@ -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ợ."
}
}
+92
View File
@@ -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>
);
}