Skip to content

HTML preview: basic in-app viewer + "View in Browser" for advanced docs#10

Closed
wynnwu wants to merge 3 commits into
remote-devfrom
003-html-preview-viewer
Closed

HTML preview: basic in-app viewer + "View in Browser" for advanced docs#10
wynnwu wants to merge 3 commits into
remote-devfrom
003-html-preview-viewer

Conversation

@wynnwu

@wynnwu wynnwu commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What

HTML summaries previously degraded to a plain-text fallback in the preview pane ("Use Open to view it styled in your browser"), even though HTML is the format users pick specifically for visual output. This adds a basic, in-app HTML viewer with a few small niceties and a smart escape hatch.

Built via the repo's Spec Kit process: see specs/003-html-preview-viewer/ (spec, research, plan, tasks).

How it behaves

  • Renders styled, in-app (FR-047). HTML summaries now show with their own styling in a WKWebView, read-only.
  • Basic, static, private by design (FR-048):
    • JavaScript disabled (allowsContentJavaScript = false)
    • Remote network loads blocked via a WKContentRuleList scoped to ^https?:// (no auto-fetched remote images/fonts/trackers, honoring Sumbee's local-first promise)
    • No cookies/cache persisted (.nonPersistent() data store)
    • Link clicks open in the user's default browser; the pane never navigates away
  • Zoomable (FR-049). The existing preview font +/- controls drive pageZoom; the size persists, shared with the Markdown preview.
  • "View in Browser" escape hatch (FR-050/051). A deterministic, unit-tested HTMLFeatureScanner flags genuinely interactive/dynamic constructs (scripts, iframe/embed/object, video/audio, canvas, form controls, inline event handlers). When any are present, a labeled View in Browser button appears in the top right of the preview toolbar. Plain styled prose, and notably the stamped YouTube source <a> link, do not trigger it.

No new dependency

WebKit is an Apple system framework; Package.swift is unchanged (the "zero third-party runtime deps" rule holds).

Verification

  • swift build clean (0 warnings), swift build -c release clean
  • swift test green: 68 tests (was 57; +11 HTMLFeatureScannerTests, including the anchor-link false-positive guard)
  • ./scripts/bundle.sh assembles dist/Sumbee.app cleanly

Not launched locally on purpose: ad-hoc signing triggers a Keychain access prompt on each new build (learnings #3/#4). Please validate the rendered output from this branch's build. Things to eyeball: a plain styled HTML summary renders nicely with no button; an HTML summary containing a script/embed/video shows the top-right View in Browser button; font +/- zooms; clicking a link opens your browser.

Out of scope

Live styled rendering during streaming (still streams as text), in-app HTML editing/printing, and an opt-in to run JS/remote loads in-app (the browser is that path).

🤖 Generated with Claude Code

HTML summaries previously degraded to a plain-text fallback in the preview
pane. They now render styled in a basic, static, private WKWebView
(FR-047/048): JavaScript disabled, remote network loads blocked via a
scoped WKContentRuleList, no cookies/cache persisted, and link clicks
routed to the user's default browser. The preview font controls zoom the
render via pageZoom (FR-049).

A deterministic, unit-tested HTMLFeatureScanner (FR-050) flags
interactive/dynamic constructs (scripts, iframe/embed/object, video/audio,
canvas, form controls, inline event handlers) while never flagging plain
anchor links, including the stamped YouTube source link. When advanced
features are present, a labeled "View in Browser" button appears in the
top right of the preview toolbar (FR-051).

No new dependency (WebKit is an Apple system framework). 68 tests pass
(+11 detector tests); clean release build and bundle. Design lives in
specs/003-html-preview-viewer/.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR upgrades Sumbee’s preview pane for .html summaries from a plain-text fallback to a basic in-app HTML renderer (via WKWebView), while preserving a “static/private by default” posture and adding a conditional “View in Browser” escape hatch for documents that contain interactive/dynamic features.

Changes:

  • Add HTMLWebView (WebKit-backed) for in-app rendering with JS disabled, non-persistent storage, and remote http(s) loads blocked via a content rule list.
  • Add deterministic HTMLFeatureScanner (plus unit tests) to detect “advanced” constructs and drive the “View in Browser” toolbar button.
  • Update docs/spec-kit artifacts plus README/CHANGELOG to reflect the new HTML preview behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Sources/SumbeeKit/Views/AssetBrowser/MarkdownPreview.swift Switch preview rendering for .html to HTMLWebView, compute and use feature-scan result to show “View in Browser”.
Sources/SumbeeKit/Views/AssetBrowser/HTMLWebView.swift New WKWebView wrapper configured for static/private viewing and external link handling.
Sources/SumbeeKit/Services/HTMLFeatureScanner.swift New pure scanner that flags advanced HTML constructs and returns stable, user-facing labels.
Tests/SumbeeKitTests/HTMLFeatureScannerTests.swift New unit tests covering advanced-feature detection and key false-positive guards (anchors/source links).
README.md Update feature description to state that HTML now renders in-app and when “View in Browser” appears.
CHANGELOG.md Add Unreleased entry documenting the new in-app HTML viewer and escape hatch behavior.
specs/003-html-preview-viewer/spec.md New feature spec describing requirements/acceptance criteria and edge cases.
specs/003-html-preview-viewer/research.md New research/decisions doc explaining WebKit choice and privacy/safety stance.
specs/003-html-preview-viewer/plan.md New implementation plan outlining components and integration points.
specs/003-html-preview-viewer/tasks.md New task breakdown tracking detection, viewer, integration, and docs work.
Comments suppressed due to low confidence (1)

Sources/SumbeeKit/Views/AssetBrowser/MarkdownPreview.swift:176

  • If reading the selected asset fails, content is set to an error string but HTML previews ignore content and render htmlRaw (currently reset to ""). This results in a blank preview for unreadable .html files instead of an error message.
        guard let raw = try? String(contentsOf: asset.url, encoding: .utf8) else {
            content = "_Couldn’t read this file._"
            return

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

completion()
return
}
guard let store = WKContentRuleListStore.default() else { completion(); return }
@wynnwu wynnwu changed the base branch from main to remote-dev June 22, 2026 15:47
wynnwu and others added 2 commits June 23, 2026 13:58
The first HTML summary opened in a session rendered as a blank white page;
every later one worked, and returning to the first then worked too (100%
repro). Root cause: the first loadHTMLString was issued from the
WKContentRuleListStore.compileContentRuleList completion handler, whose
thread is not guaranteed to be main. WKWebView is main-thread-only, so the
off-main load silently painted nothing. Subsequent loads came from the
SwiftUI updateNSView pass (main thread) once the rule list was cached,
hence only the first was blank.

Fix: keep the first paint gated behind the remote-load rule install (so a
document is never shown before remote loads are blocked, preserving the
privacy guarantee), but hop the compile completion to the main thread via
DispatchQueue.main.async before installing the rule and resuming the load.
Also add an empty/dedup load guard (skips the transient empty htmlRaw and
avoids reloading on zoom changes) and capture the controller weakly.

Root cause and fix were confirmed by an independent multi-agent review: the
3 diagnoses agreed on the off-main load (high confidence), and adversarial
verification caught that a naive "load eagerly, ungated" fix would
reintroduce a remote-load privacy hole by painting before the block
installed. Documented as learnings #18.

Clean debug + release build; 68 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Sumbee screenshots

Captured from 6b1899b. Also available as a downloadable artifact.

Main panel

Main panel

Settings - Generation

Settings - Generation

Styles editor

Styles editor

Output

Output

wynnwu added a commit that referenced this pull request Jun 23, 2026
…dvanced docs)

HTML summaries now render with their own styling inside the preview pane (a
basic, static, private WKWebView) instead of the old plain-text fallback:
JavaScript disabled, remote network loads blocked, no cookies/cache, link
clicks open in the browser, font controls zoom it. A unit-tested feature
scan flags interactive/dynamic HTML (scripts, embeds, media, forms) and
shows a top-right "View in Browser" button for those. Includes the
main-thread fix for the blank-first-render bug (learnings #18).

No new dependency (WebKit is an Apple framework). 68 tests pass. Ships PR #10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wynnwu wynnwu closed this Jun 23, 2026
@wynnwu wynnwu deleted the 003-html-preview-viewer branch June 23, 2026 08:38
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.

2 participants