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:
+5
-4
@@ -448,9 +448,9 @@ graph TB
|
|||||||
Detail["/sessions/:id"]
|
Detail["/sessions/:id"]
|
||||||
Feed["/activity"]
|
Feed["/activity"]
|
||||||
Workflows["/workflows"]
|
Workflows["/workflows"]
|
||||||
CcConfig["/cc-config"]
|
CcConfig["/cc-config (redirect)"]
|
||||||
Run["/run"]
|
Run["/run"]
|
||||||
Settings["/settings"]
|
Settings["/settings (tabs: dashboard | claude-code | ...)"]
|
||||||
NF["/* (NotFound)"]
|
NF["/* (NotFound)"]
|
||||||
|
|
||||||
Root --> Dashboard & Sessions & Detail & Feed & Workflows & CcConfig & Run & Settings & NF
|
Root --> Dashboard & Sessions & Detail & Feed & Workflows & CcConfig & Run & Settings & NF
|
||||||
@@ -476,9 +476,10 @@ function App() {
|
|||||||
<Route path="sessions/:id" element={<SessionDetail />} />
|
<Route path="sessions/:id" element={<SessionDetail />} />
|
||||||
<Route path="activity" element={<ActivityFeed />} />
|
<Route path="activity" element={<ActivityFeed />} />
|
||||||
<Route path="workflows" element={<Workflows />} />
|
<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="run" element={<Run />} />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<SettingsHub />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
+7
-6
@@ -43,8 +43,7 @@
|
|||||||
* - `./pages/SessionDetail`
|
* - `./pages/SessionDetail`
|
||||||
* - `./pages/ActivityFeed`
|
* - `./pages/ActivityFeed`
|
||||||
* - `./pages/Workflows`
|
* - `./pages/Workflows`
|
||||||
* - `./pages/Settings`
|
* - `./pages/SettingsHub`
|
||||||
* - `./pages/CcConfig`
|
|
||||||
* - `./pages/Workspace`
|
* - `./pages/Workspace`
|
||||||
*
|
*
|
||||||
* ## Testing pointers
|
* ## Testing pointers
|
||||||
@@ -68,8 +67,7 @@ import { Sessions } from "./pages/Sessions";
|
|||||||
import { SessionDetail } from "./pages/SessionDetail";
|
import { SessionDetail } from "./pages/SessionDetail";
|
||||||
import { ActivityFeed } from "./pages/ActivityFeed";
|
import { ActivityFeed } from "./pages/ActivityFeed";
|
||||||
import { Workflows } from "./pages/Workflows";
|
import { Workflows } from "./pages/Workflows";
|
||||||
import { Settings } from "./pages/Settings";
|
import { SettingsHub } from "./pages/SettingsHub";
|
||||||
import { CcConfig } from "./pages/CcConfig";
|
|
||||||
import { Workspace } from "./pages/Workspace";
|
import { Workspace } from "./pages/Workspace";
|
||||||
import { NotFound } from "./pages/NotFound";
|
import { NotFound } from "./pages/NotFound";
|
||||||
import { useWebSocket } from "./hooks/useWebSocket";
|
import { useWebSocket } from "./hooks/useWebSocket";
|
||||||
@@ -102,9 +100,12 @@ export default function App() {
|
|||||||
<Route path="activity" element={<ActivityFeed />} />
|
<Route path="activity" element={<ActivityFeed />} />
|
||||||
<Route path="workflows" element={<Workflows />} />
|
<Route path="workflows" element={<Workflows />} />
|
||||||
<Route path="lanes" element={<Navigate to="/run" replace />} />
|
<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="run" element={<Workspace />} />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<SettingsHub />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Workflow,
|
Workflow,
|
||||||
Boxes,
|
|
||||||
Play,
|
Play,
|
||||||
Settings,
|
Settings,
|
||||||
Wifi,
|
Wifi,
|
||||||
@@ -91,7 +90,6 @@ const NAV_KEYS = [
|
|||||||
{ to: "/sessions", icon: FolderOpen, key: "nav:sessions" },
|
{ to: "/sessions", icon: FolderOpen, key: "nav:sessions" },
|
||||||
{ to: "/activity", icon: Activity, key: "nav:activityFeed" },
|
{ to: "/activity", icon: Activity, key: "nav:activityFeed" },
|
||||||
{ to: "/workflows", icon: Workflow, key: "nav:workflows" },
|
{ to: "/workflows", icon: Workflow, key: "nav:workflows" },
|
||||||
{ to: "/cc-config", icon: Boxes, key: "nav:ccConfig" },
|
|
||||||
{ to: "/run", icon: Play, key: "nav:workspace" },
|
{ to: "/run", icon: Play, key: "nav:workspace" },
|
||||||
{ to: "/settings", icon: Settings, key: "nav:settings" },
|
{ to: "/settings", icon: Settings, key: "nav:settings" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -440,5 +440,15 @@
|
|||||||
"removeKeep": "Remove (keep data)",
|
"removeKeep": "Remove (keep data)",
|
||||||
"remoteSourceBadgeTitle": "Collected from a remote machine"
|
"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)",
|
"removeKeep": "Xóa (giữ dữ liệu)",
|
||||||
"remoteSourceBadgeTitle": "Được thu thập từ một máy từ xa"
|
"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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -1441,7 +1441,7 @@ curl "http://localhost:4820/api/sessions?sources=local,4d1f0e2a-7b9c-4c33-8a21-9
|
|||||||
|
|
||||||
### Claude Config Explorer
|
### Claude Config Explorer
|
||||||
|
|
||||||
The `/api/cc-config/*` namespace powers the Claude Config Explorer page. All read endpoints are pure file reads under `CLAUDE_HOME` and the project's `.claude/` dir. File mutations cover the low-risk text-file artifacts (skills, subagents, slash commands, output styles, memory), `keybindings.json`, and `settings.json` layers **via a per-key merge only** — each always creates a timestamped backup before writing. Plugin and marketplace management is delegated to the `claude plugin …` CLI. MCP server config stays read-only, because it is written concurrently by the running Claude Code CLI.
|
The `/api/cc-config/*` namespace powers the Claude Config Explorer, now the **Claude Code** tab of Settings (`/settings?tool=claude-code`). All read endpoints are pure file reads under `CLAUDE_HOME` and the project's `.claude/` dir. File mutations cover the low-risk text-file artifacts (skills, subagents, slash commands, output styles, memory), `keybindings.json`, and `settings.json` layers **via a per-key merge only** — each always creates a timestamped backup before writing. Plugin and marketplace management is delegated to the `claude plugin …` CLI. MCP server config stays read-only, because it is written concurrently by the running Claude Code CLI.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/cc-config/overview
|
GET /api/cc-config/overview
|
||||||
|
|||||||
Reference in New Issue
Block a user