← Corpus / cite-wide / workflow

cite-wide/workflow/2026-05-01_refactor

Path
workflow/2026-05-01_Refactor.md

What we’d port into our refactored code

A self-contained list:

  1. CiteWideSettings — add autoSaveUrlCitations: boolean (recommend default false per your framing), plus the toggle in the settings tab.
  2. citationFileService — add public saveAllHexCitationsFromContent(content, sourceFile?). His version uses await import(’./citationService’) to dodge a circular dep — I’d check if the cycle is real and fix it properly with a static import or a refactor, instead of dynamic-importing.
  3. main.ts — gate the citation-file creation in extractAndInsertCitation behind this.settings.autoSaveUrlCitations. Register the new save-all-hex-citations command.
  4. CitationModal.ts — header button “Save All Hex (N)” when hex count > 0; per-row “Save to Citations” button on hex groups; conditionally show “Convert All” only when numeric count > 0 (his improvement, worth keeping).
  5. Skip his any types — the audit work we just did would otherwise come undone. Use CitationGroup / CitationMatch types from the existing exports.

The eighth 8a9f3c6 polish commit — I’d skip; we can do our own polish later if needed.

The override path

Two clean options. Both keep our linear history.

Recommended: port his intent first, then force-push.

(1) implement the port — service, settings, modal, command (separate commit)

(2) verify build clean: pnpm build

(3) verify in Obsidian

(4) then:

git push —force-with-lease origin development

The —force-with-lease variant is safer than —force — it only succeeds if the remote ref hasn’t moved since our last fetch. So if Tanuj happens to push something between now and our push, we get a clean rejection instead of clobbering him silently.

This sequence means: at no point is the remote in a state where a real feature is missing. We replace his commits with a refactored version that does the same thing better.

Alternative: force-push first, port after. Faster, but the remote is briefly in a state where the canonical-archive feature is gone (until we port it back). For an active branch that anyone else pulls from, this is worse.

Couple of clarifying questions before I touch anything

  1. autoSaveUrlCitations default — false or true? Your “some, not all” framing implies false (curation-by-default). Tanuj kept true for backward compatibility. I’d flip it to false, but it’s your call.
  2. The —force-with-lease push — do you want me to do it, or do you handle pushes manually? I will not run any force-push without you saying so explicitly.
  3. Bigger “bases” question — do you mean Obsidian’s new Bases feature (the structured database-like view over markdown frontmatter, currently in catalyst) specifically, or “bases” loosely as “a foundational canonical store”? The answer changes whether we should be reshaping the citation file’s frontmatter schema for Bases-compatibility now, or just porting Tanuj’s intent and leaving schema work for later.

Tanuj’s CSS additions (full source)

Tanuj created a new readable source file at src/styles/citations.css and the minified version sits in styles.css at the project root. The minified one is byte-for-byte equivalent to the readable source — just whitespace-stripped. For reviewing the intent, only the readable source matters.

New selectors he added

These did not exist in the pre-refactor styles.css we forked from:

SelectorPurpose
.cite-wide-header-buttonsFlex column container for stacking multiple header buttons (Convert All + Save All Hex)
.cite-wide-save-all-hex-btn (+ :hover)Green “Save All Hex” button at the top of the modal
.cite-wide-save-btn (+ :hover)Green “Save to Citations” per-group button
.cite-wide-citation-idMonospace badge showing the hex/numeric ID at the top of each group
.cite-wide-citation-previewTruncated single-line preview of the citation text in the group header
.cite-wide-badge-hexAccent-colored small badge specifically for hex citations
.cite-wide-citation-text-section (+ h4)Highlighted panel inside an expanded group showing the full citation text
.cite-wide-citation-textThe citation text inside that panel
.cite-wide-separator<hr> between citation text and instances list inside a group
.cite-wide-instances-section h4Section header above the per-instance list

He also restyled almost every pre-existing selector — bigger padding, larger typography, hover transforms (translateY(-1px) + box-shadow), rounded corners.

Full source — src/styles/citations.css from origin/development

/* cite-wide/src/styles/citations.css */

.cite-wide-modal {
    padding: 2rem;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
    margin: 0 auto;
    overflow-x: hidden; /* Prevent horizontal scrolling */
    line-height: 1.6; /* Improve overall readability */
}

.cite-wide-title {
    margin-top: 0;
    margin-bottom: 1rem;
    padding-bottom: 1rem;
    border-bottom: 2px solid var(--background-modifier-border);
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--text-normal);
}

.cite-wide-container {
    margin-top: 1.5rem;
    width: 100%;
    box-sizing: border-box;
    overflow-x: hidden; /* Prevent horizontal scrolling */
}

.cite-wide-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1.5rem;
    margin-bottom: 2rem;
    padding-bottom: 1rem;
    border-bottom: 2px solid var(--background-modifier-border);
    width: 100%;
}

.cite-wide-title {
    margin: 0;
    flex-shrink: 0;
    font-size: 1.5rem;
    font-weight: 600;
}

.cite-wide-convert-all-btn {
    white-space: nowrap;
    padding: 0.75rem 1.25rem;
    font-size: 0.9rem;
    font-weight: 500;
}

.cite-wide-header-buttons {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    align-items: flex-end;
}

.cite-wide-save-all-hex-btn {
    background-color: var(--interactive-success);
    color: var(--text-on-accent);
    border: 1px solid var(--interactive-success-hover);
    white-space: nowrap;
    padding: 0.75rem 1.25rem;
    font-size: 0.9rem;
    font-weight: 500;
}

.cite-wide-save-all-hex-btn:hover {
    background-color: var(--interactive-success-hover);
}

.cite-wide-group {
    margin-bottom: 2rem;
    border: 1px solid var(--background-modifier-border);
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    transition: box-shadow 0.2s ease;
}

.cite-wide-group:hover {
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}

.cite-wide-group-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.25rem;
    background-color: var(--background-secondary);
    cursor: pointer;
    transition: background-color 0.2s;
    min-width: 0; /* Allow container to shrink */
    overflow: hidden; /* Prevent content from spilling out */
}

.cite-wide-group-header:hover {
    background-color: var(--background-modifier-hover);
}

.cite-wide-group-header-content {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex: 1;
    min-width: 0; /* Allow flex items to shrink */
}

.cite-wide-citation-id {
    font-weight: 700;
    color: var(--text-accent);
    font-family: var(--font-monospace);
    flex-shrink: 0;
    font-size: 1rem;
    padding: 0.25rem 0.5rem;
    background-color: var(--background-primary);
    border-radius: 4px;
    border: 1px solid var(--background-modifier-border);
}

.cite-wide-citation-preview {
    color: var(--text-muted);
    font-size: 0.95rem;
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: 1.4;
}

.cite-wide-group-title {
    margin: 0;
    font-size: 1.1rem;
    font-weight: 600;
}

.cite-wide-source-link {
    font-size: 0.9rem;
    opacity: 0.8;
    text-decoration: none;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    transition: background-color 0.2s;
}

.cite-wide-source-link:hover {
    background-color: var(--background-modifier-hover);
    opacity: 1;
}

.cite-wide-group-content {
    padding: 1.25rem;
    background-color: var(--background-primary);
    border-top: 1px solid var(--background-modifier-border);
}

/* Base instance styling */
.cite-wide-instance {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--background-modifier-border);
    margin-bottom: 0.5rem;
}

.cite-wide-instance:last-child {
    border-bottom: none;
    margin-bottom: 0;
}

/* Reference source specific styling */
.cite-wide-instance.cite-wide-reference-source {
    background-color: var(--background-secondary-alt);
    margin: 0 -1.25rem 1rem -1.25rem;
    padding: 1rem 1.25rem;
    border-radius: 6px;
    border: 1px solid var(--background-modifier-border);
    border-left: 4px solid var(--interactive-accent);
}

.cite-wide-line-info {
    flex: 1;
    font-family: var(--font-monospace);
    min-width: 0; /* Allow text to shrink */
    overflow: hidden; /* Prevent text from spilling out */
    font-size: 0.9rem;
    white-space: nowrap;
    text-overflow: ellipsis;
    line-height: 1.5;
}

.cite-wide-line-number {
    color: var(--text-muted);
    margin-right: 0.75rem;
    font-weight: 500;
}

.cite-wide-line-preview {
    opacity: 0.9;
    color: var(--text-normal);
}

/* Badge styling */
.cite-wide-badge {
    display: inline-block;
    padding: 0.2em 0.6em;
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.3;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    border-radius: 4px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.cite-wide-badge-reference {
    color: var(--text-normal);
    background-color: var(--background-modifier-border);
}

.cite-wide-badge-hex {
    color: var(--text-on-accent);
    background-color: var(--interactive-accent);
    font-size: 0.7em;
    padding: 0.15em 0.5em;
}

/* Adjust line info spacing when badge is present */
.cite-wide-line-info > .cite-wide-badge + span {
    margin-left: 0.75rem;
}

.cite-wide-instance-actions {
    display: flex;
    gap: 0.75rem;
    margin-left: 1rem;
    flex-shrink: 0; /* Prevent buttons from shrinking */
}

.cite-wide-convert-btn,
.cite-wide-save-btn,
.cite-wide-view-btn {
    white-space: nowrap;
    padding: 0.5rem 1rem;
    font-size: 0.85rem;
    line-height: 1.4;
    border-radius: 6px;
    transition: all 0.2s ease;
    font-weight: 500;
}

.cite-wide-convert-btn {
    background-color: var(--interactive-accent);
    color: var(--text-on-accent);
    border: 1px solid var(--interactive-accent-hover);
    flex-shrink: 0;
}

.cite-wide-convert-btn:hover {
    background-color: var(--interactive-accent-hover);
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.cite-wide-save-btn {
    background-color: var(--interactive-success);
    color: var(--text-on-accent);
    border: 1px solid var(--interactive-success-hover);
    flex-shrink: 0;
}

.cite-wide-save-btn:hover {
    background-color: var(--interactive-success-hover);
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.cite-wide-view-btn {
    background-color: var(--background-secondary);
    border: 1px solid var(--background-modifier-border);
}

.cite-wide-view-btn:hover {
    background-color: var(--background-modifier-hover);
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.cite-wide-footer {
    margin-top: 2rem;
    padding-top: 1.5rem;
    border-top: 2px solid var(--background-modifier-border);
    display: flex;
    justify-content: flex-end;
}

/* Ribbon Icon Styles */
.workspace-ribbon .side-dock-ribbon-action.cite-wide-ribbon-icon {
    color: var(--text-muted);
    transition: color 0.2s ease;
}

.workspace-ribbon .side-dock-ribbon-action.cite-wide-ribbon-icon:hover {
    color: var(--text-normal);
    background-color: var(--background-modifier-hover);
}

.workspace-ribbon .side-dock-ribbon-action.cite-wide-ribbon-icon.is-active {
    color: var(--interactive-accent);
}

/* Citation Text Section Styles */
.cite-wide-citation-text-section {
    margin-bottom: 1.5rem;
    padding: 1rem;
    background-color: var(--background-secondary);
    border-radius: 6px;
    border-left: 4px solid var(--interactive-accent);
}

.cite-wide-citation-text-section h4 {
    margin: 0 0 0.75rem 0;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.cite-wide-citation-text {
    font-size: 0.95rem;
    line-height: 1.6;
    color: var(--text-normal);
    word-wrap: break-word;
    white-space: pre-wrap;
    padding: 0.5rem 0;
}

.cite-wide-separator {
    margin: 1.5rem 0;
    border: none;
    border-top: 2px solid var(--background-modifier-border);
}

.cite-wide-instances-section h4 {
    margin: 0 0 1rem 0;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

Tanuj’s CitationModal (he didn’t write a new modal — he modified the existing one)

He did not introduce a new modal class. He extended src/modals/CitationModal.ts with:

Behavioral additions:

  • Top-of-modal “Save All Hex (N)” button when there are hex citations in the file (sibling to the “Convert All” button which is now itself only shown when there are numeric citations to convert).
  • Per-group “Save to Citations” button on every hex group (mutually exclusive with the existing “Convert to Hex” button — that one only shows on numeric groups now).
  • Group header now shows a citation-ID badge plus a truncated preview of the citation text.
  • Expanded group content now shows the full citation text in a callout panel above the per-instance list.
  • Per-instance “Convert” buttons no longer appear on hex citations (they shouldn’t — they’re already converted).

Wiring:

  • Imports citationFileService and uses it for both the per-group save (saveHexCitationToFile) and bulk save (saveAllHexCitations calling the service’s new saveAllHexCitationsFromContent).

Type-safety violations to scrub when porting:

  • contentEl.closest('.modal-container') as HTMLElement — same pattern we already had to deal with; replace with instanceof HTMLElement narrowing.
  • A setSelection import is missing on his TFile import (TFile is imported, fine; this is more of a note that no other types beyond those already in his import line are needed).
  • His helpers don’t use any directly in this file (those anys are in his citationFileService additions). The modal itself is reasonably typed.

Full source — src/modals/CitationModal.ts from origin/development

// cite-wide/src/modals/CitationModal.ts
import { App, Modal, Notice, Editor, TFile } from 'obsidian';
import { citationService } from '../services/citationService';
import type { CitationGroup, CitationMatch } from '../services/citationService';
import { citationFileService } from '../services/citationFileService';

export class CitationModal extends Modal {
    private editor: Editor;
    private content: string;
    private citationGroups: CitationGroup[] = [];

    constructor(app: App, editor: Editor) {
        super(app);
        this.editor = editor;
        this.content = editor.getValue();
    }

    async onOpen() {
        const { contentEl } = this;
        contentEl.empty();

        // Make the modal wider
        const modalContainer = contentEl.closest('.modal-container') as HTMLElement;
        const modalContent = contentEl.closest('.modal-content') as HTMLElement;

        if (modalContainer && modalContent) {
            // Set the modal container to be very wide
            modalContainer.style.width = '95vw';
            modalContainer.style.maxWidth = 'none';

            // Ensure the content takes full width
            modalContent.style.width = '100%';
            modalContent.style.maxWidth = 'none';
        }

        contentEl.addClass('cite-wide-modal');

        // Extract all citation groups
        this.citationGroups = citationService.extractCitations(this.content);

        if (this.citationGroups.length === 0) {
            contentEl.createEl('p', {
                text: 'No citations found in the current document.'
            });
            return;
        }

        // Create a container for citation groups
        const container = contentEl.createDiv('cite-wide-container');

        // Create header with title and convert all button
        const header = container.createDiv('cite-wide-header');

        // Add title on the left
        header.createEl('h2', {
            text: 'Citations in Document',
            cls: 'cite-wide-title'
        });

        // Count numeric and hex citations
        const numericCitationCount = this.citationGroups.filter(group => !group.number.startsWith('hex_')).length;
        const hexCitationCount = this.citationGroups.filter(group => group.number.startsWith('hex_')).length;

        // Create button container for multiple buttons
        const buttonContainer = header.createDiv('cite-wide-header-buttons');

        // Add convert all button (only if there are numeric citations)
        if (numericCitationCount > 0) {
            const convertAllBtn = buttonContainer.createEl('button', {
                text: `Convert All (${numericCitationCount})`,
                cls: 'mod-cta cite-wide-convert-all-btn'
            });
            convertAllBtn.addEventListener('click', () => this.convertAllCitations());
        }

        // Add save all hex citations button (only if there are hex citations)
        if (hexCitationCount > 0) {
            const saveAllHexBtn = buttonContainer.createEl('button', {
                text: `Save All Hex (${hexCitationCount})`,
                cls: 'mod-cta cite-wide-save-all-hex-btn'
            });
            saveAllHexBtn.addEventListener('click', () => this.saveAllHexCitations());
        }

        // Add each citation group
        for (const group of this.citationGroups) {
            this.renderCitationGroup(container, group);
        }
    }

    private renderCitationGroup(container: HTMLElement, group: CitationGroup) {
        const groupEl = container.createDiv('cite-wide-group');
        const header = groupEl.createDiv('cite-wide-group-header');

        // Create a collapsible header
        const headerContent = header.createDiv('cite-wide-group-header-content');

        // Add citation number/ID display
        headerContent.createEl('span', {
            text: group.number.startsWith('hex_') ? `[^${group.number.replace('hex_', '')}]` : `[${group.number}]`,
            cls: 'cite-wide-citation-id'
        });

        // Add citation text preview in header
        const referenceMatch = group.matches.find(match => match.isReferenceSource);
        if (referenceMatch) {
            const citationText = referenceMatch.lineContent.replace(/^\s*\[[^\]]+\]:\s*/, '').trim();
            const previewText = citationText.length > 80
                ? `${citationText.substring(0, 80)}...`
                : citationText;

            headerContent.createEl('span', {
                text: previewText,
                cls: 'cite-wide-citation-preview'
            });
        }

        if (group.url) {
            headerContent.createEl('a', {
                href: group.url,
                text: 'Source',
                cls: 'cite-wide-source-link',
                attr: { target: '_blank' }
            });
        }

        // Add convert button only for numeric citations (not hex citations)
        if (!group.number.startsWith('hex_')) {
            const convertBtn = header.createEl('button', {
                text: 'Convert to Hex',
                cls: 'mod-cta cite-wide-convert-btn'
            });

            convertBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this.convertCitationGroup(group);
            });
        } else {
            // Add save button for hex citations
            const saveBtn = header.createEl('button', {
                text: 'Save to Citations',
                cls: 'mod-cta cite-wide-save-btn'
            });

            saveBtn.addEventListener('click', async (e) => {
                e.stopPropagation();
                const result = await this.saveHexCitationToFile(group);
                if (result === 'saved') {
                    new Notice(`Citation saved to Citations folder: ${group.number.replace('hex_', '')}.md`);
                } else if (result === 'updated') {
                    new Notice(`Citation ${group.number.replace('hex_', '')} already exists and usage updated`);
                } else if (result === 'error') {
                    new Notice('Failed to save citation file');
                }
            });
        }

        // Create collapsible content
        const content = groupEl.createDiv('cite-wide-group-content');
        content.style.display = 'none'; // Start collapsed

        // Toggle content on header click
        header.addEventListener('click', () => {
            content.style.display = content.style.display === 'none' ? 'block' : 'none';
        });

        // Add full citation text section
        if (referenceMatch) {
            const citationTextSection = content.createDiv('cite-wide-citation-text-section');
            citationTextSection.createEl('h4', { text: 'Citation Text' });

            const citationText = referenceMatch.lineContent.replace(/^\s*\[[^\]]+\]:\s*/, '').trim();
            citationTextSection.createEl('div', {
                text: citationText,
                cls: 'cite-wide-citation-text'
            });

            // Add a separator
            content.createEl('hr', { cls: 'cite-wide-separator' });
        }

        // Add section for citation instances
        const instancesSection = content.createDiv('cite-wide-instances-section');
        instancesSection.createEl('h4', { text: 'Citation Instances' });

        // Add each citation instance
        group.matches.forEach((match: CitationMatch, matchIndex: number) => {
            const isRefSource = match.isReferenceSource === true;
            const instanceEl = instancesSection.createDiv(`cite-wide-instance ${isRefSource ? 'cite-wide-reference-source' : ''}`);

            // Show line number and preview
            const lineInfo = instanceEl.createDiv('cite-wide-line-info');

            // Add a special badge for reference sources
            if (isRefSource) {
                lineInfo.createEl('span', {
                    text: 'Reference',
                    cls: 'cite-wide-badge cite-wide-badge-reference'
                });
                lineInfo.createEl('span', { text: ' • ' });
            }

            lineInfo.createEl('span', {
                text: `Line ${match.lineNumber}: `,
                cls: 'cite-wide-line-number'
            });

            // Create a preview of the line content
            const preview = match.lineContent.trim();
            const previewText = preview.length > 100
                ? `${preview.substring(0, 100)}...`
                : preview;

            lineInfo.createEl('span', {
                text: previewText,
                cls: 'cite-wide-line-preview'
            });

            // Add view and convert buttons
            const btnContainer = instanceEl.createDiv('cite-wide-instance-actions');

            // Only add view/convert buttons for non-reference entries
            if (!isRefSource) {
                const viewBtn = btnContainer.createEl('button', {
                    text: 'View',
                    cls: 'mod-cta-outline cite-wide-view-btn'
                });

                viewBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    this.scrollToLine(match.lineNumber);
                });
            }

            // Only add convert button for non-reference entries and numeric citations (not hex citations)
            if (!isRefSource && !group.number.startsWith('hex_')) {
                const convertBtn = btnContainer.createEl('button', {
                    text: 'Convert',
                    cls: 'mod-cta cite-wide-convert-btn'
                });

                convertBtn.addEventListener('click', async (e) => {
                    e.stopPropagation();
                    await this.convertCitationInstance(group, matchIndex);
                });
            }
        });
    }

    private async convertCitationGroup(group: CitationGroup) {
        try {
            const result = citationService.convertCitation(
                this.content,
                group.number
            );

            if (result.changed) {
                await this.saveChanges(result.content);
                new Notice(`Converted citation [${group.number}] to hex format`);
                this.close();
            } else {
                new Notice('No changes were made to the document');
            }
        } catch (error) {
            console.error('Error converting citation:', error);
            new Notice('Error converting citation. See console for details.');
        }
    }

    private async convertCitationInstance(group: CitationGroup, _matchIndex: number) {
        try {
            // Use the same approach as convertCitationGroup to ensure footnote conversion
            const result = citationService.convertCitation(
                this.content,
                group.number
            );

            if (result.changed) {
                await this.saveChanges(result.content);

                // Update the content for future operations
                this.content = result.content;

                // Display the original citation format in the notice
                const displayNumber = group.number.startsWith('hex_')
                    ? `^${group.number.replace('hex_', '')}`
                    : group.number;

                new Notice(`Converted citation [${displayNumber}] to hex format`);
                this.close();
            } else {
                new Notice('No changes were made to the document');
            }
        } catch (error) {
            console.error('Error converting citation instance:', error);
            new Notice('Error converting citation. See console for details.');
        }
    }

    private async convertAllCitations() {
        try {
            let updatedContent = this.content;
            let totalConverted = 0;

            // Process each group, but only numeric citations (not hex citations)
            for (const group of this.citationGroups) {
                // Skip hex citations that are already converted
                if (group.number.startsWith('hex_')) {
                    continue;
                }

                const result = citationService.convertCitation(
                    updatedContent,
                    group.number
                );

                if (result.changed) {
                    updatedContent = result.content;
                    totalConverted += result.stats.citationsConverted;
                }
            }

            if (totalConverted > 0) {
                await this.saveChanges(updatedContent);
                new Notice(`Converted ${totalConverted} citations to hex format`);
                this.close();
            } else {
                new Notice('No citations were converted');
            }
        } catch (error) {
            console.error('Error converting citations:', error);
            new Notice('Error converting citations. See console for details.');
        }
    }

    private async saveAllHexCitations() {
        try {
            // Get the current file path for tracking
            const activeFile = this.app.workspace.getActiveFile();
            const sourceFile = activeFile ? activeFile.path : '';

            // Use the shared service method
            const result = await citationFileService.saveAllHexCitationsFromContent(this.content, sourceFile);

            if (result.saved > 0 || result.updated > 0) {
                let message = '';
                if (result.saved > 0) {
                    message += `Saved ${result.saved} new citation(s)`;
                }
                if (result.updated > 0) {
                    if (message) message += ', ';
                    message += `Updated ${result.updated} existing citation(s)`;
                }
                new Notice(message);
            } else {
                new Notice('No hex citations were saved');
            }
        } catch (error) {
            console.error('Error saving hex citations:', error);
            new Notice('Error saving hex citations. See console for details.');
        }
    }

    private async saveChanges(newContent: string) {
        const activeFile = this.app.workspace.getActiveFile();
        if (!activeFile) {
            throw new Error('No active file');
        }

        await this.app.vault.modify(activeFile, newContent);
    }

    private scrollToLine(lineNumber: number) {
        try {
            // Get the line content
            const lineContent = this.editor.getLine(lineNumber);

            // Find the citation pattern in the line (matches [1], [2], etc.)
            const citationMatch = lineContent.match(/\[(\d+)\]/);

            if (citationMatch) {
                const citationText = citationMatch[0];
                const startPos = citationMatch.index || 0;
                const endPos = startPos + citationText.length;

                // Create positions for the citation
                const from = { line: lineNumber, ch: startPos };
                const to = { line: lineNumber, ch: endPos };

                // Set cursor to the start of the citation and select it
                this.editor.setCursor(from);
                this.editor.setSelection(from, to);

                // Scroll to make the citation visible with some context
                const fromLine = Math.max(0, lineNumber - 2);
                const toLine = lineNumber + 2;

                // Create a range for the context area
                const contextRange = {
                    from: { line: fromLine, ch: 0 },
                    to: { line: toLine, ch: 0 }
                };

                // Scroll to show the context area
                this.editor.scrollIntoView(contextRange, true);

                // Then scroll to show the selection
                this.editor.scrollIntoView({ from, to }, true);

                // Focus the editor to show the selection
                this.editor.focus();

            } else {
                // Fallback to just scrolling to the line if no citation pattern is found
                const pos = { line: lineNumber, ch: 0 };
                this.editor.setCursor(pos);

                // Create a range for the context area
                const contextRange = {
                    from: { line: Math.max(0, lineNumber - 2), ch: 0 },
                    to: { line: lineNumber + 2, ch: 0 }
                };

                this.editor.scrollIntoView(contextRange, true);
                this.editor.focus();
            }

            // Close the modal after a short delay to ensure the selection is visible
            setTimeout(() => {
                this.close();
            }, 100);

        } catch (error) {
            console.error('Error scrolling to line:', error);
            this.close();
        }
    }

    private async saveHexCitationToFile(group: CitationGroup): Promise<'saved' | 'updated' | 'error' | null> {
        try {
            // Extract the hex ID from the group number
            const hexId = group.number.replace('hex_', '');

            // Check if citation file already exists
            const filename = `${hexId}.md`;
            const filepath = `${citationFileService.getCitationsFolder()}/${filename}`;
            const existingFile = this.app.vault.getAbstractFileByPath(filepath);

            if (existingFile instanceof TFile) {
                // File already exists - just update usage
                const activeFile = this.app.workspace.getActiveFile();
                const sourceFile = activeFile ? activeFile.path : '';
                await citationFileService.updateCitationUsage(existingFile, sourceFile);
                return 'updated';
            }

            // Find the reference source (the actual citation text)
            const referenceMatch = group.matches.find(match => match.isReferenceSource);

            if (!referenceMatch) {
                return 'error';
            }

            // Extract the reference text (everything after the [^hexId]: part)
            const referenceText = referenceMatch.lineContent.replace(/^\s*\[\^[a-z0-9]+\]:\s*/, '').trim();

            // Extract URL from the reference text if it exists
            let url: string | undefined;
            const urlMatch = referenceText.match(/https?:\/\/[^\s\)]+/);
            if (urlMatch) {
                url = urlMatch[0];
            }

            // Get the current file path for tracking
            const activeFile = this.app.workspace.getActiveFile();
            const sourceFile = activeFile ? activeFile.path : '';

            // Create the citation file
            const result = await citationFileService.createCitationFile(
                hexId,
                referenceText,
                url,
                sourceFile
            );

            if (result) {
                return 'saved';
            } else {
                return 'error';
            }

        } catch (error) {
            console.error('Error saving hex citation to file:', error);
            return 'error';
        }
    }

    onClose() {
        const { contentEl } = this;
        contentEl.empty();
    }
}

Tanuj’s citationFileService additions (just the new methods, our refactor would replace the rest)

These are the only two methods in citationFileService.ts that he added (everything else in his version of the file is the pre-refactor structure that our 17193a2 rewrite already replaced):

/**
 * Save all hex citations from a document to citation files
 * This is a shared method that can be used by both the modal and commands
 */
public async saveAllHexCitationsFromContent(
    content: string,
    sourceFile?: string
): Promise<{ saved: number; updated: number; errors: number }> {
    try {
        // Import citation service dynamically to avoid circular dependencies
        const { citationService } = await import('./citationService');

        // Extract all citations from the content
        const citationGroups = citationService.extractCitations(content);

        let saved = 0;
        let updated = 0;
        let errors = 0;

        // Process each hex citation group
        for (const group of citationGroups) {
            if (!group.number.startsWith('hex_')) {
                continue; // Skip non-hex citations
            }

            const result = await this.saveHexCitationFromGroup(group, sourceFile);
            if (result === 'saved') {
                saved++;
            } else if (result === 'updated') {
                updated++;
            } else {
                errors++;
            }
        }

        return { saved, updated, errors };
    } catch (error) {
        console.error('Error saving all hex citations:', error);
        throw error;
    }
}

/**
 * Save a single hex citation from a citation group
 * This is a shared method that can be used by both the modal and commands
 */
private async saveHexCitationFromGroup(
    group: any,                    // ⚠️ TYPE-SAFETY VIOLATION — should be CitationGroup
    sourceFile?: string
): Promise<'saved' | 'updated' | 'error'> {
    try {
        // Extract the hex ID from the group number
        const hexId = group.number.replace('hex_', '');

        // Check if citation file already exists
        const filename = `${hexId}.md`;
        const filepath = `${this.citationsFolder}/${filename}`;
        const existingFile = this.app.vault.getAbstractFileByPath(filepath);

        if (existingFile instanceof TFile) {
            // File already exists - just update usage
            await this.updateCitationUsage(existingFile, sourceFile);
            return 'updated';
        }

        // Find the reference source (the actual citation text)
        const referenceMatch = group.matches.find(
            (match: any) => match.isReferenceSource     // ⚠️ TYPE-SAFETY VIOLATION — should be CitationMatch
        );

        if (!referenceMatch) {
            return 'error';
        }

        // Extract the reference text (everything after the [^hexId]: part)
        const referenceText = referenceMatch.lineContent.replace(/^\s*\[\^[a-z0-9]+\]:\s*/, '').trim();

        // Extract URL from the reference text if it exists
        let url: string | undefined;
        const urlMatch = referenceText.match(/https?:\/\/[^\s\)]+/);
        if (urlMatch) {
            url = urlMatch[0];
        }

        // Create the citation file
        const result = await this.createCitationFile(hexId, referenceText, url, sourceFile);

        if (result) {
            return 'saved';
        } else {
            return 'error';
        }

    } catch (error) {
        console.error('Error saving hex citation from group:', error);
        return 'error';
    }
}

Tanuj’s settings + auto-save gating

src/settings/CiteWideSettings.ts — added one field and one toggle:

export interface CiteWideSettings {
    jinaApiKey: string;
    citationsFolder: string;
    autoSaveUrlCitations: boolean;        // NEW
}

export const DEFAULT_SETTINGS: CiteWideSettings = {
    jinaApiKey: '',
    citationsFolder: 'Citations',
    autoSaveUrlCitations: true            // NEW — Tanuj kept default ON for backward compat
};

// Inside CiteWideSettingTab.display(), added:
new Setting(containerEl)
    .setName('Auto-save URL Citations')
    .setDesc('Automatically save citations extracted from URLs as citation files. When disabled, citations will only be added to the document without creating separate files.')
    .addToggle(toggle => toggle
        .setValue(this.plugin.settings.autoSaveUrlCitations)
        .onChange(async (value) => {
            this.plugin.settings.autoSaveUrlCitations = value;
            await this.plugin.saveSettings();
        }));

main.ts — gated the auto-save inside extractAndInsertCitation:

// Replace the URL with the full formatted citation
editor.replaceSelection(result.citation);

// Create citation file for Dataview integration only if auto-save is enabled
if (this.settings.autoSaveUrlCitations) {
    const activeFile = this.app.workspace.getActiveFile();
    const sourceFile = activeFile ? activeFile.path : '';
    if (result.citationData) {
        await citationFileService.createCitationFileWithData(
            result.hexId,
            result.citationData,
            sourceFile
        );
    } else {
        await citationFileService.createCitationFile(
            result.hexId,
            result.citation,
            url,
            sourceFile
        );
    }
    new Notice(`Citation extracted and saved: ${result.hexId}`);
} else {
    new Notice(`Citation extracted successfully: ${result.hexId} (not saved to file)`);
}

main.ts — registered new command:

// Command to save all hex citations to citation files
this.addCommand({
    id: 'save-all-hex-citations',
    name: 'Save All Hex Citations to Citation Files',
    editorCallback: async (editor: Editor) => {
        try {
            await this.saveAllHexCitations(editor);
        } catch (error) {
            const errorMsg = error instanceof Error ? error.message : String(error);
            new Notice('Error saving hex citations: ' + errorMsg);
        }
    }
});

// And the helper method that command calls:
private async saveAllHexCitations(editor: Editor): Promise<void> {
    const content = editor.getValue();
    const activeFile = this.app.workspace.getActiveFile();
    const sourceFile = activeFile ? activeFile.path : '';

    const result = await citationFileService.saveAllHexCitationsFromContent(content, sourceFile);

    if (result.saved > 0 || result.updated > 0) {
        let message = '';
        if (result.saved > 0) {
            message += `Saved ${result.saved} new citation(s)`;
        }
        if (result.updated > 0) {
            if (message) message += ', ';
            message += `Updated ${result.updated} existing citation(s)`;
        }
        new Notice(message);
    } else {
        new Notice('No hex citations were saved');
    }
}