mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
ed1e4be2f6
* build: vendor xterm@5 for the Terminal sidebar tab
Adds xterm@5 + xterm-addon-fit as devDependencies and a `vendor:xterm`
build step that copies the assets into `extension/lib/` at build time.
The vendored files are .gitignored so the npm version stays the source
of truth. xterm@5 is eval-free, so no MV3 CSP changes needed.
No runtime callers yet — this just stages the assets.
* feat(server): add pty-session-cookie module for the Terminal tab
Mirrors `sse-session-cookie.ts` exactly. Mints short-lived 30-min HttpOnly
cookies for authenticating the Terminal-tab WebSocket upgrade against
the terminal-agent. Same TTL, same opportunistic-pruning shape, same
"scoped tokens never valid as root" invariant. Two registries instead of
one because the cookie names are different (`gstack_sse` vs `gstack_pty`)
and the token spaces must not overlap.
No callers yet — wired up in the next commit.
* feat(server): add terminal-agent.ts (PTY for the Terminal sidebar tab)
Translates phoenix gbrowser's Go PTY (cmd/gbd/terminal.go) into a Bun
non-compiled process. Lives separately from `sidebar-agent.ts` so a
WS-framing or PTY-cleanup bug can't take down the chat path (codex
outside-voice review caught the coupling risk).
Architecture:
- Bun.serve on 127.0.0.1:0 (never tunneled).
- POST /internal/grant accepts cookie tokens from the parent server over
loopback, authenticated with a per-boot internal token.
- GET /ws upgrades require BOTH (a) Origin: chrome-extension://<id> and
(b) the gstack_pty cookie minted by /pty-session. Either gate alone is
insufficient (CSWSH defense + auth defense).
- Lazy spawn: claude PTY is not started until the WS receives its first
data frame. Idle sidebar opens cost nothing.
- Bun PTY API: `terminal: { rows, cols, data(t, chunk) }` — verified at
impl time on Bun 1.3.10. proc.terminal.write() for input,
proc.terminal.resize() for resize, proc.kill() + 3s SIGKILL fallback
on close.
- process.on('uncaughtException'|'unhandledRejection') handlers so a
framing bug logs but doesn't kill the listener loop.
Test-only `BROWSE_TERMINAL_BINARY` env override lets the integration
tests spawn /bin/bash instead of requiring claude on every CI runner.
Not yet spawned by anything — wired in the next commit.
* feat(server): wire /pty-session route + spawn terminal-agent
Server-side glue connecting the Terminal sidebar tab to the new
terminal-agent process.
server.ts:
- New POST /pty-session route. Validates AUTH_TOKEN, mints a gstack_pty
HttpOnly cookie via pty-session-cookie.ts, posts the cookie value to
the agent's loopback /internal/grant. Returns the terminalPort + Set-Cookie
to the extension.
- /health response gains `terminalPort` (just the port number — never a
shell token). Tokens flow via the cookie path, never /health, because
/health already surfaces AUTH_TOKEN to localhost callers in headed mode
(that's a separate v1.1+ TODO).
- /pty-session and /terminal/* are deliberately NOT added to TUNNEL_PATHS,
so the dual-listener tunnel surface 404s by default-deny.
- Shutdown path now also pkills terminal-agent and unlinks its state files
(terminal-port + terminal-internal-token) so a reconnect doesn't try to
hit a dead port.
cli.ts:
- After spawning sidebar-agent.ts, also spawn terminal-agent.ts. Same
pattern: pkill old instances, Bun.spawn(['bun', 'run', script]) with
BROWSE_STATE_FILE + BROWSE_SERVER_PORT env. Non-fatal if the spawn
fails — chat still works without the terminal agent.
* feat(extension): Terminal as default sidebar tab
Adds a primary tab bar (Terminal | Chat) above the existing tab-content
panes. Terminal is the default-active tab; clicking Chat returns to the
existing claude -p one-shot flow which is preserved verbatim.
manifest.json: adds ws://127.0.0.1:*/ to host_permissions so MV3 doesn't
block the WebSocket upgrade.
sidepanel.html: new primary-tabs nav, new #tab-terminal pane with a
"Press any key to start Claude Code" bootstrap card, claude-not-found
install card, xterm mount point, and "session ended" restart UI. Loads
xterm.js + xterm-addon-fit + sidepanel-terminal.js. tab-chat is no
longer the .active default.
sidepanel.js: new activePrimaryPaneId() helper that reads which primary
tab is selected. Debug-close paths now route back to whichever primary
pane is active (was hardcoded to tab-chat). Primary-tab click handler
toggles .active classes and aria-selected. window.gstackServerPort and
window.gstackAuthToken exposed so sidepanel-terminal.js can build the
/pty-session POST and the WS URL.
sidepanel-terminal.js (new): xterm.js lifecycle. Lazy-spawn — first
keystroke fires POST /pty-session, then opens
ws://127.0.0.1:<terminalPort>/ws. Origin + cookie are set automatically
by the browser. Resize observer sends {type:"resize"} text frames.
ResizeObserver, tab-switch hooks, restart button, install-card retry.
On WS close shows "Session ended, click to restart" — no auto-reconnect
(codex outside-voice flagged that as session-burning).
sidepanel.css: primary-tabs bar + Terminal pane styling (full-height
xterm container, install card, ended state).
* test: terminal-agent + cookie module + sidebar default-tab regression
Three new test files:
terminal-agent.test.ts (16 tests): pty-session-cookie mint/validate/
revoke, Set-Cookie shape (HttpOnly + SameSite=Strict + Path=/, NO Secure
since 127.0.0.1 over HTTP), source-level guards that /pty-session and
/terminal/* are NOT in TUNNEL_PATHS, /health does NOT surface ptyToken
or gstack_pty, terminal-agent binds 127.0.0.1, /ws upgrade enforces
chrome-extension:// Origin AND gstack_pty cookie, lazy-spawn invariant
(spawnClaude is called from message handler, not upgrade), uncaughtException/
unhandledRejection handlers exist, SIGINT-then-SIGKILL cleanup.
terminal-agent-integration.test.ts (7 tests): spawns the agent as a real
subprocess in a tmp state dir. Verifies /internal/grant accepts/rejects
the loopback token, /ws gates (no Origin → 403, bad Origin → 403, no
cookie → 401), real WebSocket round-trip with /bin/bash via the
BROWSE_TERMINAL_BINARY override (write 'echo hello-pty-world\n', read it
back), and resize message acceptance.
sidebar-tabs.test.ts (13 tests): structural regression suite locking the
load-bearing invariants of the default-tab change — Terminal is .active,
Chat is not, xterm assets are loaded, debug-close path no longer hardcodes
tab-chat (uses activePrimaryPaneId), primary-tab click handler exists,
chat surface is not accidentally deleted, terminal JS does NOT auto-
reconnect on close, manifest declares ws:// + http:// localhost host
permissions, no unsafe-eval.
Plan called for Playwright + extension regression; the codebase doesn't
ship Playwright extension launcher infra, so we follow the existing
extension-test pattern (source-level structural assertions). Same
load-bearing intent — locks the invariants before they regress.
* docs: Terminal flow + threat model + v1.1 follow-ups
SIDEBAR_MESSAGE_FLOW.md: new "Terminal flow" section. Documents the WS
upgrade path (/pty-session cookie mint → /ws Origin + cookie gate →
lazy claude spawn), the dual-token model (AUTH_TOKEN for /pty-session,
gstack_pty cookie for /ws, INTERNAL_TOKEN for server↔agent loopback),
and the threat-model boundary — the Terminal tab bypasses the entire
prompt-injection security stack on purpose; user keystrokes are the
trust source. That trust assumption is load-bearing on three transport
guarantees: local-only listener, Origin gate, cookie auth. Drop any
one of those three and the tab becomes unsafe.
CLAUDE.md: extends the "Sidebar architecture" note to include
terminal-agent.ts in the read-this-first list. Adds a "Terminal tab is
its own process" note so a future contributor doesn't bolt PTY logic
onto sidebar-agent.ts.
TODOS.md: three new follow-ups under a new "Sidebar Terminal" section:
- v1.1: PTY session survives sidebar reload (Issue 1C deferred).
- v1.1+: audit /health AUTH_TOKEN distribution (codex finding #2 —
a pre-existing soft leak that cc-pty-import sidesteps but doesn't
fix).
- v1.1+: apply terminal-agent's process.on exception handlers to
sidebar-agent.ts (codex finding #4 — chat path has no fatal
handlers).
* 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.
* feat: live tab awareness for the Terminal pane
claude in the PTY now has continuous tab-aware context. Three pieces:
1. Live state files. background.js listens to chrome.tabs.onActivated /
onCreated / onRemoved / onUpdated (throttled to URL/title/status==
complete so loading spinners don't spam) and pushes a snapshot. The
sidepanel relays it as a custom event; sidepanel-terminal.js sends
{type:"tabState"} text frames over the live PTY WebSocket.
terminal-agent.ts writes:
<stateDir>/tabs.json all open tabs (id, url, title, active,
pinned, audible, windowId)
<stateDir>/active-tab.json current active tab (skips chrome:// and
chrome-extension:// internal pages)
Atomic write via tmp + rename so claude never reads a half-written
document. A fresh snapshot is pushed on WS open so the files exist by
the time claude finishes booting.
2. New $B tab-each <command> [args...] meta-command. Fans out a single
command across every open tab, returns
{command, args, total, results: [{tabId, url, title, status, output}]}.
Skips chrome:// pages; restores the originally active tab in a finally
block (so a mid-batch error doesn't leave the user looking at a
different tab); uses bringToFront: false so the OS window doesn't
jump on every fanout. Scope-checks the inner command BEFORE the loop.
3. --append-system-prompt hint at spawn time. Claude is told about both
the state files and the $B tab-each command up front, so it doesn't
have to discover the surface by trial. Passed via the --append-system-
prompt CLI flag, NOT as a leading PTY write — the hint stays out of
the visible transcript.
Tests:
- browse/test/tab-each.test.ts (new) — registration + source-level
invariants (scope check before loop, finally-restore, bringToFront:false,
chrome:// skip) + behavior tests with a mock BrowserManager that verify
iteration order, JSON shape, error handling, and active-tab restore.
- browse/test/terminal-agent.test.ts — three new assertions for
tabState handler shape, atomic-write pattern, and the
--append-system-prompt wiring at spawn.
Verified live: opened 5 tabs, ran $B tab-each url against the live
server, got per-tab JSON results back, original active tab restored
without OS focus stealing.
* chore: drop sidebar-agent test refs after chat rip
Five test files / describe blocks targeted the deleted chat path:
- browse/test/security-e2e-fullstack.test.ts (full-stack chat-pipeline E2E
with mock claude — whole file gone)
- browse/test/security-review-fullstack.test.ts (review-flow E2E with real
classifier — whole file gone)
- browse/test/security-review-sidepanel-e2e.test.ts (Playwright E2E for
the security event banner that was ripped from sidepanel.html)
- browse/test/security-audit-r2.test.ts (5 describe blocks: agent queue
permissions, isValidQueueEntry stateFile traversal, loadSession session-ID
validation, switchChatTab DocumentFragment, pollChat reentrancy guard,
/sidebar-tabs URL sanitization, sidebar-agent SIGTERM→SIGKILL escalation,
AGENT_SRC top-level read converted to graceful fallback)
- browse/test/security-adversarial-fixes.test.ts (canary stream-chunk split
detection on detectCanaryLeak; one tool-output test on sidebar-agent)
- test/skill-validation.test.ts (sidebar agent #584 describe block)
These all assumed sidebar-agent.ts existed and tested chat-queue plumbing,
chat-tab DOM round-trip, chat-polling reentrancy, or per-message classifier
canary detection. With the live PTY there is no chat queue, no chat tab,
no LLM stream to canary-scan, and no per-message subprocess. The Terminal
pane's invariants are covered by the new browse/test/sidebar-tabs.test.ts
(27 structural assertions), browse/test/terminal-agent.test.ts, and
browse/test/terminal-agent-integration.test.ts.
bun test → exit 0, 0 failures.
* chore: bump version and changelog (v1.14.0.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(extension): xterm fills the full Terminal panel height
The Terminal pane only rendered into the top portion of the panel — most
of the panel below the prompt was an empty black gap. Three layered
issues, all about xterm.js measuring dimensions during a layout state
that wasn't ready yet:
1. order-of-operations in connect(): ensureXterm() ran BEFORE
setState(LIVE), so term.open() measured els.mount while it was still
display:none. xterm caches a 0-size viewport synchronously inside
open() and never auto-recovers when the container goes visible.
Flipped: setState(LIVE) → ensureXterm.
2. first fit() ran synchronously before the browser had applied the
.active class transition. Wrapped in requestAnimationFrame so layout
has settled before fit() reads clientHeight.
3. CSS flex-overflow trap: .terminal-mount has flex:1 inside the
flex-column #tab-terminal, but .tab-content's `overflow-y: auto` and
the lack of `min-height: 0` on .terminal-mount meant the item
couldn't shrink below content size. flex:1 then refused to expand
into available space and xterm rendered into whatever its initial
2x2 measurement happened to be.
Fixes:
- extension/sidepanel-terminal.js: reorder + RAF fit
- extension/sidepanel.css: .terminal-mount gets `flex: 1 1 0` +
`min-height: 0` + `position: relative`. #tab-terminal overrides
.tab-content's `overflow-y: auto` to `overflow: hidden` (xterm has
its own viewport scroll; the parent shouldn't compete) and explicitly
re-declares `display: flex; flex-direction: column` for #tab-terminal.active.
bun test browse/test/sidebar-tabs.test.ts → 27/27 pass.
Manually verified: side panel opens → Terminal fills full panel height,
xterm scrollback works, debug-tab toggle still repaints correctly.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
189 lines
9.1 KiB
HTML
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 — 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">✛</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">✛</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">▼</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">▶</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">▶</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">×</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>
|