| name | workspace-tabs |
| description | Guide for working with workspace tabs, tab management, and duplicate tab prevention. Use when fixing tab bugs, adding tab features, or working with openTab/addNewTab functions. |
Workspace Tabs
This skill covers the workspace tab system in Nomendex, including how tabs are created, managed, and how duplicate detection works.
Key Files
src/hooks/useWorkspace.tsx - Core tab management logic
src/components/NotesCommandMenu.tsx - CMD+P quick file opener
src/contexts/WorkspaceContext.tsx - Provides workspace functions to components
src/types/Workspace.ts - Tab and workspace type definitions
Tab Creation Functions
addNewTab() - Always Creates New Tab
addNewTab({ pluginMeta, view, props })
- Always creates a new tab, even if identical tab exists
- Does NOT set the new tab as active
- Use only when you explicitly want duplicates allowed
openTab() - Smart Tab Opening (Preferred)
openTab({ pluginMeta, view, props })
- Checks for existing matching tab first
- If match found: focuses existing tab (no duplicate)
- If no match: creates new tab and sets it active
- Always use this unless you have a specific reason for duplicates
Duplicate Detection Logic
openTab() matches tabs based on:
- Plugin ID - Must match (e.g., "notes", "todos", "chat")
- View ID - Must match (e.g., "editor", "browser", "kanban")
- Props - Depends on plugin type:
| Plugin | Props Matched |
|---|
| notes | noteFileName |
| todos | project (for browser/kanban views) |
| tags | tagName |
| others | Empty props match empty props |
Critical: Stale Closure Bug
The duplicate check MUST happen inside the updateWorkspace callback to access latest state:
const openTab = useCallback(({ pluginMeta, view, props }) => {
const existingTab = workspace.tabs.find(tab => );
if (existingTab) {
updateWorkspace({ activeTabId: existingTab.id });
return existingTab;
}
}, [workspace.tabs]);
const openTab = useCallback(({ pluginMeta, view, props }) => {
let resultTab = null;
updateWorkspace((prev) => {
const existingTab = prev.tabs.find(tab => );
if (existingTab) {
resultTab = existingTab;
return { ...prev, activeTabId: existingTab.id };
}
resultTab = newTab;
return { ...prev, tabs: [...prev.tabs, newTab], activeTabId: newTab.id };
});
return resultTab;
}, [createPluginInstance, updateWorkspace]);
Common Patterns
Opening a Note from UI
const { openTab } = useWorkspaceContext();
openTab({
pluginMeta: notesPluginSerial,
view: "editor",
props: { noteFileName: "my-note.md" },
});
Opening Todos Browser
openTab({
pluginMeta: todosPluginSerial,
view: "browser",
props: { project: "work" },
});
Opening Chat
openTab({
pluginMeta: chatPluginSerial,
view: "default",
props: {},
});
Debugging Tab Issues
- Check if code uses
addNewTab vs openTab
- If using
openTab, verify the duplicate check uses prev.tabs not workspace.tabs
- Add console logs inside the
updateWorkspace callback to see actual tab state
- Check that props being matched are correct (e.g.,
noteFileName not noteId)
WorkspaceTab Structure
interface WorkspaceTab {
id: string;
title: string;
pluginInstance: {
instanceId: string;
plugin: SerializablePlugin;
instanceProps: Record<string, unknown>;
viewId: string;
};
}