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):
- Send a chat message, run
/handoff, generate a Quarto doc. The editor fills and a "Handoff" pill appears in the chat stream.
- Reload the page. The conversation restores fully, but the pill is gone.
- 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:
- 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.
- 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.
- 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
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):/handoff, generate a Quarto doc. The editor fills and a "Handoff" pill appears in the chat stream.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'svaluesbag, and we verified end-to-end that the snapshot (including full document source) round-trips to disk and back into theHandoffStore, 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.0the 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 inpyproject.tomltoshinychat>=0.7.0was 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 inpkg-r/DESCRIPTION.Recommendation: no querychat code change for now
Deliberately do not add a pill re-append workaround to
apply_handoff_snapshot():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:
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.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
feat/artifact-featurerebased ontoorigin/main, 2026-09-05)E2E_FINDINGS.mdbug 1 on the branch