Skip to content

feat: greeting messages — first-class welcome UI for chat#223

Open
gadenbuie wants to merge 49 commits into
mainfrom
217-greeting-messages
Open

feat: greeting messages — first-class welcome UI for chat#223
gadenbuie wants to merge 49 commits into
mainfrom
217-greeting-messages

Conversation

@gadenbuie
Copy link
Copy Markdown
Collaborator

@gadenbuie gadenbuie commented May 15, 2026

Closes #217

Summary

Adds native greeting messages as a first-class feature in shinychat. Greetings are a distinct UI element — separate from the conversation message stream — that welcome users when the chat loads and can include suggestion cards.

Screen.Recording.2026-05-15.at.8.42.05.AM.mov

Content paths

  • Static: chat_ui(greeting = "## Welcome!") — renders immediately on mount
  • Dynamic: chat_set_greeting() / Chat.set_greeting() with computed or streamed content
  • Generated: server listens for <id>_greeting_requested and streams an LLM-generated greeting

Key design decisions

  • Stand-alone visual: greetings are centered, styled separately from assistant messages, with their own entrance/dismiss animations
  • Dismissible by default: greeting fades out on first user message; reappears if chat is cleared
  • greeting_requested input: a state-driven Shiny input that fires when the chat is visible, empty, and has no greeting — handles init, clear, bookmark restore, and hidden-tab cases uniformly
  • chat_clear(greeting=TRUE): clears both messages and greeting, naturally triggering greeting_requested for regeneration
  • Module support (R): chat_mod_server(greeting=) accepts a static value or a function; functions are called automatically on init and after clear(greeting=TRUE), with optional client injection for one-arg functions

Protocol

Five new action types for the stand-alone greeting path: greeting, greeting_start, greeting_chunk, greeting_end, greeting_clear. Content flows through the same HAST markdown pipeline as regular messages.

What was dropped during implementation

  • as_assistant_message — removed; authors who want assistant-styled welcomes use chat_append() directly
  • include_in_history — removed; shinychat doesn't own model history

Verification

Set ANTHROPIC_API_KEY and run any of the example apps below. Things to verify:

  • Greeting streams in on load with suggestion cards
  • Clicking a suggestion fills the input
  • Sending a message dismisses the greeting with a fade animation
  • "Clear chat" re-shows the original greeting
  • "Clear chat + greeting" regenerates a fresh greeting from the LLM
haiku_app.R — R with manual chat plumbing
library(shiny)
library(bslib)
library(shinychat)
library(ellmer)

MODEL <- "claude-haiku-4-5"

GREETING_PROMPT <- r"(
You generate short, friendly welcome greetings for a programming explainer app.
Respond with markdown only — no preamble, no closing remarks.

Use exactly this structure:

## <a brief, varied welcome heading>

<one inviting sentence about explaining programming and data science concepts>

**Try one of these:**

- <span class="suggestion">programming or data science topic 1</span>
- <span class="suggestion">programming or data science topic 2</span>
- <span class="suggestion">programming or data science topic 3</span>

Suggestions should be specific concepts like "What is a closure?",
"Explain tidy evaluation", "How does gradient descent work?", etc.
Vary the wording each time. Keep the entire greeting under 60 words.
)"

CHAT_PROMPT <- "You explain programming and data science concepts concisely in 1-2 sentences. Use markdown when appropriate."

ui <- page_fillable(
  card(
    card_header(
      "Haiku greeting demo (manual)",
      toolbar(
        align = "right",
        toolbar_input_button("clear_chat", "Clear chat", icon = icon("eraser")),
        toolbar_input_button(
          "clear_chat_and_greeting",
          "Clear chat + greeting",
          icon = icon("trash")
        )
      )
    ),
    chat_ui("chat", placeholder = "Ask me anything...", fill = TRUE)
  )
)

server <- function(input, output, session) {
  client <- ellmer::chat_anthropic(
    model = MODEL,
    system_prompt = CHAT_PROMPT,
    echo = "none"
  )

  observeEvent(input$chat_user_input, {
    stream <- client$stream_async(input$chat_user_input)
    chat_append("chat", stream)
  })

  # Fires when the chat is visible, empty, and has no greeting.
  observeEvent(input$chat_greeting_requested, {
    greeter <- ellmer::chat_anthropic(
      model = MODEL,
      system_prompt = GREETING_PROMPT,
      echo = "none"
    )
    stream <- greeter$stream_async("Generate a welcome greeting for the user.")
    chat_set_greeting("chat", chat_greeting(stream))
  })

  observeEvent(input$clear_chat, {
    chat_clear("chat")
  })

  observeEvent(input$clear_chat_and_greeting, {
    chat_clear("chat", greeting = TRUE)
  })
}

shinyApp(ui, server)
haiku_mod_app.R — R with chat_mod_server()
library(shiny)
library(bslib)
library(shinychat)
library(ellmer)

MODEL <- "claude-haiku-4-5"

GREETING_PROMPT <- r"(
You generate short, friendly welcome greetings for a programming explainer app.
Respond with markdown only — no preamble, no closing remarks.

Use exactly this structure:

## <a brief, varied welcome heading>

<one inviting sentence about explaining programming and data science concepts>

**Try one of these:**

- <span class="suggestion">programming or data science topic 1</span>
- <span class="suggestion">programming or data science topic 2</span>
- <span class="suggestion">programming or data science topic 3</span>

Suggestions should be specific concepts like "What is a closure?",
"Explain tidy evaluation", "How does gradient descent work?", etc.
Vary the wording each time. Keep the entire greeting under 60 words.
)"

CHAT_PROMPT <- "You explain programming and data science concepts concisely in 1-2 sentences. Use markdown when appropriate."

ui <- page_fillable(
  card(
    card_header(
      "Haiku greeting demo (module)",
      toolbar(
        align = "right",
        toolbar_input_button("clear_chat", "Clear chat", icon = icon("eraser")),
        toolbar_input_button(
          "clear_chat_and_greeting",
          "Clear chat + greeting",
          icon = icon("trash")
        )
      )
    ),
    chat_mod_ui("chat")
  )
)

server <- function(input, output, session) {
  client <- ellmer::chat_anthropic(
    model = MODEL,
    system_prompt = CHAT_PROMPT,
    echo = "none"
  )

  chat <- chat_mod_server("chat", client, greeting = function() {
    greeter <- ellmer::chat_anthropic(
      model = MODEL,
      system_prompt = GREETING_PROMPT,
      echo = "none"
    )
    greeter$stream_async("Generate a welcome greeting for the user.")
  })

  observeEvent(input$clear_chat, {
    chat$clear()
  })

  observeEvent(input$clear_chat_and_greeting, {
    chat$clear(greeting = TRUE)
  })
}

shinyApp(ui, server)
haiku_app.py — Python
"""
Live Haiku-powered greeting demo (Python).

Generates a fresh greeting via the state-driven `<id>_greeting_requested`
Shiny input. Set ANTHROPIC_API_KEY before running.

    uv run shiny run _dev/agents/greeting/haiku_app.py
"""

from chatlas import ChatAnthropic
from shiny import App, reactive, ui

from shinychat import Chat, chat_greeting, chat_ui

MODEL = "claude-haiku-4-5"

GREETING_PROMPT = """\
You generate short, friendly welcome greetings for a programming explainer app.
Respond with markdown only — no preamble, no closing remarks.

Use exactly this structure:

## <a brief, varied welcome heading>

<one inviting sentence about explaining programming and data science concepts>

**Try one of these:**

- <span class="suggestion">programming or data science topic 1</span>
- <span class="suggestion">programming or data science topic 2</span>
- <span class="suggestion">programming or data science topic 3</span>

Suggestions should be specific concepts like "What is a closure?",
"Explain tidy evaluation", "How does gradient descent work?", etc.
Vary the wording each time. Keep the entire greeting under 60 words.
"""

CHAT_PROMPT = (
    "You explain programming and data science concepts concisely "
    "in 1-2 sentences. Use markdown when appropriate."
)


app_ui = ui.page_fillable(
    ui.card(
        ui.card_header(
            "Haiku greeting demo (Python)",
            ui.toolbar(
                ui.toolbar_input_button("clear_chat", "Clear", tooltip="Clear chat"),
                ui.toolbar_input_button(
                    "clear_chat_and_greeting",
                    "Reset",
                    tooltip="Clear chat + greeting",
                ),
                align="right",
            ),
        ),
        chat_ui("chat", placeholder="Ask me anything...", fill=True),
    ),
)


def server(input, output, session):
    chat = Chat(id="chat")
    client = ChatAnthropic(model=MODEL, system_prompt=CHAT_PROMPT)

    @chat.on_user_submit
    async def _(user_input: str):
        stream = await client.stream_async(user_input)
        await chat.append_message_stream(stream)

    # Fires when the chat is visible, empty, and has no greeting.
    @reactive.effect
    @reactive.event(input.chat_greeting_requested)
    async def _generate_greeting():
        greeter = ChatAnthropic(model=MODEL, system_prompt=GREETING_PROMPT)
        stream = await greeter.stream_async("Generate a welcome greeting for the user.")
        await chat.set_greeting(chat_greeting(stream))

    @reactive.effect
    @reactive.event(input.clear_chat)
    async def _clear_chat():
        await chat.clear_messages()

    @reactive.effect
    @reactive.event(input.clear_chat_and_greeting)
    async def _clear_chat_and_greeting():
        await chat.clear_messages(greeting=True)


app = App(app_ui, server)

gadenbuie and others added 30 commits May 14, 2026 15:13
Adds GreetingOptions + five greeting ChatAction members, a GreetingData
slice on ChatState with reducer cases (greeting, greeting_start,
greeting_chunk, greeting_end, greeting_clear) plus auto-dismiss in
INPUT_SENT/message/chunk_start and re-show in clear. Introduces the
ChatGreeting React component (stand-alone wrapper, MarkdownContent
rendering, fade-in/fade-out animations, prefers-reduced-motion), the
greeting SCSS partial, ChatContainer integration as first child of
.shiny-chat-messages-content, and chat-entry/ChatApp wiring of the
static greeting attribute.

Refs #217
Introduces chat_greeting() data wrapper with content + dismissible /
as_assistant_message / include_in_history options (validating the
dismissible+as_assistant constraint) and adds the greeting parameter
to chat_ui(). Plain strings, HTML(), and htmltools tags/tagList are
auto-wrapped; tag dependencies are collected and attached to the
container. include_in_history is kept server-only and never appears
in the JSON wire payload.

Refs #217
Adds the ChatGreeting class and chat_greeting() factory following the
ChatMessage pattern, accepting str / HTML / Tag / TagList /
AsyncIterator[str] content and the same option set as the R wrapper.
chat_ui() (and ChatExpress.ui()) gain a greeting parameter that auto-
wraps non-ChatGreeting inputs, blocks async iterators (pointing users
to Chat.set_greeting()), collects htmltools dependencies, and
serializes the wire payload onto the <shiny-chat-container> attribute.
include_in_history stays server-only.

Refs #217
Server entry point for sending greeting custom messages. Handles
NULL (greeting_clear), auto-wraps non-chat_greeting input, delegates
to chat_append() when as_assistant_message=TRUE, sends a single
greeting action for static string/HTML/tag content (collecting HTML
deps via process_ui), and streams greeting_start / greeting_chunk /
greeting_end for generator/promise content. include_in_history is
stripped from all wire payloads.

Refs #217
Async server entry point mirroring the R chat_set_greeting() shape.
None sends greeting_clear; non-ChatGreeting input is auto-wrapped;
as_assistant_message delegates to append_message / append_message_stream;
static content sends a single greeting action with HTML deps; async
iterator content streams greeting_start / greeting_chunk / greeting_end.
Adds matching TypedDicts (GreetingOptions and the five greeting
ChatAction variants) to _chat_types.py. include_in_history stays
server-only.

Refs #217
JS: 29 reducer tests for the five greeting actions plus auto-dismiss
on INPUT_SENT/message/chunk_start and re-show on clear; 4 component
tests for ChatGreeting visibility, data-dismissing transition, and
prefers-reduced-motion fast path. R: 21 testthat tests covering
chat_greeting() validation, chat_ui(greeting=) attribute serialization
and HTML deps, and chat_set_greeting() envelope across NULL, static
string, HTML, as_assistant_message delegation, and generator
streaming. Python: 19 pytest tests mirroring the R suite for
ChatGreeting, chat_ui(greeting=), and Chat.set_greeting().

Refs #217
Centered text breaks reading flow for code blocks, tables, and lists
that authors will naturally include in a greeting. Drop text-align:
center and bump --shiny-chat-greeting-max-width from 540px to 680px
so a row of three 175px suggestion cards (with 0.75rem gaps) fits
comfortably inside the greeting's 1.5rem padding.

Refs #217
Without width: 100%, the wrapper sizes to its content and only caps at
max-width, so streaming chunks that widen the content (e.g. a code
block or table appearing partway through) cause the greeting to shift
horizontally. Pin width to 100% so it claims its full slot up to
--shiny-chat-greeting-max-width from the first render.

Refs #217
Measure the greeting's offsetHeight at the moment dismiss starts and
stash it on the element as --_dismiss-height, then animate height from
that value down to 0 alongside opacity. Also pulls margin-bottom to
-2rem (matching the parent flex gap) so the next message slides up
into the freed space instead of dropping abruptly after the element
unmounts. Animation bumped to 250ms to give the collapse a perceptible
arc; overflow hidden during dismiss clips content as it shrinks.

Refs #217
The reducer would set greeting.visible=false in one commit, but the
component's local dismissing flag was promoted in a follow-up useEffect.
Between the two paints, the render check (!visible && !dismissing)
returned null — the greeting briefly vanished, the message list reflowed
up to fill the space, then the greeting came back with data-dismissing
to animate from full height. Visually the greeting "reappeared" before
collapsing.

Detect the visible: true→false transition synchronously during render
via a derived-prev-state pattern so dismissing flips in the same commit
as visible=false. The captured rendered height is now stored from the
last visible-and-not-dismissing layout effect and applied as an inline
--_dismiss-height in the same render that flips data-dismissing, so the
animation interpolates from the real height without needing an extra
DOM measurement after the dismiss flag is set.

Refs #217
Once the user submits, the new user message and assistant placeholder
both pop into the DOM. If the greeting starts collapsing in the same
beat, too many elements move at once and the eye can't track any of
them. Introduce a 350ms "pending dismiss" hold: visible=false flips
the greeting into a holding state where it stays at full height, then
data-dismissing is applied (and the height collapse runs) after the
delay. Reduced motion still removes immediately.

Refs #217
Flip the dismiss ordering: instead of holding the greeting open while
the user / assistant messages settle into the DOM, hide the new
messages until the greeting has finished collapsing. The reducer still
adds them immediately (the network side keeps working), but a
data-greeting-dismissing attribute on .shiny-chat-messages-content
suppresses any child that isn't the greeting itself for the duration
of the exit animation. When the greeting unmounts, the attribute
clears and the messages appear in their final position.

Lifts the dismiss lifecycle onto greeting state via a new
greeting.dismissing flag (set by the reducer on the same transition
that flips visible=false; cleared by a new greeting_dismissed UI
action that ChatGreeting dispatches on animationend, or immediately
when prefers-reduced-motion is set).

Refs #217
Drop the dismissed-guard short-circuit on the greeting, greeting_start,
and greeting_chunk reducer cases. A dismissed greeting now absorbs new
content silently — visible stays false, dismissed stays true — and the
fresh content surfaces the next time the user clears the chat. This
matches the natural author flow: set a dynamic greeting on every
server tick (or on profile load) without having to first clear via
chat_set_greeting(id, NULL).

Refs #217
Greetings are now a self-contained feature with their own lifecycle
(auto-dismiss, re-show on clear, replace-while-dismissed, server-only
clear). Authors who want an assistant-styled welcome can call
chat_append() directly — the de facto pattern that pre-dated this
feature — so the as_assistant_message flag was only adding a half-
implemented mode without any new capability.

Removes the parameter from chat_greeting(), the dismissible+as_assistant
validation, the field from the S3 list, and the delegation branch in
chat_set_greeting() that routed to chat_append(). chat_set_greeting()
now always sends greeting-protocol actions. Drops the two tests that
exercised the removed branches.

Refs #217
Mirrors the R change: greetings are self-contained, so the
as_assistant_message flag is removed from ChatGreeting / chat_greeting()
and from Chat.set_greeting()'s delegation branch. The method now always
routes through the greeting-action path. Authors who want an
assistant-styled welcome can call Chat.append_message() /
Chat.append_message_stream() directly.

Refs #217
Gate the reveal keyframe on a shiny-chat-greeting--enter class that is
applied for the first paint of each ChatGreeting instance and removed
after the reveal's animationend (or immediately under reduced motion).
A chat_set_greeting() that replaces a still-visible greeting keeps the
same wrapper mounted, so the new content swaps in without any entrance
animation. Clear → re-show and the regenerate pattern unmount the
wrapper, so the next mount re-runs the entrance animation as expected.

Also scope the dismiss animationend listener to its own animation name
so a stray reveal-end event can't fire greeting_dismissed.

Refs #217
useStickToBottom watches contentRef's size and auto-scrolls when it
grows. With the greeting rendered inside contentRef, a streaming
greeting that overflowed the viewport would yank the scroll to its
bottom — pulling the start of the welcome out of the reader's view.

Move <ChatGreeting> outside the contentRef wrapper (still inside the
scroll container) so only message growth participates in stick-to-
bottom. Make the scroll container a flex column so a stand-alone
greeting can margin-block:auto into the vertical center when no
messages exist, and simplify the dismiss-hide and padding selectors
now that greeting is no longer a child of .shiny-chat-messages-content.

Refs #217
The flag was stored on chat_greeting() / ChatGreeting but never read
by anything — the option was always stripped from the wire payload and
there is no server-side history slot to push greeting content into.
shinychat doesn't manage model history (authors do), so wiring this is
a feature in its own right and out of scope for the greeting work.

Same reasoning as the recent as_assistant_message drop: keep greetings
a self-contained UI feature and avoid shipping half-implemented modes
that promise behavior they don't deliver. Drops the param from R's
chat_greeting() and Python's ChatGreeting/chat_greeting(), regenerates
the R man page, and prunes the no-longer-relevant test assertions.

Refs #217
Adds a greeting argument to chat_mod_server() for the common case
where the author wants to seed a one-shot static or pre-computed
greeting at module initialization, and a namespace-aware set_greeting()
helper on the returned list for everything else (replacing the
greeting, streaming via a generator, or clearing it from any
reactive context). The returned helper closes over the module's
session/namespace so callers do not have to reach into the
chat_mod_ui()'s internal NS() wiring.

Refs #217
When the greeting was moved out of .shiny-chat-messages-content, the
click / focus / keydown handlers stayed on that inner div — so
suggestion clicks inside the greeting no longer bubbled to a handler.
Move those event handlers up to .shiny-chat-messages (the scroll
container that contains both the greeting and the messages-content
div); clicks from either subtree now reach the handlers.

Drop the height-collapse half of the dismiss animation in favor of a
plain opacity fade. The height interpolation no longer worked cleanly
once the greeting's vertical centering used margin-block:auto from
the flex scroll container — the auto margins keep absorbing space as
height shrinks. The data-greeting-dismissing attribute on
.shiny-chat-messages-content still hides newly-arrived messages during
the fade, so the visual sequence is: fade out → unmount → messages
appear.

Removes the now-unused inline --_dismiss-height style, the
useLayoutEffect that captured offsetHeight, and the margin-bottom
hack that cancelled the parent flex gap.

Refs #217
INPUT_SENT adds the user message and assistant placeholder to
messages-content synchronously, even though they're CSS-hidden via
[data-greeting-dismissing] for the duration of the fade. That flipped
messages-content from :empty to "has children" — and with it the
:has() selector that gave the greeting margin-block:auto — so the
greeting visibly jumped from centered to top-aligned the instant
INPUT_SENT landed, then faded from its new position.

Extend the centering and padding-bottom selectors to treat
[data-greeting-dismissing] as equivalent to :empty. The greeting now
holds whatever position it had until the fade completes; only after
animationend (and the resulting greeting_dismissed reducer action)
does the attribute clear, the messages become visible, and the layout
settle into its new shape.

A visible greeting can never coexist with pre-existing visible
messages (auto-dismiss fires synchronously without animation on the
"greeting + initial messages" path), so this rule is unambiguous.

Refs #217
Derive the at-bottom signal from the scroll container itself rather
than useStickToBottom's contentRef-based isAtBottom. The greeting
lives outside contentRef (so its growth does not engage stick-to-
bottom), which meant a tall standalone greeting could overflow the
scroll area without ever revealing the scroll-to-bottom button or
the input's top shadow. The button's streaming style also tracks
greeting.streaming now.
Sends "{id}_greeting_requested" with value "init" on first paint and
"cleared" after each chat_clear(), deferred by IntersectionObserver
until the chat element is visible in the viewport.
… chat_clear

Replace the lifecycle-event model ("init"/"cleared") with a
state-driven signal that fires only when the chat is visible, empty,
and has no greeting. This avoids wasted LLM greeting calls on
bookmark restore, where the old "init" fired before restored messages
arrived.

chat_clear() gains a `greeting` parameter (R: greeting = FALSE,
Python: greeting: bool = False). When TRUE, the greeting is also
cleared, causing greeting_requested to fire again naturally.
The `greeting` parameter now accepts a function in addition to static
content. When a function is provided, the module sets up an internal
observer on `greeting_requested` that calls it on init and after
`clear(greeting = TRUE)`. Zero-arg functions are called as-is; one-arg
functions receive a cloned client with empty turns.
Covers greeting appearance, suggestion cards, dismiss on message,
re-show after clear, and regeneration after clear+greeting.
gadenbuie added 4 commits May 15, 2026 08:49
CI installs shiny>=1.4.0 which predates ui.toolbar. Feature-detect
and use input_action_button as fallback.
- Add `greeting` field to ClearAction TypedDict
- Type clear_messages action as ClearAction instead of dict[str, object]
- Add isinstance guards for content type narrowing in tests
- Add pyright: ignore for hasattr-guarded ui.toolbar calls
- Remove mock-session tests (covered by Playwright e2e tests)
@gadenbuie gadenbuie marked this pull request as ready for review May 15, 2026 15:49
@gadenbuie gadenbuie requested a review from cpsievert May 15, 2026 15:49
gadenbuie added 4 commits May 15, 2026 12:05
- chat_greeting(): add Patterns section (non-dismissible, suggestions, tags)
- chat_ui(): add Greeting section explaining greeting_requested input
- chat_set_greeting(): add LLM-generated and regenerate pattern examples
- chat_clear(): add description and regenerate example
- chat_mod_server(): add Greeting section with static/zero-arg/one-arg modes
- chat_greeting(): add examples (non-dismissible, suggestions) and See Also
- chat_ui(): document greeting_requested input in greeting param
- Chat.set_greeting(): add Notes on greeting_requested, LLM and regenerate examples
- Chat.clear_messages(): expand greeting param with regenerate pattern
- quartodoc: add chat_greeting to API reference
- Merge greeting_requested into main changelog bullet (both packages)
- Move Greeting section before params in chat_ui() roxygen
- Reorder chat_mod_server() greeting modes: one-arg first, zero-arg second
- Lead with shared behavior before mode-specific details
- Fix "first load" → "first viewed on the page and empty"
- Collapse character vectors in chat_greeting() for convenience
Comment thread pkg-py/src/shinychat/_chat_types.py Outdated
gadenbuie and others added 9 commits May 18, 2026 14:49
ChatGreeting was special-casing HTML() to skip split_html_islands(),
unlike ChatMessage which routes all non-string content through it.
…elpers

Deduplicate three identical greeting dismiss blocks in the reducer
(INPUT_SENT, message, chunk_start) into a shared dismissGreeting()
helper. Extract the shared visibility computation from greeting and
greeting_start cases into computeGreetingVisibility(). Add clarifying
comment on the clear case ternary.
Move the duplicated hook from ChatGreeting.tsx and ThinkingDisplay.tsx
into its own module so both components import from a single source.
Export InitialGreeting from ChatApp.tsx and import it in chat-entry.ts
instead of defining the same interface in both files.
…etection

- Wrap greeting streaming in ExtendedTask so the session stays responsive
- Switch greeting function detection from arity to named-argument matching
- Add guard in chat_set_greeting() when a function is passed as content
Comment thread pkg-py/src/shinychat/__init__.py Outdated
gadenbuie added 2 commits May 18, 2026 17:22
The greeting function argument detection checks for `client` by name,
but three tests used `greeter`, causing the argument to never be passed.
@cpsievert
Copy link
Copy Markdown
Collaborator

I noticed that set_greeting() is a silent no-op after the greeting has been dismissed — it updates the internal state but nothing visible changes until clear(greeting=TRUE) resets things (via computeGreetingVisibility in state.ts:142-150).

I think that makes sense as a design choice, but I could see someone calling set_greeting("Try asking about X\!") mid-conversation and being confused when nothing happens. Might be worth calling this out in the set_greeting() docstrings so people don't have to discover it through debugging.

Comment thread js/src/chat/state.ts
Comment on lines +45 to +54
export interface GreetingData {
content: string
contentType: ContentType
streaming: boolean
visible: boolean
dismissed: boolean
dismissing: boolean
options: GreetingOptions
blocks: ContentBlock[]
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dismiss lifecycle uses three correlated booleans (visible, dismissed, dismissing) that have 8 possible combinations, most of which are invalid — the reducer has to be careful not to produce them. Would a single status: "visible" | "dismissing" | "dismissed" enum work here? It would make impossible states unrepresentable while keeping the same animation coordination. The layout-stability trick with [data-greeting-dismissing] and the centering CSS is clever — not suggesting changing any of that.

Comment thread pkg-r/R/chat.R
Comment on lines +1040 to +1046
if (is.character(msg)) {
ui <- list(html = msg, deps = NULL)
chunk_content_type <- "markdown"
} else {
ui <- process_ui(pre_process_ui(msg), session)
chunk_content_type <- "html"
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this means that HTML() will get treated like markdown.

):
self.dismissible = dismissible

if isinstance(content, AsyncIterator):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check is too narrow. In Python, a value can be a valid async stream for async for without being an AsyncIterator specifically. If that happens here, we will fall through into the non-stream path instead.

I think the fix here should be:

  • widen the types from AsyncIterator to AsyncIterable
  • update the runtime check accordingly, not just the annotation
  • add a test with a custom async-iterable object, not just a native async generator

I would keep this scoped to async iterables only for this PR. Supporting sync iterables would be a separate feature expansion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Greeting messages

2 participants