Refresh or Open New Workspace Retaining Open Main Tab Group Area Markdown and Attachment Tabs (Ver.1 and Ver. 2)

Use cases

  1. You want to change to a different, often used workspace but you cannot, because you have many tabs open with files you are currently working on and you don’t want to lose those (I don’t want to talk about cursor positions of those files as there is a separate Remember Cursor Position plugin that should help you in other cases).


    Workaround: you save another (temporary) workspace to save the tabs.

  2. You just want to refresh the workspace (your main or only workspace), to get rid of the annoying excess ribbon icons or you want to add more sidebar icons of a freshly installed plugin but you can only save the workpace when all the currently open md files have been already dealt with (and can be safely closed), etc.

Plugins needed to install

  • Obsidian Advanced URI plugin
  • CodeScript Toolkit plugin

With this script you can achieve both, depending which workspace you want to add to the script (currently you can only add one workspace).

import * as obsidian from 'obsidian';

interface SavedTab {
    path: string;
    viewType: string;
}

let savedTabs: SavedTab[] = [];

// Define the file extensions we want to support
const ATTACHMENT_EXTENSIONS = ['.mp3', '.mp4', '.pdf', '.jpg', '.jpeg', '.gif', '.png'];
const CODE_EXTENSIONS = ['.txt', '.sh', '.bat', '.js', '.jsx', '.ts', '.py'];

const getOpenTabs = (app: obsidian.App): SavedTab[] => {
    const results: SavedTab[] = [];
    
    app.workspace.iterateAllLeaves(leaf => {
        // Skip sidebar leaves
        const isInSidebar = leaf.getRoot().containerEl.classList.contains('mod-left-split') || 
                           leaf.getRoot().containerEl.classList.contains('mod-right-split');
        
        if (isInSidebar) return;
        
        const viewState = leaf.getViewState();
        const viewType = viewState.type;
        
        // Handle markdown files
        if (leaf.view instanceof obsidian.MarkdownView && leaf.view.file) {
            results.push({
                path: leaf.view.file.path,
                viewType: 'markdown'
            });
        }
        // Handle attachment files
        else if (viewType === 'pdf' || viewType === 'image' || viewType === 'audio' || viewType === 'video') {
            if (viewState.state?.file) {
                const filePath = viewState.state.file;
                const fileExt = '.' + filePath.split('.').pop().toLowerCase();
                
                if (ATTACHMENT_EXTENSIONS.includes(fileExt)) {
                    console.log(`Found attachment tab: ${filePath} (${viewType})`);
                    results.push({
                        path: filePath,
                        viewType: viewType
                    });
                }
            }
        }
        // Handle code/text files
        else if (viewType === 'text' || viewType === 'code') {
            if (viewState.state?.file) {
                const filePath = viewState.state.file;
                const fileExt = '.' + filePath.split('.').pop().toLowerCase();
                
                if (CODE_EXTENSIONS.includes(fileExt)) {
                    console.log(`Found code tab: ${filePath} (${viewType})`);
                    results.push({
                        path: filePath,
                        viewType: viewType
                    });
                }
            }
        }
        // For partially loaded markdown tabs
        else if (viewType === 'markdown' && viewState.state?.file) {
            console.log(`Found partially loaded markdown tab: ${viewState.state.file}`);
            results.push({
                path: viewState.state.file,
                viewType: 'markdown'
            });
        }
    });
    
    console.log(`Found ${results.length} tabs:`, results.map(r => `${r.path} (${r.viewType})`));
    return results;
};

const reopenTabs = async (app: obsidian.App, tabs: SavedTab[]): Promise<void> => {
    if (tabs.length === 0) return;
    
    try {
        // Get the main workspace leaf
        const mainLeaf = app.workspace.getLeaf();
        
        // Open first file
        const firstTab = tabs[0];
        const firstFile = app.vault.getAbstractFileByPath(firstTab.path);
        
        if (firstFile instanceof obsidian.TFile) {
            console.log(`Opening first file: ${firstTab.path} (${firstTab.viewType})`);
            
            // Open with the correct view type if needed
            if (firstTab.viewType !== 'markdown') {
                await mainLeaf.openFile(firstFile, { viewMode: firstTab.viewType });
            } else {
                await mainLeaf.openFile(firstFile);
            }
            
            await new Promise(resolve => setTimeout(resolve, 300));
            
            // Open remaining files one by one with delays
            for (let i = 1; i < tabs.length; i++) {
                const tab = tabs[i];
                const file = app.vault.getAbstractFileByPath(tab.path);
                
                if (file instanceof obsidian.TFile) {
                    console.log(`Opening file ${i}: ${tab.path} (${tab.viewType})`);
                    const newLeaf = app.workspace.getLeaf('tab');
                    
                    // Open with the correct view type if needed
                    if (tab.viewType !== 'markdown') {
                        await newLeaf.openFile(file, { viewMode: tab.viewType });
                    } else {
                        await newLeaf.openFile(file);
                    }
                    
                    await new Promise(resolve => setTimeout(resolve, 300));
                }
            }
        }
    } catch (error) {
        console.error("Error reopening tabs:", error);
    }
};

const saveAndReopenTabs = async (app: obsidian.App): Promise<void> => {
    try {
        // Get all open tabs
        savedTabs = getOpenTabs(app);
        
        // Log the number of tabs found, but always continue with the workspace refresh
        if (savedTabs.length === 0) {
            console.log("No tabs found to save, but still proceeding with workspace refresh");
            new obsidian.Notice("No tabs found to save, refreshing workspace");
        } else {
            console.log(`Saved ${savedTabs.length} tabs before refresh`);
            new obsidian.Notice(`Saved ${savedTabs.length} tab(s)`);
        }
        
        // Execute workspace refresh command - this will now always run
        console.log("Executing workspace refresh...");
        window.open("obsidian://advanced-uri?vault=VAULT_NAME&workspace=WORKSPACE_NAME");
        
        // Wait for refresh to complete
        console.log("Waiting for refresh to complete...");
        await new Promise(resolve => setTimeout(resolve, 2000));
        
        // Only attempt to reopen tabs if we saved some
        if (savedTabs.length > 0) {
            console.log("Reopening tabs...");
            await reopenTabs(app, savedTabs);
            new obsidian.Notice(`Reopened ${savedTabs.length} tab(s)`);
        }
    } catch (error) {
        console.error("Error in saveAndReopenTabs:", error);
        new obsidian.Notice("Error: Could not save and reopen tabs");
    }
};

export class RetrieveAllTabsPlugin extends obsidian.Plugin {
    async onload() {
        this.addCommand({
            id: 'save-and-reopen-tabs',
            name: 'Save current tabs and reopen them after workspace refresh',
            callback: () => saveAndReopenTabs(this.app)
        });
    }
}

export async function invoke(app: obsidian.App): Promise<void> {
    await saveAndReopenTabs(app);
}

You will need to customize the obsidian://advanced-uri?vault=VAULT_NAME&workspace=WORKSPACE_NAME line for your own vault and the name of the workspace you have registered before.

Currently, only markdown files and attachments opened by Obsidian can be retrieved. Unfortunately, non-Obsidian files registered through plugins like VSCode Editor, Unitade, etc. are not retrieved. I didn’t dive deeper into this as 99% of the time I have md files only I am working on.

The script could be enhanced with a modal that would allow the user to select the workspace required with buttons or a drop-down, but for me this is enough.

If you want to make 2 of these scripts, you will need to rename save-and-reopen-tabs id and other name variants of these in the RetrieveAllTabsPlugin and other references.

This is a Typescript file which you will need to save with .ts extension, e.g. Retrieve-All-Tabs-After-Workspace-Refresh.ts.

A little guide on how to use these kinds of scripts:

UPD.
26-02-2025

Fixed an issue when the script didn’t execute refresh of workspace when there were no tabs open.

1 Like

On a whim, I asked Claude 3.7 if it was possible to implement this…

It did it at the first time of asking:

import * as obsidian from 'obsidian';

interface SavedTab {
    path: string;
    viewType: string;
}

let savedTabs: SavedTab[] = [];

// Define the file extensions we want to support
const ATTACHMENT_EXTENSIONS = ['.mp3', '.mp4', '.pdf', '.jpg', '.jpeg', '.gif', '.png'];
const CODE_EXTENSIONS = ['.txt', '.sh', '.bat', '.js', '.jsx', '.ts', '.py'];

/**
 * Get all available workspaces from Obsidian
 */
const getAvailableWorkspaces = (app: obsidian.App): string[] => {
    // Access the workspace plugin which contains workspace data
    const workspacePlugin = (app as any).internalPlugins.plugins.workspaces;
    
    if (!workspacePlugin || !workspacePlugin.enabled) {
        console.log("Workspace plugin not available or not enabled");
        return [];
    }
    
    // Get the instance of the workspace plugin
    const workspaceInstance = workspacePlugin.instance;
    
    if (!workspaceInstance || !workspaceInstance.workspaces) {
        console.log("Workspace instance or workspace data not available");
        return [];
    }
    
    // Extract workspace names
    const workspaceNames = Object.keys(workspaceInstance.workspaces);
    console.log("Available workspaces:", workspaceNames);
    
    return workspaceNames;
};

/**
 * Modal for selecting a workspace
 */
class WorkspaceSelectionModal extends obsidian.Modal {
    workspaces: string[];
    onSelect: (workspace: string) => void;
    
    constructor(app: obsidian.App, workspaces: string[], onSelect: (workspace: string) => void) {
        super(app);
        this.workspaces = workspaces;
        this.onSelect = onSelect;
    }
    
    onOpen() {
        const {contentEl} = this;
        
        contentEl.createEl('h2', {text: 'Select Workspace to Load'});
        
        // Create dropdown if there are many workspaces
        if (this.workspaces.length > 5) {
            const selectEl = contentEl.createEl('select', {
                cls: 'workspace-dropdown'
            });
            
            this.workspaces.forEach(workspace => {
                selectEl.createEl('option', {
                    text: workspace,
                    value: workspace
                });
            });
            
            // Button container
            const buttonContainer = contentEl.createEl('div', {
                cls: 'workspace-button-container'
            });
            
            // Add some styling to the button container
            buttonContainer.style.marginTop = '20px';
            buttonContainer.style.display = 'flex';
            buttonContainer.style.justifyContent = 'flex-end';
            
            // Create button
            const button = buttonContainer.createEl('button', {
                text: 'Load Workspace',
                cls: 'mod-cta'
            });
            
            button.addEventListener('click', () => {
                const selectedWorkspace = selectEl.value;
                this.close();
                this.onSelect(selectedWorkspace);
            });
        } 
        // Create buttons for each workspace if there are fewer workspaces
        else {
            const buttonContainer = contentEl.createEl('div', {
                cls: 'workspace-buttons'
            });
            
            // Add some styling
            buttonContainer.style.display = 'flex';
            buttonContainer.style.flexDirection = 'column';
            buttonContainer.style.gap = '10px';
            
            this.workspaces.forEach(workspace => {
                const button = buttonContainer.createEl('button', {
                    text: workspace,
                    cls: 'mod-cta'
                });
                
                button.addEventListener('click', () => {
                    this.close();
                    this.onSelect(workspace);
                });
            });
        }
    }
    
    onClose() {
        const {contentEl} = this;
        contentEl.empty();
    }
}

const getOpenTabs = (app: obsidian.App): SavedTab[] => {
    const results: SavedTab[] = [];
    
    app.workspace.iterateAllLeaves(leaf => {
        // Skip sidebar leaves
        const isInSidebar = leaf.getRoot().containerEl.classList.contains('mod-left-split') || 
                           leaf.getRoot().containerEl.classList.contains('mod-right-split');
        
        if (isInSidebar) return;
        
        const viewState = leaf.getViewState();
        const viewType = viewState.type;
        
        // Handle markdown files
        if (leaf.view instanceof obsidian.MarkdownView && leaf.view.file) {
            results.push({
                path: leaf.view.file.path,
                viewType: 'markdown'
            });
        }
        // Handle attachment files
        else if (viewType === 'pdf' || viewType === 'image' || viewType === 'audio' || viewType === 'video') {
            if (viewState.state?.file) {
                const filePath = viewState.state.file;
                const fileExt = '.' + filePath.split('.').pop().toLowerCase();
                
                if (ATTACHMENT_EXTENSIONS.includes(fileExt)) {
                    console.log(`Found attachment tab: ${filePath} (${viewType})`);
                    results.push({
                        path: filePath,
                        viewType: viewType
                    });
                }
            }
        }
        // Handle code/text files
        else if (viewType === 'text' || viewType === 'code') {
            if (viewState.state?.file) {
                const filePath = viewState.state.file;
                const fileExt = '.' + filePath.split('.').pop().toLowerCase();
                
                if (CODE_EXTENSIONS.includes(fileExt)) {
                    console.log(`Found code tab: ${filePath} (${viewType})`);
                    results.push({
                        path: filePath,
                        viewType: viewType
                    });
                }
            }
        }
        // For partially loaded markdown tabs
        else if (viewType === 'markdown' && viewState.state?.file) {
            console.log(`Found partially loaded markdown tab: ${viewState.state.file}`);
            results.push({
                path: viewState.state.file,
                viewType: 'markdown'
            });
        }
    });
    
    console.log(`Found ${results.length} tabs:`, results.map(r => `${r.path} (${r.viewType})`));
    return results;
};

const reopenTabs = async (app: obsidian.App, tabs: SavedTab[]): Promise<void> => {
    if (tabs.length === 0) return;
    
    try {
        // Get the main workspace leaf for the first tab
        let mainLeaf = app.workspace.getLeaf();
        
        console.log(`Reopening all ${tabs.length} tabs at once...`);
        
        // Open first file in the current leaf
        const firstTab = tabs[0];
        const firstFile = app.vault.getAbstractFileByPath(firstTab.path);
        
        if (firstFile instanceof obsidian.TFile) {
            // Open with the correct view type if needed
            if (firstTab.viewType !== 'markdown') {
                await mainLeaf.openFile(firstFile, { viewMode: firstTab.viewType });
            } else {
                await mainLeaf.openFile(firstFile);
            }
            
            // Open remaining files in new tabs without delays
            for (let i = 1; i < tabs.length; i++) {
                const tab = tabs[i];
                const file = app.vault.getAbstractFileByPath(tab.path);
                
                if (file instanceof obsidian.TFile) {
                    const newLeaf = app.workspace.getLeaf('tab');
                    
                    // Open with the correct view type if needed
                    if (tab.viewType !== 'markdown') {
                        await newLeaf.openFile(file, { viewMode: tab.viewType });
                    } else {
                        await newLeaf.openFile(file);
                    }
                }
            }
        }
    } catch (error) {
        console.error("Error reopening tabs:", error);
    }
};

const saveAndReopenTabsInSelectedWorkspace = async (app: obsidian.App): Promise<void> => {
    try {
        // Get all open tabs
        savedTabs = getOpenTabs(app);
        
        // Log the number of tabs found
        if (savedTabs.length === 0) {
            console.log("No tabs found to save, but still proceeding with workspace selection");
            new obsidian.Notice("No tabs found to save, proceeding with workspace selection");
        } else {
            console.log(`Saved ${savedTabs.length} tabs before refresh`);
            new obsidian.Notice(`Saved ${savedTabs.length} tab(s)`);
        }
        
        // Get available workspaces
        const workspaces = getAvailableWorkspaces(app);
        
        if (workspaces.length === 0) {
            new obsidian.Notice("No workspaces found. Please save at least one workspace.");
            return;
        }
        
        // Show workspace selection modal
        new WorkspaceSelectionModal(app, workspaces, async (selectedWorkspace: string) => {
            // Execute workspace load command
            console.log(`Loading workspace: ${selectedWorkspace}`);
            new obsidian.Notice(`Loading workspace: ${selectedWorkspace}`);
            
            // Use advanced-uri plugin to load the workspace
            const vaultName = app.vault.getName();
            window.open(`obsidian://advanced-uri?vault=${vaultName}&workspace=${selectedWorkspace}`);
            
            // Wait for workspace to load
            console.log("Waiting for workspace to load...");
            await new Promise(resolve => setTimeout(resolve, 2000));
            
            // Only attempt to reopen tabs if we saved some
            if (savedTabs.length > 0) {
                console.log("Reopening tabs...");
                await reopenTabs(app, savedTabs);
                new obsidian.Notice(`Reopened ${savedTabs.length} tab(s)`);
            }
        }).open();
        
    } catch (error) {
        console.error("Error in saveAndReopenTabsInSelectedWorkspace:", error);
        new obsidian.Notice("Error: Could not save and reopen tabs");
    }
};

export class InSelectedWorkspaceRetrieveAllTabsPlugin extends obsidian.Plugin {
    async onload() {
        this.addCommand({
            id: 'in-selected-workspace-save-and-reopen-tabs',
            name: 'Save current tabs and select workspace to load',
            callback: () => saveAndReopenTabsInSelectedWorkspace(this.app)
        });
    }
}

export async function invoke(app: obsidian.App): Promise<void> {
    await saveAndReopenTabsInSelectedWorkspace(app);
}
  • Never mind the non-working code extensions that were still left in the script.

Pretty cool.
It works out of the box (once you do the steps required with the plugin mentioned), with no user changes needed in code as it was above.

You can select a saved workspace in your vault from the dropdown and the currently open tabs will be appended to the other tabs that were saved with your workspace previously.

Not sure if this is going to get broken any time when there are Obsidian API changes.
Fingers crossed no changes will happen to the Workspaces API.