diff --git a/terminal-browser-preview/README.md b/terminal-browser-preview/README.md
new file mode 100644
index 0000000..4716c1c
--- /dev/null
+++ b/terminal-browser-preview/README.md
@@ -0,0 +1,80 @@
+# Terminal + Browser Preview
+
+A Chromium-based desktop application that combines bash/zsh terminals with browser preview tabs, organized into project workspaces. Built with Electron, xterm.js, and node-pty.
+
+## 3-Level Tab Architecture
+
+```
+[Project A] [Project B] [Project C] [+] <-- project tabs (top level)
+ ┌──────────────────────┬──────────────────────┐
+ │ [bash#1] [zsh#2] [+] │ [Tab1] [Tab2] [+] │ <-- terminal & browser tabs
+ │ │ [< > ↻ URL bar 🔧] │
+ │ Terminal content │ Browser content │
+ │ │ │
+ └──────────────────────┴──────────────────────┘
+```
+
+Each project is an independent workspace with its own:
+- Multiple terminal tabs (each running a separate shell session)
+- Multiple browser tabs (each with its own webview)
+- Independent layout (split, terminal-only, browser-only, vertical)
+- Resizable split divider
+
+## Features
+
+- **Project workspaces** - Color-coded, renamable (double-click), independent
+- **Multiple terminals** - Each tab spawns its own bash/zsh PTY process
+- **Multiple browser tabs** - Chromium webviews with navigation, DevTools
+- **Split-pane layouts** - Horizontal, vertical, terminal-only, browser-only (per project)
+- **Resizable panels** - Drag divider to adjust split ratio
+- **URL intelligence** - Detects URLs, file paths, and search queries
+- **Catppuccin Mocha** dark theme
+
+## Setup
+
+```bash
+cd terminal-browser-preview
+npm install
+npx electron-rebuild # rebuild node-pty for Electron
+npm start
+```
+
+## Keyboard Shortcuts
+
+### Projects
+| Shortcut | Action |
+|----------|--------|
+| `Ctrl+Shift+P` | New project |
+| `Ctrl+Shift+W` | Close project |
+| `Ctrl+PageUp/Down` | Cycle projects |
+
+### Terminals
+| Shortcut | Action |
+|----------|--------|
+| `Ctrl+Shift+T` | New terminal tab |
+| `Ctrl+W` | Close focused tab |
+| `Ctrl+Tab` | Next tab (in focused panel) |
+| `Ctrl+Shift+Tab` | Previous tab |
+
+### Browser
+| Shortcut | Action |
+|----------|--------|
+| `Ctrl+T` | New browser tab |
+| `Ctrl+L` | Focus URL bar |
+| `Ctrl+R` | Reload page |
+
+### Layout
+| Shortcut | Action |
+|----------|--------|
+| `Ctrl+1` | Split view |
+| `Ctrl+2` | Terminal only |
+| `Ctrl+3` | Browser only |
+
+## Architecture
+
+```
+main.js Electron main process - manages PTY pool via IPC
+preload.js Secure context bridge - ID-based terminal API
+renderer.js 3-level tab manager: projects > terminals + browsers
+index.html UI layout with Catppuccin Mocha theme
+```
diff --git a/terminal-browser-preview/index.html b/terminal-browser-preview/index.html
new file mode 100644
index 0000000..0ea9778
--- /dev/null
+++ b/terminal-browser-preview/index.html
@@ -0,0 +1,454 @@
+
+
+
+
+
+ Terminal + Browser Preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Connected
+
+
+
+
+
+
+
+
+
diff --git a/terminal-browser-preview/main.js b/terminal-browser-preview/main.js
new file mode 100644
index 0000000..f264f36
--- /dev/null
+++ b/terminal-browser-preview/main.js
@@ -0,0 +1,110 @@
+const { app, BrowserWindow, ipcMain } = require('electron');
+const path = require('path');
+const os = require('os');
+const pty = require('node-pty');
+
+let mainWindow;
+const ptyProcesses = new Map(); // id -> pty process
+let nextPtyId = 1;
+
+function getShell() {
+ if (process.platform === 'win32') return 'powershell.exe';
+ return process.env.SHELL || '/bin/bash';
+}
+
+function createWindow() {
+ mainWindow = new BrowserWindow({
+ width: 1400,
+ height: 900,
+ minWidth: 800,
+ minHeight: 500,
+ title: 'Terminal + Browser Preview',
+ backgroundColor: '#1e1e2e',
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ nodeIntegration: false,
+ contextIsolation: true,
+ webviewTag: true
+ }
+ });
+
+ mainWindow.loadFile('index.html');
+
+ // ── Create a new PTY process ──────────────────────────────────────────
+ ipcMain.handle('pty:create', (_event, opts = {}) => {
+ const id = nextPtyId++;
+ const shell = getShell();
+ const cwd = opts.cwd || process.env.HOME || os.homedir();
+
+ const proc = pty.spawn(shell, [], {
+ name: 'xterm-256color',
+ cols: opts.cols || 80,
+ rows: opts.rows || 24,
+ cwd,
+ env: {
+ ...process.env,
+ TERM: 'xterm-256color',
+ COLORTERM: 'truecolor'
+ }
+ });
+
+ proc.onData((data) => {
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.send('pty:data', id, data);
+ }
+ });
+
+ proc.onExit(({ exitCode }) => {
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.send('pty:exit', id, exitCode);
+ }
+ ptyProcesses.delete(id);
+ });
+
+ ptyProcesses.set(id, proc);
+
+ const shellName = path.basename(shell);
+ return { id, shell: shellName, pid: proc.pid };
+ });
+
+ // ── Write to a PTY ────────────────────────────────────────────────────
+ ipcMain.on('pty:write', (_event, id, data) => {
+ const proc = ptyProcesses.get(id);
+ if (proc) proc.write(data);
+ });
+
+ // ── Resize a PTY ──────────────────────────────────────────────────────
+ ipcMain.on('pty:resize', (_event, id, cols, rows) => {
+ const proc = ptyProcesses.get(id);
+ if (proc) {
+ try { proc.resize(cols, rows); } catch (_) {}
+ }
+ });
+
+ // ── Kill a PTY ────────────────────────────────────────────────────────
+ ipcMain.on('pty:kill', (_event, id) => {
+ const proc = ptyProcesses.get(id);
+ if (proc) {
+ proc.kill();
+ ptyProcesses.delete(id);
+ }
+ });
+
+ mainWindow.on('closed', () => {
+ mainWindow = null;
+ for (const proc of ptyProcesses.values()) proc.kill();
+ ptyProcesses.clear();
+ });
+}
+
+app.whenReady().then(createWindow);
+
+app.on('window-all-closed', () => {
+ for (const proc of ptyProcesses.values()) proc.kill();
+ ptyProcesses.clear();
+ app.quit();
+});
+
+app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
+});
diff --git a/terminal-browser-preview/package.json b/terminal-browser-preview/package.json
new file mode 100644
index 0000000..c98b9e2
--- /dev/null
+++ b/terminal-browser-preview/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "terminal-browser-preview",
+ "version": "1.0.0",
+ "description": "Chromium-based terminal with integrated browser preview - mixes bash/zsh with a browser tab",
+ "main": "main.js",
+ "scripts": {
+ "start": "electron .",
+ "dev": "electron . --dev"
+ },
+ "keywords": ["terminal", "browser", "electron", "xterm", "preview"],
+ "license": "MIT",
+ "dependencies": {
+ "electron": "^33.0.0",
+ "@xterm/xterm": "^5.5.0",
+ "@xterm/addon-fit": "^0.10.0",
+ "@xterm/addon-web-links": "^0.11.0",
+ "node-pty": "^1.0.0"
+ },
+ "devDependencies": {
+ "electron-rebuild": "^3.2.9"
+ }
+}
diff --git a/terminal-browser-preview/preload.js b/terminal-browser-preview/preload.js
new file mode 100644
index 0000000..1bdc7c8
--- /dev/null
+++ b/terminal-browser-preview/preload.js
@@ -0,0 +1,21 @@
+const { contextBridge, ipcRenderer } = require('electron');
+
+contextBridge.exposeInMainWorld('terminalAPI', {
+ // Create a new PTY shell, returns { id, shell, pid }
+ create: (opts) => ipcRenderer.invoke('pty:create', opts),
+
+ // Write data to a specific PTY
+ write: (id, data) => ipcRenderer.send('pty:write', id, data),
+
+ // Resize a specific PTY
+ resize: (id, cols, rows) => ipcRenderer.send('pty:resize', id, cols, rows),
+
+ // Kill a specific PTY
+ kill: (id) => ipcRenderer.send('pty:kill', id),
+
+ // Listen for data from any PTY (callback receives id, data)
+ onData: (callback) => ipcRenderer.on('pty:data', (_event, id, data) => callback(id, data)),
+
+ // Listen for PTY exit (callback receives id, exitCode)
+ onExit: (callback) => ipcRenderer.on('pty:exit', (_event, id, code) => callback(id, code))
+});
diff --git a/terminal-browser-preview/renderer.js b/terminal-browser-preview/renderer.js
new file mode 100644
index 0000000..aa88440
--- /dev/null
+++ b/terminal-browser-preview/renderer.js
@@ -0,0 +1,826 @@
+/* ========================================================================
+ Terminal + Browser Preview - Renderer
+ 3-level tabs: Projects > Terminal tabs + Browser tabs
+ ======================================================================== */
+
+const { Terminal } = require('@xterm/xterm');
+const { FitAddon } = require('@xterm/addon-fit');
+const { WebLinksAddon } = require('@xterm/addon-web-links');
+
+const XTERM_THEME = {
+ background: '#1e1e2e',
+ foreground: '#cdd6f4',
+ cursor: '#f5e0dc',
+ cursorAccent: '#1e1e2e',
+ selectionBackground: '#45475a',
+ selectionForeground: '#cdd6f4',
+ black: '#45475a',
+ red: '#f38ba8',
+ green: '#a6e3a1',
+ yellow: '#f9e2af',
+ blue: '#89b4fa',
+ magenta: '#f5c2e7',
+ cyan: '#94e2d5',
+ white: '#bac2de',
+ brightBlack: '#585b70',
+ brightRed: '#f38ba8',
+ brightGreen: '#a6e3a1',
+ brightYellow: '#f9e2af',
+ brightBlue: '#89b4fa',
+ brightMagenta: '#f5c2e7',
+ brightCyan: '#94e2d5',
+ brightWhite: '#a6adc8'
+};
+
+const PROJECT_COLORS = ['#89b4fa', '#a6e3a1', '#fab387', '#cba6f7', '#f38ba8', '#f9e2af', '#94e2d5', '#f5c2e7'];
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ PROJECT MANAGEMENT ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+const projects = []; // Array of project objects
+let activeProjectId = null;
+let nextProjectId = 1;
+
+const mainEl = document.getElementById('main');
+const projectTabList = document.getElementById('project-tab-list');
+
+// A project contains: { id, name, color, layout, workspaceEl,
+// terminalTabs[], activeTerminalId, nextTerminalId,
+// browserTabs[], activeBrowserId, nextBrowserId,
+// dom: { termPanel, termTabList, termContainer, browserPanel, browserTabList,
+// webviewContainer, urlInput, divider } }
+
+function createProjectDOM(project) {
+ const ws = document.createElement('div');
+ ws.className = 'project-workspace';
+ ws.dataset.projectId = project.id;
+
+ ws.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ mainEl.appendChild(ws);
+
+ const dom = {
+ workspace: ws,
+ termPanel: ws.querySelector('.panel-terminal'),
+ termTabList: ws.querySelector('.term-tab-list'),
+ termContainer: ws.querySelector('.term-container'),
+ browserPanel: ws.querySelector('.panel-browser'),
+ browserTabList: ws.querySelector('.browser-tab-list'),
+ webviewContainer: ws.querySelector('.webview-container'),
+ urlInput: ws.querySelector('.url-input'),
+ divider: ws.querySelector('.panel-divider')
+ };
+
+ // Wire up new-tab buttons
+ ws.querySelector('.new-term-btn').addEventListener('click', () => {
+ addTerminalTab(project);
+ });
+
+ ws.querySelector('.new-browser-btn').addEventListener('click', () => {
+ addBrowserTab(project);
+ dom.urlInput.focus();
+ });
+
+ // Wire up URL bar
+ dom.urlInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ navigateBrowser(project, dom.urlInput.value);
+ dom.urlInput.blur();
+ }
+ });
+ dom.urlInput.addEventListener('focus', () => dom.urlInput.select());
+
+ // Wire up nav buttons
+ ws.querySelector('.nav-back').addEventListener('click', () => {
+ const wv = getActiveWebview(project);
+ if (wv && wv.canGoBack()) wv.goBack();
+ });
+ ws.querySelector('.nav-forward').addEventListener('click', () => {
+ const wv = getActiveWebview(project);
+ if (wv && wv.canGoForward()) wv.goForward();
+ });
+ ws.querySelector('.nav-reload').addEventListener('click', () => {
+ const wv = getActiveWebview(project);
+ if (wv) wv.reload();
+ });
+ ws.querySelector('.nav-devtools').addEventListener('click', () => {
+ const wv = getActiveWebview(project);
+ if (wv) {
+ if (wv.isDevToolsOpened()) wv.closeDevTools();
+ else wv.openDevTools();
+ }
+ });
+
+ // Wire up divider drag
+ setupDividerDrag(project, dom);
+
+ // Wire up panel focus tracking
+ dom.termPanel.addEventListener('mousedown', () => { project.focusedPanel = 'terminal'; });
+ dom.browserPanel.addEventListener('mousedown', () => { project.focusedPanel = 'browser'; });
+
+ // Resize observer for terminal fitting
+ const resizeObs = new ResizeObserver(() => {
+ const tt = getActiveTerminalTab(project);
+ if (!tt) return;
+ requestAnimationFrame(() => {
+ tt.fitAddon.fit();
+ if (tt.alive) window.terminalAPI.resize(tt.ptyId, tt.term.cols, tt.term.rows);
+ if (project.id === activeProjectId) updateStatusBar(project);
+ });
+ });
+ resizeObs.observe(dom.termContainer);
+
+ return dom;
+}
+
+async function createProject(name) {
+ const id = nextProjectId++;
+ const color = PROJECT_COLORS[(id - 1) % PROJECT_COLORS.length];
+
+ const project = {
+ id,
+ name: name || `Project ${id}`,
+ color,
+ layout: 'split',
+ focusedPanel: 'terminal',
+ terminalTabs: [],
+ activeTerminalId: null,
+ nextTerminalId: 1,
+ browserTabs: [],
+ activeBrowserId: null,
+ nextBrowserId: 1,
+ dom: null
+ };
+
+ project.dom = createProjectDOM(project);
+ projects.push(project);
+
+ // Create initial terminal + browser tab
+ await addTerminalTab(project);
+ addBrowserTab(project, 'https://duckduckgo.com');
+
+ switchProject(id);
+ renderProjectTabs();
+ return project;
+}
+
+function switchProject(id) {
+ activeProjectId = id;
+ projects.forEach(p => {
+ p.dom.workspace.classList.toggle('active', p.id === id);
+ });
+
+ const proj = getActiveProject();
+ if (proj) {
+ applyLayout(proj, proj.layout);
+ // Refit active terminal
+ const tt = getActiveTerminalTab(proj);
+ if (tt) {
+ requestAnimationFrame(() => {
+ tt.fitAddon.fit();
+ if (tt.alive) window.terminalAPI.resize(tt.ptyId, tt.term.cols, tt.term.rows);
+ tt.term.focus();
+ });
+ }
+ updateStatusBar(proj);
+ }
+ renderProjectTabs();
+}
+
+function closeProject(id) {
+ const idx = projects.findIndex(p => p.id === id);
+ if (idx === -1) return;
+
+ const proj = projects[idx];
+
+ // Kill all PTY processes
+ for (const tt of proj.terminalTabs) {
+ if (tt.alive) window.terminalAPI.kill(tt.ptyId);
+ tt.term.dispose();
+ }
+
+ // Remove DOM
+ proj.dom.workspace.remove();
+ projects.splice(idx, 1);
+
+ if (projects.length === 0) {
+ createProject();
+ } else if (activeProjectId === id) {
+ const newIdx = Math.min(idx, projects.length - 1);
+ switchProject(projects[newIdx].id);
+ }
+ renderProjectTabs();
+}
+
+function getActiveProject() {
+ return projects.find(p => p.id === activeProjectId) || null;
+}
+
+function renderProjectTabs() {
+ projectTabList.innerHTML = '';
+ projects.forEach(proj => {
+ const el = document.createElement('div');
+ el.className = 'project-tab' + (proj.id === activeProjectId ? ' active' : '');
+
+ const dot = document.createElement('span');
+ dot.className = 'color-dot';
+ dot.style.background = proj.color;
+ el.appendChild(dot);
+
+ const title = document.createElement('span');
+ title.className = 'tab-title';
+ title.textContent = proj.name;
+ title.addEventListener('dblclick', (e) => {
+ e.stopPropagation();
+ renameProject(proj);
+ });
+ el.appendChild(title);
+
+ const close = document.createElement('span');
+ close.className = 'tab-close';
+ close.textContent = '\u00d7';
+ close.addEventListener('click', (e) => {
+ e.stopPropagation();
+ closeProject(proj.id);
+ });
+ el.appendChild(close);
+
+ el.addEventListener('click', () => switchProject(proj.id));
+ projectTabList.appendChild(el);
+ });
+}
+
+function renameProject(proj) {
+ const tabEl = projectTabList.querySelector(`.project-tab:nth-child(${projects.indexOf(proj) + 1}) .tab-title`);
+ if (!tabEl) return;
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.value = proj.name;
+ input.style.cssText = 'width:100px;background:var(--overlay);border:1px solid var(--accent);color:var(--text);font-size:12px;font-family:inherit;border-radius:3px;padding:0 4px;outline:none;';
+ tabEl.replaceWith(input);
+ input.focus();
+ input.select();
+
+ const finish = () => {
+ proj.name = input.value.trim() || proj.name;
+ renderProjectTabs();
+ };
+ input.addEventListener('blur', finish);
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') input.blur();
+ if (e.key === 'Escape') { input.value = proj.name; input.blur(); }
+ });
+}
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ TERMINAL TAB MANAGEMENT (per project) ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+async function addTerminalTab(project) {
+ const tabId = project.nextTerminalId++;
+
+ const containerEl = document.createElement('div');
+ containerEl.className = 'terminal-instance';
+ project.dom.termContainer.appendChild(containerEl);
+
+ const term = new Terminal({
+ fontSize: 14,
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Consolas', monospace",
+ cursorBlink: true,
+ cursorStyle: 'bar',
+ allowProposedApi: true,
+ scrollback: 10000,
+ theme: XTERM_THEME
+ });
+
+ const fitAddon = new FitAddon();
+ term.loadAddon(fitAddon);
+ term.loadAddon(new WebLinksAddon());
+ term.open(containerEl);
+
+ const { id: ptyId, shell } = await window.terminalAPI.create();
+
+ const tab = {
+ id: tabId,
+ ptyId,
+ shell,
+ title: `${shell} #${tabId}`,
+ term,
+ fitAddon,
+ containerEl,
+ alive: true
+ };
+
+ term.onData((data) => {
+ if (tab.alive) window.terminalAPI.write(ptyId, data);
+ });
+
+ project.terminalTabs.push(tab);
+ switchTerminalTab(project, tabId);
+
+ requestAnimationFrame(() => {
+ fitAddon.fit();
+ window.terminalAPI.resize(ptyId, term.cols, term.rows);
+ if (project.id === activeProjectId) updateStatusBar(project);
+ });
+
+ return tab;
+}
+
+function switchTerminalTab(project, tabId) {
+ project.activeTerminalId = tabId;
+ project.terminalTabs.forEach(t => {
+ const isActive = t.id === tabId;
+ t.containerEl.classList.toggle('active', isActive);
+ if (isActive) {
+ requestAnimationFrame(() => {
+ t.fitAddon.fit();
+ if (t.alive) window.terminalAPI.resize(t.ptyId, t.term.cols, t.term.rows);
+ t.term.focus();
+ if (project.id === activeProjectId) updateStatusBar(project);
+ });
+ }
+ });
+ renderTerminalTabs(project);
+}
+
+function closeTerminalTab(project, tabId) {
+ const idx = project.terminalTabs.findIndex(t => t.id === tabId);
+ if (idx === -1) return;
+
+ const tab = project.terminalTabs[idx];
+ if (tab.alive) window.terminalAPI.kill(tab.ptyId);
+ tab.term.dispose();
+ tab.containerEl.remove();
+ project.terminalTabs.splice(idx, 1);
+
+ if (project.terminalTabs.length === 0) {
+ addTerminalTab(project);
+ } else if (project.activeTerminalId === tabId) {
+ const newIdx = Math.min(idx, project.terminalTabs.length - 1);
+ switchTerminalTab(project, project.terminalTabs[newIdx].id);
+ }
+ renderTerminalTabs(project);
+}
+
+function getActiveTerminalTab(project) {
+ return project.terminalTabs.find(t => t.id === project.activeTerminalId) || null;
+}
+
+function renderTerminalTabs(project) {
+ const list = project.dom.termTabList;
+ list.innerHTML = '';
+ project.terminalTabs.forEach(tab => {
+ const el = document.createElement('div');
+ el.className = 'tab-item' + (tab.id === project.activeTerminalId ? ' active' : '');
+
+ const title = document.createElement('span');
+ title.className = 'tab-title';
+ title.textContent = tab.title;
+ el.appendChild(title);
+
+ const close = document.createElement('span');
+ close.className = 'tab-close';
+ close.textContent = '\u00d7';
+ close.addEventListener('click', (e) => {
+ e.stopPropagation();
+ closeTerminalTab(project, tab.id);
+ });
+ el.appendChild(close);
+
+ el.addEventListener('click', () => switchTerminalTab(project, tab.id));
+ list.appendChild(el);
+ });
+}
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ BROWSER TAB MANAGEMENT (per project) ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+function addBrowserTab(project, url = 'about:blank') {
+ const id = project.nextBrowserId++;
+
+ const webview = document.createElement('webview');
+ webview.setAttribute('src', url);
+ webview.setAttribute('allowpopups', '');
+ webview.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:none;';
+ project.dom.webviewContainer.appendChild(webview);
+
+ const tab = { id, title: 'New Tab', url, webview };
+
+ webview.addEventListener('did-navigate', (e) => {
+ tab.url = e.url;
+ if (tab.id === project.activeBrowserId && project.id === activeProjectId) {
+ project.dom.urlInput.value = e.url;
+ }
+ });
+
+ webview.addEventListener('did-navigate-in-page', (e) => {
+ tab.url = e.url;
+ if (tab.id === project.activeBrowserId && project.id === activeProjectId) {
+ project.dom.urlInput.value = e.url;
+ }
+ });
+
+ webview.addEventListener('page-title-updated', (e) => {
+ tab.title = e.title || 'Untitled';
+ renderBrowserTabs(project);
+ });
+
+ webview.addEventListener('did-start-loading', () => {
+ if (tab.id === project.activeBrowserId && project.id === activeProjectId) {
+ document.getElementById('webview-status').textContent = 'Loading...';
+ }
+ });
+
+ webview.addEventListener('did-stop-loading', () => {
+ if (tab.id === project.activeBrowserId && project.id === activeProjectId) {
+ document.getElementById('webview-status').textContent = '';
+ }
+ });
+
+ project.browserTabs.push(tab);
+ switchBrowserTab(project, id);
+ renderBrowserTabs(project);
+ return tab;
+}
+
+function switchBrowserTab(project, id) {
+ project.activeBrowserId = id;
+ project.browserTabs.forEach(t => {
+ t.webview.style.display = t.id === id ? 'flex' : 'none';
+ });
+ const tab = project.browserTabs.find(t => t.id === id);
+ if (tab) project.dom.urlInput.value = tab.url;
+ renderBrowserTabs(project);
+}
+
+function closeBrowserTab(project, id) {
+ const idx = project.browserTabs.findIndex(t => t.id === id);
+ if (idx === -1) return;
+
+ project.browserTabs[idx].webview.remove();
+ project.browserTabs.splice(idx, 1);
+
+ if (project.browserTabs.length === 0) {
+ addBrowserTab(project);
+ } else if (project.activeBrowserId === id) {
+ const newIdx = Math.min(idx, project.browserTabs.length - 1);
+ switchBrowserTab(project, project.browserTabs[newIdx].id);
+ }
+ renderBrowserTabs(project);
+}
+
+function renderBrowserTabs(project) {
+ const list = project.dom.browserTabList;
+ list.innerHTML = '';
+ project.browserTabs.forEach(tab => {
+ const el = document.createElement('div');
+ el.className = 'tab-item' + (tab.id === project.activeBrowserId ? ' active' : '');
+
+ const title = document.createElement('span');
+ title.className = 'tab-title';
+ title.textContent = tab.title;
+ el.appendChild(title);
+
+ const close = document.createElement('span');
+ close.className = 'tab-close';
+ close.textContent = '\u00d7';
+ close.addEventListener('click', (e) => {
+ e.stopPropagation();
+ closeBrowserTab(project, tab.id);
+ });
+ el.appendChild(close);
+
+ el.addEventListener('click', () => switchBrowserTab(project, tab.id));
+ list.appendChild(el);
+ });
+}
+
+function getActiveWebview(project) {
+ const tab = project.browserTabs.find(t => t.id === project.activeBrowserId);
+ return tab ? tab.webview : null;
+}
+
+function navigateBrowser(project, input) {
+ const wv = getActiveWebview(project);
+ if (!wv) return;
+
+ let url = input.trim();
+ if (!url) return;
+
+ if (url.startsWith('/') || url.startsWith('./') || url.startsWith('~')) {
+ url = 'file://' + url;
+ } else if (!url.match(/^[a-zA-Z]+:\/\//)) {
+ if (url.match(/^[\w.-]+\.\w{2,}/)) {
+ url = 'https://' + url;
+ } else {
+ url = 'https://duckduckgo.com/?q=' + encodeURIComponent(url);
+ }
+ }
+
+ wv.setAttribute('src', url);
+ const tab = project.browserTabs.find(t => t.id === project.activeBrowserId);
+ if (tab) tab.url = url;
+}
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ PTY DATA/EXIT ROUTING ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+window.terminalAPI.onData((ptyId, data) => {
+ for (const proj of projects) {
+ const tab = proj.terminalTabs.find(t => t.ptyId === ptyId);
+ if (tab) { tab.term.write(data); return; }
+ }
+});
+
+window.terminalAPI.onExit((ptyId, exitCode) => {
+ for (const proj of projects) {
+ const tab = proj.terminalTabs.find(t => t.ptyId === ptyId);
+ if (tab) {
+ tab.alive = false;
+ tab.term.write(`\r\n\x1b[31m[Process exited with code ${exitCode}]\x1b[0m\r\n`);
+ tab.title = `(exited) ${tab.shell}`;
+ renderTerminalTabs(proj);
+ if (proj.id === activeProjectId) updateStatusBar(proj);
+ return;
+ }
+ }
+});
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ DIVIDER DRAG (per project) ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+function setupDividerDrag(project, dom) {
+ let dragging = false;
+
+ dom.divider.addEventListener('mousedown', (e) => {
+ dragging = true;
+ dom.divider.classList.add('dragging');
+ const isVert = dom.workspace.classList.contains('layout-vertical');
+ document.body.style.cursor = isVert ? 'row-resize' : 'col-resize';
+ document.body.style.userSelect = 'none';
+ dom.webviewContainer.style.pointerEvents = 'none';
+ e.preventDefault();
+ });
+
+ document.addEventListener('mousemove', (e) => {
+ if (!dragging) return;
+ const rect = dom.workspace.getBoundingClientRect();
+
+ if (dom.workspace.classList.contains('layout-vertical')) {
+ const pct = ((e.clientY - rect.top) / rect.height) * 100;
+ const clamped = Math.max(15, Math.min(85, pct));
+ dom.termPanel.style.flex = 'none';
+ dom.termPanel.style.height = clamped + '%';
+ dom.browserPanel.style.flex = 'none';
+ dom.browserPanel.style.height = (100 - clamped) + '%';
+ } else {
+ const pct = ((e.clientX - rect.left) / rect.width) * 100;
+ const clamped = Math.max(15, Math.min(85, pct));
+ dom.termPanel.style.flex = 'none';
+ dom.termPanel.style.width = clamped + '%';
+ dom.browserPanel.style.flex = 'none';
+ dom.browserPanel.style.width = (100 - clamped) + '%';
+ }
+ });
+
+ document.addEventListener('mouseup', () => {
+ if (!dragging) return;
+ dragging = false;
+ dom.divider.classList.remove('dragging');
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ dom.webviewContainer.style.pointerEvents = '';
+ const tt = getActiveTerminalTab(project);
+ if (tt) {
+ tt.fitAddon.fit();
+ if (tt.alive) window.terminalAPI.resize(tt.ptyId, tt.term.cols, tt.term.rows);
+ }
+ });
+}
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ LAYOUT ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+const layoutButtons = {
+ split: document.getElementById('btn-layout-split'),
+ terminal: document.getElementById('btn-layout-terminal'),
+ browser: document.getElementById('btn-layout-browser'),
+ vertical: document.getElementById('btn-layout-vertical')
+};
+
+function applyLayout(project, mode) {
+ const ws = project.dom.workspace;
+ ws.classList.remove('layout-terminal-only', 'layout-browser-only', 'layout-vertical');
+
+ // Reset inline drag styles
+ const { termPanel, browserPanel } = project.dom;
+ termPanel.style.flex = '';
+ termPanel.style.width = '';
+ termPanel.style.height = '';
+ browserPanel.style.flex = '';
+ browserPanel.style.width = '';
+ browserPanel.style.height = '';
+
+ switch (mode) {
+ case 'terminal': ws.classList.add('layout-terminal-only'); break;
+ case 'browser': ws.classList.add('layout-browser-only'); break;
+ case 'vertical': ws.classList.add('layout-vertical'); break;
+ }
+
+ project.layout = mode;
+
+ // Update toolbar buttons
+ Object.values(layoutButtons).forEach(b => b.classList.remove('active'));
+ if (layoutButtons[mode]) layoutButtons[mode].classList.add('active');
+ else layoutButtons.split.classList.add('active');
+
+ requestAnimationFrame(() => {
+ const tt = getActiveTerminalTab(project);
+ if (tt) {
+ tt.fitAddon.fit();
+ if (tt.alive) window.terminalAPI.resize(tt.ptyId, tt.term.cols, tt.term.rows);
+ updateStatusBar(project);
+ }
+ });
+}
+
+function setLayoutForActiveProject(mode) {
+ const proj = getActiveProject();
+ if (proj) applyLayout(proj, mode);
+}
+
+layoutButtons.split.addEventListener('click', () => setLayoutForActiveProject('split'));
+layoutButtons.terminal.addEventListener('click', () => setLayoutForActiveProject('terminal'));
+layoutButtons.browser.addEventListener('click', () => setLayoutForActiveProject('browser'));
+layoutButtons.vertical.addEventListener('click', () => setLayoutForActiveProject('vertical'));
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ STATUS BAR ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+function updateStatusBar(project) {
+ const tt = getActiveTerminalTab(project);
+ const dot = document.getElementById('pty-status');
+ const label = document.getElementById('pty-label');
+ const sizeEl = document.getElementById('terminal-size');
+
+ if (tt && tt.alive) {
+ dot.style.background = '#a6e3a1';
+ label.textContent = `${tt.shell} (pty:${tt.ptyId})`;
+ sizeEl.textContent = `${tt.term.cols}x${tt.term.rows}`;
+ } else if (tt) {
+ dot.style.background = '#f38ba8';
+ label.textContent = 'Exited';
+ sizeEl.textContent = `${tt.term.cols}x${tt.term.rows}`;
+ } else {
+ dot.style.background = '#585b70';
+ label.textContent = 'No terminal';
+ sizeEl.textContent = '';
+ }
+}
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ NEW PROJECT BUTTON ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+document.getElementById('new-project-btn').addEventListener('click', () => {
+ createProject();
+});
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ KEYBOARD SHORTCUTS ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+document.addEventListener('keydown', (e) => {
+ const mod = e.ctrlKey || e.metaKey;
+ const proj = getActiveProject();
+ if (!proj) return;
+
+ // Ctrl+Shift+P = new project
+ if (mod && e.shiftKey && e.key === 'P') {
+ e.preventDefault();
+ createProject();
+ return;
+ }
+
+ // Ctrl+Shift+T = new terminal tab
+ if (mod && e.shiftKey && e.key === 'T') {
+ e.preventDefault();
+ addTerminalTab(proj);
+ return;
+ }
+
+ // Ctrl+T = new browser tab
+ if (mod && !e.shiftKey && e.key === 't') {
+ e.preventDefault();
+ addBrowserTab(proj);
+ proj.dom.urlInput.focus();
+ return;
+ }
+
+ // Ctrl+W = close active tab in focused panel
+ if (mod && !e.shiftKey && e.key === 'w') {
+ e.preventDefault();
+ if (proj.focusedPanel === 'terminal' && proj.activeTerminalId) {
+ closeTerminalTab(proj, proj.activeTerminalId);
+ } else if (proj.focusedPanel === 'browser' && proj.activeBrowserId) {
+ closeBrowserTab(proj, proj.activeBrowserId);
+ }
+ return;
+ }
+
+ // Ctrl+Shift+W = close project
+ if (mod && e.shiftKey && e.key === 'W') {
+ e.preventDefault();
+ closeProject(proj.id);
+ return;
+ }
+
+ // Ctrl+L = focus URL bar
+ if (mod && e.key === 'l') {
+ e.preventDefault();
+ proj.dom.urlInput.focus();
+ proj.dom.urlInput.select();
+ return;
+ }
+
+ // Ctrl+1/2/3 = layout modes
+ if (mod && !e.shiftKey && e.key === '1') { e.preventDefault(); setLayoutForActiveProject('split'); return; }
+ if (mod && !e.shiftKey && e.key === '2') { e.preventDefault(); setLayoutForActiveProject('terminal'); return; }
+ if (mod && !e.shiftKey && e.key === '3') { e.preventDefault(); setLayoutForActiveProject('browser'); return; }
+
+ // Ctrl+R = reload browser
+ if (mod && e.key === 'r') {
+ e.preventDefault();
+ const wv = getActiveWebview(proj);
+ if (wv) wv.reload();
+ return;
+ }
+
+ // Ctrl+Tab / Ctrl+Shift+Tab = cycle tabs in focused panel
+ if (mod && e.key === 'Tab') {
+ e.preventDefault();
+ if (proj.focusedPanel === 'terminal' && proj.terminalTabs.length > 1) {
+ const idx = proj.terminalTabs.findIndex(t => t.id === proj.activeTerminalId);
+ const next = e.shiftKey
+ ? (idx - 1 + proj.terminalTabs.length) % proj.terminalTabs.length
+ : (idx + 1) % proj.terminalTabs.length;
+ switchTerminalTab(proj, proj.terminalTabs[next].id);
+ } else if (proj.focusedPanel === 'browser' && proj.browserTabs.length > 1) {
+ const idx = proj.browserTabs.findIndex(t => t.id === proj.activeBrowserId);
+ const next = e.shiftKey
+ ? (idx - 1 + proj.browserTabs.length) % proj.browserTabs.length
+ : (idx + 1) % proj.browserTabs.length;
+ switchBrowserTab(proj, proj.browserTabs[next].id);
+ }
+ return;
+ }
+
+ // Ctrl+PageUp / Ctrl+PageDown = cycle projects
+ if (mod && (e.key === 'PageUp' || e.key === 'PageDown')) {
+ e.preventDefault();
+ if (projects.length > 1) {
+ const idx = projects.findIndex(p => p.id === activeProjectId);
+ const next = e.key === 'PageDown'
+ ? (idx + 1) % projects.length
+ : (idx - 1 + projects.length) % projects.length;
+ switchProject(projects[next].id);
+ }
+ return;
+ }
+});
+
+// ╔═══════════════════════════════════════════════════════════════════════╗
+// ║ INITIALIZE ║
+// ╚═══════════════════════════════════════════════════════════════════════╝
+
+createProject('Project 1');