Skip to content

Fix browser pane screenshots showing placeholder instead of actual page content#247

Open
danshapiro wants to merge 58 commits intomainfrom
fix/browser-pane-screenshots
Open

Fix browser pane screenshots showing placeholder instead of actual page content#247
danshapiro wants to merge 58 commits intomainfrom
fix/browser-pane-screenshots

Conversation

@danshapiro
Copy link
Copy Markdown
Owner

Summary

  • Browser pane screenshots now capture real page content instead of showing "Iframe content is not directly capturable in browser screenshots" placeholder
  • Previously, when taking MCP screenshots of browser panes pointing to localhost services (e.g. dev servers, APIs), the screenshot showed a gray placeholder because the iframe was cross-origin — even though the content was proxied through Freshell
  • Two root causes fixed:
    • Server: The HTTP proxy at /api/proxy/http/:port/ forwarded X-Frame-Options and Content-Security-Policy headers from upstream servers verbatim, which blocked iframe contentDocument access needed for html2canvas screenshot capture
    • Client: Remote browsers (accessing Freshell over LAN/VPN) used TCP port forwarding for localhost URLs, creating cross-origin iframe URLs. Now http: localhost URLs route through the same-origin HTTP proxy regardless of browser location. https: URLs still use TCP forwarding since the HTTP proxy can't do TLS passthrough.

Changes

  • server/proxy-router.tsstripIframeBlockingHeaders() removes x-frame-options, content-security-policy, and content-security-policy-report-only from proxied responses
  • src/components/panes/BrowserPane.tsxbuildHttpProxyUrl() now handles http: localhost URLs for both local and remote browsers
  • server/mcp/freshell-tool.ts — Updated MCP tool instructions to reflect new behavior
  • Tests: 4 proxy header-stripping unit tests, 1 ui-screenshot regression test, 1 MCP instructions verification test, 2 Playwright E2E tests, updated BrowserPane + refresh flow tests

Test plan

  • npm test — all 300+ client test files, server tests, electron tests pass
  • Playwright E2E: browser pane with X-Frame-Options: DENY + CSP: frame-ancestors 'none' renders actual content in screenshot
  • Playwright E2E: truly cross-origin URL falls back to placeholder gracefully
  • Manual verification: MCP screenshot of browser pane pointing to localhost:4567 (canary server with restrictive headers) captures green page with "SCREENSHOT CANARY" text

🤖 Generated with Claude Code

Dan Shapiro and others added 30 commits March 28, 2026 22:12
…hints

When called with unrecognized params (e.g. new-tab with url), the MCP
tool now returns an error listing valid parameters. A specific hint
suggests open-browser when url is passed to new-tab. Help text updated
to clarify the distinction between new-tab and open-browser, with a new
playbook for opening URLs.
- Remove localStorage persistence for tool strip expanded state
- ToolStrip now uses local useState initialized from showTools prop
- ToolBlocks inherit initial expanded state from showTools
- Remove autoExpandAbove/completedToolOffset props (no longer needed)
- All toggle state is session-only, resets on page refresh
Add implementation plan for making terminal URLs clickable (opening in
browser panes) with right-click context menu support for open in pane,
open in tab, open in browser, and copy URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewed all source files referenced by the plan against the actual
codebase and corrected several issues:

- Fix ILinkHandler hover/leave signatures (need range param for OSC 8)
- Add explicit wrapperRef JSX attachment instructions for outer div
- Fix test file reference to canonical path (components/context-menu/)
- Add useMemo dependency array update note for ContextMenuProvider
- Clarify link provider registration order for priority
- Note cleanup needs for both hoveredUrl map and data attribute
- Add context-menu-utils.test.ts to new files list
- Document picker vs direct browser pane design decision

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dan Shapiro and others added 28 commits March 28, 2026 22:26
- Remove unnecessary empty hover/leave stubs on file path links
- Note pre-existing mock staleness in menu-defs.test.ts (6 missing actions)
- Add docs/index.html update step per repo rules
- Add timing safety note for wrapperRef in hover callbacks
- Clarify ContextMenuProvider action wiring locations and imports
- Add balanced parenthesis edge case for URL detection tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Corrects three factual errors in the implementation plan:
1. xterm.js link provider priority is first-registered=highest (not last)
2. menu-defs.test.ts does not exist yet (was incorrectly labeled "update existing")
3. Removes context-menu-constants.ts from modified files list (no changes needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
58 new tests across 8 files plus updates to 2 existing test files,
aligned to the implementation plan's TDD phases. Covers hover state
tracking, URL detection, left-click behavior, context menu integration,
multi-pane integration, and browser-use E2E smoke testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of clickable terminal URLs: module-level map for tracking hovered
URLs per pane, and findUrls utility for detecting http/https URLs in
terminal output text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…link provider

- Left-click on OSC 8 links and detected URLs now opens a browser pane
  (split right) instead of window.open
- Warning modal confirm also opens browser pane instead of window.open
- Add hover/leave callbacks to OSC 8 linkHandler for tracking hovered URL
- Register URL link provider (after file path provider) to detect plain
  http/https URLs in terminal output
- Track hovered URL in module-level map and data-hovered-url DOM attribute
- Clear hover state on terminal dispose and tab hide
- Update existing link warning and keyboard tests for new behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add hoveredUrl optional field to terminal ContextTarget
- Parse hoveredUrl from dataset in parseContextTarget
- Add URL-specific menu items (Open in pane/tab/browser, Copy URL) to
  terminal context menu when hovering a URL
- Add openUrlInPane, openUrlInTab, openUrlInBrowser, copyUrl actions
  to ContextMenuProvider
- Add tests for context-menu-utils and menu-defs URL behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- terminal-url-link-click: verifies URL click in nested pane opens
  browser pane on the correct branch (both plain URL and OSC 8 paths)
- terminal-url-context-menu: verifies URL context menu items appear
  when hovering a URL, are absent without hover, and "Open URL in pane"
  creates a browser pane split

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that when a terminal tab becomes hidden, the hovered URL
module state and DOM attribute are cleared.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With the new URL link provider registered after the file path provider,
the existing test mock was capturing the URL provider instead of the
file path provider. Fixed by only storing the first registered provider.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unnecessary escape in url-utils regex character class
- Capture wrapperRef.current in local variable before cleanup to satisfy
  react-hooks/exhaustive-deps rule

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ks, docs update

- URL detection now preserves balanced parentheses (e.g. Wikipedia URLs)
  while still stripping unbalanced trailing parens
- OSC 8 linkHandler.activate validates http/https scheme before opening
  browser panes; non-http schemes fall back to window.open
- Added clickable URLs feature to docs/index.html feature list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents non-left-click from triggering link activation on OSC 8 links,
file path links, and URL links. Adds tests for right-click and middle-click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align .opencode/.gitignore with upstream best practices: add plans/,
package-lock.json, and .freshell-mcp-state.json. Remove the transient
MCP state file from tracking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace flat list layout with a compact card grid organized by device.
Key changes:
- Device-centric grouping: local tabs first, then remote by device
- Compact card grid (auto-fill 220px min) with colored left borders
- Right-click context menus with Jump/Pull/Open pane/Copy actions
- Segmented controls for status and scope filters
- Hover-to-reveal action labels on cards
- Collapsible "Recently closed" section
- Pane type icons with distinct colors for visual scanning
- Click-to-act: click card to jump (local) or pull (remote/closed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update all 4 test files to match the new device-centric card grid layout:
- Tests now click tab cards directly instead of finding "Open copy" buttons
- Section headings updated for device-centric grouping
- New tests for: device grouping, context menus, segmented filters,
  pane kind icons, multi-pane context menu items
- Explicit cleanup between tests to prevent DOM leakage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip X-Frame-Options and Content-Security-Policy headers from proxied
responses in proxy-router.ts so browser pane iframes can render localhost
content and the MCP screenshot tool captures actual content instead of
a placeholder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Concrete test plan covering proxy header stripping, iframe screenshot
capture, Playwright E2E verification, graceful fallback preservation,
and MCP instructions accuracy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dev servers commonly send X-Frame-Options and Content-Security-Policy
headers that prevent iframe embedding. Since the proxy exists to make
localhost content embeddable in browser panes, strip these headers so
the iframe renders content and the MCP screenshot tool can capture it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents that browser pane iframes using /api/proxy/http/PORT/ URLs
are captured as image content (not placeholders) when the proxy strips
iframe-blocking headers, making the content same-origin accessible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Proxied localhost URLs now render actual iframe content instead of
placeholders. Update both the key gotchas and screenshot guidance
sections to clarify that only truly cross-origin URLs show placeholders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
R4: Fix variable shadowing of 'headers' in proxy-router.ts where the
inner const in the proxy callback shadowed the outer request headers.
Renamed to 'strippedHeaders' for clarity.

R3: Add MCP instructions content verification test ensuring the tool
instructions correctly state that proxied localhost URLs render actual
content (not placeholders) in browser pane screenshots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
R1: Test that proxied localhost URLs with X-Frame-Options: DENY and
CSP frame-ancestors 'none' headers render actual content in the iframe
(not a placeholder), and that the MCP screenshot API succeeds.

R2: Test that truly cross-origin URLs (https://example.com) correctly
fall back to placeholder behavior since they bypass the proxy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…browsers

When the browser accesses Freshell remotely, http: localhost URLs were
using TCP port forwarding which creates cross-origin iframe URLs,
preventing screenshot capture. Now buildHttpProxyUrl handles http:
localhost URLs regardless of browser location, routing through the
same-origin /api/proxy/http/:port/ path. HTTPS URLs still use TCP
forwarding since the HTTP proxy can't do TLS passthrough.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b01dfce5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +21 to +22
'content-security-policy',
'content-security-policy-report-only',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve CSP directives instead of stripping the entire header

Removing content-security-policy and content-security-policy-report-only for every proxied response drops much more than frame-ancestors, including script and sandbox protections that upstream services rely on. In the browser pane this content is served from Freshell’s own origin (/api/proxy/http/:port/...), so stripping full CSP can let a compromised or untrusted localhost app run with authenticated same-origin access to Freshell APIs. Please only relax the embedding directives (e.g., rewrite/remove frame-ancestors) instead of deleting the whole CSP policy.

Useful? React with 👍 / 👎.

)
const expanded = showTools && expandedPref
function ToolStrip({ pairs, isStreaming, showTools = true }: ToolStripProps) {
const [stripExpanded, setStripExpanded] = useState(showTools)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-sync ToolStrip expansion when showTools changes

stripExpanded is initialized from showTools only on mount, so runtime toggles of the “Show tools” setting are ignored for existing strips. A common case is turning tools off in settings while a strip is expanded: it stays expanded because rendering is driven by stale local state, not the updated prop. This regresses the toggle behavior and should be fixed by syncing state to prop changes (or deriving expansion directly from the prop).

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant