From bcfeeb5140463883cac8f8c644c50ab920f4dda6 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 26 Aug 2026 09:17:17 +0700 Subject: [PATCH] feat(sessions): merge the activity feed into a tab on /sessions Sessions and Activity Feed are two views of the same event pipeline, so they now share one page: /sessions renders a tab bar (Sessions | Activity Feed) with the active view in the ?view= query param, and /activity redirects to /sessions?view=events. The sidebar entry is dropped. The tab bar itself moves out of SettingsHub into components/PageTabs so both hubs share it, and DocumentTitle loses the two branches for paths that now only redirect. --- client/README.md | 9 +-- client/src/App.tsx | 10 ++-- client/src/components/DocumentTitle.tsx | 4 -- client/src/components/PageTabs.tsx | 60 +++++++++++++++++++ client/src/components/Sidebar.tsx | 1 - .../src/components/__tests__/Sidebar.test.tsx | 8 ++- client/src/pages/History.tsx | 49 +++++++++++++++ client/src/pages/SettingsHub.tsx | 43 ++++--------- 8 files changed, 136 insertions(+), 48 deletions(-) create mode 100644 client/src/components/PageTabs.tsx create mode 100644 client/src/pages/History.tsx diff --git a/client/README.md b/client/README.md index f12d0fab..1a1cde37 100644 --- a/client/README.md +++ b/client/README.md @@ -444,9 +444,9 @@ useEffect(() => { graph TB Root["/"] Dashboard["/ (Dashboard)"] - Sessions["/sessions"] + Sessions["/sessions (tabs: sessions | events)"] Detail["/sessions/:id"] - Feed["/activity"] + Feed["/activity (redirect)"] Workflows["/workflows"] CcConfig["/cc-config (redirect)"] Run["/run"] @@ -472,9 +472,10 @@ function App() { }> } /> - } /> + } /> } /> - } /> + {/* legacy path - the activity feed is now a tab on /sessions */} + } /> } /> {/* legacy path - Claude config now lives in a Settings tab */} } /> diff --git a/client/src/App.tsx b/client/src/App.tsx index 999d0a36..3646d629 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -39,9 +39,8 @@ * - `./components/Layout` * - `./components/SplashScreen` * - `./pages/Dashboard` - * - `./pages/Sessions` + * - `./pages/History` * - `./pages/SessionDetail` - * - `./pages/ActivityFeed` * - `./pages/Workflows` * - `./pages/SettingsHub` * - `./pages/Workspace` @@ -63,9 +62,8 @@ import { Layout } from "./components/Layout"; import { DocumentTitle } from "./components/DocumentTitle"; import { SplashScreen } from "./components/SplashScreen"; import { Dashboard } from "./pages/Dashboard"; -import { Sessions } from "./pages/Sessions"; +import { History } from "./pages/History"; import { SessionDetail } from "./pages/SessionDetail"; -import { ActivityFeed } from "./pages/ActivityFeed"; import { Workflows } from "./pages/Workflows"; import { SettingsHub } from "./pages/SettingsHub"; import { Workspace } from "./pages/Workspace"; @@ -95,9 +93,9 @@ export default function App() { }> } /> - } /> + } /> } /> - } /> + } /> } /> } /> + */ +import type { ComponentType } from "react"; + +export interface PageTab { + id: string; + label: string; + Icon: ComponentType<{ className?: string }>; + /** Small muted suffix, e.g. "soon" for a tool that is not integrated yet. */ + badge?: string; +} + +interface PageTabsProps { + tabs: PageTab[]; + activeId: string; + onSelect: (id: string) => void; + ariaLabel: string; +} + +/** + * Horizontal tab strip for a page that hosts several panels. + * @param props - Tabs to render, the active tab id, and the selection callback. + * @returns The tab bar element. + */ +export function PageTabs({ tabs, activeId, onSelect, ariaLabel }: PageTabsProps) { + return ( +
+ {tabs.map(({ id, label, Icon, badge }) => { + const isActive = id === activeId; + return ( + + ); + })} +
+ ); +} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 0b0ba3ba..33aabad3 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -88,7 +88,6 @@ function isUpdatePayload(x: unknown): x is UpdateStatusPayload { const NAV_KEYS = [ { to: "/", icon: LayoutDashboard, key: "nav:dashboard" }, { to: "/sessions", icon: FolderOpen, key: "nav:sessions" }, - { to: "/activity", icon: Activity, key: "nav:activityFeed" }, { to: "/workflows", icon: Workflow, key: "nav:workflows" }, { to: "/run", icon: Play, key: "nav:workspace" }, { to: "/settings", icon: Settings, key: "nav:settings" }, diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index 9e21f067..47af8ecc 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -33,7 +33,10 @@ describe("Sidebar", () => { renderSidebar(true); expect(screen.getByText("Dashboard")).toBeInTheDocument(); expect(screen.getByText("Sessions")).toBeInTheDocument(); - expect(screen.getByText("Activity Feed")).toBeInTheDocument(); + // Activity Feed and Claude Config are no longer top-level entries: they + // moved into tabs on /sessions and /settings respectively. + expect(screen.getByText("Settings")).toBeInTheDocument(); + expect(screen.queryByText("Activity Feed")).toBeNull(); }); it('should show "Live" when WebSocket is connected', () => { @@ -60,7 +63,8 @@ describe("Sidebar", () => { const hrefs = links.map((link) => link.getAttribute("href")); expect(hrefs).toContain("/"); expect(hrefs).toContain("/sessions"); - expect(hrefs).toContain("/activity"); + expect(hrefs).toContain("/settings"); + expect(hrefs).not.toContain("/activity"); }); it("should render both language options in expanded mode", () => { diff --git a/client/src/pages/History.tsx b/client/src/pages/History.tsx new file mode 100644 index 00000000..010a73ea --- /dev/null +++ b/client/src/pages/History.tsx @@ -0,0 +1,49 @@ +/** + * @file History.tsx + * @description Tabbed shell for /sessions that hosts the two views over the same + * event pipeline: the per-session list and the raw activity feed. The active tab + * lives in the `?view=` query param, so `/activity` can redirect into it. + * @author Nguyễn Ngọc Trí Vĩ + */ +import type { ComponentType } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Activity, FolderOpen } from "lucide-react"; +import { PageTabs } from "../components/PageTabs"; +import { Sessions } from "./Sessions"; +import { ActivityFeed } from "./ActivityFeed"; + +interface HistoryView { + id: string; + label: string; + Icon: ComponentType<{ className?: string }>; + Panel: ComponentType; +} + +/** + * Sessions page shell: grouped sessions and the raw event stream as sibling tabs. + * @returns The tab bar plus the active view. + */ +export function History() { + const { t } = useTranslation("nav"); + const [params, setParams] = useSearchParams(); + + const views: HistoryView[] = [ + { id: "sessions", label: t("sessions"), Icon: FolderOpen, Panel: Sessions }, + { id: "events", label: t("activityFeed"), Icon: Activity, Panel: ActivityFeed }, + ]; + const defaultView = views[0]!; + const active = views.find((v) => v.id === params.get("view")) ?? defaultView; + + return ( +
+ setParams(id === defaultView.id ? {} : { view: id }, { replace: true })} + tabs={views.map(({ id, label, Icon }) => ({ id, label, Icon }))} + /> + +
+ ); +} diff --git a/client/src/pages/SettingsHub.tsx b/client/src/pages/SettingsHub.tsx index e39b533b..48565225 100644 --- a/client/src/pages/SettingsHub.tsx +++ b/client/src/pages/SettingsHub.tsx @@ -7,6 +7,7 @@ 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 { PageTabs } from "../components/PageTabs"; import { Settings } from "./Settings"; import { CcConfig } from "./CcConfig"; @@ -45,37 +46,17 @@ export function SettingsHub() { return (
-
- {TOOL_TABS.map(({ id, labelKey, Icon, Panel }) => { - const isActive = id === active.id; - return ( - - ); - })} -
+ setParams(id === DASHBOARD_TAB.id ? {} : { tool: id }, { replace: true })} + tabs={TOOL_TABS.map(({ id, labelKey, Icon, Panel }) => ({ + id, + Icon, + label: t(labelKey), + ...(Panel ? {} : { badge: t("tools.soonBadge", "soon") }), + }))} + /> {active.Panel ? (