feat(ui): fold the Workflows page into Dashboard → Analytics → Workflow
The workflow analytics now render under the Analytics tab's Workflow sub-tab; ?tab= and ?sub= deep-link the Dashboard, /workflows redirects there, and the sidebar drops its Workflows item.
This commit is contained in:
+9
-9
@@ -75,11 +75,10 @@ graph TB
|
|||||||
Layout[Layout Component]
|
Layout[Layout Component]
|
||||||
|
|
||||||
subgraph "Pages"
|
subgraph "Pages"
|
||||||
Home[Dashboard<br/>Monitor / Analytics / Health tabs]
|
Home[Dashboard<br/>Monitor / Analytics (incl. Workflow) / Health tabs]
|
||||||
Sessions[Sessions]
|
Sessions[Sessions]
|
||||||
Detail[SessionDetail]
|
Detail[SessionDetail]
|
||||||
Feed[ActivityFeed]
|
Feed[ActivityFeed]
|
||||||
Workflows[Workflows]
|
|
||||||
Settings[Settings]
|
Settings[Settings]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -106,7 +105,7 @@ graph TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
Router --> Layout
|
Router --> Layout
|
||||||
Layout --> Home & Sessions & Detail & Feed & Workflows & Settings
|
Layout --> Home & Sessions & Detail & Feed & Settings
|
||||||
Home & Detail --> AgentCard & StatCard & StatusBadge
|
Home & Detail --> AgentCard & StatCard & StatusBadge
|
||||||
Feed --> EventDetail
|
Feed --> EventDetail
|
||||||
API --> REST
|
API --> REST
|
||||||
@@ -198,14 +197,14 @@ client/
|
|||||||
│ │ │ ├── HomeDirSetting.tsx # CLAUDE_HOME / CODEX_HOME editor (env-owned warning)
|
│ │ │ ├── HomeDirSetting.tsx # CLAUDE_HOME / CODEX_HOME editor (env-owned warning)
|
||||||
│ │ │ ├── CodexHooksSettings.tsx # Codex → Overview → Codex hooks card
|
│ │ │ ├── CodexHooksSettings.tsx # Codex → Overview → Codex hooks card
|
||||||
│ │ │ └── AntigravityHooksSettings.tsx # Antigravity → Antigravity hooks card
|
│ │ │ └── AntigravityHooksSettings.tsx # Antigravity → Antigravity hooks card
|
||||||
│ │ └── workflows/ # Workflows page panels: own-pipeline stats + D3 analytics (11 files)
|
│ │ └── workflows/ # Dashboard → Analytics → Workflow panels: own-pipeline stats + D3 analytics (11 files)
|
||||||
│ │
|
│ │
|
||||||
│ ├── pages/ # Route pages
|
│ ├── pages/ # Route pages
|
||||||
│ │ ├── Dashboard.tsx # Monitor / Analytics / Health tabs
|
│ │ ├── Dashboard.tsx # Monitor / Analytics / Health tabs
|
||||||
│ │ ├── Sessions.tsx # Filterable table; shows each session's real name (synced live from the transcript), falls back to the short ID
|
│ │ ├── Sessions.tsx # Filterable table; shows each session's real name (synced live from the transcript), falls back to the short ID
|
||||||
│ │ ├── SessionDetail.tsx # Agent tree + event timeline + Conversation tab (slash-command pills & output, inline rename markers)
|
│ │ ├── SessionDetail.tsx # Agent tree + event timeline + Conversation tab (slash-command pills & output, inline rename markers)
|
||||||
│ │ ├── ActivityFeed.tsx # Real-time event log; row click expands payload; Session btn navigates
|
│ │ ├── ActivityFeed.tsx # Real-time event log; row click expands payload; Session btn navigates
|
||||||
│ │ ├── Workflows.tsx
|
│ │ ├── Workflows.tsx # Workflow analytics, rendered in Dashboard → Analytics → Workflow
|
||||||
│ │ ├── Settings.tsx
|
│ │ ├── Settings.tsx
|
||||||
│ │ ├── AiToolConfig.tsx # Settings → <tool>: the generic tab of every installed, enabled catalog tool (path, Update link, sessions/cost/lanes, default model, vendor pricing, catalog-declared config files; Antigravity adds its hooks card)
|
│ │ ├── AiToolConfig.tsx # Settings → <tool>: the generic tab of every installed, enabled catalog tool (path, Update link, sessions/cost/lanes, default model, vendor pricing, catalog-declared config files; Antigravity adds its hooks card)
|
||||||
│ │ └── NotFound.tsx
|
│ │ └── NotFound.tsx
|
||||||
@@ -252,11 +251,12 @@ graph TB
|
|||||||
Sessions[Sessions<br/>filterable table]
|
Sessions[Sessions<br/>filterable table]
|
||||||
Detail[SessionDetail<br/>agent hierarchy + timeline]
|
Detail[SessionDetail<br/>agent hierarchy + timeline]
|
||||||
Feed[ActivityFeed<br/>streaming event log]
|
Feed[ActivityFeed<br/>streaming event log]
|
||||||
Workflows[Workflows<br/>Lane pipelines + D3 analytics]
|
Workflows[Workflows<br/>Analytics → Workflow sub-tab:<br/>lane pipelines + D3 analytics]
|
||||||
Settings[SettingsHub /settings/:section<br/>General · Appearance · Notifications<br/>Agents: AI tools, Claude Code, Codex, each catalog tool<br/>Routing: AI Router, Pricing · Git: credentials, worktrees<br/>Projects: one page per repo]
|
Settings[SettingsHub /settings/:section<br/>General · Appearance · Notifications<br/>Agents: AI tools, Claude Code, Codex, each catalog tool<br/>Routing: AI Router, Pricing · Git: credentials, worktrees<br/>Projects: one page per repo]
|
||||||
|
|
||||||
App --> Layout
|
App --> Layout
|
||||||
Layout --> Dashboard & Sessions & Detail & Feed & Workflows & Settings
|
Layout --> Dashboard & Sessions & Detail & Feed & Settings
|
||||||
|
Dashboard --> Workflows
|
||||||
|
|
||||||
Dashboard --> StatCard[StatCard × 6]
|
Dashboard --> StatCard[StatCard × 6]
|
||||||
Dashboard --> AgentCard[AgentCard × N]
|
Dashboard --> AgentCard[AgentCard × N]
|
||||||
@@ -469,7 +469,7 @@ graph TB
|
|||||||
Sessions["/sessions (tabs: sessions | events)"]
|
Sessions["/sessions (tabs: sessions | events)"]
|
||||||
Detail["/sessions/:id"]
|
Detail["/sessions/:id"]
|
||||||
Feed["/activity (redirect)"]
|
Feed["/activity (redirect)"]
|
||||||
Workflows["/workflows"]
|
Workflows["/workflows (redirect → /?tab=analytics&sub=workflow)"]
|
||||||
CcConfig["/cc-config (redirect)"]
|
CcConfig["/cc-config (redirect)"]
|
||||||
Lanes["/lanes (redirect)"]
|
Lanes["/lanes (redirect)"]
|
||||||
Run["/run (redirect)"]
|
Run["/run (redirect)"]
|
||||||
@@ -500,7 +500,7 @@ function App() {
|
|||||||
<Route path="sessions/:id" element={<SessionDetail />} />
|
<Route path="sessions/:id" element={<SessionDetail />} />
|
||||||
{/* legacy path - the activity feed is now a tab on /sessions */}
|
{/* legacy path - the activity feed is now a tab on /sessions */}
|
||||||
<Route path="activity" element={<Navigate to="/sessions?view=events" replace />} />
|
<Route path="activity" element={<Navigate to="/sessions?view=events" replace />} />
|
||||||
<Route path="workflows" element={<Workflows />} />
|
<Route path="workflows" element={<Navigate to="/?tab=analytics&sub=workflow" replace />} />
|
||||||
{/* legacy path - Claude config now lives in a Settings tab */}
|
{/* legacy path - Claude config now lives in a Settings tab */}
|
||||||
<Route path="cc-config" element={<Navigate to="/settings/agents/claude-code" replace />} />
|
<Route path="cc-config" element={<Navigate to="/settings/agents/claude-code" replace />} />
|
||||||
{/* legacy paths - lanes and runs are one page now */}
|
{/* legacy paths - lanes and runs are one page now */}
|
||||||
|
|||||||
+4
-3
@@ -41,7 +41,6 @@
|
|||||||
* - `./pages/Dashboard`
|
* - `./pages/Dashboard`
|
||||||
* - `./pages/History`
|
* - `./pages/History`
|
||||||
* - `./pages/SessionDetail`
|
* - `./pages/SessionDetail`
|
||||||
* - `./pages/Workflows`
|
|
||||||
* - `./pages/SettingsHub`
|
* - `./pages/SettingsHub`
|
||||||
* - `./pages/Workspace`
|
* - `./pages/Workspace`
|
||||||
*
|
*
|
||||||
@@ -64,7 +63,6 @@ import { GlobalTooltip } from "./components/Tooltip";
|
|||||||
import { Dashboard } from "./pages/Dashboard";
|
import { Dashboard } from "./pages/Dashboard";
|
||||||
import { History } from "./pages/History";
|
import { History } from "./pages/History";
|
||||||
import { SessionDetail } from "./pages/SessionDetail";
|
import { SessionDetail } from "./pages/SessionDetail";
|
||||||
import { Workflows } from "./pages/Workflows";
|
|
||||||
import { SettingsHub } from "./pages/SettingsHub";
|
import { SettingsHub } from "./pages/SettingsHub";
|
||||||
import { Workspace } from "./pages/Workspace";
|
import { Workspace } from "./pages/Workspace";
|
||||||
import { NotFound } from "./pages/NotFound";
|
import { NotFound } from "./pages/NotFound";
|
||||||
@@ -99,7 +97,10 @@ export default function App() {
|
|||||||
<Route path="sessions" element={<History />} />
|
<Route path="sessions" element={<History />} />
|
||||||
<Route path="sessions/:id" element={<SessionDetail />} />
|
<Route path="sessions/:id" element={<SessionDetail />} />
|
||||||
<Route path="activity" element={<Navigate to="/sessions?view=events" replace />} />
|
<Route path="activity" element={<Navigate to="/sessions?view=events" replace />} />
|
||||||
<Route path="workflows" element={<Workflows />} />
|
<Route
|
||||||
|
path="workflows"
|
||||||
|
element={<Navigate to="/?tab=analytics&sub=workflow" replace />}
|
||||||
|
/>
|
||||||
<Route path="lanes" element={<Navigate to="/workspace" replace />} />
|
<Route path="lanes" element={<Navigate to="/workspace" replace />} />
|
||||||
<Route
|
<Route
|
||||||
path="cc-config"
|
path="cc-config"
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ export function DocumentTitle() {
|
|||||||
title = `${t("sessions")} · ${sessionId.slice(0, 8)}`;
|
title = `${t("sessions")} · ${sessionId.slice(0, 8)}`;
|
||||||
} else if (path.startsWith("/sessions")) {
|
} else if (path.startsWith("/sessions")) {
|
||||||
title = t("sessions");
|
title = t("sessions");
|
||||||
} else if (path.startsWith("/workflows")) {
|
|
||||||
title = t("workflows");
|
|
||||||
} else if (path.startsWith("/workspace")) {
|
} else if (path.startsWith("/workspace")) {
|
||||||
title = t("workspace");
|
title = t("workspace");
|
||||||
} else if (path.startsWith("/settings")) {
|
} else if (path.startsWith("/settings")) {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import {
|
|||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Workflow,
|
|
||||||
Play,
|
Play,
|
||||||
Settings,
|
Settings,
|
||||||
Wifi,
|
Wifi,
|
||||||
@@ -89,7 +88,6 @@ function isUpdatePayload(x: unknown): x is UpdateStatusPayload {
|
|||||||
const NAV_KEYS = [
|
const NAV_KEYS = [
|
||||||
{ to: "/", icon: LayoutDashboard, key: "nav:dashboard" },
|
{ to: "/", icon: LayoutDashboard, key: "nav:dashboard" },
|
||||||
{ to: "/sessions", icon: FolderOpen, key: "nav:sessions" },
|
{ to: "/sessions", icon: FolderOpen, key: "nav:sessions" },
|
||||||
{ to: "/workflows", icon: Workflow, key: "nav:workflows" },
|
|
||||||
{ to: "/workspace", icon: Play, key: "nav:workspace" },
|
{ to: "/workspace", icon: Play, key: "nav:workspace" },
|
||||||
{ to: "/settings", icon: Settings, key: "nav:settings" },
|
{ to: "/settings", icon: Settings, key: "nav:settings" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ describe("DocumentTitle", () => {
|
|||||||
it("titles the other top-level routes", () => {
|
it("titles the other top-level routes", () => {
|
||||||
expect(titleFor("/")).toMatch(/^Dashboard · /);
|
expect(titleFor("/")).toMatch(/^Dashboard · /);
|
||||||
expect(titleFor("/sessions")).toMatch(/^Sessions · /);
|
expect(titleFor("/sessions")).toMatch(/^Sessions · /);
|
||||||
expect(titleFor("/workflows")).toMatch(/^Workflows · /);
|
|
||||||
expect(titleFor("/settings")).toMatch(/^Settings · /);
|
expect(titleFor("/settings")).toMatch(/^Settings · /);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,7 +64,6 @@
|
|||||||
"upToDate": "Up to date",
|
"upToDate": "Up to date",
|
||||||
"updateAvailable": "Update available",
|
"updateAvailable": "Update available",
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
"workflows": "Workflows",
|
|
||||||
"workspace": "Workspace",
|
"workspace": "Workspace",
|
||||||
"wsEndpoint": "WebSocket",
|
"wsEndpoint": "WebSocket",
|
||||||
"releaseAvailable": "{{version}} available",
|
"releaseAvailable": "{{version}} available",
|
||||||
|
|||||||
@@ -64,7 +64,6 @@
|
|||||||
"upToDate": "Đã là mới nhất",
|
"upToDate": "Đã là mới nhất",
|
||||||
"updateAvailable": "Có bản cập nhật",
|
"updateAvailable": "Có bản cập nhật",
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
"workflows": "Quy trình",
|
|
||||||
"workspace": "Không gian làm việc",
|
"workspace": "Không gian làm việc",
|
||||||
"wsEndpoint": "WebSocket",
|
"wsEndpoint": "WebSocket",
|
||||||
"releaseAvailable": "Có {{version}}",
|
"releaseAvailable": "Có {{version}}",
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
import { Workflows } from "./Workflows";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@@ -732,9 +733,14 @@ function AnalyticsTab() {
|
|||||||
const [costData, setCostData] = useState<CostResult | null>(null);
|
const [costData, setCostData] = useState<CostResult | null>(null);
|
||||||
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
// `?sub=` deep-links a sub-tab (the old /workflows route lands on `workflow`).
|
||||||
|
const [params] = useSearchParams();
|
||||||
const [analyticsSubTab, setAnalyticsSubTab] = useState<
|
const [analyticsSubTab, setAnalyticsSubTab] = useState<
|
||||||
"tokens" | "cost" | "workflow" | "productivity"
|
"tokens" | "cost" | "workflow" | "productivity"
|
||||||
>("cost");
|
>(() => {
|
||||||
|
const sub = params.get("sub");
|
||||||
|
return sub === "tokens" || sub === "workflow" || sub === "productivity" ? sub : "cost";
|
||||||
|
});
|
||||||
// Global data scope; a change re-runs `load` (api injects the `sources` param).
|
// Global data scope; a change re-runs `load` (api injects the `sources` param).
|
||||||
const [scope] = useDataScope();
|
const [scope] = useDataScope();
|
||||||
|
|
||||||
@@ -1340,81 +1346,89 @@ function AnalyticsTab() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{analyticsSubTab === "workflow" && (
|
{analyticsSubTab === "workflow" && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="space-y-8">
|
||||||
{/* Agent type distribution */}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div className="card p-5">
|
{/* Agent type distribution */}
|
||||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
<div className="card p-5">
|
||||||
{t("subagentTypes")}
|
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||||
</h3>
|
{t("subagentTypes")}
|
||||||
{(data?.agent_types ?? []).length === 0 ? (
|
</h3>
|
||||||
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
|
{(data?.agent_types ?? []).length === 0 ? (
|
||||||
) : (
|
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
|
||||||
<div className="space-y-3">
|
) : (
|
||||||
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
|
<div className="space-y-3">
|
||||||
<BarRow
|
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
|
||||||
key={subagent_type}
|
<BarRow
|
||||||
label={subagent_type}
|
key={subagent_type}
|
||||||
count={count}
|
label={subagent_type}
|
||||||
max={maxAgentTypeCount}
|
count={count}
|
||||||
color="bg-violet-400"
|
max={maxAgentTypeCount}
|
||||||
/>
|
color="bg-violet-400"
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Agent status donut */}
|
|
||||||
<div className="card p-5">
|
|
||||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("agentStatus")}</h3>
|
|
||||||
<DonutChart segments={agentStatusSegments} />
|
|
||||||
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
|
||||||
<div className="flex justify-between text-xs text-fg-muted">
|
|
||||||
<span>{t("totalAgentsLabel")}</span>
|
|
||||||
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
|
|
||||||
<span className="text-fg-secondary font-mono">
|
|
||||||
{fmt(data?.overview.total_agents ?? 0)}
|
|
||||||
</span>
|
|
||||||
</Tip>
|
|
||||||
</div>
|
|
||||||
{agentStatusSegments.map((s) => (
|
|
||||||
<div
|
|
||||||
key={s.label}
|
|
||||||
className="flex items-center justify-between text-xs text-fg-muted"
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
|
||||||
style={{ backgroundColor: s.color }}
|
|
||||||
/>
|
/>
|
||||||
{s.label}
|
))}
|
||||||
</span>
|
</div>
|
||||||
<Tip raw={s.value.toLocaleString()}>
|
)}
|
||||||
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
</div>
|
||||||
|
|
||||||
|
{/* Agent status donut */}
|
||||||
|
<div className="card p-5">
|
||||||
|
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||||
|
{t("agentStatus")}
|
||||||
|
</h3>
|
||||||
|
<DonutChart segments={agentStatusSegments} />
|
||||||
|
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
||||||
|
<div className="flex justify-between text-xs text-fg-muted">
|
||||||
|
<span>{t("totalAgentsLabel")}</span>
|
||||||
|
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
|
||||||
|
<span className="text-fg-secondary font-mono">
|
||||||
|
{fmt(data?.overview.total_agents ?? 0)}
|
||||||
|
</span>
|
||||||
</Tip>
|
</Tip>
|
||||||
</div>
|
</div>
|
||||||
))}
|
{agentStatusSegments.map((s) => (
|
||||||
</div>
|
<div
|
||||||
</div>
|
key={s.label}
|
||||||
|
className="flex items-center justify-between text-xs text-fg-muted"
|
||||||
{/* Event type breakdown */}
|
>
|
||||||
<div className="card p-5">
|
<span className="flex items-center gap-1.5">
|
||||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("eventTypes")}</h3>
|
<span
|
||||||
{(data?.event_types ?? []).length === 0 ? (
|
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||||
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
|
style={{ backgroundColor: s.color }}
|
||||||
) : (
|
/>
|
||||||
<div className="space-y-3">
|
{s.label}
|
||||||
{(data?.event_types ?? []).map(({ event_type, count }) => (
|
</span>
|
||||||
<BarRow
|
<Tip raw={s.value.toLocaleString()}>
|
||||||
key={event_type}
|
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
||||||
label={event_type}
|
</Tip>
|
||||||
count={count}
|
</div>
|
||||||
max={maxEventTypeCount}
|
|
||||||
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
{/* Event type breakdown */}
|
||||||
|
<div className="card p-5">
|
||||||
|
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||||
|
{t("eventTypes")}
|
||||||
|
</h3>
|
||||||
|
{(data?.event_types ?? []).length === 0 ? (
|
||||||
|
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{(data?.event_types ?? []).map(({ event_type, count }) => (
|
||||||
|
<BarRow
|
||||||
|
key={event_type}
|
||||||
|
label={event_type}
|
||||||
|
count={count}
|
||||||
|
max={maxEventTypeCount}
|
||||||
|
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* The former Workflows page, folded into this sub-tab. */}
|
||||||
|
<Workflows />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2346,7 +2360,10 @@ export function Dashboard() {
|
|||||||
const { t } = useTranslation("dashboard");
|
const { t } = useTranslation("dashboard");
|
||||||
|
|
||||||
// Persistent Tab State
|
// Persistent Tab State
|
||||||
|
const [tabParams] = useSearchParams();
|
||||||
const [activeTab, setActiveTab] = useState<"monitor" | "analytics" | "health">(() => {
|
const [activeTab, setActiveTab] = useState<"monitor" | "analytics" | "health">(() => {
|
||||||
|
const tab = tabParams.get("tab");
|
||||||
|
if (tab === "monitor" || tab === "analytics" || tab === "health") return tab;
|
||||||
return (
|
return (
|
||||||
(localStorage.getItem("dashboard_tab") as "monitor" | "analytics" | "health") || "analytics"
|
(localStorage.getItem("dashboard_tab") as "monitor" | "analytics" | "health") || "analytics"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file Workflows.tsx
|
* @file Workflows.tsx
|
||||||
* @description Workflows page. Opens on this dashboard's OWN pipelines — per-node
|
* @description The workflow analytics, shown inside Dashboard → Analytics → Workflow (the
|
||||||
|
* old `/workflows` route redirects there). Opens on this dashboard's OWN pipelines — per-node
|
||||||
* pass rates across every lane, and which skills/plugins actually get used — then
|
* pass rates across every lane, and which skills/plugins actually get used — then
|
||||||
* the Claude Code orchestration analytics: headline stats, Workflow-tool runs, the
|
* the Claude Code orchestration analytics: headline stats, Workflow-tool runs, the
|
||||||
* spawning DAG, error propagation, session complexity, compaction impact and the
|
* spawning DAG, error propagation, session complexity, compaction impact and the
|
||||||
@@ -494,7 +495,7 @@ function PageHeader({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
<h2 className="text-lg font-semibold text-fg-primary">{t("title")}</h2>
|
||||||
{wsConnected ? (
|
{wsConnected ? (
|
||||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||||
|
|||||||
@@ -9390,11 +9390,11 @@ exports[`screen snapshots > Workflows 1`] = `
|
|||||||
<div
|
<div
|
||||||
class="flex items-center gap-2"
|
class="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<h1
|
<h2
|
||||||
class="text-lg font-semibold text-fg-primary"
|
class="text-lg font-semibold text-fg-primary"
|
||||||
>
|
>
|
||||||
Workflows
|
Workflows
|
||||||
</h1>
|
</h2>
|
||||||
<span
|
<span
|
||||||
class="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full"
|
class="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -559,6 +559,17 @@ describe("screen snapshots", () => {
|
|||||||
it("Dashboard", async () => {
|
it("Dashboard", async () => {
|
||||||
await snapshot(<Dashboard />, "/");
|
await snapshot(<Dashboard />, "/");
|
||||||
});
|
});
|
||||||
|
it("Dashboard deep link opens the Workflow sub-tab with the workflow analytics", async () => {
|
||||||
|
localStorage.removeItem("dashboard_tab");
|
||||||
|
const { container } = render(
|
||||||
|
<MemoryRouter initialEntries={["/?tab=analytics&sub=workflow"]}>
|
||||||
|
<Dashboard />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
await settle();
|
||||||
|
const headings = [...container.querySelectorAll("h2")].map((h) => h.textContent);
|
||||||
|
expect(headings).toContain(i18n.t("workflows:title"));
|
||||||
|
});
|
||||||
it("Sessions", async () => {
|
it("Sessions", async () => {
|
||||||
await snapshot(<Sessions />, "/sessions");
|
await snapshot(<Sessions />, "/sessions");
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -2120,7 +2120,7 @@ DELETE /api/run/:id Kill (SIGTERM → SIGKILL after 5 s)
|
|||||||
|
|
||||||
**PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `agenthub lanes shell` can attach the same session via a real local terminal, staying in sync with the browser view.
|
**PTY streaming:** Frames from the tmux pane are streamed to the client over `/ws-pty/:runId` as binary WebSocket frames (not JSON). The Workspace page's TerminalView component feeds these frames to xterm.js for live rendering. Simultaneously, `agenthub lanes shell` can attach the same session via a real local terminal, staying in sync with the browser view.
|
||||||
|
|
||||||
Every history row carries `lane_id` (the workspace whose run it belongs to) and `tool` (the CLI that was invoked: `"claude"` or `"codex"`). `GET /api/run/history?laneId=<n>` returns only that workspace's runs. Spawned Claude processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, the Dashboard's Analytics tab, and the Workflows page automatically. Codex runs do not emit these hooks; they are recorded only in run history and `dashboard_runs`.
|
Every history row carries `lane_id` (the workspace whose run it belongs to) and `tool` (the CLI that was invoked: `"claude"` or `"codex"`). `GET /api/run/history?laneId=<n>` returns only that workspace's runs. Spawned Claude processes fire the dashboard's hooks like any other CLI session, so they show up in `/api/sessions`, and the Dashboard's Analytics tab (Workflow sub-tab included) automatically. Codex runs do not emit these hooks; they are recorded only in run history and `dashboard_runs`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -815,7 +815,7 @@ under `agenthub feature activate`: the backwards declaration prunes the records
|
|||||||
ahead of it, so the previous pass leaves nothing behind to count, while a
|
ahead of it, so the previous pass leaves nothing behind to count, while a
|
||||||
feature switch archives them first. Neither ever counted *passes* — one workspace row
|
feature switch archives them first. Neither ever counted *passes* — one workspace row
|
||||||
contributes at most one record per node, however many times it ran — so this
|
contributes at most one record per node, however many times it ran — so this
|
||||||
narrows the history, it does not skew a rate. The Workflows page renders this as one bar per node (see
|
narrows the history, it does not skew a rate. Dashboard → Analytics → Workflow renders this as one bar per node (see
|
||||||
`client/src/components/workflows/LanePipelinePanel.tsx`).
|
`client/src/components/workflows/LanePipelinePanel.tsx`).
|
||||||
|
|
||||||
**A new run is opened by a DECLARATION, never by a detection alone.** `runs` is
|
**A new run is opened by a DECLARATION, never by a detection alone.** `runs` is
|
||||||
@@ -1042,7 +1042,7 @@ Create a JSON file in your custom pipeline directory (e.g., `~/.agenthub/pipelin
|
|||||||
- **label** (string, optional): display name in the UI; defaults to `id` if omitted
|
- **label** (string, optional): display name in the UI; defaults to `id` if omitted
|
||||||
- **icon** (string, optional): unicode emoji or single character shown before the stage name
|
- **icon** (string, optional): unicode emoji or single character shown before the stage name
|
||||||
- **gate** (boolean, optional): `true` if this stage blocks unless explicitly passed; defaults to `false`
|
- **gate** (boolean, optional): `true` if this stage blocks unless explicitly passed; defaults to `false`
|
||||||
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/my-training-skill"`) or a shell one (`"agenthub lanes hook ci-gate"`). Where `skill` above names the driver for the whole pipeline, this narrows it to the step, so a reader of the pipeline map sees what moves each node instead of inferring it from the node's `detect` regexes. Omit it (or leave it empty) for a node with no single driver; use `"agenthub stage <id>"` for a node whose only "work" is the declaration itself (a terminal `done`, a gate another agent declares). Surfaced in the node's tooltip on a workspace card and as copyable text per node on the Workflows page, and run by `agenthub lanes node <id>` (below); **never executed by the dashboard**, same contract as `skill`.
|
- **command** (string, optional): the one command that drives THIS stage — a slash command (`"/my-training-skill"`) or a shell one (`"agenthub lanes hook ci-gate"`). Where `skill` above names the driver for the whole pipeline, this narrows it to the step, so a reader of the pipeline map sees what moves each node instead of inferring it from the node's `detect` regexes. Omit it (or leave it empty) for a node with no single driver; use `"agenthub stage <id>"` for a node whose only "work" is the declaration itself (a terminal `done`, a gate another agent declares). Surfaced in the node's tooltip on a workspace card and as copyable text per node in Dashboard → Analytics → Workflow, and run by `agenthub lanes node <id>` (below); **never executed by the dashboard**, same contract as `skill`.
|
||||||
- **aliases** (array, optional): alternative names for the stage (e.g., `["training", "fit"]` lets you call `agenthub stage training`). An alias may also name a **sub-state** — *why* the workspace sits on a node — without adding a node to the map. Two constraints: no alias may collide with another node's id or alias (`phaseIdx` takes the FIRST match, so a duplicate silently resolves a declaration onto the wrong node), and all aliases of one node share one record slot (`stageRecords` resolves each declared key onto its node, and the canonical id's record wins), so a sub-state that needs its own `--evidence` has to be a real node.
|
- **aliases** (array, optional): alternative names for the stage (e.g., `["training", "fit"]` lets you call `agenthub stage training`). An alias may also name a **sub-state** — *why* the workspace sits on a node — without adding a node to the map. Two constraints: no alias may collide with another node's id or alias (`phaseIdx` takes the FIRST match, so a duplicate silently resolves a declaration onto the wrong node), and all aliases of one node share one record slot (`stageRecords` resolves each declared key onto its node, and the canonical id's record wins), so a sub-state that needs its own `--evidence` has to be a real node.
|
||||||
- **detect** (array, optional): stage-detection rules for this node — see "Stage detection → Where the rules live".
|
- **detect** (array, optional): stage-detection rules for this node — see "Stage detection → Where the rules live".
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user