Skip to content

fix(interface): navigate to adapters picker in-tab - #3075

Open
gprot42 wants to merge 1 commit into
spacedriveapp:mainfrom
gprot42:fix/sources-adapters-tab-navigation
Open

fix(interface): navigate to adapters picker in-tab#3075
gprot42 wants to merge 1 commit into
spacedriveapp:mainfrom
gprot42:fix/sources-adapters-tab-navigation

Conversation

@gprot42

@gprot42 gprot42 commented Jul 8, 2026

Copy link
Copy Markdown

Summary

  • Open adapters picker via navigate() instead of createTab(), since it is a sub-page of Sources
  • Deduplicate persisted tabs that pointed at /sources/adapters
  • Keep tab title as Sources while on the adapters picker route

Test plan

  • Sources → + Add Source shows a single tab (no duplicate Adapters tab)
  • Back from Data Adapters returns to Sources
  • Reload app — no duplicate Adapters tabs restored from localStorage

Note

Key Changes: The adapters picker is now treated as a sub-page within the Sources tab rather than a separate tab. New normalizeTabs() function deduplicates old /sources/adapters tabs from persisted state and normalizes their titles. The useOpenAdaptersPicker hook handles cleanup of stale adapter tabs and navigation. The back button now navigates to /sources instead of browser history.

Written by Tembo for commit 889f605. This will update automatically on new commits.

Opening the adapters picker via createTab left duplicate Sources/Adapters
tabs in the tab bar. Navigate within the current tab, normalize persisted
tabs on load, and keep the tab title as Sources while on /sources/adapters.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Tab persistence logic in TabManagerContext deduplicates tabs by savedPath and merges /sources with /sources/adapters, renaming the kept tab "Sources". createTab now reuses existing tabs for the same path. TabNavigationSync and Adapters route update titles/navigation accordingly. SourcesHome adopts a new useOpenAdaptersPicker hook replacing direct tab creation.

Changes

Tab deduplication and Sources/Adapters merge

Layer / File(s) Summary
Tab normalization and duplicate prevention
packages/interface/src/components/TabManager/TabManagerContext.tsx
Adds normalizeTabs to dedupe persisted tabs by savedPath, merges /sources//sources/adapters into a single "Sources" tab, applies normalization to initial state, and updates createTab to activate existing tabs instead of duplicating them.
Sources/Adapters title and navigation update
packages/interface/src/components/TabManager/TabNavigationSync.tsx, packages/interface/src/routes/sources/Adapters.tsx
Removes the separate "Adapters" title mapping in favor of "Sources", and the Adapters screen's back button now navigates directly to /sources.
Adapters picker hook and Sources route wiring
packages/interface/src/routes/sources/useOpenAdaptersPicker.ts, packages/interface/src/routes/sources/index.tsx
Adds useOpenAdaptersPicker hook to close stale adapters tabs and navigate to the adapters route, and wires both "Add Source" buttons in SourcesHome to use it instead of createTab.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SourcesHome
  participant Picker as useOpenAdaptersPicker
  participant TabManager
  participant Router

  SourcesHome->>Picker: openAdaptersPicker()
  Picker->>TabManager: close stale /sources/adapters tabs
  Picker->>Router: navigate to /sources/adapters
  Router-->>TabManager: TabNavigationSync derives title "Sources"
Loading

Poem

A rabbit hopped through tabs anew,
Merged two paths into just one view,
No more duplicates hopping 'round,
"Sources" now the only sound,
Click once, picker opens wide —
Adapters found on the same side! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: opening the adapters picker in the existing tab.
Description check ✅ Passed The summary and test plan are present and aligned with the template, but the Closes #(issue) reference is missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/interface/src/components/TabManager/TabManagerContext.tsx (1)

245-278: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Eliminate redundant normalizeTabs call in activeTabId initializer.

The activeTabId initializer calls loadPersistedState() and normalizeTabs(persisted.tabs) again, even though tabs (already normalized) is defined right above and accessible in this closure. Use tabs directly to avoid the redundant computation.

♻️ Proposed refactor
 	const [activeTabId, setActiveTabId] = useState<string>(() => {
 		const persisted = loadPersistedState();
 		if (persisted && persisted.activeTabId) {
-			const normalized = normalizeTabs(persisted.tabs);
-			const tabExists = normalized.some(
+			const tabExists = tabs.some(
 				(t) => t.id === persisted.activeTabId,
 			);
 			if (tabExists) return persisted.activeTabId;
-			if (normalized[0]) return normalized[0].id;
+			if (tabs[0]) return tabs[0].id;
 		}
 		return tabs[0].id;
 	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/interface/src/components/TabManager/TabManagerContext.tsx` around
lines 245 - 278, The activeTabId initializer in TabManagerContext redundantly
reloads persisted state and calls normalizeTabs(persisted.tabs) even though the
already-initialized tabs state is available in the same closure. Update the
activeTabId useState initializer to rely on tabs directly for existence checks
and fallback selection, keeping the same behavior while removing the duplicate
normalization work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/interface/src/components/TabManager/TabManagerContext.tsx`:
- Around line 337-370: The tab creation logic inside the setTabs updater is
impure because it also calls setActiveTabId, setExplorerStates, and
setSelectionStates, which can run twice in Strict Mode and create orphaned
per-tab state. In TabManagerContext’s tab creation flow, generate the new tab ID
and newTab object first, then move the active-tab update plus the
explorer/selection state initialization outside the setTabs callback so the
updater only returns the next tabs array.

---

Nitpick comments:
In `@packages/interface/src/components/TabManager/TabManagerContext.tsx`:
- Around line 245-278: The activeTabId initializer in TabManagerContext
redundantly reloads persisted state and calls normalizeTabs(persisted.tabs) even
though the already-initialized tabs state is available in the same closure.
Update the activeTabId useState initializer to rely on tabs directly for
existence checks and fallback selection, keeping the same behavior while
removing the duplicate normalization work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7182ae86-cee3-4d1f-8b52-a2e138ba0b12

📥 Commits

Reviewing files that changed from the base of the PR and between 60369e9 and 889f605.

📒 Files selected for processing (5)
  • packages/interface/src/components/TabManager/TabManagerContext.tsx
  • packages/interface/src/components/TabManager/TabNavigationSync.tsx
  • packages/interface/src/routes/sources/Adapters.tsx
  • packages/interface/src/routes/sources/index.tsx
  • packages/interface/src/routes/sources/useOpenAdaptersPicker.ts

Comment on lines +337 to +370
setTabs((prev) => {
const existing = prev.find((tab) => tab.savedPath === tabPath);
if (existing) {
setActiveTabId(existing.id);
return prev;
}

const [pathname, search = ""] = tabPath.split("?");
const derivedTitle =
title ||
deriveTitleFromPath(pathname, search ? `?${search}` : "");

const newTab: Tab = {
id: crypto.randomUUID(),
title: derivedTitle,
icon: null,
isPinned: false,
lastActive: Date.now(),
savedPath: tabPath,
};

setExplorerStates((explorerPrev) =>
new Map(explorerPrev).set(newTab.id, {
...DEFAULT_EXPLORER_STATE,
}),
);

// Initialize empty selection state for the new tab
setSelectionStates((prev) => new Map(prev).set(newTab.id, []));
setSelectionStates((selectionPrev) =>
new Map(selectionPrev).set(newTab.id, []),
);

setTabs((prev) => [...prev, newTab]);
setActiveTabId(newTab.id);
setActiveTabId(newTab.id);
return [...prev, newTab];
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/interface/src/components/TabManager/TabManagerContext.tsx"

echo "== file info =="
wc -l "$FILE"

echo
echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== relevant slice around 337-370 =="
sed -n '300,420p' "$FILE" | cat -n

echo
echo "== search for cleanup / state syncing =="
rg -n "setExplorerStates|setSelectionStates|setActiveTabId|DEFAULT_EXPLORER_STATE|savedPath|createTab|removeTab|deleteTab|closeTab|cleanup" "$FILE"

Repository: spacedriveapp/spacedrive

Length of output: 8044


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/interface/src/components/TabManager/TabManagerContext.tsx"

echo "== loadPersistedState / savePersistedState / normalizeTabs =="
sed -n '120,220p' "$FILE" | cat -n

echo
echo "== state initialization and persistence =="
sed -n '240,340p' "$FILE" | cat -n

echo
echo "== any cleanup of explorer/selection state outside closeTab =="
rg -n "setExplorerStates|setSelectionStates|delete\\(|Object\\.fromEntries|loadPersistedState|savePersistedState|normalizeTabs" "$FILE"

Repository: spacedriveapp/spacedrive

Length of output: 8402


🌐 Web query:

React StrictMode double-invokes state updater functions functional updater side effects randomUUID

💡 Result:

In React Strict Mode, the double-invocation of state updater functions is an intentional behavior designed to help identify impure code during development [1][2][3]. Why React invokes updater functions twice: React Strict Mode runs certain functions twice in development—including state updater functions—to ensure that your logic is idempotent (producing the same result regardless of how many times it is called) [1][4][3]. Because React may choose to re-render or pause components during concurrent rendering, updater functions must be pure and free of side effects [5][6][3]. If an updater function contains side effects, the double-invocation will surface them, helping you catch potential bugs that could otherwise lead to inconsistent state or unexpected behavior in production [4][6][7]. Impurity and side effects: Any code that modifies variables outside the function scope, performs API calls, or generates random values during the render or update phase is considered impure [5][6][8]. If your updater function performs these actions, it will likely produce incorrect results (e.g., doubling an increment or generating two different IDs) when invoked twice [6][9][10]. Handling crypto.randomUUID(): You should not call crypto.randomUUID() directly inside a state updater function or render body [8]. Because these functions are expected to be pure, calling a non-deterministic function like randomUUID() will result in different values being produced each time they are invoked [8]. To use a unique ID safely: 1. Generate the ID inside an event handler (e.g., when a user clicks a button to add an item) [5][6][11]. 2. If the ID must be generated upon component initialization, use lazy state initialization: useState(() => crypto.randomUUID()) [8]. 3. By moving the generation of the UUID outside of the pure update phase, you ensure the value remains stable and predictable [8][11]. Summary for developers: The double-invocation is not a bug; it is a test harness [7]. If your code fails when run twice, it indicates a lack of idempotency [3][7]. Ensure your state updaters are pure, move side effects to event handlers or useEffect (with proper cleanup), and avoid non-deterministic operations like randomUUID() inside your render or update logic [5][6][8][7].

Citations:


Move tab creation side effects out of the setTabs updater setActiveTabId, setExplorerStates, and setSelectionStates make this callback impure; in Strict Mode it can run twice and leave per-tab state behind for a discarded UUID. Generate the tab ID first, then perform the other state updates outside setTabs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/interface/src/components/TabManager/TabManagerContext.tsx` around
lines 337 - 370, The tab creation logic inside the setTabs updater is impure
because it also calls setActiveTabId, setExplorerStates, and setSelectionStates,
which can run twice in Strict Mode and create orphaned per-tab state. In
TabManagerContext’s tab creation flow, generate the new tab ID and newTab object
first, then move the active-tab update plus the explorer/selection state
initialization outside the setTabs callback so the updater only returns the next
tabs array.

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