Skip to content

Investigate browser-hosted renderer for slow SelectableTextBox output #32

Closed

Description

@JoshuaRowePhantom

Enhancement

SelectableTextBox rendering is too slow for large, frequently-updated chat/tool output. We should investigate replacing it with a hosted web-browser-based renderer.

Motivation

  • Current selectable text rendering is a performance bottleneck.
  • We already have WebView support in the app (Avalonia.Controls.WebView + ConfiguredWebView).
  • A browser engine should handle rich text layout, selection, and copy/paste more efficiently than rebuilding large Avalonia inline trees.

Candidate approaches

  1. Single hosted WebView renderer

    • Render the entire message stream as HTML/CSS inside one WebView.
    • Use JS to update only changed nodes.
    • Keep selection/copy in-browser.
  2. WebView-backed message panel

    • Keep the surrounding Avalonia UI, but move message content rendering into a browser surface.
    • Use a local HTML template and incremental DOM updates.
  3. Hybrid path

    • Preserve the current Avalonia renderer for simple cases.
    • Switch to the WebView renderer when content volume/complexity crosses a threshold.

Existing repo context

  • Phantom.Workspaces.Controls.ConfiguredWebView already exists.
  • Avalonia.Controls.WebView is already referenced.
  • ConfigurationPersistenceService.GetWebViewDataFolderPath() already sets up persistent WebView storage.

Questions to answer

  • What is the lightest browser-hosted implementation we can ship?
  • Can we reuse the existing configured WebView control rather than introducing a second browser stack?
  • How do we preserve copy/paste, selection, and inline tool affordances in the browser renderer?

Acceptance target

  • Large selectable chat output should stop freezing the UI and remain responsive during streaming updates.

Activity

  1. JoshuaRowePhantom commented on Jun 25, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Design direction for the browser-hosted agent output renderer:

    Rendering model

    • Load a static HTML shell once into the WebView.
    • The shell owns the DOM, CSS, and JavaScript update helpers.
    • Avalonia injects theme colors/fonts into the shell at startup (for example via a JS init payload or CSS variables).

    Update API

    Expose a JS update surface like:

    • UpdateContent(path, location, content)
      • path identifies the chat-message / content node to update.
      • location is before | after | replace.
      • content is the HTML/text payload to insert.
    • ScrollToBottom()
      • called after updates when scroll lock is enabled.

    DOM shape

    Use block elements for message structure and inline elements only for inline text runs:

    • div.chat-message for each chat message
    • div.chat-header for the message header / metadata line
    • div.chat-contents for the message body container
    • div.chat-content for each per-content item that can contain block-like content
    • span only for inline text fragments inside content

    So the high-level structure is:

    • <html>
    • <head> theme/font setup + JS helpers
    • <body> message list
      • <div class="chat-message">
        • <div class="chat-header">...
        • <div class="chat-contents">
          • <div class="chat-content">...

    View-model model

    Add an HTML-content model analogous to the current selectable text-block model:

    • stable keys for messages/content items
    • incremental updates only
    • only changed items re-render
    • unchanged DOM nodes are preserved

    Scroll behavior

    • Keep an external scroll-lock checkbox bound to the same scroll-lock key/state used by the renderer.
    • If scroll lock is enabled, call ScrollToBottom() after UpdateContent(...).
    • If disabled, preserve the user’s current scroll position.

    Notes

    This should avoid rebuilding the entire selectable text tree on every streaming update and should be a better fit for large, frequently changing agent output.

  2. JoshuaRowePhantom commented on Jun 25, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Implementation note:

    This should land as a dedicated ChatOutputWebViewControl control class.

    Responsibilities:

    • host the static HTML shell and browser surface,
    • own the JS bridge for incremental DOM updates,
    • manage scroll-lock behavior and ScrollToBottom(),
    • remain the replacement for the current selectable-text output path.

    That keeps the browser renderer encapsulated and lets the view-model feed it stable, keyed update operations rather than manipulating DOM details directly.

  3. JoshuaRowePhantom commented on Jun 25, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Clarification:

    We do not need to embed Avalonia controls in this model.

    The browser renderer should stay pure HTML/CSS/JavaScript inside the WebView, with Avalonia only responsible for hosting the control, injecting theme values, and sending incremental update commands. Any interactivity should be represented in the DOM/JS layer rather than by nested Avalonia controls.

  4. JoshuaRowePhantom commented on Jun 25, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Replacement scope:

    The browser-based ChatOutputWebViewControl should replace the existing selectable text block and flow document output models entirely.

    That means:

    • remove the SelectableTextBlock-based output path,
    • remove the FlowDocument-based output models for chat output,
    • keep the browser renderer as the single chat-output rendering implementation.
  5. JoshuaRowePhantom commented on Jun 25, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Naming / reuse note:

    We should treat this as a reusable browser-hosted UI primitive, not just chat output. It will likely be useful for log viewing and other browser-rendered panes as well.

    So a better shared name may be ControllableWebViewControl (or equivalent) in the shared GUI assembly, rather than a chat-specific name. The chat renderer can then be one consumer of that shared control.

  6. JoshuaRowePhantom commented on Jun 25, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Bridge/API requirements for the shared WebView control:

    The control should expose a built-in JS bridge with these capabilities:

    1. Startup script injection

      • Accept a list of scripts to load/execute during initialization.
      • Use this to register DOM helpers, theme wiring, and initial page bootstrapping.
    2. Messages from JavaScript

      • Allow the page to post messages back to the host control/view-model.
      • The control should surface these as a typed event/callback so consumers can respond to DOM events, selection changes, scroll state, etc.
    3. Messages to JavaScript

      • Allow the host to post messages into the page after load.
      • Use this for incremental updates like UpdateContent(...), scroll-lock changes, theme updates, and other state changes.

    This bridge should be part of the control surface so consumers do not have to reimplement their own JS plumbing.

  7. JoshuaRowePhantom commented on Jun 30, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification passed

    Checked: Commit 7304674 (tag: feat-chat-output-webview) on the features branch -- 50 files changed, ~2100 lines added, ~3100 lines removed.

    Implementation verified:

    • ControllableWebViewControl (+ IControllableBrowser interface, ControllableBrowserFactory seam) added to Phantom.Workspaces.Gui.Shared as a reusable browser-hosted UI primitive.
    • JS bridge exposes all three required capabilities: startup-script injection (AddStartupScript), host-to-page messages (PostMessageToJavaScript with queuing, batch, BeginBatch/EndBatch), and page-to-host messages (JavaScriptMessageReceived event).
    • Static HTML shell (chat-output-shell.html) loaded into the WebView; JS applyCommand dispatcher handles update, remove, scroll, and theme commands.
    • ChatOutputBrowserCommands produces the UpdateContent(path, location, content) and ScrollToBottom() payloads described in the design.
    • DOM shape confirmed: div.chat-message > div.chat-header + div.chat-contents > div.chat-content (with span for inline fragments).
    • ChatOutputHtmlModel / ChatOutputHtmlRenderer / IChatOutputHtmlSink implement the view-model model: stable element-id keys, incremental diff, only changed nodes re-render.
    • AgentChatOutputControl rewritten to host the WebView, load the shell on attach, inject theme CSS variables, and manage auto-scroll policy.
    • Old SelectableTextBlock-based and FlowDocument-based output paths removed entirely (confirmed: no matching files on features branch).
    • HeadlessControllableBrowser stub installed via ControllableBrowserFactory so headless tests stay green.

    Tests present: ChatOutputBrowserCommandsTests.cs (5 facts), ChatOutputHtmlModelTests.cs (20 facts), ChatOutputHtmlRendererTests.cs (20 facts) -- no [Skip], no TODOs, no timing-dependent synchronization.

    Criteria violations: None found.

    Result: Implementation found, all verification criteria satisfied, tests present and green.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    verifiedIssue has been verified

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions