Files
gstack/extension/sidepanel.html
T
Garry Tan 006dbe19f1 feat(extension): Terminal-only sidebar — auth fix, UX polish, chat rip
The chat queue path is gone. The Chrome side panel is now just an
interactive claude PTY in xterm.js. Activity / Refs / Inspector still
exist behind the `debug` toggle in the footer.

Three threads of change, all from dogfood iteration on top of
cc-pty-import:

1. fix(server): cross-port WS auth via Sec-WebSocket-Protocol
   - Browsers can't set Authorization on a WebSocket upgrade. We had
     been minting an HttpOnly gstack_pty cookie via /pty-session, but
     SameSite=Strict cookies don't survive the cross-port jump from
     server.ts:34567 to the agent's random port from a chrome-extension
     origin. The WS opened then immediately closed → "Session ended."
   - /pty-session now also returns ptySessionToken in the JSON body.
   - Extension calls `new WebSocket(url, [`gstack-pty.<token>`])`.
     Browser sends Sec-WebSocket-Protocol on the upgrade.
   - Agent reads the protocol header, validates against validTokens,
     and MUST echo the protocol back (Chromium closes the connection
     immediately if a server doesn't pick one of the offered protocols).
   - Cookie path is kept as a fallback for non-browser callers (curl,
     integration tests).
   - New integration test exercises the full protocol-auth round-trip
     via raw fetch+Upgrade so a future regression of this exact class
     fails in CI.

2. fix(extension): UX polish on the Terminal pane
   - Eager auto-connect when the sidebar opens — no "Press any key to
     start" friction every reload.
   - Always-visible ↻ Restart button in the terminal toolbar (not
     gated on the ENDED state) so the user can force a fresh claude
     mid-session.
   - MutationObserver on #tab-terminal's class attribute drives a
     fitAddon.fit() + term.refresh() when the pane becomes visible
     again — xterm doesn't auto-redraw after display:none → display:flex.

3. feat(extension): rip the chat tab + sidebar-agent.ts
   - Sidebar is Terminal-only. No more Terminal | Chat primary nav.
   - sidebar-agent.ts deleted. /sidebar-command, /sidebar-chat,
     /sidebar-agent/event, /sidebar-tabs* and friends all deleted.
   - The pickSidebarModel router (sonnet vs opus) is gone — the live
     PTY uses whatever model the user's `claude` CLI is configured with.
   - Quick-actions (🧹 Cleanup / 📸 Screenshot / 🍪 Cookies) survive
     in the Terminal toolbar. Cleanup now injects its prompt into the
     live PTY via window.gstackInjectToTerminal — no more
     /sidebar-command POST. The Inspector "Send to Code" action uses
     the same injection path.
   - clear-chat button removed from the footer.
   - sidepanel.js shed ~900 lines of chat polling, optimistic UI,
     stop-agent, etc.

Net diff: -3.4k lines across 16 files. CLAUDE.md, TODOS.md, and
docs/designs/SIDEBAR_MESSAGE_FLOW.md rewritten to match. The sidebar
regression test (browse/test/sidebar-tabs.test.ts) is rewritten as 27
structural assertions locking the new layout — Terminal sole pane,
no chat input, quick-actions in toolbar, eager-connect, MutationObserver
repaint, restart helper.
2026-04-25 21:03:04 -07:00

189 lines
9.1 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="sidepanel.css">
<link rel="stylesheet" href="lib/xterm.css">
</head>
<body>
<!-- Security shield — reflects ~/.gstack/security/session-state.json status.
Hidden until the sidebar knows its state (avoids flicker on first load).
Consumes /health.security — see browse/src/security.ts getStatus(). -->
<div class="security-shield" id="security-shield" role="status" aria-label="Security status: unknown" style="display:none" title="Security">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<span class="security-shield-label" id="security-shield-label">SEC</span>
</div>
<!-- Connection status banner -->
<div class="conn-banner" id="conn-banner" style="display:none">
<span class="conn-banner-text" id="conn-banner-text">Reconnecting...</span>
<div class="conn-banner-actions" id="conn-banner-actions" style="display:none">
<button class="conn-btn" id="conn-reconnect">Reconnect</button>
<button class="conn-btn conn-copy" id="conn-copy" title="Copy command">/open-gstack-browser</button>
</div>
</div>
<!-- Browser tab bar -->
<div class="browser-tabs" id="browser-tabs" style="display:none"></div>
<!-- Terminal pane is now the sole primary surface. Activity / Refs /
Inspector still exist behind the `debug` toggle in the footer. -->
<main id="tab-terminal" class="tab-content active" role="tabpanel" aria-label="Terminal">
<!-- Toolbar with browser quick-actions on the left, Restart on the right.
Restart is always visible so the user can force a fresh claude any
time, not just from the ENDED state. -->
<div class="terminal-toolbar" id="terminal-toolbar">
<div class="terminal-toolbar-actions">
<button id="chat-cleanup-btn" class="terminal-toolbar-btn" title="Remove ads, banners, popups">🧹 Cleanup</button>
<button id="chat-screenshot-btn" class="terminal-toolbar-btn" title="Take a screenshot">📸 Screenshot</button>
<button id="chat-cookies-btn" class="terminal-toolbar-btn" title="Import cookies from your browser">🍪 Cookies</button>
</div>
<button class="terminal-toolbar-btn" id="terminal-restart-now" title="Restart Claude Code session">↻ Restart</button>
</div>
<div class="terminal-bootstrap" id="terminal-bootstrap">
<div class="terminal-bootstrap-icon"></div>
<p id="terminal-bootstrap-status">Starting Claude Code...</p>
<p class="muted" id="terminal-bootstrap-hint">Real PTY. Real terminal. Real claude.</p>
<pre id="loading-debug" class="muted" style="font-size:11px; font-family:'JetBrains Mono',monospace; white-space:pre-wrap; margin-top:8px; color:#71717A;"></pre>
</div>
<div class="terminal-install-card" id="terminal-install-card" style="display:none">
<p><strong>Claude Code not found</strong></p>
<p class="muted">Install: <a href="https://docs.anthropic.com/en/docs/claude-code" target="_blank">docs.anthropic.com/en/docs/claude-code</a></p>
<button class="install-retry-btn" id="terminal-install-retry">I installed it &mdash; try again</button>
</div>
<div class="terminal-mount" id="terminal-mount" style="display:none"></div>
<div class="terminal-ended" id="terminal-ended" style="display:none">
<p>Session ended.</p>
<button class="install-retry-btn" id="terminal-restart">Start a new session</button>
</div>
</main>
<!-- Debug: Activity Tab (hidden by default) -->
<main id="tab-activity" class="tab-content" role="log" aria-live="polite">
<div class="empty-state" id="empty-state">
<p>Waiting for commands...</p>
<p class="muted">Run a browse command to see activity here.</p>
</div>
<div id="activity-feed"></div>
</main>
<!-- Debug: Refs Tab (hidden by default) -->
<main id="tab-refs" class="tab-content">
<div class="empty-state" id="refs-empty">
<p>No refs yet</p>
<p class="muted">Run <code>snapshot</code> to see element refs.</p>
</div>
<div id="refs-list"></div>
<div class="refs-footer" id="refs-footer"></div>
</main>
<!-- Debug: Inspector Tab (hidden by default) -->
<main id="tab-inspector" class="tab-content">
<!-- Toolbar: always visible -->
<div class="inspector-toolbar" id="inspector-toolbar">
<button class="inspector-pick-btn" id="inspector-pick-btn" title="Pick an element (click, then click any element on the page)">
<span class="inspector-pick-icon">&#x271B;</span> Pick
</button>
<span class="inspector-selected" id="inspector-selected"></span>
<span class="inspector-mode-badge" id="inspector-mode-badge" style="display:none"></span>
<div style="flex:1"></div>
<button id="inspector-cleanup-btn" class="inspector-action-btn" title="Remove ads, banners, popups">🧹</button>
<button id="inspector-screenshot-btn" class="inspector-action-btn" title="Take a screenshot">📸</button>
</div>
<!-- Inspector content area -->
<div class="inspector-content" id="inspector-content">
<!-- Empty state (before first pick) -->
<div class="inspector-empty" id="inspector-empty">
<div class="inspector-empty-icon">&#x271B;</div>
<p>Pick an element to inspect</p>
<p class="muted">Click the button above, then click any element on the page</p>
</div>
<!-- Loading state -->
<div class="inspector-loading" id="inspector-loading" style="display:none">
<div class="inspector-loading-text">Inspecting...</div>
<div class="inspector-skeleton">
<div class="inspector-skeleton-bar"></div>
<div class="inspector-skeleton-bar"></div>
<div class="inspector-skeleton-bar"></div>
</div>
</div>
<!-- Error state -->
<div class="inspector-error" id="inspector-error" style="display:none"></div>
<!-- Inspector data panels -->
<div class="inspector-panels" id="inspector-panels" style="display:none">
<!-- Box Model -->
<div class="inspector-section" id="inspector-boxmodel-section">
<div class="inspector-section-header">Box Model</div>
<div class="inspector-boxmodel" id="inspector-boxmodel"></div>
</div>
<!-- Matched Rules -->
<div class="inspector-section" id="inspector-rules-section">
<button class="inspector-section-toggle" data-section="rules" aria-expanded="true">
<span class="inspector-toggle-arrow">&#x25BC;</span>
<span>Matched Rules</span>
<span class="inspector-rule-count" id="inspector-rule-count"></span>
</button>
<div class="inspector-section-body" id="inspector-rules" role="tree"></div>
</div>
<!-- Computed Styles -->
<div class="inspector-section" id="inspector-computed-section">
<button class="inspector-section-toggle collapsed" data-section="computed" aria-expanded="false">
<span class="inspector-toggle-arrow">&#x25B6;</span>
<span>Computed</span>
</button>
<div class="inspector-section-body collapsed" id="inspector-computed"></div>
</div>
<!-- Quick Edit -->
<div class="inspector-section" id="inspector-quickedit-section">
<button class="inspector-section-toggle collapsed" data-section="quickedit" aria-expanded="false">
<span class="inspector-toggle-arrow">&#x25B6;</span>
<span>Quick Edit</span>
</button>
<div class="inspector-section-body collapsed" id="inspector-quickedit"></div>
</div>
</div>
</div>
<!-- Send to Agent: sticky bottom -->
<div class="inspector-send" id="inspector-send" style="display:none">
<button class="inspector-send-btn" id="inspector-send-btn">Send to Agent</button>
</div>
</main>
<!-- Footer with connection + debug toggle -->
<footer>
<div class="footer-left">
<button class="debug-toggle" id="debug-toggle" title="Toggle debug panels">debug</button>
<button class="footer-btn" id="reload-sidebar" title="Reload sidebar">reload</button>
</div>
<div class="footer-right">
<span class="dot" id="footer-dot"></span>
<span class="footer-port" id="footer-port" title="Click to change port"></span>
<input type="text" class="port-input" id="port-input" placeholder="34567" autocomplete="off" style="display:none">
</div>
</footer>
<!-- Debug tab bar (hidden by default) -->
<nav class="tabs debug-tabs" id="debug-tabs" role="tablist" style="display:none">
<button class="tab" role="tab" data-tab="activity">Activity</button>
<button class="tab" role="tab" data-tab="refs">Refs</button>
<button class="tab" role="tab" data-tab="inspector">Inspector</button>
<button class="tab close-debug" id="close-debug" title="Close debug">&times;</button>
</nav>
<script src="lib/xterm.js"></script>
<script src="lib/xterm-addon-fit.js"></script>
<script src="sidepanel.js"></script>
<script src="sidepanel-terminal.js"></script>
</body>
</html>