<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { page } from '$app/stores';
  import { goto } from '$app/navigation';
  import CodeEditor from '$lib/components/CodeEditor.svelte';
  import PRDetail from '$lib/components/PRDetail.svelte';
  import UserBadge from '$lib/components/UserBadge.svelte';
  import EventCopyButton from '$lib/components/EventCopyButton.svelte';
  import RepoHeaderEnhanced from '$lib/components/RepoHeaderEnhanced.svelte';
  import TabsMenu from '$lib/components/TabsMenu.svelte';
  import NostrLinkRenderer from '$lib/components/NostrLinkRenderer.svelte';
  import TagsTab from './components/TagsTab.svelte';
  import { downloadRepository as downloadRepoUtil } from './utils/download.js';
  import { buildApiHeaders } from './utils/api-client.js';
  import '$lib/styles/repo.css';
  import { getPublicKeyWithNIP07, isNIP07Available, signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js';
  import { NostrClient } from '$lib/services/nostr/nostr-client.js';
  import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '$lib/config.js';
  import { getUserRelays } from '$lib/services/nostr/user-relays.js';
  import { BookmarksService } from '$lib/services/nostr/bookmarks-service.js';
  import { KIND } from '$lib/types/nostr.js';
  import { nip19 } from 'nostr-tools';
  import { userStore } from '$lib/stores/user-store.js';
  import { settingsStore } from '$lib/services/settings-store.js';
  // Note: Announcements are now stored in nostr/repo-events.jsonl, not .nostr-announcement
  import type { NostrEvent } from '$lib/types/nostr.js';
  import { hasUnlimitedAccess } from '$lib/utils/user-access.js';
  import { fetchUserEmail, fetchUserName } from '$lib/utils/user-profile.js';

  // Get page data for OpenGraph metadata - use state to avoid SSR issues
  // Guard against SSR - $page store can only be accessed in component context
  let pageData = $state<{
    title?: string;
    description?: string;
    image?: string;
    banner?: string;
    repoUrl?: string;
    announcement?: NostrEvent;
    gitDomain?: string;
  }>({});
  
  // Update pageData from $page when available (client-side)
  $effect(() => {
    if (typeof window === 'undefined' || !isMounted) return;
    try {
      const data = $page.data as typeof pageData;
      if (data && isMounted) {
        pageData = data || {};
      }
    } catch (err) {
      // Ignore SSR errors and errors during destruction
      if (isMounted) {
        console.warn('Failed to update pageData:', err);
      }
    }
  });

  // Guard params access during SSR - use state that gets updated reactively
  // Params come from the route, so we can parse from URL or get from $page.params on client
  let npub = $state('');
  let repo = $state('');
  
  // Update params from $page when available (client-side)
  $effect(() => {
    if (typeof window === 'undefined' || !isMounted) return;
    try {
      const params = $page.params as { npub?: string; repo?: string };
      if (params && isMounted) {
        if (params.npub && params.npub !== npub) npub = params.npub;
        if (params.repo && params.repo !== repo) repo = params.repo;
      }
    } catch {
      // If $page.params fails, try to parse from URL path
      if (!isMounted) return;
      try {
        if (typeof window !== 'undefined') {
          const pathParts = window.location.pathname.split('/').filter(Boolean);
          if (pathParts[0] === 'repos' && pathParts[1] && pathParts[2] && isMounted) {
            npub = pathParts[1];
            repo = pathParts[2];
          }
        }
      } catch {
        // Ignore errors - params will be set eventually
      }
    }
  });

  // Extract fields from announcement for convenience
  const repoAnnouncement = $derived(pageData.announcement);
  const repoName = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'name')?.[1] || repo);
  const repoDescription = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'description')?.[1] || '');
  const repoCloneUrls = $derived(repoAnnouncement?.tags
    .filter((t: string[]) => t[0] === 'clone')
    .flatMap((t: string[]) => t.slice(1))
    .filter((url: string) => url && typeof url === 'string') as string[] || []);
  const repoMaintainers = $derived(repoAnnouncement?.tags
    .filter((t: string[]) => t[0] === 'maintainers')
    .flatMap((t: string[]) => t.slice(1))
    .filter((m: string) => m && typeof m === 'string') as string[] || []);
  const repoOwnerPubkeyDerived = $derived(repoAnnouncement?.pubkey || '');
  const repoLanguage = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'language')?.[1]);
  const repoTopics = $derived(repoAnnouncement?.tags
    .filter((t: string[]) => t[0] === 't' && t[1] !== 'private')
    .map((t: string[]) => t[1])
    .filter((t: string) => t && typeof t === 'string') as string[] || []);
  const repoWebsite = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'website')?.[1]);
  const repoIsPrivate = $derived(repoAnnouncement?.tags.some((t: string[]) =>
    (t[0] === 'private' && t[1] === 'true') || (t[0] === 't' && t[1] === 'private')
  ) || false);
  
  // Safe page URL for SSR - computed from pageData or current URL
  // Must be completely SSR-safe to prevent "Cannot read properties of null" errors
  const pageUrl = $derived.by(() => {
    try {
      // First try pageData (safest)
      if (pageData && typeof pageData === 'object' && pageData.repoUrl) {
        const url = pageData.repoUrl;
        if (typeof url === 'string' && url.trim()) {
          return url;
        }
      }
      
      // During SSR, return empty string immediately
      if (typeof window === 'undefined') {
        return '';
      }
      
      // On client, try to get from current location as fallback
      try {
        if (window && window.location && window.location.protocol && window.location.host && window.location.pathname) {
          return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
        }
      } catch (err) {
        // Silently ignore errors during SSR or if window.location is unavailable
        console.debug('Could not get page URL from window.location:', err);
      }
      
      return '';
    } catch (err) {
      // Catch any unexpected errors and return empty string
      console.debug('Error computing pageUrl:', err);
      return '';
    }
  });
  
  // Safe Twitter card type - avoid IIFE in head during SSR
  const twitterCardType = $derived.by(() => {
    try {
      const banner = (pageData?.banner || repoBanner) || (pageData?.image || repoImage);
      if (banner && typeof banner === 'string' && banner.trim()) {
        return "summary_large_image";
      }
      return "summary";
    } catch {
      return "summary";
    }
  });
  

  let loading = $state(true);
  let error = $state<string | null>(null);
  let repoNotFound = $state(false); // Track if repository doesn't exist
  let files = $state<Array<{ name: string; path: string; type: 'file' | 'directory'; size?: number }>>([]);
  let currentPath = $state('');
  let currentFile = $state<string | null>(null);
  let fileContent = $state('');
  let fileLanguage = $state<'markdown' | 'asciidoc' | 'text'>('text');
  let editedContent = $state('');
  let hasChanges = $state(false);
  let saving = $state(false);
  let branches = $state<Array<string | { name: string; commit?: any }>>([]);
  let currentBranch = $state<string | null>(null);
  let defaultBranch = $state<string | null>(null);
  let commitMessage = $state('');
  let userPubkey = $state<string | null>(null);
  let userPubkeyHex = $state<string | null>(null);
  let showCommitDialog = $state(false);
  let activeTab = $state<'files' | 'history' | 'tags' | 'issues' | 'prs' | 'docs' | 'discussions' | 'patches' | 'releases' | 'code-search'>('files');
  let showRepoMenu = $state(false);
  
  // Tabs will be defined as derived after issues and prs are declared
  
  // Component mount tracking to prevent state updates after destruction
  let isMounted = $state(true);
  
  // Helper function to safely update state only if component is still mounted
  function safeStateUpdate<T>(updateFn: () => T): T | null {
    if (!isMounted) return null;
    try {
      return updateFn();
    } catch (err) {
      // Silently ignore errors during destruction
      if (isMounted) {
        console.warn('State update error (component may be destroying):', err);
      }
      return null;
    }
  }
  
  // Store event listener handler for cleanup
  let clickOutsideHandler: ((event: MouseEvent) => void) | null = null;
  
  // Auto-save
  let autoSaveInterval: ReturnType<typeof setInterval> | null = null;

  // Load maintainers when page data changes (only once per repo, with guard)
  let lastRepoKey = $state<string | null>(null);
  let maintainersEffectRan = $state(false);
  
  $effect(() => {
    // Guard against SSR and component destruction
    if (typeof window === 'undefined' || !isMounted) return;
    try {
      const data = $page.data as typeof pageData;
      if (!data || !isMounted) return;
      
      const currentRepoKey = `${npub}/${repo}`;
      
      // Reset flags if repo changed
      if (currentRepoKey !== lastRepoKey && isMounted) {
        maintainersLoaded = false;
        maintainersEffectRan = false;
        lastRepoKey = currentRepoKey;
      }
      
      // Only load if:
      // 1. We have page data
      // 2. Effect hasn't run yet for this repo
      // 3. We're not currently loading
      // 4. Component is still mounted
      if (isMounted && 
          (repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) && 
          !maintainersEffectRan && 
          !loadingMaintainers) {
        maintainersEffectRan = true; // Mark as ran to prevent re-running
        maintainersLoaded = true; // Set flag before loading to prevent concurrent calls
        loadAllMaintainers().catch(err => {
          if (!isMounted) return;
          maintainersLoaded = false; // Reset on error so we can retry
          maintainersEffectRan = false; // Allow retry
          console.warn('Failed to load maintainers:', err);
        });
      }
    } catch (err) {
      // Ignore SSR errors and errors during destruction
      if (isMounted) {
        console.warn('Maintainers effect error:', err);
      }
    }
  });

  // Watch for auto-save setting changes
  $effect(() => {
    if (!isMounted) return;
    // Check auto-save setting and update interval (async, but don't await)
    settingsStore.getSettings().then(settings => {
      if (!isMounted) return;
      if (settings.autoSave && !autoSaveInterval) {
        // Auto-save was enabled, set it up
        setupAutoSave();
      } else if (!settings.autoSave && autoSaveInterval) {
        // Auto-save was disabled, clear interval
        if (autoSaveInterval) {
          clearInterval(autoSaveInterval);
          autoSaveInterval = null;
        }
      }
    }).catch(err => {
      if (isMounted) {
        console.warn('Failed to check auto-save setting:', err);
      }
    });
  });

  // Sync with userStore
  $effect(() => {
    if (!isMounted) return;
    try {
      const currentUser = $userStore;
      if (!currentUser || !isMounted) return;
      
      const wasLoggedIn = userPubkey !== null || userPubkeyHex !== null;
      
      if (currentUser.userPubkey && currentUser.userPubkeyHex && isMounted) {
        const wasDifferent = userPubkey !== currentUser.userPubkey || userPubkeyHex !== currentUser.userPubkeyHex;
        userPubkey = currentUser.userPubkey;
        userPubkeyHex = currentUser.userPubkeyHex;
        
        // Reload data when user logs in or pubkey changes
        if (wasDifferent && isMounted) {
          // Reset repoNotFound flag when user logs in, so we can retry loading
          repoNotFound = false;
          // Clear cached email and name when user changes
          cachedUserEmail = null;
          cachedUserName = null;
          
          if (!isMounted) return;
          checkMaintainerStatus().catch(err => {
            if (isMounted) console.warn('Failed to reload maintainer status after login:', err);
          });
          loadBookmarkStatus().catch(err => {
            if (isMounted) console.warn('Failed to reload bookmark status after login:', err);
          });
          // Reset flags to allow reload
          maintainersLoaded = false;
          maintainersEffectRan = false;
          lastRepoKey = null;
          loadAllMaintainers().catch(err => {
            if (isMounted) console.warn('Failed to reload maintainers after login:', err);
          });
          // Recheck clone status after login (force refresh) - delay slightly to ensure auth headers are ready
          setTimeout(() => {
            if (isMounted) {
              checkCloneStatus(true).catch(err => {
                if (isMounted) console.warn('Failed to recheck clone status after login:', err);
              });
            }
          }, 100);
          // Reload all repository data with the new user context
          if (!loading && isMounted) {
            loadBranches().catch(err => {
              if (isMounted) console.warn('Failed to reload branches after login:', err);
            });
            loadFiles().catch(err => {
              if (isMounted) console.warn('Failed to reload files after login:', err);
            });
            loadReadme().catch(err => {
              if (isMounted) console.warn('Failed to reload readme after login:', err);
            });
            loadTags().catch(err => {
              if (isMounted) console.warn('Failed to reload tags after login:', err);
            });
            // Reload discussions when user logs in (needs user context for relay selection)
            loadDiscussions().catch(err => {
              if (isMounted) console.warn('Failed to reload discussions after login:', err);
            });
          }
        }
      } else if (isMounted) {
        userPubkey = null;
        userPubkeyHex = null;
        // Clear cached email and name when user logs out
        cachedUserEmail = null;
        cachedUserName = null;
        
        // Reload data when user logs out to hide private content
        if (wasLoggedIn && isMounted) {
          checkMaintainerStatus().catch(err => {
            if (isMounted) console.warn('Failed to reload maintainer status after logout:', err);
          });
          loadBookmarkStatus().catch(err => {
            if (isMounted) console.warn('Failed to reload bookmark status after logout:', err);
          });
          // Reset flags to allow reload
          maintainersLoaded = false;
          maintainersEffectRan = false;
          lastRepoKey = null;
          loadAllMaintainers().catch(err => {
            if (isMounted) console.warn('Failed to reload maintainers after logout:', err);
          });
          // If repo is private and user logged out, reload to trigger access check
          if (!loading && activeTab === 'files' && isMounted) {
            loadFiles().catch(err => {
              if (isMounted) console.warn('Failed to reload files after logout:', err);
            });
          }
        }
      }
    } catch (err) {
      // Ignore errors during destruction
      if (isMounted) {
        console.warn('User store sync error:', err);
      }
    }
  });

  // Navigation stack for directories
  let pathStack = $state<string[]>([]);

  // New file creation
  let showCreateFileDialog = $state(false);
  let newFileName = $state('');
  let newFileContent = $state('');

  // Branch creation
  let showCreateBranchDialog = $state(false);
  let newBranchName = $state('');
  let newBranchFrom = $state<string | null>(null);
  let defaultBranchName = $state('master'); // Default branch from settings

  // Commit history
  let commits = $state<Array<{ hash: string; message: string; author: string; date: string; files: string[] }>>([]);
  let loadingCommits = $state(false);
  let selectedCommit = $state<string | null>(null);
  let showDiff = $state(false);
  let diffData = $state<Array<{ file: string; additions: number; deletions: number; diff: string }>>([]);

  // Tags
  let tags = $state<Array<{ name: string; hash: string; message?: string; date?: number }>>([]);
  let selectedTag = $state<string | null>(null);
  let showCreateTagDialog = $state(false);
  let newTagName = $state('');
  let newTagMessage = $state('');
  let newTagRef = $state('HEAD');

  // Maintainer status
  let isMaintainer = $state(false);
  let loadingMaintainerStatus = $state(false);
  
  // All maintainers (including owner) for display
  let allMaintainers = $state<Array<{ pubkey: string; isOwner: boolean }>>([]);
  let loadingMaintainers = $state(false);
  let maintainersLoaded = $state(false); // Guard to prevent repeated loads
  
  // Clone status
  let isRepoCloned = $state<boolean | null>(null); // null = unknown, true = cloned, false = not cloned
  let checkingCloneStatus = $state(false);
  let cloning = $state(false);
  
  // Word wrap toggle
  let wordWrap = $state(false);
  
  // Function to toggle word wrap and refresh highlighting
  async function toggleWordWrap() {
    wordWrap = !wordWrap;
    console.log('Word wrap toggled:', wordWrap);
    // Force DOM update by accessing the element
    await new Promise(resolve => {
      requestAnimationFrame(() => {
        requestAnimationFrame(resolve);
      });
    });
    // Re-apply syntax highlighting to refresh the display
    if (currentFile && fileContent) {
      const ext = currentFile.split('.').pop() || '';
      await applySyntaxHighlighting(fileContent, ext);
    }
  }
  let copyingCloneUrl = $state(false);
  let apiFallbackAvailable = $state<boolean | null>(null); // null = unknown, true = API fallback works, false = doesn't work
  
  // Helper: Check if repo needs to be cloned for write operations
  const needsClone = $derived(isRepoCloned === false);
  // Helper: Check if we can use API fallback for read-only operations
  const canUseApiFallback = $derived(apiFallbackAvailable === true);
  // Helper: Check if we have any way to view the repo (cloned or API fallback)
  const canViewRepo = $derived(isRepoCloned === true || canUseApiFallback);
  const cloneTooltip = 'Please clone this repo to use this feature.';
  
  // Copy clone URL to clipboard
  async function copyCloneUrl() {
    if (copyingCloneUrl) return;
    
    copyingCloneUrl = true;
    try {
      // Use the current page URL to get the correct host and port
      // This ensures we use the same domain/port the user is currently viewing
      // Guard against SSR - $page store can only be accessed in component context
      if (typeof window === 'undefined') return;
      // Guard against SSR - $page.url might not be available
      if (typeof window === 'undefined' || !$page?.url) {
        return '';
      }
      const currentUrl = $page.url;
      const host = currentUrl.host; // Includes port if present (e.g., "localhost:5173")
      const protocol = currentUrl.protocol.slice(0, -1); // Remove trailing ":"
      
      // Use /api/git/ format for better compatibility with commit signing hook
      const cloneUrl = `${protocol}://${host}/api/git/${npub}/${repo}.git`;
      const cloneCommand = `git clone ${cloneUrl}`;
      
      // Try to use the Clipboard API
      if (navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(cloneCommand);
        alert(`Clone command copied to clipboard!\n\n${cloneCommand}`);
      } else {
        // Fallback: create a temporary textarea
        const textarea = document.createElement('textarea');
        textarea.value = cloneCommand;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
        alert(`Clone command copied to clipboard!\n\n${cloneCommand}`);
      }
    } catch (err) {
      console.error('Failed to copy clone command:', err);
      alert('Failed to copy clone command to clipboard');
    } finally {
      copyingCloneUrl = false;
    }
  }
  
  // Verification status
  let verificationStatus = $state<{ 
    verified: boolean; 
    error?: string; 
    message?: string;
    cloneVerifications?: Array<{ url: string; verified: boolean; ownerPubkey: string | null; error?: string }>;
  } | null>(null);
  let showVerificationDialog = $state(false);
  let verificationFileContent = $state<string | null>(null);
  let loadingVerification = $state(false);

  // Deletion request
  let deletingAnnouncement = $state(false);
  let announcementEventId = $state<string | null>(null);

  // Issues
  let issues = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; kind: number; tags?: string[][] }>>([]);
  let loadingIssues = $state(false);
  let showCreateIssueDialog = $state(false);
  let newIssueSubject = $state('');
  let newIssueContent = $state('');
  let newIssueLabels = $state<string[]>(['']);
  let updatingIssueStatus = $state<Record<string, boolean>>({});
  let selectedIssue = $state<string | null>(null);
  let issueReplies = $state<Array<{ id: string; content: string; author: string; created_at: number; tags: string[][] }>>([]);
  let loadingIssueReplies = $state(false);

  // Pull Requests
  let prs = $state<Array<{ id: string; subject: string; content: string; status: string; author: string; created_at: number; commitId?: string; kind: number }>>([]);
  let loadingPRs = $state(false);
  let showCreatePRDialog = $state(false);
  let newPRSubject = $state('');
  let newPRContent = $state('');
  let newPRCommitId = $state('');
  let newPRBranchName = $state('');
  let newPRLabels = $state<string[]>(['']);
  let selectedPR = $state<string | null>(null);
  
  // Tabs menu - defined after issues and prs
  // Order: Files, Issues, PRs, Patches, Discussion, History, Tags, Code Search, Docs
  // Show tabs that require cloned repo when repo is cloned OR API fallback is available
  const tabs = $derived.by(() => {
    const allTabs = [
      { id: 'files', label: 'Files', icon: '/icons/file-text.svg', requiresClone: true },
      { id: 'issues', label: 'Issues', icon: '/icons/alert-circle.svg', requiresClone: false },
      { id: 'prs', label: 'Pull Requests', icon: '/icons/git-pull-request.svg', requiresClone: false },
      { id: 'patches', label: 'Patches', icon: '/icons/clipboard-list.svg', requiresClone: false },
      { id: 'discussions', label: 'Discussions', icon: '/icons/message-circle.svg', requiresClone: false },
      { id: 'history', label: 'Commit History', icon: '/icons/git-commit.svg', requiresClone: true },
      { id: 'tags', label: 'Tags', icon: '/icons/tag.svg', requiresClone: true },
      { id: 'code-search', label: 'Code Search', icon: '/icons/search.svg', requiresClone: true },
      { id: 'docs', label: 'Docs', icon: '/icons/book.svg', requiresClone: false }
    ];
    
    // Show all tabs if repo is cloned OR API fallback is available
    // Otherwise, only show tabs that don't require cloning
    if (isRepoCloned === false && !canUseApiFallback) {
      return allTabs.filter(tab => !tab.requiresClone).map(({ requiresClone, ...tab }) => tab);
    }
    
    // Return all tabs when repo is cloned, API fallback is available, or status is unknown (remove requiresClone property)
    return allTabs.map(({ requiresClone, ...tab }) => tab);
  });
  
  // Redirect to a valid tab if current tab requires cloning but repo isn't cloned and API fallback isn't available
  $effect(() => {
    if (!isMounted) return;
    if (isRepoCloned === false && !canUseApiFallback && tabs.length > 0) {
      const currentTab = tabs.find(t => t.id === activeTab);
      if (!currentTab && isMounted) {
        // Current tab requires cloning, switch to first available tab
        activeTab = tabs[0].id as typeof activeTab;
      }
    }
  });

  // Patches
  let patches = $state<Array<{ id: string; subject: string; content: string; author: string; created_at: number; kind: number; description?: string; tags?: string[][] }>>([]);
  let loadingPatches = $state(false);
  let selectedPatch = $state<string | null>(null);
  let showCreatePatchDialog = $state(false);
  let newPatchContent = $state('');
  let newPatchSubject = $state('');
  let creatingPatch = $state(false);

  // Documentation
  let documentationContent = $state<string | null>(null);
  let documentationHtml = $state<string | null>(null);
  let documentationKind = $state<number | null>(null);
  let loadingDocs = $state(false);

  // Discussion threads
  let showCreateThreadDialog = $state(false);
  let newThreadTitle = $state('');
  let newThreadContent = $state('');
  let creatingThread = $state(false);
  
  // Thread replies
  let expandedThreads = $state<Set<string>>(new Set());
  let showReplyDialog = $state(false);
  let replyingToThread = $state<{ id: string; kind?: number; pubkey?: string; author: string } | null>(null);
  let replyingToComment = $state<{ id: string; kind?: number; pubkey?: string; author: string } | null>(null);
  let replyContent = $state('');
  let creatingReply = $state(false);

  // Releases
  let releases = $state<Array<{
    id: string;
    tagName: string;
    tagHash?: string;
    releaseNotes?: string;
    isDraft?: boolean;
    isPrerelease?: boolean;
    created_at: number;
    pubkey: string;
  }>>([]);
  let loadingReleases = $state(false);
  let showCreateReleaseDialog = $state(false);
  let newReleaseTagName = $state('');
  let newReleaseTagHash = $state('');
  let newReleaseNotes = $state('');
  let newReleaseIsDraft = $state(false);
  let newReleaseIsPrerelease = $state(false);
  let creatingRelease = $state(false);

  // Code Search
  let codeSearchQuery = $state('');
  let codeSearchResults = $state<Array<{
    file: string;
    line: number;
    content: string;
    branch: string;
  }>>([]);
  let loadingCodeSearch = $state(false);
  let codeSearchScope = $state<'repo' | 'all'>('repo');

  // Discussions
  let selectedDiscussion = $state<string | null>(null);
  let discussions = $state<Array<{ 
    type: 'thread' | 'comments'; 
    id: string; 
    title: string; 
    content: string; 
    author: string; 
    createdAt: number;
    kind?: number;
    pubkey?: string;
    comments?: Array<{ 
      id: string; 
      content: string; 
      author: string; 
      createdAt: number;
      kind?: number;
      pubkey?: string;
      replies?: Array<{
        id: string;
        content: string;
        author: string;
        createdAt: number;
        kind?: number;
        pubkey?: string;
        replies?: Array<{
          id: string;
          content: string;
          author: string;
          createdAt: number;
          kind?: number;
          pubkey?: string;
        }>;
      }>;
    }> 
  }>>([]);
  let loadingDiscussions = $state(false);

  // Discussion events cache for reply/quote blurbs
  let discussionEvents = $state<Map<string, NostrEvent>>(new Map());
  
  // Nostr link cache for embedded events and profiles
  let nostrLinkEvents = $state<Map<string, NostrEvent>>(new Map());
  let nostrLinkProfiles = $state<Map<string, string>>(new Map()); // npub -> pubkey hex

  // Parse nostr: links from content and extract IDs/pubkeys
  function parseNostrLinks(content: string): Array<{ type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile'; value: string; start: number; end: number }> {
    const links: Array<{ type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile'; value: string; start: number; end: number }> = [];
    const nostrLinkRegex = /nostr:(nevent1|naddr1|note1|npub1|profile1)[a-zA-Z0-9]+/g;
    let match;
    
    while ((match = nostrLinkRegex.exec(content)) !== null) {
      const fullMatch = match[0];
      const prefix = match[1];
      let type: 'nevent' | 'naddr' | 'note1' | 'npub' | 'profile';
      
      if (prefix === 'nevent1') type = 'nevent';
      else if (prefix === 'naddr1') type = 'naddr';
      else if (prefix === 'note1') type = 'note1';
      else if (prefix === 'npub1') type = 'npub';
      else if (prefix === 'profile1') type = 'profile';
      else continue;
      
      links.push({
        type,
        value: fullMatch,
        start: match.index,
        end: match.index + fullMatch.length
      });
    }
    
    return links;
  }

  // Load events/profiles from nostr: links
  async function loadNostrLinks(content: string) {
    const links = parseNostrLinks(content);
    if (links.length === 0) return;

    const eventIds: string[] = [];
    const aTags: string[] = [];
    const npubs: string[] = [];

    for (const link of links) {
      try {
        if (link.type === 'nevent' || link.type === 'note1') {
          const decoded = nip19.decode(link.value.replace('nostr:', ''));
          if (decoded.type === 'nevent') {
            eventIds.push(decoded.data.id);
          } else if (decoded.type === 'note') {
            eventIds.push(decoded.data as string);
          }
        } else if (link.type === 'naddr') {
          const decoded = nip19.decode(link.value.replace('nostr:', ''));
          if (decoded.type === 'naddr') {
            const aTag = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`;
            aTags.push(aTag);
          }
        } else if (link.type === 'npub' || link.type === 'profile') {
          const decoded = nip19.decode(link.value.replace('nostr:', ''));
          if (decoded.type === 'npub') {
            npubs.push(link.value);
            nostrLinkProfiles.set(link.value, decoded.data as string);
          }
        }
      } catch {
        // Invalid nostr link, skip
      }
    }

    // Fetch events
    if (eventIds.length > 0) {
      try {
        const events = await Promise.race([
          nostrClient.fetchEvents([{ ids: eventIds, limit: eventIds.length }]),
          new Promise<NostrEvent[]>((resolve) => setTimeout(() => resolve([]), 10000))
        ]);
        
        for (const event of events) {
          nostrLinkEvents.set(event.id, event);
        }
      } catch {
        // Ignore fetch errors
      }
    }

    // Fetch a-tag events
    if (aTags.length > 0) {
      for (const aTag of aTags) {
        const parts = aTag.split(':');
        if (parts.length === 3) {
          try {
            const kind = parseInt(parts[0]);
            const pubkey = parts[1];
            const dTag = parts[2];
            const events = await Promise.race([
              nostrClient.fetchEvents([{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }]),
              new Promise<NostrEvent[]>((resolve) => setTimeout(() => resolve([]), 10000))
            ]);
            
            if (events.length > 0) {
              nostrLinkEvents.set(events[0].id, events[0]);
            }
          } catch {
            // Ignore fetch errors
          }
        }
      }
    }
  }

  // Get event from nostr: link
  function getEventFromNostrLink(link: string): NostrEvent | undefined {
    try {
      if (link.startsWith('nostr:nevent1') || link.startsWith('nostr:note1')) {
        const decoded = nip19.decode(link.replace('nostr:', ''));
        if (decoded.type === 'nevent') {
          return nostrLinkEvents.get(decoded.data.id);
        } else if (decoded.type === 'note') {
          return nostrLinkEvents.get(decoded.data as string);
        }
      } else if (link.startsWith('nostr:naddr1')) {
        const decoded = nip19.decode(link.replace('nostr:', ''));
        if (decoded.type === 'naddr') {
          const eventId = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`;
          return Array.from(nostrLinkEvents.values()).find(e => {
            const dTag = e.tags.find(t => t[0] === 'd')?.[1];
            return e.kind === decoded.data.kind && 
                   e.pubkey === decoded.data.pubkey && 
                   dTag === decoded.data.identifier;
          });
        }
      }
    } catch {
      // Invalid link
    }
    return undefined;
  }

  // Get pubkey from nostr: npub/profile link
  function getPubkeyFromNostrLink(link: string): string | undefined {
    return nostrLinkProfiles.get(link);
  }

  // Process content with nostr links into parts for rendering
  function processContentWithNostrLinks(content: string): Array<{ type: 'text' | 'event' | 'profile' | 'placeholder'; value: string; event?: NostrEvent; pubkey?: string }> {
    const links = parseNostrLinks(content);
    if (links.length === 0) {
      return [{ type: 'text', value: content }];
    }

    const parts: Array<{ type: 'text' | 'event' | 'profile' | 'placeholder'; value: string; event?: NostrEvent; pubkey?: string }> = [];
    let lastIndex = 0;

    for (const link of links) {
      // Add text before link
      if (link.start > lastIndex) {
        const textPart = content.slice(lastIndex, link.start);
        if (textPart) {
          parts.push({ type: 'text', value: textPart });
        }
      }

      // Add link
      const event = getEventFromNostrLink(link.value);
      const pubkey = getPubkeyFromNostrLink(link.value);
      if (event) {
        parts.push({ type: 'event', value: link.value, event });
      } else if (pubkey) {
        parts.push({ type: 'profile', value: link.value, pubkey });
      } else {
        parts.push({ type: 'placeholder', value: link.value });
      }

      lastIndex = link.end;
    }

    // Add remaining text
    if (lastIndex < content.length) {
      const textPart = content.slice(lastIndex);
      if (textPart) {
        parts.push({ type: 'text', value: textPart });
      }
    }

    return parts;
  }

  // Load full events for discussions and comments to get tags for blurbs
  async function loadDiscussionEvents(discussionsList: typeof discussions) {
    const eventIds = new Set<string>();
    
    // Collect all event IDs
    for (const discussion of discussionsList) {
      if (discussion.id) {
        eventIds.add(discussion.id);
      }
      if (discussion.comments) {
        for (const comment of discussion.comments) {
          if (comment.id) {
            eventIds.add(comment.id);
          }
          if (comment.replies) {
            for (const reply of comment.replies) {
              if (reply.id) {
                eventIds.add(reply.id);
              }
              if (reply.replies) {
                for (const nestedReply of reply.replies) {
                  if (nestedReply.id) {
                    eventIds.add(nestedReply.id);
                  }
                }
              }
            }
          }
        }
      }
    }

    if (eventIds.size === 0) return;

    try {
      const events = await Promise.race([
        nostrClient.fetchEvents([{ ids: Array.from(eventIds), limit: eventIds.size }]),
        new Promise<NostrEvent[]>((resolve) => setTimeout(() => resolve([]), 10000))
      ]);
      
      for (const event of events) {
        discussionEvents.set(event.id, event);
      }
    } catch {
      // Ignore fetch errors
    }
  }

  // Get discussion event by ID
  function getDiscussionEvent(eventId: string): NostrEvent | undefined {
    return discussionEvents.get(eventId);
  }

  // Get referenced event from discussion event (e-tag, a-tag, q-tag)
  function getReferencedEventFromDiscussion(event: NostrEvent): NostrEvent | undefined {
    // Check e-tag
    const eTag = event.tags.find(t => t[0] === 'e' && t[1])?.[1];
    if (eTag) {
      const referenced = discussionEvents.get(eTag);
      if (referenced) return referenced;
    }
    
    // Check a-tag
    const aTag = event.tags.find(t => t[0] === 'a' && t[1])?.[1];
    if (aTag) {
      const parts = aTag.split(':');
      if (parts.length === 3) {
        const kind = parseInt(parts[0]);
        const pubkey = parts[1];
        const dTag = parts[2];
        return Array.from(discussionEvents.values()).find(e => 
          e.kind === kind && 
          e.pubkey === pubkey && 
          e.tags.find(t => t[0] === 'd' && t[1] === dTag)
        );
      }
    }
    
    // Check q-tag
    const qTag = event.tags.find(t => t[0] === 'q' && t[1])?.[1];
    if (qTag) {
      return discussionEvents.get(qTag);
    }
    
    return undefined;
  }

  // Format time for discussions
  function formatDiscussionTime(timestamp: number): string {
    const date = new Date(timestamp * 1000);
    const now = new Date();
    const diffMs = now.getTime() - date.getTime();
    const diffMins = Math.floor(diffMs / 60000);
    const diffHours = Math.floor(diffMs / 3600000);
    const diffDays = Math.floor(diffMs / 86400000);

    if (diffMins < 1) return 'just now';
    if (diffMins < 60) return `${diffMins}m ago`;
    if (diffHours < 24) return `${diffHours}h ago`;
    if (diffDays < 7) return `${diffDays}d ago`;
    return date.toLocaleDateString();
  }

  // Create a nostrClient instance for fetching events
  let nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);

  // README
  let readmeContent = $state<string | null>(null);
  let readmePath = $state<string | null>(null);
  let readmeIsMarkdown = $state(false);
  let loadingReadme = $state(false);
  let readmeHtml = $state<string>('');
  let highlightedFileContent = $state<string>('');
  let fileHtml = $state<string>(''); // Rendered HTML for markdown/asciidoc/HTML files
  let showFilePreview = $state(true); // Toggle between preview and raw view (default: preview)
  let copyingFile = $state(false); // Track copy operation
  let isImageFile = $state(false); // Track if current file is an image
  let imageUrl = $state<string | null>(null); // URL for image files

  // Rewrite image paths in HTML to point to repository file API
  function rewriteImagePaths(html: string, filePath: string | null): string {
    if (!html || !filePath) return html;
    
    // Get the directory of the current file
    const fileDir = filePath.includes('/') 
      ? filePath.substring(0, filePath.lastIndexOf('/'))
      : '';
    
    // Get current branch for the API URL
    const branch = currentBranch || defaultBranch || 'main';
    
    // Rewrite relative image paths
    return html.replace(/<img([^>]*)\ssrc=["']([^"']+)["']([^>]*)>/gi, (match, before, src, after) => {
      // Skip if it's already an absolute URL (http/https/data)
      if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('data:') || src.startsWith('/api/')) {
        return match;
      }
      
      // Resolve relative path
      let imagePath: string;
      if (src.startsWith('/')) {
        // Absolute path from repo root
        imagePath = src.substring(1);
      } else if (src.startsWith('./')) {
        // Relative to current file directory
        imagePath = fileDir ? `${fileDir}/${src.substring(2)}` : src.substring(2);
      } else {
        // Relative to current file directory
        imagePath = fileDir ? `${fileDir}/${src}` : src;
      }
      
      // Normalize path (remove .. and .)
      const pathParts = imagePath.split('/').filter(p => p !== '.' && p !== '');
      const normalizedPath: string[] = [];
      for (const part of pathParts) {
        if (part === '..') {
          normalizedPath.pop();
        } else {
          normalizedPath.push(part);
        }
      }
      imagePath = normalizedPath.join('/');
      
      // Build API URL
      const apiUrl = `/api/repos/${npub}/${repo}/raw?path=${encodeURIComponent(imagePath)}&ref=${encodeURIComponent(branch)}`;
      
      return `<img${before} src="${apiUrl}"${after}>`;
    });
  }

  // Fork
  let forkInfo = $state<{ isFork: boolean; originalRepo: { npub: string; repo: string } | null } | null>(null);
  let forking = $state(false);

  // Bookmarks
  let isBookmarked = $state(false);
  let loadingBookmark = $state(false);
  let bookmarksService: BookmarksService | null = null;
  let repoAddress = $state<string | null>(null);

  // Repository images
  let repoImage = $state<string | null>(null);
  let repoBanner = $state<string | null>(null);
  
  // Safe values for head section to prevent SSR errors (must be after repoImage/repoBanner declaration)
  const safeRepo = $derived(repo || 'Repository');
  const safeRepoName = $derived.by(() => {
    try {
      return repoName || repo || 'Repository';
    } catch {
      return repo || 'Repository';
    }
  });
  const safeRepoDescription = $derived.by(() => {
    try {
      return repoDescription || '';
    } catch {
      return '';
    }
  });
  const safeTitle = $derived.by(() => {
    try {
      return pageData?.title || `${safeRepo} - Repository`;
    } catch {
      return `${safeRepo} - Repository`;
    }
  });
  const safeDescription = $derived.by(() => {
    try {
      return pageData?.description || `Repository: ${safeRepo}`;
    } catch {
      return `Repository: ${safeRepo}`;
    }
  });
  const safeImage = $derived.by(() => {
    try {
      return pageData?.image || repoImage || null;
    } catch {
      return null;
    }
  });
  const safeBanner = $derived.by(() => {
    try {
      return pageData?.banner || repoBanner || null;
    } catch {
      return null;
    }
  });
  const hasImage = $derived.by(() => {
    try {
      return safeImage && typeof safeImage === 'string' && safeImage.trim() !== '';
    } catch {
      return false;
    }
  });
  const hasBanner = $derived.by(() => {
    try {
      return safeBanner && typeof safeBanner === 'string' && safeBanner.trim() !== '';
    } catch {
      return false;
    }
  });
  
  // Additional safe values for head section to avoid IIFEs
  const safeOgDescription = $derived.by(() => {
    try {
      return pageData?.description || safeRepoDescription || `Repository: ${safeRepoName || safeRepo || 'Repository'}`;
    } catch {
      return 'Repository';
    }
  });
  const safeTwitterDescription = $derived.by(() => {
    try {
      return pageData?.description || safeRepoDescription || `Repository: ${safeRepoName || safeRepo || 'Repository'}`;
    } catch {
      return 'Repository';
    }
  });
  const safeTwitterCard = $derived.by(() => {
    try {
      return twitterCardType || 'summary';
    } catch {
      return 'summary';
    }
  });
  const safePageUrl = $derived.by(() => {
    try {
      return pageUrl || '';
    } catch {
      return '';
    }
  });

  // Repository owner pubkey (decoded from npub) - kept for backward compatibility with some functions
  let repoOwnerPubkeyState = $state<string | null>(null);

  // Mobile view toggle for file list/file viewer
  let showFileListOnMobile = $state(true);
  // Mobile view toggle for all left/right panels (issues, PRs, patches, discussions, docs, history, tags)
  let showLeftPanelOnMobile = $state(true);
  // Mobile collapse for clone URLs
  let cloneUrlsExpanded = $state(false);
  // Show all clone URLs (beyond the first 3)
  let showAllCloneUrls = $state(false);
  
  // Clone URL reachability
  let cloneUrlReachability = $state<Map<string, { reachable: boolean; error?: string; checkedAt: number; serverType: 'git' | 'grasp' | 'unknown' }>>(new Map());
  let loadingReachability = $state(false);
  let checkingReachability = $state<Set<string>>(new Set());
  
  // Guard to prevent README auto-load loop
  let readmeAutoLoadAttempted = $state(false);
  let readmeAutoLoadTimeout: ReturnType<typeof setTimeout> | null = null;

  // Load clone URL reachability status
  async function loadCloneUrlReachability(forceRefresh: boolean = false) {
    if (!repoCloneUrls || repoCloneUrls.length === 0) {
      return;
    }
    
    if (loadingReachability) return;
    
    loadingReachability = true;
    try {
      const response = await fetch(
        `/api/repos/${npub}/${repo}/clone-urls/reachability${forceRefresh ? '?forceRefresh=true' : ''}`,
        {
          headers: buildApiHeaders()
        }
      );
      
      if (response.ok) {
        const data = await response.json();
        const newMap = new Map<string, { reachable: boolean; error?: string; checkedAt: number; serverType: 'git' | 'grasp' | 'unknown' }>();
        
        if (data.results && Array.isArray(data.results)) {
          for (const result of data.results) {
            newMap.set(result.url, {
              reachable: result.reachable,
              error: result.error,
              checkedAt: result.checkedAt,
              serverType: result.serverType || 'unknown'
            });
          }
        }
        
        cloneUrlReachability = newMap;
      }
    } catch (err) {
      console.warn('Failed to load clone URL reachability:', err);
    } finally {
      loadingReachability = false;
      checkingReachability.clear();
    }
  }

  async function loadReadme() {
    if (repoNotFound) return;
    loadingReadme = true;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/readme?ref=${currentBranch}`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        const data = await response.json();
        if (data.found) {
          readmeContent = data.content;
          readmePath = data.path;
          readmeIsMarkdown = data.isMarkdown;
          
          // Reset preview mode for README
          showFilePreview = true;
          readmeHtml = '';
          
          // Render markdown or asciidoc if needed
          if (readmeContent) {
            const ext = readmePath?.split('.').pop()?.toLowerCase() || '';
            if (readmeIsMarkdown || ext === 'md' || ext === 'markdown') {
              try {
                const MarkdownIt = (await import('markdown-it')).default;
                const hljsModule = await import('highlight.js');
                const hljs = hljsModule.default || hljsModule;
                
                const md = new MarkdownIt({
                  html: true, // Enable HTML tags in source
                  linkify: true, // Autoconvert URL-like text to links
                  typographer: true, // Enable some language-neutral replacement + quotes beautification
                  breaks: true, // Convert '\n' in paragraphs into <br>
                  highlight: function (str: string, lang: string): string {
                    if (lang && hljs.getLanguage(lang)) {
                      try {
                        return '<pre class="hljs"><code>' +
                               hljs.highlight(str, { language: lang }).value +
                               '</code></pre>';
                      } catch (err) {
                        // Fallback to escaped HTML if highlighting fails
                        // This is expected for unsupported languages
                      }
                    }
                    return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
                  }
                });
                
                let rendered = md.render(readmeContent);
                // Rewrite image paths to point to repository API
                rendered = rewriteImagePaths(rendered, readmePath);
                readmeHtml = rendered;
                console.log('[README] Markdown rendered successfully, HTML length:', readmeHtml.length);
              } catch (err) {
                console.error('[README] Error rendering markdown:', err);
                readmeHtml = '';
              }
            } else if (ext === 'adoc' || ext === 'asciidoc') {
              try {
                const Asciidoctor = (await import('@asciidoctor/core')).default;
                const asciidoctor = Asciidoctor();
                const converted = asciidoctor.convert(readmeContent, {
                  safe: 'safe',
                  attributes: {
                    'source-highlighter': 'highlight.js'
                  }
                });
                let rendered = typeof converted === 'string' ? converted : String(converted);
                // Rewrite image paths to point to repository API
                rendered = rewriteImagePaths(rendered, readmePath);
                readmeHtml = rendered;
                readmeIsMarkdown = true; // Treat as markdown for display purposes
              } catch (err) {
                console.error('[README] Error rendering asciidoc:', err);
                readmeHtml = '';
              }
            } else if (ext === 'html' || ext === 'htm') {
              // Rewrite image paths to point to repository API
              readmeHtml = rewriteImagePaths(readmeContent, readmePath);
              readmeIsMarkdown = true; // Treat as markdown for display purposes
            } else {
              readmeHtml = '';
            }
          }
        }
      }
    } catch (err) {
      console.error('Error loading README:', err);
    } finally {
      loadingReadme = false;
    }
  }

  // Map file extensions to highlight.js language names
  function getHighlightLanguage(ext: string): string {
    const langMap: Record<string, string> = {
      'js': 'javascript',
      'ts': 'typescript',
      'jsx': 'javascript',
      'tsx': 'typescript',
      'json': 'json',
      'css': 'css',
      'html': 'xml',
      'xml': 'xml',
      'yaml': 'yaml',
      'yml': 'yaml',
      'py': 'python',
      'rb': 'ruby',
      'go': 'go',
      'rs': 'rust',
      'java': 'java',
      'c': 'c',
      'cpp': 'cpp',
      'h': 'c',
      'hpp': 'cpp',
      'sh': 'bash',
      'bash': 'bash',
      'zsh': 'bash',
      'sql': 'sql',
      'php': 'php',
      'swift': 'swift',
      'kt': 'kotlin',
      'scala': 'scala',
      'r': 'r',
      'm': 'objectivec',
      'mm': 'objectivec',
      'vue': 'xml',
      'svelte': 'xml',
      'dockerfile': 'dockerfile',
      'toml': 'toml',
      'ini': 'ini',
      'conf': 'ini',
      'log': 'plaintext',
      'txt': 'plaintext',
      'md': 'markdown',
      'markdown': 'markdown',
      'mdown': 'markdown',
      'mkdn': 'markdown',
      'mkd': 'markdown',
      'mdwn': 'markdown',
      'adoc': 'asciidoc',
      'asciidoc': 'asciidoc',
      'ad': 'asciidoc',
    };
    return langMap[ext.toLowerCase()] || 'plaintext';
  }

  // Check if file type supports preview mode
  function supportsPreview(ext: string): boolean {
    const previewExtensions = ['md', 'markdown', 'adoc', 'asciidoc', 'html', 'htm', 'csv'];
    return previewExtensions.includes(ext.toLowerCase());
  }

  // Check if a file is an image based on extension
  function isImageFileType(ext: string): boolean {
    const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'apng', 'avif'];
    return imageExtensions.includes(ext.toLowerCase());
  }

  // Render markdown, asciidoc, or HTML files as HTML
  async function renderFileAsHtml(content: string, ext: string) {
    try {
      const lowerExt = ext.toLowerCase();
      
      if (lowerExt === 'md' || lowerExt === 'markdown') {
        // Render markdown
        const MarkdownIt = (await import('markdown-it')).default;
        const hljsModule = await import('highlight.js');
        const hljs = hljsModule.default || hljsModule;
        
        const md = new MarkdownIt({
          html: true,
          linkify: true,
          typographer: true,
          breaks: true,
          highlight: function (str: string, lang: string): string {
            if (lang && hljs.getLanguage(lang)) {
              try {
                return hljs.highlight(str, { language: lang }).value;
              } catch (__) {}
            }
            try {
              return hljs.highlightAuto(str).value;
            } catch (__) {}
            return '';
          }
        });
        
        let rendered = md.render(content);
        // Rewrite image paths to point to repository API
        rendered = rewriteImagePaths(rendered, currentFile);
        fileHtml = rendered;
      } else if (lowerExt === 'adoc' || lowerExt === 'asciidoc') {
        // Render asciidoc
        const Asciidoctor = (await import('@asciidoctor/core')).default;
        const asciidoctor = Asciidoctor();
        const converted = asciidoctor.convert(content, {
          safe: 'safe',
          attributes: {
            'source-highlighter': 'highlight.js'
          }
        });
        let rendered = typeof converted === 'string' ? converted : String(converted);
        // Rewrite image paths to point to repository API
        rendered = rewriteImagePaths(rendered, currentFile);
        fileHtml = rendered;
      } else if (lowerExt === 'html' || lowerExt === 'htm') {
        // HTML files - rewrite image paths
        let rendered = content;
        rendered = rewriteImagePaths(rendered, currentFile);
        fileHtml = rendered;
      } else if (lowerExt === 'csv') {
        // Parse CSV and render as HTML table
        fileHtml = renderCsvAsTable(content);
      }
    } catch (err) {
      console.error('Error rendering file as HTML:', err);
      fileHtml = '';
    }
  }

  // Parse CSV content and render as HTML table
  function renderCsvAsTable(csvContent: string): string {
    try {
      // Parse CSV - handle quoted fields and escaped quotes
      const lines = csvContent.split(/\r?\n/).filter(line => line.trim() !== '');
      if (lines.length === 0) {
        return '<div class="csv-empty"><p>Empty CSV file</p></div>';
      }

      const rows: string[][] = [];
      
      for (const line of lines) {
        const row: string[] = [];
        let currentField = '';
        let inQuotes = false;
        
        for (let i = 0; i < line.length; i++) {
          const char = line[i];
          const nextChar = line[i + 1];
          
          if (char === '"') {
            if (inQuotes && nextChar === '"') {
              // Escaped quote
              currentField += '"';
              i++; // Skip next quote
            } else {
              // Toggle quote state
              inQuotes = !inQuotes;
            }
          } else if (char === ',' && !inQuotes) {
            // Field separator
            row.push(currentField);
            currentField = '';
          } else {
            currentField += char;
          }
        }
        
        // Add the last field
        row.push(currentField);
        rows.push(row);
      }

      if (rows.length === 0) {
        return '<div class="csv-empty"><p>No data in CSV file</p></div>';
      }

      // Find the maximum number of columns to ensure consistent table structure
      const maxColumns = Math.max(...rows.map(row => row.length));

      // Determine if first row should be treated as header (if it has more than 1 row)
      const hasHeader = rows.length > 1;
      const headerRow = hasHeader ? rows[0] : null;
      const dataRows = hasHeader ? rows.slice(1) : rows;

      // Build HTML table
      let html = '<div class="csv-table-wrapper"><table class="csv-table">';
      
      // Add header row if we have one
      if (hasHeader && headerRow) {
        html += '<thead><tr>';
        for (let i = 0; i < maxColumns; i++) {
          const cell = headerRow[i] || '';
          html += `<th>${escapeHtml(cell)}</th>`;
        }
        html += '</tr></thead>';
      }
      
      // Add data rows
      html += '<tbody>';
      for (const row of dataRows) {
        html += '<tr>';
        for (let i = 0; i < maxColumns; i++) {
          const cell = row[i] || '';
          html += `<td>${escapeHtml(cell)}</td>`;
        }
        html += '</tr>';
      }
      html += '</tbody></table></div>';

      return html;
    } catch (err) {
      console.error('Error parsing CSV:', err);
      return `<div class="csv-error"><p>Error parsing CSV: ${escapeHtml(err instanceof Error ? err.message : String(err))}</p></div>`;
    }
  }

  // Escape HTML to prevent XSS
  function escapeHtml(text: string): string {
    const map: Record<string, string> = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#039;'
    };
    return text.replace(/[&<>"']/g, (m) => map[m]);
  }

  async function applySyntaxHighlighting(content: string, ext: string) {
    try {
      const hljsModule = await import('highlight.js');
      // highlight.js v11+ uses default export
      const hljs = hljsModule.default || hljsModule;
      const lang = getHighlightLanguage(ext);
      
      // Register Markdown language if needed (not in highlight.js by default)
      if (lang === 'markdown' && !hljs.getLanguage('markdown')) {
        hljs.registerLanguage('markdown', function(hljs) {
          return {
            name: 'Markdown',
            aliases: ['md', 'mkdown', 'mkd'],
            contains: [
              // Headers
              {
                className: 'section',
                begin: /^#{1,6}\s+/,
                relevance: 10
              },
              // Bold
              {
                className: 'strong',
                begin: /\*\*[^*]+\*\*/,
                relevance: 0
              },
              {
                className: 'strong',
                begin: /__[^_]+__/,
                relevance: 0
              },
              // Italic
              {
                className: 'emphasis',
                begin: /\*[^*]+\*/,
                relevance: 0
              },
              {
                className: 'emphasis',
                begin: /_[^_]+_/,
                relevance: 0
              },
              // Inline code
              {
                className: 'code',
                begin: /`[^`]+`/,
                relevance: 0
              },
              // Code blocks
              {
                className: 'code',
                begin: /^```[\w]*/,
                end: /^```$/,
                contains: [{ begin: /./ }]
              },
              // Links
              {
                className: 'link',
                begin: /\[/,
                end: /\]/,
                contains: [
                  {
                    className: 'string',
                    begin: /\(/,
                    end: /\)/
                  }
                ]
              },
              // Images
              {
                className: 'string',
                begin: /!\[/,
                end: /\]/
              },
              // Lists
              {
                className: 'bullet',
                begin: /^(\s*)([*+-]|\d+\.)\s+/,
                relevance: 0
              },
              // Blockquotes
              {
                className: 'quote',
                begin: /^>\s+/,
                relevance: 0
              },
              // Horizontal rules
              {
                className: 'horizontal_rule',
                begin: /^(\*{3,}|-{3,}|_{3,})$/,
                relevance: 0
              }
            ]
          };
        });
      }
      
      // Register AsciiDoc language if needed (not in highlight.js by default)
      if (lang === 'asciidoc' && !hljs.getLanguage('asciidoc')) {
        hljs.registerLanguage('asciidoc', function(hljs) {
          return {
            name: 'AsciiDoc',
            aliases: ['adoc', 'asciidoc', 'ad'],
            contains: [
              // Headers
              {
                className: 'section',
                begin: /^={1,6}\s+/,
                relevance: 10
              },
              // Bold
              {
                className: 'strong',
                begin: /\*\*[^*]+\*\*/,
                relevance: 0
              },
              // Italic
              {
                className: 'emphasis',
                begin: /_[^_]+_/,
                relevance: 0
              },
              // Inline code
              {
                className: 'code',
                begin: /`[^`]+`/,
                relevance: 0
              },
              // Code blocks
              {
                className: 'code',
                begin: /^----+$/,
                end: /^----+$/,
                contains: [{ begin: /./ }]
              },
              // Lists
              {
                className: 'bullet',
                begin: /^(\*+|\.+|-+)\s+/,
                relevance: 0
              },
              // Links
              {
                className: 'link',
                begin: /link:/,
                end: /\[/,
                contains: [{ begin: /\[/, end: /\]/ }]
              },
              // Comments
              {
                className: 'comment',
                begin: /^\/\/.*$/,
                relevance: 0
              },
              // Attributes
              {
                className: 'attr',
                begin: /^:.*:$/,
                relevance: 0
              }
            ]
          };
        });
      }
      
      // Apply highlighting
      if (lang === 'plaintext') {
        highlightedFileContent = `<pre><code class="hljs">${hljs.highlight(content, { language: 'plaintext' }).value}</code></pre>`;
      } else if (hljs.getLanguage(lang)) {
        highlightedFileContent = `<pre><code class="hljs language-${lang}">${hljs.highlight(content, { language: lang }).value}</code></pre>`;
      } else {
        // Fallback to auto-detection
        highlightedFileContent = `<pre><code class="hljs">${hljs.highlightAuto(content).value}</code></pre>`;
      }
    } catch (err) {
      console.error('Error applying syntax highlighting:', err);
      // Fallback to plain text
      highlightedFileContent = `<pre><code class="hljs">${content}</code></pre>`;
    }
  }

  async function loadForkInfo() {
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/fork`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        forkInfo = await response.json();
      }
    } catch (err) {
      console.error('Error loading fork info:', err);
    }
  }

  // Helper function to count all replies recursively (including nested ones)
  function countAllReplies(comments: Array<{ replies?: Array<any> }> | undefined): number {
    if (!comments || comments.length === 0) {
      return 0;
    }
    let count = comments.length;
    for (const comment of comments) {
      if (comment.replies && comment.replies.length > 0) {
        count += countAllReplies(comment.replies);
      }
    }
    return count;
  }

  async function checkCloneStatus(force: boolean = false) {
    if (checkingCloneStatus) return;
    if (!force && isRepoCloned !== null) {
      console.log(`[Clone Status] Skipping check - already checked: ${isRepoCloned}, force: ${force}`);
      return;
    }
    
    checkingCloneStatus = true;
    try {
      // Check if repo exists locally by trying to fetch branches
      // 404 = repo not cloned, 403 = repo exists but access denied (cloned), 200 = cloned and accessible
      const url = `/api/repos/${npub}/${repo}/branches`;
      console.log(`[Clone Status] Checking clone status for ${npub}/${repo}...`);
      const response = await fetch(url, {
        headers: buildApiHeaders()
      });
      // If response is 403, repo exists (cloned) but user doesn't have access
      // If response is 404, repo doesn't exist (not cloned)
      // If response is 200, repo exists and is accessible (cloned)
      const wasCloned = response.status !== 404;
      isRepoCloned = wasCloned;
      
      // If repo is not cloned, check if API fallback is available
      if (!wasCloned) {
        // Try to detect API fallback by checking if we have clone URLs
        if (repoCloneUrls && repoCloneUrls.length > 0) {
          // We have clone URLs, so API fallback might work - will be detected when loadBranches() runs
          apiFallbackAvailable = null; // Will be set to true if a subsequent request succeeds
        } else {
          apiFallbackAvailable = false;
        }
      } else {
        // Repo is cloned, API fallback not needed
        apiFallbackAvailable = false;
      }
      
      console.log(`[Clone Status] Repo ${wasCloned ? 'is cloned' : 'is not cloned'} (status: ${response.status}), API fallback: ${apiFallbackAvailable}`);
    } catch (err) {
      // On error, assume not cloned
      console.warn('[Clone Status] Error checking clone status:', err);
      isRepoCloned = false;
      apiFallbackAvailable = false;
    } finally {
      checkingCloneStatus = false;
    }
  }

  async function cloneRepository() {
    if (cloning) return;
    
    cloning = true;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/clone`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        }
      });

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({ message: response.statusText }));
        throw new Error(errorData.message || `Failed to clone repository: ${response.statusText}`);
      }

      const data = await response.json();
      
      if (data.alreadyExists) {
        alert('Repository already exists locally.');
        // Force refresh clone status
        await checkCloneStatus(true);
      } else {
        alert('Repository cloned successfully! The repository is now available on this server.');
        // Force refresh clone status
        await checkCloneStatus(true);
        // Reset API fallback status since repo is now cloned
        apiFallbackAvailable = false;
        // Reload data to use the cloned repo instead of API
        await Promise.all([
          loadBranches(),
          loadFiles(currentPath),
          loadReadme(),
          loadTags(),
          loadCommitHistory()
        ]);
      }
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Failed to clone repository';
      alert(`Error: ${errorMessage}`);
      console.error('Error cloning repository:', err);
    } finally {
      cloning = false;
    }
  }

  async function forkRepository() {
    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    forking = true;
    error = null;

    try {
      // Security: Truncate npub in logs
      const truncatedNpub = npub.length > 16 ? `${npub.slice(0, 12)}...` : npub;
      console.log(`[Fork UI] Starting fork of ${truncatedNpub}/${repo}...`);
      const response = await fetch(`/api/repos/${npub}/${repo}/fork`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({ userPubkey })
      });

      const data = await response.json();
      
      if (response.ok && data.success !== false) {
        const message = data.message || `Repository forked successfully! Published to ${data.fork?.publishedTo?.announcement || 0} relay(s).`;
        console.log(`[Fork UI] ✓ ${message}`);
        // Security: Truncate npub in logs
        const truncatedForkNpub = data.fork.npub.length > 16 ? `${data.fork.npub.slice(0, 12)}...` : data.fork.npub;
        console.log(`[Fork UI]   - Fork location: /repos/${truncatedForkNpub}/${data.fork.repo}`);
        console.log(`[Fork UI]   - Announcement ID: ${data.fork.announcementId}`);
        console.log(`[Fork UI]   - Ownership Transfer ID: ${data.fork.ownershipTransferId}`);
        
        alert(`${message}\n\nRedirecting to your fork...`);
        goto(`/repos/${data.fork.npub}/${data.fork.repo}`);
      } else {
        const errorMessage = data.error || 'Failed to fork repository';
        const errorDetails = data.details ? `\n\nDetails: ${data.details}` : '';
        const fullError = `${errorMessage}${errorDetails}`;
        
        console.error(`[Fork UI] ✗ Fork failed: ${errorMessage}`);
        if (data.details) {
          console.error(`[Fork UI] Details: ${data.details}`);
        }
        if (data.eventName) {
          console.error(`[Fork UI] Failed event: ${data.eventName}`);
        }
        
        error = fullError;
        alert(`Fork failed!\n\n${fullError}`);
      }
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Failed to fork repository';
      console.error(`[Fork UI] ✗ Unexpected error: ${errorMessage}`, err);
      error = errorMessage;
      alert(`Fork failed!\n\n${errorMessage}`);
    } finally {
      forking = false;
    }
  }

  async function loadDiscussions() {
    if (repoNotFound) return;
    loadingDiscussions = true;
    error = null;
    try {
      const decoded = nip19.decode(npub);
      if (decoded.type !== 'npub') {
        throw new Error('Invalid npub format');
      }
      const repoOwnerPubkey = decoded.data as string;

      // Fetch repo announcement to get chat-relay tags and announcement ID
      const client = new NostrClient(DEFAULT_NOSTR_RELAYS);
      const events = await client.fetchEvents([
        {
          kinds: [KIND.REPO_ANNOUNCEMENT],
          authors: [repoOwnerPubkeyDerived],
          '#d': [repo],
          limit: 1
        }
      ]);

      if (events.length === 0) {
        discussions = [];
        return;
      }

      const announcement = events[0];
      const chatRelays = announcement.tags
        .filter(t => t[0] === 'chat-relay')
        .flatMap(t => t.slice(1))
        .filter(url => url && typeof url === 'string') as string[];

      // Get default relays
      const { getGitUrl } = await import('$lib/config.js');
      const { DiscussionsService } = await import('$lib/services/nostr/discussions-service.js');
      
      // Get user's relays if available
      let userRelays: string[] = [];
      const currentUserPubkey = $userStore.userPubkey || userPubkey;
      if (currentUserPubkey) {
        try {
          const { outbox } = await getUserRelays(currentUserPubkey, client);
          userRelays = outbox;
        } catch (err) {
          console.warn('Failed to get user relays, using defaults:', err);
        }
      }

      // Combine all available relays: default + search + chat + user relays
      const allRelays = [...new Set([
        ...DEFAULT_NOSTR_RELAYS,
        ...DEFAULT_NOSTR_SEARCH_RELAYS,
        ...chatRelays,
        ...userRelays
      ])];
      
      console.log('[Discussions] Using all available relays for threads:', allRelays);
      console.log('[Discussions] Chat relays from announcement:', chatRelays);

      const discussionsService = new DiscussionsService(allRelays);
      const discussionEntries = await discussionsService.getDiscussions(
        repoOwnerPubkey,
        repo,
        announcement.id,
        announcement.pubkey,
        allRelays, // Use all relays for threads
        allRelays  // Use all relays for comments too
      );
      
      console.log('[Discussions] Found', discussionEntries.length, 'discussion entries');

      discussions = discussionEntries.map(entry => ({
        type: entry.type,
        id: entry.id,
        title: entry.title,
        content: entry.content,
        author: entry.author,
        createdAt: entry.createdAt,
        kind: entry.kind,
        pubkey: entry.pubkey,
        comments: entry.comments
      }));

      // Fetch full events for discussions and comments to get tags for blurbs
      await loadDiscussionEvents(discussions);
      
      // Fetch nostr: links from discussion content
      for (const discussion of discussions) {
        if (discussion.content) {
          await loadNostrLinks(discussion.content);
        }
        if (discussion.comments) {
          for (const comment of discussion.comments) {
            if (comment.content) {
              await loadNostrLinks(comment.content);
            }
            if (comment.replies) {
              for (const reply of comment.replies) {
                if (reply.content) {
                  await loadNostrLinks(reply.content);
                }
                if (reply.replies) {
                  for (const nestedReply of reply.replies) {
                    if (nestedReply.content) {
                      await loadNostrLinks(nestedReply.content);
                    }
                  }
                }
              }
            }
          }
        }
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to load discussions';
      console.error('Error loading discussions:', err);
    } finally {
      loadingDiscussions = false;
    }
  }


  async function createDiscussionThread() {
    if (!userPubkey || !userPubkeyHex) {
      error = 'You must be logged in to create a discussion thread';
      return;
    }

    if (!newThreadTitle.trim()) {
      error = 'Thread title is required';
      return;
    }

    creatingThread = true;
    error = null;

    try {
      const decoded = nip19.decode(npub);
      if (decoded.type !== 'npub') {
        throw new Error('Invalid npub format');
      }
      const repoOwnerPubkey = decoded.data as string;

      // Get repo announcement to get the repo address
      const client = new NostrClient(DEFAULT_NOSTR_RELAYS);
      const events = await client.fetchEvents([
        {
          kinds: [KIND.REPO_ANNOUNCEMENT],
          authors: [repoOwnerPubkeyDerived],
          '#d': [repo],
          limit: 1
        }
      ]);

      if (events.length === 0) {
        throw new Error('Repository announcement not found');
      }

      const announcement = events[0];
      const repoAddress = `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkey}:${repo}`;

      // Get chat relays from announcement, or use default relays
      const chatRelays = announcement.tags
        .filter(t => t[0] === 'chat-relay')
        .flatMap(t => t.slice(1))
        .filter(url => url && typeof url === 'string') as string[];

      // Combine all available relays
      let allRelays = [...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS, ...chatRelays];
      if (userPubkey) {
        try {
          const { outbox } = await getUserRelays(userPubkey, client);
          allRelays = [...allRelays, ...outbox];
        } catch (err) {
          console.warn('Failed to get user relays:', err);
        }
      }
      allRelays = [...new Set(allRelays)]; // Deduplicate

      // Create kind 11 thread event
      const threadEventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
        kind: KIND.THREAD,
        pubkey: userPubkeyHex,
        created_at: Math.floor(Date.now() / 1000),
        tags: [
          ['a', repoAddress],
          ['title', newThreadTitle.trim()],
          ['t', 'repo']
        ],
        content: newThreadContent.trim() || ''
      };

      // Sign the event using NIP-07
      const signedEvent = await signEventWithNIP07(threadEventTemplate);

      // Publish to all available relays
      const publishClient = new NostrClient(allRelays);
      const result = await publishClient.publishEvent(signedEvent, allRelays);

      if (result.failed.length > 0 && result.success.length === 0) {
        throw new Error('Failed to publish thread to all relays');
      }

      // Clear form and close dialog
      newThreadTitle = '';
      newThreadContent = '';
      showCreateThreadDialog = false;

      // Reload discussions
      await loadDiscussions();
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create discussion thread';
      console.error('Error creating discussion thread:', err);
    } finally {
      creatingThread = false;
    }
  }

  async function createThreadReply() {
    if (!userPubkey || !userPubkeyHex) {
      error = 'You must be logged in to reply';
      return;
    }

    if (!replyContent.trim()) {
      error = 'Reply content is required';
      return;
    }

    if (!replyingToThread && !replyingToComment) {
      error = 'Must reply to either a thread or a comment';
      return;
    }

    creatingReply = true;
    error = null;

    try {
      const decoded = nip19.decode(npub);
      if (decoded.type !== 'npub') {
        throw new Error('Invalid npub format');
      }
      const repoOwnerPubkey = decoded.data as string;

      // Get repo announcement to get the repo address and relays
      const client = new NostrClient(DEFAULT_NOSTR_RELAYS);
      nostrClient = client; // Store for use in other functions
      const events = await client.fetchEvents([
        {
          kinds: [KIND.REPO_ANNOUNCEMENT],
          authors: [repoOwnerPubkeyDerived],
          '#d': [repo],
          limit: 1
        }
      ]);

      if (events.length === 0) {
        throw new Error('Repository announcement not found');
      }

      const announcement = events[0];
      
      // Get chat relays from announcement, or use default relays
      const chatRelays = announcement.tags
        .filter(t => t[0] === 'chat-relay')
        .flatMap(t => t.slice(1))
        .filter(url => url && typeof url === 'string') as string[];

      // Combine all available relays
      let allRelays = [...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS, ...chatRelays];
      if (userPubkey) {
        try {
          const { outbox } = await getUserRelays(userPubkey, client);
          allRelays = [...allRelays, ...outbox];
        } catch (err) {
          console.warn('Failed to get user relays:', err);
        }
      }
      allRelays = [...new Set(allRelays)]; // Deduplicate

      let rootEventId: string;
      let rootKind: number;
      let rootPubkey: string;
      let parentEventId: string;
      let parentKind: number;
      let parentPubkey: string;

      if (replyingToComment) {
        // Replying to a comment - use the comment object we already have
        const comment = replyingToComment;
        
        // Determine root: if we have a thread, use it as root; otherwise use announcement
        if (replyingToThread) {
          rootEventId = replyingToThread.id;
          rootKind = replyingToThread.kind || KIND.THREAD;
          rootPubkey = replyingToThread.pubkey || replyingToThread.author;
        } else {
          // Comment is directly on announcement (in "Comments" pseudo-thread)
          rootEventId = announcement.id;
          rootKind = KIND.REPO_ANNOUNCEMENT;
          rootPubkey = announcement.pubkey;
        }

        // Parent is the comment we're replying to
        parentEventId = comment.id;
        parentKind = comment.kind || KIND.COMMENT;
        parentPubkey = comment.pubkey || comment.author;
      } else if (replyingToThread) {
        // Replying directly to a thread - use the thread object we already have
        rootEventId = replyingToThread.id;
        rootKind = replyingToThread.kind || KIND.THREAD;
        rootPubkey = replyingToThread.pubkey || replyingToThread.author;
        parentEventId = replyingToThread.id;
        parentKind = replyingToThread.kind || KIND.THREAD;
        parentPubkey = replyingToThread.pubkey || replyingToThread.author;
      } else {
        throw new Error('Must specify thread or comment to reply to');
      }

      // Create kind 1111 comment event
      const commentEventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
        kind: KIND.COMMENT,
        pubkey: userPubkeyHex,
        created_at: Math.floor(Date.now() / 1000),
        tags: [
          ['e', parentEventId, '', 'reply'], // Parent event
          ['k', parentKind.toString()], // Parent kind
          ['p', parentPubkey], // Parent pubkey
          ['E', rootEventId], // Root event
          ['K', rootKind.toString()], // Root kind
          ['P', rootPubkey] // Root pubkey
        ],
        content: replyContent.trim()
      };

      // Sign the event using NIP-07
      const signedEvent = await signEventWithNIP07(commentEventTemplate);

      // Publish to all available relays
      const publishClient = new NostrClient(allRelays);
      const result = await publishClient.publishEvent(signedEvent, allRelays);

      if (result.failed.length > 0 && result.success.length === 0) {
        throw new Error('Failed to publish reply to all relays');
      }

      // Save thread ID before clearing (for expanding after reload)
      const threadIdToExpand = replyingToThread?.id;

      // Clear form and close dialog
      replyContent = '';
      showReplyDialog = false;
      replyingToThread = null;
      replyingToComment = null;

      // Reload discussions to show the new reply
      await loadDiscussions();
      
      // Expand the thread if we were replying to a thread
      if (threadIdToExpand) {
        expandedThreads.add(threadIdToExpand);
        expandedThreads = new Set(expandedThreads); // Trigger reactivity
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create reply';
      console.error('Error creating reply:', err);
    } finally {
      creatingReply = false;
    }
  }

  function toggleThread(threadId: string) {
    if (expandedThreads.has(threadId)) {
      expandedThreads.delete(threadId);
    } else {
      expandedThreads.add(threadId);
    }
    // Trigger reactivity
    expandedThreads = new Set(expandedThreads);
  }

  async function loadDocumentation() {
    if (loadingDocs) return;
    // Reset documentation when reloading
    documentationHtml = null;
    documentationContent = null;
    documentationKind = null;
    
    loadingDocs = true;
    try {
      // Guard against SSR - $page store can only be accessed in component context
      if (typeof window === 'undefined') return;
      // Check if repo is private and user has access
      const data = $page.data as typeof pageData;
      if (repoIsPrivate) {
        // Check access via API
        const accessResponse = await fetch(`/api/repos/${npub}/${repo}/access`, {
          headers: buildApiHeaders()
        });
        if (accessResponse.ok) {
          const accessData = await accessResponse.json();
          if (!accessData.canView) {
            // User doesn't have access, don't load documentation
            loadingDocs = false;
            return;
          }
        } else {
          // Access check failed, don't load documentation
          loadingDocs = false;
          return;
        }
      }
      
      const decoded = nip19.decode(npub);
      if (decoded.type === 'npub') {
        const repoOwnerPubkey = decoded.data as string;
        const client = new NostrClient(DEFAULT_NOSTR_RELAYS);
        
        // First, get the repo announcement to find the documentation tag
        const announcementEvents = await client.fetchEvents([
          {
            kinds: [KIND.REPO_ANNOUNCEMENT],
            authors: [repoOwnerPubkeyDerived],
            '#d': [repo],
            limit: 1
          }
        ]);

        if (announcementEvents.length === 0) {
          loadingDocs = false;
          return;
        }

        const announcement = announcementEvents[0];
        
        // Look for documentation tag in the announcement
        const documentationTag = announcement.tags.find(t => t[0] === 'documentation');
        
        documentationKind = null;
        
        if (documentationTag && documentationTag[1]) {
          // Parse the a-tag format: kind:pubkey:identifier
          const docAddress = documentationTag[1];
          const parts = docAddress.split(':');
          
          if (parts.length >= 3) {
            documentationKind = parseInt(parts[0]);
            const docPubkey = parts[1];
            const docIdentifier = parts.slice(2).join(':'); // In case identifier contains ':'
            
            // Fetch the documentation event
            const docEvents = await client.fetchEvents([
              {
                kinds: [documentationKind],
                authors: [docPubkey],
                '#d': [docIdentifier],
                limit: 1
              }
            ]);
            
            if (docEvents.length > 0) {
              documentationContent = docEvents[0].content || null;
            } else {
              console.warn('Documentation event not found:', docAddress);
              documentationContent = null;
            }
          } else {
            console.warn('Invalid documentation tag format:', docAddress);
            documentationContent = null;
          }
        } else {
          // No documentation tag, try to use announcement content as fallback
          documentationContent = announcement.content || null;
          // Announcement is kind 30617, not a doc kind, so keep documentationKind as null
        }
        
        // Render content based on kind: AsciiDoc for 30041 or 30818, Markdown otherwise
        if (documentationContent) {
          // Check if we should use AsciiDoc parser (kinds 30041 or 30818)
          const useAsciiDoc = documentationKind === 30041 || documentationKind === 30818;
          
          if (useAsciiDoc) {
            // Use AsciiDoc parser
            const Asciidoctor = (await import('@asciidoctor/core')).default;
            const asciidoctor = Asciidoctor();
            const converted = asciidoctor.convert(documentationContent, {
              safe: 'safe',
              attributes: {
                'source-highlighter': 'highlight.js'
              }
            });
            // Convert to string if it's a Document object
            documentationHtml = typeof converted === 'string' ? converted : String(converted);
          } else {
            // Use Markdown parser
            const MarkdownIt = (await import('markdown-it')).default;
            const hljsModule = await import('highlight.js');
            const hljs = hljsModule.default || hljsModule;
            
            const md = new MarkdownIt({
              highlight: function (str: string, lang: string): string {
                if (lang && hljs.getLanguage(lang)) {
                  try {
                    return hljs.highlight(str, { language: lang }).value;
                  } catch (__) {}
                }
                return '';
              }
            });
            
            documentationHtml = md.render(documentationContent);
          }
        } else {
          // No content found, clear HTML
          documentationHtml = null;
        }
      }
    } catch (err) {
      console.error('Error loading documentation:', err);
      documentationHtml = null;
    } finally {
      loadingDocs = false;
    }
  }

  async function loadRepoImages() {
    try {
      // Get images from page data (loaded from announcement)
      // Use $page.data directly to ensure we get the latest data
      // Guard against SSR - $page store can only be accessed in component context
      if (typeof window === 'undefined') return;
      const data = $page.data as typeof pageData;
      if (data.image) {
        repoImage = data.image;
        console.log('[Repo Images] Loaded image from pageData:', repoImage);
      }
      if (data.banner) {
        repoBanner = data.banner;
        console.log('[Repo Images] Loaded banner from pageData:', repoBanner);
      }

      // Also fetch from announcement directly as fallback (only if not private or user has access)
      if (!repoImage && !repoBanner) {
        // Guard against SSR - $page store can only be accessed in component context
        if (typeof window === 'undefined') return;
        const data = $page.data as typeof pageData;
        // Check access for private repos
        if (repoIsPrivate) {
          const headers: Record<string, string> = {};
          if (userPubkey) {
            try {
              const decoded = nip19.decode(userPubkey);
              if (decoded.type === 'npub') {
                headers['X-User-Pubkey'] = decoded.data as string;
              } else {
                headers['X-User-Pubkey'] = userPubkey;
              }
            } catch {
              headers['X-User-Pubkey'] = userPubkey;
            }
          }
          
          const accessResponse = await fetch(`/api/repos/${npub}/${repo}/access`, { headers });
          if (!accessResponse.ok) {
            // Access check failed, don't fetch images
            return;
          }
          const accessData = await accessResponse.json();
          if (!accessData.canView) {
            // User doesn't have access, don't fetch images
            return;
          }
        }
        
        const decoded = nip19.decode(npub);
        if (decoded.type === 'npub') {
          const repoOwnerPubkey = decoded.data as string;
          const client = new NostrClient(DEFAULT_NOSTR_RELAYS);
          const events = await client.fetchEvents([
            {
              kinds: [30617], // REPO_ANNOUNCEMENT
              authors: [repoOwnerPubkeyDerived],
              '#d': [repo],
              limit: 1
            }
          ]);

          if (events.length > 0) {
            const announcement = events[0];
            const imageTag = announcement.tags.find((t: string[]) => t[0] === 'image');
            const bannerTag = announcement.tags.find((t: string[]) => t[0] === 'banner');
            
            if (imageTag?.[1]) {
              repoImage = imageTag[1];
              console.log('[Repo Images] Loaded image from announcement:', repoImage);
            }
            if (bannerTag?.[1]) {
              repoBanner = bannerTag[1];
              console.log('[Repo Images] Loaded banner from announcement:', repoBanner);
            }
          } else {
            console.log('[Repo Images] No announcement found');
          }
        }
      }
      
      if (!repoImage && !repoBanner) {
        console.log('[Repo Images] No images found in announcement');
      }
    } catch (err) {
      console.error('Error loading repo images:', err);
    }
  }

  // Reactively update images when pageData changes (only once, when data becomes available)
  $effect(() => {
    // Guard against SSR and component destruction
    if (typeof window === 'undefined' || !isMounted) return;
    try {
      const data = $page.data as typeof pageData;
      if (!data || !isMounted) return;
      // Only update if we have new data and don't already have the images set
      if (data.image && data.image !== repoImage && isMounted) {
        repoImage = data.image;
        console.log('[Repo Images] Updated image from pageData (reactive):', repoImage);
      }
      if (data.banner && data.banner !== repoBanner && isMounted) {
        repoBanner = data.banner;
        console.log('[Repo Images] Updated banner from pageData (reactive):', repoBanner);
      }
    } catch (err) {
      // Ignore errors during destruction
      if (isMounted) {
        console.warn('Image update effect error:', err);
      }
    }
  });

  onMount(async () => {
    // Initialize bookmarks service
    bookmarksService = new BookmarksService(DEFAULT_NOSTR_SEARCH_RELAYS);
    
    // Load clone URL reachability status
    loadCloneUrlReachability().catch(err => console.warn('Failed to load clone URL reachability:', err));
    
    // Decode npub to get repo owner pubkey for bookmark address
    try {
      const decoded = nip19.decode(npub);
      if (decoded.type === 'npub') {
        repoOwnerPubkeyState = decoded.data as string;
        repoAddress = `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkeyState}:${repo}`;
      }
    } catch (err) {
      console.warn('Failed to decode npub for bookmark address:', err);
    }

    // Close menu when clicking outside (handled by RepoHeaderEnhanced component)
    clickOutsideHandler = (event: MouseEvent) => {
      if (!isMounted) return;
      try {
        const target = event.target as HTMLElement;
        if (showRepoMenu && !target.closest('.repo-header') && isMounted) {
          showRepoMenu = false;
        }
      } catch (err) {
        // Ignore errors during destruction
        if (isMounted) {
          console.warn('Click outside handler error:', err);
        }
      }
    };

    document.addEventListener('click', clickOutsideHandler);

    await loadBranches();
    if (!isMounted) return;
    
    // Skip other API calls if repository doesn't exist
    if (repoNotFound) {
      loading = false;
      return;
    }
    
    // loadBranches() already handles setting currentBranch to the default branch
    await loadFiles();
    if (!isMounted) return;
    
    await checkAuth();
    if (!isMounted) return;
    
    await loadTags();
    if (!isMounted) return;
    
    await checkMaintainerStatus();
    if (!isMounted) return;
    
    await loadBookmarkStatus();
    if (!isMounted) return;
    
    await loadAllMaintainers();
    if (!isMounted) return;
    
    // Check clone status (needed to disable write operations)
    await checkCloneStatus();
    if (!isMounted) return;
    
    await checkVerification();
    if (!isMounted) return;
    
    await loadReadme();
    if (!isMounted) return;
    
    await loadForkInfo();
    if (!isMounted) return;
    
    await loadRepoImages();
    if (!isMounted) return;
    
    // Load clone URL reachability status
    loadCloneUrlReachability().catch(err => {
      if (isMounted) console.warn('Failed to load clone URL reachability:', err);
    });
    
    // Set up auto-save if enabled
    setupAutoSave().catch(err => {
      if (isMounted) console.warn('Failed to setup auto-save:', err);
    });
  });
  
  // Cleanup on destroy - only register on client side to prevent SSR errors
  if (typeof window !== 'undefined') {
    onDestroy(() => {
      try {
        // Mark component as unmounted first to prevent any state updates
        isMounted = false;
        
        // Clean up intervals and timeouts
        if (autoSaveInterval) {
          clearInterval(autoSaveInterval);
          autoSaveInterval = null;
        }
        
        if (readmeAutoLoadTimeout) {
          clearTimeout(readmeAutoLoadTimeout);
          readmeAutoLoadTimeout = null;
        }
        
        // Clean up event listeners
        if (clickOutsideHandler && typeof document !== 'undefined') {
          document.removeEventListener('click', clickOutsideHandler);
          clickOutsideHandler = null;
        }
      } catch (err) {
        // Ignore all errors during cleanup - component is being destroyed anyway
      }
    });
  }

  async function checkAuth() {
    // Check userStore first
    const currentUser = $userStore;
    if (currentUser.userPubkey && currentUser.userPubkeyHex) {
      userPubkey = currentUser.userPubkey;
      userPubkeyHex = currentUser.userPubkeyHex;
      // Recheck maintainer status and bookmark status after auth
      await checkMaintainerStatus();
      await loadBookmarkStatus();
      return;
    }
    
    // Fallback: try NIP-07 if store doesn't have it
    try {
      if (isNIP07Available()) {
        const pubkey = await getPublicKeyWithNIP07();
        userPubkey = pubkey;
        // Convert to hex if needed
        if (/^[0-9a-f]{64}$/i.test(pubkey)) {
          userPubkeyHex = pubkey.toLowerCase();
        } else {
          try {
            const decoded = nip19.decode(pubkey);
            if (decoded.type === 'npub') {
              userPubkeyHex = decoded.data as string;
            }
          } catch {
            userPubkeyHex = pubkey;
          }
        }
        // Recheck maintainer status and bookmark status after auth
        await checkMaintainerStatus();
        await loadBookmarkStatus();
      }
    } catch (err) {
      console.log('NIP-07 not available or user not connected');
      userPubkey = null;
      userPubkeyHex = null;
    }
  }

  async function login() {
    // Check userStore first
    const currentUser = $userStore;
    if (currentUser.userPubkey && currentUser.userPubkeyHex) {
      userPubkey = currentUser.userPubkey;
      userPubkeyHex = currentUser.userPubkeyHex;
      // Re-check maintainer status and bookmark status after login
      await checkMaintainerStatus();
      await loadBookmarkStatus();
      // Check for pending transfers (user is already logged in via store)
      if (userPubkeyHex) {
        try {
          const response = await fetch('/api/transfers/pending', {
            headers: {
              'X-User-Pubkey': userPubkeyHex
            }
          });
          if (response.ok) {
            const data = await response.json();
            if (data.pendingTransfers && data.pendingTransfers.length > 0) {
              window.dispatchEvent(new CustomEvent('pendingTransfers', { 
                detail: { transfers: data.pendingTransfers } 
              }));
            }
          }
        } catch (err) {
          console.error('Failed to check for pending transfers:', err);
        }
      }
      return;
    }
    
    // Fallback: try NIP-07 - need to check write access and update store
    try {
      if (!isNIP07Available()) {
        alert('NIP-07 extension not found. Please install a Nostr extension like Alby or nos2x.');
        return;
      }
      const pubkey = await getPublicKeyWithNIP07();
      let pubkeyHex: string;
      // Convert to hex if needed
      if (/^[0-9a-f]{64}$/i.test(pubkey)) {
        pubkeyHex = pubkey.toLowerCase();
        userPubkey = pubkey;
      } else {
        try {
          const decoded = nip19.decode(pubkey);
          if (decoded.type === 'npub') {
            pubkeyHex = decoded.data as string;
            userPubkey = pubkey;
          } else {
            throw new Error('Invalid pubkey format');
          }
        } catch {
          error = 'Invalid public key format';
          return;
        }
      }
      
      userPubkeyHex = pubkeyHex;
      
      // Check write access and update user store
      const { determineUserLevel } = await import('$lib/services/nostr/user-level-service.js');
      const levelResult = await determineUserLevel(userPubkey, userPubkeyHex);
      
      // Update user store with write access level
      userStore.setUser(
        levelResult.userPubkey,
        levelResult.userPubkeyHex,
        levelResult.level,
        levelResult.error || null
      );
      
      // Update activity tracking
      const { updateActivity } = await import('$lib/services/activity-tracker.js');
      updateActivity();
      
      // Check for pending transfer events
      if (userPubkeyHex) {
        try {
          const response = await fetch('/api/transfers/pending', {
            headers: {
              'X-User-Pubkey': userPubkeyHex
            }
          });
          if (response.ok) {
            const data = await response.json();
            if (data.pendingTransfers && data.pendingTransfers.length > 0) {
              window.dispatchEvent(new CustomEvent('pendingTransfers', { 
                detail: { transfers: data.pendingTransfers } 
              }));
            }
          }
        } catch (err) {
          console.error('Failed to check for pending transfers:', err);
        }
      }
      
      // Re-check maintainer status and bookmark status after login
      await checkMaintainerStatus();
      await loadBookmarkStatus();
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to connect';
      console.error('Login error:', err);
    }
  }


  async function loadBookmarkStatus() {
    if (!userPubkey || !repoAddress || !bookmarksService) return;
    
    try {
      isBookmarked = await bookmarksService.isBookmarked(userPubkey, repoAddress);
    } catch (err) {
      console.warn('Failed to load bookmark status:', err);
    }
  }

  async function toggleBookmark() {
    if (!userPubkey || !repoAddress || !bookmarksService || loadingBookmark) return;
    
    loadingBookmark = true;
    try {
      // Get user's relays for publishing
      const { getUserRelays } = await import('$lib/services/nostr/user-relays.js');
      const allSearchRelays = [...new Set([...DEFAULT_NOSTR_SEARCH_RELAYS, ...DEFAULT_NOSTR_RELAYS])];
      const fullRelayClient = new NostrClient(allSearchRelays);
      const { outbox, inbox } = await getUserRelays(userPubkey, fullRelayClient);
      const userRelays = combineRelays(outbox.length > 0 ? outbox : inbox, DEFAULT_NOSTR_RELAYS);
      
      let success = false;
      if (isBookmarked) {
        success = await bookmarksService.removeBookmark(userPubkey, repoAddress, userRelays);
      } else {
        success = await bookmarksService.addBookmark(userPubkey, repoAddress, userRelays);
      }
      
      if (success) {
        isBookmarked = !isBookmarked;
      } else {
        alert(`Failed to ${isBookmarked ? 'remove' : 'add'} bookmark. Please try again.`);
      }
    } catch (err) {
      console.error('Failed to toggle bookmark:', err);
      alert(`Failed to ${isBookmarked ? 'remove' : 'add'} bookmark: ${String(err)}`);
    } finally {
      loadingBookmark = false;
    }
  }

  async function copyEventId() {
    if (!repoAddress || !repoOwnerPubkeyDerived) {
      alert('Repository address not available');
      return;
    }

    try {
      // Parse the repo address: kind:pubkey:identifier
      const parts = repoAddress.split(':');
      if (parts.length < 3) {
        throw new Error('Invalid repository address format');
      }

      const kind = parseInt(parts[0]);
      const pubkey = parts[1];
      const identifier = parts.slice(2).join(':'); // In case identifier contains ':'

      // Generate naddr synchronously
      const naddr = nip19.naddrEncode({
        kind,
        pubkey,
        identifier,
        relays: [] // Optional: could include relays if available
      });

      // Copy naddr to clipboard immediately (while we have user activation)
      try {
        await navigator.clipboard.writeText(naddr);
      } catch (clipboardErr) {
        // Fallback: use execCommand for older browsers or if clipboard API fails
        const textArea = document.createElement('textarea');
        textArea.value = naddr;
        textArea.style.position = 'fixed';
        textArea.style.opacity = '0';
        document.body.appendChild(textArea);
        textArea.select();
        try {
          document.execCommand('copy');
          textArea.remove();
        } catch (execErr) {
          textArea.remove();
          throw new Error('Failed to copy to clipboard. Please copy manually: ' + naddr);
        }
      }
      
      // Show message with naddr
      alert(`Event ID copied to clipboard!\n\nnaddr (repository address):\n${naddr}`);
    } catch (err) {
      console.error('Failed to copy event ID:', err);
      alert(`Failed to copy event ID: ${err instanceof Error ? err.message : String(err)}`);
    }
  }

  async function checkMaintainerStatus() {
    if (repoNotFound || !userPubkey) {
      isMaintainer = false;
      return;
    }

    loadingMaintainerStatus = true;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/maintainers?userPubkey=${encodeURIComponent(userPubkey)}`);
      if (response.ok) {
        const data = await response.json();
        isMaintainer = data.isMaintainer || false;
      }
    } catch (err) {
      console.error('Failed to check maintainer status:', err);
      isMaintainer = false;
    } finally {
      loadingMaintainerStatus = false;
    }
  }

  async function loadAllMaintainers() {
    if (repoNotFound || loadingMaintainers) return;
    
    loadingMaintainers = true;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/maintainers`);
      if (response.ok) {
        const data = await response.json();
        const owner = data.owner;
        const maintainers = data.maintainers || [];
        
        // Create array with all maintainers, marking the owner
        const allMaintainersList: Array<{ pubkey: string; isOwner: boolean }> = [];
        const seen = new Set<string>();
        const ownerLower = owner?.toLowerCase();
        
        // Process all maintainers, marking owner and deduplicating
        for (const maintainer of maintainers) {
          const maintainerLower = maintainer.toLowerCase();
          
          // Skip if we've already added this pubkey (case-insensitive check)
          if (seen.has(maintainerLower)) {
            continue;
          }
          
          // Mark as seen
          seen.add(maintainerLower);
          
          // Determine if this is the owner
          const isOwner = ownerLower && maintainerLower === ownerLower;
          
          // Add to list
          allMaintainersList.push({ 
            pubkey: maintainer, 
            isOwner: !!isOwner
          });
        }
        
        // Sort: owner first, then other maintainers
        allMaintainersList.sort((a, b) => {
          if (a.isOwner && !b.isOwner) return -1;
          if (!a.isOwner && b.isOwner) return 1;
          return 0;
        });
        
        // Ensure owner is always included (in case they weren't in maintainers list)
        if (owner && !seen.has(ownerLower)) {
          allMaintainersList.unshift({ pubkey: owner, isOwner: true });
        }
        
        allMaintainers = allMaintainersList;
      }
    } catch (err) {
      console.error('Failed to load maintainers:', err);
      maintainersLoaded = false; // Reset flag on error
      // Fallback to pageData if available
      if (repoOwnerPubkeyDerived) {
        allMaintainers = [{ pubkey: repoOwnerPubkeyDerived, isOwner: true }];
        if (repoMaintainers) {
          for (const maintainer of repoMaintainers) {
            if (maintainer.toLowerCase() !== repoOwnerPubkeyDerived.toLowerCase()) {
              allMaintainers.push({ pubkey: maintainer, isOwner: false });
            }
          }
        }
      }
    } finally {
      loadingMaintainers = false;
    }
  }

  async function checkVerification() {
    if (repoNotFound) return;
    loadingVerification = true;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/verify`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        const data = await response.json();
        console.log('[Verification] Response:', data);
        verificationStatus = data;
      } else {
        console.warn('[Verification] Response not OK:', response.status, response.statusText);
        verificationStatus = { verified: false, error: `Verification check failed: ${response.status}` };
      }
    } catch (err) {
      console.error('[Verification] Failed to check verification:', err);
      verificationStatus = { verified: false, error: 'Failed to check verification' };
    } finally {
      loadingVerification = false;
      console.log('[Verification] Status after check:', verificationStatus);
    }
  }

  async function generateAnnouncementFileForRepo() {
    if (!repoOwnerPubkeyDerived || !userPubkeyHex) {
      error = 'Unable to generate announcement file: missing repository or user information';
      return;
    }

    try {
      // Fetch the repository announcement event
      const nostrClient = new NostrClient([...new Set([...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS])]);
      const events = await nostrClient.fetchEvents([
        {
          kinds: [KIND.REPO_ANNOUNCEMENT],
          authors: [repoOwnerPubkeyDerived],
          '#d': [repo],
          limit: 1
        }
      ]);

      if (events.length === 0) {
        error = 'Repository announcement not found. Please ensure the repository is registered on Nostr.';
        return;
      }

      const announcement = events[0] as NostrEvent;
      // Generate announcement event JSON (for download/reference)
      verificationFileContent = JSON.stringify(announcement, null, 2) + '\n';
      showVerificationDialog = true;
    } catch (err) {
      console.error('Failed to generate announcement file:', err);
      error = `Failed to generate announcement file: ${err instanceof Error ? err.message : String(err)}`;
    }
  }

  function copyVerificationToClipboard() {
    if (!verificationFileContent) return;
    
    navigator.clipboard.writeText(verificationFileContent).then(() => {
      alert('Verification file content copied to clipboard!');
    }).catch((err) => {
      console.error('Failed to copy:', err);
      alert('Failed to copy to clipboard. Please select and copy manually.');
    });
  }

  async function deleteAnnouncement() {
    if (!userPubkey || !userPubkeyHex) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    if (!repoOwnerPubkeyDerived || userPubkeyHex !== repoOwnerPubkeyDerived) {
      alert('Only the repository owner can delete the announcement');
      return;
    }

    // First confirmation
    if (!confirm('WARNING: Are you sure you want to delete this repository announcement?\n\nThis will permanently delete the repository announcement from Nostr relays. This action CANNOT be undone.\n\nClick OK to continue, or Cancel to abort.')) {
      return;
    }

    // Second confirmation for critical operation
    if (!confirm('FINAL CONFIRMATION: This will permanently delete the repository announcement.\n\nAre you absolutely certain you want to proceed?\n\nThis action CANNOT be undone.')) {
      return;
    }

    deletingAnnouncement = true;
    error = null;

    try {
      // Fetch the repository announcement to get its event ID
      const nostrClient = new NostrClient([...new Set([...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS])]);
      const events = await nostrClient.fetchEvents([
        {
          kinds: [KIND.REPO_ANNOUNCEMENT],
          authors: [repoOwnerPubkeyDerived],
          '#d': [repo],
          limit: 1
        }
      ]);

      if (events.length === 0) {
        throw new Error('Repository announcement not found');
      }

      const announcement = events[0];
      announcementEventId = announcement.id;

      // Get user relays
      const { outbox } = await getUserRelays(userPubkeyHex, nostrClient);
      const combinedRelays = combineRelays(outbox);

      // Create deletion request (NIP-09)
      const deletionRequestTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
        kind: KIND.DELETION_REQUEST,
        pubkey: userPubkeyHex,
        created_at: Math.floor(Date.now() / 1000),
        content: `Requesting deletion of repository announcement for ${repo}`,
        tags: [
          ['e', announcement.id], // Reference to the announcement event
          ['a', `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkeyDerived}:${repo}`], // Repository address
          ['k', KIND.REPO_ANNOUNCEMENT.toString()] // Kind of event being deleted
        ]
      };

      // Sign with NIP-07
      const signedDeletionRequest = await signEventWithNIP07(deletionRequestTemplate);

      // Publish to relays
      const publishResult = await nostrClient.publishEvent(signedDeletionRequest, combinedRelays);

      if (publishResult.success.length > 0) {
        alert(`Deletion request published successfully to ${publishResult.success.length} relay(s).`);
      } else {
        throw new Error(`Failed to publish deletion request to any relay. Errors: ${publishResult.failed.map(f => `${f.relay}: ${f.error}`).join('; ')}`);
      }
    } catch (err) {
      console.error('Failed to delete announcement:', err);
      error = err instanceof Error ? err.message : 'Failed to send deletion request';
      alert(error);
    } finally {
      deletingAnnouncement = false;
    }
  }

  function downloadVerificationFile() {
    if (!verificationFileContent) return;
    
    const blob = new Blob([verificationFileContent], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'announcement-event.json';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  // buildApiHeaders is now imported from utils/api-client.ts - using it directly

  // Safe wrapper functions for SSR - use function declarations that check at call time
  // This ensures they're always defined and never null, even during SSR
  function safeCopyCloneUrl() {
    if (typeof window === 'undefined') return Promise.resolve();
    try {
      return copyCloneUrl();
    } catch (err) {
      console.warn('Error in copyCloneUrl:', err);
      return Promise.resolve();
    }
  }

  function safeDeleteBranch(branchName: string) {
    if (typeof window === 'undefined') return Promise.resolve();
    try {
      return deleteBranch(branchName);
    } catch (err) {
      console.warn('Error in deleteBranch:', err);
      return Promise.resolve();
    }
  }

  function safeToggleBookmark() {
    if (typeof window === 'undefined') return Promise.resolve();
    try {
      return toggleBookmark();
    } catch (err) {
      console.warn('Error in toggleBookmark:', err);
      return Promise.resolve();
    }
  }

  function safeForkRepository() {
    if (typeof window === 'undefined') return Promise.resolve();
    try {
      return forkRepository();
    } catch (err) {
      console.warn('Error in forkRepository:', err);
      return Promise.resolve();
    }
  }

  function safeCloneRepository() {
    if (typeof window === 'undefined') return Promise.resolve();
    try {
      return cloneRepository();
    } catch (err) {
      console.warn('Error in cloneRepository:', err);
      return Promise.resolve();
    }
  }

  function safeHandleBranchChange(branch: string) {
    if (typeof window === 'undefined') return;
    try {
      handleBranchChangeDirect(branch);
    } catch (err) {
      console.warn('Error in handleBranchChangeDirect:', err);
    }
  }

  // Download function - now using extracted utility
  async function downloadRepository(ref?: string, filename?: string): Promise<void> {
    await downloadRepoUtil({
      npub,
      repo,
      ref,
      filename
    });
  }

  async function loadBranches() {
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/branches`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        branches = await response.json();
        
        // If repo is not cloned but we got branches, API fallback is available
        if (isRepoCloned === false && branches.length > 0) {
          apiFallbackAvailable = true;
        }
        if (branches.length > 0) {
          // Branches can be an array of objects with .name property or array of strings
          const branchNames = branches.map((b: any) => typeof b === 'string' ? b : b.name);
          
          // Fetch the actual default branch from the API
          try {
            const defaultBranchResponse = await fetch(`/api/repos/${npub}/${repo}/default-branch`, {
              headers: buildApiHeaders()
            });
            if (defaultBranchResponse.ok) {
              const defaultBranchData = await defaultBranchResponse.json();
              defaultBranch = defaultBranchData.defaultBranch || defaultBranchData.branch || null;
            }
          } catch (err) {
            console.warn('Failed to fetch default branch, using fallback logic:', err);
          }
          
          // Fallback: Detect default branch: prefer master, then main, then first branch
          if (!defaultBranch) {
            if (branchNames.includes('master')) {
              defaultBranch = 'master';
            } else if (branchNames.includes('main')) {
              defaultBranch = 'main';
            } else {
              defaultBranch = branchNames[0];
            }
          }
          
          // Only update currentBranch if it's not set or if the current branch doesn't exist
          // Also validate that currentBranch doesn't contain invalid characters (like '#')
          if (!currentBranch || 
              typeof currentBranch !== 'string' || 
              currentBranch.includes('#') ||
              !branchNames.includes(currentBranch)) {
            currentBranch = defaultBranch;
          }
        } else {
          // No branches exist - set currentBranch to null to show "no branches" in header
          currentBranch = null;
        }
      } else if (response.status === 404) {
        // Check if this is a "not cloned" error - API fallback might be available
        const errorText = await response.text().catch(() => '');
        if (errorText.includes('not cloned locally')) {
          // Repository is not cloned - check if API fallback might be available
          if (repoCloneUrls && repoCloneUrls.length > 0) {
            // We have clone URLs, so API fallback might work - mark as unknown for now
            // It will be set to true if a subsequent request succeeds
            apiFallbackAvailable = null;
            // Don't set repoNotFound or error yet - allow API fallback to be attempted
          } else {
            // No clone URLs, API fallback won't work
            repoNotFound = true;
            apiFallbackAvailable = false;
            error = errorText || `Repository not found. This repository exists in Nostr but hasn't been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.`;
          }
        } else {
          // Generic 404 - repository doesn't exist
          repoNotFound = true;
          apiFallbackAvailable = false;
          error = `Repository not found. This repository exists in Nostr but hasn't been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.`;
        }
      } else if (response.status === 403) {
        // Access denied - don't set repoNotFound, allow retry after login
        const errorText = await response.text().catch(() => response.statusText);
        error = `Access denied: ${errorText}. You may need to log in or you may not have permission to view this repository.`;
        console.warn('[Branches] Access denied, user may need to log in');
      }
    } catch (err) {
      console.error('Failed to load branches:', err);
    }
  }

  async function loadFiles(path: string = '') {
    // Skip if repository doesn't exist
    if (repoNotFound) return;
    
    loading = true;
    error = null;
    try {
      // Validate and get a valid branch name
      let branchName: string;
      if (typeof currentBranch === 'string' && currentBranch.trim() !== '' && !currentBranch.includes('#')) {
        const branchNames = branches.map((b: any) => typeof b === 'string' ? b : b.name);
        if (branchNames.includes(currentBranch)) {
          branchName = currentBranch;
        } else {
          branchName = defaultBranch || (branches.length > 0 
            ? (typeof branches[0] === 'string' ? branches[0] : branches[0].name)
            : 'master');
        }
      } else {
        branchName = defaultBranch || (branches.length > 0 
          ? (typeof branches[0] === 'string' ? branches[0] : branches[0].name)
          : 'master');
      }
      
      const url = `/api/repos/${npub}/${repo}/tree?ref=${encodeURIComponent(branchName)}&path=${encodeURIComponent(path)}`;
      const response = await fetch(url, {
        headers: buildApiHeaders()
      });
      
      if (!response.ok) {
        if (response.status === 404) {
          // Check if this is a "not cloned" error - API fallback might be available
          const errorText = await response.text().catch(() => '');
          if (errorText.includes('not cloned locally')) {
            // Repository is not cloned - check if API fallback might be available
            if (repoCloneUrls && repoCloneUrls.length > 0) {
              // We have clone URLs, so API fallback might work - mark as unknown for now
              // It will be set to true if a subsequent request succeeds
              apiFallbackAvailable = null;
              // Don't set repoNotFound - allow API fallback to be attempted
            } else {
              // No clone URLs, API fallback won't work
              repoNotFound = true;
              apiFallbackAvailable = false;
            }
            // Throw error but use the actual error text from the API
            throw new Error(errorText || 'Repository not found. This repository exists in Nostr but hasn\'t been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.');
          } else {
            // Generic 404 - repository doesn't exist
            repoNotFound = true;
            apiFallbackAvailable = false;
            throw new Error(`Repository not found. This repository exists in Nostr but hasn't been provisioned on this server yet. The server will automatically provision it soon, or you can contact the server administrator.`);
          }
        } else if (response.status === 403) {
          // 403 means access denied - don't set repoNotFound, just show error
          // This allows retry after login
          const accessDeniedError = new Error(`Access denied: ${response.statusText}. You may need to log in or you may not have permission to view this repository.`);
          // Log as info since this is normal client behavior (not logged in or no access)
          console.info('Access denied (normal behavior):', accessDeniedError.message);
          throw accessDeniedError;
        }
        throw new Error(`Failed to load files: ${response.statusText}`);
      }

      files = await response.json();
      currentPath = path;
      
      // If repo is not cloned but we got files, API fallback is available
      if (isRepoCloned === false && files.length > 0) {
        apiFallbackAvailable = true;
      }
      
      // Auto-load README if we're in the root directory and no file is currently selected
      // Only attempt once per path to prevent loops
      if (path === '' && !currentFile && !readmeAutoLoadAttempted) {
        const readmeFile = findReadmeFile(files);
        if (readmeFile) {
          readmeAutoLoadAttempted = true;
          // Clear any existing timeout
          if (readmeAutoLoadTimeout) {
            clearTimeout(readmeAutoLoadTimeout);
          }
          // Small delay to ensure UI is ready
          readmeAutoLoadTimeout = setTimeout(() => {
            loadFile(readmeFile.path).catch(err => {
              // If load fails (e.g., 429 rate limit), reset the flag after a delay
              // so we can retry later, but not immediately
              if (err instanceof Error && err.message.includes('Too Many Requests')) {
                console.warn('[README] Rate limited, will retry later');
                setTimeout(() => {
                  readmeAutoLoadAttempted = false;
                }, 5000); // Retry after 5 seconds
              } else {
                // For other errors, reset immediately
                readmeAutoLoadAttempted = false;
              }
            });
            readmeAutoLoadTimeout = null;
          }, 100);
        }
      } else if (path !== '' || currentFile) {
        // Reset flag when navigating away from root or when a file is selected
        readmeAutoLoadAttempted = false;
        if (readmeAutoLoadTimeout) {
          clearTimeout(readmeAutoLoadTimeout);
          readmeAutoLoadTimeout = null;
        }
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to load files';
      // Only log as error if it's not a 403 (access denied), which is normal behavior
      if (err instanceof Error && err.message.includes('Access denied')) {
        // Already logged as info above, don't log again
      } else {
        console.error('Error loading files:', err);
      }
    } finally {
      loading = false;
    }
  }

  // Helper function to find README file in file list
  function findReadmeFile(fileList: Array<{ name: string; path: string; type: 'file' | 'directory' }>): { name: string; path: string; type: 'file' | 'directory' } | null {
    // Priority order for README files (most common first)
    const readmeExtensions = ['md', 'markdown', 'txt', 'adoc', 'asciidoc', 'rst', 'org'];
    
    // First, try to find README with extensions (prioritized order)
    for (const ext of readmeExtensions) {
      const readmeFile = fileList.find(file => 
        file.type === 'file' && 
        file.name.toLowerCase() === `readme.${ext}`
      );
      if (readmeFile) {
        return readmeFile;
      }
    }
    
    // Then check for README without extension
    const readmeNoExt = fileList.find(file => 
      file.type === 'file' && 
      file.name.toLowerCase() === 'readme'
    );
    if (readmeNoExt) {
      return readmeNoExt;
    }
    
    // Finally, check for any file starting with "readme." (case-insensitive)
    const readmeAny = fileList.find(file => 
      file.type === 'file' && 
      file.name.toLowerCase().startsWith('readme.')
    );
    if (readmeAny) {
      return readmeAny;
    }
    
    return null;
  }

  async function loadFile(filePath: string) {
    loading = true;
    error = null;
    try {
      // Ensure currentBranch is a string (branch name), not an object
      // If currentBranch is not set, use the first available branch or 'master' as fallback
      let branchName: string;
      
      if (typeof currentBranch === 'string' && currentBranch.trim() !== '') {
        // Validate that currentBranch is actually a valid branch name
        // Check if it exists in the branches list
        const branchNames = branches.map((b: any) => typeof b === 'string' ? b : b.name);
        if (branchNames.includes(currentBranch)) {
          branchName = currentBranch;
        } else {
          // currentBranch is set but not in branches list, use defaultBranch or fallback
          branchName = defaultBranch || (branches.length > 0 
            ? (typeof branches[0] === 'string' ? branches[0] : branches[0].name)
            : 'master');
        }
      } else if (typeof currentBranch === 'object' && currentBranch !== null && 'name' in currentBranch) {
        branchName = (currentBranch as { name: string }).name;
      } else {
        // currentBranch is null, undefined, or invalid - use defaultBranch or fallback
        branchName = defaultBranch || (branches.length > 0 
          ? (typeof branches[0] === 'string' ? branches[0] : branches[0].name)
          : 'master');
      }
      
      // Final validation: ensure branchName is a valid string
      // Note: We allow '#' in branch names for existing branches (they'll be URL-encoded)
      // Only reject if it's empty or not a string
      if (!branchName || typeof branchName !== 'string' || branchName.trim() === '') {
        console.warn('[loadFile] Invalid branch name detected, using fallback:', branchName);
        branchName = defaultBranch || (branches.length > 0 
          ? (typeof branches[0] === 'string' ? branches[0] : branches[0].name)
          : 'master');
      }
      
      // Determine language from file extension first to check if it's an image
      const ext = filePath.split('.').pop()?.toLowerCase() || '';
      
      // Check if this is an image file BEFORE making the API call
      isImageFile = isImageFileType(ext);
      
      if (isImageFile) {
        // For image files, construct the raw file URL and skip loading text content
        imageUrl = `/api/repos/${npub}/${repo}/raw?path=${encodeURIComponent(filePath)}&ref=${encodeURIComponent(branchName)}`;
        fileContent = ''; // Clear content for images
        editedContent = ''; // Clear edited content for images
        fileHtml = ''; // Clear HTML for images
        highlightedFileContent = ''; // Clear highlighted content
        fileLanguage = 'text';
        currentFile = filePath;
        hasChanges = false;
      } else {
        // Not an image, load file content normally
        imageUrl = null;
        
        const url = `/api/repos/${npub}/${repo}/file?path=${encodeURIComponent(filePath)}&ref=${encodeURIComponent(branchName)}`;
        const response = await fetch(url, {
          headers: buildApiHeaders()
        });
        
        if (!response.ok) {
          // Handle rate limiting specifically to prevent loops
          if (response.status === 429) {
            const error = new Error(`Failed to load file: Too Many Requests`);
            console.warn('[File Load] Rate limited, please wait before retrying');
            throw error;
          }
          throw new Error(`Failed to load file: ${response.statusText}`);
        }

        const data = await response.json();
        fileContent = data.content;
        editedContent = data.content;
        currentFile = filePath;
        hasChanges = false;
        
        // Reset README auto-load flag when a file is successfully loaded
        if (filePath && filePath.toLowerCase().includes('readme')) {
          readmeAutoLoadAttempted = false;
        }
        
        if (ext === 'md' || ext === 'markdown') {
          fileLanguage = 'markdown';
        } else if (ext === 'adoc' || ext === 'asciidoc') {
          fileLanguage = 'asciidoc';
        } else {
          fileLanguage = 'text';
        }
        
        // Reset preview mode to default (preview) when loading a new file
        showFilePreview = true;
        fileHtml = '';
        
        // Render markdown/asciidoc/HTML/CSV files as HTML for preview
        if (fileContent && (ext === 'md' || ext === 'markdown' || ext === 'adoc' || ext === 'asciidoc' || ext === 'html' || ext === 'htm' || ext === 'csv')) {
          await renderFileAsHtml(fileContent, ext || '');
        }
        
        // Apply syntax highlighting
        // For files that support HTML preview (markdown, HTML, etc.), only show highlighting in raw mode
        // For code files and other non-markup files, always show syntax highlighting
        const hasHtmlPreview = supportsPreview(ext);
        if (fileContent) {
          if (hasHtmlPreview) {
            // Markup files: only show highlighting when not in preview mode (raw mode)
            if (!showFilePreview) {
              await applySyntaxHighlighting(fileContent, ext || '');
            }
          } else {
            // Code files and other non-markup files: always show syntax highlighting
            await applySyntaxHighlighting(fileContent, ext || '');
          }
        }
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to load file';
      console.error('Error loading file:', err);
    } finally {
      loading = false;
    }
  }

  function handleContentChange(value: string) {
    editedContent = value;
    hasChanges = value !== fileContent;
  }

  function handleFileClick(file: { name: string; path: string; type: 'file' | 'directory' }) {
    if (file.type === 'directory') {
      pathStack.push(currentPath);
      loadFiles(file.path);
    } else {
      loadFile(file.path);
      // On mobile, switch to file viewer when a file is clicked
      if (window.innerWidth <= 768) {
        showFileListOnMobile = false;
      }
    }
  }

  // Copy file content to clipboard
  async function copyFileContent(event?: Event) {
    if (!fileContent || copyingFile) return;
    
    copyingFile = true;
    try {
      await navigator.clipboard.writeText(fileContent);
      // Show temporary feedback
      const button = event?.target as HTMLElement;
      if (button) {
        const originalTitle = button.getAttribute('title') || '';
        button.setAttribute('title', 'Copied!');
        setTimeout(() => {
          button.setAttribute('title', originalTitle);
        }, 2000);
      }
    } catch (err) {
      console.error('Failed to copy file content:', err);
      alert('Failed to copy file content to clipboard');
    } finally {
      copyingFile = false;
    }
  }

  // Download file
  function downloadFile() {
    if (!fileContent || !currentFile) return;
    
    try {
      // Determine MIME type based on file extension
      const ext = currentFile.split('.').pop()?.toLowerCase() || '';
      const mimeTypes: Record<string, string> = {
        'js': 'text/javascript',
        'ts': 'text/typescript',
        'json': 'application/json',
        'css': 'text/css',
        'html': 'text/html',
        'htm': 'text/html',
        'md': 'text/markdown',
        'txt': 'text/plain',
        'csv': 'text/csv',
        'xml': 'application/xml',
        'svg': 'image/svg+xml',
        'py': 'text/x-python',
        'java': 'text/x-java-source',
        'c': 'text/x-csrc',
        'cpp': 'text/x-c++src',
        'h': 'text/x-csrc',
        'hpp': 'text/x-c++src',
        'sh': 'text/x-shellscript',
        'bash': 'text/x-shellscript',
        'yaml': 'text/yaml',
        'yml': 'text/yaml',
        'toml': 'text/toml',
        'ini': 'text/plain',
        'conf': 'text/plain',
        'log': 'text/plain'
      };
      
      const mimeType = mimeTypes[ext] || 'text/plain';
      const blob = new Blob([fileContent], { type: mimeType });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = currentFile.split('/').pop() || 'file';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (err) {
      console.error('Failed to download file:', err);
      alert('Failed to download file');
    }
  }

  function handleBack() {
    if (pathStack.length > 0) {
      const parentPath = pathStack.pop() || '';
      loadFiles(parentPath);
    } else {
      loadFiles('');
    }
  }

  // Cache for user profile email and name
  let cachedUserEmail = $state<string | null>(null);
  let cachedUserName = $state<string | null>(null);
  let fetchingUserEmail = $state(false);
  let fetchingUserName = $state(false);

  async function getUserEmail(): Promise<string> {
    // Check settings store first
    try {
      const settings = await settingsStore.getSettings();
      if (settings.userEmail && settings.userEmail.trim()) {
        cachedUserEmail = settings.userEmail.trim();
        return cachedUserEmail;
      }
    } catch (err) {
      console.warn('Failed to get userEmail from settings:', err);
    }

    // Return cached email if available
    if (cachedUserEmail) {
      return cachedUserEmail;
    }

    // If no user pubkey, can't proceed
    if (!userPubkeyHex) {
      throw new Error('User not authenticated');
    }

    // Prevent concurrent fetches
    if (fetchingUserEmail) {
      // Wait a bit and retry (shouldn't happen, but just in case)
      await new Promise(resolve => setTimeout(resolve, 100));
      if (cachedUserEmail) {
        return cachedUserEmail;
      }
    }

    fetchingUserEmail = true;
    let prefillEmail: string;
    
    try {
      // Fetch from kind 0 event (cache or relays)
      prefillEmail = await fetchUserEmail(userPubkeyHex, userPubkey || undefined, DEFAULT_NOSTR_RELAYS);
    } catch (err) {
      console.warn('Failed to fetch user profile for email:', err);
      // Fallback to shortenednpub@gitrepublic.web
      const npubFromPubkey = userPubkeyHex ? nip19.npubEncode(userPubkeyHex) : (userPubkey || 'unknown');
      const shortenedNpub = npubFromPubkey.substring(0, 20);
      prefillEmail = `${shortenedNpub}@gitrepublic.web`;
    } finally {
      fetchingUserEmail = false;
    }
    
    // Prompt user for email address
    const userEmail = prompt(
      'Please enter your email address for git commits.\n\n' +
      'This will be used as the author email in your commits.\n' +
      'You can use any email address you prefer.',
      prefillEmail
    );

    if (userEmail && userEmail.trim()) {
      // Basic email validation
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (emailRegex.test(userEmail.trim())) {
        cachedUserEmail = userEmail.trim();
        // Save to settings store
        settingsStore.setSetting('userEmail', cachedUserEmail).catch(console.error);
        return cachedUserEmail;
      } else {
        alert('Invalid email format. Using fallback email address.');
      }
    }

    // Use fallback if user cancelled or entered invalid email
    cachedUserEmail = prefillEmail;
    return cachedUserEmail;
  }

  async function getUserName(): Promise<string> {
    // Check settings store first
    try {
      const settings = await settingsStore.getSettings();
      if (settings.userName && settings.userName.trim()) {
        cachedUserName = settings.userName.trim();
        return cachedUserName;
      }
    } catch (err) {
      console.warn('Failed to get userName from settings:', err);
    }

    // Return cached name if available
    if (cachedUserName) {
      return cachedUserName;
    }

    // If no user pubkey, can't proceed
    if (!userPubkeyHex) {
      throw new Error('User not authenticated');
    }

    // Prevent concurrent fetches
    if (fetchingUserName) {
      // Wait a bit and retry (shouldn't happen, but just in case)
      await new Promise(resolve => setTimeout(resolve, 100));
      if (cachedUserName) {
        return cachedUserName;
      }
    }

    fetchingUserName = true;
    let prefillName: string;
    
    try {
      // Fetch from kind 0 event (cache or relays)
      prefillName = await fetchUserName(userPubkeyHex, userPubkey || undefined, DEFAULT_NOSTR_RELAYS);
    } catch (err) {
      console.warn('Failed to fetch user profile for name:', err);
      // Fallback to shortened npub (20 chars)
      const npubFromPubkey = userPubkeyHex ? nip19.npubEncode(userPubkeyHex) : (userPubkey || 'unknown');
      prefillName = npubFromPubkey.substring(0, 20);
    } finally {
      fetchingUserName = false;
    }
    
    // Prompt user for name
    const userName = prompt(
      'Please enter your name for git commits.\n\n' +
      'This will be used as the author name in your commits.\n' +
      'You can use any name you prefer.',
      prefillName
    );

    if (userName && userName.trim()) {
      cachedUserName = userName.trim();
      // Save to settings store
      settingsStore.setSetting('userName', cachedUserName).catch(console.error);
      return cachedUserName;
    }

    // Use fallback if user cancelled
    cachedUserName = prefillName;
    return cachedUserName;
  }

  async function setupAutoSave() {
    // Clear existing interval if any
    if (autoSaveInterval) {
      clearInterval(autoSaveInterval);
      autoSaveInterval = null;
    }
    
    // Check if auto-save is enabled
    try {
      const settings = await settingsStore.getSettings();
      if (!settings.autoSave) {
        return; // Auto-save disabled
      }
    } catch (err) {
      console.warn('Failed to check auto-save setting:', err);
      return;
    }
    
    // Set up interval to auto-save every 10 minutes
    autoSaveInterval = setInterval(async () => {
      await autoSaveFile();
    }, 10 * 60 * 1000); // 10 minutes
  }
  
  async function autoSaveFile() {
    // Only auto-save if:
    // 1. There are changes
    // 2. A file is open
    // 3. User is logged in
    // 4. User is a maintainer
    // 5. Not currently saving
    // 6. Not in clone state
    if (!hasChanges || !currentFile || !userPubkey || !isMaintainer || saving || needsClone) {
      return;
    }
    
    // Check auto-save setting again (in case it changed)
    try {
      const settings = await settingsStore.getSettings();
      if (!settings.autoSave) {
        // Auto-save was disabled, clear interval
        if (autoSaveInterval) {
          clearInterval(autoSaveInterval);
          autoSaveInterval = null;
        }
        return;
      }
    } catch (err) {
      console.warn('Failed to check auto-save setting:', err);
      return;
    }
    
    // Generate a default commit message
    const autoCommitMessage = `Auto-save: ${new Date().toLocaleString()}`;
    
    try {
      // Get user email and name from settings
      const authorEmail = await getUserEmail();
      const authorName = await getUserName();
      
      // Sign commit with NIP-07 (client-side)
      let commitSignatureEvent: NostrEvent | null = null;
      if (isNIP07Available()) {
        try {
          const { KIND } = await import('$lib/types/nostr.js');
          const timestamp = Math.floor(Date.now() / 1000);
          const eventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
            kind: KIND.COMMIT_SIGNATURE,
            pubkey: '', // Will be filled by NIP-07
            created_at: timestamp,
            tags: [
              ['author', authorName, authorEmail],
              ['message', autoCommitMessage]
            ],
            content: `Signed commit: ${autoCommitMessage}`
          };
          commitSignatureEvent = await signEventWithNIP07(eventTemplate);
        } catch (err) {
          console.warn('Failed to sign commit with NIP-07:', err);
          // Continue without signature if signing fails
        }
      }
      
      const response = await fetch(`/api/repos/${npub}/${repo}/file`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({
          path: currentFile,
          content: editedContent,
          commitMessage: autoCommitMessage,
          authorName: authorName,
          authorEmail: authorEmail,
          branch: currentBranch,
          userPubkey: userPubkey,
          commitSignatureEvent: commitSignatureEvent
        })
      });

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({ message: response.statusText }));
        console.warn('Auto-save failed:', errorData.message || 'Failed to save file');
        return;
      }

      // Reload file to get updated content
      await loadFile(currentFile);
      // Note: We don't show an alert for auto-save, it's silent
      console.log('Auto-saved file:', currentFile);
    } catch (err) {
      console.warn('Error during auto-save:', err);
      // Don't show error to user, it's silent
    }
  }

  async function saveFile() {
    if (!currentFile || !commitMessage.trim()) {
      alert('Please enter a commit message');
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension to save files');
      return;
    }

    // Validate branch selection
    if (!currentBranch || typeof currentBranch !== 'string') {
      alert('Please select a branch before saving the file');
      return;
    }

    saving = true;
    error = null;

    try {
      // Get user email and name (from profile or prompt)
      const authorEmail = await getUserEmail();
      const authorName = await getUserName();
      
      // Sign commit with NIP-07 (client-side)
      let commitSignatureEvent: NostrEvent | null = null;
      if (isNIP07Available()) {
        try {
          const { KIND } = await import('$lib/types/nostr.js');
          const timestamp = Math.floor(Date.now() / 1000);
          const eventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
            kind: KIND.COMMIT_SIGNATURE,
            pubkey: '', // Will be filled by NIP-07
            created_at: timestamp,
            tags: [
              ['author', authorName, authorEmail],
              ['message', commitMessage.trim()]
            ],
            content: `Signed commit: ${commitMessage.trim()}`
          };
          commitSignatureEvent = await signEventWithNIP07(eventTemplate);
        } catch (err) {
          console.warn('Failed to sign commit with NIP-07:', err);
          // Continue without signature if signing fails
        }
      }
      
      const response = await fetch(`/api/repos/${npub}/${repo}/file`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({
          path: currentFile,
          content: editedContent,
          commitMessage: commitMessage.trim(),
          authorName: authorName,
          authorEmail: authorEmail,
          branch: currentBranch,
          userPubkey: userPubkey,
          commitSignatureEvent: commitSignatureEvent // Send the signed event to server
        })
      });

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({ message: response.statusText }));
        const errorMessage = errorData.message || errorData.error || 'Failed to save file';
        throw new Error(errorMessage);
      }

      // Reload file to get updated content
      await loadFile(currentFile);
      commitMessage = '';
      showCommitDialog = false;
      alert('File saved successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to save file';
      console.error('Error saving file:', err);
    } finally {
      saving = false;
    }
  }

  function handleBranchChangeDirect(branch: string) {
    currentBranch = branch;
    // Create a synthetic event for the existing handler
    const syntheticEvent = {
      target: { value: branch }
    } as unknown as Event;
    handleBranchChange(syntheticEvent);
  }

  async function handleBranchChange(event: Event) {
    const target = event.target as HTMLSelectElement;
    currentBranch = target.value;
    
    // Reload all branch-dependent data
    const reloadPromises: Promise<void>[] = [];
    
    // Always reload files (and current file if open)
    if (currentFile) {
      reloadPromises.push(loadFile(currentFile).catch(err => console.warn('Failed to reload file after branch change:', err)));
    } else {
      reloadPromises.push(loadFiles(currentPath).catch(err => console.warn('Failed to reload files after branch change:', err)));
    }
    
    // Reload README (branch-specific)
    reloadPromises.push(loadReadme().catch(err => console.warn('Failed to reload README after branch change:', err)));
    
    // Reload commit history if history tab is active
    if (activeTab === 'history') {
      reloadPromises.push(loadCommitHistory().catch(err => console.warn('Failed to reload commit history after branch change:', err)));
    }
    
    // Reload documentation if docs tab is active (might be branch-specific)
    if (activeTab === 'docs') {
      // Reset documentation to force reload
      documentationHtml = null;
      documentationContent = null;
      documentationKind = null;
      reloadPromises.push(loadDocumentation().catch(err => console.warn('Failed to reload documentation after branch change:', err)));
    }
    
    // Wait for all reloads to complete
    await Promise.all(reloadPromises);
  }

  async function createFile() {
    if (!newFileName.trim()) {
      alert('Please enter a file name');
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    // Validate branch selection
    if (!currentBranch || typeof currentBranch !== 'string') {
      alert('Please select a branch before creating the file');
      return;
    }

    saving = true;
    error = null;

    try {
      // Get user email and name (from profile or prompt)
      const authorEmail = await getUserEmail();
      const authorName = await getUserName();
      const filePath = currentPath ? `${currentPath}/${newFileName}` : newFileName;
      const commitMsg = `Create ${newFileName}`;
      
      // Sign commit with NIP-07 (client-side)
      let commitSignatureEvent: NostrEvent | null = null;
      if (isNIP07Available()) {
        try {
          const { KIND } = await import('$lib/types/nostr.js');
          const timestamp = Math.floor(Date.now() / 1000);
          const eventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
            kind: KIND.COMMIT_SIGNATURE,
            pubkey: '', // Will be filled by NIP-07
            created_at: timestamp,
            tags: [
              ['author', authorName, authorEmail],
              ['message', commitMsg]
            ],
            content: `Signed commit: ${commitMsg}`
          };
          commitSignatureEvent = await signEventWithNIP07(eventTemplate);
        } catch (err) {
          console.warn('Failed to sign commit with NIP-07:', err);
          // Continue without signature if signing fails
        }
      }
      
      const response = await fetch(`/api/repos/${npub}/${repo}/file`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({
          path: filePath,
          content: newFileContent,
          commitMessage: commitMsg,
          authorName: authorName,
          authorEmail: authorEmail,
          branch: currentBranch,
          action: 'create',
          userPubkey: userPubkey,
          commitSignatureEvent: commitSignatureEvent // Send the signed event to server
        })
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Failed to create file');
      }

      showCreateFileDialog = false;
      newFileName = '';
      newFileContent = '';
      await loadFiles(currentPath);
      alert('File created successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create file';
    } finally {
      saving = false;
    }
  }

  async function deleteFile(filePath: string) {
    if (!confirm(`Are you sure you want to delete "${filePath}"?\n\nThis will permanently delete the file from the repository. This action cannot be undone.\n\nClick OK to delete, or Cancel to abort.`)) {
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    // Validate branch selection
    if (!currentBranch || typeof currentBranch !== 'string') {
      alert('Please select a branch before deleting the file');
      return;
    }

    saving = true;
    error = null;

    try {
      // Get user email and name (from profile or prompt)
      const authorEmail = await getUserEmail();
      const authorName = await getUserName();
      const commitMsg = `Delete ${filePath}`;
      
      // Sign commit with NIP-07 (client-side)
      let commitSignatureEvent: NostrEvent | null = null;
      if (isNIP07Available()) {
        try {
          const { KIND } = await import('$lib/types/nostr.js');
          const timestamp = Math.floor(Date.now() / 1000);
          const eventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
            kind: KIND.COMMIT_SIGNATURE,
            pubkey: '', // Will be filled by NIP-07
            created_at: timestamp,
            tags: [
              ['author', authorName, authorEmail],
              ['message', commitMsg]
            ],
            content: `Signed commit: ${commitMsg}`
          };
          commitSignatureEvent = await signEventWithNIP07(eventTemplate);
        } catch (err) {
          console.warn('Failed to sign commit with NIP-07:', err);
          // Continue without signature if signing fails
        }
      }
      
      const response = await fetch(`/api/repos/${npub}/${repo}/file`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({
          path: filePath,
          commitMessage: commitMsg,
          authorName: authorName,
          authorEmail: authorEmail,
          branch: currentBranch,
          action: 'delete',
          userPubkey: userPubkey,
          commitSignatureEvent: commitSignatureEvent // Send the signed event to server
        })
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Failed to delete file');
      }

      if (currentFile === filePath) {
        currentFile = null;
      }
      await loadFiles(currentPath);
      alert('File deleted successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to delete file';
    } finally {
      saving = false;
    }
  }

  async function createBranch() {
    if (!newBranchName.trim()) {
      alert('Please enter a branch name');
      return;
    }

    saving = true;
    error = null;

    try {
      // If no branches exist, don't pass fromBranch (will use --orphan)
      // Otherwise, use the selected branch or current branch
      let fromBranch: string | undefined = newBranchFrom || currentBranch || undefined;
      
      // Include announcement if available (for empty repos)
      const requestBody: { branchName: string; fromBranch?: string; announcement?: NostrEvent } = {
        branchName: newBranchName
      };
      if (branches.length > 0 && fromBranch) {
        requestBody.fromBranch = fromBranch;
      }
      // Pass announcement if available (especially useful for empty repos)
      if (repoAnnouncement) {
        requestBody.announcement = repoAnnouncement;
      }

      const response = await fetch(`/api/repos/${npub}/${repo}/branches`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify(requestBody)
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Failed to create branch');
      }

      showCreateBranchDialog = false;
      newBranchName = '';
      await loadBranches();
      alert('Branch created successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create branch';
    } finally {
      saving = false;
    }
  }

  async function deleteBranch(branchName: string) {
    if (!confirm(`Are you sure you want to delete the branch "${branchName}"?\n\nThis will permanently delete the branch from the repository. This action CANNOT be undone.\n\nClick OK to delete, or Cancel to abort.`)) {
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    // Prevent deleting the current branch
    if (branchName === currentBranch) {
      alert('Cannot delete the currently selected branch. Please switch to a different branch first.');
      return;
    }

    saving = true;
    error = null;

    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/branches`, {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({
          branchName: branchName
        })
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Failed to delete branch');
      }

      await loadBranches();
      alert('Branch deleted successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to delete branch';
      alert(error);
    } finally {
      saving = false;
    }
  }

  async function loadCommitHistory() {
    loadingCommits = true;
    error = null;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/commits?branch=${currentBranch}&limit=50`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        const data = await response.json();
        // Normalize commits: API-based commits use 'sha', local commits use 'hash'
        commits = data.map((commit: any) => ({
          hash: commit.hash || commit.sha || '',
          message: commit.message || 'No message',
          author: commit.author || 'Unknown',
          date: commit.date || new Date().toISOString(),
          files: commit.files || []
        })).filter((commit: any) => commit.hash); // Filter out commits without hash
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to load commit history';
    } finally {
      loadingCommits = false;
    }
  }

  async function viewDiff(commitHash: string) {
    // Set selected commit immediately so it shows in the right panel
    selectedCommit = commitHash;
    showDiff = false; // Start with false, will be set to true when diff loads
    loadingCommits = true;
    error = null;
    try {
      // Normalize commit hash (handle both 'hash' and 'sha' properties)
      const getCommitHash = (c: any) => c.hash || c.sha || '';
      const commitIndex = commits.findIndex(c => getCommitHash(c) === commitHash);
      const parentHash = commitIndex >= 0
        ? (commits[commitIndex + 1] ? getCommitHash(commits[commitIndex + 1]) : `${commitHash}^`)
        : `${commitHash}^`;
      
      const response = await fetch(`/api/repos/${npub}/${repo}/diff?from=${parentHash}&to=${commitHash}`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        diffData = await response.json();
        showDiff = true;
      } else {
        // Handle 404 or other errors
        const errorText = await response.text().catch(() => response.statusText);
        if (response.status === 404) {
          // Check if this is an API fallback commit (repo not cloned or empty)
          if (isRepoCloned === false || (isRepoCloned === true && apiFallbackAvailable)) {
            error = 'Diffs are not available for commits viewed via API fallback. Please clone the repository to view diffs.';
          } else {
            error = `Commit not found: ${errorText || 'The commit may not exist in the repository'}`;
          }
        } else {
          error = `Failed to load diff: ${errorText || response.statusText}`;
        }
      }
    } catch (err) {
      // Handle network errors
      if (err instanceof TypeError && err.message.includes('NetworkError')) {
        error = 'Network error: Unable to fetch diff. Please check your connection and try again.';
      } else {
        error = err instanceof Error ? err.message : 'Failed to load diff';
      }
    } finally {
      loadingCommits = false;
    }
  }

  async function loadTags() {
    if (repoNotFound) return;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/tags`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        tags = await response.json();
        // Auto-select first tag if none selected
        if (tags.length > 0 && !selectedTag) {
          selectedTag = tags[0].name;
        }
      }
    } catch (err) {
      console.error('Failed to load tags:', err);
    }
  }

  async function createTag() {
    if (!newTagName.trim()) {
      alert('Please enter a tag name');
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    saving = true;
    error = null;

    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/tags`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({
          tagName: newTagName,
          ref: newTagRef,
          message: newTagMessage || undefined,
          userPubkey: userPubkey
        })
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Failed to create tag');
      }

      showCreateTagDialog = false;
      newTagName = '';
      newTagMessage = '';
      await loadTags();
      alert('Tag created successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create tag';
    } finally {
      saving = false;
    }
  }

  async function loadReleases() {
    if (repoNotFound) return;
    loadingReleases = true;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/releases`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        const data = await response.json();
        releases = data.map((release: any) => ({
          id: release.id,
          tagName: release.tags.find((t: string[]) => t[0] === 'tag')?.[1] || '',
          tagHash: release.tags.find((t: string[]) => t[0] === 'r' && t[2] === 'tag')?.[1],
          releaseNotes: release.content || '',
          isDraft: release.tags.some((t: string[]) => t[0] === 'draft' && t[1] === 'true'),
          isPrerelease: release.tags.some((t: string[]) => t[0] === 'prerelease' && t[1] === 'true'),
          created_at: release.created_at,
          pubkey: release.pubkey
        }));
      }
    } catch (err) {
      console.error('Failed to load releases:', err);
    } finally {
      loadingReleases = false;
    }
  }

  async function createRelease() {
    if (!newReleaseTagName.trim() || !newReleaseTagHash.trim()) {
      alert('Please enter a tag name and tag hash');
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    if (!isMaintainer && userPubkeyHex !== repoOwnerPubkeyDerived) {
      alert('Only repository owners and maintainers can create releases');
      return;
    }

    creatingRelease = true;
    error = null;

    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/releases`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...buildApiHeaders()
        },
        body: JSON.stringify({
          tagName: newReleaseTagName,
          tagHash: newReleaseTagHash,
          releaseNotes: newReleaseNotes,
          isDraft: newReleaseIsDraft,
          isPrerelease: newReleaseIsPrerelease
        })
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Failed to create release');
      }

      showCreateReleaseDialog = false;
      newReleaseTagName = '';
      newReleaseTagHash = '';
      newReleaseNotes = '';
      newReleaseIsDraft = false;
      newReleaseIsPrerelease = false;
      await loadReleases();
      // Reload tags to show release indicator
      await loadTags();
      alert('Release created successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create release';
      alert(error);
    } finally {
      creatingRelease = false;
    }
  }

  async function performCodeSearch() {
    if (!codeSearchQuery.trim() || codeSearchQuery.length < 2) {
      codeSearchResults = [];
      return;
    }

    loadingCodeSearch = true;
    error = null;

    try {
      const url = codeSearchScope === 'repo' 
        ? `/api/repos/${npub}/${repo}/code-search?q=${encodeURIComponent(codeSearchQuery.trim())}`
        : `/api/code-search?q=${encodeURIComponent(codeSearchQuery.trim())}&repo=${encodeURIComponent(`${npub}/${repo}`)}`;
      
      const response = await fetch(url, {
        headers: buildApiHeaders()
      });

      if (response.ok) {
        codeSearchResults = await response.json();
      } else {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Failed to search code');
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to search code';
      codeSearchResults = [];
    } finally {
      loadingCodeSearch = false;
    }
  }

  async function loadIssues() {
    loadingIssues = true;
    error = null;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/issues`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        const data = await response.json();
        issues = data.map((issue: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; kind?: number }) => ({
          id: issue.id,
          subject: issue.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled',
          content: issue.content,
          status: issue.status || 'open',
          author: issue.pubkey,
          created_at: issue.created_at,
          kind: issue.kind || KIND.ISSUE,
          tags: issue.tags || []
        }));
        // Auto-select first issue if none selected
        if (issues.length > 0 && !selectedIssue) {
          selectedIssue = issues[0].id;
          loadIssueReplies(issues[0].id);
        }
      } else {
        // Handle non-OK responses
        const errorText = await response.text().catch(() => response.statusText);
        let errorMessage = `Failed to load issues: ${response.status} ${response.statusText}`;
        try {
          const errorData = JSON.parse(errorText);
          if (errorData.message) {
            errorMessage = errorData.message;
          }
        } catch {
          // If parsing fails, use the text as-is
          if (errorText) {
            errorMessage = errorText;
          }
        }
        console.error('[Issues] Failed to load:', errorMessage);
        error = errorMessage;
        // Don't clear issues array - keep existing issues if any
        // issues = []; // Only clear if you want to show empty state on error
      }
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Failed to load issues';
      console.error('[Issues] Error loading issues:', err);
      error = errorMessage;
    } finally {
      loadingIssues = false;
    }
  }

  async function loadIssueReplies(issueId: string) {
    loadingIssueReplies = true;
    try {
      const replies = await nostrClient.fetchEvents([
        {
          kinds: [KIND.COMMENT],
          '#e': [issueId],
          limit: 100
        }
      ]) as NostrEvent[];
      
      issueReplies = replies.map(reply => ({
        id: reply.id,
        content: reply.content,
        author: reply.pubkey,
        created_at: reply.created_at,
        tags: reply.tags || []
      })).sort((a, b) => a.created_at - b.created_at);
    } catch (err) {
      console.error('[Issues] Error loading replies:', err);
      issueReplies = [];
    } finally {
      loadingIssueReplies = false;
    }
  }

  async function createIssue() {
    if (!newIssueSubject.trim() || !newIssueContent.trim()) {
      alert('Please enter a subject and content');
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    saving = true;
    error = null;

    try {
      const { IssuesService } = await import('$lib/services/nostr/issues-service.js');
      
      const decoded = nip19.decode(npub);
      if (decoded.type !== 'npub') {
        throw new Error('Invalid npub format');
      }
      const repoOwnerPubkey = decoded.data as string;

      // Get user's relays and combine with defaults
      const tempClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
      const { outbox } = await getUserRelays(userPubkey, tempClient);
      const combinedRelays = combineRelays(outbox);

      const issuesService = new IssuesService(combinedRelays);
      const issue = await issuesService.createIssue(
        repoOwnerPubkey,
        repo,
        newIssueSubject.trim(),
        newIssueContent.trim(),
        newIssueLabels.filter(l => l.trim())
      );

      showCreateIssueDialog = false;
      newIssueSubject = '';
      newIssueContent = '';
      newIssueLabels = [''];
      await loadIssues();
      alert('Issue created successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create issue';
      console.error('Error creating issue:', err);
    } finally {
      saving = false;
    }
  }

  async function updateIssueStatus(issueId: string, issueAuthor: string, status: 'open' | 'closed' | 'resolved' | 'draft') {
    if (!userPubkeyHex) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    // Check if user is maintainer or issue author
    const isAuthor = userPubkeyHex === issueAuthor;
    if (!isMaintainer && !isAuthor) {
      alert('Only repository maintainers or issue authors can update issue status');
      return;
    }

    updatingIssueStatus = { ...updatingIssueStatus, [issueId]: true };
    error = null;

    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/issues`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          issueId,
          issueAuthor,
          status
        })
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || 'Failed to update issue status');
      }

      await loadIssues();
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to update issue status';
      console.error('Error updating issue status:', err);
    } finally {
      updatingIssueStatus = { ...updatingIssueStatus, [issueId]: false };
    }
  }

  async function loadPRs() {
    loadingPRs = true;
    error = null;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/prs`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        const data = await response.json();
        prs = data.map((pr: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; commitId?: string; kind?: number }) => ({
          id: pr.id,
          subject: pr.tags.find((t: string[]) => t[0] === 'subject')?.[1] || 'Untitled',
          content: pr.content,
          status: pr.status || 'open',
          author: pr.pubkey,
          created_at: pr.created_at,
          commitId: pr.tags.find((t: string[]) => t[0] === 'c')?.[1],
          kind: pr.kind || KIND.PULL_REQUEST
        }));
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to load pull requests';
    } finally {
      loadingPRs = false;
    }
  }

  async function createPR() {
    if (!newPRSubject.trim() || !newPRContent.trim() || !newPRCommitId.trim()) {
      alert('Please enter a subject, content, and commit ID');
      return;
    }

    if (!userPubkey) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    saving = true;
    error = null;

    try {
      const { PRsService } = await import('$lib/services/nostr/prs-service.js');
      const { getGitUrl } = await import('$lib/config.js');
      
      const decoded = nip19.decode(npub);
      if (decoded.type !== 'npub') {
        throw new Error('Invalid npub format');
      }
      const repoOwnerPubkey = decoded.data as string;

      // Get user's relays and combine with defaults
      const tempClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
      const { outbox } = await getUserRelays(userPubkey, tempClient);
      const combinedRelays = combineRelays(outbox);

      const cloneUrl = getGitUrl(npub, repo);
      const prsService = new PRsService(combinedRelays);
      const pr = await prsService.createPullRequest(
        repoOwnerPubkey,
        repo,
        newPRSubject.trim(),
        newPRContent.trim(),
        newPRCommitId.trim(),
        cloneUrl,
        newPRBranchName.trim() || undefined,
        newPRLabels.filter(l => l.trim())
      );

      showCreatePRDialog = false;
      newPRSubject = '';
      newPRContent = '';
      newPRCommitId = '';
      newPRBranchName = '';
      newPRLabels = [''];
      await loadPRs();
      alert('Pull request created successfully!');
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create pull request';
      console.error('Error creating PR:', err);
    } finally {
      saving = false;
    }
  }

  async function createPatch() {
    if (!newPatchContent.trim()) {
      alert('Please enter patch content');
      return;
    }

    if (!userPubkey || !userPubkeyHex) {
      alert('Please connect your NIP-07 extension');
      return;
    }

    creatingPatch = true;
    error = null;

    try {
      const decoded = nip19.decode(npub);
      if (decoded.type !== 'npub') {
        throw new Error('Invalid npub format');
      }
      const repoOwnerPubkey = decoded.data as string;
      const repoAddress = `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkey}:${repo}`;

      // Get user's relays and combine with defaults
      const tempClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
      const { outbox } = await getUserRelays(userPubkey, tempClient);
      const combinedRelays = combineRelays(outbox);

      // Create patch event (kind 1617)
      const patchEventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
        kind: KIND.PATCH,
        pubkey: userPubkeyHex,
        created_at: Math.floor(Date.now() / 1000),
        tags: [
          ['a', repoAddress],
          ['p', repoOwnerPubkey],
          ['t', 'root']
        ],
        content: newPatchContent.trim()
      };

      // Add subject if provided
      if (newPatchSubject.trim()) {
        patchEventTemplate.tags.push(['subject', newPatchSubject.trim()]);
      }

      // Sign the event using NIP-07
      const signedEvent = await signEventWithNIP07(patchEventTemplate);

      // Publish to all available relays
      const publishClient = new NostrClient(combinedRelays);
      const result = await publishClient.publishEvent(signedEvent, combinedRelays);

      if (result.failed.length > 0 && result.success.length === 0) {
        throw new Error('Failed to publish patch to all relays');
      }

      showCreatePatchDialog = false;
      newPatchContent = '';
      newPatchSubject = '';
      alert('Patch created successfully!');
      // Reload patches
      await loadPatches();
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to create patch';
      console.error('Error creating patch:', err);
    } finally {
      creatingPatch = false;
    }
  }

  async function loadPatches() {
    if (repoNotFound) return;
    loadingPatches = true;
    error = null;
    try {
      const response = await fetch(`/api/repos/${npub}/${repo}/patches`, {
        headers: buildApiHeaders()
      });
      if (response.ok) {
        const data = await response.json();
        patches = data.map((patch: { id: string; tags: string[][]; content: string; pubkey: string; created_at: number; kind?: number }) => {
          // Extract subject/title from various sources
          let subject = patch.tags.find((t: string[]) => t[0] === 'subject')?.[1];
          const description = patch.tags.find((t: string[]) => t[0] === 'description')?.[1];
          const alt = patch.tags.find((t: string[]) => t[0] === 'alt')?.[1];
          
          // If no subject tag, try description or alt
          if (!subject) {
            if (description) {
              subject = description.trim();
            } else if (alt) {
              // Remove "git patch: " prefix if present
              subject = alt.replace(/^git patch:\s*/i, '').trim();
            } else {
              // Try to extract from patch content (git patch format)
              const subjectMatch = patch.content.match(/^Subject:\s*\[PATCH[^\]]*\]\s*(.+)$/m);
              if (subjectMatch) {
                subject = subjectMatch[1].trim();
              } else {
                // Try simpler Subject: line
                const simpleSubjectMatch = patch.content.match(/^Subject:\s*(.+)$/m);
                if (simpleSubjectMatch) {
                  subject = simpleSubjectMatch[1].trim();
                }
              }
            }
          }
          
          return {
            id: patch.id,
            subject: subject || 'Untitled',
            content: patch.content,
            author: patch.pubkey,
            created_at: patch.created_at,
            kind: patch.kind || KIND.PATCH,
            description: description?.trim(),
            tags: patch.tags || []
          };
        });
      }
    } catch (err) {
      error = err instanceof Error ? err.message : 'Failed to load patches';
      console.error('Error loading patches:', err);
    } finally {
      loadingPatches = false;
    }
  }

  // Only load tab content when tab actually changes, not on every render
  let lastTab = $state<string | null>(null);
  $effect(() => {
    if (!isMounted) return;
    if (activeTab !== lastTab) {
      lastTab = activeTab;
      if (!isMounted) return;
      
      if (activeTab === 'files') {
        // Files tab - ensure files are loaded and README is shown if available
        if (files.length === 0 || currentPath !== '') {
          loadFiles('').catch(err => {
            if (isMounted) console.warn('Failed to load files:', err);
          });
        } else if (files.length > 0 && !currentFile && isMounted) {
          // Files already loaded, ensure README is shown
          const readmeFile = findReadmeFile(files);
          if (readmeFile) {
            setTimeout(() => {
              if (isMounted) {
                loadFile(readmeFile.path).catch(err => {
                  if (isMounted) console.warn('Failed to load README file:', err);
                });
              }
            }, 100);
          }
        }
      } else if (activeTab === 'history' && isMounted) {
        loadCommitHistory().catch(err => {
          if (isMounted) console.warn('Failed to load commit history:', err);
        });
      } else if (activeTab === 'tags' && isMounted) {
        loadTags().catch(err => {
          if (isMounted) console.warn('Failed to load tags:', err);
        });
        loadReleases().catch(err => {
          if (isMounted) console.warn('Failed to load releases:', err);
        }); // Load releases to check for tag associations
      } else if (activeTab === 'code-search') {
        // Code search is performed on demand, not auto-loaded
      } else if (activeTab === 'issues' && isMounted) {
        loadIssues().catch(err => {
          if (isMounted) console.warn('Failed to load issues:', err);
        });
      } else if (activeTab === 'prs' && isMounted) {
        loadPRs().catch(err => {
          if (isMounted) console.warn('Failed to load PRs:', err);
        });
      } else if (activeTab === 'docs' && isMounted) {
        loadDocumentation().catch(err => {
          if (isMounted) console.warn('Failed to load documentation:', err);
        });
      } else if (activeTab === 'discussions' && isMounted) {
        loadDiscussions().catch(err => {
          if (isMounted) console.warn('Failed to load discussions:', err);
        });
      } else if (activeTab === 'patches' && isMounted) {
        loadPatches().catch(err => {
          if (isMounted) console.warn('Failed to load patches:', err);
        });
      }
    }
  });

  // Reload all branch-dependent data when branch changes
  let lastBranch = $state<string | null>(null);
  $effect(() => {
    if (!isMounted) return;
    if (currentBranch && currentBranch !== lastBranch) {
      lastBranch = currentBranch;
      if (!isMounted) return;
      
      // Reload README (always branch-specific)
      loadReadme().catch(err => {
        if (isMounted) console.warn('Failed to reload README after branch change:', err);
      });
      
      // Reload files if files tab is active
      if (activeTab === 'files' && isMounted) {
        if (currentFile) {
          loadFile(currentFile).catch(err => {
            if (isMounted) console.warn('Failed to reload file after branch change:', err);
          });
        } else {
          loadFiles(currentPath).catch(err => {
            if (isMounted) console.warn('Failed to reload files after branch change:', err);
          });
        }
      }
      
      // Reload commit history if history tab is active
      if (activeTab === 'history' && isMounted) {
        loadCommitHistory().catch(err => {
          if (isMounted) console.warn('Failed to reload commit history after branch change:', err);
        });
      }
      
      // Reload documentation if docs tab is active (reset to force reload)
      if (activeTab === 'docs' && isMounted) {
        documentationHtml = null;
        documentationContent = null;
        documentationKind = null;
        loadDocumentation().catch(err => {
          if (isMounted) console.warn('Failed to reload documentation after branch change:', err);
        });
      }
    }
  });
</script>

<svelte:head>
  <title>{safeTitle || 'Repository'}</title>
  <meta name="description" content={safeDescription || 'Repository'} />
  
  <!-- OpenGraph / Facebook -->
  <meta property="og:type" content="website" />
  <meta property="og:title" content={safeTitle || 'Repository'} />
  <meta property="og:description" content={safeOgDescription} />
  <meta property="og:url" content={safePageUrl} />
  {#if hasImage && safeImage}
    <meta property="og:image" content={safeImage} />
  {/if}
  {#if hasBanner && safeBanner}
    <meta property="og:image:width" content="1200" />
    <meta property="og:image:height" content="630" />
  {/if}
  
  <!-- Twitter Card -->
  <meta name="twitter:card" content={safeTwitterCard} />
  <meta name="twitter:title" content={safeTitle || 'Repository'} />
  <meta name="twitter:description" content={safeTwitterDescription} />
  {#if hasBanner && safeBanner}
    <meta name="twitter:image" content={safeBanner} />
  {:else if hasImage && safeImage}
    <meta name="twitter:image" content={safeImage} />
  {/if}
</svelte:head>

<div class="container">
  <!-- Banner hidden on mobile, shown on desktop -->
  {#if repoBanner && typeof repoBanner === 'string' && repoBanner.trim()}
    <div class="repo-banner desktop-only">
      <img src={repoBanner} alt="" onerror={(e) => { 
        if (typeof window !== 'undefined') {
          console.error('[Repo Images] Failed to load banner:', repoBanner); 
          const target = e.target as HTMLImageElement;
          if (target) target.style.display = 'none';
        }
      }} />
    </div>
  {/if}
  
  {#if repoOwnerPubkeyDerived}
    <RepoHeaderEnhanced
      repoName={repoName || ''}
      repoDescription={repoDescription || ''}
      ownerNpub={npub || ''}
      ownerPubkey={repoOwnerPubkeyDerived || ''}
      isMaintainer={isMaintainer || false}
      isPrivate={repoIsPrivate || false}
      cloneUrls={repoCloneUrls || []}
      branches={branches || []}
      currentBranch={currentBranch || null}
      topics={repoTopics || []}
      defaultBranch={defaultBranch || null}
      isRepoCloned={isRepoCloned || false}
      copyingCloneUrl={copyingCloneUrl || false}
      onBranchChange={safeHandleBranchChange}
      onCopyCloneUrl={safeCopyCloneUrl}
      onDeleteBranch={safeDeleteBranch}
      onMenuToggle={() => { if (typeof showRepoMenu !== 'undefined') showRepoMenu = !showRepoMenu; }}
      showMenu={showRepoMenu || false}
      userPubkey={userPubkey || null}
      isBookmarked={isBookmarked || false}
      loadingBookmark={loadingBookmark || false}
      onToggleBookmark={safeToggleBookmark}
      onFork={safeForkRepository}
      forking={forking || false}
      onCloneToServer={safeCloneRepository}
      cloning={cloning || false}
      checkingCloneStatus={checkingCloneStatus || false}
      onCreateIssue={() => { if (typeof showCreateIssueDialog !== 'undefined') showCreateIssueDialog = true; }}
      onCreatePR={() => { if (typeof showCreatePRDialog !== 'undefined') showCreatePRDialog = true; }}
      onCreatePatch={() => { if (typeof showCreatePatchDialog !== 'undefined') showCreatePatchDialog = true; }}
      onCreateBranch={async () => {
        if (!userPubkey || !isMaintainer || needsClone) return;
        try {
          const settings = await settingsStore.getSettings();
          defaultBranchName = settings.defaultBranch || 'master';
        } catch {
          defaultBranchName = 'master';
        }
        // Preset the default branch name in the input field
        newBranchName = defaultBranchName;
        newBranchFrom = null; // Reset from branch selection
        showCreateBranchDialog = true;
      }}
      onSettings={() => goto(`/signup?npub=${npub}&repo=${repo}`)}
      onGenerateVerification={repoOwnerPubkeyDerived && userPubkeyHex === repoOwnerPubkeyDerived && verificationStatus?.verified !== true ? generateAnnouncementFileForRepo : undefined}
      onDeleteAnnouncement={repoOwnerPubkeyDerived && userPubkeyHex === repoOwnerPubkeyDerived ? deleteAnnouncement : undefined}
      deletingAnnouncement={deletingAnnouncement}
      hasUnlimitedAccess={hasUnlimitedAccess($userStore.userLevel)}
      needsClone={needsClone}
      allMaintainers={allMaintainers}
      onCopyEventId={copyEventId}
    />
  {/if}

  <!-- Additional repo metadata (website, clone URLs with verification) -->

  {#if repoWebsite || (repoCloneUrls && repoCloneUrls.length > 0) || repoLanguage || (repoTopics && repoTopics.length > 0) || forkInfo?.isFork}
    <div class="repo-metadata-section">
      {#if repoWebsite}
        <div class="repo-website">
          <a href={repoWebsite} target="_blank" rel="noopener noreferrer">
            <img src="/icons/external-link.svg" alt="" class="icon-inline" />
            {repoWebsite}
          </a>
        </div>
      {/if}
      {#if repoLanguage}
        <span class="repo-language">
          <img src="/icons/file-text.svg" alt="" class="icon-inline" />
          {repoLanguage}
        </span>
      {/if}
      {#if forkInfo?.isFork && forkInfo.originalRepo}
        <span class="fork-badge">Forked from <a href={`/repos/${forkInfo.originalRepo.npub}/${forkInfo.originalRepo.repo}`}>{forkInfo.originalRepo.repo}</a></span>
      {/if}
      {#if repoCloneUrls && repoCloneUrls.length > 0}
        <div class="repo-clone-urls">
          <div style="display: flex; align-items: center; gap: 0.5rem;">
            <button 
              class="clone-label-button"
              onclick={() => cloneUrlsExpanded = !cloneUrlsExpanded}
              aria-expanded={cloneUrlsExpanded}
            >
              <span class="clone-label">Clone URLs:</span>
            <img src="/icons/chevron-down.svg" alt="" class="clone-toggle-icon icon-inline" class:expanded={cloneUrlsExpanded} />
          </button>
          <button
            class="reachability-refresh-button"
            onclick={() => loadCloneUrlReachability(true)}
            disabled={loadingReachability}
            title="Refresh reachability status"
          >
            {#if loadingReachability}
              Checking...
            {:else}
              <img src="/icons/refresh-cw.svg" alt="" class="refresh-icon icon-inline" />
              <span>Check Reachability</span>
            {/if}
          </button>
          </div>
          <div class="clone-url-list" class:collapsed={!cloneUrlsExpanded}>
            {#if isRepoCloned === true}
              <button 
                class="copy-clone-url-button"
                onclick={() => copyCloneUrl()}
                disabled={copyingCloneUrl}
                title="Copy clone URL"
              >
                <img src="/icons/copy.svg" alt="" class="icon-inline" />
                {copyingCloneUrl ? 'Copying...' : 'Copy Clone URL'}
              </button>
            {/if}
            {#each (showAllCloneUrls ? repoCloneUrls : repoCloneUrls.slice(0, 3)) as cloneUrl}
            {@const cloneVerification = verificationStatus?.cloneVerifications?.find(cv => {
              const normalizeUrl = (url: string) => url.replace(/\/$/, '').toLowerCase().replace(/^https?:\/\//, '');
              const normalizedCv = normalizeUrl(cv.url);
              const normalizedClone = normalizeUrl(cloneUrl);
              return normalizedCv === normalizedClone || 
                     normalizedCv.includes(normalizedClone) || 
                     normalizedClone.includes(normalizedCv);
            })}
            {@const reachability = cloneUrlReachability.get(cloneUrl)}
            {@const isChecking = checkingReachability.has(cloneUrl)}
            <div class="clone-url-wrapper">
              <code class="clone-url">{cloneUrl}</code>
              {#if loadingVerification}
                <span class="verification-badge loading" title="Checking verification...">
                  <span style="opacity: 0.5;">⋯</span>
                </span>
              {:else if cloneVerification !== undefined}
                <span 
                  class="verification-badge" 
                  class:verified={cloneVerification.verified} 
                  class:unverified={!cloneVerification.verified}
                  title={cloneVerification.verified ? 'Verified ownership' : (cloneVerification.error || 'Unverified')}
                >
                  {#if cloneVerification.verified}
                    <img src="/icons/check-circle.svg" alt="Verified" class="icon-inline" />
                  {:else}
                    <img src="/icons/alert-triangle.svg" alt="Unverified" class="icon-inline" />
                  {/if}
                </span>
              {:else if verificationStatus}
                <span class="verification-badge unverified" title="Verification status unknown">
                  <img src="/icons/alert-triangle.svg" alt="Unknown" class="icon-inline" />
                </span>
              {:else}
                <span class="verification-badge unverified" title="Verification not checked">
                  <img src="/icons/alert-triangle.svg" alt="Not checked" class="icon-inline" />
                </span>
              {/if}
              {#if isChecking || loadingReachability}
                <span class="reachability-badge loading" title="Checking reachability...">
                  <span style="opacity: 0.5;">⋯</span>
                </span>
              {:else if reachability !== undefined}
                <span 
                  class="reachability-badge" 
                  class:reachable={reachability.reachable} 
                  class:unreachable={!reachability.reachable}
                  title={reachability.reachable 
                    ? `Reachable${reachability.serverType === 'grasp' ? ' (GRASP server)' : reachability.serverType === 'git' ? ' (Git server)' : ''}` 
                    : (reachability.error || 'Unreachable')}
                >
                  {#if reachability.reachable}
                    <img src="/icons/check-circle.svg" alt="Reachable" class="icon-inline icon-success" />
                  {:else}
                    <img src="/icons/x-circle.svg" alt="Unreachable" class="icon-inline icon-error" />
                  {/if}
                </span>
                {#if reachability.serverType === 'grasp'}
                  <span class="server-type-badge grasp-badge" title="GRASP server (git server with Nostr relay and GRASP features)">
                    GRASP
                  </span>
                {:else if reachability.serverType === 'git'}
                  <span class="server-type-badge git-badge" title="Git server (standard git smart HTTP)">
                    Git
                  </span>
                {/if}
              {/if}
            </div>
            {/each}
            {#if repoCloneUrls.length > 3}
              <button 
                class="clone-more" 
                onclick={() => showAllCloneUrls = !showAllCloneUrls}
                title={showAllCloneUrls ? 'Show fewer' : 'Show all clone URLs'}
              >
                {showAllCloneUrls ? `-${repoCloneUrls.length - 3} less` : `+${repoCloneUrls.length - 3} more`}
              </button>
            {/if}
          </div>
        </div>
      {/if}
    </div>
  {/if}

  <main class="repo-view">
    {#if isRepoCloned === false && (canUseApiFallback || apiFallbackAvailable === null)}
      <div class="read-only-banner">
        <div class="banner-content">
          <img src="/icons/alert-circle.svg" alt="Info" class="banner-icon" />
          <span>
            {#if apiFallbackAvailable === null}
              Checking external clone URLs for read-only access...
            {:else}
              This repository is displayed in <strong>read-only mode</strong> using data from external clone URLs. To enable editing and full features, clone this repository to the server.
            {/if}
          </span>
          {#if hasUnlimitedAccess($userStore.userLevel) && apiFallbackAvailable !== null}
            <button 
              class="clone-button-banner"
              onclick={cloneRepository}
              disabled={cloning || checkingCloneStatus}
            >
              {cloning ? 'Cloning...' : (checkingCloneStatus ? 'Checking...' : 'Clone to Server')}
            </button>
          {/if}
        </div>
      </div>
    {/if}
    {#if error}
      <div class="error">
        <div class="error-message">
          <strong>Error:</strong> {error}
        </div>
        {#if error.includes('not cloned locally') && hasUnlimitedAccess($userStore.userLevel)}
          <div class="error-actions">
            <button 
              class="clone-button-inline"
              onclick={cloneRepository}
              disabled={cloning || checkingCloneStatus}
            >
              {cloning ? 'Cloning...' : (checkingCloneStatus ? 'Checking...' : 'Clone to Server')}
            </button>
          </div>
        {/if}
      </div>
    {/if}

    <!-- Tabs -->
    
    {#if isRepoCloned === false && !canUseApiFallback && tabs.length === 0}
      <div class="repo-not-cloned-message">
        <div class="message-content">
          <h2>Repository Not Cloned</h2>
          <p>This repository has not been cloned to the server yet, and read-only access via external clone URLs is not available.</p>
          {#if hasUnlimitedAccess($userStore.userLevel)}
            <p>Use the "Clone to Server" option in the repository menu to clone this repository.</p>
          {:else}
            <p>Contact a server administrator with unlimited access to clone this repository.</p>
          {/if}
        </div>
      </div>
    {:else}
    <div class="repo-layout">
      <!-- File Tree Sidebar -->
      {#if activeTab === 'files' && canViewRepo}
      <aside class="file-tree" class:hide-on-mobile={!showFileListOnMobile && activeTab === 'files'}>
        <div class="file-tree-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Files {#if isRepoCloned === false && canUseApiFallback}<span class="read-only-badge">Read-Only</span>{/if}</h2>
          <button 
            onclick={toggleWordWrap} 
            class="word-wrap-button"
            title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
            aria-label={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
          >
            {wordWrap ? 'Wrap' : 'No Wrap'}
          </button>
          <div class="file-tree-actions">
            {#if pathStack.length > 0 || currentPath}
              <button onclick={handleBack} class="back-button">← Back</button>
            {/if}
            {#if userPubkey && isMaintainer}
              <button 
                onclick={() => {
                  if (!userPubkey || !isMaintainer || needsClone) return;
                  showCreateFileDialog = true;
                }} 
                class="create-file-button"
                disabled={needsClone}
                title={needsClone ? cloneTooltip : 'Create a new file'}
              >
                <img src="/icons/plus.svg" alt="New File" class="icon" />
              </button>
            {/if}
            <button 
              onclick={() => showFileListOnMobile = !showFileListOnMobile} 
              class="mobile-toggle-button"
              title="Show file viewer"
            >
              <img src="/icons/arrow-right.svg" alt="Show file viewer" class="icon-inline" />
            </button>
          </div>
        </div>
        {#if loading && !currentFile}
          <div class="loading">Loading files...</div>
        {:else}
          <ul class="file-list">
            {#each files as file}
              <li class="file-item" class:directory={file.type === 'directory'} class:selected={currentFile === file.path}>
                <button onclick={() => handleFileClick(file)} class="file-button">
                  {#if file.type === 'directory'}
                    <img src="/icons/folder.svg" alt="Directory" class="icon-inline folder-icon" />
                  {:else}
                    <img src="/icons/file-text.svg" alt="File" class="icon-inline file-icon" />
                  {/if}
                  {file.name}
                  {#if file.size !== undefined}
                    <span class="file-size">({(file.size / 1024).toFixed(1)} KB)</span>
                  {/if}
                </button>
                {#if userPubkey && isMaintainer && file.type === 'file'}
                  <button 
                    onclick={() => {
                      if (needsClone) return;
                      deleteFile(file.path);
                    }} 
                    class="delete-file-button" 
                    disabled={needsClone}
                    title={needsClone ? cloneTooltip : 'Delete file'}
                  >
                    <img src="/icons/x.svg" alt="Delete" class="icon-small" />
                  </button>
                {/if}
              </li>
            {/each}
          </ul>
        {/if}
      </aside>
      {/if}

      <!-- Commit History View -->
      {#if activeTab === 'history' && canViewRepo}
      <aside class="history-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'history'}>
        <div class="history-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Commits {#if isRepoCloned === false && canUseApiFallback}<span class="read-only-badge">Read-Only</span>{/if}</h2>
          <button 
            onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
            class="mobile-toggle-button"
            title="Show content"
          >
            <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
          </button>
        </div>
        {#if loadingCommits}
          <div class="loading">Loading commits...</div>
        {:else if commits.length > 0}
          <ul class="commit-list">
            {#each commits as commit}
              {@const commitHash = commit.hash || (commit as any).sha || ''}
              {#if commitHash}
                <li class="commit-item" class:selected={selectedCommit === commitHash}>
                  <button onclick={() => viewDiff(commitHash)} class="commit-button">
                    <div class="commit-hash">{commitHash.slice(0, 7)}</div>
                    <div class="commit-message">{commit.message || 'No message'}</div>
                    <div class="commit-meta">
                      <span>{commit.author || 'Unknown'}</span>
                      <span>{commit.date ? new Date(commit.date).toLocaleString() : 'Unknown date'}</span>
                    </div>
                  </button>
                </li>
              {/if}
            {/each}
          </ul>
        {/if}
      </aside>
      {/if}

      <!-- Tags View -->
      <TagsTab
        {npub}
        {repo}
        {tags}
        {releases}
        {selectedTag}
        {isMaintainer}
        {userPubkeyHex}
        repoOwnerPubkeyDerived={repoOwnerPubkeyDerived}
        {isRepoCloned}
        {canViewRepo}
        {canUseApiFallback}
        {needsClone}
        {cloneTooltip}
        {activeTab}
        {tabs}
        {showLeftPanelOnMobile}
        onTagSelect={(tagName) => selectedTag = tagName}
        onTabChange={(tab) => activeTab = tab as typeof activeTab}
        onToggleMobilePanel={() => showLeftPanelOnMobile = !showLeftPanelOnMobile}
        onCreateTag={() => showCreateTagDialog = true}
        onCreateRelease={(tagName, tagHash) => {
          newReleaseTagName = tagName;
          newReleaseTagHash = tagHash;
          showCreateReleaseDialog = true;
        }}
        onLoadTags={loadTags}
      />

      <!-- Code Search View -->
      {#if activeTab === 'code-search' && canViewRepo}
      <aside class="code-search-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'code-search'}>
        <div class="code-search-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Code Search</h2>
          <button 
            onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
            class="mobile-toggle-button"
            title="Show content"
          >
            <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
          </button>
        </div>
      </aside>
      {/if}

      <!-- Issues View -->
      {#if activeTab === 'issues'}
      <aside class="issues-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'issues'}>
        <div class="issues-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Issues</h2>
          {#if userPubkey}
            <button onclick={() => {
              if (!userPubkey) return;
              showCreateIssueDialog = true;
            }} class="create-issue-button" title="Create a new issue">
              <img src="/icons/plus.svg" alt="New Issue" class="icon" />
            </button>
          {/if}
          <button 
            onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
            class="mobile-toggle-button"
            title="Show content"
          >
            <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
          </button>
        </div>
        {#if loadingIssues}
          <div class="loading">Loading issues...</div>
        {:else if issues.length > 0}
          <ul class="issue-list">
            {#each issues as issue}
              <li class="issue-item" class:selected={selectedIssue === issue.id}>
                <button 
                  onclick={() => {
                    selectedIssue = issue.id;
                    loadIssueReplies(issue.id);
                  }}
                  class="issue-item-button"
                >
                  <div class="issue-header">
                    <span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}>
                      {issue.status}
                    </span>
                  </div>
                  <div class="issue-subject">{issue.subject}</div>
                </button>
              </li>
            {/each}
          </ul>
        {/if}
      </aside>
      {/if}

      <!-- Pull Requests View -->
      {#if activeTab === 'prs'}
      <aside class="prs-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'prs'}>
        <div class="prs-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Pull Requests</h2>
          {#if userPubkey}
            <button onclick={() => {
              if (!userPubkey) return;
              showCreatePRDialog = true;
            }} class="create-pr-button" title="Create a new pull request">
              <img src="/icons/plus.svg" alt="New PR" class="icon" />
            </button>
          {/if}
          <button 
            onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
            class="mobile-toggle-button"
            title="Show content"
          >
            <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
          </button>
        </div>
        {#if loadingPRs}
          <div class="loading">Loading pull requests...</div>
        {:else if prs.length > 0}
          <ul class="pr-list">
            {#each prs as pr}
              <li class="pr-item">
                <div class="pr-header">
                  <span class="pr-status" class:open={pr.status === 'open'} class:closed={pr.status === 'closed'} class:merged={pr.status === 'merged'}>
                    {pr.status}
                  </span>
                  <span class="pr-subject">{pr.subject}</span>
                </div>
                <div class="pr-meta">
                  <span>#{pr.id.slice(0, 7)}</span>
                  {#if pr.commitId}
                    <span class="pr-commit">Commit: {pr.commitId.slice(0, 7)}</span>
                  {/if}
                  <span>{new Date(pr.created_at * 1000).toLocaleDateString()}</span>
                  <EventCopyButton eventId={pr.id} kind={pr.kind} pubkey={pr.author} />
                </div>
              </li>
            {/each}
          </ul>
        {/if}
      </aside>
      {/if}

      <!-- Patches View -->
      {#if activeTab === 'patches'}
      <aside class="patches-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'patches'}>
        <div class="patches-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Patches</h2>
          {#if userPubkey}
            <button 
              onclick={() => showCreatePatchDialog = true}
              class="create-patch-button"
              title="Create a new patch"
            >
              <img src="/icons/plus.svg" alt="New Patch" class="icon" />
            </button>
          {/if}
          <button 
            onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
            class="mobile-toggle-button"
            title="Show content"
          >
            <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
          </button>
        </div>
        {#if loadingPatches}
          <div class="loading">Loading patches...</div>
        {:else if patches.length > 0}
          <ul class="patch-list">
            {#each patches as patch}
              <li class="patch-item" class:selected={selectedPatch === patch.id}>
                <button 
                  onclick={() => selectedPatch = patch.id}
                  class="patch-item-button"
                >
                  <div class="patch-header">
                    <span class="patch-subject">{patch.subject}</span>
                  </div>
                  <div class="patch-meta">
                    <span>#{patch.id.slice(0, 7)}</span>
                    <span>{new Date(patch.created_at * 1000).toLocaleDateString()}</span>
                    <EventCopyButton eventId={patch.id} kind={patch.kind} pubkey={patch.author} />
                  </div>
                </button>
              </li>
            {/each}
          </ul>
        {/if}
      </aside>
      {/if}

      <!-- Discussions View -->
      {#if activeTab === 'discussions'}
      <aside class="discussions-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'discussions'}>
        <div class="discussions-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Discussions</h2>
          {#if userPubkey}
            <button 
              onclick={() => showCreateThreadDialog = true}
              class="create-discussion-button"
              disabled={creatingThread}
              title={creatingThread ? 'Creating...' : 'New Discussion Thread'}
            >
              <img src="/icons/plus.svg" alt="New Discussion" class="icon" />
            </button>
          {/if}
          <button 
            onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
            class="mobile-toggle-button"
            title="Show content"
          >
            <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
          </button>
        </div>
        {#if loadingDiscussions}
          <div class="loading">Loading discussions...</div>
        {:else if discussions.length > 0}
          <ul class="discussion-list">
            {#each discussions as discussion}
              {@const hasComments = discussion.comments && discussion.comments.length > 0}
              {@const totalReplies = hasComments ? countAllReplies(discussion.comments) : 0}
              <li class="discussion-item" class:selected={selectedDiscussion === discussion.id}>
                <button 
                  onclick={() => selectedDiscussion = discussion.id}
                  class="discussion-item-button"
                >
                  <div class="discussion-header">
                    <span class="discussion-title">{discussion.title}</span>
                  </div>
                  <div class="discussion-meta">
                    {#if discussion.type === 'thread'}
                      <span class="discussion-type">Thread</span>
                      {#if hasComments}
                        <span class="comment-count">{totalReplies} {totalReplies === 1 ? 'reply' : 'replies'}</span>
                      {/if}
                    {:else}
                      <span class="discussion-type">Comments</span>
                    {/if}
                    <span>{new Date(discussion.createdAt * 1000).toLocaleDateString()}</span>
                    <EventCopyButton eventId={discussion.id} kind={discussion.kind} pubkey={discussion.pubkey} />
                  </div>
                </button>
              </li>
            {/each}
          </ul>
        {/if}
      </aside>
      {/if}

      <!-- Docs View -->
      {#if activeTab === 'docs'}
      <aside class="docs-sidebar" class:hide-on-mobile={!showLeftPanelOnMobile && activeTab === 'docs'}>
        <div class="docs-header">
          <TabsMenu 
            activeTab={activeTab} 
            {tabs} 
            onTabChange={(tab) => activeTab = tab as typeof activeTab}
          />
          <h2>Docs</h2>
          <button 
            onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
            class="mobile-toggle-button"
            title="Show content"
          >
            <img src="/icons/arrow-right.svg" alt="Show content" class="icon-inline" />
          </button>
        </div>
      </aside>
      {/if}

      <!-- Editor Area / Diff View / README -->
      <div class="editor-area" class:hide-on-mobile={(showFileListOnMobile && activeTab === 'files') || (showLeftPanelOnMobile && activeTab !== 'files')}>
        {#if activeTab === 'files' && readmeContent && !currentFile}
          <div class="readme-section">
            <div class="readme-header">
              <h3>README</h3>
              <div class="readme-actions">
                {#if readmePath && supportsPreview((readmePath.split('.').pop() || '').toLowerCase())}
                  <button 
                    onclick={() => {
                      showFilePreview = !showFilePreview;
                    }}
                    class="preview-toggle-button"
                    title={showFilePreview ? 'Show raw' : 'Show preview'}
                  >
                    {showFilePreview ? 'Raw' : 'Preview'}
                  </button>
                {/if}
                <a href={`/api/repos/${npub}/${repo}/raw?path=${readmePath}`} target="_blank" class="raw-link">View Raw</a>
                <button 
                  type="button"
                  class="download-link"
                  onclick={async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    e.stopImmediatePropagation();
                          await downloadRepository();
                  }}
                >Download ZIP</button>
                <button 
                  onclick={() => showFileListOnMobile = !showFileListOnMobile} 
                  class="mobile-toggle-button"
                  title="Show file list"
                >
                  <img src="/icons/arrow-right.svg" alt="Show file list" class="icon-inline mobile-toggle-left" />
                </button>
              </div>
            </div>
            {#if loadingReadme}
              <div class="loading">Loading README...</div>
            {:else if showFilePreview && readmeHtml && readmeHtml.trim()}
              <div class="readme-content markdown">
                {@html readmeHtml}
              </div>
            {:else if readmeContent}
              <div class="readme-content">
                <pre><code class="hljs language-text">{readmeContent}</code></pre>
              </div>
            {/if}
          </div>
        {/if}

        {#if activeTab === 'files' && currentFile}
          <div class="editor-header">
            <span class="file-path">{currentFile}</span>
            <div class="editor-actions">
              {#if branches.length > 0 && isMaintainer}
                <select 
                  bind:value={currentBranch} 
                  class="branch-selector" 
                  disabled={saving || needsClone} 
                  title="Select branch"
                  onchange={() => {
                    // Use the existing handleBranchChange function
                    handleBranchChangeDirect(currentBranch || '');
                  }}
                >
                  {#each branches as branch}
                    {@const branchName = typeof branch === 'string' ? branch : branch.name}
                    <option value={branchName}>{branchName}{#if branchName === defaultBranch} (default){/if}</option>
                  {/each}
                </select>
              {:else if currentBranch && isMaintainer}
                <span class="branch-display" title="Current branch">{currentBranch}</span>
              {/if}
              {#if hasChanges}
                <span class="unsaved-indicator">● Unsaved changes</span>
              {/if}
              {#if currentFile && supportsPreview((currentFile.split('.').pop() || '').toLowerCase()) && !isMaintainer}
                <button 
                  onclick={() => {
                    showFilePreview = !showFilePreview;
                    if (!showFilePreview && fileContent && currentFile) {
                      // When switching to raw, apply syntax highlighting
                      const ext = currentFile.split('.').pop() || '';
                      applySyntaxHighlighting(fileContent, ext).catch(err => console.error('Error applying syntax highlighting:', err));
                    }
                  }}
                  class="preview-toggle-button"
                  title={showFilePreview ? 'Show raw' : 'Show preview'}
                >
                  {showFilePreview ? 'Raw' : 'Preview'}
                </button>
              {/if}
              {#if currentFile && fileContent}
                <button 
                  onclick={(e) => copyFileContent(e)}
                  disabled={copyingFile}
                  class="file-action-button"
                  title="Copy raw content to clipboard"
                >
                  <img src="/icons/copy.svg" alt="Copy" class="icon-inline" />
                </button>
                <button 
                  onclick={downloadFile}
                  class="file-action-button"
                  title="Download file"
                >
                  <img src="/icons/download.svg" alt="Download" class="icon-inline" />
                </button>
              {/if}
              {#if isMaintainer}
                <button 
                  onclick={() => {
                    if (!userPubkey || !isMaintainer || needsClone) return;
                    showCommitDialog = true;
                  }} 
                  disabled={!hasChanges || saving || needsClone} 
                  class="save-button"
                  title={needsClone ? cloneTooltip : (hasChanges ? 'Save changes' : 'No changes to save')}
                >
                  {saving ? 'Saving...' : 'Save'}
                </button>
              {:else if userPubkey}
                <span class="non-maintainer-notice">Only maintainers can edit files. Submit a PR instead.</span>
              {/if}
              <button 
                onclick={() => showFileListOnMobile = !showFileListOnMobile} 
                class="mobile-toggle-button"
                title="Show file list"
              >
                <img src="/icons/arrow-right.svg" alt="Show file list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
          </div>
          
          {#if loading}
            <div class="loading">Loading file...</div>
          {:else}
            <div class="editor-container">
              {#if isMaintainer}
                <CodeEditor 
                  content={editedContent} 
                  language={fileLanguage}
                  onChange={handleContentChange}
                  readOnly={needsClone || (isRepoCloned === false && canUseApiFallback)}
                />
              {:else}
                <div class="read-only-editor" class:word-wrap={wordWrap}>
                  {#if isImageFile && imageUrl}
                    <!-- Image file: display as image -->
                    <div class="file-preview image-preview">
                      <img src={imageUrl} alt={currentFile?.split('/').pop() || 'Image'} class="file-image" />
                    </div>
                  {:else if currentFile && showFilePreview && fileHtml && supportsPreview((currentFile.split('.').pop() || '').toLowerCase())}
                    <!-- Preview mode: show rendered HTML -->
                    <div class="file-preview markdown">
                      {@html fileHtml}
                    </div>
                  {:else if highlightedFileContent}
                    <!-- Raw mode: show syntax highlighted code -->
                    {@html highlightedFileContent}
                  {:else}
                    <!-- Fallback: plain text -->
                    <pre><code class="hljs">{fileContent}</code></pre>
                  {/if}
                </div>
              {/if}
            </div>
          {/if}
        {:else if activeTab === 'files'}
          <div class="content-header-mobile">
            <button 
              onclick={() => showFileListOnMobile = !showFileListOnMobile} 
              class="mobile-toggle-button"
              title="Show file list"
            >
              <img src="/icons/arrow-right.svg" alt="Show file list" class="icon-inline mobile-toggle-left" />
            </button>
          </div>
          <div class="empty-state">
            <p>Select a file from the sidebar to view and edit it</p>
          </div>
        {/if}

        {#if activeTab === 'history'}
          <div class="commits-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'history'}>
            <div class="content-header-mobile">
              <button 
                onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
                class="mobile-toggle-button"
                title="Show list"
              >
                <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
            {#if selectedCommit}
              {@const commit = commits.find(c => (c.hash || (c as any).sha) === selectedCommit)}
              {#if commit}
                <div class="commit-detail">
                  <div class="commit-detail-header">
                    <h3>{commit.message || 'No message'}</h3>
                    <button onclick={() => { showDiff = false; selectedCommit = null; }} class="close-button">×</button>
                  </div>
                  <div class="commit-meta-detail">
                    <span>#{selectedCommit.slice(0, 7)}</span>
                    <span>{commit.author || 'Unknown'}</span>
                    <span>{commit.date ? new Date(commit.date).toLocaleString() : 'Unknown date'}</span>
                  </div>
                  {#if loadingCommits}
                    <div class="loading">Loading diff...</div>
                  {:else if showDiff && diffData.length > 0}
                    <div class="diff-view">
                      {#each diffData as diff}
                        <div class="diff-file">
                          <div class="diff-file-header">
                            <span class="diff-file-name">{diff.file}</span>
                            <span class="diff-stats">
                              <span class="additions">+{diff.additions}</span>
                              <span class="deletions">-{diff.deletions}</span>
                            </span>
                          </div>
                          <pre class="diff-content"><code>{diff.diff}</code></pre>
                        </div>
                      {/each}
                    </div>
                  {:else if showDiff}
                    <div class="empty-state">
                      <p>No diff data available</p>
                    </div>
                  {:else}
                    <div class="empty-state">
                      <p>Loading diff...</p>
                    </div>
                  {/if}
                </div>
              {/if}
            {:else}
              <div class="empty-state">
                <p>Select a commit from the sidebar to view details</p>
              </div>
            {/if}
          </div>
        {/if}

        <!-- Tags content is now handled by TagsTab component -->


        {#if activeTab === 'code-search' && canViewRepo}
          <div class="code-search-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'code-search'}>
            <div class="content-header-mobile">
              <button 
                onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
                class="mobile-toggle-button"
                title="Show list"
              >
                <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
            <div class="code-search-form">
              <div class="search-input-group">
                <input 
                  type="text" 
                  bind:value={codeSearchQuery}
                  placeholder="Search code..."
                  onkeydown={(e) => e.key === 'Enter' && performCodeSearch()}
                  class="code-search-input"
                />
                <select bind:value={codeSearchScope} class="code-search-scope">
                  <option value="repo">This Repository</option>
                  <option value="all">All Repositories</option>
                </select>
                <button onclick={performCodeSearch} disabled={loadingCodeSearch || !codeSearchQuery.trim()} class="search-button">
                  {loadingCodeSearch ? 'Searching...' : 'Search'}
                </button>
              </div>
            </div>
            {#if loadingCodeSearch}
              <div class="empty-state">
                <p>Searching...</p>
              </div>
            {:else if codeSearchResults.length > 0}
              <div class="code-search-results">
                <h3>Found {codeSearchResults.length} result{codeSearchResults.length !== 1 ? 's' : ''}</h3>
                {#each codeSearchResults as result}
                  <div class="code-search-result-item">
                    <div class="result-header">
                      <span class="result-file">{result.file}</span>
                      <span class="result-line">Line {result.line}</span>
                      {#if codeSearchScope === 'all' && 'repo' in result}
                        <span class="result-repo">{result.repo || npub}/{result.repo || repo}</span>
                      {/if}
                    </div>
                    <pre class="result-content">{result.content}</pre>
                  </div>
                {/each}
              </div>
            {:else if codeSearchQuery.trim() && !loadingCodeSearch}
              <div class="empty-state">
                <p>No results found</p>
              </div>
            {/if}
          </div>
        {/if}

        {#if activeTab === 'issues'}
          <div class="issues-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'issues'}>
            <div class="content-header-mobile">
              <button 
                onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
                class="mobile-toggle-button"
                title="Show list"
              >
                <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
            {#if loadingIssues}
              <div class="empty-state">
                <p>Loading issues...</p>
              </div>
            {:else if error}
              <div class="empty-state error-state">
                <p>Error loading issues: {error}</p>
                <button onclick={loadIssues} class="retry-button">Retry</button>
              </div>
            {:else if issues.length === 0}
              <div class="empty-state">
                <p>No issues found. Create one to get started!</p>
              </div>
            {:else if selectedIssue}
              {@const issue = issues.find(i => i.id === selectedIssue)}
              {#if issue}
                <div class="issue-detail">
                  <h3>{issue.subject}</h3>
                  <div class="issue-meta-detail">
                    <span class="issue-status" class:open={issue.status === 'open'} class:closed={issue.status === 'closed'} class:resolved={issue.status === 'resolved'}>
                      {issue.status}
                    </span>
                    <span>#{issue.id.slice(0, 7)}</span>
                    <span>Created {new Date(issue.created_at * 1000).toLocaleString()}</span>
                    <EventCopyButton eventId={issue.id} kind={issue.kind} pubkey={issue.author} />
                  </div>
                  <div class="issue-body">
                    {@html issue.content.replace(/\n/g, '<br>')}
                  </div>
                  
                  {#if userPubkeyHex && (isMaintainer || userPubkeyHex === issue.author)}
                    <div class="issue-actions">
                      {#if issue.status === 'open'}
                        <button onclick={() => updateIssueStatus(issue.id, issue.author, 'closed')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn close-btn">
                          {updatingIssueStatus[issue.id] ? 'Closing...' : 'Close'}
                        </button>
                        <button onclick={() => updateIssueStatus(issue.id, issue.author, 'resolved')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn resolve-btn">
                          {updatingIssueStatus[issue.id] ? 'Resolving...' : 'Resolve'}
                        </button>
                      {:else if issue.status === 'closed' || issue.status === 'resolved'}
                        <button onclick={() => updateIssueStatus(issue.id, issue.author, 'open')} disabled={updatingIssueStatus[issue.id]} class="issue-action-btn reopen-btn">
                          {updatingIssueStatus[issue.id] ? 'Reopening...' : 'Reopen'}
                        </button>
                      {/if}
                    </div>
                  {/if}
                  
                  <div class="issue-replies">
                    <h4>Replies ({issueReplies.length})</h4>
                    {#if loadingIssueReplies}
                      <div class="loading">Loading replies...</div>
                    {:else if issueReplies.length === 0}
                      <div class="empty-state">
                        <p>No replies yet.</p>
                      </div>
                    {:else}
                      {#each issueReplies as reply}
                        <div class="issue-reply">
                          <div class="reply-header">
                            <UserBadge pubkey={reply.author} />
                            <span class="reply-date">{new Date(reply.created_at * 1000).toLocaleString()}</span>
                            <EventCopyButton eventId={reply.id} kind={KIND.COMMENT} pubkey={reply.author} />
                          </div>
                          <div class="reply-body">
                            {@html reply.content.replace(/\n/g, '<br>')}
                          </div>
                        </div>
                      {/each}
                    {/if}
                  </div>
                </div>
              {/if}
            {:else}
              <div class="empty-state">
                <p>Select an issue to view details</p>
              </div>
            {/if}
          </div>
        {/if}

        {#if activeTab === 'prs'}
          <div class="prs-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'prs'}>
            <div class="content-header-mobile">
              <button 
                onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
                class="mobile-toggle-button"
                title="Show list"
              >
                <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
            {#if prs.length === 0}
              <div class="empty-state">
                <p>No pull requests found. Create one to get started!</p>
              </div>
            {:else if selectedPR}
              {#each prs.filter(p => p.id === selectedPR) as pr}
                {@const decoded = nip19.decode(npub)}
                {#if decoded.type === 'npub'}
                  {@const repoOwnerPubkey = decoded.data as string}
                  <PRDetail
                    {pr}
                    {npub}
                    {repo}
                    {repoOwnerPubkey}
                    isMaintainer={isMaintainer}
                    userPubkeyHex={userPubkeyHex ?? undefined}
                    onStatusUpdate={loadPRs}
                  />
                  <button onclick={() => selectedPR = null} class="back-btn">← Back to PR List</button>
                {/if}
              {/each}
            {:else}
              {#each prs as pr}
                <div 
                  class="pr-detail" 
                  role="button"
                  tabindex="0"
                  onclick={() => selectedPR = pr.id}
                  onkeydown={(e) => {
                    if (e.key === 'Enter' || e.key === ' ') {
                      e.preventDefault();
                      selectedPR = pr.id;
                    }
                  }}
                  style="cursor: pointer;">
                  <h3>{pr.subject}</h3>
                  <div class="pr-meta-detail">
                    <span class="pr-status" class:open={pr.status === 'open'} class:closed={pr.status === 'closed'} class:merged={pr.status === 'merged'}>
                      {pr.status}
                    </span>
                    {#if pr.commitId}
                      <span>Commit: {pr.commitId.slice(0, 7)}</span>
                    {/if}
                    <span>Created {new Date(pr.created_at * 1000).toLocaleString()}</span>
                  </div>
                  <div class="pr-body">
                    {@html pr.content.replace(/\n/g, '<br>')}
                  </div>
                </div>
              {/each}
            {/if}
          </div>
        {/if}

        {#if activeTab === 'patches'}
          <div class="patches-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'patches'}>
            <div class="content-header-mobile">
              <button 
                onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
                class="mobile-toggle-button"
                title="Show list"
              >
                <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
            {#if patches.length === 0}
              <div class="empty-state">
                <p>No patches found. Create one to get started!</p>
              </div>
            {:else if selectedPatch}
              {#each patches.filter(p => p.id === selectedPatch) as patch}
                <div class="patch-detail">
                  <h3>{patch.subject}</h3>
                  <div class="patch-meta-detail">
                    <span>#{patch.id.slice(0, 7)}</span>
                    <span>Created {new Date(patch.created_at * 1000).toLocaleString()}</span>
                    <EventCopyButton eventId={patch.id} kind={patch.kind} pubkey={patch.author} />
                    {#if (isMaintainer || userPubkeyHex === repoOwnerPubkeyDerived) && isRepoCloned}
                      <button 
                        onclick={async () => {
                          if (!confirm('Apply this patch to the repository? This will create a commit with the patch changes.')) return;
                          try {
                            const response = await fetch(`/api/repos/${npub}/${repo}/patches/${patch.id}/apply`, {
                              method: 'POST',
                              headers: {
                                'Content-Type': 'application/json',
                                ...buildApiHeaders()
                              },
                              body: JSON.stringify({
                                branch: currentBranch || 'main',
                                commitMessage: `Apply patch ${patch.id.slice(0, 8)}: ${patch.subject}`
                              })
                            });
                            if (response.ok) {
                              const data = await response.json();
                              alert(`Patch applied successfully! Commit: ${data.commitHash.slice(0, 7)}`);
                              // Reload files to show changes
                              if (activeTab === 'files') {
                                loadFiles(currentPath);
                              }
                            } else {
                              const errorData = await response.json();
                              alert(`Failed to apply patch: ${errorData.message || 'Unknown error'}`);
                            }
                          } catch (err) {
                            alert(`Failed to apply patch: ${err instanceof Error ? err.message : 'Unknown error'}`);
                          }
                        }}
                        class="apply-patch-button"
                        title="Apply this patch to the repository"
                      >
                        Apply Patch
                      </button>
                    {/if}
                  </div>
                  {#if patch.description && patch.description !== patch.subject}
                    <div class="patch-description">{patch.description}</div>
                  {/if}
                  <div class="patch-body">
                    <pre class="patch-content">{patch.content}</pre>
                  </div>
                </div>
              {/each}
            {:else}
              <div class="empty-state">
                <p>Select a patch from the sidebar to view it</p>
              </div>
            {/if}
          </div>
        {/if}

        {#if activeTab === 'discussions'}
          <div class="discussions-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'discussions'}>
            <div class="content-header-mobile">
              <button 
                onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
                class="mobile-toggle-button"
                title="Show list"
              >
                <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
            {#if discussions.length === 0}
              <div class="empty-state">
                <p>No discussions found. {#if userPubkey}Create a new discussion thread to get started!{:else}Log in to create a discussion thread.{/if}</p>
              </div>
            {:else if selectedDiscussion}
              {#each discussions.filter(d => d.id === selectedDiscussion) as discussion}
                {@const isExpanded = discussion.type === 'thread' && expandedThreads.has(discussion.id)}
                {@const hasComments = discussion.comments && discussion.comments.length > 0}
                <div class="discussion-detail">
                  <div class="discussion-header-detail">
                    <h3>{discussion.title}</h3>
                    <div class="discussion-meta-detail">
                      {#if discussion.type === 'thread'}
                        <span class="discussion-type">Thread</span>
                        {#if hasComments}
                          {@const totalReplies = countAllReplies(discussion.comments)}
                          <span class="comment-count">{totalReplies} {totalReplies === 1 ? 'reply' : 'replies'}</span>
                        {/if}
                      {:else}
                        <span class="discussion-type">Comments</span>
                      {/if}
                      <span>Created {new Date(discussion.createdAt * 1000).toLocaleString()}</span>
                      <EventCopyButton eventId={discussion.id} kind={discussion.kind} pubkey={discussion.pubkey} />
                      {#if discussion.type === 'thread' && userPubkey}
                        <button 
                          class="create-reply-button"
                          onclick={() => {
                            replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author };
                            replyingToComment = null;
                            showReplyDialog = true;
                          }}
                          title="Reply to thread"
                        >
                          <img src="/icons/plus.svg" alt="Reply" class="icon" />
                        </button>
                      {/if}
                    </div>
                  </div>
                  {#if discussion.content}
                    <div class="discussion-body">
                      <p>{discussion.content}</p>
                    </div>
                  {/if}
                  {#if discussion.type === 'thread' && hasComments}
                    {@const totalReplies = countAllReplies(discussion.comments)}
                    <div class="comments-section">
                      <h4>Replies ({totalReplies})</h4>
                      {#each discussion.comments! as comment}
                        <div class="comment-item">
                          <div class="comment-meta">
                            <UserBadge pubkey={comment.author} />
                            <span>{new Date(comment.createdAt * 1000).toLocaleString()}</span>
                            <EventCopyButton eventId={comment.id} kind={comment.kind} pubkey={comment.pubkey} />
                            {#if userPubkey}
                              <button 
                                class="create-reply-button"
                                onclick={() => {
                                  replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author };
                                  replyingToComment = { id: comment.id, kind: comment.kind, pubkey: comment.pubkey, author: comment.author };
                                  showReplyDialog = true;
                                }}
                                title="Reply to comment"
                              >
                                <img src="/icons/plus.svg" alt="Reply" class="icon" />
                              </button>
                            {/if}
                          </div>
                          {#if true}
                            {@const commentEvent = getDiscussionEvent(comment.id)}
                            {@const referencedEvent = commentEvent ? getReferencedEventFromDiscussion(commentEvent) : undefined}
                            {@const parts = processContentWithNostrLinks(comment.content)}
                            <div class="comment-content">
                              {#if referencedEvent}
                                <div class="referenced-event">
                                  <div class="referenced-event-header">
                                    <UserBadge pubkey={referencedEvent.pubkey} disableLink={true} />
                                    <span class="referenced-event-time">{formatDiscussionTime(referencedEvent.created_at)}</span>
                                  </div>
                                  <div class="referenced-event-content">{referencedEvent.content || '(No content)'}</div>
                                </div>
                              {/if}
                              <div>
                                {#each parts as part}
                                  {#if part.type === 'text'}
                                    <span>{part.value}</span>
                                  {:else if part.type === 'event' && part.event}
                                    <div class="nostr-link-event">
                                      <div class="nostr-link-event-header">
                                        <UserBadge pubkey={part.event.pubkey} disableLink={true} />
                                        <span class="nostr-link-event-time">{formatDiscussionTime(part.event.created_at)}</span>
                                      </div>
                                      <div class="nostr-link-event-content">{part.event.content || '(No content)'}</div>
                                    </div>
                                  {:else if part.type === 'profile' && part.pubkey}
                                    <UserBadge pubkey={part.pubkey} />
                                  {:else}
                                    <span class="nostr-link-placeholder">{part.value}</span>
                                  {/if}
                                {/each}
                              </div>
                            </div>
                          {/if}
                          {#if comment.replies && comment.replies.length > 0}
                            <div class="nested-replies">
                              {#each comment.replies as reply}
                                <div class="comment-item nested-comment">
                                  <div class="comment-meta">
                                    <UserBadge pubkey={reply.author} />
                                    <span>{new Date(reply.createdAt * 1000).toLocaleString()}</span>
                                    <EventCopyButton eventId={reply.id} kind={reply.kind} pubkey={reply.pubkey} />
                                    {#if userPubkey}
                                      <button 
                                        class="create-reply-button"
                                        onclick={() => {
                                          replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author };
                                          replyingToComment = { id: reply.id, kind: reply.kind, pubkey: reply.pubkey, author: reply.author };
                                          showReplyDialog = true;
                                        }}
                                        title="Reply to comment"
                                      >
                                        <img src="/icons/plus.svg" alt="Reply" class="icon" />
                                      </button>
                                    {/if}
                                  </div>
                                  <div class="comment-content">
                                    <p>{reply.content}</p>
                                  </div>
                                  {#if reply.replies && reply.replies.length > 0}
                                    <div class="nested-replies">
                                      {#each reply.replies as nestedReply}
                                        <div class="comment-item nested-comment">
                                          <div class="comment-meta">
                                            <UserBadge pubkey={nestedReply.author} />
                                            <span>{new Date(nestedReply.createdAt * 1000).toLocaleString()}</span>
                                            <EventCopyButton eventId={nestedReply.id} kind={nestedReply.kind} pubkey={nestedReply.pubkey} />
                                            {#if userPubkey}
                                              <button 
                                                class="create-reply-button"
                                                onclick={() => {
                                                  replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author };
                                                  replyingToComment = { id: nestedReply.id, kind: nestedReply.kind, pubkey: nestedReply.pubkey, author: nestedReply.author };
                                                  showReplyDialog = true;
                                                }}
                                                title="Reply to comment"
                                              >
                                                <img src="/icons/plus.svg" alt="Reply" class="icon" />
                                              </button>
                                            {/if}
                                          </div>
                                          <div class="comment-content">
                                            <p>{nestedReply.content}</p>
                                          </div>
                                        </div>
                                      {/each}
                                    </div>
                                  {/if}
                                </div>
                              {/each}
                            </div>
                          {/if}
                        </div>
                      {/each}
                    </div>
                  {:else if discussion.type === 'comments' && hasComments}
                    {@const totalReplies = countAllReplies(discussion.comments)}
                    <div class="comments-section">
                      <h4>Comments ({totalReplies})</h4>
                      {#each discussion.comments! as comment}
                        <div class="comment-item">
                          <div class="comment-meta">
                            <UserBadge pubkey={comment.author} />
                            <span>{new Date(comment.createdAt * 1000).toLocaleString()}</span>
                            <EventCopyButton eventId={comment.id} kind={comment.kind} pubkey={comment.pubkey} />
                            {#if userPubkey}
                              <button 
                                class="create-reply-button"
                                onclick={() => {
                                  replyingToThread = { id: discussion.id, kind: discussion.kind, pubkey: discussion.pubkey, author: discussion.author };
                                  replyingToComment = { id: comment.id, kind: comment.kind, pubkey: comment.pubkey, author: comment.author };
                                  showReplyDialog = true;
                                }}
                                title="Reply to comment"
                              >
                                <img src="/icons/plus.svg" alt="Reply" class="icon" />
                              </button>
                            {/if}
                          </div>
                          {#if true}
                            {@const commentEvent = getDiscussionEvent(comment.id)}
                            {@const referencedEvent = commentEvent ? getReferencedEventFromDiscussion(commentEvent) : undefined}
                            {@const parts = processContentWithNostrLinks(comment.content)}
                            <div class="comment-content">
                              {#if referencedEvent}
                                <div class="referenced-event">
                                  <div class="referenced-event-header">
                                    <UserBadge pubkey={referencedEvent.pubkey} disableLink={true} />
                                    <span class="referenced-event-time">{formatDiscussionTime(referencedEvent.created_at)}</span>
                                  </div>
                                  <div class="referenced-event-content">{referencedEvent.content || '(No content)'}</div>
                                </div>
                              {/if}
                              <div>
                                {#each parts as part}
                                  {#if part.type === 'text'}
                                    <span>{part.value}</span>
                                  {:else if part.type === 'event' && part.event}
                                    <div class="nostr-link-event">
                                      <div class="nostr-link-event-header">
                                        <UserBadge pubkey={part.event.pubkey} disableLink={true} />
                                        <span class="nostr-link-event-time">{formatDiscussionTime(part.event.created_at)}</span>
                                      </div>
                                      <div class="nostr-link-event-content">{part.event.content || '(No content)'}</div>
                                    </div>
                                  {:else if part.type === 'profile' && part.pubkey}
                                    <UserBadge pubkey={part.pubkey} />
                                  {:else}
                                    <span class="nostr-link-placeholder">{part.value}</span>
                                  {/if}
                                {/each}
                              </div>
                            </div>
                          {/if}
                        </div>
                      {/each}
                    </div>
                  {/if}
                </div>
              {/each}
            {:else}
              <div class="empty-state">
                <p>Select a discussion from the sidebar to view it</p>
              </div>
            {/if}
          </div>
        {/if}

        {#if activeTab === 'docs'}
          <div class="docs-content" class:hide-on-mobile={showLeftPanelOnMobile && activeTab === 'docs'}>
            <div class="content-header-mobile">
              <button 
                onclick={() => showLeftPanelOnMobile = !showLeftPanelOnMobile} 
                class="mobile-toggle-button"
                title="Show list"
              >
                <img src="/icons/arrow-right.svg" alt="Show list" class="icon-inline mobile-toggle-left" />
              </button>
            </div>
            {#if loadingDocs}
              <div class="loading">Loading documentation...</div>
            {:else if documentationHtml}
              <div class="documentation-body">
                {@html documentationHtml}
              </div>
            {:else if documentationContent === null}
              <div class="empty-state">
                <p>No documentation found for this repository.</p>
              </div>
            {:else}
              <div class="empty-state">
                <p>Documentation content is empty.</p>
              </div>
            {/if}
          </div>
        {/if}
      </div>
    </div>
    {/if}
  </main>

  <!-- Create File Dialog -->
  {#if showCreateFileDialog && userPubkey && isMaintainer}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new file"
      onclick={() => showCreateFileDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreateFileDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New File</h3>
        {#if branches.length > 0}
          <label>
            Branch:
            <select bind:value={currentBranch} disabled={saving}>
              {#each branches as branch}
                {@const branchName = typeof branch === 'string' ? branch : branch.name}
                <option value={branchName}>{branchName}{#if branchName === defaultBranch} (default){/if}</option>
              {/each}
            </select>
          </label>
        {:else if currentBranch}
          <label>
            Branch:
            <input type="text" value={currentBranch} disabled />
          </label>
        {/if}
        <label>
          File Name:
          <input type="text" bind:value={newFileName} placeholder="filename.md" />
        </label>
        <label>
          Content:
          <textarea bind:value={newFileContent} rows="10" placeholder="File content..."></textarea>
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreateFileDialog = false} class="cancel-button">Cancel</button>
          <button 
            onclick={createFile} 
            disabled={!newFileName.trim() || saving || needsClone || !currentBranch} 
            class="save-button"
            title={needsClone ? cloneTooltip : (!currentBranch ? 'Please select a branch' : '')}
          >
            {saving ? 'Creating...' : 'Create'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Create Branch Dialog -->
  {#if showCreateBranchDialog && userPubkey && isMaintainer}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new branch"
      onclick={() => showCreateBranchDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreateBranchDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New Branch</h3>
        <label>
          Branch Name:
          <input type="text" bind:value={newBranchName} placeholder="feature/new-feature" />
        </label>
        <label>
          From Branch:
          <select bind:value={newBranchFrom}>
            {#if branches.length === 0}
              <option value={null}>No branches - will create initial branch</option>
            {:else}
              {#each branches as branch}
                {@const branchName = typeof branch === 'string' ? branch : (branch as { name: string }).name}
                {@const isDefaultBranch = branchName === defaultBranchName}
                {#if !isDefaultBranch}
                  <option value={branchName}>{branchName}</option>
                {/if}
              {/each}
            {/if}
          </select>
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreateBranchDialog = false} class="cancel-button">Cancel</button>
          <button 
            onclick={createBranch} 
            disabled={!newBranchName.trim() || saving || needsClone} 
            class="save-button"
            title={needsClone ? cloneTooltip : ''}
          >
            {saving ? 'Creating...' : 'Create Branch'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Create Tag Dialog -->
  {#if showCreateTagDialog && userPubkey && isMaintainer}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new tag"
      onclick={() => showCreateTagDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreateTagDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New Tag</h3>
        <label>
          Tag Name:
          <input type="text" bind:value={newTagName} placeholder="v1.0.0" />
        </label>
        <label>
          Reference (commit/branch):
          <input type="text" bind:value={newTagRef} placeholder="HEAD" />
        </label>
        <label>
          Message (optional, for annotated tag):
          <textarea bind:value={newTagMessage} rows="3" placeholder="Tag message..."></textarea>
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreateTagDialog = false} class="cancel-button">Cancel</button>
          <button 
            onclick={createTag} 
            disabled={!newTagName.trim() || saving || needsClone} 
            class="save-button"
            title={needsClone ? cloneTooltip : ''}
          >
            {saving ? 'Creating...' : 'Create Tag'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Create Release Dialog -->
  {#if showCreateReleaseDialog && userPubkey && (isMaintainer || userPubkeyHex === repoOwnerPubkeyDerived) && isRepoCloned}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new release"
      onclick={() => showCreateReleaseDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreateReleaseDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New Release</h3>
        <label>
          Tag Name:
          <input type="text" bind:value={newReleaseTagName} placeholder="v1.0.0" />
        </label>
        <label>
          Tag Hash (commit hash):
          <input type="text" bind:value={newReleaseTagHash} placeholder="abc1234..." />
        </label>
        <label>
          Release Notes:
          <textarea bind:value={newReleaseNotes} rows="10" placeholder="Release notes in markdown..."></textarea>
        </label>
        <label>
          <input type="checkbox" bind:checked={newReleaseIsDraft} />
          Draft Release
        </label>
        <label>
          <input type="checkbox" bind:checked={newReleaseIsPrerelease} />
          Pre-release
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreateReleaseDialog = false} class="cancel-button">Cancel</button>
          <button 
            onclick={createRelease} 
            disabled={!newReleaseTagName.trim() || !newReleaseTagHash.trim() || creatingRelease} 
            class="save-button"
          >
            {creatingRelease ? 'Creating...' : 'Create Release'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Create Issue Dialog -->
  {#if showCreateIssueDialog && userPubkey}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new issue"
      onclick={() => showCreateIssueDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreateIssueDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New Issue</h3>
        <label>
          Subject:
          <input type="text" bind:value={newIssueSubject} placeholder="Issue title..." />
        </label>
        <label>
          Description:
          <textarea bind:value={newIssueContent} rows="10" placeholder="Describe the issue..."></textarea>
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreateIssueDialog = false} class="cancel-button">Cancel</button>
          <button onclick={createIssue} disabled={!newIssueSubject.trim() || !newIssueContent.trim() || saving} class="save-button">
            {saving ? 'Creating...' : 'Create Issue'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Create Discussion Thread Dialog -->
  {#if showCreateThreadDialog && userPubkey}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new discussion thread"
      onclick={() => showCreateThreadDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreateThreadDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New Discussion Thread</h3>
        <label>
          Title:
          <input type="text" bind:value={newThreadTitle} placeholder="Thread title..." />
        </label>
        <label>
          Content:
          <textarea bind:value={newThreadContent} rows="10" placeholder="Start the discussion..."></textarea>
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreateThreadDialog = false} class="cancel-button">Cancel</button>
          <button onclick={createDiscussionThread} disabled={!newThreadTitle.trim() || creatingThread} class="save-button">
            {creatingThread ? 'Creating...' : 'Create Thread'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Reply to Thread/Comment Dialog -->
  {#if showReplyDialog && userPubkey && (replyingToThread || replyingToComment)}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Reply to thread"
      onclick={() => {
        showReplyDialog = false;
        replyingToThread = null;
        replyingToComment = null;
        replyContent = '';
      }}
      onkeydown={(e) => e.key === 'Escape' && (showReplyDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>
          {#if replyingToComment}
            Reply to Comment
          {:else if replyingToThread}
            Reply to Thread
          {:else}
            Reply
          {/if}
        </h3>
        <label>
          Your Reply:
          <textarea bind:value={replyContent} rows="8" placeholder="Write your reply..."></textarea>
        </label>
        <div class="modal-actions">
          <button 
            onclick={() => {
              showReplyDialog = false;
              replyingToThread = null;
              replyingToComment = null;
              replyContent = '';
            }} 
            class="cancel-button"
          >
            Cancel
          </button>
          <button 
            onclick={() => createThreadReply()} 
            disabled={!replyContent.trim() || creatingReply} 
            class="save-button"
          >
            {creatingReply ? 'Posting...' : 'Post Reply'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Create PR Dialog -->
  {#if showCreatePRDialog && userPubkey}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new pull request"
      onclick={() => showCreatePRDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreatePRDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New Pull Request</h3>
        <label>
          Subject:
          <input type="text" bind:value={newPRSubject} placeholder="PR title..." />
        </label>
        <label>
          Description:
          <textarea bind:value={newPRContent} rows="8" placeholder="Describe your changes..."></textarea>
        </label>
        <label>
          Commit ID:
          <input type="text" bind:value={newPRCommitId} placeholder="Commit hash..." />
        </label>
        <label>
          Branch Name (optional):
          <input type="text" bind:value={newPRBranchName} placeholder="feature/new-feature" />
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreatePRDialog = false} class="cancel-button">Cancel</button>
          <button onclick={createPR} disabled={!newPRSubject.trim() || !newPRContent.trim() || !newPRCommitId.trim() || saving} class="save-button">
            {saving ? 'Creating...' : 'Create PR'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Create Patch Dialog -->
  {#if showCreatePatchDialog && userPubkey}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Create new patch"
      onclick={() => showCreatePatchDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCreatePatchDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Create New Patch</h3>
        <p class="help-text">Enter your patch content in git format-patch format. Patches should be under 60KB.</p>
        <label>
          Subject (optional):
          <input type="text" bind:value={newPatchSubject} placeholder="Patch title..." />
        </label>
        <label>
          Patch Content:
          <textarea bind:value={newPatchContent} rows="15" placeholder="Paste your git format-patch output here..."></textarea>
        </label>
        <div class="modal-actions">
          <button onclick={() => showCreatePatchDialog = false} class="cancel-button">Cancel</button>
          <button onclick={createPatch} disabled={!newPatchContent.trim() || creatingPatch} class="save-button">
            {creatingPatch ? 'Creating...' : 'Create Patch'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Commit Dialog -->
  {#if showCommitDialog && userPubkey && isMaintainer}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Commit changes"
      onclick={() => showCommitDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showCommitDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <h3>Commit Changes</h3>
        {#if branches.length > 0}
          <label>
            Branch:
            <select bind:value={currentBranch} disabled={saving}>
              {#each branches as branch}
                {@const branchName = typeof branch === 'string' ? branch : branch.name}
                <option value={branchName}>{branchName}{#if branchName === defaultBranch} (default){/if}</option>
              {/each}
            </select>
          </label>
        {:else if currentBranch}
          <label>
            Branch:
            <input type="text" value={currentBranch} disabled />
          </label>
        {/if}
        <label>
          Commit Message:
          <textarea 
            bind:value={commitMessage} 
            placeholder="Describe your changes..."
            rows="4"
          ></textarea>
        </label>
        <div class="modal-actions">
          <button onclick={() => showCommitDialog = false} class="cancel-button">Cancel</button>
          <button 
            onclick={saveFile} 
            disabled={!commitMessage.trim() || saving || needsClone || !currentBranch} 
            class="save-button"
            title={needsClone ? cloneTooltip : (!currentBranch ? 'Please select a branch' : '')}
          >
            {saving ? 'Saving...' : 'Commit & Save'}
          </button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Verification File Dialog -->
  {#if showVerificationDialog && verificationFileContent}
    <div 
      class="modal-overlay" 
      role="dialog"
      aria-modal="true"
      aria-label="Repository verification file"
      onclick={() => showVerificationDialog = false}
      onkeydown={(e) => e.key === 'Escape' && (showVerificationDialog = false)}
      tabindex="-1"
    >
      <!-- svelte-ignore a11y_click_events_have_key_events -->
      <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
      <div 
        class="modal verification-modal" 
        role="document"
        onclick={(e) => e.stopPropagation()}
      >
        <div class="modal-header">
          <h3>Repository Verification File</h3>
        </div>
        <div class="modal-body">
          <p class="verification-instructions">
            The announcement event should be saved to <code>nostr/repo-events.jsonl</code> in your repository.
            You can download the announcement event JSON below for reference.
          </p>
          <div class="verification-file-content">
            <div class="file-header">
              <span class="filename">announcement-event.json</span>
              <div class="file-actions">
                <button onclick={copyVerificationToClipboard} class="copy-button">Copy</button>
                <button onclick={downloadVerificationFile} class="download-button">Download</button>
              </div>
            </div>
            <pre class="file-content"><code>{verificationFileContent}</code></pre>
          </div>
        </div>
        <div class="modal-actions">
          <button onclick={() => showVerificationDialog = false} class="cancel-button">Close</button>
        </div>
      </div>
    </div>
  {/if}
</div>

<style>
  /* Word wrap styles - ensure they apply with highest specificity */
  :global(.read-only-editor.word-wrap) {
    overflow-x: hidden !important;
  }

  :global(.read-only-editor.word-wrap pre) {
    white-space: pre-wrap !important;
    word-wrap: break-word !important;
    overflow-wrap: break-word !important;
    overflow-x: hidden !important;
    overflow-y: visible !important;
    max-width: 100% !important;
  }

  :global(.read-only-editor.word-wrap pre code),
  :global(.read-only-editor.word-wrap pre code.hljs),
  :global(.read-only-editor.word-wrap code.hljs),
  :global(.read-only-editor.word-wrap .hljs) {
    white-space: pre-wrap !important;
    word-wrap: break-word !important;
    overflow-wrap: break-word !important;
    overflow-x: hidden !important;
    overflow-y: visible !important;
    display: block !important;
    max-width: 100% !important;
  }

  :global(.read-only-editor.word-wrap pre code.hljs *),
  :global(.read-only-editor.word-wrap pre code.hljs span),
  :global(.read-only-editor.word-wrap code.hljs *),
  :global(.read-only-editor.word-wrap .hljs *) {
    white-space: pre-wrap !important;
  }

  /* Image preview styling */
  :global(.image-preview) {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 1rem;
    background: var(--bg-secondary);
    border-radius: 4px;
    min-height: 200px;
  }

  :global(.file-image) {
    max-width: 100%;
    max-height: 80vh;
    height: auto;
    object-fit: contain;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }

  /* Tag-related styles have been moved to TagsTab.svelte component */
</style>
