directlx-claude-config/plans/radiant-mapping-goose.md

10 KiB
Raw Blame History

Plan: External URLs Feature

Context

User wants to manage bookmarked external URLs in HiveOps Browser:

  1. A new "External URLs" tab in the Settings window to add/remove named URL entries
  2. A "External URLs" menu item in the application menu listing those URLs
  3. Clicking a URL from the menu opens it in a new tab within the main window (not a new OS window)

Architecture Decision: BrowserView Tab Bar

The main window uses BrowserViews (no parent HTML). Adding in-window tabs requires:

  • A tabbarView BrowserView (36px tall) at the top of the main window — visible only when at least one external tab is open
  • Each external URL tab gets its own BrowserView when opened, persisted in externalViews Map
  • updateBounds() is updated to account for the tab bar height
  • The "home" tab always uses the existing incidentView

Data Structure (stored via electron-store)

"externalUrls": [
  { "id": "uuid-1", "name": "My Dashboard", "url": "https://example.com" }
]

IDs are generated with crypto.randomUUID().


Files to Modify

1. src/main/config.js

  • Add externalUrls: [] to defaultConfig
  • Add externalUrls to getAll() return object
  • Add to setAll(): if (settings.externalUrls !== undefined) store.set('externalUrls', settings.externalUrls)
  • Add convenience methods:
    • getExternalUrls()store.get('externalUrls', [])
    • setExternalUrls(urls)store.set('externalUrls', urls)

2. src/main/main.js

New variables (top of file):

let tabbarView = null;
const externalViews = new Map(); // tabId → BrowserView
let activeTabId = 'home';

Modify updateBounds():

function updateBounds() {
  if (!mainWindow || mainWindow.isDestroyed()) return;
  if (!incidentView || !statusbarView) return;
  const STATUS_BAR_H = 32;
  const TAB_BAR_H = (tabbarView && externalViews.size > 0) ? 36 : 0;
  const [w, h] = mainWindow.getContentSize();

  if (tabbarView) {
    tabbarView.setBounds({ x: 0, y: 0, width: TAB_BAR_H > 0 ? w : 0, height: TAB_BAR_H });
  }

  const contentH = h - STATUS_BAR_H - TAB_BAR_H;
  // Show only active view; hide others by setting zero bounds
  [['home', incidentView], ...externalViews].forEach(([tid, view]) => {
    if (view && !view.webContents.isDestroyed()) {
      if (tid === activeTabId) {
        view.setBounds({ x: 0, y: TAB_BAR_H, width: w, height: contentH });
      } else {
        view.setBounds({ x: 0, y: TAB_BAR_H, width: 0, height: 0 });
      }
    }
  });

  statusbarView.setBounds({ x: 0, y: h - STATUS_BAR_H, width: w, height: STATUS_BAR_H });
}

Modify createMainWindow() — after creating incidentView and statusbarView, create tabbarView:

tabbarView = new BrowserView({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    nodeIntegration: false,
    contextIsolation: true
  }
});
mainWindow.addBrowserView(tabbarView);
tabbarView.webContents.loadFile(path.join(__dirname, '../renderer/tabbar.html'));

Also add tabbarView cleanup in the closed event.

Add openExternalTab(id, name, url) function:

function openExternalTab(id, name, url) {
  if (externalViews.has(id)) {
    switchToTab(id);
    return;
  }
  const view = new BrowserView({ webPreferences: { nodeIntegration: false, contextIsolation: true } });
  mainWindow.addBrowserView(view);
  externalViews.set(id, view);
  view.webContents.loadURL(url);
  switchToTab(id);
}

Add switchToTab(tabId) function:

function switchToTab(tabId) {
  activeTabId = tabId;
  updateBounds();
  sendTabsToTabbar();
}

Add closeExternalTab(tabId) function:

function closeExternalTab(tabId) {
  const view = externalViews.get(tabId);
  if (view) {
    mainWindow.removeBrowserView(view);
    view.webContents.destroy();
    externalViews.delete(tabId);
  }
  if (activeTabId === tabId) {
    activeTabId = 'home';
  }
  updateBounds();
  sendTabsToTabbar();
}

Add sendTabsToTabbar() function:

function sendTabsToTabbar() {
  if (!tabbarView || tabbarView.webContents.isDestroyed()) return;
  const tabs = [
    { id: 'home', name: 'HiveOps', closeable: false }
  ];
  for (const [id] of externalViews) {
    const urlEntry = config.getExternalUrls().find(u => u.id === id);
    tabs.push({ id, name: urlEntry ? urlEntry.name : id, closeable: true });
  }
  tabbarView.webContents.send('update-tabs', { tabs, activeTabId });
}

Modify createMenu() — add "External URLs" menu after the existing File menu (or as a top-level menu):

{
  label: 'External URLs',
  submenu: buildExternalUrlsSubmenu()
}

Add buildExternalUrlsSubmenu() function:

function buildExternalUrlsSubmenu() {
  const urls = config.getExternalUrls();
  if (urls.length === 0) {
    return [
      { label: 'No URLs configured', enabled: false },
      { type: 'separator' },
      { label: 'Manage External URLs...', click: () => openSettings() }
    ];
  }
  return [
    ...urls.map(entry => ({
      label: entry.name,
      click: () => openExternalTab(entry.id, entry.name, entry.url)
    })),
    { type: 'separator' },
    { label: 'Manage External URLs...', click: () => openSettings() }
  ];
}

Add IPC handlers:

ipcMain.handle('get-external-urls', () => config.getExternalUrls());

ipcMain.handle('save-external-urls', (event, urls) => {
  config.setExternalUrls(urls);
  createMenu(); // rebuild menu with updated URLs
  return config.getExternalUrls();
});

ipcMain.on('switch-tab', (event, tabId) => switchToTab(tabId));

ipcMain.on('close-tab', (event, tabId) => closeExternalTab(tabId));

Settings window height — increase from 530 to 600:

settingsWindow = new BrowserWindow({ width: 500, height: 600, ... });

3. src/main/preload.js

Add to the contextBridge.exposeInMainWorld object:

getExternalUrls: () => ipcRenderer.invoke('get-external-urls'),
saveExternalUrls: (urls) => ipcRenderer.invoke('save-external-urls', urls),
switchTab: (tabId) => ipcRenderer.send('switch-tab', tabId),
closeTab: (tabId) => ipcRenderer.send('close-tab', tabId),
onUpdateTabs: (callback) => ipcRenderer.on('update-tabs', (event, data) => callback(data)),

4. src/renderer/settings.html

  • Add tab button: <button class="tab-btn" data-tab="externalurls">External URLs</button>
  • Add tab panel #tab-externalurls:
    • Form row with name text input + url URL input + "Add" button
    • Unordered list #external-urls-list where each item shows: name, URL, delete button
  • JavaScript:
    • loadExternalUrls() — calls window.electronAPI.getExternalUrls() and renders the list
    • "Add" button handler: validates inputs, adds entry with crypto.randomUUID(), saves
    • Delete button handler: removes entry by id, saves
    • External URLs are saved independently (not via the main form submit); each add/delete auto-saves

5. New file: src/renderer/tabbar.html

Tab bar UI that receives tab data from main process and sends switch/close events back:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';">
  <link rel="stylesheet" href="tabbar.css">
</head>
<body>
  <div id="tab-container"></div>
  <script>
    window.electronAPI.onUpdateTabs(({ tabs, activeTabId }) => {
      const container = document.getElementById('tab-container');
      container.innerHTML = '';
      tabs.forEach(tab => {
        const el = document.createElement('div');
        el.className = 'tab' + (tab.id === activeTabId ? ' active' : '');
        el.dataset.id = tab.id;
        const label = document.createElement('span');
        label.className = 'tab-label';
        label.textContent = tab.name;
        label.addEventListener('click', () => window.electronAPI.switchTab(tab.id));
        el.appendChild(label);
        if (tab.closeable) {
          const close = document.createElement('button');
          close.className = 'tab-close';
          close.textContent = '×';
          close.addEventListener('click', (e) => { e.stopPropagation(); window.electronAPI.closeTab(tab.id); });
          el.appendChild(close);
        }
        container.appendChild(el);
      });
    });
  </script>
</body>
</html>

6. New file: src/renderer/tabbar.css

* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #2c3e50; display: flex; align-items: center; height: 36px; overflow: hidden; -webkit-app-region: no-drag; user-select: none; }
#tab-container { display: flex; height: 100%; gap: 2px; padding: 4px 8px; }
.tab { display: flex; align-items: center; gap: 4px; padding: 0 12px; background: #3d5166; color: #adb5bd; border-radius: 4px; cursor: pointer; max-width: 200px; font-size: 13px; transition: background 0.15s, color 0.15s; }
.tab:hover { background: #4e6a82; color: #fff; }
.tab.active { background: #2196f3; color: #fff; }
.tab-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tab-close { background: none; border: none; color: inherit; cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; opacity: 0.7; }
.tab-close:hover { opacity: 1; }

7. src/renderer/settings.css

Add styles for the URL list management UI:

.url-list { list-style: none; margin-top: 0.5rem; }
.url-list-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0; border-bottom: 1px solid #eee; }
.url-list-item .url-name { font-weight: 500; min-width: 100px; }
.url-list-item .url-href { flex: 1; color: #666; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.url-add-row { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.url-add-row input[name="newUrlName"] { width: 120px; }
.url-add-row input[name="newUrlHref"] { flex: 1; }
.btn-sm { padding: 0.25rem 0.6rem; font-size: 0.85rem; }

Verification

  1. Run npm start
  2. Open Settings (Ctrl+,) → verify "External URLs" tab appears
  3. Add a URL entry (name + URL) → verify it appears in the list
  4. Click Save & Apply (or verify auto-save on add)
  5. Open the application menu → verify "External URLs" submenu shows the entry
  6. Click the URL from the menu → verify a tab bar appears at top of main window with "HiveOps" and the new tab
  7. Click the new tab → verify the external URL loads
  8. Click "HiveOps" tab → verify switch back to incidentView
  9. Click × on the external tab → verify tab closes, tab bar disappears if no more external tabs
  10. Delete URL in settings → verify menu updates after close/reopen