# 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) ```json "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):** ```javascript let tabbarView = null; const externalViews = new Map(); // tabId → BrowserView let activeTabId = 'home'; ``` **Modify `updateBounds()`:** ```javascript 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: ```javascript 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:** ```javascript 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:** ```javascript function switchToTab(tabId) { activeTabId = tabId; updateBounds(); sendTabsToTabbar(); } ``` **Add `closeExternalTab(tabId)` function:** ```javascript 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:** ```javascript 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): ```javascript { label: 'External URLs', submenu: buildExternalUrlsSubmenu() } ``` **Add `buildExternalUrlsSubmenu()` function:** ```javascript 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:** ```javascript 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: ```javascript settingsWindow = new BrowserWindow({ width: 500, height: 600, ... }); ``` ### 3. `src/main/preload.js` Add to the `contextBridge.exposeInMainWorld` object: ```javascript 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: `` - 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: ```html
``` ### 6. New file: `src/renderer/tabbar.css` ```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: ```css .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