posit-dev / posit-dev/querychat

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

Open
#289 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
212
Forks
29
Avg merge
22h 24m
Merged PRs (30d)
27

Description

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 https://github.com/posit-dev/shinychat/pull/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

https://github.com/posit-dev/shinychat/pull/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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with pkg-r/R/handoff_server.R and pkg-r/R/handoff_orchestrator.R, then review the existing R unit test test-handoff_server.R and the Python end-to-end tests test_12_viz_bookmark.py and test_13_attachments.py. Track shinychat#311 and verify the installed shinychat-r supports UI-message replay before adding the conditional R and unskipped Python restore-cycle coverage. Done means the restored handoff pill reopens the panel with its generated source, while snapshot unit tests continue to pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, python, r
Domain
frontend, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.