Skip to content

WebView reload/refresh wipes all chat content (DOM never repopulated) #39

Closed

Description

@JoshuaRowePhantom

Describe the bug

Switching between light and dark themes at runtime has no effect on the browser-hosted chat output surface. The chat output retains the colours of whichever theme was active when the WebView first loaded.

If the underlying WebView reloads after the initial page load — for example due to an OS/renderer restart, a crash-recovery cycle, or any navigation event that isn't triggered by reassigning HtmlShell — all chat content disappears and the page remains blank.

Root cause

ControllableWebViewControl queues outbound messages in pendingMessages only while isShellLoaded == false. The flag is cleared only when HtmlShell is reassigned.

AgentChatOutputControl.AttachOutputModel sets HtmlShell and immediately constructs a ChatOutputHtmlModel. The model constructor (ApplyInitialTransform) emits all initial DOM insert operations, which land in pendingMessages (because isShellLoaded is still false) and are flushed when navigation completes — correct for the first load.

On a spontaneous reload (without HtmlShell being reassigned):

  1. isShellLoaded is already true — flag is never cleared.
  2. OnNavigationCompleted fires, finds pendingMessages empty, runs startup scripts.
  3. Ready fires → OnBrowserReady posts only the Theme command.
  4. The DOM reverts to the bare shell; the live ChatOutputHtmlModel never replays.

Options considered

Option A — Move model creation to OnBrowserReady ✅ recommended

OnNavigationCompleted already fires Ready on every navigation completion — first load and spontaneous reload alike. AttachOutputModel would set HtmlShell (triggering navigation) without creating the model. OnBrowserReady would:

  1. Post the theme command (so CSS variables are set before DOM ops arrive).
  2. Dispose any existing outputModel.
  3. Construct a fresh ChatOutputHtmlModel for subscribedViewModel.

Because Ready fires after isShellLoaded has been set to true, every message from ApplyInitialTransform is delivered directly — no queuing needed. The handler fires identically for the first load and for any subsequent reload, so both cases share the same code path.

HeadlessControllableBrowser must be updated to raise Ready synchronously when HtmlShell is assigned so that headless tests can exercise the new model-creation path without a real WebView.

Pros: Single code path for first-load and reload. Clean invariant: model exists iff browser is ready. No duplicate logic.
Cons: Minor restructure of AttachOutputModel. pendingMessages becomes vestigial for initial content (but still useful as a safety net for any message sent between HtmlShell assignment and Ready).


Why not use a JavaScript "ready" message?

A JS-initiated approach — having the page post { type: "ready" } via DOMContentLoaded, with the host recreating the model on that message — would also work, but is redundant. The C# Ready event already fires at exactly the right moment (after OnNavigationCompleted, so the page is ready to receive PostMessageToJavaScript calls). Adding a JS-to-host round-trip would introduce a new message type and JS in the shell without fixing anything that the existing Ready event doesn't already cover.


Why not prohibit refresh (block F5 / Ctrl+R)?

Blocking keyboard shortcuts via JavaScript would prevent user-triggered reloads but would not handle spontaneous reloads caused by WebView2 crashes, OS-level renderer restarts, or process suspension/resume. The fix must handle the reload case regardless of cause; prohibiting refresh is not a substitute.


Option B — Add Replay() to ChatOutputHtmlModel

Add a method that re-emits all currently tracked history and running-item models back to the sink. OnBrowserReady would call outputModel?.Replay() in addition to posting the theme.

Pros: Stable object graph; subscriptions and idSequence are preserved across reloads.
Cons: Duplicates the insertion logic from the transformers' OnInsert; the IsInserted flag on each model would need resetting; the running-item Activate() call would need replaying. More moving parts and a second code path that must stay in sync with the primary insertion path.


Recommended fix

Option A. Changes expected in:

  • Phantom.Workspaces.Agent.Gui/Controls/AgentChatOutputControl.axaml.cs — move model creation from AttachOutputModel to OnBrowserReady
  • Phantom.Workspaces.Agent.Gui.Tests/HeadlessControllableBrowser.cs — raise Ready synchronously on HtmlShell set

Activity

  1. added a commit that references this issue on Jun 27, 2026
    784331b
  2. JoshuaRowePhantom commented on Jun 30, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification passed

    Checked: Commit 784331b (features branch) - AgentChatOutputControl.axaml.cs, HeadlessControllableBrowser.cs, and the new AgentChatOutputControlTests.cs (26 test methods).

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

    Implementation

    • AttachOutputModel now only sets HtmlShell (triggering navigation) without creating the model.
    • OnBrowserReady posts the theme command first, disposes any existing outputModel, then constructs a fresh ChatOutputHtmlModel for subscribedViewModel -- identical path for first-load and spontaneous reload.
    • HeadlessControllableBrowser.HtmlShell.set raises Ready synchronously; FireReady() helper simulates spontaneous reloads in tests.

    Tests covering described behaviour

    • HeadlessControllableBrowser_HtmlShellSet_RaisesReadySynchronously -- Ready fires on set
    • AgentChatOutputControl_InitialHistoryWithMessages_UsesOneBatch -- OnBrowserReady with history delivers update commands in a single batch
    • AgentChatOutputControl_SpuriousReload_PostsThemeCommandAgain -- spontaneous FireReady re-posts theme
    • OnBrowserReady_AfterHtmlShellSet_StillPostsThemeCommand -- second Ready with attached viewModel re-posts theme (live theme-switch path)
    • ActualThemeVariantChanged_PostsThemeCommandToBrowser -- runtime theme switching posts theme command
    • OnBrowserReady_ScrollsToBottom_AfterInitialContentLoad, OnBrowserReady_SetsAutoScrollEnabled_AfterInitialContentLoad, OnBrowserReady_DoesNotDoubleScroll_WhenSettingAutoScrollEnabled -- scroll behaviour after reload

    No disabled tests, no unresolved TODOs, no timing-dependent synchronisation.

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

    bugSomething isn't workingverifiedIssue has been verified

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions