diff --git a/src/components/Home/RecentlyViewedSection/RecentlyViewedSection.tsx b/src/components/Home/RecentlyViewedSection/RecentlyViewedSection.tsx
new file mode 100644
index 000000000..18bbc9d91
--- /dev/null
+++ b/src/components/Home/RecentlyViewedSection/RecentlyViewedSection.tsx
@@ -0,0 +1,79 @@
+import { useNavigate } from "@tanstack/react-router";
+import { Clock, GitBranch, Play } from "lucide-react";
+
+import { InlineStack } from "@/components/ui/layout";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Paragraph, Text } from "@/components/ui/typography";
+import {
+ type RecentlyViewedItem,
+ useRecentlyViewed,
+} from "@/hooks/useRecentlyViewed";
+import { EDITOR_PATH, RUNS_BASE_PATH } from "@/routes/router";
+
+function getRecentlyViewedUrl(item: RecentlyViewedItem): string {
+ if (item.type === "pipeline") return `${EDITOR_PATH}/${item.id}`;
+ if (item.type === "run") return `${RUNS_BASE_PATH}/${item.id}`;
+ // component support to be added later
+ return "/";
+}
+
+const RecentlyViewedChip = ({ item }: { item: RecentlyViewedItem }) => {
+ const navigate = useNavigate();
+
+ const tooltipContent =
+ item.type === "run" ? `${item.name} #${item.id}` : item.name;
+
+ return (
+
+
+ navigate({ to: getRecentlyViewedUrl(item) })}
+ className={`flex items-center gap-1.5 pl-2 pr-2 py-1 border rounded-md cursor-pointer w-48 ${
+ item.type === "pipeline"
+ ? "bg-violet-50/50 hover:bg-violet-50 border-violet-100"
+ : "bg-emerald-50/50 hover:bg-emerald-50 border-emerald-100"
+ }`}
+ >
+ {item.type === "pipeline" ? (
+
+ ) : (
+
+ )}
+
{item.name}
+
+
+ {tooltipContent}
+
+ );
+};
+
+export const RecentlyViewedSection = () => {
+ const { recentlyViewed } = useRecentlyViewed();
+
+ return (
+
+
+
+
+ Recently Viewed
+
+
+
+ {recentlyViewed.length === 0 ? (
+
+ Nothing viewed yet. Open a pipeline or run to see it here.
+
+ ) : (
+
+ {recentlyViewed.map((item) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/src/hooks/useRecentlyViewed.ts b/src/hooks/useRecentlyViewed.ts
new file mode 100644
index 000000000..c33318539
--- /dev/null
+++ b/src/hooks/useRecentlyViewed.ts
@@ -0,0 +1,70 @@
+import { useCallback, useSyncExternalStore } from "react";
+
+import { getStorage } from "@/utils/typedStorage";
+
+const RECENTLY_VIEWED_KEY = "Home/recently_viewed";
+const MAX_ITEMS = 5;
+
+export type RecentlyViewedType = "pipeline" | "run" | "component";
+
+export interface RecentlyViewedItem {
+ type: RecentlyViewedType;
+ id: string;
+ name: string;
+ viewedAt: number;
+}
+
+type RecentlyViewedStorageMapping = {
+ [RECENTLY_VIEWED_KEY]: RecentlyViewedItem[];
+};
+
+const storage = getStorage<
+ typeof RECENTLY_VIEWED_KEY,
+ RecentlyViewedStorageMapping
+>();
+
+// useSyncExternalStore requires getSnapshot to return a stable reference.
+let cachedJson: string | null = null;
+let cachedItems: RecentlyViewedItem[] = [];
+
+function readRecentlyViewed(): RecentlyViewedItem[] {
+ const json = localStorage.getItem(RECENTLY_VIEWED_KEY);
+ if (json === cachedJson) return cachedItems;
+ cachedJson = json;
+ cachedItems = json ? (JSON.parse(json) as RecentlyViewedItem[]) : [];
+ return cachedItems;
+}
+
+function subscribe(callback: () => void) {
+ const handler = (event: StorageEvent) => {
+ if (event.key === RECENTLY_VIEWED_KEY) callback();
+ };
+ window.addEventListener("storage", handler);
+ return () => window.removeEventListener("storage", handler);
+}
+
+export function useRecentlyViewed() {
+ const recentlyViewed = useSyncExternalStore(
+ subscribe,
+ readRecentlyViewed,
+ () => [],
+ );
+
+ const addRecentlyViewed = useCallback(
+ (item: Omit) => {
+ const current = readRecentlyViewed();
+ // Remove any existing entry for the same item, then prepend the fresh one
+ const deduped = current.filter(
+ (i) => !(i.type === item.type && i.id === item.id),
+ );
+ const updated = [{ ...item, viewedAt: Date.now() }, ...deduped].slice(
+ 0,
+ MAX_ITEMS,
+ );
+ storage.setItem(RECENTLY_VIEWED_KEY, updated);
+ },
+ [],
+ );
+
+ return { recentlyViewed, addRecentlyViewed };
+}
diff --git a/src/routes/Dashboard/Dashboard.tsx b/src/routes/Dashboard/Dashboard.tsx
index e7d490a59..313cfd7c2 100644
--- a/src/routes/Dashboard/Dashboard.tsx
+++ b/src/routes/Dashboard/Dashboard.tsx
@@ -1,4 +1,5 @@
import { FavoritesSection } from "@/components/Home/FavoritesSection/FavoritesSection";
+import { RecentlyViewedSection } from "@/components/Home/RecentlyViewedSection/RecentlyViewedSection";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Text } from "@/components/ui/typography";
@@ -18,6 +19,7 @@ export const Dashboard = () => {
Beta
+
);
diff --git a/src/routes/Editor/Editor.tsx b/src/routes/Editor/Editor.tsx
index 82bdf67ff..12873226f 100644
--- a/src/routes/Editor/Editor.tsx
+++ b/src/routes/Editor/Editor.tsx
@@ -2,6 +2,7 @@ import "@/styles/editor.css";
import { DndContext } from "@dnd-kit/core";
import { ReactFlowProvider } from "@xyflow/react";
+import { useEffect } from "react";
import PipelineEditor from "@/components/Editor/PipelineEditor";
import { InfoBox } from "@/components/shared/InfoBox";
@@ -9,9 +10,20 @@ import { LoadingScreen } from "@/components/shared/LoadingScreen";
import { BlockStack } from "@/components/ui/layout";
import { Paragraph } from "@/components/ui/typography";
import { useLoadComponentSpecFromPath } from "@/hooks/useLoadComponentSpecFromPath";
+import { useRecentlyViewed } from "@/hooks/useRecentlyViewed";
const Editor = () => {
const { componentSpec, error } = useLoadComponentSpecFromPath();
+ const { addRecentlyViewed } = useRecentlyViewed();
+
+ useEffect(() => {
+ if (!componentSpec?.name) return;
+ addRecentlyViewed({
+ type: "pipeline",
+ id: componentSpec.name,
+ name: componentSpec.name,
+ });
+ }, [componentSpec?.name, addRecentlyViewed]);
if (error) {
return (
diff --git a/src/routes/PipelineRun/PipelineRun.tsx b/src/routes/PipelineRun/PipelineRun.tsx
index 65255e16c..b111cf640 100644
--- a/src/routes/PipelineRun/PipelineRun.tsx
+++ b/src/routes/PipelineRun/PipelineRun.tsx
@@ -11,6 +11,7 @@ import { BlockStack } from "@/components/ui/layout";
import { Paragraph } from "@/components/ui/typography";
import { faviconManager } from "@/favicon";
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
+import { useRecentlyViewed } from "@/hooks/useRecentlyViewed";
import { useBackend } from "@/providers/BackendProvider";
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
import {
@@ -28,6 +29,8 @@ const PipelineRunContent = () => {
const { setComponentSpec, clearComponentSpec, componentSpec } =
useComponentSpec();
const { configured, available, ready } = useBackend();
+ const { addRecentlyViewed } = useRecentlyViewed();
+ const params = useParams({ strict: false });
const {
details,
@@ -76,6 +79,13 @@ const PipelineRunContent = () => {
`Tangle - ${componentSpec?.name || ""} - ${params.id}`,
});
+ useEffect(() => {
+ const id =
+ "id" in params && typeof params.id === "string" ? params.id : null;
+ if (!componentSpec?.name || !id) return;
+ addRecentlyViewed({ type: "run", id, name: componentSpec.name });
+ }, [componentSpec?.name, params, addRecentlyViewed]);
+
if (isLoading || !ready) {
return ;
}