Skip to content

Feature/implement sess-aware html viewer - #1852

Open
Fred-Wu wants to merge 14 commits into
REditorSupport:mainfrom
Fred-Wu:feature/reuse-html-widget-viewer
Open

Fred-Wu wants to merge 14 commits into
REditorSupport:mainfrom
Fred-Wu:feature/reuse-html-widget-viewer

Conversation

@Fred-Wu

@Fred-Wu Fred-Wu commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1421

Summary

HTML outputs previously opened a new tab each time, without session-specific navigation or restoration. This PR introduces one reusable HTML Viewer per R session, covering widgets such as Plotly, flextable, and profvis, plus other standalone HTML pages.

Changes

  • Share local HTML opened through viewer, page_viewer, and browseURL() in the same session history.
  • Add Back/Forward, deletion, and session-info icons in the VS Code native editor toolbar.
  • Reuse the data/list viewers’ session-info tooltip in the tab title, and process-lifecycle monitoring.
  • Retain the latest 50 output paths and selected position after tab closure, session detachment/reconnection, and VS Code reload.
  • Add R: Restore HTML Viewer, with session selection when multiple histories are available.
  • Remove saved history when the originating R process is confirmed to have exited.
  • Isolate complete widget documents in an iframe so their styles do not affect the toolbar.
  • Handle concurrent output requests, stale loads, disposed panels, missing historical files, and changing dependency directories.
  • Normalise HTML Viewer tab names while preserving individual output titles in history.

Existing viewer placement and disable settings are preserved. Shiny applications and other HTTP/HTTPS pages continue to use their existing browser route.

Scope

History stores file references rather than copies of HTML outputs or dependencies; those files must remain available. R Markdown Preview and Interactive’s expanded-output panels retain their separate implementations.

Validation

Build, typecheck, lint, and focused tests passed. Coverage includes session ownership, concurrent loads, navigation controls, persistence and restoration, process exit cleanup, viewer routing, and real VS Code iframe rendering with relative dependencies.

- Keep up to 50 HTML outputs per session while the Viewer is open
- Add Back/Forward buttons, keyboard shortcuts, and session info
- Isolate widget content in an iframe
- Handle stale loads, missing files, and Viewer disposal
- Add navigation, session ownership, and browser rendering tests
- Retain session history and selection after the Viewer tab closes
- Persist history across VS Code reloads and session reconnects
- Add R: Restore HTML Viewer and reuse existing session panels
- Clear saved history when the originating R process exits
- Add lifecycle, persistence, and browser restoration tests
- Share viewer, page_viewer, and local browser HTML in session history
- Include profvis outputs in navigation and restoration
- Normalise tab names to HTML Viewer
- Preserve output titles and existing HTTP/HTTPS browser routing
@Fred-Wu Fred-Wu changed the title Feature/reuse html widget viewer Feature/implement sess-aware html viewer Oct 8, 2026
@Fred-Wu
Fred-Wu marked this pull request as ready for review October 8, 2026 08:10
@Fred-Wu
Fred-Wu requested review from eitsupi, randy3k and renkun-ken and removed request for eitsupi, randy3k and renkun-ken October 8, 2026 08:14

@eitsupi eitsupi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ChatGPT-assisted review

I reviewed this PR with ChatGPT, focusing primarily on separation of concerns and maintainability.

Overall, the session-scoped HTML Viewer, retained history, and lifecycle handling look well thought out. The new "ViewerSessionContext" is also a useful improvement.

I have a few concerns about the implementation boundaries:

  1. Session management and Viewer responsibilities

    "webViewer/index.ts" imports "activeSession" and "getViewerSessionContext" from "session.ts", while "session.ts" already imports the Viewer, creating a circular dependency.

    Ideally, the Viewer should manage presentation and history without knowing how sessions are discovered or resolved. Could we pass the necessary session resolver and active-session accessor when initializing the Viewer manager? This seems like a small improvement in responsibility separation rather than a need for a larger abstraction.

  2. Reliance on VS Code internals

    "webViewer/webview/toolbar.ts" constructs a "fake.html" iframe URL using VS Code-specific internal parameters such as "id" and "vscode-coi".

    This concerns me because the rendering implementation now depends on undocumented VS Code behavior. Could we investigate an approach based on supported Webview APIs? If this workaround is unavoidable, its assumptions and compatibility risks should at least be documented clearly.

  3. Session identity consistency (minor)

    "WidgetHistoryStore.remember()" distinguishes processes using host/PID, but "HtmlWidgetViewerManager.open()" reuses panels based only on "sessionId". It would be good to clarify the identity contract or ensure these two components behave consistently if a session ID is reused for a different process.

These concerns are about the current design rather than hypothetical future backends. I would avoid introducing additional abstractions solely for future arf integration (YAGNI). The existing "ViewerSessionContext" is a reasonable starting point, and keeping responsibilities clearly separated should be sufficient for now.

Thanks for the substantial work on this feature!

@Fred-Wu

Fred-Wu commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

2. eliance on VS Code internals
"webViewer/webview/toolbar.ts" constructs a "fake.html" iframe URL using VS Code-specific internal parameters such as "id" and "vscode-coi".
This concerns me because the rendering implementation now depends on undocumented VS Code behavior. Could we investigate an approach based on supported Webview APIs? If this workaround is unavoidable, its assumptions and compatibility risks should at least be documented clearly.

Thanks for reviewing this @eitsupi. I agree that this could cause compatibility issues if VS Code changes those parameters in the future. Using the native editor toolbar would be an alternative, but we would just have less control over its layout and behaviour. Also, given the unknown number of extensions users may have installed that add toolbar buttons, the toolbar could become crowded.

Here is what it looks like if using the native toolbar

Screen.Recording.2026-10-08.at.11.08.31.pm.mov

- Remove the custom toolbar and internal iframe workaround
- Show session information through the tab title
- Preserve process identity, exit handling, and history across reloads

@renkun-ken renkun-ken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the latest revision, 6c1422d. The injected session access and native editor toolbar address the earlier circular dependency and private iframe concerns. I found two rendering regressions and left inline comments with concrete reproductions.

Validation: built and typechecked an isolated snapshot; focused lint passed; 22 Node/content tests and 62 extension tests passed locally (VS Code 1.110.0). Additional browser reproductions failed for inline script/tag preservation and an existing resource base: a valid script containing '</body>' stops executing, SVG/script <title> content is removed, and <base href="assets/"> makes fetch('data.json') request the output directory instead of assets/data.json. These cases are not covered by the passing tests.

Comment thread src/webViewer/index.ts
Comment on lines +418 to +422
let html = /<head\b[^>]*>/i.test(source)
? source.replace(/<title\b[^>]*>[\s\S]*?<\/title\s*>/gi, '').replace(/<head\b[^>]*>/i, match => match + head)
: `<!doctype html><html><head>${head}</head><body>${source}</body></html>`;
const script = `<script src="${escapeHtml(String(scriptUri))}" data-generation="${generation}" data-session-owned="${sessionOwned}" data-viewer-state="${escapeHtml(JSON.stringify(state ?? null))}"></script>`;
html = /<\/body\s*>/i.test(html) ? html.replace(/<\/body\s*>/i, script + '</body>') : html + script;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Restrict document edits to actual head/body elements

These regex replacements also match tag-shaped text inside scripts and SVGs. For example, a valid standalone page containing <script>const example = '</body>'; window.example = example;</script> gets the viewer script inserted into that string, including a closing </script> that terminates the original script; I reproduced the page's scripts failing in a real VS Code webview. The global title replacement likewise removes an SVG's accessible <title> and alters strings such as '<svg><title>Accessible chart</title></svg>' (Plotly's JS bundle contains this pattern). Please locate the actual document head/title and body boundary with an HTML-aware parser or tokenizer, preserving script text and SVG titles, and cover these cases in the rendering tests.

Comment thread src/webViewer/index.ts
worker-src https: data: filesystem: blob:;
frame-src https: data: blob:;
`;
const head = `<meta http-equiv="Content-Security-Policy" content="${CSP}"><base href="${escapeHtml(baseUri)}/"><title>${escapeHtml(title)}</title><link rel="stylesheet" href="${escapeHtml(String(styleUri))}">`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Preserve an output's existing resource base

The new base is prepended before the output's own <base>, so the browser ignores the authored base and resolves every relative resource against the HTML file's directory. I reproduced this with index.html containing <base href="assets/">, an existing assets/data.json, and fetch('data.json'): the webview requests .../data.json and fails instead of loading .../assets/data.json. Absolute authored bases are overridden too. Please inject the directory base only when no effective base is present; when an existing base is relative, resolve it against the original file location and convert local targets to webview URIs. Add a rendering test with an authored base and a relative resource.

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.

Create a fixed Viewer pane instead of spawning new ones

3 participants