Claude Message Info

Add metadata to Claude messages: index, branch, timestamp, UUID, artifact commands

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Claude Message Info
// @namespace    http://tampermonkey.net/
// @version      0.2.0
// @description  Add metadata to Claude messages: index, branch, timestamp, UUID, artifact commands
// @author       MRL
// @match        https://claude.ai/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

const LOG_PREFIX = "[Claude Message Info]:";

/**
 * Escape HTML special characters
 */
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// =============================================
// CONFIGURATION & SELECTORS
// =============================================

const CONFIG = {
    retryAttempts: 3,
    retryDelay: 500,
    debounceDelay: 300,
    streamingDebounce: 300,
    initialDelay: 1000,
    apiTimeout: 10000,
    newChatPollingInterval: 500,
    newChatMaxWaitTime: 15000,
    INITIAL_PARENT_UUID: "00000000-0000-4000-8000-000000000000",
    topMargin: 200
};

const Settings = {
    defaults: {
        graph: {
            direction: 'top-down',         // 'left-right' | 'top-down' | 'bottom-up'
            panelMode: 'push',             // 'overlay' | 'push'
            viewMode: 'tree',              // 'tree' | 'tree-fixed' | 'branches'
            nodeGrouping: 'individual',    // 'individual' | 'pairs'
            edgeModes: ['parent-child', 'siblings'],       // 'parent-child' | 'siblings'
            showNodeText: false            // Show content preview under nodes
        }
    },

    current: null,
    load() {
        const saved = GM_getValue('cmi-settings', null);
        this.current = saved ? JSON.parse(saved) : JSON.parse(JSON.stringify(this.defaults));
        return this.current;
    },
    save() {
        GM_setValue('cmi-settings', JSON.stringify(this.current));
    },
    get(module, key) {
        if (!this.current) this.load();
        return this.current[module]?.[key] ?? this.defaults[module]?.[key];
    },
    set(module, key, value) {
        if (!this.current) this.load();
        if (!this.current[module]) this.current[module] = {};
        this.current[module][key] = value;
    }
};

const SELECTORS = {
    // =============================================
    // INJECTED ELEMENTS
    // Selectors for elements that this script injects into the DOM
    // =============================================
    injected: {
        // Message metadata badges showing index, branch, timestamp
        timestampMetadata: '.claude-timestamp-metadata',

        // Artifact command badges (create/update/rewrite)
        artifactCommand: '.claude-artifact-command',

        // Tree visualization toggle button
        // treeButton: '.claude-tree-button',

        // Tree panel wrapper container ID (without # prefix)
        // treePanelWrapper: 'claude-tree-panel-wrapper'
    },

    // =============================================
    // CORE CHAT STRUCTURE
    // Main structural elements of the Claude chat interface
    // =============================================
    chat: {
        // Main chat panel container (used for MutationObserver)
        // panelContainer: '.relative.flex.h-full.min-h-0.w-full.flex-col',

        // Main layout and width constraint containers
        // maxWidthContainer: '.mx-auto.flex.size-full.max-w-3xl' // Max-width container that constrains chat content width

        // Parent container that holds all message turns
        messageListContainer: '.flex-1.flex-col.gap-3',

        // Base selector for all chat turns (contains data-test-render-count)
        turnBase: 'div[data-test-render-count]',

        // Main container that can be resized in push mode
        mainContainer: '.flex.flex-1.h-full.w-full.overflow-hidden.relative'
    },

    // =============================================
    // MESSAGE CONTAINERS
    // Selectors for message wrappers and content areas
    // Multiple fallback options for resilience
    // =============================================
    message: {
        // Message container selectors (ordered by priority)
        containers: [
            '.flex-1.flex-col.gap-3 > div[data-test-render-count]',  // Primary: direct children in chat list
            'div[data-test-render-count]',                           // Fallback: any render count wrapper
            '.message-container > div',                              // Future-proof: potential class changes
            '[role="article"]'                                       // Semantic: ARIA article elements
        ],

        // Message card/group containers
        groups: [
            '.group.relative',          // Primary: message card container
            '.message-group',           // Fallback: semantic class name
            '[data-message-group]'      // Future-proof: data attribute
        ],

        // Specific message type identifiers
        userTestId: '[data-testid="user-message"]',
        claudeResponse: '.font-claude-response',

        // Sibling/branch indicator (displays "1 / 3" for branch navigation)
        siblingInfo: 'span.self-center.shrink-0.select-none.font-small.text-text-300'
    },

    // =============================================
    // ARTIFACT ELEMENTS
    // Selectors for artifact blocks and version information
    // =============================================
    artifact: {
        // Artifact block containers
        blocks: [
            '.artifact-block-cell',     // Primary: artifact wrapper
            '[data-artifact-block]',    // Future-proof: data attribute
            '.artifact-container'       // Fallback: semantic class name
        ],

        // Artifact version information line
        versionInfo: [
            '.text-xs.line-clamp-1.text-text-400',  // Primary: version text line
            '.artifact-version',                    // Fallback: semantic class name
            '[data-artifact-version]'               // Future-proof: data attribute
        ]
    },

    // =============================================
    // CONTROL ELEMENTS
    // Navigation buttons, streaming indicators, and interactive controls
    // =============================================
    controls: {
        // Streaming state indicators
        streaming: [
            '[data-is-streaming]',      // Primary: streaming state attribute
            '.streaming-indicator',     // Fallback: semantic class name
            '[aria-busy="true"]'        // Semantic: ARIA busy state
        ],

        // Branch navigation buttons (any navigation button)
        navigationButtons: [
            'button svg path[d*="M13.2402"]',  // Left arrow SVG path
            'button svg path[d*="M6.13378"]',  // Right arrow SVG path
            '[data-navigation-button]'         // Future-proof: data attribute
        ],

        // Specific branch switching paths for click operations
        branchSwitch: {
            right: 'button[type="button"]:not([disabled]) svg path[d*="M6.13378 3.16011"]',
            left: 'button[type="button"] svg path[d*="M13.2402 3.07224"]'
        },

        // Edit mode form (textarea with ID attribute)
        editForm: 'form textarea[id]'
    },

    // =============================================
    // NAVIGATION & SCROLLING
    // Elements used for scroll anchoring and navigation within messages
    // =============================================
    navigation: {
        // Scrollable content anchors (ordered by priority)
        scrollAnchors: [
            '[data-testid="user-message"]',
            '.font-claude-response',
            'p',
            'li',
            'pre'
        ]
    }
};

const ATTRIBUTES = {
    // Attribute names used for MutationObserver
    IS_STREAMING: 'data-is-streaming'
};

// =============================================
// UTILITY FUNCTIONS
// =============================================

/**
 * Universal selector finder - tries multiple selectors until one works
 */
function findElements(selectorArray, context = document) {
    for (const selector of selectorArray) {
        // Skip empty or invalid selectors
        if (!selector || typeof selector !== 'string' || selector.trim() === '') {
            continue;
        }

        try {
            const elements = context.querySelectorAll(selector);
            if (elements.length > 0) {
                return Array.from(elements);
            }
        } catch (e) {
            console.warn(`${LOG_PREFIX} Invalid selector: ${selector}`, e);
        }
    }
    return [];
}

/**
 * Find first matching element from selector array
 */
function findElement(selectorArray, context = document) {
    for (const selector of selectorArray) {
        // Skip empty or invalid selectors
        if (!selector || typeof selector !== 'string' || selector.trim() === '') {
            continue;
        }

        try {
            const element = context.querySelector(selector);
            if (element) return element;
        } catch (e) {
            console.warn(`${LOG_PREFIX} Invalid selector: ${selector}`, e);
        }
    }
    return null;
}

/**
 * Safe element query with error handling
 */
function safeQuery(element, selector) {
    try {
        return element?.querySelector(selector) || null;
    } catch (e) {
        return null;
    }
}

// =============================================
// API FUNCTIONS
// =============================================

/**
 * Extracts conversation ID from current URL
 */
function getConversationId() {
    const match = window.location.pathname.match(/\/chat\/([^/?]+)/);
    return match ? match[1] : null;
}

/**
 * Gets organization ID from browser cookies
 */
function getOrgId() {
    const cookies = document.cookie.split(';');
    for (const cookie of cookies) {
        const [name, value] = cookie.trim().split('=');
        if (name === 'lastActiveOrg') {
            return value;
        }
    }
    throw new Error('Could not find organization ID');
}

/**
 * Fetches conversation data from Claude API
 */
async function getConversationData() {
    const conversationId = getConversationId();
    if (!conversationId) {
        return null;
    }

    const orgId = getOrgId();
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), CONFIG.apiTimeout);

    try {
        const response = await fetch(
            `/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=true&rendering_mode=messages&render_all_tools=true`,
            { signal: controller.signal }
        );

        clearTimeout(timeoutId);

        if (!response.ok) {
            throw new Error(`API request failed: ${response.status}`);
        }

        return await response.json();
    } catch (error) {
        clearTimeout(timeoutId);
        if (error.name === 'AbortError') {
            console.error(`${LOG_PREFIX} API request timeout`);
        }
        throw error;
    }
}

// =============================================
// BRANCH BUILDING FUNCTIONS
// =============================================

/**
 * Builds conversation tree structure from messages
 */
function buildConversationTree(messages) {
    const nodes = {};
    const childrenMap = {};
    const rootNodes = [];

    // Create nodes object
    messages.forEach(message => {
        nodes[message.uuid] = { ...message };
    });

    // Build childrenMap with UUID arrays
    messages.forEach(message => {
        const parentUuid = message.parent_message_uuid || CONFIG.INITIAL_PARENT_UUID;
        if (!childrenMap[parentUuid]) {
            childrenMap[parentUuid] = [];
        }
        childrenMap[parentUuid].push(message.uuid);
    });

    // Sort children by creation time
    for (const parentUuid in childrenMap) {
        childrenMap[parentUuid].sort((a, b) =>
            new Date(nodes[a].created_at) - new Date(nodes[b].created_at)
        );
    }

    // Get root nodes
    const rootUuids = childrenMap[CONFIG.INITIAL_PARENT_UUID] || [];
    rootUuids.forEach(uuid => rootNodes.push(uuid));

    return { nodes, childrenMap, rootNodes };
}

/**
 * Finds main branch path from current_leaf_message_uuid
 */
function findMainBranchPath(tree, currentLeafUuid) {
    if (!currentLeafUuid) {
        return [];
    }

    const mainPath = [];
    let currentMessage = tree.nodes[currentLeafUuid];

    while (currentMessage) {
        mainPath.unshift(currentMessage);

        const parentUuid = currentMessage.parent_message_uuid;
        if (parentUuid === CONFIG.INITIAL_PARENT_UUID || !parentUuid) {
            break;
        }

        currentMessage = tree.nodes[parentUuid];
    }

    return mainPath;
}

/**
 * Finds main branch path from message with maximum index
 */
function buildPathFromMaxIndex(tree) {
    let maxIndexMessage = null;
    let maxIndex = -1;

    // Find message with maximum index
    Object.values(tree.nodes).forEach(message => {
        if (message.index > maxIndex) {
            maxIndex = message.index;
            maxIndexMessage = message;
        }
    });

    if (!maxIndexMessage) return [];

    // Build path backwards through parent_message_uuid
    const mainPath = [];
    let currentMessage = maxIndexMessage;

    while (currentMessage) {
        mainPath.unshift(currentMessage);

        const parentUuid = currentMessage.parent_message_uuid;
        if (parentUuid === CONFIG.INITIAL_PARENT_UUID || !parentUuid) {
            break;
        }

        currentMessage = tree.nodes[parentUuid];
    }

    return mainPath;
}

/**
 * Gets all branch information including branch points
 */
function getAllBranchInfo(tree) {
    const messageToBranch = new Map();
    const nodeToChildren = new Map();
    const parentLookup = new Map();

    // Build base relationships
    const allNodes = [];

    tree.rootNodes.forEach(rootUuid => collect(rootUuid));

    function collect(nodeUuid) {
        const node = tree.nodes[nodeUuid];
        if (!node) return;

        allNodes.push(node);
        const children = tree.childrenMap[nodeUuid] || [];
        nodeToChildren.set(node.uuid, children);

        children.forEach(childUuid => {
            parentLookup.set(childUuid, node);
            collect(childUuid);
        });
    }

    // Find main branch (by max index)
    const mainBranchPath = buildPathFromMaxIndex(tree);
    const mainBranchUuids = new Set(mainBranchPath.map(n => n.uuid));

    // Find branch points
    const branchStartPoints = [];

    for (const node of allNodes) {
        const children = nodeToChildren.get(node.uuid) || [];
        if (children.length > 1) {
            // Sort children by index
            const childNodes = children.map(uuid => tree.nodes[uuid]).filter(n => n);
            const sorted = childNodes.sort((a, b) => a.index - b.index);

            // All children except first are branch starts
            for (let i = 1; i < sorted.length; i++) {
                branchStartPoints.push({ node: sorted[i], index: sorted[i].index });
            }
        }
    }

    // Sort and number branch start points
    const sortedRoots = tree.rootNodes.map(uuid => tree.nodes[uuid]).filter(n => n)
        .sort((a, b) => a.index - b.index);

    for (let i = 1; i < sortedRoots.length; i++) {
        branchStartPoints.push({ node: sortedRoots[i], index: sortedRoots[i].index });
    }

    branchStartPoints.sort((a, b) => a.index - b.index);

    const nodeToBranchNumber = new Map();
    branchStartPoints.forEach((p, i) => nodeToBranchNumber.set(p.node.uuid, i + 2));

    // Recursive branch assignment
    function assignBranch(nodeUuid, currentBranch) {
        const node = tree.nodes[nodeUuid];
        if (!node) return;

        const isMainBranch = mainBranchUuids.has(node.uuid);
        messageToBranch.set(node.uuid, {
            branchIndex: currentBranch,
            isMainBranch
        });

        const children = nodeToChildren.get(node.uuid) || [];
        if (children.length === 0) return;

        const childNodes = children.map(uuid => tree.nodes[uuid]).filter(n => n);
        const sorted = childNodes.sort((a, b) => a.index - b.index);

        // First child continues current branch
        assignBranch(sorted[0].uuid, currentBranch);

        // Other children start new branches
        for (let i = 1; i < sorted.length; i++) {
            const child = sorted[i];
            const newBranch = nodeToBranchNumber.get(child.uuid) || currentBranch;
            assignBranch(child.uuid, newBranch);
        }
    }

    // Assign branches starting from sorted roots
    if (sortedRoots.length > 0) {
        assignBranch(sortedRoots[0].uuid, 1);

        for (let i = 1; i < sortedRoots.length; i++) {
            const root = sortedRoots[i];
            const branchNumber = nodeToBranchNumber.get(root.uuid);
            if (branchNumber != null) {
                assignBranch(root.uuid, branchNumber);
            }
        }
    }

    // --- API for branch retrieval
    // function getBranchPath(nodeUuid) {
        // Restores the path from the root to this node
        // const path = [];
        // let current = allNodes.find(n => n.uuid === nodeUuid);
        // while (current) {
            // path.unshift(current);
            // current = parentLookup.get(current.uuid);
        // }
        // return path;
    // }

    // function getFullBranch(branchIndex) {
        // Collects all messages belonging to a single branch
        // const branchNodes = allNodes.filter(
            // n => messageToBranch.get(n.uuid)?.branchIndex === branchIndex
        // );
        // Sort by index to ensure order
        // return branchNodes.sort((a, b) => a.index - b.index);
    // }

    return {
        messageToBranch,     // Quickly find the branch number by uuid
        mainBranchUuids,     // For determining the main branch
        // getBranchPath,    // Path from the root to a specific node
        // getFullBranch,    // The entire branch by its number
        allNodes,            // All tree nodes
        parentLookup,        // Parent relationships
        nodeToChildren       // Children relationships
    };
}

// =============================================
// CLAUDE API - From ClaudePowerestManager&Enhancer
// =============================================
const ClaudeAPI = {
    conversationTree: null,
    currentLinearBranch: null,
    currentConversationUuid: null,
    isInitialized: false,

    /**
     * Smart initialization of the conversation tree - avoids redundant requests
     */
    async tryInitializeConversationTree() {
        try {
            const currentUrl = window.location.href;
            const pathParts = new URL(currentUrl).pathname.split('/');
            const conversationUuid = (pathParts[1] === 'chat' && pathParts[2]) ? pathParts[2] : null;

            if (!conversationUuid || conversationUuid === 'new') {
                return false;
            }

            // Check if already initialized for the current conversation
            if (this.isInitialized && this.currentConversationUuid === conversationUuid) {
                return true;
            }

            const conversationData = await getConversationData();
            if (!conversationData) return false;

            this.conversationTree = buildConversationTree(conversationData.chat_messages);
            this.currentConversationUuid = conversationUuid;
            this.isInitialized = true;

            await this.updateCurrentLinearBranch();
            return true;
        } catch (error) {
            console.warn(`${LOG_PREFIX} Failed to initialize conversation tree:`, error);
            this.isInitialized = false;
        }
        return false;
    },

    // Detect conversation switch, reset initialization state
    checkConversationChange() {
        const currentUrl = window.location.href;
        const pathParts = new URL(currentUrl).pathname.split('/');
        const conversationUuid = (pathParts[1] === 'chat' && pathParts[2]) ? pathParts[2] : null;

        if (conversationUuid !== this.currentConversationUuid) {
            console.log(`${LOG_PREFIX} Detected conversation switch: ${this.currentConversationUuid?.slice(-8) || 'none'} → ${conversationUuid?.slice(-8) || 'none'}`);
            this.isInitialized = false;
            this.conversationTree = null;
            this.currentLinearBranch = null;
            this.currentConversationUuid = conversationUuid;
        }
    },

    // Analyze the linear branch currently displayed on the frontend
    async updateCurrentLinearBranch() {
        if (!this.conversationTree) {
            // Try to auto-initialize the conversation tree
            await this.tryInitializeConversationTree();
            if (!this.conversationTree) {
                this.currentLinearBranch = [];
                return;
            }
        }

        const turns = this.findCurrentTurns();

        // Build the linear branch: the frontend DOM display must represent a complete parent-child sequential chain
        const branch = this.buildLinearBranchFromDOM(turns);
        this.currentLinearBranch = branch;

        return turns; // Return turns to avoid duplicate calls
    },

    // Find conversation turns based on the actual DOM structure - using a precise selector
    findCurrentTurns() {
        const elements = document.querySelectorAll(SELECTORS.chat.turnBase);
        const validElements = Array.from(elements).filter(el => {
            // Check if it contains a user message or a Claude response content
            const hasUserMessage = !!el.querySelector(SELECTORS.message.userTestId);
            const hasClaudeResponse = !!el.querySelector(SELECTORS.message.claudeResponse);

            // Must be either a user message or a Claude response
            return hasUserMessage || hasClaudeResponse;
        });

        return validElements;
    },

    // Build the branch based on the sequential parent-child relationship in the DOM
    buildLinearBranchFromDOM(turns) {
        const branch = [];
        let expectedParentUuid = CONFIG.INITIAL_PARENT_UUID; // Root node UUID (virtual, not displayed on frontend)

        // Pre-extract all sibling info to avoid redundant calls
        const turnsWithSiblingInfo = turns.map(turn => ({
            turn,
            siblingInfo: this.extractSiblingInfo(turn)
        }));

        // Since the root node is not displayed on the frontend, the first DOM turn corresponds to the direct child of the root node
        for (let i = 0; i < turnsWithSiblingInfo.length; i++) {
            const { turn, siblingInfo } = turnsWithSiblingInfo[i];
            const nodeUuid = this.findNodeByPositionWithCachedInfo(turn, expectedParentUuid, siblingInfo);

            if (nodeUuid) {
                const node = { ...this.conversationTree.nodes[nodeUuid], uuid: nodeUuid };
                branch.push(node);
                expectedParentUuid = nodeUuid;
            } else {
                console.warn(`${LOG_PREFIX} DOM turn ${i + 1} cannot match node (expected parent: ${expectedParentUuid.slice(-8)})`);
                break; // Stop building, as the parent-child chain is broken
            }
        }

        return branch;
    },

    // Find the exact node in the conversation tree based on position info and cached sibling info
    findNodeByPositionWithCachedInfo(turnElement, expectedParentUuid, siblingInfo) {
        const isUser = !!turnElement.querySelector(SELECTORS.message.userTestId);
        const expectedSender = isUser ? 'human' : 'assistant';
        const { nodes, childrenMap } = this.conversationTree;

        // Get all children of the expected parent, filtering out dirty data and hidden nodes
        const siblings = childrenMap[expectedParentUuid] || [];
        const sameTypeSiblings = siblings.filter(uuid => {
            const node = nodes[uuid];
            if (!node || node.sender !== expectedSender) return false;

            // Filter out hidden human nodes (human without assistant child)
            if (node.sender === 'human') {
                const humanChildren = childrenMap[uuid] || [];
                const hasAssistantChild = humanChildren.some(childUuid => {
                    const child = nodes[childUuid];
                    return child && child.sender === 'assistant';
                });
                return hasAssistantChild; // Only include human nodes that have assistant children
            }

            return true;
        });

        if (siblingInfo) {
            // Use precise position matching when sibling info is available
            if (sameTypeSiblings.length === siblingInfo.totalSiblings) {
                const targetIndex = siblingInfo.currentIndex;
                if (targetIndex >= 0 && targetIndex < sameTypeSiblings.length) {
                    return sameTypeSiblings[targetIndex];
                }
            }
        } else {
            // Logic when sibling info is not available
            if (sameTypeSiblings.length === 1) {
                return sameTypeSiblings[0];
            } else if (sameTypeSiblings.length > 1) {
                // Select the earliest node by time (usually the main branch)
                const sortedSiblings = sameTypeSiblings.sort((a, b) =>
                    new Date(nodes[a].created_at) - new Date(nodes[b].created_at)
                );
                return sortedSiblings[0];
            }
        }

        return null;
    },

    // Find the key positioning element: <span class="self-center shrink-0 select-none font-small text-text-300">a / b</span> where b is the total number of siblings (including itself), and a is its 1-based index among siblings.
    extractSiblingInfo(turnElement) {

        // Precise class matching
        const siblingSpan = turnElement.querySelector(SELECTORS.message.siblingInfo);

        if (siblingSpan) {
            const text = siblingSpan.textContent?.trim();
            const match = text.match(/(\d+)\s*\/\s*(\d+)/);
            if (match) {
                const currentPosition = parseInt(match[1]); // 1-based index position
                const totalSiblings = parseInt(match[2]);   // Total number including itself

                return {
                    currentIndex: currentPosition - 1, // Convert to 0-based index for array operations
                    totalSiblings: totalSiblings
                };
            }
        }

        return null;
    },

    // Check if the target node is in the current linear branch
    isNodeInCurrentBranch(nodeUuid) {
        if (!this.currentLinearBranch) return false;
        return this.currentLinearBranch.some(node => node && node.uuid === nodeUuid);
    }
};

// =============================================
// BRANCH SWITCHER - From ClaudePowerestManager&Enhancer
// =============================================
const BranchSwitcher = {
    // Utility function
    wait(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    },

    // Check if node is hidden by Claude (human message without assistant response)
    isNodeHidden(nodeUuid) {
        const { nodes, childrenMap } = ClaudeAPI.conversationTree;
        const node = nodes[nodeUuid];
        if (!node || node.sender !== 'human') return false;

        // Check if this human node has any assistant children
        const children = childrenMap[nodeUuid] || [];
        const hasAssistantChild = children.some(childUuid => {
            const child = nodes[childUuid];
            return child && child.sender === 'assistant';
        });

        return !hasAssistantChild;
    },

    // Find nearest visible ancestor (skipping hidden nodes)
    findVisibleAncestor(nodeUuid) {
        const { nodes } = ClaudeAPI.conversationTree;
        let currentUuid = nodeUuid;

        while (currentUuid && currentUuid !== CONFIG.INITIAL_PARENT_UUID) {
            const node = nodes[currentUuid];
            if (!node) return null;

            // Check if node is in DOM or if it's hidden
            if (ClaudeAPI.isNodeInCurrentBranch(currentUuid)) {
                return currentUuid;
            }

            // Skip if hidden
            if (!this.isNodeHidden(currentUuid)) {
                // Node should be visible but isn't - might be in different branch
                break;
            }

            // Move to parent
            currentUuid = node.parent_message_uuid;
        }

        return currentUuid || CONFIG.INITIAL_PARENT_UUID;
    },

    // Switch to the target stage node
    async switchToTargetStageNode(targetNodeUuid) {
        console.log(`${LOG_PREFIX} Attempting to switch to stage target node: ${targetNodeUuid.slice(-8)}`);

        // 1. Check if the stage target node is already on the frontend
        if (ClaudeAPI.isNodeInCurrentBranch(targetNodeUuid)) {
            console.log(`${LOG_PREFIX} Target node already in current frontend display`);
            return true;
        }

        // 2. Get target node and find visible ancestor (skipping hidden nodes)
        const { nodes, childrenMap } = ClaudeAPI.conversationTree;
        const targetNode = nodes[targetNodeUuid];
        if (!targetNode) {
            console.error(`${LOG_PREFIX} Cannot find target node: ${targetNodeUuid.slice(-8)}`);
            return false;
        }

        const directParentUuid = targetNode.parent_message_uuid || CONFIG.INITIAL_PARENT_UUID;

        // Find visible ancestor (skipping human nodes without assistant)
        const parentUuid = this.findVisibleAncestor(directParentUuid);
        if (!parentUuid) {
            console.error(`${LOG_PREFIX} Cannot find visible ancestor`);
            return false;
        }

        console.log(`${LOG_PREFIX} Using parent: ${parentUuid === CONFIG.INITIAL_PARENT_UUID ? 'ROOT' : parentUuid.slice(-8)}`);

        // Check parent is in frontend
        let isParentInFrontend = false;
        if (parentUuid === CONFIG.INITIAL_PARENT_UUID) {
            isParentInFrontend = ClaudeAPI.currentLinearBranch && ClaudeAPI.currentLinearBranch.length > 0;
        } else {
            isParentInFrontend = ClaudeAPI.isNodeInCurrentBranch(parentUuid);
        }

        if (!isParentInFrontend) {
            console.error(`${LOG_PREFIX} Parent not in frontend: ${parentUuid.slice(-8)}`);
            return false;
        }

        // 3. Calculate the required operation
        // If direct parent is hidden, find all visible descendants of visible ancestor
        let siblings, sameTypeSiblings;

        if (directParentUuid !== parentUuid) {
            // Direct parent is hidden - find all visible descendants of visible ancestor
            console.log(`${LOG_PREFIX} Direct parent ${directParentUuid.slice(-8)} is hidden, finding visible descendants`);

            // Get all descendants recursively
            const getAllDescendants = (nodeUuid, depth = 0) => {
                if (depth > 10) return []; // Prevent infinite recursion
                const children = childrenMap[nodeUuid] || [];
                let descendants = [];

                for (const childUuid of children) {
                    const child = nodes[childUuid];
                    if (!child) continue;

                    // Check if this node is hidden
                    const isHidden = this.isNodeHidden(childUuid);

                    if (!isHidden && child.sender === targetNode.sender) {
                        descendants.push(childUuid);
                    }

                    // Recurse into children
                    if (isHidden) {
                        const childDescendants = getAllDescendants(childUuid, depth + 1);
                        descendants = descendants.concat(childDescendants);
                    }
                }

                return descendants;
            };

            sameTypeSiblings = getAllDescendants(parentUuid);
        } else {
            // Direct parent is visible - use normal logic
            siblings = childrenMap[parentUuid] || [];
            sameTypeSiblings = siblings.filter(uuid => {
                const node = nodes[uuid];
                if (!node || node.sender !== targetNode.sender) return false;
                return !this.isNodeHidden(uuid);
            });
        }

        // 3.1 Calculate the position index of the stage target node among its parent's children
        const targetIndex = sameTypeSiblings.indexOf(targetNodeUuid);
        if (targetIndex === -1) {
            console.error(`${LOG_PREFIX} Cannot find target node in siblings`);
            return false;
        }

        // 3.2 Calculate the position of the currently displayed sibling node
        let currentIndex = -1;
        if (parentUuid === CONFIG.INITIAL_PARENT_UUID) {
            // Root node scenario: Find the first node of the same type
            if (ClaudeAPI.currentLinearBranch && ClaudeAPI.currentLinearBranch.length > 0) {
                const firstNodeOfSameType = ClaudeAPI.currentLinearBranch.find(node =>
                    node && node.sender === targetNode.sender
                );
                if (firstNodeOfSameType) {
                    currentIndex = sameTypeSiblings.indexOf(firstNodeOfSameType.uuid);
                }
            }
        } else {
            // Non-root node scenario: Find the first node of the same type after the parent node
            const parentIndexInBranch = ClaudeAPI.currentLinearBranch.findIndex(node =>
                node && node.uuid === parentUuid
            );
            if (parentIndexInBranch !== -1) {
                for (let i = parentIndexInBranch + 1; i < ClaudeAPI.currentLinearBranch.length; i++) {
                    const node = ClaudeAPI.currentLinearBranch[i];
                    if (node && node.sender === targetNode.sender) {
                        currentIndex = sameTypeSiblings.indexOf(node.uuid);
                        break;
                    }
                }
            }
        }

        if (currentIndex === -1) {
            console.error(`${LOG_PREFIX} Cannot determine current sibling position`);
            return false;
        }

        // 3.3 Calculate the position difference
        const diff = targetIndex - currentIndex;
        console.log(`${LOG_PREFIX} Need to switch ${diff} steps (target: ${targetIndex}, current: ${currentIndex})`);

        if (diff === 0) {
            console.log(`${LOG_PREFIX} Already at target position`);
            return true;
        }

        // 4. Execute the switch operation
        const direction = diff > 0 ? 'right' : 'left';
        const steps = Math.abs(diff);

        // Find the index of the frontend node to operate on
        let frontendNodeIndex = -1;
        if (parentUuid === CONFIG.INITIAL_PARENT_UUID) {
            for (let i = 0; i < ClaudeAPI.currentLinearBranch.length; i++) {
                const node = ClaudeAPI.currentLinearBranch[i];
                if (node && node.sender === targetNode.sender) {
                    frontendNodeIndex = i + 1; // Convert to 1-based index
                    break;
                }
            }
        } else {
            // Non-root node scenario: Find the first node of the same type after the parent node
            const parentIndexInBranch = ClaudeAPI.currentLinearBranch.findIndex(node =>
                node && node.uuid === parentUuid
            );
            if (parentIndexInBranch !== -1) {
                for (let i = parentIndexInBranch + 1; i < ClaudeAPI.currentLinearBranch.length; i++) {
                    const node = ClaudeAPI.currentLinearBranch[i];
                    if (node && node.sender === targetNode.sender) {
                        frontendNodeIndex = i + 1; // Convert to 1-based index
                        break;
                    }
                }
            }
        }

        if (frontendNodeIndex === -1) {
            console.error(`${LOG_PREFIX} Cannot determine frontend node index`);
            return false;
        }

        // Execute switch steps
        for (let step = 0; step < steps; step++) {
            console.log(`${LOG_PREFIX} Executing step ${step + 1}/${steps} ${direction} switch`);

            const success = await this.clickNodeSwitch(direction, frontendNodeIndex);
            if (!success) {
                console.error(`${LOG_PREFIX} Step ${step + 1} switch failed`);
                return false;
            }

            // Wait for the switch to complete
            await this.wait(300);

            // Update current branch status
            await ClaudeAPI.updateCurrentLinearBranch();
        }

        // 5. Verify successful switch
        await this.wait(200);
        await ClaudeAPI.updateCurrentLinearBranch();

        const success = ClaudeAPI.isNodeInCurrentBranch(targetNodeUuid);
        console.log(`${LOG_PREFIX} Switch ${success ? 'successful' : 'failed'}: ${targetNodeUuid.slice(-8)}`);

        return success;
    },

    // Recursively switch to the target node
    async switchToTargetNode(targetNodeUuid) {
        console.log(`${LOG_PREFIX} Starting switch to target node: ${targetNodeUuid.slice(-8)}`);

        // Ensure conversation tree is initialized
        if (!ClaudeAPI.conversationTree) {
            await ClaudeAPI.tryInitializeConversationTree();
            if (!ClaudeAPI.conversationTree) {
                console.error(`${LOG_PREFIX} Conversation tree not initialized`);
                return false;
            }
        }

        // Update current branch status
        await ClaudeAPI.updateCurrentLinearBranch();

        // Try to switch directly to the target node
        if (await this.switchToTargetStageNode(targetNodeUuid)) {
            return true;
        }

        // If direct switch fails, recursively switch to the parent node
        const { nodes } = ClaudeAPI.conversationTree;
        const targetNode = nodes[targetNodeUuid];
        if (!targetNode) {
            console.error(`${LOG_PREFIX} Cannot find target node: ${targetNodeUuid.slice(-8)}`);
            return false;
        }

        const parentUuid = targetNode.parent_message_uuid;
        if (!parentUuid || parentUuid === CONFIG.INITIAL_PARENT_UUID) {
            console.error(`${LOG_PREFIX} Reached root node, cannot continue recursion`);
            return false;
        }

        console.log(`${LOG_PREFIX} Recursively switching to parent node: ${parentUuid.slice(-8)}`);

        // Recursive call to switch to the parent node
        if (await this.switchToTargetNode(parentUuid)) {
            // After successful parent node switch, try switching to the target node again
            console.log(`${LOG_PREFIX} Parent node switch successful, retrying target node`);
            return await this.switchToTargetStageNode(targetNodeUuid);
        } else {
            console.error(`${LOG_PREFIX} Recursive switch failed, cannot switch to parent: ${parentUuid.slice(-8)}`);
            return false;
        }
    },

    // Generic switch function (simplified, for interacting with existing buttons)
    async clickNodeSwitch(direction, frontendIndex = 1) {
        const turns = ClaudeAPI.findCurrentTurns();
        if (frontendIndex < 1 || frontendIndex > turns.length) {
            console.error(`${LOG_PREFIX} Frontend node index out of range. Valid range: 1-${turns.length}`);
            return false;
        }

        // Find the switch button in the specified frontend node
        const targetTurn = turns[frontendIndex - 1];
        let buttonSelector;

        if (direction === 'right') {
            buttonSelector = SELECTORS.controls.branchSwitch.right;
        } else if (direction === 'left') {
            buttonSelector = SELECTORS.controls.branchSwitch.left;
        } else {
            console.error(`${LOG_PREFIX} Invalid direction parameter. Use 'left' or 'right'`);
            return false;
        }

        const buttonPath = targetTurn.querySelector(buttonSelector);

        if (buttonPath) {
            const button = buttonPath.closest('button');
            if (button && !button.disabled) {
                console.log(`${LOG_PREFIX} Clicking frontend node #${frontendIndex} ${direction === 'right' ? 'right' : 'left'} switch button`);
                button.click();
                await new Promise(resolve => setTimeout(resolve, 200));
                return true;
            }
        }

        console.error(`${LOG_PREFIX} Frontend node #${frontendIndex} has no available ${direction === 'right' ? 'right' : 'left'} switch button`);
        return false;
    }
};

// =============================================
// LINEAR NAVIGATOR - From ClaudePowerestManager&Enhancer
// =============================================
const LinearNavigator = {
    // Scroll to element
    scrollToElement(element, topMargin = CONFIG.topMargin) {
        if (!element) return;

        const anchor = this.findAnchor(element);
        const scroller = this.getScrollContainer(anchor);
        if (!scroller) return;

        const isWindow = scroller === document.documentElement ||
                        scroller === document.body ||
                        scroller === document.scrollingElement;

        const scrollerRect = isWindow ?
            { top: 0, height: window.innerHeight } :
            scroller.getBoundingClientRect();

        const anchorRect = anchor.getBoundingClientRect();
        const currentScrollTop = isWindow ? window.scrollY : scroller.scrollTop;
        const targetScrollTop = currentScrollTop + (anchorRect.top - scrollerRect.top) - topMargin;
        const maxScrollTop = scroller.scrollHeight - scroller.clientHeight;
        const finalScrollTop = Math.max(0, Math.min(targetScrollTop, maxScrollTop));

        scroller.scrollTo({ top: finalScrollTop, behavior: 'smooth' });

        // Highlight effect
        this.addHighlight(element);
        if (anchor !== element) this.addHighlight(anchor);
    },

    findAnchor(turnElement) {
        const selectors = SELECTORS.navigation.scrollAnchors;

        for (const selector of selectors) {
            const element = turnElement.querySelector(selector);
            if (element && element.offsetParent) return element;
        }
        return turnElement;
    },

    getScrollContainer(element) {
        let el = element;
        while (el && el !== document.documentElement) {
            const style = getComputedStyle(el);
            if ((style.overflowY === 'auto' || style.overflowY === 'scroll') &&
                el.scrollHeight > el.clientHeight) {
                return el;
            }
            el = el.parentElement;
        }
        return document.scrollingElement || document.documentElement;
    },

    addHighlight(element) {
        element.classList.add('highlight-pulse');
        setTimeout(() => element.classList.remove('highlight-pulse'), 3100);
    },

    // Linear jump to a specific node (supports cross-branch navigation)
    async jumpToNode(nodeUuid) {
        // First, update the current linear branch status
        await ClaudeAPI.updateCurrentLinearBranch();

        // 2.1 Current frontend linear branch contains the node, jump directly
        if (ClaudeAPI.isNodeInCurrentBranch(nodeUuid)) {
            this.jumpToNodeInCurrentBranch(nodeUuid);
            await new Promise(resolve => setTimeout(resolve, 500));
            return true;
        } else {
            // 2.2 Current frontend linear branch does not contain the node, perform cross-branch jump
            console.log(`${LOG_PREFIX} Target node not in current branch, starting cross-branch jump: ${nodeUuid.slice(-8)}`);

            // Call the branch switcher for cross-branch jump
            const switchSuccess = await BranchSwitcher.switchToTargetNode(nodeUuid);

            if (switchSuccess) {
                // After successful branch switch, execute page jump to the target node
                console.log(`${LOG_PREFIX} Branch switch successful, executing page jump`);
                await new Promise(resolve => setTimeout(resolve, 300));

                // Update branch status and jump
                await ClaudeAPI.updateCurrentLinearBranch();
                this.jumpToNodeInCurrentBranch(nodeUuid);
                await new Promise(resolve => setTimeout(resolve, 500));
                return true;
            } else {
                console.error(`${LOG_PREFIX} Cross-branch jump failed: ${nodeUuid.slice(-8)}`);
                return false;
            }
        }
    },

    // Jump within the current branch
    jumpToNodeInCurrentBranch(nodeUuid, cachedTurns = null) {
        const element = document.getElementById(nodeUuid) ||
                       this.findElementByNodeUuid(nodeUuid, cachedTurns);
        if (element) {
            this.scrollToElement(element);
        }
    },

    findElementByNodeUuid(nodeUuid, cachedTurns = null) {
        // Find directly by ID
        const directElement = document.getElementById(nodeUuid);
        if (directElement) return directElement;

        // Find the corresponding DOM element in the current linear branch based on position
        if (!ClaudeAPI.currentLinearBranch) return null;

        // Find the index of the target node in the current branch
        const nodeIndex = ClaudeAPI.currentLinearBranch.findIndex(node => node.uuid === nodeUuid);
        if (nodeIndex === -1) return null;

        // Use cached turns or get all currently displayed turns
        const turns = cachedTurns || ClaudeAPI.findCurrentTurns();

        if (nodeIndex < turns.length) {
            return turns[nodeIndex];
        }

        return null;
    }
};

// =============================================
// DOM MANIPULATION
// =============================================

/**
 * Formats timestamp for display
 */
function formatTimestamp(isoString) {
    const date = new Date(isoString);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');

    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

/**
 * Injects metadata into a DOM element
 */
function injectMetadata(container, messageData, messageToBranch, domPosition, totalDomMessages) {
    // Check if already added
    if (container.querySelector(SELECTORS.injected.timestampMetadata)) {
        return;
    }

    // Get branch info for this specific message
    const branchInfo = messageToBranch.get(messageData.uuid);
    const branchNumber = branchInfo ? branchInfo.branchIndex : '?';

    // Hardcode
    // Determine if main or side branch BY SPECIFIC MESSAGE UUID
    // Not by branch info, because a branch can contain both main and side messages
    const isMainBranch = branchInfo ? branchInfo.isMainBranch : false;
    const branchStatus = isMainBranch ? 'Main' : 'Side';

    // Check if message was canceled
    const isCanceled = messageData.stop_reason === 'user_canceled';
    const canceledText = isCanceled ? ' | CANCELED' : '';

    // Create metadata element
    const metadata = document.createElement('div');
    metadata.className = SELECTORS.injected.timestampMetadata.substring(1);
    metadata.style.cssText = `
        position: absolute;
        top: -15px;
        right: 8px;
        font-size: 10px;
        color: var(--text-400, #94a3b8);
        opacity: 0.7;
        padding: 2px 6px;
        background: var(--bg-200, rgba(0, 0, 0, 0.05));
        border-radius: 4px;
        /* backdrop-filter: blur(4px); */
        z-index: 2;
        pointer-events: auto;
        white-space: nowrap;
    `;

    const timestamp = formatTimestamp(messageData.created_at);
    metadata.textContent = `#${messageData.index} [${domPosition}/${totalDomMessages}] | Branch ${branchNumber} | ${branchStatus}${canceledText} | ${timestamp}`;

    const tooltipLines = [
        `API Index: ${messageData.index}`,
        `DOM Position: ${domPosition} of ${totalDomMessages}`,
        `Branch: ${branchNumber}`,
        `Status: ${branchStatus}`,
        `Created: ${new Date(messageData.created_at).toLocaleString()}`,
        `UUID: ${messageData.uuid}`,
        `Parent UUID: ${messageData.parent_message_uuid || 'ROOT'}`
    ];

    if (isCanceled) {
        tooltipLines.push('Stop Reason: User Canceled');
    }

    metadata.title = tooltipLines.join('\n');

    // Find where to insert
    const groupDiv = findElement(SELECTORS.message.groups, container);
    if (groupDiv) {
        groupDiv.style.position = 'relative';
        groupDiv.prepend(metadata);

        // Check boundaries after prepending to DOM
        requestAnimationFrame(() => {
            const maxWContainer = document.querySelector(SELECTORS.chat.messageListContainer);
            if (maxWContainer) {
                const containerRect = maxWContainer.getBoundingClientRect();
                const groupRect = groupDiv.getBoundingClientRect();
                const metadataRect = metadata.getBoundingClientRect();

                // If left edge of metadata overflows left edge of max-w-3xl container
                if (metadataRect.left < containerRect.left) {
                    // Target position: 3px from left edge of container
                    const targetLeftAbsolute = containerRect.left + 3;
                    // Calculate offset relative to groupDiv
                    const leftOffset = targetLeftAbsolute - groupRect.left;

                    // Switch from right to left positioning
                    metadata.style.right = 'auto';
                    metadata.style.left = leftOffset + 'px';
                }
            }
        });
    }
}

/**
 * Injects artifact command badge into artifact block
 */
function injectArtifactMetadata(artifactBlock, command, canceled) {
    // Check if already added
    if (artifactBlock.querySelector(SELECTORS.injected.artifactCommand)) {
        return;
    }

    // Find the version info line
    const versionLine = findElement(SELECTORS.artifact.versionInfo, artifactBlock);
    if (!versionLine) return;

    // Create command badge
    const commandBadge = document.createElement('span');
    commandBadge.className = SELECTORS.injected.artifactCommand.substring(1);

    const commandColors = {
        'create': '#22c55e',
        'rewrite': '#eab308',
        'update': '#3b82f6'
    };

    const bgColor = commandColors[command] || '#6b7280';

    commandBadge.style.cssText = `
        display: inline-block;
        padding: 1px 4px;
        margin-left: 4px;
        font-size: 9px;
        font-weight: 600;
        color: white;
        background-color: ${bgColor};
        border-radius: 3px;
        text-transform: uppercase;
        vertical-align: middle;
    `;

    commandBadge.textContent = command + ' ARTIFACT';

    if (canceled) {
        commandBadge.style.opacity = '0.5';
        commandBadge.title = 'Generation was canceled';
    } else {
        commandBadge.title = `Command: ${command}`;
    }

    // IMPORTANT: Just append the badge without clearing versionLine content
    try {
        versionLine.appendChild(commandBadge);
    } catch (e) {
        // Silently ignore if insertion fails
    }
}

/**
 * Removes all existing metadata badges
 */
function clearMetadata() {
    document.querySelectorAll(SELECTORS.injected.timestampMetadata).forEach(el => el.remove());
    document.querySelectorAll(SELECTORS.injected.artifactCommand).forEach(el => el.remove());
}

// =============================================
// TREE VISUALIZATION
// =============================================

let graphPanel = null;
let graphCanvas = null;
let toggleButton = null;
let currentTreeData = null;
let canvasOffset = { x: 0, y: 0 };
let canvasScale = 1;
let isDragging = false;
let dragStart = { x: 0, y: 0 };
let activeBranchUuids = new Set();
let isResizing = false;

/**
 * Creates the graph visualization panel
 */
function createGraphPanel() {
    if (graphPanel) return;

    // Create panel container
    graphPanel = document.createElement('div');
    graphPanel.id = 'claude-graph-panel';
    graphPanel.style.cssText = `
        position: fixed;
        top: 0;
        right: -450px;
        width: 450px;
        height: 100vh;
        background: #1e293b;
        box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
        z-index: 10000;
        transition: right 0.3s ease;
        display: flex;
        flex-direction: column;
        overflow: hidden;
    `;

    // Header
    const header = document.createElement('div');
    header.style.cssText = `
        padding: 16px;
        background: #0f172a;
        color: #e2e8f0;
        font-weight: 600;
        font-size: 14px;
        border-bottom: 1px solid #334155;
        display: flex;
        justify-content: space-between;
        align-items: center;
    `;
    header.innerHTML = `
        <div>
            <div>Conversation Tree</div>
            <div style="font-size: 10px; font-weight: normal; color: #94a3b8; margin-top: 4px;">
                Drag to pan • Scroll to zoom • Click nodes for info
            </div>
        </div>
        <div style="display: flex; gap: 8px; align-items: center;">
            <button id="claude-graph-settings" style="
                background: #334155;
                border: none;
                color: #e2e8f0;
                cursor: pointer;
                font-size: 11px;
                padding: 4px 8px;
                border-radius: 4px;
                font-weight: 500;
                margin-right: 8px;
            " title="Settings">⚙️</button>
            <button id="claude-graph-reset" style="
                background: #334155;
                border: none;
                color: #e2e8f0;
                cursor: pointer;
                font-size: 11px;
                padding: 4px 8px;
                border-radius: 4px;
                font-weight: 500;
            " title="Reset zoom and position">Reset</button>
            <button id="claude-graph-close" style="
                background: transparent;
                border: none;
                color: #94a3b8;
                cursor: pointer;
                font-size: 18px;
                padding: 0;
                width: 24px;
                height: 24px;
                display: flex;
                align-items: center;
                justify-content: center;
                background: rgba(51, 65, 85, 0.6);
                color: #cbd5e1;
            ">×</button>
        </div>
    `;

    // Resizer handle (Claude-style)
    const resizer = document.createElement('div');
    resizer.id = 'claude-graph-resizer';
    resizer.className = 'claude-graph-resizer-group';
    resizer.innerHTML = `
        <div class="claude-graph-resizer-line"></div>
        <div class="claude-graph-resizer-handle"></div>
    `;
    resizer.addEventListener('mousedown', startResizing);
    graphPanel.appendChild(resizer);

    // Canvas container
    const canvasContainer = document.createElement('div');
    canvasContainer.style.cssText = `
        flex: 1;
        overflow: auto;
        position: relative;
        background: #0f172a;
    `;

    // Canvas
    graphCanvas = document.createElement('canvas');
    graphCanvas.style.cssText = `
        display: block;
        cursor: grab;
    `;

    canvasContainer.appendChild(graphCanvas);
    graphPanel.appendChild(header);
    graphPanel.appendChild(canvasContainer);
    document.body.appendChild(graphPanel);

    // Close button handler
    document.getElementById('claude-graph-close').addEventListener('click', () => {
        closeGraphPanel();
    });

    // Reset button handler
    document.getElementById('claude-graph-reset').addEventListener('click', () => {
        canvasOffset = { x: 0, y: 0 };
        canvasScale = 1;
        if (currentTreeData) {
            renderGraph(currentTreeData);
        }
    });

    // Settings button handler
    document.getElementById('claude-graph-settings').addEventListener('click', () => {
        showGraphSettings();
    });

    // Canvas click handler for node selection
    graphCanvas.addEventListener('click', (e) => {
        if (!currentTreeData || isDragging) return;

        const rect = graphCanvas.getBoundingClientRect();
        const x = (e.clientX - rect.left - canvasOffset.x) / canvasScale;
        const y = (e.clientY - rect.top - canvasOffset.y) / canvasScale;

        // Find clicked node
        const nodePos = findNodeAtPosition(x, y, currentTreeData.nodePositions);
        if (nodePos) {
            showNodeDetails(nodePos.node, currentTreeData.messageToBranch);
        }
    });

    // Tooltip on hover
    graphCanvas.addEventListener('mousemove', (e) => {
        if (!currentTreeData) return;

        if (isDragging) {
            const dx = e.clientX - dragStart.x;
            const dy = e.clientY - dragStart.y;
            canvasOffset.x += dx;
            canvasOffset.y += dy;
            dragStart = { x: e.clientX, y: e.clientY };
            renderGraph(currentTreeData);
            return;
        }

        const rect = graphCanvas.getBoundingClientRect();
        const x = (e.clientX - rect.left - canvasOffset.x) / canvasScale;
        const y = (e.clientY - rect.top - canvasOffset.y) / canvasScale;

        const nodePos = findNodeAtPosition(x, y, currentTreeData.nodePositions);
        if (nodePos) {
            graphCanvas.style.cursor = 'pointer';
            graphCanvas.title = `#${nodePos.node.index} | Branch ${nodePos.branchIndex} | ${nodePos.node.sender} | ${formatTimestamp(nodePos.node.created_at)}`;
        } else {
            graphCanvas.style.cursor = isDragging ? 'grabbing' : 'grab';
            graphCanvas.title = '';
        }
    });

    // Mouse down - start dragging
    graphCanvas.addEventListener('mousedown', (e) => {
        const rect = graphCanvas.getBoundingClientRect();
        const x = (e.clientX - rect.left - canvasOffset.x) / canvasScale;
        const y = (e.clientY - rect.top - canvasOffset.y) / canvasScale;

        const nodePos = findNodeAtPosition(x, y, currentTreeData?.nodePositions);
        if (!nodePos) {
            isDragging = true;
            dragStart = { x: e.clientX, y: e.clientY };
            graphCanvas.style.cursor = 'grabbing';
        }
    });

    // Mouse up - stop dragging
    graphCanvas.addEventListener('mouseup', () => {
        isDragging = false;
        graphCanvas.style.cursor = 'grab';
    });

    // Mouse leave - stop dragging
    graphCanvas.addEventListener('mouseleave', () => {
        isDragging = false;
        graphCanvas.style.cursor = 'grab';
    });

    // Zoom with mouse wheel
    graphCanvas.addEventListener('wheel', (e) => {
        e.preventDefault();

        const rect = graphCanvas.getBoundingClientRect();
        const mouseX = e.clientX - rect.left;
        const mouseY = e.clientY - rect.top;

        const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
        const newScale = Math.max(0.3, Math.min(3, canvasScale * zoomDelta));

        // Zoom to cursor position
        const scaleDiff = newScale - canvasScale;
        canvasOffset.x -= (mouseX - canvasOffset.x) * (scaleDiff / canvasScale);
        canvasOffset.y -= (mouseY - canvasOffset.y) * (scaleDiff / canvasScale);

        canvasScale = newScale;
        renderGraph(currentTreeData);
    });
}

/**
 * Handle window resize
 */
function handleResize() {
    if (graphPanel && graphPanel.style.right === '0px' && currentTreeData) {
        renderGraph(currentTreeData);
    }
}

// Add resize listener
window.addEventListener('resize', debounce(handleResize, 250));

/**
 * Start resizing the panel
 */
function startResizing(e) {
    isResizing = true;
    document.documentElement.style.userSelect = 'none';
    document.addEventListener('mousemove', resizePanel);
    document.addEventListener('mouseup', stopResizing);

    // Add visual feedback
    const resizer = document.querySelector('.claude-graph-resizer-group');
    if (resizer) resizer.classList.add('resizing');

    e.preventDefault();
}
/**
 * Handle resize while panel is open
 */
function resizePanel(e) {
    if (!isResizing || !graphPanel) return;
    const newWidth = window.innerWidth - e.clientX;
    if (newWidth >= 300 && newWidth <= window.innerWidth - 100) {
        graphPanel.style.width = `${newWidth}px`;

        // Update push mode on resize
        const mode = Settings.get('graph', 'panelMode') || 'overlay';
        if (mode === 'push' && graphPanel.style.right === '0px') {
            applyPanelMode(mode);
        }

        // Force re-render with new size
        if (currentTreeData) {
            // Use setTimeout to allow DOM to update
            setTimeout(() => {
                renderGraph(currentTreeData);
            }, 10);
        }
    }
}
/**
 * Stop resizing the panel
 */
function stopResizing() {
    isResizing = false;
    document.documentElement.style.userSelect = '';
    document.removeEventListener('mousemove', resizePanel);
    document.removeEventListener('mouseup', stopResizing);

    // Remove visual feedback
    const resizer = document.querySelector('.claude-graph-resizer-group');
    if (resizer) resizer.classList.remove('resizing');
}

/**
 * Creates the toggle button
 */
function createToggleButton() {
    if (toggleButton) return;

    toggleButton = document.createElement('button');
    toggleButton.id = 'claude-graph-toggle';
    toggleButton.className = 'graph-toggle-indicator';
    toggleButton.innerHTML = `
        <div class="indicator-wrapper">
            <svg class="tree-network-icon" viewBox="0 0 24 24" fill="none">
                <circle cx="12" cy="12" r="2" fill="#60a5fa"/>
                <path d="M12 2v8m0 0v8m10-10h-8m0 0h-8" stroke="#64748b" stroke-width="1.5"/>
                <circle cx="12" cy="2" r="1.5" fill="#94a3b8"/>
                <circle cx="12" cy="22" r="1.5" fill="#94a3b8"/>
                <circle cx="22" cy="12" r="1.5" fill="#94a3b8"/>
                <circle cx="2" cy="12" r="1.5" fill="#94a3b8"/>
            </svg>
            <div class="branch-indicator" id="branch-indicator"></div>
        </div>
    `;

    toggleButton.addEventListener('click', () => {
        toggleGraphPanel();
    });

    document.body.appendChild(toggleButton);
}

/**
 * Update branch indicator based on conversation state
*/
/**
function updateBranchIndicator() {
    if (!toggleButton) return;

    const indicator = toggleButton.querySelector('.branch-indicator');
    if (!indicator) return;

    if (!currentTreeData) {
        indicator.style.display = 'none';
        return;
    }

    const { messageToBranch } = currentTreeData;

    // Count unique branches
    const branches = new Set();
    messageToBranch.forEach(info => branches.add(info.branchIndex));

    toggleButton.classList.remove('single-branch', 'main-branch', 'side-branch');

    // Only 1 branch - blue indicator
    if (branches.size === 1) {
        toggleButton.classList.add('single-branch');
        indicator.style.display = 'block';
        return;
    }

    // Multiple branches - determine which one we are currently on
    if (ClaudeAPI.currentLinearBranch && ClaudeAPI.currentLinearBranch.length > 0) {
        // Get the last message in the current branch
        const lastMessage = ClaudeAPI.currentLinearBranch[ClaudeAPI.currentLinearBranch.length - 1];
        const branchInfo = messageToBranch.get(lastMessage.uuid);

        if (branchInfo && branchInfo.isMainBranch) {
            toggleButton.classList.add('main-branch'); // Green
        } else {
            toggleButton.classList.add('side-branch'); // Orange
        }
    }

    indicator.style.display = 'block';
}
*/
function updateBranchIndicator() {
    if (!toggleButton) return;
    const indicator = toggleButton.querySelector('.branch-indicator');
    if (!indicator) return;

    toggleButton.classList.remove('single-branch', 'main-branch', 'side-branch');
    toggleButton.classList.add('main-branch'); // Green
    indicator.style.display = 'block';
}

/**
 * Toggle graph panel visibility
 */
function toggleGraphPanel() {
    if (!graphPanel) return;

    const isOpen = graphPanel.style.right === '0px';

    if (isOpen) {
        closeGraphPanel();
    } else {
        openGraphPanel();
    }
}

/**
 * Open graph panel
 */
function openGraphPanel() {
    if (!graphPanel) return;

    const mode = Settings.get('graph', 'panelMode') || 'overlay';

    // Apply mode BEFORE animation
    if (mode === 'push') {
        applyPanelMode(mode);
    }

    // Then open the panel
    graphPanel.style.right = '0px';

    setTimeout(() => {
        if (currentTreeData) {
            renderGraph(currentTreeData);
        }
    }, 100);
}
/**
 * Close graph panel
 */
function closeGraphPanel() {
    if (!graphPanel) return;
    const width = parseInt(getComputedStyle(graphPanel).width);
    graphPanel.style.right = `-${width}px`;

    // Restore width
    const mode = Settings.get('graph', 'panelMode') || 'overlay';
    if (mode === 'push') {
        const mainContainer = document.querySelector(SELECTORS.chat.mainContainer);
        if (mainContainer) {
            mainContainer.style.width = '';
            mainContainer.style.transition = '';
        }
    }
}
/**
 * Apply panel mode (overlay or push)
 */
function applyPanelMode(mode) {
    const mainContainer = document.querySelector(SELECTORS.chat.mainContainer);

    if (!mainContainer) {
        console.warn(`${LOG_PREFIX} Cannot find main container for panel mode`);
        return;
    }

    if (mode === 'push') {
        const panelWidth = parseInt(getComputedStyle(graphPanel).width);

        // Reduce container width
        mainContainer.style.width = `calc(100% - ${panelWidth}px)`;
        mainContainer.style.transition = 'width 0.3s ease';
        // console.log(`${LOG_PREFIX} Push mode: container width = calc(100% - ${panelWidth}px)`);

    } else {
        // Overlay mode: restore width
        mainContainer.style.width = '';
        mainContainer.style.transition = '';
        // console.log(`${LOG_PREFIX} Overlay mode: restored container width`);
    }
}

// =============================================
// LAYOUT UTILITIES
// =============================================

/**
 * Calculate depth of a node in the tree
 */
function calculateNodeDepth(node, parentLookup) {
    let depth = 0;
    let current = node;
    while (parentLookup.has(current.uuid)) {
        depth++;
        current = parentLookup.get(current.uuid);
    }
    return depth;
}

/**
 * Get the center position of a pair, or the node position if not a pair
 */
function getNodePosition(nodePos, nodePositions) {
    if (!nodePos) return null;

    if (nodePos.isPair && nodePos.pairPartner) {
        const partnerPos = nodePositions.get(nodePos.pairPartner);
        if (partnerPos) {
            return {
                x: (nodePos.x + partnerPos.x) / 2,
                y: (nodePos.y + partnerPos.y) / 2
            };
        }
    }

    return { x: nodePos.x, y: nodePos.y };
}

/**
 * Check if we should skip drawing edge to this node (it's a pair partner)
 */
function shouldSkipPairPartner(currentPos, nextNodeUuid) {
    return currentPos?.isPair && currentPos.pairPartner === nextNodeUuid;
}

/**
 * Calculate X and Y coordinates based on direction, index, and level
 */
function calculateCoordinates(direction, indexPosition, level, horizontalSpacing, verticalSpacing, startX, startY) {
    let x, y;

    if (direction === 'top-down') {
        x = startX + indexPosition * horizontalSpacing;
        y = startY + level * verticalSpacing;
    } else if (direction === 'bottom-up') {
        x = startX + indexPosition * horizontalSpacing;
        y = startY - level * verticalSpacing;
    } else { // 'left-right'
        x = startX + level * verticalSpacing;
        y = startY + indexPosition * horizontalSpacing;
    }

    return { x, y };
}

/**
 * Calculate pair coordinates (human and assistant positions)
 */
function calculatePairCoordinates(direction, indexPosition, level, horizontalSpacing, verticalSpacing, pairGap, startX, startY) {
    const center = calculateCoordinates(direction, indexPosition, level, horizontalSpacing, verticalSpacing, startX, startY);

    let humanX, humanY, assistantX, assistantY;

    if (direction === 'top-down' || direction === 'bottom-up') {
        humanX = center.x - pairGap / 2;
        assistantX = center.x + pairGap / 2;
        humanY = assistantY = center.y;
    } else { // 'left-right'
        humanY = center.y - pairGap / 2;
        assistantY = center.y + pairGap / 2;
        humanX = assistantX = center.x;
    }

    return { humanX, humanY, assistantX, assistantY };
}

/**
 * Calculate node positions for tree layout
 */
function calculateTreeLayout(branchInfo) {
    const nodeGrouping = Settings.get('graph', 'nodeGrouping') || 'individual';

    if (nodeGrouping === 'pairs') {
        return calculateTreeLayoutPairs(branchInfo);
    }

    // Individual mode
    const { allNodes, messageToBranch, parentLookup } = branchInfo;
    const direction = Settings.get('graph', 'direction');

    // Sort nodes by index
    const sortedNodes = [...allNodes].sort((a, b) => a.index - b.index);

    const nodePositions = new Map();
    const levelNodes = new Map();

    // First pass: determine the depth of each node and group by level
    sortedNodes.forEach(node => {
        let depth = 0;
        let current = node;
        while (parentLookup.has(current.uuid)) {
            depth++;
            current = parentLookup.get(current.uuid);
        }

        if (!levelNodes.has(depth)) {
            levelNodes.set(depth, []);
        }
        levelNodes.get(depth).push(node);

        // Temporarily store depth
        node._depth = depth;
    });

    const nodeSpacing = 70; // Vertical distance between nodes
    const levelSpacing = 100; // Horizontal distance between levels
    const startX = 50;
    const startY = 50;

    levelNodes.forEach((nodes, depth) => {
        // Sort nodes at each level by index for sequence
        nodes.sort((a, b) => a.index - b.index);

        nodes.forEach((node, positionInLevel) => {
            const branchInfo = messageToBranch.get(node.uuid);

            let x, y;
            if (direction === 'top-down') {
                x = startX + positionInLevel * nodeSpacing;
                y = startY + depth * levelSpacing;  // Plus = down
            } else if (direction === 'bottom-up') {
                x = startX + positionInLevel * nodeSpacing;
                y = startY - depth * levelSpacing;  // Minus = up
            } else { // 'left-right' or default
                x = startX + depth * levelSpacing;
                y = startY + positionInLevel * nodeSpacing;
            }

            nodePositions.set(node.uuid, {
                x: x,
                y: y,
                depth: depth,
                node: node,
                branchIndex: branchInfo?.branchIndex || '?',
                isMainBranch: branchInfo?.isMainBranch || false
            });
        });
    });

    // Clean up temporary data
    sortedNodes.forEach(node => delete node._depth);

    return nodePositions;
}
/**
 * Calculate node positions for tree layout with fixed branch columns
 */
function calculateTreeLayoutFixed(branchInfo) {
    const nodeGrouping = Settings.get('graph', 'nodeGrouping') || 'individual';
    const { allNodes, messageToBranch, parentLookup } = branchInfo;
    const direction = Settings.get('graph', 'direction');

    if (nodeGrouping === 'pairs') {
        // Pairs mode with fixed branches
        const pairs = groupNodesToPairs(allNodes, messageToBranch, parentLookup);
        const nodePositions = new Map();

        const pairGap = 25;
        const branchSpacing = 120;
        const levelSpacing = 70;
        const startX = 50;
        const startY = 50;

        pairs.forEach(pair => {
            const firstNode = pair.nodes[0];
            const depth = calculateNodeDepth(firstNode, parentLookup);
            const branchInfo = pair.branchInfo;
            const branchIdx = branchInfo?.branchIndex || 1;

            if (pair.type === 'pair') {
                const coords = calculatePairCoordinates(
                    direction,
                    branchIdx - 1,
                    depth,
                    branchSpacing,
                    levelSpacing,
                    pairGap,
                    startX,
                    startY
                );

                nodePositions.set(pair.human.uuid, {
                    x: coords.humanX,
                    y: coords.humanY,
                    depth: depth,
                    node: pair.human,
                    branchIndex: branchInfo?.branchIndex || '?',
                    isMainBranch: branchInfo?.isMainBranch || false,
                    isPair: true,
                    pairPartner: pair.assistant.uuid
                });

                nodePositions.set(pair.assistant.uuid, {
                    x: coords.assistantX,
                    y: coords.assistantY,
                    depth: depth,
                    node: pair.assistant,
                    branchIndex: branchInfo?.branchIndex || '?',
                    isMainBranch: branchInfo?.isMainBranch || false,
                    isPair: true,
                    pairPartner: pair.human.uuid
                });
            } else {
                const coords = calculateCoordinates(
                    direction,
                    branchIdx - 1,
                    depth,
                    branchSpacing,
                    levelSpacing,
                    startX,
                    startY
                );

                nodePositions.set(pair.node.uuid, {
                    x: coords.x,
                    y: coords.y,
                    depth: depth,
                    node: pair.node,
                    branchIndex: branchInfo?.branchIndex || '?',
                    isMainBranch: branchInfo?.isMainBranch || false,
                    isPair: false
                });
            }
        });

        return nodePositions;
    }

    // Individual mode
    const sortedNodes = [...allNodes].sort((a, b) => a.index - b.index);
    const nodePositions = new Map();
    const branchSpacing = 120;
    const levelSpacing = 100;
    const startX = 50;
    const startY = 50;

    sortedNodes.forEach(node => {
        const branchInfo = messageToBranch.get(node.uuid);
        const branchIdx = branchInfo?.branchIndex || 1;
        const depth = calculateNodeDepth(node, parentLookup);

        const coords = calculateCoordinates(
            direction,
            branchIdx - 1,
            depth,
            branchSpacing,
            levelSpacing,
            startX,
            startY
        );

        nodePositions.set(node.uuid, {
            x: coords.x,
            y: coords.y,
            depth: depth,
            node: node,
            branchIndex: branchInfo?.branchIndex || '?',
            isMainBranch: branchInfo?.isMainBranch || false
        });
    });

    return nodePositions;
}
/**
 * Groups nodes into human-assistant pairs
 */
function groupNodesToPairs(allNodes, messageToBranch, parentLookup) {
    const pairs = [];
    const processed = new Set();

    // Sort by index
    const sorted = [...allNodes].sort((a, b) => a.index - b.index);

    for (let i = 0; i < sorted.length; i++) {
        const node = sorted[i];

        if (processed.has(node.uuid)) continue;

        if (node.sender === 'human') {
            // Find next assistant with this node as parent
            const children = allNodes.filter(n =>
                n.parent_message_uuid === node.uuid &&
                n.sender === 'assistant'
            );

            if (children.length > 0) {
                // Take first by index
                const assistant = children.sort((a, b) => a.index - b.index)[0];

                pairs.push({
                    type: 'pair',
                    human: node,
                    assistant: assistant,
                    nodes: [node, assistant],
                    branchInfo: messageToBranch.get(assistant.uuid) // Use assistant's branch info
                });

                processed.add(node.uuid);
                processed.add(assistant.uuid);
            } else {
                // Human without assistant
                pairs.push({
                    type: 'single',
                    node: node,
                    nodes: [node],
                    branchInfo: messageToBranch.get(node.uuid)
                });
                processed.add(node.uuid);
            }
        } else if (node.sender === 'assistant' && !processed.has(node.uuid)) {
            // Orphaned assistant
            pairs.push({
                type: 'single',
                node: node,
                nodes: [node],
                branchInfo: messageToBranch.get(node.uuid)
            });
            processed.add(node.uuid);
        }
    }

    return pairs;
}
/**
 * Calculate layout for pairs mode
 */
function calculateTreeLayoutPairs(branchInfo) {
    const direction = Settings.get('graph', 'direction');
    const { allNodes, messageToBranch, parentLookup } = branchInfo;

    const pairs = groupNodesToPairs(allNodes, messageToBranch, parentLookup);
    const nodePositions = new Map();
    const levelPairs = new Map();

    // Group pairs by depth level
    pairs.forEach(pair => {
        const firstNode = pair.nodes[0];
        const depth = calculateNodeDepth(firstNode, parentLookup);

        if (!levelPairs.has(depth)) {
            levelPairs.set(depth, []);
        }
        levelPairs.get(depth).push({ ...pair, depth });
    });

    const pairGap = 25;
    const pairSpacing = 70;
    const levelSpacing = 70;
    const startX = 50;
    const startY = 50;

    levelPairs.forEach((pairs, depth) => {
        pairs.sort((a, b) => a.nodes[0].index - b.nodes[0].index);

        pairs.forEach((pair, positionInLevel) => {
            const branchInfo = pair.branchInfo;

            if (pair.type === 'pair') {
                const coords = calculatePairCoordinates(
                    direction,
                    positionInLevel,
                    depth,
                    pairSpacing,
                    levelSpacing,
                    pairGap,
                    startX,
                    startY
                );

                nodePositions.set(pair.human.uuid, {
                    x: coords.humanX,
                    y: coords.humanY,
                    depth: depth,
                    node: pair.human,
                    branchIndex: branchInfo?.branchIndex || '?',
                    isMainBranch: branchInfo?.isMainBranch || false,
                    isPair: true,
                    pairPartner: pair.assistant.uuid
                });

                nodePositions.set(pair.assistant.uuid, {
                    x: coords.assistantX,
                    y: coords.assistantY,
                    depth: depth,
                    node: pair.assistant,
                    branchIndex: branchInfo?.branchIndex || '?',
                    isMainBranch: branchInfo?.isMainBranch || false,
                    isPair: true,
                    pairPartner: pair.human.uuid
                });
            } else {
                const coords = calculateCoordinates(
                    direction,
                    positionInLevel,
                    depth,
                    pairSpacing,
                    levelSpacing,
                    startX,
                    startY
                );

                nodePositions.set(pair.node.uuid, {
                    x: coords.x,
                    y: coords.y,
                    depth: depth,
                    node: pair.node,
                    branchIndex: branchInfo?.branchIndex || '?',
                    isMainBranch: branchInfo?.isMainBranch || false,
                    isPair: false
                });
            }
        });
    });

    return nodePositions;
}
/**
 * Calculate layout for branches view mode
 */
function calculateBranchesLayout(branchInfo) {
    const direction = Settings.get('graph', 'direction');
    const nodeGrouping = Settings.get('graph', 'nodeGrouping') || 'individual';
    const branches = extractBranchesByIndex(branchInfo);
    const { messageToBranch, parentLookup, allNodes } = branchInfo;

    const nodePositions = new Map();
    const branchSpacing = 120;
    const startX = 50;
    const startY = 50;

    // Check if using pairs mode
    if (nodeGrouping === 'pairs') {
        const nodeSpacing = 70;
        const pairGap = 25;

        branches.forEach((branch, branchArrayIndex) => {
            const { branchIndex, nodes } = branch;

            // Calculate branch start level
            let branchStartLevel = 0;
            if (branchIndex > 1 && nodes.length > 0) {
                const firstNode = nodes[0];
                const parentNode = parentLookup.get(firstNode.uuid);
                if (parentNode) {
                    branchStartLevel = calculateNodeDepth(parentNode, parentLookup);
                }
            }

            // Group nodes in this branch into pairs
            const branchPairs = groupNodesToPairs(nodes, messageToBranch, parentLookup);

            branchPairs.forEach(pair => {
                const firstNodeIndex = nodes.findIndex(n => n.uuid === pair.nodes[0].uuid);
                const actualLevel = branchStartLevel + firstNodeIndex;
                const branchInfo = pair.branchInfo;

                if (pair.type === 'pair') {
                    const coords = calculatePairCoordinates(
                        direction,
                        branchArrayIndex,
                        actualLevel,
                        branchSpacing,
                        nodeSpacing,
                        pairGap,
                        startX,
                        startY
                    );

                    nodePositions.set(pair.human.uuid, {
                        x: coords.humanX,
                        y: coords.humanY,
                        depth: actualLevel,
                        node: pair.human,
                        branchIndex: branchInfo?.branchIndex || '?',
                        isMainBranch: branchInfo?.isMainBranch || false,
                        isPair: true,
                        pairPartner: pair.assistant.uuid
                    });

                    nodePositions.set(pair.assistant.uuid, {
                        x: coords.assistantX,
                        y: coords.assistantY,
                        depth: actualLevel,
                        node: pair.assistant,
                        branchIndex: branchInfo?.branchIndex || '?',
                        isMainBranch: branchInfo?.isMainBranch || false,
                        isPair: true,
                        pairPartner: pair.human.uuid
                    });
                } else {
                    const coords = calculateCoordinates(
                        direction,
                        branchArrayIndex,
                        actualLevel,
                        branchSpacing,
                        nodeSpacing,
                        startX,
                        startY
                    );

                    nodePositions.set(pair.node.uuid, {
                        x: coords.x,
                        y: coords.y,
                        depth: actualLevel,
                        node: pair.node,
                        branchIndex: branchInfo?.branchIndex || '?',
                        isMainBranch: branchInfo?.isMainBranch || false,
                        isPair: false
                    });
                }
            });
        });
    } else {
        // Individual mode
        const nodeSpacing = 70;

        branches.forEach((branch, branchArrayIndex) => {
            const { branchIndex, nodes } = branch;

            // Calculate branch start level
            let branchStartLevel = 0;
            if (branchIndex > 1 && nodes.length > 0) {
                const firstNode = nodes[0];
                const parentNode = parentLookup.get(firstNode.uuid);
                if (parentNode) {
                    branchStartLevel = calculateNodeDepth(parentNode, parentLookup);
                }
            }

            nodes.forEach((node, positionInBranch) => {
                const branchInfo = messageToBranch.get(node.uuid);
                const actualLevel = branchStartLevel + positionInBranch;

                const coords = calculateCoordinates(
                    direction,
                    branchArrayIndex,
                    actualLevel,
                    branchSpacing,
                    nodeSpacing,
                    startX,
                    startY
                );

                nodePositions.set(node.uuid, {
                    x: coords.x,
                    y: coords.y,
                    depth: actualLevel,
                    node: node,
                    branchIndex: branchInfo?.branchIndex || '?',
                    isMainBranch: branchInfo?.isMainBranch || false
                });
            });
        });
    }

    return nodePositions;
}
/**
 * Extract all branches grouped by branchIndex
 */
function extractBranchesByIndex(branchInfo) {
    const { allNodes, messageToBranch } = branchInfo;
    const branches = new Map(); // branchIndex -> array of nodes

    allNodes.forEach(node => {
        const info = messageToBranch.get(node.uuid);
        if (info) {
            const branchIdx = info.branchIndex;
            if (!branches.has(branchIdx)) {
                branches.set(branchIdx, []);
            }
            branches.get(branchIdx).push(node);
        }
    });

    // Sort nodes within each branch by index
    branches.forEach((nodes, branchIdx) => {
        nodes.sort((a, b) => a.index - b.index);
    });

    // Convert to array and sort by branch number
    const result = Array.from(branches.entries())
        .sort((a, b) => a[0] - b[0])
        .map(([branchIdx, nodes]) => ({ branchIndex: branchIdx, nodes }));

    return result;
}

/**
 * Render the tree graph on canvas
 */
function renderGraph(treeData) {
    if (!graphCanvas || !treeData) return;

    const { allNodes, messageToBranch, parentLookup } = treeData;
    const viewMode = Settings.get('graph', 'viewMode') || 'tree';
    const edgeModes = Settings.get('graph', 'edgeModes') || ['parent-child'];

    // Choose layout based on viewMode
    let nodePositions;
    if (viewMode === 'branches') {
        nodePositions = calculateBranchesLayout(treeData);
    } else if (viewMode === 'tree-fixed') {
        nodePositions = calculateTreeLayoutFixed(treeData);
    } else {
        nodePositions = calculateTreeLayout(treeData);
    }

    // Store for click detection
    currentTreeData = { ...treeData, nodePositions };

    // Get the actual container size instead of calculating from nodes
    const canvasContainer = graphCanvas.parentElement;
    const containerRect = canvasContainer.getBoundingClientRect();

    const canvasWidth = Math.max(containerRect.width, 400);
    const canvasHeight = Math.max(containerRect.height, 400);

    // Set canvas size with device pixel ratio for crisp rendering
    const dpr = window.devicePixelRatio || 1;
    graphCanvas.width = canvasWidth * dpr;
    graphCanvas.height = canvasHeight * dpr;
    graphCanvas.style.width = `${canvasWidth}px`;
    graphCanvas.style.height = `${canvasHeight}px`;

    const ctx = graphCanvas.getContext('2d');
    ctx.scale(dpr, dpr);

    // Clear canvas
    ctx.fillStyle = '#0f172a';
    ctx.fillRect(0, 0, canvasWidth, canvasHeight);

    // Apply transformation for zoom and pan
    ctx.save();
    ctx.translate(canvasOffset.x, canvasOffset.y);
    ctx.scale(canvasScale, canvasScale);

    // Calculate visible area in graph coordinates
    const visibleLeft = -canvasOffset.x / canvasScale;
    const visibleTop = -canvasOffset.y / canvasScale;
    const visibleRight = (canvasWidth - canvasOffset.x) / canvasScale;
    const visibleBottom = (canvasHeight - canvasOffset.y) / canvasScale;

    /**
     * Draw an edge between two nodes, using pair centers if applicable
     */
    function drawEdge(ctx, fromPos, toPos, nodePositions, style = {}) {
        const {
            strokeStyle = '#3b82f6',
            lineWidth = 2,
            lineDash = [],
            curved = false,
            direction = 'top-down'
        } = style;

        // Get actual positions (centers if pairs)
        const fromCoords = getNodePosition(fromPos, nodePositions);
        const toCoords = getNodePosition(toPos, nodePositions);

        if (!fromCoords || !toCoords) return false;

        ctx.beginPath();
        ctx.strokeStyle = strokeStyle;
        ctx.lineWidth = lineWidth;
        ctx.setLineDash(lineDash);

        ctx.moveTo(fromCoords.x, fromCoords.y);

        if (curved) {
            // Draw bezier curve
            if (direction === 'top-down' || direction === 'bottom-up') {
                const controlPointY = (fromCoords.y + toCoords.y) / 2;
                ctx.bezierCurveTo(
                    fromCoords.x, controlPointY,
                    toCoords.x, controlPointY,
                    toCoords.x, toCoords.y
                );
            } else {
                const controlPointX = (fromCoords.x + toCoords.x) / 2;
                ctx.bezierCurveTo(
                    controlPointX, fromCoords.y,
                    controlPointX, toCoords.y,
                    toCoords.x, toCoords.y
                );
            }
        } else {
            // Draw straight line
            ctx.lineTo(toCoords.x, toCoords.y);
        }

        ctx.stroke();
        ctx.setLineDash([]);

        return true;
    }

    // Draw parent-child edges if selected
    if (edgeModes.includes('parent-child')) {
        if (viewMode === 'tree' || viewMode === 'tree-fixed') {
            const processedPairs = new Set();
            const direction = Settings.get('graph', 'direction');

            allNodes.forEach(node => {
                const parent = parentLookup.get(node.uuid);
                if (!parent) return;

                const childPos = nodePositions.get(node.uuid);
                const parentPos = nodePositions.get(parent.uuid);
                if (!childPos || !parentPos) return;

                // Skip if parent and child are partners in the same pair
                if (childPos.isPair && childPos.pairPartner === parent.uuid) {
                    return;
                }

                // Skip if this pair was already processed
                if (childPos.isPair && childPos.pairPartner) {
                    const pairKey = [node.uuid, childPos.pairPartner].sort().join('-');
                    if (processedPairs.has(pairKey)) return;
                    processedPairs.add(pairKey);
                }

                // If child is not part of a pair, use direct parent coordinates (not pair center)
                // If child is part of a pair, use pair centers
                let childCoords, parentCoords;

                if (childPos.isPair) {
                    // Child is in a pair - use centers for both
                    childCoords = getNodePosition(childPos, nodePositions);
                    parentCoords = getNodePosition(parentPos, nodePositions);
                } else {
                    // Child is individual - use direct coordinates
                    childCoords = { x: childPos.x, y: childPos.y };
                    parentCoords = { x: parentPos.x, y: parentPos.y };
                }

                if (!childCoords || !parentCoords) return;

                const childInView = isPointInView(childCoords.x, childCoords.y, visibleLeft, visibleTop, visibleRight, visibleBottom);
                const parentInView = isPointInView(parentCoords.x, parentCoords.y, visibleLeft, visibleTop, visibleRight, visibleBottom);

                if (childInView || parentInView) {
                    ctx.beginPath();
                    ctx.strokeStyle = childPos.isMainBranch ? '#3b82f6' : '#64748b';
                    ctx.lineWidth = childPos.isMainBranch ? 2 : 1;

                    ctx.moveTo(parentCoords.x, parentCoords.y);

                    if (direction === 'top-down' || direction === 'bottom-up') {
                        const controlPointY = (parentCoords.y + childCoords.y) / 2;
                        ctx.bezierCurveTo(
                            parentCoords.x, controlPointY,
                            childCoords.x, controlPointY,
                            childCoords.x, childCoords.y
                        );
                    } else {
                        const controlPointX = (parentCoords.x + childCoords.x) / 2;
                        ctx.bezierCurveTo(
                            controlPointX, parentCoords.y,
                            controlPointX, childCoords.y,
                            childCoords.x, childCoords.y
                        );
                    }

                    ctx.stroke();
                }
            });
        } else {
            // Branches mode
            const branches = extractBranchesByIndex(treeData);

            // Draw cross-branch connections (where branches split off)
            branches.forEach(branch => {
                const { nodes } = branch;
                if (nodes.length === 0) return;

                const firstNode = nodes[0];
                const parentUuid = firstNode.parent_message_uuid;

                if (parentUuid && parentUuid !== CONFIG.INITIAL_PARENT_UUID) {
                    const firstNodePos = nodePositions.get(firstNode.uuid);
                    const parentPos = nodePositions.get(parentUuid);

                    if (firstNodePos && parentPos) {
                        // Visibility check
                        const childCoords = getNodePosition(firstNodePos, nodePositions);
                        const parentCoords = getNodePosition(parentPos, nodePositions);

                        if (!childCoords || !parentCoords) return;

                        const firstInView = isPointInView(childCoords.x, childCoords.y, visibleLeft, visibleTop, visibleRight, visibleBottom);
                        const parentInView = isPointInView(parentCoords.x, parentCoords.y, visibleLeft, visibleTop, visibleRight, visibleBottom);

                        if (firstInView || parentInView) {
                            drawEdge(ctx, parentPos, firstNodePos, nodePositions, {
                                strokeStyle: firstNodePos.isMainBranch ? '#3b82f6' : '#64748b',
                                lineWidth: firstNodePos.isMainBranch ? 2 : 1.5,
                                lineDash: [5, 5],
                                curved: false
                            });
                        }
                    }
                }
            });

            // Draw sequential edges within each branch
            branches.forEach(branch => {
                const { nodes } = branch;

                for (let i = 0; i < nodes.length - 1; i++) {
                    const currentNode = nodes[i];
                    const nextNode = nodes[i + 1];

                    const currentPos = nodePositions.get(currentNode.uuid);
                    const nextPos = nodePositions.get(nextNode.uuid);

                    if (!currentPos || !nextPos) continue;

                    // Skip if current and next are partners in the same pair
                    if (shouldSkipPairPartner(currentPos, nextNode.uuid)) continue;

                    // Visibility check
                    const currentCoords = getNodePosition(currentPos, nodePositions);
                    const nextCoords = getNodePosition(nextPos, nodePositions);

                    if (!currentCoords || !nextCoords) continue;

                    const currentInView = isPointInView(currentCoords.x, currentCoords.y, visibleLeft, visibleTop, visibleRight, visibleBottom);
                    const nextInView = isPointInView(nextCoords.x, nextCoords.y, visibleLeft, visibleTop, visibleRight, visibleBottom);

                    if (currentInView || nextInView) {
                        drawEdge(ctx, currentPos, nextPos, nodePositions, {
                            strokeStyle: currentPos.isMainBranch ? '#3b82f6' : '#64748b',
                            lineWidth: currentPos.isMainBranch ? 2 : 1,
                            curved: false
                        });
                    }
                }
            });
        }
    }

    // Draw sibling edges if selected
    if (edgeModes.includes('siblings')) {
        const siblingGroups = new Map();

        allNodes.forEach(node => {
            const parentUuid = node.parent_message_uuid || CONFIG.INITIAL_PARENT_UUID;
            if (!siblingGroups.has(parentUuid)) {
                siblingGroups.set(parentUuid, { human: [], assistant: [] });
            }
            const group = siblingGroups.get(parentUuid);
            if (node.sender === 'human') {
                group.human.push(node);
            } else {
                group.assistant.push(node);
            }
        });

        // Draw sibling connections (orange dashed lines between branches)
        siblingGroups.forEach((group, parentUuid) => {
            ['human', 'assistant'].forEach(senderType => {
                const siblings = group[senderType];
                if (siblings.length < 2) return;

                siblings.sort((a, b) => a.index - b.index);

                for (let i = 0; i < siblings.length - 1; i++) {
                    const current = siblings[i];
                    const next = siblings[i + 1];

                    const currentPos = nodePositions.get(current.uuid);
                    const nextPos = nodePositions.get(next.uuid);

                    if (currentPos && nextPos) {
                        const currentInView = isPointInView(currentPos.x, currentPos.y, visibleLeft, visibleTop, visibleRight, visibleBottom);
                        const nextInView = isPointInView(nextPos.x, nextPos.y, visibleLeft, visibleTop, visibleRight, visibleBottom);

                        if (currentInView || nextInView) {
                            ctx.beginPath();
                            ctx.strokeStyle = '#f59e0b';
                            ctx.lineWidth = 1.5;
                            ctx.setLineDash([3, 3]);

                            ctx.moveTo(currentPos.x, currentPos.y);
                            ctx.lineTo(nextPos.x, nextPos.y);
                            ctx.stroke();
                            ctx.setLineDash([]);
                        }
                    }
                }
            });
        });
    }

    // Draw pair backgrounds
    if (Settings.get('graph', 'nodeGrouping') === 'pairs') {
        nodePositions.forEach((pos1, uuid1) => {
            if (pos1.isPair && pos1.pairPartner) {
                const pos2 = nodePositions.get(pos1.pairPartner);

                // Draw only once per pair (when processing human)
                if (pos2 && pos1.node.sender === 'human') {
                    const isVisible = isPointInView(pos1.x, pos1.y, visibleLeft, visibleTop, visibleRight, visibleBottom) ||
                                     isPointInView(pos2.x, pos2.y, visibleLeft, visibleTop, visibleRight, visibleBottom);

                    if (!isVisible) return;

                    // Calculate pair bounding box
                    const minX = Math.min(pos1.x, pos2.x);
                    const maxX = Math.max(pos1.x, pos2.x);
                    const minY = Math.min(pos1.y, pos2.y);
                    const maxY = Math.max(pos1.y, pos2.y);

                    const padding = 18;
                    const boxX = minX - padding;
                    const boxY = minY - padding;
                    const boxWidth = (maxX - minX) + padding * 2;
                    const boxHeight = (maxY - minY) + padding * 2;

                    // Determine pair color
                    const isMainBranch = pos1.isMainBranch;
                    const isActive = activeBranchUuids.has(uuid1) && activeBranchUuids.has(pos1.pairPartner);

                    // Draw background box
                    ctx.fillStyle = isMainBranch
                        ? 'rgba(59, 130, 246, 0.08)'
                        : 'rgba(100, 116, 139, 0.08)';

                    ctx.strokeStyle = isActive
                        ? 'rgba(251, 191, 36, 0.5)'
                        : (isMainBranch ? 'rgba(59, 130, 246, 0.25)' : 'rgba(100, 116, 139, 0.25)');

                    ctx.lineWidth = isActive ? 2 : 1;

                    ctx.beginPath();
                    ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 6);
                    ctx.fill();
                    ctx.stroke();

                    // Draw connecting line between nodes
                    ctx.strokeStyle = isMainBranch
                        ? 'rgba(59, 130, 246, 0.4)'
                        : 'rgba(100, 116, 139, 0.4)';
                    ctx.lineWidth = 1.5;
                    ctx.setLineDash([4, 4]);
                    ctx.beginPath();
                    ctx.moveTo(pos1.x, pos1.y);
                    ctx.lineTo(pos2.x, pos2.y);
                    ctx.stroke();
                    ctx.setLineDash([]);

                    // Draw small pair indicator label
                    const centerX = (pos1.x + pos2.x) / 2;
                    const centerY = (pos1.y + pos2.y) / 2;

                    // Draw small circle background for label
                    ctx.beginPath();
                    ctx.arc(centerX, centerY, 8, 0, Math.PI * 2);
                    ctx.fillStyle = '#0f172a';
                    ctx.fill();
                    ctx.strokeStyle = isMainBranch ? '#3b82f6' : '#64748b';
                    ctx.lineWidth = 1.5;
                    ctx.stroke();

                    // Draw "P" for pair
                    ctx.fillStyle = isMainBranch ? '#60a5fa' : '#94a3b8';
                    ctx.font = 'bold 8px sans-serif';
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.fillText('P', centerX, centerY);

                    // Content text preview (below the pair box) human inly
                    const showText = Settings.get('graph', 'showNodeText');
                    if (showText) {
                        // Show text for assistant node (usually has more content)
                        const assistantNode = pos1.node.sender === 'human' ? pos1.node : pos2.node;
                        const nodeText = getNodeText(assistantNode);

                        if (nodeText) {
                            const previewText = truncateText(nodeText, 50);
                            ctx.fillStyle = '#94a3b8';
                            ctx.font = '9px sans-serif';
                            ctx.textAlign = 'center';
                            ctx.textBaseline = 'top';

                            // Draw text with background - centered below the box
                            const textMetrics = ctx.measureText(previewText);
                            const textWidth = textMetrics.width;
                            const textHeight = 12;
                            const textPadding = 4;
                            const textY = boxY + boxHeight + 4; // 4px below box
                            const textX = centerX;

                            // Background rectangle
                            ctx.fillStyle = 'rgba(15, 23, 42, 0.9)';
                            ctx.fillRect(
                                textX - textWidth / 2 - textPadding,
                                textY,
                                textWidth + textPadding * 2,
                                textHeight + textPadding
                            );

                            // Text
                            ctx.fillStyle = '#94a3b8';
                            ctx.fillText(previewText, textX, textY + 4);
                        }
                    }
                }
            }
        });
    }

    // Draw nodes
    nodePositions.forEach((pos, uuid) => {
        // Visibility check
        if (!isPointInView(pos.x, pos.y, visibleLeft, visibleTop, visibleRight, visibleBottom)) {
            return;
        }

        const node = pos.node;
        const branchInfo = messageToBranch.get(uuid);
        const isMainBranch = branchInfo?.isMainBranch || false;
        const isCanceled = node.stop_reason === 'user_canceled';
        const isActiveBranch = activeBranchUuids.has(uuid);

        // Node circle
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, 12, 0, Math.PI * 2);

        if (isCanceled) {
            ctx.fillStyle = '#ef4444';
        } else if (isMainBranch) {
            ctx.fillStyle = '#3b82f6';
        } else {
            ctx.fillStyle = '#64748b';
        }
        ctx.fill();

        // Border - brighter for active branch
        if (isActiveBranch) {
            ctx.strokeStyle = '#fbbf24';
            ctx.lineWidth = 3;
        } else {
            ctx.strokeStyle = isMainBranch ? '#60a5fa' : '#94a3b8';
            ctx.lineWidth = 2;
        }
        ctx.stroke();

        // Index text
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 10px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(node.index.toString(), pos.x, pos.y);

        // Sender indicator
        const senderColor = node.sender === 'human' ? '#10b981' : '#8b5cf6';
        ctx.fillStyle = senderColor;
        ctx.beginPath();
        ctx.arc(pos.x + 10, pos.y - 10, 3, 0, Math.PI * 2);
        ctx.fill();

        // Content text preview (below the node) - only in individual mode
        const showText = Settings.get('graph', 'showNodeText');
        const isPairsMode = Settings.get('graph', 'nodeGrouping') === 'pairs';

        if (showText && !isPairsMode) {
            const nodeText = getNodeText(node);
            if (nodeText) {
                const previewText = truncateText(nodeText, 40);
                ctx.fillStyle = '#94a3b8';
                ctx.font = '9px sans-serif';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'top';

                // Draw text with background for better readability
                const textMetrics = ctx.measureText(previewText);
                const textWidth = textMetrics.width;
                const textHeight = 12;
                const padding = 4;

                // Background rectangle
                ctx.fillStyle = 'rgba(15, 23, 42, 0.9)';
                ctx.fillRect(
                    pos.x - textWidth / 2 - padding,
                    pos.y + 16,
                    textWidth + padding * 2,
                    textHeight + padding
                );

                // Text
                ctx.fillStyle = '#94a3b8';
                ctx.fillText(previewText, pos.x, pos.y + 20);
            }
        }
    });

    ctx.restore();

    // Draw legend (doesn't scale)
    drawLegend(ctx, canvasWidth, viewMode, edgeModes);
}

/**
 * Check if point is in visible area
 */
function isPointInView(x, y, left, top, right, bottom, padding = 50) {
    return x >= (left - padding) &&
           x <= (right + padding) &&
           y >= (top - padding) &&
           y <= (bottom + padding);
}

/**
 * Draw legend separately
 */
function drawLegend(ctx, canvasWidth, viewMode) {
    const legendY = 20;

    // Main branch
    ctx.fillStyle = '#3b82f6';
    ctx.beginPath();
    ctx.arc(20, legendY, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#e2e8f0';
    ctx.font = '11px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText('Main', 32, legendY + 4);

    // Side branch
    ctx.fillStyle = '#64748b';
    ctx.beginPath();
    ctx.arc(80, legendY, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#e2e8f0';
    ctx.fillText('Side', 92, legendY + 4);

    // Active branch (highlighted)
    ctx.strokeStyle = '#fbbf24';
    ctx.lineWidth = 3;
    ctx.fillStyle = '#3b82f6';
    ctx.beginPath();
    ctx.arc(135, legendY, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
    ctx.fillStyle = '#e2e8f0';
    ctx.fillText('Active', 147, legendY + 4);

    // Canceled
    ctx.fillStyle = '#ef4444';
    ctx.beginPath();
    ctx.arc(205, legendY, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#e2e8f0';
    ctx.fillText('Canceled', 217, legendY + 4);

    // Zoom info
    ctx.fillStyle = '#94a3b8';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'right';
    const modeText = viewMode === 'branches' ? 'Branches' : 'Tree';
    ctx.fillText(`${modeText} | Zoom: ${Math.round(canvasScale * 100)}%`, canvasWidth - 10, legendY + 4);
}

/**
 * Find node at given position
 */
function findNodeAtPosition(x, y, nodePositions) {
    if (!nodePositions) return null;

    for (const [uuid, pos] of nodePositions) {
        const distance = Math.sqrt((x - pos.x) ** 2 + (y - pos.y) ** 2);
        if (distance <= 12) {
            return pos;
        }
    }
    return null;
}

/**
 * Show detailed info about a node and provide jump functionality
 * Uses cross-branch navigation from ClaudePowerestManager&Enhancer
 */
function showNodeDetails(node, messageToBranch) {
    const branchInfo = messageToBranch.get(node.uuid);
    const branchNumber = branchInfo ? branchInfo.branchIndex : '?';
    const branchStatus = branchInfo?.isMainBranch ? 'Main' : 'Side';

    // Get and format content text
    const nodeText = getNodeText(node);
    const formattedText = formatTextContent(nodeText);
    const hasContent = formattedText.trim().length > 0;

    // Escape HTML for safe display
    const escapedPreview = escapeHtml(formattedText.substring(0, 100) + (formattedText.length > 100 ? '...' : ''));
    const escapedFull = escapeHtml(formattedText);

    // Create modal window
    const modal = document.createElement('div');
    modal.className = 'cpm-graph-modal-overlay';
    modal.innerHTML = `
        <div class="cpm-graph-modal">
            <div class="cpm-graph-modal-header">
                <h3>Message #${node.index}</h3>
                <button class="cpm-graph-modal-close">×</button>
            </div>
            <div class="cpm-graph-modal-body">
                <div class="cpm-graph-info-row">
                    <span class="cpm-graph-label">UUID:</span>
                    <span class="cpm-graph-value">${node.uuid}</span>
                </div>
                <div class="cpm-graph-info-row">
                    <span class="cpm-graph-label">Parent UUID:</span>
                    <span class="cpm-graph-value">${node.parent_message_uuid || 'ROOT'}</span>
                </div>
                <div class="cpm-graph-info-row">
                    <span class="cpm-graph-label">Sender:</span>
                    <span class="cpm-graph-value">${node.sender}</span>
                </div>
                <div class="cpm-graph-info-row">
                    <span class="cpm-graph-label">Branch:</span>
                    <span class="cpm-graph-value">${branchNumber}</span>
                </div>
                <div class="cpm-graph-info-row">
                    <span class="cpm-graph-label">Status:</span>
                    <span class="cpm-graph-value cpm-graph-status-${branchStatus.toLowerCase()}">${branchStatus}</span>
                </div>
                <div class="cpm-graph-info-row">
                    <span class="cpm-graph-label">Created:</span>
                    <span class="cpm-graph-value">${formatTimestamp(node.created_at)}</span>
                </div>
                <div class="cpm-graph-info-row">
                    <span class="cpm-graph-label">Stop Reason:</span>
                    <span class="cpm-graph-value">${node.stop_reason || 'completed'}</span>
                </div>
            </div>
            ${hasContent ? `
            <div class="cpm-graph-content-section">
                <div class="cpm-graph-content-header">Content</div>
                <div class="cpm-graph-content-box">
                    <div class="cpm-graph-content-text" id="content-preview">${escapedPreview}</div>
                    <div class="cpm-graph-content-text" id="content-full" style="display: none;">${escapedFull}</div>
                </div>
                ${formattedText.length > 100 ? '<button class="cpm-graph-content-toggle" id="content-toggle">Show full text</button>' : ''}
            </div>
            ` : ''}
            <div class="cpm-graph-modal-footer">
                <button class="cpm-graph-btn cpm-graph-btn-primary" id="cpm-graph-jump-btn">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">
                        <path d="M5 12h14M12 5l7 7-7 7"/>
                    </svg>
                    Jump to Message
                </button>
            </div>
        </div>
    `;

    document.body.appendChild(modal);

    // Toggle full text display
    const toggleBtn = modal.querySelector('#content-toggle');
    if (toggleBtn) {
        toggleBtn.addEventListener('click', () => {
            const preview = modal.querySelector('#content-preview');
            const full = modal.querySelector('#content-full');

            if (full.style.display === 'none') {
                preview.style.display = 'none';
                full.style.display = 'block';
                toggleBtn.textContent = 'Show less';
            } else {
                preview.style.display = 'block';
                full.style.display = 'none';
                toggleBtn.textContent = 'Show full text';
            }
        });
    }

    // Close modal window
    const closeModal = () => modal.remove();
    modal.addEventListener('click', (e) => {
        if (e.target === modal) closeModal();
    });

    // Close button in the header (the cross icon)
    const closeBtn = modal.querySelector('.cpm-graph-modal-close');
    if (closeBtn) {
        closeBtn.addEventListener('click', closeModal);
    }

    // Jump to message
    const jumpBtn = modal.querySelector('#cpm-graph-jump-btn');
    jumpBtn.addEventListener('click', async () => {
        closeModal();
        // closeGraphPanel();

        // Use LinearNavigator.jumpToNode which supports cross-branch navigation
        console.log(`${LOG_PREFIX} Initiating cross-branch jump to node: ${node.uuid.slice(-8)}`);
        await LinearNavigator.jumpToNode(node.uuid);
    });
}

/**
 * Extract text content from node
 */
function getNodeText(node) {
    if (!node || !node.content) return '';

    // Handle array content
    if (Array.isArray(node.content)) {
        const textParts = node.content
            .filter(item => item.type === 'text' && item.text)
            .map(item => item.text);
        return textParts.join('\n');
    }

    // Handle string content
    if (typeof node.content === 'string') {
        return node.content;
    }

    return '';
}

/**
 * Truncate text for display
 */
function truncateText(text, maxLength = 50) {
    if (!text) return '';

    // Replace literal \n with spaces for preview
    text = text.replace(/\\n/g, ' ').replace(/\n/g, ' ');

    // Remove extra spaces
    text = text.replace(/\s+/g, ' ').trim();

    if (text.length <= maxLength) return text;
    return text.substring(0, maxLength) + '...';
}

/**
 * Format text for display (convert \n to actual line breaks)
 */
function formatTextContent(text) {
    if (!text) return '';
    // Replace literal \n with actual line breaks
    return text.replace(/\\n/g, '\n');
}

/**
 * Update graph with new data
 */
async function updateGraph(activeBranch = null) {
    try {
        const conversationData = await getConversationData();
        if (!conversationData || !conversationData.chat_messages) return;

        const tree = buildConversationTree(conversationData.chat_messages);
        const branchInfo = getAllBranchInfo(tree);

        currentTreeData = branchInfo;

        // Save information about the active branch
        if (activeBranch && activeBranch.length > 0) {
            activeBranchUuids = new Set(activeBranch.map(msg => msg.uuid));
        }

        updateBranchIndicator();

        // Render if panel is open
        if (graphPanel && graphPanel.style.right === '0px') {
            renderGraph(branchInfo);
        }
    } catch (error) {
        console.error(`${LOG_PREFIX} Error updating graph:`, error);
    }
}

// =============================================
// INJECTION LOGIC
// =============================================

/**
 * Main function to inject timestamps into all messages
 */
async function injectTimestamps(retryCount = 0) {
    try {
        // Get conversation data from API
        const conversationData = await getConversationData();

        if (!conversationData || !conversationData.chat_messages || conversationData.chat_messages.length === 0) {
            console.log(`${LOG_PREFIX} ❌ No messages found in API`);
            return false;
        }

        // Build conversation tree
        const tree = buildConversationTree(conversationData.chat_messages);

        // Get all branch information (uses MAX INDEX for Main/Side determination)
        const { messageToBranch, mainBranchUuids } = getAllBranchInfo(tree);

        // Update graph data
        currentTreeData = getAllBranchInfo(tree);

        // Find active branch from current_leaf_message_uuid (for DOM matching)
        let activeBranch = findMainBranchPath(tree, conversationData.current_leaf_message_uuid);

        // FALLBACK: If current_leaf_message_uuid is null or not found, use max index path
        if (activeBranch.length === 0) {
            console.log(`${LOG_PREFIX} ⚠️ current_leaf_message_uuid not found, using max index fallback`);
            activeBranch = buildPathFromMaxIndex(tree);
        }

        // Still no branch? Something is wrong
        if (activeBranch.length === 0) {
            console.log(`${LOG_PREFIX} ❌ No active branch found even with fallback`);
            return false;
        }

        console.log(`${LOG_PREFIX} ✅ Using branch with ${activeBranch.length} messages`);

        // Update graph with active branch info
        updateGraph(activeBranch);

        // Get DOM elements using flexible selectors
        const messageContainers = findElements(SELECTORS.message.containers);

        if (messageContainers.length === 0) {
            // Retry if DOM not ready yet (max 3 attempts)
            if (retryCount < CONFIG.retryAttempts) {
                // console.log(`${LOG_PREFIX} ⏳ DOM not ready, retrying... (${retryCount + 1}/${CONFIG.retryAttempts})`);
                setTimeout(() => injectTimestamps(retryCount + 1), CONFIG.retryDelay);
                return false;
            } else {
                // console.log(`${LOG_PREFIX} ❌ No message containers found in DOM after ${CONFIG.retryAttempts} retries`);
                return false;
            }
        }

        console.log(`${LOG_PREFIX} 📊 API (active branch): ${activeBranch.length} messages, DOM: ${messageContainers.length} elements`);

        // Clear old metadata
        clearMetadata();

        // Match and inject
        const totalDomMessages = messageContainers.length;

        // Process each message
        activeBranch.forEach((msg, index) => {
            if (index >= messageContainers.length) return;

            const container = messageContainers[index];

            // Inject message metadata
            injectMetadata(
                container,
                msg,
                messageToBranch,
                index + 1,
                totalDomMessages
            );

            // Look for artifacts in this message's content
            if (msg.content && Array.isArray(msg.content)) {
                const artifacts = msg.content.filter(item =>
                    item.type === 'tool_use' && item.name === 'artifacts' && item.input
                );

                if (artifacts.length > 0) {
                    // console.log(`${LOG_PREFIX} 🎨 Found ${artifacts.length} artifacts in message #${msg.index}`);

                    // Find artifact blocks in this DOM container
                    const artifactBlocks = findElements(SELECTORS.artifact.blocks, container);
                    // console.log(`${LOG_PREFIX} 📦 Found ${artifactBlocks.length} artifact blocks in DOM for message #${msg.index}`);

                    // Match artifacts to blocks (in order)
                    artifacts.forEach((artifact, artifactIndex) => {
                        if (artifactIndex < artifactBlocks.length) {
                            const command = artifact.input.command;
                            const canceled = msg.stop_reason === 'user_canceled';

                            // console.log(`${LOG_PREFIX} ✅ Injecting command "${command}" for artifact #${artifactIndex} in message #${msg.index}`);
                            injectArtifactMetadata(artifactBlocks[artifactIndex], command, canceled);
                        }
                    });
                }
            }
        });

        // Get unique branch count for logging
        const branches = new Set();
        messageToBranch.forEach(info => branches.add(info.branchIndex));

        console.log(`${LOG_PREFIX} 📊 Injected timestamps for ${Math.min(activeBranch.length, messageContainers.length)} messages`);
        console.log(`${LOG_PREFIX} 📊 Main branch UUIDs (from max index): ${mainBranchUuids.size} messages`);
        console.log(`${LOG_PREFIX} 📊 Total branches found: ${branches.size}`);

        return true;

    } catch (error) {
        console.error(`${LOG_PREFIX} ❌ Error:`, error);
        return false;
    }
}

// =============================================
// DEBOUNCE
// =============================================

function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

// =============================================
// INITIALIZATION
// =============================================

function init() {
    Settings.load();
    // Create graph UI
    createGraphPanel();
    createToggleButton();

    // Initial injection
    setTimeout(() => {
        console.log(`${LOG_PREFIX} 🚀 Initial injection after page load`);
        injectTimestamps();
    }, CONFIG.initialDelay);

    // Watch for URL changes
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            console.log(`${LOG_PREFIX} 🔗 URL changed, re-injecting...`);
            ClaudeAPI.checkConversationChange();
            clearMetadata();
            setTimeout(() => {
                injectTimestamps();
            }, 1500);
        }
    }).observe(document, { subtree: true, childList: true });

    // Watch for new user messages (they appear immediately when sent)
    let lastMessageCount = 0;
    const userMessageObserver = new MutationObserver(debounce(() => {
        const messageContainers = document.querySelectorAll(SELECTORS.message.containers[0]);

        // If new messages appeared, inject timestamps
        if (messageContainers.length > lastMessageCount) {
            console.log(`${LOG_PREFIX} 📨 New user message detected, updating...`);
            lastMessageCount = messageContainers.length;
            setTimeout(() => {
                injectTimestamps();
            }, 300);
        }
    }, 200));

    // Observe message list for new messages
    const observeMessages = () => {
        const messageList = document.querySelector(SELECTORS.chat.messageListContainer);
        if (messageList) {
            userMessageObserver.observe(messageList, {
                childList: true,
                subtree: false
            });
            // Initialize message count
            lastMessageCount = messageList.querySelectorAll(SELECTORS.chat.turnBase).length;
        } else {
            // Retry if container not found
            setTimeout(observeMessages, 500);
        }
    };

    setTimeout(observeMessages, 1000);

    // Watch for streaming completion
    const streamingObserver = new MutationObserver(debounce((mutations) => {
        for (const mutation of mutations) {
            if (mutation.type === 'attributes' &&
                mutation.attributeName === ATTRIBUTES.IS_STREAMING) {
                const target = mutation.target;
                const isStreaming = target.getAttribute(ATTRIBUTES.IS_STREAMING);

                // When streaming completes
                if (isStreaming === 'false') {
                    console.log(`${LOG_PREFIX} ✅ Streaming completed, updating metadata...`);
                    setTimeout(() => {
                        injectTimestamps();
                    }, 500);
                }
            }
        }
    }, CONFIG.streamingDebounce));

    // Start observing for streaming changes
    const observeStreaming = () => {
        const messageContainers = document.querySelectorAll(SELECTORS.controls.streaming[0]);
        messageContainers.forEach(container => {
            streamingObserver.observe(container, {
                attributes: true,
                attributeFilter: [ATTRIBUTES.IS_STREAMING]
            });
        });
    };

    // Initial observation
    setTimeout(observeStreaming, 1000);

    // Re-observe when new messages appear
    new MutationObserver(debounce(() => {
        observeStreaming();
    }, 500)).observe(document.body, {
        childList: true,
        subtree: true
    });

    // Watch for version switching clicks
    document.addEventListener('click', debounce((e) => {
        const target = e.target.closest('button');
        if (target) {
            // Check if it's a version navigation button
            const hasArrowIcon = findElements(SELECTORS.controls.navigationButtons, target).length > 0;

            if (hasArrowIcon) {
                console.log(`${LOG_PREFIX} 🔄 Version switch detected`);
                setTimeout(() => {
                    injectTimestamps();
                }, 500);
            }
        }
    }, 100), true);

    // Watch for edit form changes
    let editFormPresent = false;
    const editModeObserver = new MutationObserver(debounce(() => {
        const hasEditForm = document.querySelector(SELECTORS.controls.editForm) !== null;

        // Edit form disappeared (user saved/canceled)
        if (editFormPresent && !hasEditForm) {
            console.log(`${LOG_PREFIX} 💾 Edit completed, restoring metadata...`);
            setTimeout(() => {
                injectTimestamps();
            }, 200);
        }

        editFormPresent = hasEditForm;
    }, 100));

    editModeObserver.observe(document.body, {
        childList: true,
        subtree: true
    });
}

// Start when DOM is ready
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
} else {
    init();
}

// =============================================
// SETTINGS and STYLES
// =============================================

function showGraphSettings() {
    const modal = document.createElement('div');
    modal.className = 'cpm-graph-modal-overlay';

    // Get current settings
    const currentDirection = Settings.get('graph', 'direction') || 'left-right';
    const currentViewMode = Settings.get('graph', 'viewMode') || 'tree';
    const currentGrouping = Settings.get('graph', 'nodeGrouping') || 'individual';
    const currentEdgeModes = Settings.get('graph', 'edgeModes') || ['parent-child'];
    const currentPanelMode = Settings.get('graph', 'panelMode') || 'push';

    modal.innerHTML = `
        <div class="cpm-graph-modal cpm-settings-modal">
            <div class="cpm-graph-modal-header">
                <h3>⚙️ Graph Settings</h3>
                <button class="cpm-graph-modal-close">×</button>
            </div>

            <div class="cpm-graph-modal-body">
                <!-- Layout Direction -->
                <div class="setting-block">
                    <div class="setting-header">
                        <div class="setting-icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <path d="M12 5v14M5 12l7 7 7-7"/>
                            </svg>
                        </div>
                        <div class="setting-info">
                            <div class="setting-label">Layout Direction</div>
                            <div class="setting-desc">Choose how the tree flows</div>
                        </div>
                    </div>
                    <div class="direction-pills">
                        <div class="direction-pill">
                            <input type="radio" name="direction" id="dir-top" value="top-down" ${currentDirection === 'top-down' ? 'checked' : ''}>
                            <label for="dir-top">
                                <svg viewBox="0 0 40 40" fill="none">
                                    <circle cx="20" cy="8" r="3" class="node" fill="#64748b"/>
                                    <circle cx="13" cy="20" r="3" class="node" fill="#64748b"/>
                                    <circle cx="27" cy="20" r="3" class="node" fill="#64748b"/>
                                    <circle cx="20" cy="32" r="3" class="node" fill="#64748b"/>
                                    <path d="M19 11 L14 17" class="edge" stroke="#475569" stroke-width="1.5"/>
                                    <path d="M21 11 L26 17" class="edge" stroke="#475569" stroke-width="1.5"/>
                                    <path d="M15 23 L19 29" class="edge" stroke="#475569" stroke-width="1.5"/>
                                    <path d="M25 23 L21 29" class="edge" stroke="#475569" stroke-width="1.5"/>
                                </svg>
                                <span class="direction-text">Top-Down</span>
                            </label>
                        </div>
                        <div class="direction-pill">
                            <input type="radio" name="direction" id="dir-left" value="left-right" ${currentDirection === 'left-right' ? 'checked' : ''}>
                            <label for="dir-left">
                                <svg viewBox="0 0 40 40" fill="none">
                                    <circle cx="8" cy="20" r="3" class="node" fill="#3b82f6"/>
                                    <circle cx="20" cy="13" r="3" class="node" fill="#3b82f6"/>
                                    <circle cx="20" cy="27" r="3" class="node" fill="#3b82f6"/>
                                    <circle cx="32" cy="20" r="3" class="node" fill="#3b82f6"/>
                                    <path d="M11 20 L17 14" class="edge" stroke="#60a5fa" stroke-width="1.5"/>
                                    <path d="M11 20 L17 26" class="edge" stroke="#60a5fa" stroke-width="1.5"/>
                                    <path d="M23 14 L29 19" class="edge" stroke="#60a5fa" stroke-width="1.5"/>
                                    <path d="M23 26 L29 21" class="edge" stroke="#60a5fa" stroke-width="1.5"/>
                                </svg>
                                <span class="direction-text">Left-Right</span>
                            </label>
                        </div>
                        <div class="direction-pill">
                            <input type="radio" name="direction" id="dir-bottom" value="bottom-up" ${currentDirection === 'bottom-up' ? 'checked' : ''}>
                            <label for="dir-bottom">
                                <svg viewBox="0 0 40 40" fill="none">
                                    <circle cx="20" cy="32" r="3" class="node" fill="#64748b"/>
                                    <circle cx="13" cy="20" r="3" class="node" fill="#64748b"/>
                                    <circle cx="27" cy="20" r="3" class="node" fill="#64748b"/>
                                    <circle cx="20" cy="8" r="3" class="node" fill="#64748b"/>
                                    <path d="M19 29 L14 23" class="edge" stroke="#475569" stroke-width="1.5"/>
                                    <path d="M21 29 L26 23" class="edge" stroke="#475569" stroke-width="1.5"/>
                                    <path d="M15 17 L19 11" class="edge" stroke="#475569" stroke-width="1.5"/>
                                    <path d="M25 17 L21 11" class="edge" stroke="#475569" stroke-width="1.5"/>
                                </svg>
                                <span class="direction-text">Bottom-Up</span>
                            </label>
                        </div>
                    </div>
                </div>

                <!-- View Mode -->
                <div class="setting-block">
                    <div class="setting-header">
                        <div class="setting-icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <circle cx="12" cy="5" r="2"/>
                                <circle cx="7" cy="13" r="2"/>
                                <circle cx="17" cy="13" r="2"/>
                                <path d="M12 7v4M7 11l5-2M17 11l-5-2"/>
                            </svg>
                        </div>
                        <div class="setting-info">
                            <div class="setting-label">View Mode</div>
                            <div class="setting-desc">Display as tree or branches</div>
                        </div>
                    </div>
                    <div class="view-toggle">
                        <button class="view-option ${currentViewMode === 'tree' ? 'active' : ''}" data-view="tree">
                            <svg viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="2">
                                <circle cx="20" cy="8" r="3"/>
                                <circle cx="13" cy="22" r="3"/>
                                <circle cx="27" cy="22" r="3"/>
                                <circle cx="20" cy="32" r="3"/>
                                <path d="M19 11 L14 19"/>
                                <path d="M21 11 L26 19"/>
                                <path d="M15 25 L19 29"/>
                                <path d="M25 25 L21 29"/>
                            </svg>
                            <span class="view-label">Tree</span>
                            <span class="view-desc">Hierarchical structure</span>
                        </button>
                        <button class="view-option ${currentViewMode === 'tree-fixed' ? 'active' : ''}" data-view="tree-fixed">
                            <svg viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="2">
                                <circle cx="14" cy="8" r="3"/>
                                <circle cx="14" cy="18" r="3"/>
                                <circle cx="26" cy="18" r="3"/>
                                <circle cx="26" cy="28" r="3"/>
                                <path d="M14 11 L14 15"/>
                                <path d="M14 18 L26 18" stroke-dasharray="3,2"/>
                                <path d="M26 21 L26 25"/>
                            </svg>
                            <span class="view-label">Tree Fixed</span>
                            <span class="view-desc">Branches stay aligned</span>
                        </button>
                        <button class="view-option ${currentViewMode === 'branches' ? 'active' : ''}" data-view="branches">
                            <svg viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="2">
                                <circle cx="8" cy="12" r="2.5"/>
                                <circle cx="18" cy="12" r="2.5"/>
                                <circle cx="28" cy="12" r="2.5"/>
                                <line x1="10.5" y1="12" x2="15.5" y2="12"/>
                                <line x1="20.5" y1="12" x2="25.5" y2="12"/>
                                <circle cx="8" cy="28" r="2.5"/>
                                <circle cx="18" cy="28" r="2.5"/>
                                <circle cx="28" cy="28" r="2.5"/>
                                <circle cx="38" cy="28" r="2.5"/>
                                <line x1="10.5" y1="28" x2="15.5" y2="28"/>
                                <line x1="20.5" y1="28" x2="25.5" y2="28"/>
                                <line x1="30.5" y1="28" x2="35.5" y2="28"/>
                            </svg>
                            <span class="view-label">Branches</span>
                            <span class="view-desc">Test - Linear sequences</span>
                        </button>
                    </div>
                </div>

                <!-- Node Grouping -->
                <div class="setting-block">
                    <div class="setting-header">
                        <div class="setting-icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <rect x="3" y="3" width="7" height="7" rx="1"/>
                                <rect x="14" y="3" width="7" height="7" rx="1"/>
                                <rect x="3" y="14" width="7" height="7" rx="1"/>
                                <rect x="14" y="14" width="7" height="7" rx="1"/>
                            </svg>
                        </div>
                        <div class="setting-info">
                            <div class="setting-label">Node Grouping</div>
                            <div class="setting-desc">How messages are grouped</div>
                        </div>
                    </div>
                    <div class="grouping-options">
                        <div class="grouping-card">
                            <input type="radio" name="grouping" id="group-individual" value="individual" ${currentGrouping === 'individual' ? 'checked' : ''}>
                            <label for="group-individual">
                                <div class="grouping-icon">
                                    <svg viewBox="0 0 40 40" fill="none">
                                        <circle cx="10" cy="10" r="4" class="node" fill="#64748b"/>
                                        <circle cx="20" cy="10" r="4" class="node" fill="#64748b"/>
                                        <circle cx="30" cy="10" r="4" class="node" fill="#64748b"/>
                                        <circle cx="10" cy="30" r="4" class="node" fill="#64748b"/>
                                        <circle cx="20" cy="30" r="4" class="node" fill="#64748b"/>
                                        <circle cx="30" cy="30" r="4" class="node" fill="#64748b"/>
                                    </svg>
                                </div>
                                <div class="grouping-content">
                                    <div class="grouping-title">Individual Messages</div>
                                    <div class="grouping-desc">Each message separate</div>
                                </div>
                                <div class="grouping-check">
                                    <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3">
                                        <polyline points="20 6 9 17 4 12"/>
                                    </svg>
                                </div>
                            </label>
                        </div>
                        <div class="grouping-card">
                            <input type="radio" name="grouping" id="group-pairs" value="pairs" ${currentGrouping === 'pairs' ? 'checked' : ''}>
                            <label for="group-pairs">
                                <div class="grouping-icon">
                                    <svg viewBox="0 0 40 40" fill="none">
                                        <rect x="4" y="4" width="14" height="12" rx="2" fill="none" stroke="#64748b" stroke-width="1.5"/>
                                        <circle cx="9" cy="10" r="2.5" class="node" fill="#64748b"/>
                                        <circle cx="13" cy="10" r="2.5" class="node" fill="#64748b"/>
                                        <rect x="22" y="24" width="14" height="12" rx="2" fill="none" stroke="#64748b" stroke-width="1.5"/>
                                        <circle cx="27" cy="30" r="2.5" class="node" fill="#64748b"/>
                                        <circle cx="31" cy="30" r="2.5" class="node" fill="#64748b"/>
                                    </svg>
                                </div>
                                <div class="grouping-content">
                                    <div class="grouping-title">Human-Assistant Pairs</div>
                                    <div class="grouping-desc">Group Q&A together</div>
                                </div>
                                <div class="grouping-check">
                                    <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3">
                                        <polyline points="20 6 9 17 4 12"/>
                                    </svg>
                                </div>
                            </label>
                        </div>
                    </div>
                </div>

                <!-- Connection Type -->
                <div class="setting-block">
                    <div class="setting-header">
                        <div class="setting-icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <circle cx="6" cy="6" r="3"/>
                                <circle cx="18" cy="18" r="3"/>
                                <path d="M8.5 8.5l7 7"/>
                            </svg>
                        </div>
                        <div class="setting-info">
                            <div class="setting-label">Connection Type</div>
                            <div class="setting-desc">How nodes are connected (can select both)</div>
                        </div>
                    </div>
                    <div class="edge-buttons">
                        <button class="edge-btn ${currentEdgeModes.includes('parent-child') ? 'active' : ''}" data-edge="parent-child">
                            <svg viewBox="0 0 40 40" fill="none">
                                <circle cx="20" cy="8" r="3" class="node" fill="#3b82f6"/>
                                <circle cx="10" cy="32" r="3" class="node" fill="#3b82f6"/>
                                <circle cx="20" cy="32" r="3" class="node" fill="#3b82f6"/>
                                <circle cx="30" cy="32" r="3" class="node" fill="#3b82f6"/>
                                <path d="M20 11 L10 29" class="edge" stroke="#60a5fa" stroke-width="1.5"/>
                                <path d="M20 11 L20 29" class="edge" stroke="#60a5fa" stroke-width="1.5"/>
                                <path d="M20 11 L30 29" class="edge" stroke="#60a5fa" stroke-width="1.5"/>
                            </svg>
                            <span class="edge-label">Parent-Child</span>
                            <span class="edge-desc">Hierarchical relationships</span>
                        </button>
                        <button class="edge-btn ${currentEdgeModes.includes('siblings') ? 'active' : ''}" data-edge="siblings">
                            <svg viewBox="0 0 40 40" fill="none">
                                <circle cx="10" cy="20" r="3" class="node" fill="#64748b"/>
                                <circle cx="20" cy="20" r="3" class="node" fill="#64748b"/>
                                <circle cx="30" cy="20" r="3" class="node" fill="#64748b"/>
                                <line x1="13" y1="20" x2="17" y2="20" class="edge" stroke="#475569" stroke-width="1.5"/>
                                <line x1="23" y1="20" x2="27" y2="20" class="edge" stroke="#475569" stroke-width="1.5"/>
                            </svg>
                            <span class="edge-label">Siblings</span>
                            <span class="edge-desc">Alternative versions</span>
                        </button>
                    </div>
                </div>

                <!-- Panel Behavior -->
                <div class="setting-block">
                    <div class="setting-header">
                        <div class="setting-icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <rect x="3" y="3" width="18" height="18" rx="2"/>
                                <path d="M15 3v18"/>
                            </svg>
                        </div>
                        <div class="setting-info">
                            <div class="setting-label">Panel Behavior</div>
                            <div class="setting-desc">Push content or float over</div>
                        </div>
                    </div>
                    <div class="panel-switch ${currentPanelMode === 'push' ? 'active' : ''}" data-panel="${currentPanelMode}">
                        <div class="panel-info">
                            <div class="panel-icon">
                                <svg viewBox="0 0 40 40" fill="none" stroke="#60a5fa" stroke-width="2">
                                    <rect x="6" y="8" width="20" height="24" rx="2"/>
                                    <rect x="29" y="8" width="10" height="24" rx="2" fill="rgba(59, 130, 246, 0.2)"/>
                                    <path d="M26 20 L29 20" marker-end="url(#arrow)"/>
                                    <defs>
                                        <marker id="arrow" markerWidth="6" markerHeight="6" refX="3" refY="2" orient="auto">
                                            <polygon points="0 0, 3 2, 0 4" fill="#60a5fa"/>
                                        </marker>
                                    </defs>
                                </svg>
                            </div>
                            <div class="panel-text">
                                <div class="panel-title">${currentPanelMode === 'push' ? 'Push Mode' : 'Overlay Mode'}</div>
                                <div class="panel-desc">${currentPanelMode === 'push' ? 'Panel pushes content aside' : 'Panel floats over content'}</div>
                            </div>
                        </div>
                        <div class="toggle-switch">
                            <div class="toggle-slider"></div>
                        </div>
                    </div>
                </div>

                <!-- Show Node Text -->
                <div class="setting-block">
                    <div class="setting-header">
                        <div class="setting-icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <path d="M4 7h16M4 12h16M4 17h10"/>
                            </svg>
                        </div>
                        <div class="setting-info">
                            <div class="setting-label">Content Preview</div>
                            <div class="setting-desc">Show text under nodes</div>
                        </div>
                    </div>
                    <div class="panel-switch ${Settings.get('graph', 'showNodeText') ? 'active' : ''}" id="show-text-switch">
                        <div class="panel-info">
                            <div class="panel-icon">
                                <svg viewBox="0 0 40 40" fill="none" stroke="#60a5fa" stroke-width="2">
                                    <circle cx="20" cy="12" r="4"/>
                                    <rect x="10" y="22" width="20" height="10" rx="2" fill="rgba(59, 130, 246, 0.2)"/>
                                    <line x1="13" y1="25" x2="27" y2="25"/>
                                    <line x1="13" y1="29" x2="23" y2="29"/>
                                </svg>
                            </div>
                            <div class="panel-text">
                                <div class="panel-title">${Settings.get('graph', 'showNodeText') ? 'Preview On' : 'Preview Off'}</div>
                                <div class="panel-desc">${Settings.get('graph', 'showNodeText') ? 'Text visible below nodes' : 'Text hidden'}</div>
                            </div>
                        </div>
                        <div class="toggle-switch">
                            <div class="toggle-slider"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="cpm-graph-modal-footer">
                <button class="cpm-graph-btn cpm-graph-btn-secondary" id="cpm-graph-settings-reset">Reset to Defaults</button>
                <button class="cpm-graph-btn cpm-graph-btn-primary" id="cpm-graph-settings-apply">Apply Changes</button>
            </div>
        </div>
    `;

    document.body.appendChild(modal);

    // State management
    let selectedDirection = currentDirection;
    let selectedViewMode = currentViewMode;
    let selectedGrouping = currentGrouping;
    let selectedEdgeModes = [...currentEdgeModes];
    let selectedPanelMode = currentPanelMode;
    let selectedShowNodeText = Settings.get('graph', 'showNodeText') || false;

    // Direction change
    modal.querySelectorAll('input[name="direction"]').forEach(radio => {
        radio.addEventListener('change', (e) => {
            selectedDirection = e.target.value;
        });
    });

    // View mode toggle
    modal.querySelectorAll('.view-option').forEach(btn => {
        btn.addEventListener('click', () => {
            modal.querySelectorAll('.view-option').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            selectedViewMode = btn.dataset.view;
        });
    });

    // Grouping change
    modal.querySelectorAll('input[name="grouping"]').forEach(radio => {
        radio.addEventListener('change', (e) => {
            selectedGrouping = e.target.value;
        });
    });

    // Edge mode toggle
    modal.querySelectorAll('.edge-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            const edgeType = btn.dataset.edge;

            if (selectedEdgeModes.includes(edgeType)) {
                selectedEdgeModes = selectedEdgeModes.filter(mode => mode !== edgeType);
                btn.classList.remove('active');
            } else {
                selectedEdgeModes.push(edgeType);
                btn.classList.add('active');
            }
        });
    });

    // Panel mode toggle
    const panelSwitch = modal.querySelector('.panel-switch');
    panelSwitch.addEventListener('click', () => {
        panelSwitch.classList.toggle('active');
        const isActive = panelSwitch.classList.contains('active');
        selectedPanelMode = isActive ? 'push' : 'overlay';

        const title = panelSwitch.querySelector('.panel-title');
        const desc = panelSwitch.querySelector('.panel-desc');
        if (isActive) {
            title.textContent = 'Push Mode';
            desc.textContent = 'Panel pushes content aside';
        } else {
            title.textContent = 'Overlay Mode';
            desc.textContent = 'Panel floats over content';
        }
    });

    // Show node text toggle
    const showTextSwitch = modal.querySelector('#show-text-switch');
    showTextSwitch.addEventListener('click', () => {
        showTextSwitch.classList.toggle('active');
        const isActive = showTextSwitch.classList.contains('active');
        selectedShowNodeText = isActive;

        const title = showTextSwitch.querySelector('.panel-title');
        const desc = showTextSwitch.querySelector('.panel-desc');
        if (isActive) {
            title.textContent = 'Preview On';
            desc.textContent = 'Text visible below nodes';
        } else {
            title.textContent = 'Preview Off';
            desc.textContent = 'Text hidden';
        }
    });

    // Close modal
    const closeModal = () => modal.remove();
    modal.addEventListener('click', (e) => {
        if (e.target === modal) closeModal();
    });
    modal.querySelector('.cpm-graph-modal-close').addEventListener('click', closeModal);

    // Reset button
    modal.querySelector('#cpm-graph-settings-reset').addEventListener('click', () => {
        const defaults = Settings.defaults.graph;

        selectedDirection = defaults.direction;
        selectedViewMode = defaults.viewMode;
        selectedGrouping = defaults.nodeGrouping;
        selectedEdgeModes = [...defaults.edgeModes];
        selectedPanelMode = defaults.panelMode;
        selectedShowNodeText = defaults.showNodeText;

        // Direction radios
        modal.querySelectorAll('input[name="direction"]').forEach(radio => {
            radio.checked = radio.value === defaults.direction;
        });

        // Grouping radios
        modal.querySelectorAll('input[name="grouping"]').forEach(radio => {
            radio.checked = radio.value === defaults.nodeGrouping;
        });

        // View mode buttons
        modal.querySelectorAll('.view-option').forEach(btn => {
            btn.classList.toggle('active', btn.dataset.view === defaults.viewMode);
        });

        // Edge mode buttons
        modal.querySelectorAll('.edge-btn').forEach(btn => {
            const isActive = defaults.edgeModes.includes(btn.dataset.edge);
            btn.classList.toggle('active', isActive);
        });

        // Panel mode switch
        const isPush = defaults.panelMode === 'push';
        panelSwitch.classList.toggle('active', isPush);
        panelSwitch.querySelector('.panel-title').textContent = isPush ? 'Push Mode' : 'Overlay Mode';
        panelSwitch.querySelector('.panel-desc').textContent = isPush
            ? 'Panel pushes content aside'
            : 'Panel floats over content';

        // Show node text switch
        const showText = defaults.showNodeText;
        showTextSwitch.classList.toggle('active', showText);
        showTextSwitch.querySelector('.panel-title').textContent = showText ? 'Preview On' : 'Preview Off';
        showTextSwitch.querySelector('.panel-desc').textContent = showText
            ? 'Text visible below nodes'
            : 'Text hidden';
    });

    // Apply button
    modal.querySelector('#cpm-graph-settings-apply').addEventListener('click', () => {
        Settings.set('graph', 'direction', selectedDirection);
        Settings.set('graph', 'viewMode', selectedViewMode);
        Settings.set('graph', 'nodeGrouping', selectedGrouping);
        Settings.set('graph', 'edgeModes', selectedEdgeModes);
        Settings.set('graph', 'panelMode', selectedPanelMode);
        Settings.set('graph', 'showNodeText', selectedShowNodeText);
        Settings.save();
        closeModal();

        // Re-render graph with new settings
        if (currentTreeData) {
            renderGraph(currentTreeData);
        }

        // Apply panel mode if panel is open
        if (graphPanel && graphPanel.style.right === '0px') {
            applyPanelMode(selectedPanelMode);
        }
    });
}

GM_addStyle(`
    /* Base styles for the script */
    .claude-timestamp-metadata {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    }

    /* Styles for the graph */
    #claude-graph-panel {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    }

    /* Claude-style Resizer */
    .claude-graph-resizer-group {
        position: absolute;
        top: 50%;
        left: -7px;
        width: 14px;
        height: 100%;
        transform: translateY(-50%);
        cursor: col-resize;
        z-index: 10;
        user-select: none;
        display: grid;
        place-items: center;
        transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        animation: resizer-fade-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .claude-graph-resizer-line {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 50%;
        width: 0.5px;
        background: rgba(148, 163, 184, 0.2);
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        transition-delay: 50ms;
        transform: translateX(-50%);
        transform-origin: center;
    }

    .claude-graph-resizer-handle {
        position: relative;
        width: 8px;
        height: 24px;
        border-radius: 9999px;
        border: 0.5px solid rgba(148, 163, 184, 0.3);
        background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
        box-shadow:
            0 1px 3px rgba(0, 0, 0, 0.2),
            inset 0 1px 0 rgba(255, 255, 255, 0.05);
        cursor: col-resize;
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        z-index: 1;
        overflow: hidden;
    }

    .claude-graph-resizer-handle::before {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        width: 2px;
        height: 8px;
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(148, 163, 184, 0.15) 20%,
            rgba(148, 163, 184, 0.25) 50%,
            rgba(148, 163, 184, 0.15) 80%,
            transparent
        );
        border-radius: 1px;
        transform: translate(-50%, -50%);
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .claude-graph-resizer-group:hover .claude-graph-resizer-line {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(59, 130, 246, 0.4) 20%,
            rgba(59, 130, 246, 0.7) 50%,
            rgba(59, 130, 246, 0.4) 80%,
            transparent
        );
        width: 2px;
        box-shadow:
            0 0 8px rgba(59, 130, 246, 0.3),
            0 0 16px rgba(59, 130, 246, 0.1);
    }

    .claude-graph-resizer-group:hover .claude-graph-resizer-handle {
        border-color: rgba(59, 130, 246, 0.6);
        background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
        box-shadow:
            0 0 0 2px rgba(59, 130, 246, 0.2),
            0 2px 8px rgba(0, 0, 0, 0.3),
            0 0 12px rgba(59, 130, 246, 0.4),
            inset 0 1px 0 rgba(255, 255, 255, 0.2);
        transform: scale(1.05);
        animation: resizer-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
    }

    .claude-graph-resizer-group:hover .claude-graph-resizer-handle::before {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(255, 255, 255, 0.2) 20%,
            rgba(255, 255, 255, 0.4) 50%,
            rgba(255, 255, 255, 0.2) 80%,
            transparent
        );
        box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
    }

    .claude-graph-resizer-group:active .claude-graph-resizer-handle {
        border-color: rgba(37, 99, 235, 0.8);
        background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
        box-shadow:
            0 0 0 3px rgba(37, 99, 235, 0.4),
            0 4px 12px rgba(0, 0, 0, 0.4),
            0 0 20px rgba(37, 99, 235, 0.6),
            0 0 40px rgba(37, 99, 235, 0.3),
            inset 0 1px 0 rgba(255, 255, 255, 0.3),
            inset 0 -1px 0 rgba(0, 0, 0, 0.2);
        transform: scale(1.1);
        animation: none;
    }

    .claude-graph-resizer-group:active .claude-graph-resizer-line {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(37, 99, 235, 0.6) 10%,
            rgba(37, 99, 235, 0.9) 50%,
            rgba(37, 99, 235, 0.6) 90%,
            transparent
        );
        width: 3px;
        box-shadow:
            0 0 12px rgba(37, 99, 235, 0.5),
            0 0 24px rgba(37, 99, 235, 0.3),
            0 0 36px rgba(37, 99, 235, 0.1);
    }

    .claude-graph-resizer-group:active .claude-graph-resizer-handle::before {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(255, 255, 255, 0.4) 10%,
            rgba(255, 255, 255, 0.7) 50%,
            rgba(255, 255, 255, 0.4) 90%,
            transparent
        );
        box-shadow:
            0 0 6px rgba(255, 255, 255, 0.5),
            0 0 12px rgba(255, 255, 255, 0.2);
        transform: translate(-50%, -50%) scale(1.2);
    }

    .claude-graph-resizer-group.resizing .claude-graph-resizer-handle {
        border-color: rgba(37, 99, 235, 0.8);
        background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
        box-shadow:
            0 0 0 3px rgba(37, 99, 235, 0.3),
            0 4px 12px rgba(0, 0, 0, 0.4),
            0 0 16px rgba(37, 99, 235, 0.5);
        transform: scale(1.08);
    }

    .claude-graph-resizer-group.resizing .claude-graph-resizer-line {
        background: linear-gradient(
            to bottom,
            transparent,
            rgba(37, 99, 235, 0.5) 20%,
            rgba(37, 99, 235, 0.8) 50%,
            rgba(37, 99, 235, 0.5) 80%,
            transparent
        );
        width: 2.5px;
        box-shadow:
            0 0 10px rgba(37, 99, 235, 0.4),
            0 0 20px rgba(37, 99, 235, 0.2);
    }

    .claude-graph-resizer-group:focus-visible .claude-graph-resizer-handle {
        outline: 2px solid rgba(59, 130, 246, 0.6);
        outline-offset: 2px;
    }

    @keyframes resizer-fade-in {
        from {
            opacity: 0;
            transform: translateY(-50%) scale(0.8);
        }
        to {
            opacity: 1;
            transform: translateY(-50%) scale(1);
        }
    }

    @keyframes resizer-pulse {
        0%, 100% {
            box-shadow:
                0 0 0 2px rgba(59, 130, 246, 0.2),
                0 2px 8px rgba(0, 0, 0, 0.3),
                0 0 12px rgba(59, 130, 246, 0.4);
        }
        50% {
            box-shadow:
                0 0 0 3px rgba(59, 130, 246, 0.3),
                0 2px 8px rgba(0, 0, 0, 0.3),
                0 0 16px rgba(59, 130, 246, 0.5);
        }
    }

    /* Toggle Button with Branch Indicator */
    .graph-toggle-indicator {
        position: fixed;
        top: 290px;
        right: 0;
        width: 50px;
        height: 50px;
        border-radius: 16px 0 0 16px;
        background: linear-gradient(135deg, rgba(14, 20, 30, 0.95) 0%, rgba(10, 17, 32, 0.9) 100%);
        border: 1.5px solid rgba(59, 130, 246, 0.4);
        cursor: pointer;
        z-index: 10001;
        backdrop-filter: blur(10px);
        /* box-shadow: */
            /* 0 0 0 1px rgba(56, 189, 248, 0.2), */
            /* 0 0 25px rgba(59, 130, 246, 0.4), */
            /* inset 0 1px 0 rgba(148, 163, 184, 0.1); */
        transition: all 0.3s ease;
    }
    .indicator-wrapper {
        position: relative;
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .tree-network-icon {
        width: 26px;
        height: 26px;
        transition: all 0.3s ease;
    }
    .branch-indicator {
        position: absolute;
        top: 4px;
        right: 8px;
        width: 10px;
        height: 10px;
        border-radius: 50%;
        background: #3b82f6;
        border: 2px solid #0f172a;
        display: none;
        transition: all 0.3s ease;
    }
    /* Only one branch - blue, no animation - not work */
    .graph-toggle-indicator.single-branch .branch-indicator {
        background: #3b82f6;
        display: block;
    }
    /* Multiple branches, on main branch - green, soft pulse */
    .graph-toggle-indicator.main-branch .branch-indicator {
        background: #22c55e;
        display: block;
        animation: soft-pulse 2s infinite;
    }
    /* On a side branch - orange, active pulse - not work */
    .graph-toggle-indicator.side-branch .branch-indicator {
        background: #f59e0b;
        display: block;
        animation: pulse 1s infinite;
    }
    @keyframes soft-pulse {
        0%, 100% { transform: scale(1); opacity: 1; }
        50% { transform: scale(1.1); opacity: 0.9; }
    }
    /* animate-pulse flex flex-col gap-8 fade-out-bottom h-[calc(100vh-12.5rem)] */
    /* @keyframes pulse { */
        /* 0%, 100% { transform: scale(1); opacity: 1; } */
        /* 50% { transform: scale(1.2); opacity: 0.8; } */
    /* } */
    .graph-toggle-indicator:hover {
        border-color: rgba(59, 130, 246, 0.7);
        transform: scale(1.05);
        box-shadow:
            0 0 0 1px rgba(56, 189, 248, 0.4),
            0 0 40px rgba(59, 130, 246, 0.6),
            0 0 60px rgba(59, 130, 246, 0.3),
            inset 0 1px 0 rgba(148, 163, 184, 0.2);
    }
    .graph-toggle-indicator:hover .tree-network-icon {
        transform: rotate(45deg);
    }

    .highlight-pulse {
        animation: cpm-highlight-pulse 3s ease-out;
    }

    @keyframes cpm-highlight-pulse {
        0%, 100% { background-color: rgba(255, 243, 205, 0); }
        20% { background-color: rgba(255, 243, 205, 1); }
    }
    /* Modal Styles */
    .cpm-graph-modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.6);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 10001;
        animation: fadeIn 0.2s ease;
    }

    @keyframes fadeIn {
        from { opacity: 0; }
        to { opacity: 1; }
    }

    .cpm-graph-modal {
        background: linear-gradient(to bottom, #1e293b 0%, #0f172a 100%);
        border-radius: 16px;
        box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(59, 130, 246, 0.2);
        min-width: 420px;
        max-width: 480px;
        animation: slideUp 0.3s ease;
        overflow: hidden;
    }

    @keyframes slideUp {
        from {
            transform: translateY(20px);
            opacity: 0;
        }
        to {
            transform: translateY(0);
            opacity: 1;
        }
    }

    .cpm-graph-modal-header {
        padding: 24px;
        background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
        border-bottom: 1px solid rgba(59, 130, 246, 0.2);
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .cpm-graph-modal-header h3 {
        margin: 0;
        color: #e2e8f0;
        font-size: 20px;
        font-weight: 700;
        background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
    }

    .cpm-graph-modal-close {
        background: rgba(51, 65, 85, 0.5);
        border: none;
        color: #94a3b8;
        font-size: 24px;
        line-height: 1;
        cursor: pointer;
        padding: 0;
        width: 36px;
        height: 36px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 8px;
        transition: all 0.2s;
        font-weight: 300;
    }

    .cpm-graph-modal-close:hover {
        background: rgba(239, 68, 68, 0.2);
        color: #f87171;
        transform: rotate(90deg);
    }

    .cpm-graph-modal-body {
        padding: 24px;
        max-height: 400px;
        overflow-y: auto;
    }

    .cpm-graph-modal-body::-webkit-scrollbar {
        width: 6px;
    }

    .cpm-graph-modal-body::-webkit-scrollbar-track {
        background: rgba(51, 65, 85, 0.3);
        border-radius: 3px;
    }

    .cpm-graph-modal-body::-webkit-scrollbar-thumb {
        background: rgba(148, 163, 184, 0.5);
        border-radius: 3px;
    }

    .cpm-graph-modal-body::-webkit-scrollbar-thumb:hover {
        background: rgba(148, 163, 184, 0.7);
    }

    .cpm-graph-info-row {
        display: flex;
        margin-bottom: 14px;
        align-items: flex-start;
        padding: 8px 12px;
        background: rgba(51, 65, 85, 0.3);
        border-radius: 6px;
        transition: background 0.2s;
    }

    .cpm-graph-info-row:hover {
        background: rgba(51, 65, 85, 0.5);
    }

    .cpm-graph-info-row:last-child {
        margin-bottom: 0;
    }

    .cpm-graph-label {
        color: #94a3b8;
        font-size: 13px;
        font-weight: 600;
        min-width: 110px;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        font-size: 11px;
    }

    .cpm-graph-value {
        color: #e2e8f0;
        font-size: 14px;
        word-break: break-all;
        line-height: 1.5;
    }

    .cpm-graph-content-section {
        padding: 16px 24px;
        border-top: 1px solid rgba(51, 65, 85, 0.5);
        background: rgba(15, 23, 42, 0.3);
    }

    .cpm-graph-content-header {
        color: #94a3b8;
        font-size: 11px;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        margin-bottom: 8px;
    }

    .cpm-graph-content-box {
        background: rgba(15, 23, 42, 0.6);
        border: 1px solid rgba(71, 85, 105, 0.3);
        border-radius: 8px;
        padding: 12px;
        max-height: 200px;
        overflow-y: auto;
    }

    .cpm-graph-content-text {
        color: #cbd5e1;
        font-family: 'Roboto Mono', monospace;
        font-size: 11px;
        line-height: 1.6;
        white-space: pre-wrap;
        word-wrap: break-word;
        overflow-wrap: break-word;
    }

    .cpm-graph-content-toggle {
        margin-top: 8px;
        padding: 6px 12px;
        width: 100%;
        background: rgba(59, 130, 246, 0.2);
        border: 1px solid rgba(59, 130, 246, 0.3);
        border-radius: 6px;
        color: #60a5fa;
        font-size: 11px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s;
    }

    .cpm-graph-content-toggle:hover {
        background: rgba(59, 130, 246, 0.3);
        border-color: rgba(59, 130, 246, 0.5);
    }

    .cpm-graph-status-main {
        color: #60a5fa;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        font-size: 12px;
        background: rgba(59, 130, 246, 0.2);
        padding: 2px 8px;
        border-radius: 4px;
        display: inline-block;
    }

    .cpm-graph-status-side {
        color: #94a3b8;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        font-size: 12px;
        background: rgba(100, 116, 139, 0.2);
        padding: 2px 8px;
        border-radius: 4px;
        display: inline-block;
    }

    .cpm-graph-modal-footer {
        padding: 16px 24px;
        border-top: 1px solid #334155;
        display: flex;
        gap: 12px;
        justify-content: center;
    }

    .cpm-graph-btn {
        padding: 12px 24px;
        border-radius: 8px;
        border: none;
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s;
        font-family: inherit;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .cpm-graph-btn:hover {
        background: rgba(71, 85, 105, 0.8);
        color: #e2e8f0;
    }

    .cpm-graph-btn-primary {
        background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
        color: white;
        box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
    }

    .cpm-graph-btn-primary:hover {
        background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
        transform: translateY(-2px);
        box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
    }

    .cpm-graph-btn-primary:active {
        transform: translateY(0);
        box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
    }

    .cpm-graph-btn-secondary {
        background: rgba(51, 65, 85, 0.6);
        color: #cbd5e1;
    }

    .cpm-graph-btn-secondary:hover {
        background: rgba(71, 85, 105, 0.8);
    }

    /* Settings Modal Styles */
    .cpm-settings-modal {
        min-width: 900px;
        max-width: 95vw;
    }

    .cpm-settings-modal .cpm-graph-modal-body {
        padding: 32px;
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 24px;
        max-height: 620px;
        overflow-y: auto;
    }

    .setting-block {
        background: linear-gradient(135deg, rgba(30, 41, 59, 0.6) 0%, rgba(15, 23, 42, 0.7) 100%);
        border: 1px solid rgba(71, 85, 105, 0.4);
        border-radius: 16px;
        padding: 20px;
        transition: all 0.3s;
    }

    .setting-block:hover {
        border-color: rgba(59, 130, 246, 0.4);
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
    }

    .setting-header {
        display: flex;
        align-items: flex-start;
        gap: 12px;
        margin-bottom: 16px;
    }

    .setting-icon {
        width: 40px;
        height: 40px;
        background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(37, 99, 235, 0.1) 100%);
        border-radius: 10px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 20px;
        flex-shrink: 0;
    }

    .setting-icon svg {
        width: 20px;
        height: 20px;
        color: #60a5fa;
    }

    .setting-info {
        flex: 1;
    }

    .setting-label {
        color: #e2e8f0;
        font-size: 15px;
        font-weight: 600;
        margin-bottom: 4px;
    }

    .setting-desc {
        color: #64748b;
        font-size: 12px;
        line-height: 1.4;
    }

    /* Direction Pills */
    .direction-pills {
        display: flex;
        gap: 8px;
    }

    .direction-pill {
        flex: 1;
        position: relative;
    }

    .direction-pill input {
        position: absolute;
        opacity: 0;
    }

    .direction-pill label {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
        padding: 14px 8px;
        background: rgba(15, 23, 42, 0.6);
        border: 2px solid rgba(71, 85, 105, 0.3);
        border-radius: 10px;
        cursor: pointer;
        transition: all 0.2s;
    }

    .direction-pill:hover label {
        border-color: rgba(59, 130, 246, 0.4);
        background: rgba(15, 23, 42, 0.8);
    }

    .direction-pill input:checked + label {
        border-color: #3b82f6;
        background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(37, 99, 235, 0.15) 100%);
        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
    }

    .direction-pill svg {
        width: 32px;
        height: 32px;
    }

    .direction-pill .node {
        transition: fill 0.2s;
    }

    .direction-pill .edge {
        transition: stroke 0.2s;
    }

    .direction-pill:hover .node,
    .direction-pill input:checked + label .node {
        fill: #60a5fa;
    }

    .direction-pill:hover .edge,
    .direction-pill input:checked + label .edge {
        stroke: #60a5fa;
    }

    .direction-text {
        color: #94a3b8;
        font-size: 11px;
        font-weight: 600;
        text-align: center;
        transition: color 0.2s;
    }

    .direction-pill input:checked + label .direction-text {
        color: #60a5fa;
    }

    /* View Toggle */
    .view-toggle {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        gap: 10px;
        background: rgba(15, 23, 42, 0.4);
        padding: 6px;
        border-radius: 12px;
    }

    .view-option {
        padding: 16px 12px;
        background: transparent;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        transition: all 0.2s;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 6px;
        font-family: inherit;
        min-height: 110px;
    }

    .view-option:hover {
        background: rgba(51, 65, 85, 0.4);
    }

    .view-option.active {
        background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
        box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
    }

    .view-option svg {
        width: 36px;
        height: 36px;
    }

    .view-label {
        color: #e2e8f0;
        font-size: 13px;
        font-weight: 600;
        transition: color 0.2s;
        margin-bottom: 2px;
    }

    .view-option.active .view-label {
        color: white;
    }

    .view-desc {
        color: #94a3b8;
        font-size: 10px;
        text-align: center;
        line-height: 1.3;
    }

    .view-option.active .view-desc {
        color: rgba(255, 255, 255, 0.8);
    }

    /* Grouping Cards */
    .grouping-options {
        display: flex;
        flex-direction: column;
        gap: 10px;
    }

    .grouping-card {
        position: relative;
    }

    .grouping-card input {
        position: absolute;
        opacity: 0;
    }

    .grouping-card label {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 12px 16px;
        background: rgba(15, 23, 42, 0.6);
        border: 2px solid rgba(71, 85, 105, 0.3);
        border-radius: 10px;
        cursor: pointer;
        transition: all 0.2s;
    }

    .grouping-card:hover label {
        border-color: rgba(59, 130, 246, 0.4);
        background: rgba(15, 23, 42, 0.8);
    }

    .grouping-card input:checked + label {
        border-color: #3b82f6;
        background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(37, 99, 235, 0.15) 100%);
        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
    }

    .grouping-icon {
        width: 36px;
        height: 36px;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .grouping-icon svg {
        width: 100%;
        height: 100%;
    }

    .grouping-icon .node {
        transition: fill 0.2s;
    }

    .grouping-card:hover .node,
    .grouping-card input:checked + label .node {
        fill: #60a5fa;
    }

    .grouping-content {
        flex: 1;
    }

    .grouping-title {
        color: #e2e8f0;
        font-size: 13px;
        font-weight: 600;
        margin-bottom: 2px;
        transition: color 0.2s;
    }

    .grouping-card input:checked + label .grouping-title {
        color: #60a5fa;
    }

    .grouping-desc {
        color: #64748b;
        font-size: 11px;
    }

    .grouping-check {
        width: 20px;
        height: 20px;
        border-radius: 50%;
        border: 2px solid #475569;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: all 0.2s;
    }

    .grouping-card input:checked + label .grouping-check {
        border-color: #3b82f6;
        background: #3b82f6;
    }

    .grouping-check svg {
        width: 12px;
        height: 12px;
        opacity: 0;
        transition: opacity 0.2s;
    }

    .grouping-card input:checked + label .grouping-check svg {
        opacity: 1;
    }

    /* Edge Buttons */
    .edge-buttons {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
    }

    .edge-btn {
        padding: 14px 12px;
        background: rgba(15, 23, 42, 0.6);
        border: 2px solid rgba(71, 85, 105, 0.3);
        border-radius: 10px;
        cursor: pointer;
        transition: all 0.2s;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 6px;
        font-family: inherit;
        min-height: 110px;
    }

    .edge-btn:hover {
        border-color: rgba(59, 130, 246, 0.4);
        background: rgba(15, 23, 42, 0.8);
    }

    .edge-btn.active {
        border-color: #3b82f6;
        background: linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(37, 99, 235, 0.15) 100%);
        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
    }

    .edge-btn svg {
        width: 28px;
        height: 28px;
    }

    .edge-btn .node,
    .edge-btn .edge {
        transition: all 0.2s;
    }

    .edge-btn:hover .node,
    .edge-btn.active .node {
        fill: #60a5fa;
    }

    .edge-btn:hover .edge,
    .edge-btn.active .edge {
        stroke: #60a5fa;
    }

    .edge-label {
        color: #e2e8f0;
        font-size: 11px;
        font-weight: 600;
        text-align: center;
        margin-bottom: 2px;
        transition: color 0.2s;
    }

    .edge-btn.active .edge-label {
        color: #60a5fa;
    }

    .edge-desc {
        color: #64748b;
        font-size: 10px;
        text-align: center;
        line-height: 1.3;
    }

    /* Panel Switch */
    .panel-switch {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 14px 16px;
        background: rgba(15, 23, 42, 0.6);
        border: 2px solid rgba(71, 85, 105, 0.3);
        border-radius: 12px;
        cursor: pointer;
        transition: all 0.2s;
    }

    .panel-switch:hover {
        border-color: rgba(59, 130, 246, 0.4);
    }

    .panel-info {
        display: flex;
        align-items: center;
        gap: 12px;
    }

    .panel-icon {
        width: 32px;
        height: 32px;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .panel-icon svg {
        width: 100%;
        height: 100%;
    }

    .panel-text {
        flex: 1;
    }

    .panel-title {
        color: #e2e8f0;
        font-size: 13px;
        font-weight: 600;
        margin-bottom: 2px;
    }

    .panel-desc {
        color: #64748b;
        font-size: 11px;
    }

    .toggle-switch {
        width: 48px;
        height: 26px;
        background: rgba(71, 85, 105, 0.6);
        border-radius: 13px;
        position: relative;
        transition: background 0.2s;
    }

    .panel-switch.active .toggle-switch {
        background: #3b82f6;
    }

    .toggle-slider {
        width: 20px;
        height: 20px;
        background: white;
        border-radius: 50%;
        position: absolute;
        top: 3px;
        left: 3px;
        transition: transform 0.2s;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    }

    .panel-switch.active .toggle-slider {
        transform: translateX(22px);
    }

    @media (max-width: 900px) {
        .cpm-settings-modal {
            min-width: 90vw;
        }

        .cpm-settings-modal .cpm-graph-modal-body {
            grid-template-columns: 1fr;
        }
    }
`);
})();