Skip to content

feat(desktop): implement router history with forward/back navigation#1131

Merged
saddlepaddle merged 3 commits into
mainfrom
implement-router-history-with-forward-back-navigat
Feb 2, 2026
Merged

feat(desktop): implement router history with forward/back navigation#1131
saddlepaddle merged 3 commits into
mainfrom
implement-router-history-with-forward-back-navigat

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 2, 2026

Summary

  • Replace createHashHistory with a custom persistent history (persistent-hash-history) that stores navigation entries in localStorage and syncs the URL hash
  • Add back/forward navigation buttons (Cmd+[/]) and a "Recently Viewed" dropdown to the top bar showing workspaces and tasks with proper status icons
  • History persists across app restarts and caps at 100 entries

Test plan

  • Back/forward buttons work and disable at bounds
  • Cmd+[ and Cmd+] hotkeys navigate back/forward
  • Recently viewed dropdown shows workspaces and tasks with correct status icons, slugs, and titles
  • History persists after app restart (check localStorage router-history key)
  • No duplicate entries in recently viewed when navigating back then to a new page
  • bun test apps/desktop/src/renderer/lib/persistent-hash-history/ passes (19 tests)
  • bun run typecheck --filter=@superset/desktop passes

Summary by CodeRabbit

  • New Features

    • Back/forward navigation controls added to the top bar
    • "Recently viewed" dropdown showing recent workspaces and tasks
    • Keyboard shortcuts: Navigate Back (Cmd+[) and Navigate Forward (Cmd+])
    • Navigation history now persists across sessions
  • Behavior Changes

    • Settings auto-navigation now replaces the current history entry (affects back/forward behavior)
    • Improved route matching for workspace/task active-state detection (more permissive)

Replace createHashHistory with a custom persistent history that stores
entries in localStorage and syncs the URL hash. Adds back/forward
navigation buttons and a recently viewed dropdown to the top bar.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

Replaces hash-based history with a new persistentHistory (localStorage-backed) router, adds navigation UI (back/forward buttons + recently-viewed dropdown), registers navigation hotkeys, and wires persistence and UI to the router/history API.

Changes

Cohort / File(s) Summary
Persistent History
apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts, apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.test.ts, apps/desktop/src/renderer/lib/persistent-hash-history/index.ts
Add a RouterHistory-compatible persistentHistory singleton with localStorage-backed sliding window (MAX_ENTRIES=100), full push/replace/back/forward/go API, persistence/restore, and comprehensive tests covering truncation, bounds, persistence, corruption handling, and hash sync.
Router Entrypoint
apps/desktop/src/renderer/index.tsx
Switch router to use the new persistentHistory singleton instead of the previous HashHistory instance.
TopBar Navigation UI
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx, .../NavigationControls/NavigationControls.tsx, .../NavigationControls/index.ts
Add NavigationControls component into TopBar (back/forward buttons, hotkey wiring, canGoBack/canGoForward logic) and adjust left-cluster spacing.
Recently Viewed Dropdown & Hook
apps/desktop/src/renderer/.../HistoryDropdown/HistoryDropdown.tsx, .../HistoryDropdown/index.ts, .../useRecentlyViewed/useRecentlyViewed.ts, .../useRecentlyViewed/index.ts
New HistoryDropdown UI showing deduplicated recent workspace/task entries; new useRecentlyViewed hook parses persistent history to extract resource type/entityId/timestamp and returns limited, cached results.
Hotkeys: Category & Keys
apps/desktop/src/shared/hotkeys.ts, apps/desktop/src/renderer/stores/hotkeys/store.ts
Add "Navigation" hotkey category and two hotkeys: NAVIGATE_BACK (meta+[) and NAVIGATE_FORWARD (meta+]); include category initialization in hotkey store.
Settings Navigation Behavior
apps/desktop/src/renderer/routes/_authenticated/settings/layout.tsx
Change auto-navigation on search match to use replace: true (replace current history instead of pushing).
Misc. UI Route Matching
apps/desktop/src/renderer/screens/main/.../WorkspaceListItem.tsx, .../WorkspaceSidebarHeader.tsx
Enable fuzzy route matching for workspace/tasks active-state detection.
Minor Formatting
apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
Whitespace removal only.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant TopBar
  participant Router as TanStackRouter
  participant persistentHistory as persistentHistory
  participant LocalStorage as localStorage

  rect rgba(220,235,255,0.5)
    User->>TopBar: click Back / Forward / History item
    TopBar->>Router: call router.navigate (or router.history.back()/forward())
    Router->>persistentHistory: invoke back/forward/push/replace
    persistentHistory->>LocalStorage: persist updated entries + index
    persistentHistory->>Router: update current location (hash/url)
    Router->>TopBar: update location/state (re-render)
    TopBar->>User: render updated route
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Hopping back through hashes and time,

I store each path in a neat little line.
Buttons to jump and a dropdown to view,
Hotkeys that whisper “back” and “through,”
A rabbit's delight — history made fine! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: implementing router history with forward/back navigation, which aligns with the primary objective of the changeset.
Description check ✅ Passed The PR description covers the key objectives, includes a test plan with specific verification steps, and adequately summarizes the changes, though it deviates from the template structure with a custom summary format.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch implement-router-history-with-forward-back-navigat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 2, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts`:
- Around line 117-122: When restoring persisted history, don't overwrite
original timestamps with Date.now(); instead read persisted.timestamps (falling
back to Date.now() for missing entries) and use those values when building the
states array. Update the load path that currently creates timestamps: replace
the entries.map(() => Date.now()) logic with a mapping that pulls
persisted.timestamps[i] || Date.now(), and propagate that timestamp into the
LocationState object (either by adding a timestamp argument to assignKeyAndIndex
or by setting the timestamp on each state after assignKeyAndIndex). Also ensure
the save path persists timestamps alongside persisted.entries so future reloads
can restore them.
- Around line 86-108: Add unit tests for the parseHref function in
persistent-hash-history.ts to validate parsing across cases: no search/hash,
search only, hash only, search before hash, hash before search, and empty
query/hash segments; assert returned HistoryLocation fields (href, pathname,
search, hash, state) match expected values for each input and include at least
one test using a non-root pathname (e.g., '/workspace/abc') and one with both
query and fragment to ensure the substring logic (searchIndex/hashIndex and
pathname slicing) in parseHref behaves correctly and prevents regressions.
🧹 Nitpick comments (6)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/NavigationControls/components/HistoryDropdown/hooks/useRecentlyViewed/useRecentlyViewed.ts (1)

35-37: Consider extracting default limit to a named constant.

The default limit of 20 is a magic number. Per coding guidelines, consider extracting it to a module-level constant for clarity and easier maintenance.

♻️ Suggested refactor
+const DEFAULT_RECENTLY_VIEWED_LIMIT = 20;
+
-export function useRecentlyViewed(limit = 20): RecentlyViewedEntry[] {
+export function useRecentlyViewed(limit = DEFAULT_RECENTLY_VIEWED_LIMIT): RecentlyViewedEntry[] {
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/NavigationControls/components/HistoryDropdown/HistoryDropdown.tsx (1)

110-117: Type assertion on statusType could cause runtime issues.

The task.statusType as StatusType assertion bypasses type checking. If the database contains an unexpected status type, this could cause rendering issues or crashes in StatusIcon.

Consider validating the type or using a type guard before rendering:

♻️ Suggested approach
// Option 1: Add a fallback for unknown types
const validStatusTypes = ["todo", "in_progress", "done", /* ... other valid types */] as const;
const safeStatusType = validStatusTypes.includes(task.statusType as StatusType) 
  ? (task.statusType as StatusType) 
  : "todo"; // fallback

// Option 2: If StatusIcon already handles unknown types gracefully, document this assumption
apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts (1)

40-42: Silent error swallowing in localStorage operations.

Per coding guidelines, errors should at minimum be logged. While localStorage failures are non-critical, logging helps with debugging:

♻️ Suggested improvement
 } catch {
-	// Corrupted or unavailable — fall through to default
+	console.warn("[persistent-hash-history/load] Failed to load persisted state, using default");
 }

 } catch {
-	// localStorage full or unavailable — ignore
+	console.warn("[persistent-hash-history/persist] Failed to persist state");
 }

As per coding guidelines: "Never silently swallow errors with catch(() => {}) or catch(e) { return null }" and "Log errors at minimum if not re-throwing".

Also applies to: 60-62

apps/desktop/src/renderer/lib/persistent-hash-history/index.ts (1)

1-1: Consider exporting additional types for consumers.

The barrel only exports persistentHistory, but HistoryEntry and PersistentHashHistory types might be useful for consumers who need type-safe access to getEntries() results.

♻️ Suggested addition
-export { persistentHistory } from "./persistent-hash-history";
+export {
+	type HistoryEntry,
+	type PersistentHashHistory,
+	persistentHistory,
+} from "./persistent-hash-history";
apps/desktop/src/renderer/index.tsx (1)

5-7: Consider using the renderer/ alias for consistency.

Other files in this PR use the renderer/ alias (e.g., renderer/lib/persistent-hash-history), but this file uses a relative import. For consistency:

♻️ Suggested change
-import { persistentHistory } from "./lib/persistent-hash-history";
+import { persistentHistory } from "renderer/lib/persistent-hash-history";

As per coding guidelines: "Use alias as defined in tsconfig.json when possible".

apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/NavigationControls/NavigationControls.tsx (1)

12-13: Add canGoForward() method to PersistentHashHistory for consistency with canGoBack().

The custom history implementation exposes canGoBack() as a dedicated API method, but canGoForward in NavigationControls relies on manually accessing location.state.__TSR_index. While __TSR_index is intentionally set in the persistent-hash-history implementation and the calculation is correct, this creates an asymmetry: one capability uses the public API while the other accesses state directly.

Adding a canGoForward() method to PersistentHashHistory would:

  • Maintain API consistency with canGoBack()
  • Reduce direct state access in UI components
  • Make the logic testable alongside other history methods

Comment on lines +86 to +108
function parseHref(href: string, state: LocationState): HistoryLocation {
const searchIndex = href.indexOf("?");
const hashIndex = href.indexOf("#");
return {
href,
pathname: href.substring(
0,
hashIndex > 0
? searchIndex > 0
? Math.min(hashIndex, searchIndex)
: hashIndex
: searchIndex > 0
? searchIndex
: href.length,
),
hash: hashIndex > -1 ? href.substring(hashIndex) : "",
search:
searchIndex > -1
? href.slice(searchIndex, hashIndex === -1 ? undefined : hashIndex)
: "",
state,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the file and explore the persistent-hash-history module
fd -e ts -e tsx "persistent-hash-history" apps/desktop/src/renderer/lib/

Repository: superset-sh/superset

Length of output: 230


🏁 Script executed:

# Check test files for this module
fd -e test.ts -e test.tsx -e spec.ts -e spec.tsx | grep -i "persistent-hash-history\|history"

Repository: superset-sh/superset

Length of output: 149


🏁 Script executed:

# Search for parseHref usage and tests
rg -n "parseHref" --type ts --type tsx apps/

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Read the test file to check edge cases
cat -n apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.test.ts

Repository: superset-sh/superset

Length of output: 9352


🏁 Script executed:

#!/bin/bash
# Read the full implementation file for context
cat -n apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts

Repository: superset-sh/superset

Length of output: 5964


Add unit tests for parseHref to ensure correct parsing of URLs with various components.

The function correctly handles empty search and hash strings, but lacks dedicated test coverage. While the logic handles URLs with query and hash in any order, the function is only used with simple paths (e.g., /tasks, /workspace/abc) in practice. However, explicit tests would clarify the expected behavior and prevent regressions if the usage pattern changes.

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts`
around lines 86 - 108, Add unit tests for the parseHref function in
persistent-hash-history.ts to validate parsing across cases: no search/hash,
search only, hash only, search before hash, hash before search, and empty
query/hash segments; assert returned HistoryLocation fields (href, pathname,
search, hash, state) match expected values for each input and include at least
one test using a non-root pathname (e.g., '/workspace/abc') and one with both
query and fragment to ensure the substring logic (searchIndex/hashIndex and
pathname slicing) in parseHref behaves correctly and prevents regressions.

Comment on lines +117 to +122
const entries: string[] = [...persisted.entries];
const timestamps: number[] = entries.map(() => Date.now());
const states: LocationState[] = entries.map((_entry, i) =>
assignKeyAndIndex(i),
);
let index = persisted.index;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Timestamps are lost on app reload.

When loading persisted state, all entries are assigned the current timestamp (Date.now()), losing the original navigation timestamps. This affects the "Recently Viewed" feature's ability to show accurate chronological order after an app restart.

Consider persisting timestamps alongside paths:

🐛 Suggested fix
 interface PersistedState {
-	entries: string[];
+	entries: Array<{ path: string; timestamp: number }>;
 	index: number;
 }

 function loadPersistedState(): PersistedState {
 	// ... parsing logic updated to handle new structure
 }

 function persistState(entries: string[], timestamps: number[], index: number) {
-	const capped = entries.length > MAX_ENTRIES
-		? entries.slice(entries.length - MAX_ENTRIES)
-		: entries;
+	const combined = entries.map((path, i) => ({ path, timestamp: timestamps[i]! }));
+	const capped = combined.length > MAX_ENTRIES
+		? combined.slice(combined.length - MAX_ENTRIES)
+		: combined;
 	// ...
 }
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts`
around lines 117 - 122, When restoring persisted history, don't overwrite
original timestamps with Date.now(); instead read persisted.timestamps (falling
back to Date.now() for missing entries) and use those values when building the
states array. Update the load path that currently creates timestamps: replace
the entries.map(() => Date.now()) logic with a mapping that pulls
persisted.timestamps[i] || Date.now(), and propagate that timestamp into the
LocationState object (either by adding a timestamp argument to assignKeyAndIndex
or by setting the timestamp on each state after assignKeyAndIndex). Also ensure
the save path persists timestamps alongside persisted.entries so future reloads
can restore them.

Remove non-null assertions in persistent-hash-history, use fuzzy route
matching for Tasks and workspace sidebar highlighting.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts`:
- Around line 22-57: Both loadPersistedState and persistState currently swallow
all exceptions; update their catch blocks to log the caught error with
contextual prefixed messages (e.g. "[persistent-hash-history/load] <message>"
and "[persistent-hash-history/persist] <message>") using console.error and
include the Error object so JSON/localStorage/quota issues are visible; modify
loadPersistedState's catch to console.error("[persistent-hash-history/load]
failed to load persisted state:", err) and persistState's catch to
console.error("[persistent-hash-history/persist] failed to persist state:", err)
while leaving the rest of the logic unchanged.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts (1)

43-103: Use params objects for multi-arg helpers.

persistState, assignKeyAndIndex, and parseHref all take 2+ positional arguments. Please switch these to named params objects for clarity and to avoid future boolean/positional confusion.

♻️ Example refactor (apply similarly to these helpers)
-function persistState(entries: string[], index: number) {
+function persistState({
+	entries,
+	index,
+}: {
+	entries: string[];
+	index: number;
+}) {
 	try {
@@
-		localStorage.setItem(
+		localStorage.setItem(
 			STORAGE_KEY,
 			JSON.stringify({ entries: capped, index: cappedIndex }),
 		);
 	} catch {}
 }
 
-function assignKeyAndIndex(
-	index: number,
-	state?: LocationState,
-): LocationState {
+function assignKeyAndIndex({
+	index,
+	state,
+}: {
+	index: number;
+	state?: LocationState;
+}): LocationState {
 	const key = createRandomKey();
 	return {
 		...(state ?? {}),
@@
-function parseHref(href: string, state: LocationState): HistoryLocation {
+function parseHref({
+	href,
+	state,
+}: {
+	href: string;
+	state: LocationState;
+}): HistoryLocation {
 	const searchIndex = href.indexOf("?");
 	const hashIndex = href.indexOf("#");
 	return {
 		href,
@@
-const getLocation = () =>
-	parseHref(entries[index] ?? "/", states[index] ?? assignKeyAndIndex(index));
+const getLocation = () =>
+	parseHref({
+		href: entries[index] ?? "/",
+		state: states[index] ?? assignKeyAndIndex({ index }),
+	});
@@
-			persistState(entries, index);
+			persistState({ entries, index });

As per coding guidelines: Functions with 2+ parameters should accept a single params object with named properties instead of positional arguments.

Comment on lines +22 to +57
function loadPersistedState(): PersistedState {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw) as PersistedState;
if (
Array.isArray(parsed.entries) &&
parsed.entries.length > 0 &&
typeof parsed.index === "number"
) {
const index = Math.min(
Math.max(parsed.index, 0),
parsed.entries.length - 1,
);
return { entries: parsed.entries, index };
}
}
} catch {}
return { entries: ["/"], index: 0 };
}

function persistState(entries: string[], index: number) {
try {
const capped =
entries.length > MAX_ENTRIES
? entries.slice(entries.length - MAX_ENTRIES)
: entries;
const cappedIndex =
entries.length > MAX_ENTRIES
? index - (entries.length - MAX_ENTRIES)
: index;
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({ entries: capped, index: cappedIndex }),
);
} catch {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t swallow localStorage errors—log with context.

Both loadPersistedState and persistState silently ignore failures, which makes storage issues invisible in production. Please log with the required prefix so we can diagnose storage/quota/JSON failures.

🔧 Suggested fix
 function loadPersistedState(): PersistedState {
 	try {
 		const raw = localStorage.getItem(STORAGE_KEY);
@@
-	} catch {}
+	} catch (error) {
+		console.error(
+			"[router-history/loadPersistedState] failed to read persisted history",
+			error,
+		);
+	}
 	return { entries: ["/"], index: 0 };
 }
 
 function persistState(entries: string[], index: number) {
 	try {
@@
-	} catch {}
+	} catch (error) {
+		console.error(
+			"[router-history/persistState] failed to persist history",
+			error,
+		);
+	}
 }

As per coding guidelines: Never swallow errors silently; at minimum log them with context; use prefixed console logging with pattern [domain/operation] message for all logging.

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/lib/persistent-hash-history/persistent-hash-history.ts`
around lines 22 - 57, Both loadPersistedState and persistState currently swallow
all exceptions; update their catch blocks to log the caught error with
contextual prefixed messages (e.g. "[persistent-hash-history/load] <message>"
and "[persistent-hash-history/persist] <message>") using console.error and
include the Error object so JSON/localStorage/quota issues are visible; modify
loadPersistedState's catch to console.error("[persistent-hash-history/load]
failed to load persisted state:", err) and persistState's catch to
console.error("[persistent-hash-history/persist] failed to persist state:", err)
while leaving the rest of the logic unchanged.

@saddlepaddle saddlepaddle merged commit 8523f4a into main Feb 2, 2026
13 checks passed
@Kitenite Kitenite deleted the implement-router-history-with-forward-back-navigat branch February 4, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant