posit-dev / posit-dev/shinychat
Greeting message is mis-ordered when restoring a conversation from history
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 139
- Forks
- 28
- Avg merge
- 23h 44m
- Merged PRs (30d)
- 50
Description
Summary
If a chat starts with an assistant "greeting" message (via chat_ui(messages = ...), chat_append(), or chat_server(greeting = ...)) and the user then has at least one real exchange, navigating away via the history navigator and back reorders the greeting so it lands after the user's first message instead of before it.
Expected order: [greeting (A0), user (U1), response (A1)]
Actual order after restore: [user (U1), greeting (A0), response (A1)]
Repro app
library(shiny)
library(bslib)
library(shinychat)
library(ellmer)
setClass(
"ReproEchoProvider",
representation(name = "character", model = "character")
)
make_echo_client <- function() {
stored_turns <- list()
client <- list(
get_turns = function() stored_turns,
set_turns = function(value) {
stored_turns <<- value
invisible(client)
},
get_tools = function() list(),
get_provider = function() {
methods::new("ReproEchoProvider", name = "Echo", model = "echo-test")
},
get_model = function() "echo-test",
clone = function() make_echo_client()
)
class(client) <- c("Chat", "R6")
client
}
record_exchange <- function(client, user_text, assistant_text) {
turns <- c(
client$get_turns(),
list(
UserTurn(contents = list(ContentText(user_text))),
AssistantTurn(contents = list(ContentText(assistant_text)))
)
)
client$set_turns(turns)
}
ui <- page_fillable(
chat_ui(
"chat",
fill = TRUE,
messages = list("Hi! I'm a greeting message."),
placeholder = "Type a message"
)
)
server <- function(input, output, session) {
client <- make_echo_client()
chat_enable_history(
"chat",
client,
options = history_options(
store = "memory",
scope = "repro-user",
title = NULL
)
)
observeEvent(input$chat_user_input, {
user_text <- input$chat_user_input
if (is.list(user_text)) user_text <- user_text[[1]]
assistant_text <- paste0("echo: ", user_text)
record_exchange(client, user_text, assistant_text)
chat_append("chat", assistant_text)
})
}
shinyApp(ui, server)
Steps:
- Load the app - greeting shows: "Hi! I'm a greeting message."
- Send a message, wait for the echoed response.
- Open the history navigator, click "New conversation".
- Open the history navigator again, click back into the first conversation.
- Observe the greeting is now the second message instead of the first.
Verified with a scripted shinytest2 run driving the actual history navigator UI (trigger button, "New conversation", conversation list item) - not just a code-level inference. Resulting order after restore: ["Tell me a joke", "Hi! I'm a greeting message.", "echo: Tell me a joke"].
Likely culprit
The greeting is UI-only - it's rendered client-side and never becomes an ellmer turn (chat_append(), chat_ui(messages = ...), and the chat_server(greeting = ...) machinery all funnel into UI-only message sends; client$add_turn() or equivalent is never called for it).
extend_record_linear() builds one history-tree node per ellmer turn-group, then separately distributes the browser's reported UI messages onto those nodes:
- "user"-role UI messages are matched to the corresponding user-turn node.
- Everything else falls through to
fallback, defined as the last newly created node.
Since the greeting has no backing turn, it's turn-less and gets swept into fallback - which, once a real exchange exists, is the node for the most recent assistant turn - rather than getting its own leading node. That's why it ends up appended right before/after the real response instead of staying in its original leading position.
Other relevant call sites:
on_response() - supplies recorded_turns (ellmer turns only) and messages/get_reported_messages() (full UI list including the greeting) to extend_record_linear().
replay_ui() - walks the node path in order and flattens each node's ui list, which is where the reordering becomes visible.
chat_append() - confirms the greeting never touches the ellmer client's turn history.
This isn't a persistence bug (the greeting is saved via FileConversationStore$put()) - it's a node-assignment bug in extend_record_linear()'s heuristic for placing turn-less UI messages. A fix likely needs to special-case UI messages reported before the first turn-bearing node (anchor them to a dedicated pre-turn/root node) rather than defaulting them to fallback. Leaving the exact approach open for whoever picks this up.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with extend_record_linear() in pkg-r/R/chat_history_types.R, then trace how on_response() in pkg-r/R/chat_history.R supplies messages and how replay_ui() restores them. Reproduce the issue with the supplied Shiny app or scripted shinytest2 flow, and verify that a greeting remains before the first user message after restoring a conversation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- r
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 67/100