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.
This commit is contained in:
2026-08-26 09:17:17 +07:00
parent 341197aac0
commit bcfeeb5140
8 changed files with 136 additions and 48 deletions
-4
View File
@@ -24,12 +24,8 @@ export function DocumentTitle() {
title = `${t("sessions")} · ${sessionId.slice(0, 8)}`;
} else if (path.startsWith("/sessions")) {
title = t("sessions");
} else if (path.startsWith("/activity")) {
title = t("activityFeed");
} else if (path.startsWith("/workflows")) {
title = t("workflows");
} else if (path.startsWith("/cc-config")) {
title = t("ccConfig");
} else if (path.startsWith("/run")) {
title = t("run");
} else if (path.startsWith("/settings")) {
+60
View File
@@ -0,0 +1,60 @@
/**
* @file PageTabs.tsx
* @description Shared top-of-page tab bar used by the pages that merged several
* former routes into one (Settings hub, Sessions/Events history). Rendering only -
* the owning page keeps the active tab in its own query param.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
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 (
<div
role="tablist"
aria-label={ariaLabel}
className="flex items-center gap-1.5 overflow-x-auto no-scrollbar border-b border-border/60 pb-2"
>
{tabs.map(({ id, label, Icon, badge }) => {
const isActive = id === activeId;
return (
<button
key={id}
type="button"
role="tab"
aria-selected={isActive}
onClick={() => onSelect(id)}
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" />
{label}
{badge && <span className="text-[10px] text-fg-muted">{badge}</span>}
</button>
);
})}
</div>
);
}
-1
View File
@@ -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" },
@@ -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", () => {