diff --git a/frontend/src/_util/recent-pages-history.js b/frontend/src/_util/recent-pages-history.js
new file mode 100644
index 00000000..2aba7887
--- /dev/null
+++ b/frontend/src/_util/recent-pages-history.js
@@ -0,0 +1,116 @@
+'use strict';
+
+const RECENT_PAGES_STORAGE_KEY = 'studio:recent-pages-history';
+const RECENT_PAGES_CHANGED_EVENT = 'studio-recent-pages-changed';
+const RECENT_PAGES_LOCK_NAME = `studio-lock:${RECENT_PAGES_STORAGE_KEY}`;
+const MAX_RECENT_PAGES = 10;
+
+/** Collapses hash-style prefixes (e.g. `/#/`) to a normal path; keeps query string. */
+function normalizeTrackedPath(fullPath) {
+ if (!fullPath || typeof fullPath !== 'string') {
+ return '/';
+ }
+ const qIndex = fullPath.indexOf('?');
+ const rawMain = qIndex === -1 ? fullPath : fullPath.slice(0, qIndex);
+ const qs = qIndex === -1 ? '' : fullPath.slice(qIndex);
+
+ let pathOnly = rawMain.trim().replace(/#/g, '');
+ pathOnly = pathOnly.replace(/^\/+/u, '/');
+ if (pathOnly !== '/' && /\/$/u.test(pathOnly)) {
+ pathOnly = pathOnly.replace(/\/+$/u, '');
+ }
+ if (pathOnly === '' || pathOnly === '/') {
+ pathOnly = '/';
+ } else if (!pathOnly.startsWith('/')) {
+ pathOnly = `/${pathOnly}`;
+ }
+
+ const out = qs ? pathOnly + qs : pathOnly;
+ return out;
+}
+
+function safeReadRecentPages() {
+ if (typeof window === 'undefined' || !window.localStorage) {
+ return [];
+ }
+ try {
+ const raw = window.localStorage.getItem(RECENT_PAGES_STORAGE_KEY);
+ if (!raw) {
+ return [];
+ }
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) {
+ return [];
+ }
+ return parsed
+ .map(entry => {
+ if (!entry || typeof entry.path !== 'string') {
+ return null;
+ }
+ return { ...entry, path: normalizeTrackedPath(entry.path) };
+ })
+ .filter(
+ entry =>
+ entry &&
+ typeof entry.label === 'string' &&
+ typeof entry.visitedAt === 'number' &&
+ entry.path !== ''
+ )
+ .slice(0, MAX_RECENT_PAGES);
+ } catch {
+ return [];
+ }
+}
+
+function notifyRecentPagesChangedFromThisTab() {
+ if (typeof window === 'undefined') {
+ return;
+ }
+ try {
+ window.dispatchEvent(new CustomEvent(RECENT_PAGES_CHANGED_EVENT));
+ } catch {
+ /* ignore */
+ }
+}
+
+function saveRecentPages(entries) {
+ if (typeof window === 'undefined' || !window.localStorage) {
+ return;
+ }
+ window.localStorage.setItem(
+ RECENT_PAGES_STORAGE_KEY,
+ JSON.stringify(entries.slice(0, MAX_RECENT_PAGES))
+ );
+ notifyRecentPagesChangedFromThisTab();
+}
+
+function withRecentPagesStorageLock(syncFn) {
+ if (typeof window === 'undefined' || !window.localStorage) {
+ syncFn();
+ return Promise.resolve();
+ }
+
+ try {
+ const locksApi =
+ typeof navigator !== 'undefined' && navigator.locks !== undefined ? navigator.locks : null;
+
+ if (locksApi !== null && typeof locksApi.request === 'function') {
+ return locksApi.request(RECENT_PAGES_LOCK_NAME, syncFn);
+ }
+ } catch {
+ /* fall through */
+ }
+
+ syncFn();
+ return Promise.resolve();
+}
+
+module.exports = {
+ RECENT_PAGES_STORAGE_KEY,
+ RECENT_PAGES_CHANGED_EVENT,
+ MAX_RECENT_PAGES,
+ normalizeTrackedPath,
+ safeReadRecentPages,
+ saveRecentPages,
+ withRecentPagesStorageLock
+};
diff --git a/frontend/src/index.js b/frontend/src/index.js
index 0a88f1d1..4a487e3d 100644
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -15,8 +15,64 @@ const { routes, hasAccess } = require('./routes');
const Toast = require('vue-toastification').default;
const { useToast } = require('vue-toastification');
const appendCSS = require('./appendCSS');
+const {
+ safeReadRecentPages,
+ saveRecentPages,
+ MAX_RECENT_PAGES,
+ normalizeTrackedPath,
+ withRecentPagesStorageLock
+} = require('./_util/recent-pages-history');
appendCSS(require('vue-toastification/dist/index.css'));
+const TRACKED_RECENT_PAGE_ROUTE_NAMES = new Set(['model', 'document', 'dashboard', 'chat']);
+
+function formatHistoryLabel(route) {
+ if (!route || typeof route.path !== 'string') {
+ return 'Unknown page';
+ }
+ if (route.name === 'model') {
+ return route.params?.model ? `Model: ${route.params.model}` : 'Model';
+ }
+ if (route.name === 'document') {
+ const model = route.params?.model ? `${route.params.model} ` : '';
+ const documentId = route.params?.documentId ? String(route.params.documentId) : '';
+ const shortDocumentId = documentId ? documentId.slice(0, 8) : '';
+ return `Document: ${model}${shortDocumentId}`.trim();
+ }
+ if (route.name === 'dashboard') {
+ return route.params?.dashboardId ? `Dashboard: ${route.params.dashboardId}` : 'Dashboard';
+ }
+ if (route.name === 'chat') {
+ return route.params?.threadId ? `Chat: ${route.params.threadId}` : 'Chat';
+ }
+ const normalizedPath = route.path.replace(/^\//, '');
+ return normalizedPath || 'Home';
+}
+
+function trackRecentPage(route) {
+ if (!route || typeof route.path !== 'string') {
+ return;
+ }
+ if (!TRACKED_RECENT_PAGE_ROUTE_NAMES.has(route.name)) {
+ return;
+ }
+ if (typeof route.path === 'string' && route.path.includes('code=')) {
+ return;
+ }
+ const path = normalizeTrackedPath(route.fullPath || route.path || '/');
+ const label = formatHistoryLabel(route);
+ const visitedAt = Date.now();
+
+ void withRecentPagesStorageLock(() => {
+ const existing = safeReadRecentPages();
+ const deduped = existing.filter(entry => normalizeTrackedPath(entry.path) !== path);
+ const next = [...deduped, { path, label, visitedAt }]
+ .sort((a, b) => b.visitedAt - a.visitedAt)
+ .slice(0, MAX_RECENT_PAGES);
+ saveRecentPages(next);
+ });
+}
+
const app = Vue.createApp({
template: '