/ src / entrypoints / popup / App.svelte
App.svelte
   1  <script lang="ts">
   2    import { onMount } from 'svelte';
   3    import { Button, ContextMenu, Modal, Input, ConfirmDialog } from '../../components/common';
   4    import { SaveTabsModal } from '../../components/workspace';
   5    import type { Workspace, ParentWorkspace, ChildWorkspace, SavedTab } from '../../lib/types';
   6  
   7    let workspaces = $state<Workspace[]>([]);
   8    let loading = $state(true);
   9    let error = $state<string | null>(null);
  10    let savingTabs = $state(false);
  11    let showSaveModal = $state(false);
  12    let pendingTabs = $state<SavedTab[]>([]);
  13  
  14    // Context menu state
  15    let contextMenu = $state<{ workspace: Workspace; x: number; y: number } | null>(null);
  16  
  17    // Rename modal state
  18    let renameModalOpen = $state(false);
  19    let renameValue = $state('');
  20    let workspaceToRename = $state<Workspace | null>(null);
  21  
  22    // Move to parent modal state
  23    let moveModalOpen = $state(false);
  24    let workspaceToMove = $state<Workspace | null>(null);
  25    let selectedParentId = $state<string | null>(null);
  26  
  27    // Delete confirmation state
  28    let deleteConfirmOpen = $state(false);
  29    let workspaceToDelete = $state<Workspace | null>(null);
  30  
  31    // Context switch confirmation state
  32    let contextSwitchConfirmOpen = $state(false);
  33    let pendingContextSwitch = $state<Workspace | null>(null);
  34    let contextSwitchImpact = $state<{ windowsToClose: number; transcendentToKeep: number }>({ windowsToClose: 0, transcendentToKeep: 0 });
  35  
  36    // Drag and drop state
  37    let draggedWorkspace = $state<Workspace | null>(null);
  38    let dropTargetId = $state<string | null>(null);
  39    let dropPosition = $state<'inside' | 'standalone' | null>(null);
  40  
  41    // Open windows tracking
  42    let openWorkspaceIds = $state<Set<string>>(new Set());
  43    let activeContextId = $state<string | null>(null);
  44  
  45    // Current window workspace (if this window is assigned)
  46    let currentWindowWorkspaceId = $state<string | null>(null);
  47  
  48    // Collapsed parent contexts (collapsed by default)
  49    let expandedParents = $state<Set<string>>(new Set());
  50  
  51    // Playing media tab (if any)
  52    let playingTab = $state<{ tabId: number; windowId: number; title: string; favIconUrl?: string } | null>(null);
  53  
  54    function toggleParentExpanded(parentId: string) {
  55      const newSet = new Set(expandedParents);
  56      if (newSet.has(parentId)) {
  57        newSet.delete(parentId);
  58      } else {
  59        newSet.add(parentId);
  60      }
  61      expandedParents = newSet;
  62    }
  63  
  64    // Derived current window workspace
  65    const currentWindowWorkspace = $derived(
  66      currentWindowWorkspaceId ? workspaces.find(ws => ws.id === currentWindowWorkspaceId) : null
  67    );
  68  
  69    // Computed - standalone count for empty state check
  70    const standaloneCount = $derived(workspaces.filter(ws => ws.type === 'standalone').length);
  71  
  72    // Parent workspaces (for save modal and move modal)
  73    const parentWorkspaces = $derived(
  74      workspaces.filter((ws): ws is ParentWorkspace => ws.type === 'parent')
  75        .sort((a, b) => a.name.localeCompare(b.name))
  76    );
  77  
  78    // Transcendent workspaces (always visible section)
  79    const transcendentWorkspaces = $derived(
  80      workspaces.filter(ws => ws.isTranscendent)
  81        .sort((a, b) => a.name.localeCompare(b.name))
  82    );
  83  
  84    // Recent non-transcendent workspaces
  85    const recentWorkspaces = $derived(
  86      [...workspaces]
  87        .filter(ws => !ws.isTranscendent)
  88        .sort((a, b) => new Date(b.metadata.lastAccessed).getTime() - new Date(a.metadata.lastAccessed).getTime())
  89        .slice(0, 5)
  90    );
  91  
  92    // Active context workspace
  93    const activeContextWorkspace = $derived(
  94      activeContextId ? workspaces.find(ws => ws.id === activeContextId) : null
  95    );
  96  
  97    // Helper to check if a workspace is open
  98    function isWorkspaceOpen(ws: Workspace): boolean {
  99      if (ws.type === 'parent') {
 100        return workspaces.some(child =>
 101          child.type === 'child' &&
 102          (child as ChildWorkspace).parentId === ws.id &&
 103          openWorkspaceIds.has(child.id)
 104        );
 105      }
 106      return openWorkspaceIds.has(ws.id);
 107    }
 108  
 109    // Count of open children for a parent
 110    function getOpenChildCount(parentWs: Workspace): number {
 111      if (parentWs.type !== 'parent') return 0;
 112      return workspaces.filter(child =>
 113        child.type === 'child' &&
 114        (child as ChildWorkspace).parentId === parentWs.id &&
 115        openWorkspaceIds.has(child.id)
 116      ).length;
 117    }
 118  
 119    // Check if all children that would open in context switch are already open
 120    function areAllChildrenOpen(parentWs: Workspace): boolean {
 121      if (parentWs.type !== 'parent') return false;
 122      const children = workspaces.filter(child =>
 123        child.type === 'child' &&
 124        (child as ChildWorkspace).parentId === parentWs.id &&
 125        (child.openInContextSwitch !== false)
 126      );
 127      if (children.length === 0) return true;
 128      return children.every(child => openWorkspaceIds.has(child.id));
 129    }
 130  
 131    // Context menu items
 132    const contextMenuItems = $derived.by(() => {
 133      if (!contextMenu) return [];
 134      const ws = contextMenu.workspace;
 135      const items: Array<{ id: string; label: string; icon?: string; danger?: boolean; separator?: boolean; disabled?: boolean }> = [];
 136  
 137      // "Switch to Context" only for parent workspaces (contexts)
 138      if (ws.type === 'parent') {
 139        const childCount = workspaces.filter(w => w.type === 'child' && (w as ChildWorkspace).parentId === ws.id).length;
 140        const allOpen = areAllChildrenOpen(ws);
 141        items.push({
 142          id: 'switch-context',
 143          label: allOpen ? 'All Windows Open' : 'Switch to Context',
 144          icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>',
 145          disabled: childCount === 0 || allOpen
 146        });
 147        items.push({
 148          id: 'open-all-windows',
 149          label: 'Open All in New Windows',
 150          icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/></svg>',
 151          disabled: childCount === 0 || allOpen
 152        });
 153        items.push({ id: 'sep0', label: '', separator: true });
 154      }
 155  
 156      // "Open in New Window" for standalone and child workspaces (they have tabs)
 157      if (ws.type !== 'parent') {
 158        items.push({
 159          id: 'open-new-window',
 160          label: 'Open in New Window',
 161          icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>'
 162        });
 163        items.push({ id: 'sep0', label: '', separator: true });
 164      }
 165  
 166      items.push({
 167        id: 'rename',
 168        label: 'Rename',
 169        icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>'
 170      });
 171  
 172      // Add "Move to Parent" for standalones and children
 173      if (ws.type === 'standalone' || ws.type === 'child') {
 174        items.push({
 175          id: 'move',
 176          label: ws.type === 'child' ? 'Move to Different Parent' : 'Add to Parent',
 177          icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>'
 178        });
 179      }
 180  
 181      // Add "Convert to Parent" for standalones
 182      if (ws.type === 'standalone') {
 183        items.push({
 184          id: 'convert-to-parent',
 185          label: 'Convert to Parent',
 186          icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"/></svg>'
 187        });
 188      }
 189  
 190      // Add "Make Standalone" for children
 191      if (ws.type === 'child') {
 192        items.push({
 193          id: 'make-standalone',
 194          label: 'Make Standalone',
 195          icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>'
 196        });
 197      }
 198  
 199      items.push({ id: 'sep1', label: '', separator: true });
 200      items.push({
 201        id: 'delete',
 202        label: 'Delete',
 203        icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>',
 204        danger: true
 205      });
 206  
 207      return items;
 208    });
 209  
 210    // Message listener for tab updates from background
 211    function handleTabUpdates(message: { type: string; payload?: { workspaceId: string; windowId: number; tabCount: number } }) {
 212      if (message.type === 'TABS_UPDATED') {
 213        // Reload workspaces when tabs are auto-saved
 214        loadWorkspaces();
 215      }
 216    }
 217  
 218    onMount(async () => {
 219      // Check if dashboard tab exists in current window
 220      const dashboardUrl = browser.runtime.getURL('/workspace-dashboard.html');
 221      const currentWindowTabs = await browser.tabs.query({ currentWindow: true });
 222      const dashboardTab = currentWindowTabs.find(tab => tab.url?.startsWith(dashboardUrl));
 223  
 224      if (!dashboardTab) {
 225        // No dashboard in current window - open and pin one, then close popup
 226        const newTab = await browser.tabs.create({
 227          url: dashboardUrl,
 228          pinned: true,
 229          active: true,
 230        });
 231        // Move the pinned tab to the beginning
 232        if (newTab.id) {
 233          await browser.tabs.move(newTab.id, { index: 0 });
 234        }
 235        window.close();
 236        return;
 237      }
 238  
 239      // Parse dashboard tab URL to get workspace assignment
 240      if (dashboardTab.url) {
 241        try {
 242          const url = new URL(dashboardTab.url);
 243          const wsId = url.searchParams.get('workspaceId');
 244          if (wsId) {
 245            currentWindowWorkspaceId = wsId;
 246          }
 247        } catch {
 248          // Invalid URL, ignore
 249        }
 250      }
 251  
 252      // Dashboard exists - load workspaces and show popup
 253      await loadWorkspaces();
 254  
 255      // Listen for tab updates from background (auto-save sync)
 256      browser.runtime.onMessage.addListener(handleTabUpdates);
 257  
 258      return () => {
 259        browser.runtime.onMessage.removeListener(handleTabUpdates);
 260      };
 261    });
 262  
 263    async function loadWorkspaces() {
 264      loading = true;
 265      error = null;
 266      try {
 267        const response = await browser.runtime.sendMessage({ type: 'GET_WORKSPACES' });
 268        workspaces = response || [];
 269        // Also scan for open windows and playing media after loading workspaces
 270        await scanOpenWindows();
 271        await scanForPlayingMedia();
 272      } catch (e) {
 273        error = e instanceof Error ? e.message : 'Failed to load workspaces';
 274      } finally {
 275        loading = false;
 276      }
 277    }
 278  
 279    async function scanOpenWindows() {
 280      try {
 281        const windows = await browser.windows.getAll({ populate: true });
 282        const dashboardBaseUrl = browser.runtime.getURL('workspace-dashboard.html');
 283        const openIds = new Set<string>();
 284        const openParentIds = new Set<string>();
 285  
 286        for (const win of windows) {
 287          if (!win.tabs) continue;
 288          for (const tab of win.tabs) {
 289            if (tab.url?.startsWith(dashboardBaseUrl)) {
 290              const url = new URL(tab.url);
 291              const wsId = url.searchParams.get('workspaceId');
 292              const wsParentId = url.searchParams.get('parentId');
 293              if (wsId) {
 294                openIds.add(wsId);
 295                if (wsParentId) {
 296                  openParentIds.add(wsParentId);
 297                }
 298              }
 299            }
 300          }
 301        }
 302  
 303        // Determine active context
 304        let detectedContext: string | null = null;
 305        if (openParentIds.size === 1) {
 306          detectedContext = [...openParentIds][0];
 307        }
 308  
 309        openWorkspaceIds = openIds;
 310        activeContextId = detectedContext;
 311      } catch (e) {
 312        console.error('Failed to scan open windows:', e);
 313      }
 314    }
 315  
 316    async function scanForPlayingMedia() {
 317      try {
 318        const allTabs = await browser.tabs.query({ audible: true });
 319        if (allTabs.length > 0) {
 320          // Get the first audible tab
 321          const tab = allTabs[0];
 322          if (tab.id && tab.windowId) {
 323            playingTab = {
 324              tabId: tab.id,
 325              windowId: tab.windowId,
 326              title: tab.title || 'Playing media',
 327              favIconUrl: tab.favIconUrl,
 328            };
 329          }
 330        } else {
 331          playingTab = null;
 332        }
 333      } catch (e) {
 334        console.error('Failed to scan for playing media:', e);
 335        playingTab = null;
 336      }
 337    }
 338  
 339    async function goToPlayingTab() {
 340      if (!playingTab) return;
 341      try {
 342        // Focus the window first
 343        await browser.windows.update(playingTab.windowId, { focused: true });
 344        // Then activate the tab
 345        await browser.tabs.update(playingTab.tabId, { active: true });
 346        window.close();
 347      } catch (e) {
 348        console.error('Failed to go to playing tab:', e);
 349      }
 350    }
 351  
 352    function openDashboard() {
 353      browser.tabs.create({
 354        url: browser.runtime.getURL('/workspace-dashboard.html'),
 355      });
 356      window.close();
 357    }
 358  
 359    function openOptions() {
 360      browser.runtime.openOptionsPage();
 361    }
 362  
 363    async function saveCurrentTabs() {
 364      console.log('[Popup] saveCurrentTabs called');
 365      savingTabs = true;
 366      try {
 367        const tabs = await browser.tabs.query({ currentWindow: true });
 368        const tabData: SavedTab[] = tabs
 369          .filter(tab => tab.url && !tab.url.startsWith('chrome-extension://') && !tab.url.startsWith('moz-extension://'))
 370          .map(tab => ({
 371            url: tab.url!,
 372            title: tab.title || tab.url!,
 373            pinned: tab.pinned || false,
 374          }));
 375  
 376        console.log('[Popup] Captured', tabData.length, 'tabs');
 377  
 378        if (tabData.length === 0) {
 379          error = 'No tabs to save';
 380          savingTabs = false;
 381          return;
 382        }
 383  
 384        pendingTabs = tabData;
 385        console.log('[Popup] Opening SaveTabsModal with', pendingTabs.length, 'tabs');
 386        showSaveModal = true;
 387      } catch (e) {
 388        console.error('[Popup] Error in saveCurrentTabs:', e);
 389        error = e instanceof Error ? e.message : 'Failed to get tabs';
 390      } finally {
 391        savingTabs = false;
 392      }
 393    }
 394  
 395    async function handleSaveWorkspace(name: string, parentId: string | null, options: { openInContextSwitch: boolean; syncEnabled: boolean }) {
 396      try {
 397        let newWorkspace: Workspace | null = null;
 398  
 399        // Convert proxy objects to plain objects for Firefox compatibility
 400        const tabs = JSON.parse(JSON.stringify(pendingTabs));
 401  
 402        if (parentId) {
 403          const result = await browser.runtime.sendMessage({
 404            type: 'CREATE_CHILD',
 405            payload: { parentId, name, tabs, openInContextSwitch: options.openInContextSwitch, syncEnabled: options.syncEnabled },
 406          });
 407          if (!result.success) throw new Error(result.error || 'Failed to create child workspace');
 408          newWorkspace = result.workspace;
 409        } else {
 410          const result = await browser.runtime.sendMessage({
 411            type: 'CREATE_STANDALONE',
 412            payload: { name, tabs, openInContextSwitch: options.openInContextSwitch, syncEnabled: options.syncEnabled },
 413          });
 414          if (!result.success) throw new Error(result.error || 'Failed to create standalone workspace');
 415          newWorkspace = result.workspace;
 416        }
 417  
 418        // Auto-assign window: Update the dashboard tab URL to reflect the new workspace assignment
 419        if (newWorkspace) {
 420          const dashboardUrl = browser.runtime.getURL('/workspace-dashboard.html');
 421          const currentWindowTabs = await browser.tabs.query({ currentWindow: true });
 422          const dashboardTab = currentWindowTabs.find(tab => tab.url?.startsWith(dashboardUrl));
 423  
 424          if (dashboardTab?.id) {
 425            // Build new URL with workspace assignment
 426            const newDashboardUrl = new URL(dashboardUrl);
 427            newDashboardUrl.searchParams.set('workspaceId', newWorkspace.id);
 428            newDashboardUrl.searchParams.set('type', newWorkspace.type);
 429            if (newWorkspace.isTranscendent) {
 430              newDashboardUrl.searchParams.set('transcendent', 'true');
 431            }
 432            if (newWorkspace.type === 'child' && (newWorkspace as ChildWorkspace).parentId) {
 433              newDashboardUrl.searchParams.set('parentId', (newWorkspace as ChildWorkspace).parentId);
 434            }
 435  
 436            await browser.tabs.update(dashboardTab.id, { url: newDashboardUrl.toString() });
 437            currentWindowWorkspaceId = newWorkspace.id;
 438          }
 439  
 440          // Auto-generate summary in background (fire and forget)
 441          browser.runtime.sendMessage({
 442            type: 'GENERATE_WORKSPACE_SUMMARY',
 443            payload: { workspaceId: newWorkspace.id },
 444          }).catch(() => {
 445            // Silently ignore errors - summary generation is optional
 446          });
 447        }
 448  
 449        await loadWorkspaces();
 450        showSaveModal = false;
 451        pendingTabs = [];
 452      } catch (e) {
 453        error = e instanceof Error ? e.message : 'Failed to save workspace';
 454      }
 455    }
 456  
 457    function handleCancelSave() {
 458      showSaveModal = false;
 459      pendingTabs = [];
 460    }
 461  
 462    function selectWorkspace(workspace: Workspace) {
 463      browser.tabs.create({
 464        url: browser.runtime.getURL(`/workspace-dashboard.html?workspaceId=${workspace.id}&type=${workspace.type}`),
 465      });
 466      window.close();
 467    }
 468  
 469    // Context menu handlers
 470    function handleContextMenu(e: MouseEvent, workspace: Workspace) {
 471      e.preventDefault();
 472      e.stopPropagation();
 473      contextMenu = { workspace, x: e.clientX, y: e.clientY };
 474    }
 475  
 476    function handleContextMenuSelect(id: string) {
 477      if (!contextMenu) return;
 478      const ws = contextMenu.workspace;
 479  
 480      switch (id) {
 481        case 'switch-context':
 482          switchToContext(ws);
 483          break;
 484        case 'open-all-windows':
 485          openAllInNewWindows(ws);
 486          break;
 487        case 'open-new-window':
 488          openInNewWindow(ws);
 489          break;
 490        case 'rename':
 491          workspaceToRename = ws;
 492          renameValue = ws.name;
 493          renameModalOpen = true;
 494          break;
 495        case 'move':
 496          workspaceToMove = ws;
 497          selectedParentId = ws.type === 'child' ? (ws as ChildWorkspace).parentId : null;
 498          moveModalOpen = true;
 499          break;
 500        case 'make-standalone':
 501          makeStandalone(ws);
 502          break;
 503        case 'convert-to-parent':
 504          convertToParent(ws);
 505          break;
 506        case 'delete':
 507          // Check if this is a parent with transcendent children
 508          if (ws.type === 'parent') {
 509            const transcendentChildren = workspaces.filter(
 510              w => w.type === 'child' &&
 511              (w as ChildWorkspace).parentId === ws.id &&
 512              w.isTranscendent
 513            );
 514            if (transcendentChildren.length > 0) {
 515              error = `Cannot delete: has ${transcendentChildren.length} transcendent child workspace(s)`;
 516              break;
 517            }
 518          }
 519          workspaceToDelete = ws;
 520          deleteConfirmOpen = true;
 521          break;
 522      }
 523  
 524      contextMenu = null;
 525    }
 526  
 527    async function handleRename() {
 528      if (!workspaceToRename || !renameValue.trim()) return;
 529  
 530      const trimmedName = renameValue.trim();
 531  
 532      // Skip validation if name hasn't changed
 533      if (trimmedName.toLowerCase() === workspaceToRename.name.toLowerCase()) {
 534        renameModalOpen = false;
 535        return;
 536      }
 537  
 538      // Check for duplicate name within the same scope
 539      const duplicateExists = workspaces.some(ws => {
 540        if (ws.id === workspaceToRename!.id) return false;
 541        if (ws.name.toLowerCase() === trimmedName.toLowerCase()) {
 542          if (workspaceToRename!.type === 'child' && ws.type === 'child') {
 543            return (ws as ChildWorkspace).parentId === (workspaceToRename as ChildWorkspace).parentId;
 544          }
 545          if (workspaceToRename!.type !== 'child' && ws.type !== 'child') {
 546            return true;
 547          }
 548        }
 549        return false;
 550      });
 551  
 552      if (duplicateExists) {
 553        error = `A workspace named "${trimmedName}" already exists`;
 554        return;
 555      }
 556  
 557      try {
 558        await browser.runtime.sendMessage({
 559          type: 'RENAME_WORKSPACE',
 560          payload: { id: workspaceToRename.id, name: trimmedName }
 561        });
 562        await loadWorkspaces();
 563        renameModalOpen = false;
 564      } catch (e) {
 565        error = e instanceof Error ? e.message : 'Failed to rename workspace';
 566      }
 567    }
 568  
 569    async function handleMove() {
 570      if (!workspaceToMove || !selectedParentId) return;
 571  
 572      try {
 573        await browser.runtime.sendMessage({
 574          type: 'MOVE_WORKSPACE',
 575          payload: { id: workspaceToMove.id, newParentId: selectedParentId }
 576        });
 577        await loadWorkspaces();
 578        moveModalOpen = false;
 579      } catch (e) {
 580        error = e instanceof Error ? e.message : 'Failed to move workspace';
 581      }
 582    }
 583  
 584    async function makeStandalone(workspace: Workspace) {
 585      try {
 586        await browser.runtime.sendMessage({
 587          type: 'MOVE_WORKSPACE',
 588          payload: { id: workspace.id, newParentId: null }
 589        });
 590        await loadWorkspaces();
 591      } catch (e) {
 592        error = e instanceof Error ? e.message : 'Failed to make standalone';
 593      }
 594    }
 595  
 596    async function convertToParent(workspace: Workspace) {
 597      if (workspace.type !== 'standalone') return;
 598  
 599      try {
 600        await browser.runtime.sendMessage({
 601          type: 'CONVERT_TO_PARENT',
 602          payload: { id: workspace.id }
 603        });
 604        await loadWorkspaces();
 605      } catch (e) {
 606        error = e instanceof Error ? e.message : 'Failed to convert to parent';
 607      }
 608    }
 609  
 610    async function calculateContextSwitchImpact(): Promise<{ windowsToClose: number; transcendentToKeep: number; windowIds: number[] }> {
 611      const windows = await browser.windows.getAll({ populate: true });
 612      const dashboardBaseUrl = browser.runtime.getURL('workspace-dashboard.html');
 613      const transcendentIds = new Set(workspaces.filter(ws => ws.isTranscendent).map(ws => ws.id));
 614  
 615      let windowsToClose = 0;
 616      let transcendentToKeep = 0;
 617      const windowIdsToClose: number[] = [];
 618  
 619      for (const win of windows) {
 620        if (!win.tabs || !win.id) continue;
 621  
 622        const dashboardTab = win.tabs.find(tab => tab.url?.startsWith(dashboardBaseUrl));
 623        if (dashboardTab) {
 624          const url = new URL(dashboardTab.url!);
 625          const wsId = url.searchParams.get('workspaceId');
 626          if (wsId && transcendentIds.has(wsId)) {
 627            transcendentToKeep++;
 628            continue;
 629          }
 630        }
 631  
 632        windowsToClose++;
 633        windowIdsToClose.push(win.id);
 634      }
 635  
 636      return { windowsToClose, transcendentToKeep, windowIds: windowIdsToClose };
 637    }
 638  
 639    async function initiateContextSwitch(workspace: Workspace) {
 640      if (workspace.type !== 'parent') {
 641        error = 'Context switch is only available for parent workspaces';
 642        return;
 643      }
 644  
 645      const children = workspaces.filter(w => w.type === 'child' && (w as ChildWorkspace).parentId === workspace.id);
 646      if (children.length === 0) {
 647        error = 'This parent context has no child workspaces';
 648        return;
 649      }
 650  
 651      const impact = await calculateContextSwitchImpact();
 652      contextSwitchImpact = { windowsToClose: impact.windowsToClose, transcendentToKeep: impact.transcendentToKeep };
 653      pendingContextSwitch = workspace;
 654      contextSwitchConfirmOpen = true;
 655    }
 656  
 657    async function executeContextSwitch() {
 658      if (!pendingContextSwitch || pendingContextSwitch.type !== 'parent') return;
 659  
 660      try {
 661        // Only open children that have openInContextSwitch enabled (or undefined, defaulting to true)
 662        const children = workspaces.filter(w =>
 663          w.type === 'child' &&
 664          (w as ChildWorkspace).parentId === pendingContextSwitch!.id &&
 665          (w.openInContextSwitch !== false) // Default to true if not set
 666        );
 667        const impact = await calculateContextSwitchImpact();
 668  
 669        // Close non-transcendent windows
 670        const currentWindow = await browser.windows.getCurrent();
 671        for (const windowId of impact.windowIds) {
 672          if (windowId !== currentWindow.id) {
 673            try {
 674              await browser.windows.remove(windowId);
 675            } catch (e) {
 676              console.warn('Failed to close window:', windowId, e);
 677            }
 678          }
 679        }
 680  
 681        // Open child workspaces in new windows with dashboard tabs
 682        for (const child of children) {
 683          const childTabs = (child as { tabs?: { url: string; title: string; pinned?: boolean }[] }).tabs || [];
 684          if (childTabs.length > 0) {
 685            const dashboardUrl = new URL(browser.runtime.getURL('workspace-dashboard.html'));
 686            dashboardUrl.searchParams.set('workspaceId', child.id);
 687            dashboardUrl.searchParams.set('type', 'child');
 688            dashboardUrl.searchParams.set('transcendent', child.isTranscendent.toString());
 689            dashboardUrl.searchParams.set('parentId', (child as ChildWorkspace).parentId);
 690  
 691            const newWindow = await browser.windows.create({
 692              url: dashboardUrl.toString(),
 693              focused: false,
 694            });
 695  
 696            if (newWindow.id) {
 697              const tabs = await browser.tabs.query({ windowId: newWindow.id });
 698              if (tabs[0]?.id) {
 699                await browser.tabs.update(tabs[0].id, { pinned: true });
 700              }
 701  
 702              for (const tab of childTabs) {
 703                await browser.tabs.create({
 704                  windowId: newWindow.id,
 705                  url: tab.url,
 706                  pinned: tab.pinned || false,
 707                  active: false,
 708                });
 709              }
 710            }
 711          }
 712        }
 713  
 714        // Save active context
 715        await browser.storage.local.set({ activeContextId: pendingContextSwitch.id });
 716  
 717        // Close current window if needed
 718        if (currentWindow.id && impact.windowIds.includes(currentWindow.id)) {
 719          await browser.windows.remove(currentWindow.id);
 720        } else {
 721          window.close();
 722        }
 723      } catch (e) {
 724        error = e instanceof Error ? e.message : 'Failed to switch context';
 725      } finally {
 726        contextSwitchConfirmOpen = false;
 727        pendingContextSwitch = null;
 728      }
 729    }
 730  
 731    function cancelContextSwitch() {
 732      contextSwitchConfirmOpen = false;
 733      pendingContextSwitch = null;
 734    }
 735  
 736    async function switchToContext(workspace: Workspace) {
 737      await initiateContextSwitch(workspace);
 738    }
 739  
 740    async function openInNewWindow(workspace: Workspace) {
 741      if (workspace.type === 'parent') return;
 742  
 743      const tabs = (workspace as { tabs?: { url: string; title: string; pinned?: boolean }[] }).tabs || [];
 744  
 745      try {
 746        // Create dashboard URL for this workspace
 747        const dashboardUrl = new URL(browser.runtime.getURL('workspace-dashboard.html'));
 748        dashboardUrl.searchParams.set('workspaceId', workspace.id);
 749        dashboardUrl.searchParams.set('type', workspace.type);
 750        dashboardUrl.searchParams.set('transcendent', workspace.isTranscendent.toString());
 751        if (workspace.type === 'child') {
 752          dashboardUrl.searchParams.set('parentId', (workspace as ChildWorkspace).parentId);
 753        }
 754  
 755        // Create new window with dashboard tab
 756        const newWindow = await browser.windows.create({
 757          url: dashboardUrl.toString(),
 758          focused: true,
 759        });
 760  
 761        if (newWindow.id) {
 762          // Pin the dashboard tab
 763          const windowTabs = await browser.tabs.query({ windowId: newWindow.id });
 764          if (windowTabs[0]?.id) {
 765            await browser.tabs.update(windowTabs[0].id, { pinned: true });
 766          }
 767  
 768          // Add workspace tabs
 769          for (const tab of tabs) {
 770            await browser.tabs.create({
 771              windowId: newWindow.id,
 772              url: tab.url,
 773              pinned: tab.pinned || false,
 774              active: false,
 775            });
 776          }
 777        }
 778  
 779        window.close();
 780      } catch (e) {
 781        error = e instanceof Error ? e.message : 'Failed to open in new window';
 782      }
 783    }
 784  
 785    async function openAllInNewWindows(parentWorkspace: Workspace) {
 786      if (parentWorkspace.type !== 'parent') return;
 787  
 788      const children = workspaces.filter(
 789        w => w.type === 'child' && (w as ChildWorkspace).parentId === parentWorkspace.id
 790      );
 791  
 792      if (children.length === 0) {
 793        error = 'This parent has no child workspaces';
 794        return;
 795      }
 796  
 797      try {
 798        for (const child of children) {
 799          const childTabs = (child as { tabs?: { url: string; title: string; pinned?: boolean }[] }).tabs || [];
 800  
 801          // Create dashboard URL for this child
 802          const dashboardUrl = new URL(browser.runtime.getURL('workspace-dashboard.html'));
 803          dashboardUrl.searchParams.set('workspaceId', child.id);
 804          dashboardUrl.searchParams.set('type', 'child');
 805          dashboardUrl.searchParams.set('transcendent', child.isTranscendent.toString());
 806          dashboardUrl.searchParams.set('parentId', (child as ChildWorkspace).parentId);
 807  
 808          // Create new window with dashboard tab
 809          const newWindow = await browser.windows.create({
 810            url: dashboardUrl.toString(),
 811            focused: false,
 812          });
 813  
 814          if (newWindow.id) {
 815            // Pin the dashboard tab
 816            const windowTabs = await browser.tabs.query({ windowId: newWindow.id });
 817            if (windowTabs[0]?.id) {
 818              await browser.tabs.update(windowTabs[0].id, { pinned: true });
 819            }
 820  
 821            // Add workspace tabs
 822            for (const tab of childTabs) {
 823              await browser.tabs.create({
 824                windowId: newWindow.id,
 825                url: tab.url,
 826                pinned: tab.pinned || false,
 827                active: false,
 828              });
 829            }
 830          }
 831        }
 832  
 833        window.close();
 834      } catch (e) {
 835        error = e instanceof Error ? e.message : 'Failed to open windows';
 836      }
 837    }
 838  
 839    async function handleDelete() {
 840      if (!workspaceToDelete) return;
 841  
 842      try {
 843        await browser.runtime.sendMessage({
 844          type: 'DELETE_WORKSPACE',
 845          payload: { id: workspaceToDelete.id, keepChildren: true }
 846        });
 847        await loadWorkspaces();
 848      } catch (e) {
 849        error = e instanceof Error ? e.message : 'Failed to delete workspace';
 850      }
 851    }
 852  
 853    // Drag and drop handlers
 854    function handleDragStart(e: DragEvent, workspace: Workspace) {
 855      if (workspace.type === 'parent') {
 856        e.preventDefault();
 857        return;
 858      }
 859      draggedWorkspace = workspace;
 860      e.dataTransfer?.setData('text/plain', workspace.id);
 861      if (e.dataTransfer) {
 862        e.dataTransfer.effectAllowed = 'move';
 863      }
 864    }
 865  
 866    function handleDragOver(e: DragEvent, targetId: string, position: 'inside' | 'standalone') {
 867      if (!draggedWorkspace) return;
 868      if (draggedWorkspace.id === targetId) return;
 869  
 870      e.preventDefault();
 871      if (e.dataTransfer) {
 872        e.dataTransfer.dropEffect = 'move';
 873      }
 874      dropTargetId = targetId;
 875      dropPosition = position;
 876    }
 877  
 878    function handleDragLeave() {
 879      dropTargetId = null;
 880      dropPosition = null;
 881    }
 882  
 883    async function handleDrop(e: DragEvent, targetId: string, position: 'inside' | 'standalone') {
 884      e.preventDefault();
 885      if (!draggedWorkspace) return;
 886  
 887      try {
 888        if (position === 'inside') {
 889          // Move into parent
 890          await browser.runtime.sendMessage({
 891            type: 'MOVE_WORKSPACE',
 892            payload: { id: draggedWorkspace.id, newParentId: targetId }
 893          });
 894        } else {
 895          // Make standalone (dropped on standalone zone)
 896          await browser.runtime.sendMessage({
 897            type: 'MOVE_WORKSPACE',
 898            payload: { id: draggedWorkspace.id, newParentId: null }
 899          });
 900        }
 901        await loadWorkspaces();
 902      } catch (e) {
 903        error = e instanceof Error ? (e as Error).message : 'Failed to move workspace';
 904      } finally {
 905        draggedWorkspace = null;
 906        dropTargetId = null;
 907        dropPosition = null;
 908      }
 909    }
 910  
 911    function handleDragEnd() {
 912      draggedWorkspace = null;
 913      dropTargetId = null;
 914      dropPosition = null;
 915    }
 916  
 917    function isDraggable(ws: Workspace): boolean {
 918      return ws.type === 'standalone' || ws.type === 'child';
 919    }
 920  </script>
 921  
 922  <div class="popup">
 923    <!-- Header -->
 924    <header class="popup-header">
 925      <div class="flex items-center gap-2">
 926        <div class="logo-mark">M</div>
 927        <div>
 928          <h1 class="popup-title">Mnemonic</h1>
 929          <p class="popup-subtitle">Workspace Manager</p>
 930        </div>
 931      </div>
 932      {#if playingTab}
 933        <button class="go-to-playing" onclick={goToPlayingTab} title={playingTab.title}>
 934          <svg class="playing-icon" viewBox="0 0 24 24" fill="currentColor">
 935            <path d="M8 5v14l11-7z"/>
 936          </svg>
 937          <span>Go to Playing</span>
 938        </button>
 939      {/if}
 940    </header>
 941  
 942    <main class="popup-content">
 943      {#if loading}
 944        <div class="loading">
 945          <div class="loading-spinner"></div>
 946          <span class="loading-text">Loading...</span>
 947        </div>
 948      {:else if error}
 949        <div class="error">
 950          <svg class="w-5 h-5 mb-2" fill="currentColor" viewBox="0 0 20 20">
 951            <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
 952          </svg>
 953          <span>{error}</span>
 954          <button class="retry-btn" onclick={() => { error = null; loadWorkspaces(); }}>Retry</button>
 955        </div>
 956      {:else}
 957        <!-- Quick Actions -->
 958        <div class="quick-actions">
 959          {#if currentWindowWorkspace}
 960            <div class="active-workspace-display">
 961              <svg class="w-5 h-5 text-phosphor" fill="none" stroke="currentColor" viewBox="0 0 24 24">
 962                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
 963              </svg>
 964              <span class="active-workspace-name">{currentWindowWorkspace.name}</span>
 965              {#if currentWindowWorkspace.isTranscendent}
 966                <svg class="w-4 h-4 text-phosphor" fill="currentColor" viewBox="0 0 20 20">
 967                  <path d="M14.5 10c0 1.38-1.12 2.5-2.5 2.5-.83 0-1.57-.41-2.02-1.03L8.5 10l1.48-1.47A2.49 2.49 0 0112 7.5c1.38 0 2.5 1.12 2.5 2.5z" />
 968                </svg>
 969              {/if}
 970            </div>
 971          {:else}
 972            <button class="quick-action" onclick={saveCurrentTabs} disabled={savingTabs}>
 973              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
 974                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
 975              </svg>
 976              <span>{savingTabs ? 'Saving...' : 'Save Current Tabs'}</span>
 977            </button>
 978          {/if}
 979        </div>
 980  
 981        <!-- Transcendent Workspaces -->
 982        {#if transcendentWorkspaces.length > 0}
 983          <div class="transcendent-section">
 984            <h2 class="section-title section-title-transcendent">
 985              <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
 986                <path d="M14.5 10c0 1.38-1.12 2.5-2.5 2.5-.83 0-1.57-.41-2.02-1.03L8.5 10l1.48-1.47A2.49 2.49 0 0112 7.5c1.38 0 2.5 1.12 2.5 2.5zm-9 0c0-1.38 1.12-2.5 2.5-2.5.83 0 1.57.41 2.02 1.03L11.5 10l-1.48 1.47A2.49 2.49 0 018 12.5c-1.38 0-2.5-1.12-2.5-2.5zm4.5 0l2-2c.78-.78 1.81-1.17 2.83-1.17 2.21 0 4 1.79 4 4s-1.79 4-4 4c-1.02 0-2.05-.39-2.83-1.17l-2-2-2 2c-.78.78-1.81 1.17-2.83 1.17-2.21 0-4-1.79-4-4s1.79-4 4-4c1.02 0 2.05.39 2.83 1.17l2 2z" />
 987              </svg>
 988              Transcendent
 989            </h2>
 990            <div class="transcendent-list">
 991              {#each transcendentWorkspaces as ws (ws.id)}
 992                <button
 993                  class="transcendent-item"
 994                  class:dragging={draggedWorkspace?.id === ws.id}
 995                  draggable={isDraggable(ws)}
 996                  onclick={() => selectWorkspace(ws)}
 997                  oncontextmenu={(e) => handleContextMenu(e, ws)}
 998                  ondragstart={(e) => handleDragStart(e, ws)}
 999                  ondragend={handleDragEnd}
1000                >
1001                  <svg class="w-4 h-4 text-phosphor" fill="currentColor" viewBox="0 0 20 20">
1002                    <path d="M14.5 10c0 1.38-1.12 2.5-2.5 2.5-.83 0-1.57-.41-2.02-1.03L8.5 10l1.48-1.47A2.49 2.49 0 0112 7.5c1.38 0 2.5 1.12 2.5 2.5zm-9 0c0-1.38 1.12-2.5 2.5-2.5.83 0 1.57.41 2.02 1.03L11.5 10l-1.48 1.47A2.49 2.49 0 018 12.5c-1.38 0-2.5-1.12-2.5-2.5zm4.5 0l2-2c.78-.78 1.81-1.17 2.83-1.17 2.21 0 4 1.79 4 4s-1.79 4-4 4c-1.02 0-2.05-.39-2.83-1.17l-2-2-2 2c-.78.78-1.81 1.17-2.83 1.17-2.21 0-4-1.79-4-4s1.79-4 4-4c1.02 0 2.05.39 2.83 1.17l2 2z" />
1003                  </svg>
1004                  <span class="transcendent-name">{ws.name}</span>
1005                </button>
1006              {/each}
1007            </div>
1008          </div>
1009        {/if}
1010  
1011        <!-- Parent Workspaces (Contexts) with drop zones -->
1012        {#if parentWorkspaces.length > 0}
1013          <div class="section">
1014            <h2 class="section-title">Contexts</h2>
1015            <div class="parent-list">
1016              {#each parentWorkspaces as parent (parent.id)}
1017                {@const children = workspaces.filter(ws => ws.type === 'child' && (ws as ChildWorkspace).parentId === parent.id)}
1018                {@const isExpanded = expandedParents.has(parent.id)}
1019                <div
1020                  class="parent-item"
1021                  class:drop-target={dropTargetId === parent.id && dropPosition === 'inside'}
1022                  role="listitem"
1023                  ondragover={(e) => handleDragOver(e, parent.id, 'inside')}
1024                  ondragleave={handleDragLeave}
1025                  ondrop={(e) => handleDrop(e, parent.id, 'inside')}
1026                >
1027                  <div
1028                    class="parent-header"
1029                    role="group"
1030                    oncontextmenu={(e) => handleContextMenu(e, parent)}
1031                  >
1032                    <button
1033                      class="parent-toggle"
1034                      onclick={() => toggleParentExpanded(parent.id)}
1035                      title={isExpanded ? "Collapse" : "Expand"}
1036                    >
1037                      <svg class="chevron-icon" class:expanded={isExpanded} fill="none" stroke="currentColor" viewBox="0 0 24 24">
1038                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
1039                      </svg>
1040                    </button>
1041                    {#if isWorkspaceOpen(parent)}
1042                      <span class="open-indicator" title="{getOpenChildCount(parent)} window(s) open"></span>
1043                    {/if}
1044                    <button class="parent-name-btn" onclick={() => selectWorkspace(parent)}>
1045                      <svg class="w-4 h-4 text-phosphor" fill="currentColor" viewBox="0 0 20 20">
1046                        <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
1047                      </svg>
1048                      <span class="parent-name">{parent.name}</span>
1049                    </button>
1050                    <span class="parent-count">{children.length}</span>
1051                  </div>
1052                  {#if isExpanded && children.length > 0}
1053                    <div class="children-list">
1054                      {#each children as child (child.id)}
1055                        <button
1056                          class="child-item"
1057                          class:dragging={draggedWorkspace?.id === child.id}
1058                          class:is-open={isWorkspaceOpen(child)}
1059                          draggable={true}
1060                          onclick={() => selectWorkspace(child)}
1061                          oncontextmenu={(e) => handleContextMenu(e, child)}
1062                          ondragstart={(e) => handleDragStart(e, child)}
1063                          ondragend={handleDragEnd}
1064                        >
1065                          {#if isWorkspaceOpen(child)}
1066                            <span class="open-indicator" title="Window open"></span>
1067                          {/if}
1068                          <span class="child-name">{child.name}</span>
1069                          <span class="child-tabs">{(child as {tabs: unknown[]}).tabs?.length || 0} tabs</span>
1070                        </button>
1071                      {/each}
1072                    </div>
1073                  {/if}
1074                </div>
1075              {/each}
1076            </div>
1077          </div>
1078        {/if}
1079  
1080        <!-- Standalone Workspaces with drop zone -->
1081        <div
1082          class="section standalone-section"
1083          class:drop-target={dropPosition === 'standalone'}
1084          role="region"
1085          aria-label="Standalone workspaces"
1086          ondragover={(e) => { if (draggedWorkspace?.type === 'child') handleDragOver(e, 'standalone-zone', 'standalone'); }}
1087          ondragleave={handleDragLeave}
1088          ondrop={(e) => handleDrop(e, 'standalone-zone', 'standalone')}
1089        >
1090          <h2 class="section-title">Standalone</h2>
1091          {#if workspaces.filter(ws => ws.type === 'standalone').length > 0}
1092            <div class="standalone-list">
1093              {#each workspaces.filter(ws => ws.type === 'standalone') as ws (ws.id)}
1094                <button
1095                  class="standalone-item"
1096                  class:dragging={draggedWorkspace?.id === ws.id}
1097                  class:transcendent={ws.isTranscendent}
1098                  class:is-open={isWorkspaceOpen(ws)}
1099                  draggable={true}
1100                  onclick={() => selectWorkspace(ws)}
1101                  oncontextmenu={(e) => handleContextMenu(e, ws)}
1102                  ondragstart={(e) => handleDragStart(e, ws)}
1103                  ondragend={handleDragEnd}
1104                >
1105                  {#if isWorkspaceOpen(ws)}
1106                    <span class="open-indicator" title="Window open"></span>
1107                  {/if}
1108                  {#if ws.isTranscendent}
1109                    <svg class="w-4 h-4 text-phosphor" fill="currentColor" viewBox="0 0 20 20">
1110                      <path d="M14.5 10c0 1.38-1.12 2.5-2.5 2.5-.83 0-1.57-.41-2.02-1.03L8.5 10l1.48-1.47A2.49 2.49 0 0112 7.5c1.38 0 2.5 1.12 2.5 2.5zm-9 0c0-1.38 1.12-2.5 2.5-2.5.83 0 1.57.41 2.02 1.03L11.5 10l-1.48 1.47A2.49 2.49 0 018 12.5c-1.38 0-2.5-1.12-2.5-2.5zm4.5 0l2-2c.78-.78 1.81-1.17 2.83-1.17 2.21 0 4 1.79 4 4s-1.79 4-4 4c-1.02 0-2.05-.39-2.83-1.17l-2-2-2 2c-.78.78-1.81 1.17-2.83 1.17-2.21 0-4-1.79-4-4s1.79-4 4-4c1.02 0 2.05.39 2.83 1.17l2 2z" />
1111                    </svg>
1112                  {:else}
1113                    <svg class="w-4 h-4 text-text-muted" fill="currentColor" viewBox="0 0 20 20">
1114                      <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 011.523-1.943A5.977 5.977 0 0116 10c0 .34-.028.675-.083 1H15a2 2 0 00-2 2v2.197A5.973 5.973 0 0110 16v-2a2 2 0 00-2-2 2 2 0 01-2-2 2 2 0 00-1.668-1.973z" clip-rule="evenodd" />
1115                    </svg>
1116                  {/if}
1117                  <span class="standalone-name">{ws.name}</span>
1118                  <span class="standalone-tabs">{(ws as {tabs: unknown[]}).tabs?.length || 0} tabs</span>
1119                </button>
1120              {/each}
1121            </div>
1122          {:else}
1123            <p class="empty-hint">Drop workspaces here to make standalone</p>
1124          {/if}
1125        </div>
1126  
1127        {#if parentWorkspaces.length === 0 && standaloneCount === 0 && transcendentWorkspaces.length === 0}
1128          <div class="empty-state">
1129            <p class="empty-text">No workspaces yet</p>
1130            <p class="empty-hint">Open the dashboard to create your first workspace</p>
1131          </div>
1132        {/if}
1133      {/if}
1134    </main>
1135  
1136    <!-- Footer Actions -->
1137    <footer class="popup-actions">
1138      <Button variant="primary" onclick={openDashboard}>
1139        Open Dashboard
1140      </Button>
1141      <Button variant="ghost" onclick={openOptions}>
1142        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1143          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
1144          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
1145        </svg>
1146      </Button>
1147    </footer>
1148  </div>
1149  
1150  <!-- Context Menu -->
1151  {#if contextMenu}
1152    <ContextMenu
1153      items={contextMenuItems}
1154      x={contextMenu.x}
1155      y={contextMenu.y}
1156      onSelect={handleContextMenuSelect}
1157      onClose={() => contextMenu = null}
1158    />
1159  {/if}
1160  
1161  <!-- Save Tabs Modal -->
1162  <SaveTabsModal
1163    bind:open={showSaveModal}
1164    parents={parentWorkspaces}
1165    tabs={pendingTabs}
1166    onSave={handleSaveWorkspace}
1167    onCancel={handleCancelSave}
1168  />
1169  
1170  <!-- Rename Modal -->
1171  <Modal bind:open={renameModalOpen} title="Rename Workspace">
1172    <Input
1173      bind:value={renameValue}
1174      label="New Name"
1175      placeholder="Enter new name"
1176    />
1177  
1178    {#snippet footer()}
1179      <Button variant="ghost" onclick={() => renameModalOpen = false}>Cancel</Button>
1180      <Button variant="primary" onclick={handleRename} disabled={!renameValue.trim()}>Rename</Button>
1181    {/snippet}
1182  </Modal>
1183  
1184  <!-- Move to Parent Modal -->
1185  <Modal bind:open={moveModalOpen} title="Move to Parent">
1186    <div class="move-form">
1187      <label class="field-label" for="parent-select">Select Parent Workspace</label>
1188      <select id="parent-select" class="select" bind:value={selectedParentId}>
1189        <option value={null} disabled>Choose a parent...</option>
1190        {#each parentWorkspaces.filter(p => workspaceToMove?.type !== 'child' || (workspaceToMove as ChildWorkspace).parentId !== p.id) as parent (parent.id)}
1191          <option value={parent.id}>{parent.name}</option>
1192        {/each}
1193      </select>
1194    </div>
1195  
1196    {#snippet footer()}
1197      <Button variant="ghost" onclick={() => moveModalOpen = false}>Cancel</Button>
1198      <Button variant="primary" onclick={handleMove} disabled={!selectedParentId}>Move</Button>
1199    {/snippet}
1200  </Modal>
1201  
1202  <!-- Delete Confirmation -->
1203  <ConfirmDialog
1204    bind:open={deleteConfirmOpen}
1205    title="Delete Workspace"
1206    message="Are you sure you want to delete '{workspaceToDelete?.name}'? {workspaceToDelete?.type === 'parent' ? 'Child workspaces will become standalone.' : ''}"
1207    variant="danger"
1208    confirmText="Delete"
1209    onconfirm={handleDelete}
1210  />
1211  
1212  <!-- Context Switch Confirmation -->
1213  <Modal bind:open={contextSwitchConfirmOpen} title="Switch Context">
1214    <div class="context-switch-confirm">
1215      <div class="confirm-header">
1216        <svg class="w-5 h-5 text-status-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1217          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
1218        </svg>
1219        <span class="text-sm">Switch to "{pendingContextSwitch?.name}"?</span>
1220      </div>
1221  
1222      <div class="confirm-impact">
1223        {#if contextSwitchImpact.windowsToClose > 0}
1224          <div class="impact-item text-status-error">
1225            <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1226              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
1227            </svg>
1228            <span>Close {contextSwitchImpact.windowsToClose} window{contextSwitchImpact.windowsToClose !== 1 ? 's' : ''}</span>
1229          </div>
1230        {/if}
1231        {#if contextSwitchImpact.transcendentToKeep > 0}
1232          <div class="impact-item text-phosphor">
1233            <svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
1234              <path d="M14.5 10c0 1.38-1.12 2.5-2.5 2.5-.83 0-1.57-.41-2.02-1.03L8.5 10l1.48-1.47A2.49 2.49 0 0112 7.5c1.38 0 2.5 1.12 2.5 2.5z" />
1235            </svg>
1236            <span>Keep {contextSwitchImpact.transcendentToKeep} transcendent</span>
1237          </div>
1238        {/if}
1239        <div class="impact-item text-status-success">
1240          <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1241            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
1242          </svg>
1243          <span>Open {pendingContextSwitch ? workspaces.filter(w => w.type === 'child' && (w as ChildWorkspace).parentId === pendingContextSwitch.id && w.openInContextSwitch !== false).length : 0} workspace(s)</span>
1244        </div>
1245      </div>
1246  
1247      <p class="text-xs text-text-muted">Unsaved work may be lost.</p>
1248    </div>
1249  
1250    {#snippet footer()}
1251      <Button variant="ghost" size="sm" onclick={cancelContextSwitch}>
1252        Cancel
1253      </Button>
1254      <Button variant="primary" size="sm" onclick={executeContextSwitch}>
1255        Switch
1256      </Button>
1257    {/snippet}
1258  </Modal>
1259  
1260  <style>
1261    .context-switch-confirm {
1262      display: flex;
1263      flex-direction: column;
1264      gap: 0.75rem;
1265    }
1266  
1267    .confirm-header {
1268      display: flex;
1269      align-items: center;
1270      gap: 0.5rem;
1271    }
1272  
1273    .confirm-impact {
1274      background: rgba(15, 61, 61, 0.3);
1275      border-radius: 6px;
1276      padding: 0.625rem;
1277      display: flex;
1278      flex-direction: column;
1279      gap: 0.375rem;
1280    }
1281  
1282    .impact-item {
1283      display: flex;
1284      align-items: center;
1285      gap: 0.375rem;
1286      font-size: 0.75rem;
1287    }
1288  
1289    .popup {
1290      width: 340px;
1291      min-height: 280px;
1292      max-height: 580px;
1293      display: flex;
1294      flex-direction: column;
1295      background-color: var(--color-bg);
1296      overflow: hidden;
1297      position: relative;
1298    }
1299  
1300    .popup-header {
1301      display: flex;
1302      align-items: center;
1303      justify-content: space-between;
1304      padding: 0.875rem 1rem;
1305      border-bottom: 1px solid rgba(217, 137, 46, 0.2);
1306      background-color: var(--color-bg-light);
1307    }
1308  
1309    .logo-mark {
1310      width: 32px;
1311      height: 32px;
1312      display: flex;
1313      align-items: center;
1314      justify-content: center;
1315      background-color: var(--color-phosphor);
1316      color: var(--color-bg);
1317      font-weight: bold;
1318      font-size: 1.125rem;
1319      border-radius: 6px;
1320    }
1321  
1322    .popup-title {
1323      font-size: 1rem;
1324      font-weight: 600;
1325      color: var(--color-phosphor);
1326      margin: 0;
1327      line-height: 1.2;
1328    }
1329  
1330    .popup-subtitle {
1331      font-size: 0.625rem;
1332      color: var(--color-text-muted);
1333      margin: 0;
1334      text-transform: uppercase;
1335      letter-spacing: 0.05em;
1336    }
1337  
1338    .go-to-playing {
1339      display: flex;
1340      align-items: center;
1341      gap: 0.375rem;
1342      padding: 0.375rem 0.625rem;
1343      background: rgba(16, 185, 129, 0.15);
1344      border: 1px solid rgba(16, 185, 129, 0.4);
1345      border-radius: 6px;
1346      color: #10b981;
1347      font-size: 0.6875rem;
1348      font-weight: 500;
1349      cursor: pointer;
1350      transition: all 0.15s;
1351      white-space: nowrap;
1352    }
1353  
1354    .go-to-playing:hover {
1355      background: rgba(16, 185, 129, 0.25);
1356      border-color: #10b981;
1357    }
1358  
1359    .playing-icon {
1360      width: 12px;
1361      height: 12px;
1362      animation: pulse-play 1.5s ease-in-out infinite;
1363    }
1364  
1365    @keyframes pulse-play {
1366      0%, 100% {
1367        opacity: 1;
1368      }
1369      50% {
1370        opacity: 0.5;
1371      }
1372    }
1373  
1374    .popup-content {
1375      flex: 1;
1376      overflow-y: auto;
1377      padding: 0.75rem;
1378    }
1379  
1380    .loading, .error, .empty-state {
1381      display: flex;
1382      flex-direction: column;
1383      align-items: center;
1384      justify-content: center;
1385      padding: 2rem 1rem;
1386      text-align: center;
1387    }
1388  
1389    .loading-spinner {
1390      width: 24px;
1391      height: 24px;
1392      border: 2px solid var(--color-bg-light);
1393      border-top-color: var(--color-phosphor);
1394      border-radius: 50%;
1395      animation: spin 0.8s linear infinite;
1396      margin-bottom: 0.5rem;
1397    }
1398  
1399    @keyframes spin {
1400      to { transform: rotate(360deg); }
1401    }
1402  
1403    .loading-text {
1404      color: var(--color-text-secondary);
1405      font-size: 0.875rem;
1406    }
1407  
1408    .error {
1409      color: #f87171;
1410    }
1411  
1412    .retry-btn {
1413      margin-top: 0.5rem;
1414      padding: 0.25rem 0.75rem;
1415      font-size: 0.75rem;
1416      background: transparent;
1417      border: 1px solid currentColor;
1418      border-radius: 4px;
1419      cursor: pointer;
1420      transition: background-color 0.2s;
1421    }
1422  
1423    .retry-btn:hover {
1424      background-color: rgba(248, 113, 113, 0.1);
1425    }
1426  
1427    .quick-actions {
1428      margin-bottom: 0.75rem;
1429    }
1430  
1431    .quick-action {
1432      width: 100%;
1433      display: flex;
1434      align-items: center;
1435      justify-content: center;
1436      gap: 0.5rem;
1437      padding: 0.625rem;
1438      background-color: var(--color-bg-light);
1439      border: 1px solid rgba(217, 137, 46, 0.3);
1440      border-radius: 6px;
1441      color: var(--color-phosphor);
1442      font-size: 0.875rem;
1443      cursor: pointer;
1444      transition: all 0.2s;
1445    }
1446  
1447    .quick-action:hover:not(:disabled) {
1448      background-color: var(--color-phosphor);
1449      color: var(--color-bg);
1450    }
1451  
1452    .quick-action:disabled {
1453      opacity: 0.6;
1454      cursor: not-allowed;
1455    }
1456  
1457    .active-workspace-display {
1458      width: 100%;
1459      display: flex;
1460      align-items: center;
1461      justify-content: center;
1462      gap: 0.5rem;
1463      padding: 0.625rem;
1464      background-color: rgba(217, 137, 46, 0.15);
1465      border: 1px solid rgba(217, 137, 46, 0.4);
1466      border-radius: 6px;
1467      color: var(--color-phosphor);
1468      font-size: 0.875rem;
1469    }
1470  
1471    .active-workspace-name {
1472      font-weight: 500;
1473      white-space: nowrap;
1474      overflow: hidden;
1475      text-overflow: ellipsis;
1476      max-width: 180px;
1477    }
1478  
1479    .section {
1480      margin-bottom: 0.75rem;
1481    }
1482  
1483    .section-title {
1484      font-size: 0.625rem;
1485      font-weight: 500;
1486      text-transform: uppercase;
1487      letter-spacing: 0.05em;
1488      color: var(--color-text-muted);
1489      margin-bottom: 0.5rem;
1490      padding-left: 0.25rem;
1491    }
1492  
1493    .transcendent-section {
1494      margin-bottom: 0.75rem;
1495      padding: 0.5rem;
1496      background: linear-gradient(135deg, rgba(217, 137, 46, 0.1) 0%, rgba(217, 137, 46, 0.05) 100%);
1497      border: 1px solid rgba(217, 137, 46, 0.2);
1498      border-radius: 6px;
1499    }
1500  
1501    .section-title-transcendent {
1502      display: flex;
1503      align-items: center;
1504      gap: 0.375rem;
1505      color: var(--color-phosphor);
1506    }
1507  
1508    .transcendent-list {
1509      display: flex;
1510      flex-wrap: wrap;
1511      gap: 0.375rem;
1512      margin-top: 0.375rem;
1513    }
1514  
1515    .transcendent-item {
1516      display: flex;
1517      align-items: center;
1518      gap: 0.375rem;
1519      padding: 0.375rem 0.625rem;
1520      background: rgba(217, 137, 46, 0.15);
1521      border: 1px solid rgba(217, 137, 46, 0.3);
1522      border-radius: 9999px;
1523      cursor: pointer;
1524      transition: all 0.15s;
1525      font-size: 0.75rem;
1526    }
1527  
1528    .transcendent-item:hover {
1529      background: var(--color-phosphor);
1530      border-color: var(--color-phosphor);
1531    }
1532  
1533    .transcendent-item:hover .transcendent-name {
1534      color: var(--color-bg);
1535    }
1536  
1537    .transcendent-item:hover :global(svg) {
1538      color: var(--color-bg);
1539    }
1540  
1541    .transcendent-item.dragging {
1542      opacity: 0.5;
1543    }
1544  
1545    .transcendent-name {
1546      color: var(--color-phosphor);
1547      white-space: nowrap;
1548    }
1549  
1550    /* Parent workspaces */
1551    .parent-list {
1552      display: flex;
1553      flex-direction: column;
1554      gap: 0.5rem;
1555    }
1556  
1557    .parent-item {
1558      border: 1px solid rgba(217, 137, 46, 0.2);
1559      border-radius: 6px;
1560      overflow: hidden;
1561      transition: all 0.2s;
1562    }
1563  
1564    .parent-item.drop-target {
1565      border-color: var(--color-phosphor);
1566      background-color: rgba(217, 137, 46, 0.1);
1567    }
1568  
1569    .parent-header {
1570      display: flex;
1571      align-items: center;
1572      gap: 0.375rem;
1573      padding: 0.375rem 0.5rem;
1574      background-color: var(--color-bg-light);
1575      width: 100%;
1576      transition: background-color 0.15s;
1577    }
1578  
1579    .parent-header:hover {
1580      background-color: rgba(217, 137, 46, 0.15);
1581    }
1582  
1583    .parent-toggle {
1584      display: flex;
1585      align-items: center;
1586      justify-content: center;
1587      width: 20px;
1588      height: 20px;
1589      padding: 0;
1590      background: none;
1591      border: none;
1592      cursor: pointer;
1593      color: var(--color-text-muted);
1594      border-radius: 3px;
1595      transition: all 0.15s;
1596    }
1597  
1598    .parent-toggle:hover {
1599      background-color: rgba(217, 137, 46, 0.2);
1600      color: var(--color-phosphor);
1601    }
1602  
1603    .chevron-icon {
1604      width: 14px;
1605      height: 14px;
1606      transition: transform 0.2s ease;
1607    }
1608  
1609    .chevron-icon.expanded {
1610      transform: rotate(90deg);
1611    }
1612  
1613    .parent-name-btn {
1614      display: flex;
1615      align-items: center;
1616      gap: 0.375rem;
1617      flex: 1;
1618      padding: 0.25rem 0.375rem;
1619      background: none;
1620      border: none;
1621      cursor: pointer;
1622      border-radius: 4px;
1623      transition: background-color 0.15s;
1624      text-align: left;
1625    }
1626  
1627    .parent-name-btn:hover {
1628      background-color: rgba(217, 137, 46, 0.15);
1629    }
1630  
1631    .parent-name {
1632      font-size: 0.8125rem;
1633      color: var(--color-text-primary);
1634      font-weight: 500;
1635    }
1636  
1637    .parent-count {
1638      font-size: 0.625rem;
1639      color: var(--color-text-muted);
1640      background-color: rgba(217, 137, 46, 0.2);
1641      padding: 0.125rem 0.375rem;
1642      border-radius: 9999px;
1643    }
1644  
1645    .children-list {
1646      border-top: 1px solid rgba(217, 137, 46, 0.15);
1647    }
1648  
1649    .child-item {
1650      display: flex;
1651      align-items: center;
1652      gap: 0.5rem;
1653      padding: 0.375rem 0.625rem 0.375rem 1.5rem;
1654      background: transparent;
1655      border: none;
1656      width: 100%;
1657      cursor: grab;
1658      transition: background-color 0.15s;
1659      text-align: left;
1660    }
1661  
1662    .child-item:hover {
1663      background-color: rgba(217, 137, 46, 0.1);
1664    }
1665  
1666    .child-item.dragging {
1667      opacity: 0.5;
1668      cursor: grabbing;
1669    }
1670  
1671    .child-name {
1672      flex: 1;
1673      font-size: 0.75rem;
1674      color: var(--color-text-secondary);
1675    }
1676  
1677    .child-tabs {
1678      font-size: 0.625rem;
1679      color: var(--color-text-muted);
1680    }
1681  
1682    /* Open indicator */
1683    .open-indicator {
1684      width: 6px;
1685      height: 6px;
1686      border-radius: 50%;
1687      background-color: #10b981;
1688      animation: pulse 2s infinite;
1689      flex-shrink: 0;
1690    }
1691  
1692    @keyframes pulse {
1693      0%, 100% {
1694        opacity: 1;
1695      }
1696      50% {
1697        opacity: 0.5;
1698      }
1699    }
1700  
1701    .child-item.is-open,
1702    .standalone-item.is-open {
1703      background-color: rgba(16, 185, 129, 0.1);
1704    }
1705  
1706    .child-item.is-open:hover,
1707    .standalone-item.is-open:hover {
1708      background-color: rgba(16, 185, 129, 0.15);
1709    }
1710  
1711    /* Standalone workspaces */
1712    .standalone-section {
1713      padding: 0.5rem;
1714      border: 1px dashed rgba(217, 137, 46, 0.2);
1715      border-radius: 6px;
1716      min-height: 60px;
1717      transition: all 0.2s;
1718    }
1719  
1720    .standalone-section.drop-target {
1721      border-color: var(--color-phosphor);
1722      background-color: rgba(217, 137, 46, 0.1);
1723    }
1724  
1725    .standalone-list {
1726      display: flex;
1727      flex-direction: column;
1728      gap: 0.25rem;
1729    }
1730  
1731    .standalone-item {
1732      display: flex;
1733      align-items: center;
1734      gap: 0.5rem;
1735      padding: 0.5rem 0.625rem;
1736      background: transparent;
1737      border: none;
1738      border-radius: 4px;
1739      cursor: grab;
1740      transition: background-color 0.15s;
1741      text-align: left;
1742      width: 100%;
1743    }
1744  
1745    .standalone-item:hover {
1746      background-color: var(--color-bg-light);
1747    }
1748  
1749    .standalone-item.dragging {
1750      opacity: 0.5;
1751      cursor: grabbing;
1752    }
1753  
1754    .standalone-item.transcendent {
1755      background-color: rgba(217, 137, 46, 0.1);
1756      border-left: 2px solid var(--color-phosphor);
1757    }
1758  
1759    .standalone-name {
1760      flex: 1;
1761      font-size: 0.8125rem;
1762      color: var(--color-text-primary);
1763      white-space: nowrap;
1764      overflow: hidden;
1765      text-overflow: ellipsis;
1766    }
1767  
1768    .standalone-tabs {
1769      font-size: 0.625rem;
1770      color: var(--color-text-muted);
1771      white-space: nowrap;
1772    }
1773  
1774    .empty-text {
1775      color: var(--color-text-secondary);
1776      margin-bottom: 0.25rem;
1777    }
1778  
1779    .empty-hint {
1780      font-size: 0.75rem;
1781      color: var(--color-text-muted);
1782      text-align: center;
1783    }
1784  
1785    .popup-actions {
1786      display: flex;
1787      gap: 0.5rem;
1788      padding: 0.75rem;
1789      border-top: 1px solid rgba(217, 137, 46, 0.2);
1790      background-color: var(--color-bg-dark);
1791    }
1792  
1793    .popup-actions :global(button:first-child) {
1794      flex: 1;
1795    }
1796  
1797    /* Move modal styles */
1798    .move-form {
1799      display: flex;
1800      flex-direction: column;
1801      gap: 0.5rem;
1802    }
1803  
1804    .field-label {
1805      font-size: 0.875rem;
1806      font-weight: 500;
1807      color: var(--color-text-secondary);
1808    }
1809  
1810    .select {
1811      width: 100%;
1812      padding: 0.5rem 0.75rem;
1813      background-color: var(--color-bg-dark);
1814      color: var(--color-text-primary);
1815      border: 1px solid rgba(217, 137, 46, 0.3);
1816      border-radius: 6px;
1817      font-size: 0.875rem;
1818      cursor: pointer;
1819    }
1820  
1821    .select:focus {
1822      outline: none;
1823      border-color: var(--color-phosphor);
1824    }
1825  </style>