Skip to content

Feature/implement sess-aware html viewer - #1852

Merged
Fred-Wu merged 26 commits into
REditorSupport:mainfrom
Fred-Wu:feature/reuse-html-widget-viewer
Oct 11, 2026
Merged

Fred-Wu merged 26 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.
  • Add Back/Forward controls to the VS Code native editor toolbar, with Remove and session-info controls in the overflow menu.
  • 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.
  • Render HTML documents directly in the Webview, preserving authored HTML content, scripts, titles, and relative dependencies, including existing HTML base URLs.
  • Restrict local resource access to permitted directories while supporting dependencies within trusted workspace locations.
  • Handle concurrent output requests, stale loads, disposed panels, missing historical files, and changing dependency directories.
  • Display an error page for missing or unreadable HTML files, with navigation immediately available.
  • Show loading progress in the VS Code status bar, with output details in the tooltip. Navigation becomes available after five seconds without load confirmation while the HTML continues loading. After ten seconds, the spinner changes to a static message until loading is confirmed or the output is replaced. The indicator is hidden when switching to another tab and dismissed when navigating away or closing the viewer.
  • 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.

Build, typecheck, lint, and tests passed on Windows, macOS, and Linux. Coverage includes session ownership, concurrent loads, navigation controls, persistence and restoration, process exit cleanup, viewer routing, loading timeouts, CSP-blocked acknowledgements, resource access restrictions, and real VS Code Webview rendering with relative dependencies.

@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

@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 Outdated
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 Outdated
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.

Fred-Wu added 14 commits October 9, 2026 21:01
- 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
- Remove the custom toolbar and internal iframe workaround
- Show session information through the tab title
- Preserve process identity, exit handling, and history across reloads
@Fred-Wu
Fred-Wu force-pushed the feature/reuse-html-widget-viewer branch from 6c1422d to 71b794a Compare October 9, 2026 10:03

@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.

Follow-up review of 71b794a against the previously reviewed 6c1422d. The feature has been rebased onto current main, but the HTML rendering implementation is unchanged. Both earlier P2 findings remain unresolved: tag/script preservation and authored resource bases.

The rebase also introduces a P1 build blocker, detailed inline: main removed fs-extra and its declarations, but this PR still imports it in the viewer and two test files. I reproduced compilation failing with Could not resolve "fs-extra" and typecheck reporting TS2307 for all three imports in an isolated snapshot using only dependencies declared by the latest manifest. The latest packaging and all three platform test jobs fail on that same compile error; lint also fails. The earlier 84 passing tests apply to 6c1422d, not this revision.

Comment thread src/webViewer/index.ts Outdated
import { readContent, UriIcon } from '../util';
import { extensionContext } from '../extension';
import { randomUUID } from 'crypto';
import { readFile } from 'fs-extra';

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.

[P1] Migrate the remaining fs-extra imports after rebasing

Current main removed fs-extra and @types/fs-extra from package.json and the lockfile, so this import prevents the extension from building with the latest dependency set. I reproduced node esbuild.js failing at this line with Could not resolve "fs-extra"; the packaging job and macOS/Linux/Windows test jobs fail for the same reason. Typecheck also reports TS2307 here and in src/test/suite/htmlWidgetViewer.test.ts:2 and src/test/suite/dataViewerSessions.test.ts:2. Please use node:fs/promises for this read and migrate the two tests' imports/stubs to match before rerunning the checks. A local installation retaining the removed package can mask the failure.

@Fred-Wu
Fred-Wu requested review from eitsupi and renkun-ken October 9, 2026 11:50

@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 head, 6296d15, including the changes since 71b794a. All three findings from my earlier reviews are addressed: the undeclared fs-extra imports are removed, HTML source splicing preserves authored scripts and SVG titles, and local authored base URLs resolve correctly with the required resource roots.

Validation in an isolated snapshot with frozen-lockfile dependencies: build, TypeScript compilation, and whole-source lint passed; 22 Node/content tests and 79 focused extension tests passed on VS Code 1.110.0. An additional reproduction through RMarkdownKnitManager exposed one new P2 regression from the js-yaml upgrade, detailed inline: merged frontmatter silently loses its custom knit function because the Knit parser still uses the new default schema.

Comment thread package.json Outdated

@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.

I haven't looked into it in detail, but could you revert version updates for unrelated dependent packages?

@Fred-Wu

Fred-Wu commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

I have restored package dependencies.

@Fred-Wu
Fred-Wu requested review from eitsupi and renkun-ken October 10, 2026 02:29

@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 follow-up review (752621b)

I found one concern:

Local resource access scope: In getWebviewHtml(), an authored <base href="/"> causes the filesystem root to be added to localResourceRoots. This behavior is also explicitly covered by a test. Since the displayed HTML can execute scripts, allowing arbitrary document-provided base paths to expand the Webview's filesystem access seems unsafe. Could we restrict additional resource roots to trusted directories while preserving support for legitimate relative dependencies?

One non-blocking UI suggestion: all four toolbar actions are still in the navigation group. I would prefer keeping Back/Forward visible and moving Remove/Info into the overflow menu. The Info command could also be named more explicitly to indicate that it toggles session information in the tab title.

Fred Wu added 3 commits October 10, 2026 15:02
- Keep the current output visible while the next page loads
- Disable navigation buttons until the selected page is ready
- Preserve resource-access restrictions
- Add regression tests for smooth transitions and button states
@Fred-Wu
Fred-Wu requested a review from eitsupi October 10, 2026 05:41

@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 follow-up review (27cdc1f)

One remaining concern is the new widget/loaded handshake. If the injected script cannot run (e.g., an HTML document with script-src 'none') or the window load event never completes, entry.loading remains true indefinitely, disabling Back, Forward, and Remove.

Could we provide a fallback so navigation remains usable even when the acknowledgement cannot arrive?

- Show loading progress in the VS Code status bar
- Unlock navigation after five seconds while retaining the loading message
- Clear progress on completion, failure, replacement, or disposal
- Cover timeouts, stale loads, and CSP-blocked confirmations
@Fred-Wu
Fred-Wu marked this pull request as draft October 10, 2026 11:11
@Fred-Wu
Fred-Wu marked this pull request as ready for review October 10, 2026 11:21
@Fred-Wu
Fred-Wu requested a review from eitsupi October 10, 2026 11:34

@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 follow-up review (e2c6b5a)

startLoading() leaves the window.withProgress() task pending after the five-second fallback. For HTML with script-src 'none', widget/loaded cannot arrive, so navigation recovers but the window-level “Still loading” progress indicator remains until the output is replaced or the panel is closed.

Please end the progress task when the fallback releases navigation, reusing entry.finishLoading?.() and then updating the toolbar. Ending the wait does not need to imply that the HTML loaded successfully.

The timeout test should also assert that progress completes without receiving widget/loaded, rather than expecting task.done === false.

@Fred-Wu

Fred-Wu commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

ChatGPT-assisted follow-up review (e2c6b5a)

startLoading() leaves the window.withProgress() task pending after the five-second fallback. For HTML with script-src 'none', widget/loaded cannot arrive, so navigation recovers but the window-level “Still loading” progress indicator remains until the output is replaced or the panel is closed.

Please end the progress task when the fallback releases navigation, reusing entry.finishLoading?.() and then updating the toolbar. Ending the wait does not need to imply that the HTML loaded successfully.

The timeout test should also assert that progress completes without receiving widget/loaded, rather than expecting task.done === false.

I intentionally kept this behaviour as during loading a large html or an unknown successfulness loading users could navigate away, delete or close the window to dismiss the progress indicator.

@eitsupi

eitsupi commented Oct 10, 2026

Copy link
Copy Markdown
Member

I see.
How about adding a short code comment near the timeout handler explaining that this is intentional?
Without that explanation, the unresolved withProgress() task could easily be mistaken for a bug during future maintenance.

- Show “Load not confirmed. Please wait or retry.” after ten seconds
- Keep the five-second navigation fallback after HTML assignment
- Clear loading status on completion, replacement, or viewer closure
- Remove unused test configuration and obsolete test mocks
- Update loading and CSP regression tests
@Fred-Wu

Fred-Wu commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

I see. How about adding a short code comment near the timeout handler explaining that this is intentional? Without that explanation, the unresolved withProgress() task could easily be mistaken for a bug during future maintenance.

Updated with a 10s final state static message if load is still not confirmed. User actions such as navigating away, rendering again, removing or closing would dismiss the message. Switching to another tab hides the message as well. Comments are also added to the codes.

@Fred-Wu Fred-Wu added this to the 3.2.0 milestone Oct 11, 2026

@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.

One minor documentation note: the PR description still mentions iframe-based rendering, although the implementation now uses the native editor toolbar without that iframe.

@Fred-Wu
Fred-Wu merged commit 577fea0 into REditorSupport:main Oct 11, 2026
5 checks passed
@Fred-Wu
Fred-Wu deleted the feature/reuse-html-widget-viewer branch October 11, 2026 05:02
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