From 6de44dadad3aabb80999c4e3d9cefba011cfe5f3 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 12:17:37 -0400 Subject: [PATCH 1/8] feat(pkg-r): use shinychat greeting API for chat greeting Render a static greeting via chat_ui(greeting=) and generate a dynamic greeting on input$chat_greeting_requested via chat_set_greeting(), replacing the startup observer that appended the greeting as a message. Greetings are non-dismissible. Generated greetings stream from an isolated client (tools = NULL) so they stay out of the conversation history, and the has_greeted bookmark bookkeeping is removed. --- pkg-r/R/QueryChat.R | 2 +- pkg-r/R/querychat_module.R | 56 +++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 6ac188453..a2831438c 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -680,7 +680,7 @@ QueryChat <- R6::R6Class( # implicitly by UI functions like shinychat.chat_ui(). id <- id %||% namespaced_id(self$id) - mod_ui(id, ...) + mod_ui(id, ..., greeting = self$greeting) }, #' @description diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index 3af8a2746..f09f0ff15 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -1,6 +1,13 @@ # Main module UI function -mod_ui <- function(id, ..., enable_cancel = TRUE) { +mod_ui <- function(id, ..., greeting = NULL, enable_cancel = TRUE) { ns <- shiny::NS(id) + + if (!is.null(greeting) && any(nzchar(greeting))) { + greeting <- shinychat::chat_greeting(greeting, dismissible = FALSE) + } else { + greeting <- NULL + } + htmltools::tagList( htmltools::htmlDependency( "querychat", @@ -15,6 +22,7 @@ mod_ui <- function(id, ..., enable_cancel = TRUE) { height = "100%", class = "querychat", enable_cancel = enable_cancel, + greeting = greeting, ... ) ) @@ -32,7 +40,6 @@ mod_server <- function( shiny::moduleServer(id, function(input, output, session) { current_title <- shiny::reactiveVal(NULL, label = "current_title") current_query <- shiny::reactiveVal(NULL, label = "current_query") - has_greeted <- shiny::reactiveVal(FALSE, label = "has_greeted") filtered_df <- shiny::reactive(label = "filtered_df", { data_source$execute_query(query = current_query()) }) @@ -83,28 +90,25 @@ mod_server <- function( session = session ) - # Prepopulate the chat UI with a welcome message that appears to be from the - # chat model (but is actually hard-coded). This is just for the user, not for - # the chat model to see. - shiny::observe(label = "greet_on_startup", { - if (has_greeted()) { - return() - } - - greeting_content <- if (!is.null(greeting) && any(nzchar(greeting))) { - greeting - } else { - cli::cli_warn(c( - "No {.arg greeting} provided to {.fn QueryChat}. Using the LLM {.arg client} to generate one now.", - "i" = "For faster startup, lower cost, and determinism, consider providing a {.arg greeting} to {.fn QueryChat}.", - "i" = "You can use your {.help querychat::QueryChat} object's {.fn $generate_greeting} method to generate a greeting." - )) - chat$stream_async(GREETING_PROMPT) - } - - shinychat::chat_append("chat", greeting_content) - has_greeted(TRUE) - }) + if (is.null(greeting)) { + shiny::observeEvent( + input$chat_greeting_requested, + label = "on_greeting_requested", + { + cli::cli_warn(c( + "No {.arg greeting} provided to {.fn QueryChat}. Using the LLM {.arg client} to generate one now.", + "i" = "For faster startup, lower cost, and determinism, consider providing a {.arg greeting} to {.fn QueryChat}.", + "i" = "You can use your {.help querychat::QueryChat} object's {.fn $generate_greeting} method to generate a greeting." + )) + greeting_client <- client(tools = NULL) + stream <- greeting_client$stream_async(GREETING_PROMPT) + shinychat::chat_set_greeting( + "chat", + shinychat::chat_greeting(stream, dismissible = FALSE) + ) + } + ) + } ctrl <- ellmer::stream_controller() @@ -142,7 +146,6 @@ mod_server <- function( shiny::onBookmark(function(state) { state$values$querychat_sql <- current_query() state$values$querychat_title <- current_title() - state$values$querychat_has_greeted <- has_greeted() if (length(viz_widgets) > 0) { state$values$querychat_viz_widgets <- viz_widgets } @@ -155,9 +158,6 @@ mod_server <- function( if (!is.null(state$values$querychat_title)) { current_title(state$values$querychat_title) } - if (!is.null(state$values$querychat_has_greeted)) { - has_greeted(state$values$querychat_has_greeted) - } if (!is.null(state$values$querychat_viz_widgets)) { restored <- restore_viz_widgets( data_source, From dab8b9234480e6e63bc8b18607c74ae227616dc8 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 12:17:37 -0400 Subject: [PATCH 2/8] feat(pkg-py): use shinychat greeting API for chat greeting Render a static greeting via chat_ui(greeting=) and generate a dynamic greeting on the greeting_requested input via Chat.set_greeting(), replacing the startup effect that appended the greeting as a message. Greetings are non-dismissible. Generated greetings stream from an isolated client (tools=None) so they stay out of the conversation history, and the has_greeted bookmark bookkeeping is removed. --- pkg-py/src/querychat/_shiny.py | 4 ++-- pkg-py/src/querychat/_shiny_module.py | 26 ++++++++++---------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/pkg-py/src/querychat/_shiny.py b/pkg-py/src/querychat/_shiny.py index 9f05132db..905f5477a 100644 --- a/pkg-py/src/querychat/_shiny.py +++ b/pkg-py/src/querychat/_shiny.py @@ -405,7 +405,7 @@ def ui(self, *, id: Optional[str] = None, **kwargs): A UI component. """ - return mod_ui(id or self.id, preload_viz=has_viz_tool(self.tools), **kwargs) + return mod_ui(id or self.id, preload_viz=has_viz_tool(self.tools), greeting=self.greeting, **kwargs) def server( self, @@ -819,7 +819,7 @@ def ui(self, *, id: Optional[str] = None, **kwargs): A UI component. """ - return mod_ui(id or self.id, preload_viz=has_viz_tool(self.tools), **kwargs) + return mod_ui(id or self.id, preload_viz=has_viz_tool(self.tools), greeting=self.greeting, **kwargs) def df(self) -> IntoFrameT: """ diff --git a/pkg-py/src/querychat/_shiny_module.py b/pkg-py/src/querychat/_shiny_module.py index 73683ad28..47b2b0a0a 100644 --- a/pkg-py/src/querychat/_shiny_module.py +++ b/pkg-py/src/querychat/_shiny_module.py @@ -56,11 +56,13 @@ def __getattr__(self, _name: str): @module.ui -def mod_ui(*, preload_viz: bool = False, **kwargs): +def mod_ui(*, preload_viz: bool = False, greeting: str | None = None, **kwargs): css_path = Path(__file__).parent / "static" / "css" / "styles.css" js_path = Path(__file__).parent / "static" / "js" / "querychat.js" kwargs.setdefault("enable_cancel", True) + if greeting: + kwargs.setdefault("greeting", shinychat.chat_greeting(greeting, dismissible=False)) tag = shinychat.chat_ui(CHAT_ID, **kwargs) tag.add_class("querychat") @@ -128,7 +130,6 @@ def mod_server( # Reactive values to store state sql = ReactiveStringOrNone(None) title = ReactiveStringOrNone(None) - has_greeted = reactive.value[bool](False) # noqa: FBT003 if not callable(client): raise TypeError("mod_server() requires a callable client factory.") @@ -207,14 +208,11 @@ async def _(user_input: str): def _handle_cancel(): ctrl.cancel() - @reactive.effect - async def greet_on_startup(): - if has_greeted(): - return + if greeting is None: - if greeting: - await chat_ui.append_message(greeting) - elif greeting is None: + @reactive.effect + @reactive.event(input[f"{CHAT_ID}_greeting_requested"]) + async def _handle_greeting_requested(): warnings.warn( "No greeting provided to `QueryChat()`. Using the LLM `client` to generate one now. " "For faster startup, lower cost, and determinism, consider providing a greeting " @@ -222,10 +220,9 @@ async def greet_on_startup(): GreetWarning, stacklevel=2, ) - stream = await chat.stream_async(GREETING_PROMPT, echo="none") - await chat_ui.append_message_stream(stream) - - has_greeted.set(True) + greeting_client = client(tools=None) + stream = await greeting_client.stream_async(GREETING_PROMPT, echo="none") + await chat_ui.set_greeting(shinychat.chat_greeting(stream, dismissible=False)) # Handle update button clicks @reactive.effect @@ -252,7 +249,6 @@ def _on_bookmark(x: BookmarkState) -> None: vals = x.values vals["querychat_sql"] = sql.get() vals["querychat_title"] = title.get() - vals["querychat_has_greeted"] = has_greeted.get() if viz_widgets: vals["querychat_viz_widgets"] = viz_widgets @@ -263,8 +259,6 @@ def _on_restore(x: RestoreState) -> None: sql.set(vals["querychat_sql"]) if "querychat_title" in vals: title.set(vals["querychat_title"]) - if "querychat_has_greeted" in vals: - has_greeted.set(vals["querychat_has_greeted"]) if "querychat_viz_widgets" in vals: restored = restore_viz_widgets( data_source, vals["querychat_viz_widgets"] From 13ed0df53ff8b795974723b8c1cfc9c5c050ce10 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 12:32:01 -0400 Subject: [PATCH 3/8] fix(pkg-r): persist generated greeting across bookmark restore shinychat bookmarking only saves the ellmer client's chat state, not the greeting set via chat_set_greeting(). Since generated greetings are produced on an isolated client, they are absent on reload. Capture the generated greeting text and save/restore it via the module's onBookmark/onRestore hooks, re-displaying it on restore and reusing it instead of regenerating when the greeting is requested again. --- pkg-r/R/querychat_module.R | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index f09f0ff15..17ad48a93 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -40,6 +40,9 @@ mod_server <- function( shiny::moduleServer(id, function(input, output, session) { current_title <- shiny::reactiveVal(NULL, label = "current_title") current_query <- shiny::reactiveVal(NULL, label = "current_query") + # Holds a generated greeting so it can be saved and restored on bookmark. + # Static greetings live in the UI (chat_ui(greeting=)) and persist already. + current_greeting <- shiny::reactiveVal(NULL, label = "current_greeting") filtered_df <- shiny::reactive(label = "filtered_df", { data_source$execute_query(query = current_query()) }) @@ -95,6 +98,14 @@ mod_server <- function( input$chat_greeting_requested, label = "on_greeting_requested", { + # Re-display a restored greeting rather than generating a new one. + if (!is.null(current_greeting())) { + shinychat::chat_set_greeting( + "chat", + shinychat::chat_greeting(current_greeting(), dismissible = FALSE) + ) + return() + } cli::cli_warn(c( "No {.arg greeting} provided to {.fn QueryChat}. Using the LLM {.arg client} to generate one now.", "i" = "For faster startup, lower cost, and determinism, consider providing a {.arg greeting} to {.fn QueryChat}.", @@ -102,10 +113,14 @@ mod_server <- function( )) greeting_client <- client(tools = NULL) stream <- greeting_client$stream_async(GREETING_PROMPT) - shinychat::chat_set_greeting( + p <- shinychat::chat_set_greeting( "chat", shinychat::chat_greeting(stream, dismissible = FALSE) ) + # Capture the generated greeting so it can be bookmarked and restored. + promises::then(p, function(value) { + current_greeting(greeting_client$last_turn()@text) + }) } ) } @@ -146,6 +161,9 @@ mod_server <- function( shiny::onBookmark(function(state) { state$values$querychat_sql <- current_query() state$values$querychat_title <- current_title() + if (!is.null(current_greeting())) { + state$values$querychat_greeting <- current_greeting() + } if (length(viz_widgets) > 0) { state$values$querychat_viz_widgets <- viz_widgets } @@ -158,6 +176,17 @@ mod_server <- function( if (!is.null(state$values$querychat_title)) { current_title(state$values$querychat_title) } + if (!is.null(state$values$querychat_greeting)) { + current_greeting(state$values$querychat_greeting) + shinychat::chat_set_greeting( + "chat", + shinychat::chat_greeting( + state$values$querychat_greeting, + dismissible = FALSE + ), + session = session + ) + } if (!is.null(state$values$querychat_viz_widgets)) { restored <- restore_viz_widgets( data_source, From a2a52bac6a8bc9507898cc820b92772680f9f3af Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 12:32:01 -0400 Subject: [PATCH 4/8] fix(pkg-py): persist generated greeting across bookmark restore shinychat bookmarking only saves the chatlas client's chat state, not the greeting set via Chat.set_greeting(). Since generated greetings are produced on an isolated client, they are absent on reload. Capture the generated greeting text and save/restore it via the module's on_bookmark/on_restore hooks, re-displaying it on restore and reusing it instead of regenerating when the greeting is requested again. --- pkg-py/src/querychat/_shiny_module.py | 38 ++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/pkg-py/src/querychat/_shiny_module.py b/pkg-py/src/querychat/_shiny_module.py index 47b2b0a0a..699159011 100644 --- a/pkg-py/src/querychat/_shiny_module.py +++ b/pkg-py/src/querychat/_shiny_module.py @@ -62,7 +62,9 @@ def mod_ui(*, preload_viz: bool = False, greeting: str | None = None, **kwargs): kwargs.setdefault("enable_cancel", True) if greeting: - kwargs.setdefault("greeting", shinychat.chat_greeting(greeting, dismissible=False)) + kwargs.setdefault( + "greeting", shinychat.chat_greeting(greeting, dismissible=False) + ) tag = shinychat.chat_ui(CHAT_ID, **kwargs) tag.add_class("querychat") @@ -130,6 +132,9 @@ def mod_server( # Reactive values to store state sql = ReactiveStringOrNone(None) title = ReactiveStringOrNone(None) + # Holds a generated greeting so it can be saved and restored on bookmark. + # Static greetings live in the UI (chat_ui(greeting=)) and persist already. + current_greeting = ReactiveStringOrNone(None) if not callable(client): raise TypeError("mod_server() requires a callable client factory.") @@ -200,7 +205,9 @@ def filtered_df(): # Handle user input @chat_ui.on_user_submit async def _(user_input: str): - stream = await chat.stream_async(user_input, echo="none", content="all", controller=ctrl) + stream = await chat.stream_async( + user_input, echo="none", content="all", controller=ctrl + ) await chat_ui.append_message_stream(stream) @reactive.effect @@ -213,6 +220,13 @@ def _handle_cancel(): @reactive.effect @reactive.event(input[f"{CHAT_ID}_greeting_requested"]) async def _handle_greeting_requested(): + # Re-display a restored greeting rather than generating a new one. + existing = current_greeting.get() + if existing is not None: + await chat_ui.set_greeting( + shinychat.chat_greeting(existing, dismissible=False) + ) + return warnings.warn( "No greeting provided to `QueryChat()`. Using the LLM `client` to generate one now. " "For faster startup, lower cost, and determinism, consider providing a greeting " @@ -222,7 +236,13 @@ async def _handle_greeting_requested(): ) greeting_client = client(tools=None) stream = await greeting_client.stream_async(GREETING_PROMPT, echo="none") - await chat_ui.set_greeting(shinychat.chat_greeting(stream, dismissible=False)) + await chat_ui.set_greeting( + shinychat.chat_greeting(stream, dismissible=False) + ) + # Capture the generated greeting so it can be bookmarked and restored. + last_turn = greeting_client.get_last_turn(role="assistant") + if last_turn is not None: + current_greeting.set(last_turn.text) # Handle update button clicks @reactive.effect @@ -249,16 +269,26 @@ def _on_bookmark(x: BookmarkState) -> None: vals = x.values vals["querychat_sql"] = sql.get() vals["querychat_title"] = title.get() + greeting_val = current_greeting.get() + if greeting_val is not None: + vals["querychat_greeting"] = greeting_val if viz_widgets: vals["querychat_viz_widgets"] = viz_widgets @session.bookmark.on_restore - def _on_restore(x: RestoreState) -> None: + async def _on_restore(x: RestoreState) -> None: vals = x.values if "querychat_sql" in vals: sql.set(vals["querychat_sql"]) if "querychat_title" in vals: title.set(vals["querychat_title"]) + if "querychat_greeting" in vals: + current_greeting.set(vals["querychat_greeting"]) + await chat_ui.set_greeting( + shinychat.chat_greeting( + vals["querychat_greeting"], dismissible=False + ) + ) if "querychat_viz_widgets" in vals: restored = restore_viz_widgets( data_source, vals["querychat_viz_widgets"] From 319352beaf47631bc38ba6ccbe2c6d80d08aa5c2 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 13:04:19 -0400 Subject: [PATCH 5/8] docs: note shinychat#253 workaround at greeting bookmark sites The generated-greeting bookmark/restore plumbing exists only because shinychat does not persist or expose greeting state. Point the workaround sites at posit-dev/shinychat#253 and spell out what to remove if it is fixed. --- pkg-py/src/querychat/_shiny_module.py | 4 ++++ pkg-r/R/querychat_module.R | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pkg-py/src/querychat/_shiny_module.py b/pkg-py/src/querychat/_shiny_module.py index 699159011..8698e05bd 100644 --- a/pkg-py/src/querychat/_shiny_module.py +++ b/pkg-py/src/querychat/_shiny_module.py @@ -134,6 +134,10 @@ def mod_server( title = ReactiveStringOrNone(None) # Holds a generated greeting so it can be saved and restored on bookmark. # Static greetings live in the UI (chat_ui(greeting=)) and persist already. + # Workaround for posit-dev/shinychat#253: shinychat does not bookmark + # greetings or expose their state. If that issue is fixed, this value, the + # get_last_turn() capture below, and the greeting handling in + # on_bookmark/on_restore can be dropped (and the shinychat minimum bumped). current_greeting = ReactiveStringOrNone(None) if not callable(client): diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index 17ad48a93..17b32f7c5 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -42,6 +42,10 @@ mod_server <- function( current_query <- shiny::reactiveVal(NULL, label = "current_query") # Holds a generated greeting so it can be saved and restored on bookmark. # Static greetings live in the UI (chat_ui(greeting=)) and persist already. + # Workaround for posit-dev/shinychat#253: shinychat does not bookmark + # greetings or expose their state. If that issue is fixed, this reactiveVal, + # the last_turn() capture below, and the greeting handling in + # onBookmark/onRestore can be dropped (and the shinychat minimum bumped). current_greeting <- shiny::reactiveVal(NULL, label = "current_greeting") filtered_df <- shiny::reactive(label = "filtered_df", { data_source$execute_query(query = current_query()) From 96f5a7dfb7b65e0f142a55bc44f2ecfc685c2b28 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 13:08:36 -0400 Subject: [PATCH 6/8] docs: explain the empty-restore greeting double-set On empty-chat bookmark restore both the greeting_requested guard and onRestore set the greeting (harmless, identical content); on non-empty restore greeting_requested never fires, so onRestore is the only path. Document this at the guard branch in both packages. --- pkg-py/src/querychat/_shiny_module.py | 3 +++ pkg-r/R/querychat_module.R | 3 +++ 2 files changed, 6 insertions(+) diff --git a/pkg-py/src/querychat/_shiny_module.py b/pkg-py/src/querychat/_shiny_module.py index 8698e05bd..74d4a9059 100644 --- a/pkg-py/src/querychat/_shiny_module.py +++ b/pkg-py/src/querychat/_shiny_module.py @@ -225,6 +225,9 @@ def _handle_cancel(): @reactive.event(input[f"{CHAT_ID}_greeting_requested"]) async def _handle_greeting_requested(): # Re-display a restored greeting rather than generating a new one. + # On empty-chat restore both this and on_restore set the greeting + # (harmless, identical content); on non-empty restore this never + # fires, so on_restore is the only path that re-displays. existing = current_greeting.get() if existing is not None: await chat_ui.set_greeting( diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index 17b32f7c5..f3002a0e5 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -103,6 +103,9 @@ mod_server <- function( label = "on_greeting_requested", { # Re-display a restored greeting rather than generating a new one. + # On empty-chat restore both this and onRestore set the greeting + # (harmless, identical content); on non-empty restore this never fires, + # so onRestore is the only path that re-displays. if (!is.null(current_greeting())) { shinychat::chat_set_greeting( "chat", From f87bc2a45414b1c0cc23d7469318a0407d8c4bc9 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 13:16:03 -0400 Subject: [PATCH 7/8] docs: add NEWS/CHANGELOG entries for the greeting API migration Refs #249 --- pkg-py/CHANGELOG.md | 1 + pkg-r/NEWS.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/pkg-py/CHANGELOG.md b/pkg-py/CHANGELOG.md index 1660dbb81..9dbc10839 100644 --- a/pkg-py/CHANGELOG.md +++ b/pkg-py/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +* Chat greetings now use shinychat's greeting API (requires shinychat >= 0.4.0). A provided `greeting` renders instantly when the app loads, and when no `greeting` is given one is generated on demand without being added to the conversation history. Generated greetings are now preserved across bookmark/restore. (#249) * The query tool result card now starts collapsed by default. Users can still expand it to see the SQL query and results. Set `QUERYCHAT_TOOL_DETAILS=expanded` to restore the previous behavior. (#239) ## [0.6.1] - 2026-05-26 diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index 6c755bb5b..178b156f1 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -1,5 +1,9 @@ # querychat (development version) +## Improvements + +* Chat greetings now use shinychat's greeting API (requires shinychat >= 0.4.0). A provided `greeting` renders instantly when the app loads, and when no `greeting` is given one is generated on demand without being added to the conversation history. Generated greetings are now preserved across bookmark/restore. (#249) + # querychat 0.3.0 ## New features From 19e3ba59b055450d1a7801c0bed33851d5e2df6c Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Jun 2026 14:00:03 -0400 Subject: [PATCH 8/8] test(pkg-py): assert greeting in shinychat greeting slot The greeting now renders in shinychat's dedicated greeting slot (.shiny-chat-greeting) rather than as a persistent message in .shiny-chat-messages-content. Point the e2e greeting assertions and viz setup fixtures at the greeting slot instead of loc_messages / loc_latest_message. --- pkg-py/tests/playwright/test_01_hello_app.py | 6 ++++-- pkg-py/tests/playwright/test_02_prompt_app.py | 3 ++- pkg-py/tests/playwright/test_03_sidebar_apps.py | 6 ++++-- pkg-py/tests/playwright/test_10_viz_inline.py | 5 ++--- pkg-py/tests/playwright/test_11_viz_footer.py | 3 ++- pkg-py/tests/playwright/test_12_viz_bookmark.py | 5 ++--- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pkg-py/tests/playwright/test_01_hello_app.py b/pkg-py/tests/playwright/test_01_hello_app.py index b954a027d..6d003c61e 100644 --- a/pkg-py/tests/playwright/test_01_hello_app.py +++ b/pkg-py/tests/playwright/test_01_hello_app.py @@ -40,7 +40,8 @@ def test_app_loads_successfully(self) -> None: def test_welcome_message_appears(self) -> None: """INIT-02: Chat shows LLM greeting.""" - expect(self.chat.loc_messages).to_contain_text("Hello", timeout=30000) + greeting = self.chat.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text("Hello", timeout=30000) def test_default_sql_query_shown(self) -> None: """INIT-03: SQL panel shows default query.""" @@ -66,7 +67,8 @@ def test_chat_input_visible(self) -> None: def test_suggestion_links_present(self) -> None: """INIT-07: Suggestions are visible in greeting.""" - expect(self.chat.loc_messages).to_contain_text( + greeting = self.chat.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text( re.compile(r"survived|class|age", re.IGNORECASE), timeout=30000 ) diff --git a/pkg-py/tests/playwright/test_02_prompt_app.py b/pkg-py/tests/playwright/test_02_prompt_app.py index cf2380f7d..76ac596b9 100644 --- a/pkg-py/tests/playwright/test_02_prompt_app.py +++ b/pkg-py/tests/playwright/test_02_prompt_app.py @@ -37,7 +37,8 @@ def test_app_loads_successfully(self) -> None: def test_custom_greeting_appears(self) -> None: """INIT-02: Custom greeting from greeting.md is shown.""" # The custom greeting should contain content from greeting.md - expect(self.chat.loc_messages).to_contain_text("Hello", timeout=30000) + greeting = self.chat.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text("Hello", timeout=30000) def test_default_sql_query_shown(self) -> None: """INIT-03: SQL panel shows default query.""" diff --git a/pkg-py/tests/playwright/test_03_sidebar_apps.py b/pkg-py/tests/playwright/test_03_sidebar_apps.py index 484f371ff..5cec8a9ba 100644 --- a/pkg-py/tests/playwright/test_03_sidebar_apps.py +++ b/pkg-py/tests/playwright/test_03_sidebar_apps.py @@ -44,7 +44,8 @@ def test_page_title(self) -> None: def test_welcome_message_appears(self) -> None: """Chat shows LLM greeting.""" - expect(self.chat.loc_messages).to_contain_text("Hello", timeout=30000) + greeting = self.chat.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text("Hello", timeout=30000) def test_card_header_initial(self) -> None: """Card header shows 'Titanic Dataset' initially.""" @@ -127,7 +128,8 @@ def setup(self, page: Page, app_03_core: str, chat_03_core: ChatController) -> N def test_welcome_message_appears(self) -> None: """Chat shows LLM greeting.""" - expect(self.chat.loc_messages).to_contain_text("Hello", timeout=30000) + greeting = self.chat.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text("Hello", timeout=30000) def test_card_header_initial(self) -> None: """Card header shows 'Titanic Dataset' initially.""" diff --git a/pkg-py/tests/playwright/test_10_viz_inline.py b/pkg-py/tests/playwright/test_10_viz_inline.py index a6f98d036..b97a0e214 100644 --- a/pkg-py/tests/playwright/test_10_viz_inline.py +++ b/pkg-py/tests/playwright/test_10_viz_inline.py @@ -29,9 +29,8 @@ def setup( """Navigate to the viz app before each test.""" page.goto(app_10_viz) page.wait_for_selector("shiny-chat-container", timeout=30000) - expect(chat_10_viz.loc_latest_message).to_contain_text( - "Welcome", timeout=30000 - ) + greeting = chat_10_viz.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text("Welcome", timeout=30000) self.page = page self.chat = chat_10_viz diff --git a/pkg-py/tests/playwright/test_11_viz_footer.py b/pkg-py/tests/playwright/test_11_viz_footer.py index 71c3e9a57..6ceed7e29 100644 --- a/pkg-py/tests/playwright/test_11_viz_footer.py +++ b/pkg-py/tests/playwright/test_11_viz_footer.py @@ -46,7 +46,8 @@ def _send_viz_prompt( """Navigate to the viz app and trigger a visualization before each test.""" page.goto(app_10_viz) page.wait_for_selector("shiny-chat-container", timeout=30_000) - expect(chat_10_viz.loc_latest_message).to_contain_text("Welcome", timeout=30_000) + greeting = chat_10_viz.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text("Welcome", timeout=30_000) chat_10_viz.set_user_input(VIZ_PROMPT) chat_10_viz.send_user_input(method="click") diff --git a/pkg-py/tests/playwright/test_12_viz_bookmark.py b/pkg-py/tests/playwright/test_12_viz_bookmark.py index 24cb8540d..125b47711 100644 --- a/pkg-py/tests/playwright/test_12_viz_bookmark.py +++ b/pkg-py/tests/playwright/test_12_viz_bookmark.py @@ -78,9 +78,8 @@ def setup( page.goto(app_viz_bookmark) page.wait_for_selector("shiny-chat-container", timeout=30_000) - expect(chat_viz_bookmark.loc_latest_message).to_contain_text( - "Welcome", timeout=30_000 - ) + greeting = chat_viz_bookmark.loc.locator(".shiny-chat-greeting") + expect(greeting).to_contain_text("Welcome", timeout=30_000) self.pre_viz_url = page.url