diff --git a/packages/web/src/components/conclusions/ConclusionBrowser.tsx b/packages/web/src/components/conclusions/ConclusionBrowser.tsx
index 347ab55..7d38c76 100644
--- a/packages/web/src/components/conclusions/ConclusionBrowser.tsx
+++ b/packages/web/src/components/conclusions/ConclusionBrowser.tsx
@@ -1,6 +1,6 @@
import { Link, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
-import { ArrowLeft, Eye, Lightbulb, Plus, Search, Trash2, X } from "lucide-react";
+import { Eye, Lightbulb, Plus, Search, Trash2, X } from "lucide-react";
import { useMemo, useState } from "react";
import { z } from "zod";
import {
@@ -10,6 +10,7 @@ import {
useQueryConclusions,
} from "@/api/queries";
import type { components } from "@/api/schema.d.ts";
+import { Breadcrumb } from "@/components/layout/Breadcrumb";
import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
import { EmptyState } from "@/components/shared/EmptyState";
import { ErrorAlert } from "@/components/shared/ErrorAlert";
@@ -108,15 +109,7 @@ export function ConclusionBrowser() {
return (
-
-
- {mask(workspaceId)}
-
+
Conclusions
diff --git a/packages/web/src/components/layout/Breadcrumb.tsx b/packages/web/src/components/layout/Breadcrumb.tsx
new file mode 100644
index 0000000..cbe84b1
--- /dev/null
+++ b/packages/web/src/components/layout/Breadcrumb.tsx
@@ -0,0 +1,104 @@
+import { Link, useRouter } from "@tanstack/react-router";
+import { ChevronRight } from "lucide-react";
+import { useDemo } from "@/hooks/useDemo";
+
+const SECTION_LABELS: Record
= {
+ peers: "Peers",
+ sessions: "Sessions",
+ conclusions: "Conclusions",
+ webhooks: "Webhooks",
+ chat: "Chat",
+};
+
+const KNOWN_SECTIONS = new Set(Object.keys(SECTION_LABELS));
+
+type Segment = { label: string; href: string | null; mono?: boolean };
+
+function buildSegments(pathname: string, mask: (v: string) => string): Segment[] {
+ if (!pathname.startsWith("/workspaces")) return [];
+
+ const rest = pathname.slice("/workspaces".length); // "" | "/wid" | "/wid/peers" | ...
+
+ if (!rest) return [{ label: "Workspaces", href: null }];
+
+ const parts = rest.slice(1).split("/"); // ["wid"] | ["wid", "peers"] | ...
+ const wid = parts[0];
+ if (!wid) return [{ label: "Workspaces", href: null }];
+
+ const segments: Segment[] = [{ label: "Workspaces", href: "/workspaces" }];
+
+ if (parts.length === 1) {
+ segments.push({ label: mask(wid), href: null, mono: true });
+ return segments;
+ }
+ segments.push({ label: mask(wid), href: `/workspaces/${wid}`, mono: true });
+
+ const section = parts[1];
+ if (!section || !KNOWN_SECTIONS.has(section)) return segments;
+
+ if (parts.length === 2) {
+ segments.push({ label: SECTION_LABELS[section], href: null });
+ return segments;
+ }
+ segments.push({ label: SECTION_LABELS[section], href: `/workspaces/${wid}/${section}` });
+
+ const subId = parts[2];
+ if (!subId) return segments;
+
+ if (parts.length === 3) {
+ segments.push({ label: mask(subId), href: null, mono: true });
+ return segments;
+ }
+ segments.push({
+ label: mask(subId),
+ href: `/workspaces/${wid}/${section}/${subId}`,
+ mono: true,
+ });
+
+ const subSection = parts[3];
+ if (subSection && SECTION_LABELS[subSection]) {
+ segments.push({ label: SECTION_LABELS[subSection], href: null });
+ }
+
+ return segments;
+}
+
+export function Breadcrumb() {
+ const { state } = useRouter();
+ const { mask } = useDemo();
+ const segments = buildSegments(state.location.pathname, mask);
+
+ if (segments.length <= 1) return null;
+
+ return (
+
+ );
+}
diff --git a/packages/web/src/components/peers/PeerList.tsx b/packages/web/src/components/peers/PeerList.tsx
index 800813c..9c3c0ad 100644
--- a/packages/web/src/components/peers/PeerList.tsx
+++ b/packages/web/src/components/peers/PeerList.tsx
@@ -1,9 +1,10 @@
-import { Link, useNavigate, useParams } from "@tanstack/react-router";
+import { useNavigate, useParams } from "@tanstack/react-router";
import { motion, type Variants } from "framer-motion";
-import { ArrowLeft, ChevronRight, Clock, Eye, Users } from "lucide-react";
+import { ChevronRight, Clock, Eye, Users } from "lucide-react";
import { useMemo, useState } from "react";
import { usePeers } from "@/api/queries";
import type { components } from "@/api/schema.d.ts";
+import { Breadcrumb } from "@/components/layout/Breadcrumb";
import { EmptyState } from "@/components/shared/EmptyState";
import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { JsonViewer } from "@/components/shared/JsonViewer";
@@ -105,15 +106,7 @@ export function PeerList() {
return (
-
-
- {mask(workspaceId)}
-
+
Peers
diff --git a/packages/web/src/components/sessions/SessionDetail.tsx b/packages/web/src/components/sessions/SessionDetail.tsx
index 8213279..ed453af 100644
--- a/packages/web/src/components/sessions/SessionDetail.tsx
+++ b/packages/web/src/components/sessions/SessionDetail.tsx
@@ -1,4 +1,4 @@
-import { Link, useNavigate, useParams } from "@tanstack/react-router";
+import { useNavigate, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { AlignLeft, Clock, Copy, MessageSquare, Search, Trash2, Users, X } from "lucide-react";
import { useState } from "react";
@@ -15,6 +15,7 @@ import {
useSessionSummaries,
} from "@/api/queries";
import type { components } from "@/api/schema.d.ts";
+import { Breadcrumb } from "@/components/layout/Breadcrumb";
import { Badge } from "@/components/shared/Badge";
import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
import { JsonViewer } from "@/components/shared/JsonViewer";
@@ -110,23 +111,7 @@ export function SessionDetail() {
return (
-
-
- {mask(workspaceId)}
-
- /
-
- Sessions
-
-
+
diff --git a/packages/web/src/components/sessions/SessionList.tsx b/packages/web/src/components/sessions/SessionList.tsx
index 84172f1..b4d2049 100644
--- a/packages/web/src/components/sessions/SessionList.tsx
+++ b/packages/web/src/components/sessions/SessionList.tsx
@@ -1,9 +1,10 @@
-import { Link, useNavigate, useParams } from "@tanstack/react-router";
+import { useNavigate, useParams } from "@tanstack/react-router";
import { motion, type Variants } from "framer-motion";
-import { ArrowLeft, ChevronRight, CircleDot, Clock, MessageSquare } from "lucide-react";
+import { ChevronRight, CircleDot, Clock, MessageSquare } from "lucide-react";
import { useMemo, useState } from "react";
import { useSessions } from "@/api/queries";
import type { components } from "@/api/schema.d.ts";
+import { Breadcrumb } from "@/components/layout/Breadcrumb";
import { EmptyState } from "@/components/shared/EmptyState";
import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { Pagination } from "@/components/shared/Pagination";
@@ -66,15 +67,7 @@ export function SessionList() {
return (
-
-
- {mask(workspaceId)}
-
+
Sessions
diff --git a/packages/web/src/components/workspaces/WebhookManager.tsx b/packages/web/src/components/workspaces/WebhookManager.tsx
index 2296745..e97c6f9 100644
--- a/packages/web/src/components/workspaces/WebhookManager.tsx
+++ b/packages/web/src/components/workspaces/WebhookManager.tsx
@@ -1,10 +1,10 @@
-import { Link } from "@tanstack/react-router";
import { open } from "@tauri-apps/plugin-shell";
import { AnimatePresence, motion } from "framer-motion";
-import { ArrowLeft, ExternalLink, Plus, Trash2, Webhook, Zap } from "lucide-react";
+import { ExternalLink, Plus, Trash2, Webhook, Zap } from "lucide-react";
import { useState } from "react";
import { z } from "zod";
import { useCreateWebhook, useDeleteWebhook, useTestWebhook, useWebhooks } from "@/api/queries";
+import { Breadcrumb } from "@/components/layout/Breadcrumb";
import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { Skeleton } from "@/components/shared/Skeleton";
@@ -55,15 +55,7 @@ export function WebhookManager({ workspaceId }: Props) {
return (
-
-
- {mask(workspaceId)}
-
+