Skip to content

R: handoff pill lost on history restore — blocked on shinychat-r UI message state (shinychat#311) #289

Description

@cpsievert

Problem

On the R side (pkg-r, shinychat rc-v0.5.0), a completed handoff becomes unreachable after a page reload with history enabled (history = TRUE, browser/localStorage mode).

Repro (restore_demo/app.R):

  1. Send a chat message, run /handoff, generate a Quarto doc. The editor fills and a "Handoff" pill appears in the chat stream.
  2. Reload the page. The conversation restores fully, but the pill is gone.
  3. The handoff panel's editor stays empty and there is no UI path back to the handoff.

Note: the panel staying closed on restore is by design — the bug is that the pill (the only entry point back to the panel) disappears.

Root cause

The handoff state survives the reload correctly. build_handoff_snapshot() / apply_handoff_snapshot() (pkg-r/R/handoff_server.R) ride along in the history record's values bag, and we verified end-to-end that the snapshot (including full document source) round-trips to disk and back into the HandoffStore, and survives subsequent saves.

What doesn't survive is the pill. It is appended at generation time via chat_module$append() (pkg-r/R/handoff_orchestrator.R, append_pill), and shinychat-r's history restore only replays recorded ellmer turns — server-appended UI messages are transient and are not part of the restored message state. apply_handoff_snapshot() deliberately restores UI-neutral (clears the active handoff, closes the panel), assuming the pill will always be there as the way back in. In R, it isn't.

Python works — and shows the intended end state

The Python port is architecturally identical, but with shinychat>=0.7.0 the full cycle works: after reload the pill is present, and clicking it reopens the panel with the editor refilled from the restored snapshot. This is because shinychat-py 0.7.0 restored server-owned UI message state (notably posit-dev/shinychat#380, "revert browser-owned chat message state"), so appended messages replay with the conversation. Bumping the pin in pyproject.toml to shinychat>=0.7.0 was the entire Python fix — no querychat-py changes needed.

Relevant incoming shinychat work

posit-dev/shinychat#311 ("make the server the source of truth for chat messages") is the R-side counterpart: once shinychat-r faithfully restores UI message state (including chat_append() output), the pill will survive reload in R the same way it does in Python, and this bug should close with only a version-pin bump in pkg-r/DESCRIPTION.

Recommendation: no querychat code change for now

Deliberately do not add a pill re-append workaround to apply_handoff_snapshot():

  • It would diverge the R and Python ports, which currently share the same neutral-restore design.
  • It would produce duplicate pills once shinychat-r gains append replay (and would already do so if ported to Python, where replay works today).

If a stopgap is ever needed before the shinychat-r fix lands, it must be guarded (only re-append when the pill isn't already present in the replayed stream).

Regression tests

Add coverage now so the fix is detected (and the behavior locked in) when shinychat-r ships:

  1. R e2e (shinytest2 or playwright), conditionally skipped: generate handoff → save history → reload/restore → assert the pill is present in the chat stream → click it → assert the editor contains the generated source. Skip unless the installed shinychat-r supports UI-message-state restore; the test flipping from skip to pass is the signal that this issue is closed.
  2. Python e2e (playwright), unskipped: same cycle against shinychat>=0.7.0, to guard the working behavior against regression. This also fills a known gap — the existing Python suite (test_12_viz_bookmark.py, test_13_attachments.py) never exercises the handoff generate → restore cycle.
  3. Unit-level (both ports): snapshot → apply_handoff_snapshot → assert store contents and source survive. This already passes in R (test-handoff_server.R) and was re-verified against real on-disk records during the investigation; keep it as the lower-level guard so any future failure is immediately attributable to the UI replay layer rather than the snapshot layer.

Environment

  • querychat 0.3.0.9000 (feat/artifact-feature rebased onto origin/main, 2026-09-05)
  • shinychat-r 0.5.0 (rc-v0.5.0) — bug present
  • shinychat-py 0.7.0 (PyPI) — bug absent
  • Full context: E2E_FINDINGS.md bug 1 on the branch

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions