diff --git a/extension/manifest.json b/extension/manifest.json index 81b31804..502c5bb7 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -4,7 +4,7 @@ "version": "0.1.0", "description": "Live activity feed and @ref overlays for gstack browse", "permissions": ["sidePanel", "storage", "activeTab", "scripting"], - "host_permissions": ["http://127.0.0.1:*/"], + "host_permissions": ["http://127.0.0.1:*/", "ws://127.0.0.1:*/"], "action": { "default_icon": { "16": "icons/icon-16.png", diff --git a/extension/sidepanel-terminal.js b/extension/sidepanel-terminal.js new file mode 100644 index 00000000..b74406d7 --- /dev/null +++ b/extension/sidepanel-terminal.js @@ -0,0 +1,324 @@ +/** + * Terminal sidebar tab — interactive Claude Code PTY in xterm.js. + * + * Lifecycle (per plan + codex review): + * 1. Sidebar opens. Terminal is the default-active tab. + * 2. Bootstrap card shows "Press any key to start Claude Code." + * 3. On first keystroke (lazy spawn — codex finding #8): the extension + * a) POSTs /pty-session on the browse server with the AUTH_TOKEN to + * mint a short-lived HttpOnly cookie scoped to the terminal-agent. + * b) Opens ws://127.0.0.1:/ws — the cookie travels + * automatically. Terminal-agent validates the cookie + the + * chrome-extension:// Origin (codex finding #9), then spawns + * claude in a PTY. + * 4. Bytes pump both ways. Resize observer sends {type:"resize"} text + * frames; tab-switch hooks send {type:"tabSwitch"} frames. + * 5. PTY exits or WS closes -> we show "Session ended" with a restart + * button. We do NOT auto-reconnect (codex finding #8: auto-reconnect + * = burn fresh claude session every time). + * + * Keep this file dependency-free. xterm.js + xterm-addon-fit are loaded + * via + + diff --git a/extension/sidepanel.js b/extension/sidepanel.js index 6f449990..423f0468 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -914,14 +914,22 @@ const debugTabs = document.getElementById('debug-tabs'); const closeDebug = document.getElementById('close-debug'); let debugOpen = false; +// Resolve the primary surface tab the user expects to see when debug is +// closed. Default-active is Terminal per /plan-eng-review Issue 1B. +function activePrimaryPaneId() { + const sel = document.querySelector('.primary-tab.active'); + const pane = sel?.dataset.pane; + return pane ? `tab-${pane}` : 'tab-terminal'; +} + debugToggle.addEventListener('click', () => { debugOpen = !debugOpen; debugToggle.classList.toggle('active', debugOpen); debugTabs.style.display = debugOpen ? 'flex' : 'none'; if (!debugOpen) { - // Close debug panels, show chat + // Close debug panels, restore the active primary surface (Terminal or Chat). document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - document.getElementById('tab-chat').classList.add('active'); + document.getElementById(activePrimaryPaneId()).classList.add('active'); document.querySelectorAll('.debug-tabs .tab').forEach(t => t.classList.remove('active')); } }); @@ -931,7 +939,7 @@ closeDebug.addEventListener('click', () => { debugToggle.classList.remove('active'); debugTabs.style.display = 'none'; document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - document.getElementById('tab-chat').classList.add('active'); + document.getElementById(activePrimaryPaneId()).classList.add('active'); }); document.querySelectorAll('.debug-tabs .tab:not(.close-debug)').forEach(tab => { @@ -945,6 +953,30 @@ document.querySelectorAll('.debug-tabs .tab:not(.close-debug)').forEach(tab => { }); }); +// Primary-tab switching: Terminal | Chat. Pure show/hide. Both panes keep +// their state when hidden — switching to Chat doesn't kill the PTY, and +// switching back to Terminal doesn't reset the chat scroll position. +document.querySelectorAll('.primary-tab').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.primary-tab').forEach(b => { + b.classList.remove('active'); + b.setAttribute('aria-selected', 'false'); + }); + btn.classList.add('active'); + btn.setAttribute('aria-selected', 'true'); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + const pane = btn.dataset.pane; + document.getElementById(`tab-${pane}`).classList.add('active'); + // Close debug when switching primary tabs (debug is opt-in). + if (debugOpen) { + debugOpen = false; + debugToggle.classList.remove('active'); + debugTabs.style.display = 'none'; + document.querySelectorAll('.debug-tabs .tab').forEach(t => t.classList.remove('active')); + } + }); +}); + // ─── Activity Feed ────────────────────────────────────────────── function getEntryClass(entry) { @@ -1660,6 +1692,16 @@ function updateConnection(url, token) { const wasConnected = !!serverUrl; serverUrl = url; serverToken = token || null; + // Expose for sidepanel-terminal.js (PTY surface). The terminal pane needs + // the bootstrap token to POST /pty-session and the port to derive the WS + // URL. We never expose the PTY token — it lives in an HttpOnly cookie. + if (url) { + try { window.gstackServerPort = parseInt(new URL(url).port, 10); } catch {} + window.gstackAuthToken = token || null; + } else { + window.gstackServerPort = null; + window.gstackAuthToken = null; + } if (url) { document.getElementById('footer-dot').className = 'dot connected'; const port = new URL(url).port;