Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions js/dist/shinychat.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions js/dist/shinychat.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/src/chat/ChatApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ function makeInitialGreeting(
greeting: InitialGreeting,
messagesLength: number,
): GreetingData {
const dismissible = greeting.options.dismissible !== false
const persistent = greeting.options.persistent === true
const status: GreetingData["status"] =
dismissible && messagesLength > 0 ? "dismissed" : "visible"
!persistent && messagesLength > 0 ? "dismissed" : "visible"
return {
content: greeting.content,
contentType: greeting.contentType,
Expand Down
17 changes: 7 additions & 10 deletions js/src/chat/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,19 @@ function removeLoadingMessage(messages: ChatMessageData[]): ChatMessageData[] {
}

function dismissGreeting(greeting: GreetingData | null): GreetingData | null {
if (
greeting?.options.dismissible !== false &&
greeting?.status === "visible"
) {
if (greeting?.options.persistent !== true && greeting?.status === "visible") {
return { ...greeting, status: "dismissing" }
}
return greeting
}

function computeGreetingVisibility(
prior: GreetingData | null,
dismissible: boolean,
persistent: boolean,
hasMessages: boolean,
): "visible" | "dismissing" | "dismissed" {
if (prior?.status === "dismissed") return "dismissed"
if (dismissible && hasMessages) return "dismissed"
if (!persistent && hasMessages) return "dismissed"
return "visible"
}

Expand Down Expand Up @@ -904,13 +901,13 @@ export function chatReducer(state: ChatState, action: AnyAction): ChatState {
}

case "greeting": {
const dismissible = action.options.dismissible !== false
const persistent = action.options.persistent === true
// If a greeting was already dismissed, accept the new content silently so
// it surfaces the next time the message list is cleared. Otherwise apply
// the standard auto-dismiss rule when initial messages exist.
const status = computeGreetingVisibility(
state.greeting,
dismissible,
persistent,
state.messages.length > 0,
)
return {
Expand All @@ -933,10 +930,10 @@ export function chatReducer(state: ChatState, action: AnyAction): ChatState {
}

case "greeting_start": {
const dismissible = action.options.dismissible !== false
const persistent = action.options.persistent === true
const status = computeGreetingVisibility(
state.greeting,
dismissible,
persistent,
state.messages.length > 0,
)
return {
Expand Down
2 changes: 1 addition & 1 deletion js/src/transport/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { AttachmentPayload } from "../chat/attachments"
export type ContentType = "markdown" | "html" | "text" | "thinking"

export interface GreetingOptions {
dismissible?: boolean
persistent?: boolean
}

export type MessagePayloadSegment = {
Expand Down
2 changes: 1 addition & 1 deletion js/tests/chat/ChatApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ describe("greeting_dismissed Shiny input", () => {
type: "greeting",
content: "Hello!",
content_type: "markdown",
options: { dismissible: true },
options: {},
})
})

Expand Down
34 changes: 17 additions & 17 deletions js/tests/chat/state.greeting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("chatReducer — greeting actions", () => {
])
})

it("auto-dismisses when dismissible:true and messages exist", () => {
it("auto-dismisses when persistent:false (default) and messages exist", () => {
const state = makeState({
messages: [
{
Expand All @@ -60,12 +60,12 @@ describe("chatReducer — greeting actions", () => {
type: "greeting",
content: "Hi there",
content_type: "markdown",
options: { dismissible: true },
options: {},
})
expect(next.greeting).toMatchObject({ status: "dismissed" })
})

it("auto-dismisses when options.dismissible is omitted (defaults to dismissible) and messages exist", () => {
it("auto-dismisses when options.persistent is omitted (defaults to non-persistent) and messages exist", () => {
const state = makeState({
messages: [
{
Expand All @@ -86,7 +86,7 @@ describe("chatReducer — greeting actions", () => {
expect(next.greeting).toMatchObject({ status: "dismissed" })
})

it("does not auto-dismiss when dismissible:false even if messages exist", () => {
it("does not auto-dismiss when persistent:true even if messages exist", () => {
const state = makeState({
messages: [
{
Expand All @@ -102,7 +102,7 @@ describe("chatReducer — greeting actions", () => {
type: "greeting",
content: "Sticky greeting",
content_type: "markdown",
options: { dismissible: false },
options: { persistent: true },
})
expect(next.greeting).toMatchObject({ status: "visible" })
})
Expand Down Expand Up @@ -147,7 +147,7 @@ describe("chatReducer — greeting actions", () => {
})
})

it("auto-dismisses when dismissible and messages exist", () => {
it("auto-dismisses when non-persistent and messages exist", () => {
const state = makeState({
messages: [
{
Expand Down Expand Up @@ -418,15 +418,15 @@ describe("chatReducer — greeting actions", () => {
})
})

describe("INPUT_SENT dismisses greeting", () => {
it("dismisses a dismissible visible greeting on user input", () => {
describe("INPUT_SENT dismisses non-persistent greeting", () => {
it("dismisses a non-persistent visible greeting on user input", () => {
const state = makeState({
greeting: {
content: "Hello",
contentType: "markdown",
streaming: false,
status: "visible",
options: { dismissible: true },
options: {},
blocks: [],
},
})
Expand All @@ -438,14 +438,14 @@ describe("chatReducer — greeting actions", () => {
expect(next.greeting).toMatchObject({ status: "dismissing" })
})

it("does not dismiss a non-dismissible greeting on user input", () => {
it("does not dismiss a persistent greeting on user input", () => {
const state = makeState({
greeting: {
content: "Hello",
contentType: "markdown",
streaming: false,
status: "visible",
options: { dismissible: false },
options: { persistent: true },
blocks: [],
},
})
Expand All @@ -459,7 +459,7 @@ describe("chatReducer — greeting actions", () => {
})

describe("message dismisses greeting", () => {
it("dismisses a dismissible visible greeting when a message arrives", () => {
it("dismisses a non-persistent visible greeting when a message arrives", () => {
const state = makeState({
greeting: {
content: "Hello",
Expand All @@ -480,14 +480,14 @@ describe("chatReducer — greeting actions", () => {
expect(next.greeting).toMatchObject({ status: "dismissing" })
})

it("does not dismiss a non-dismissible greeting when a message arrives", () => {
it("does not dismiss a persistent greeting when a message arrives", () => {
const state = makeState({
greeting: {
content: "Hello",
contentType: "markdown",
streaming: false,
status: "visible",
options: { dismissible: false },
options: { persistent: true },
blocks: [],
},
})
Expand All @@ -503,7 +503,7 @@ describe("chatReducer — greeting actions", () => {
})

describe("chunk_start dismisses greeting", () => {
it("dismisses a dismissible visible greeting when streaming starts", () => {
it("dismisses a non-persistent visible greeting when streaming starts", () => {
const state = makeState({
greeting: {
content: "Hello",
Expand All @@ -524,14 +524,14 @@ describe("chatReducer — greeting actions", () => {
expect(next.greeting).toMatchObject({ status: "dismissing" })
})

it("does not dismiss a non-dismissible greeting when streaming starts", () => {
it("does not dismiss a persistent greeting when streaming starts", () => {
const state = makeState({
greeting: {
content: "Hello",
contentType: "markdown",
streaming: false,
status: "visible",
options: { dismissible: false },
options: { persistent: true },
blocks: [],
},
})
Expand Down
2 changes: 1 addition & 1 deletion js/tests/chat/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe("chatReducer", () => {
contentType: "markdown",
streaming: false,
status: "visible",
options: { dismissible: true },
options: {},
blocks: [
{ type: "content", content: "Hello!", contentType: "markdown" },
],
Expand Down
2 changes: 2 additions & 0 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Bug fixes

* The `dismissible` parameter of `chat_greeting()` has been renamed to `persistent` with an inverted value. `dismissible=False` (greeting stays visible) is now `persistent=True`. The old `dismissible` argument still works but warns. (#260)

* Fixed suggestion cards and the greeting overflowing the chat container in narrow spaces such as sidebars. (#255)

## [0.5.1] - 2026-06-15
Expand Down
10 changes: 5 additions & 5 deletions pkg-py/src/shinychat/_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1679,7 +1679,7 @@ async def set_greeting(
* A markdown string, :class:`~htmltools.HTML`, :class:`~htmltools.Tag`, or
:class:`~htmltools.TagList`: displayed as a stand-alone greeting.
* A :func:`~shinychat.chat_greeting` object with options such as
``dismissible``.
``persistent``.
* A :func:`~shinychat.chat_greeting` wrapping an
:class:`~typing.AsyncIterable` of strings: streams the greeting content
chunk-by-chunk.
Expand All @@ -1697,7 +1697,7 @@ async def set_greeting(

Examples
--------
Static greeting (stand-alone, dismissible by default):
Static greeting (stand-alone, dismissed on first message by default):

```python
@reactive.effect
Expand All @@ -1714,7 +1714,7 @@ async def _():
async def _():
greeting = chat_greeting(
"## Welcome!",
dismissible=True,
persistent=True,
)
await chat.set_greeting(greeting)
```
Expand Down Expand Up @@ -1775,7 +1775,7 @@ async def _():
if not isinstance(greeting, ChatGreeting):
greeting = chat_greeting(greeting)

options: GreetingOptions = {"dismissible": greeting.dismissible}
options: GreetingOptions = {"persistent": greeting.persistent}
html_deps = self._serialize_html_deps(greeting.html_deps) if greeting.html_deps else None

content = greeting.content
Expand Down Expand Up @@ -2398,7 +2398,7 @@ def chat_ui(
greeting_payload: dict[str, object] = {
"content": greeting.content,
"content_type": greeting.content_type,
"options": {"dismissible": greeting.dismissible},
"options": {"persistent": greeting.persistent},
}
greeting_attr = json.dumps(greeting_payload)
greeting_deps = greeting.html_deps
Expand Down
52 changes: 41 additions & 11 deletions pkg-py/src/shinychat/_chat_types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import warnings
from typing import Any, AsyncIterable, Literal, Union

from htmltools import HTML, HTMLDependency, Tag, TagChild, TagList
Expand All @@ -8,6 +9,7 @@
from ._attachments import Attachment
from ._html_islands import split_html_islands
from ._typing_extensions import NotRequired, TypedDict
from ._utils_types import DEPRECATED, DEPRECATED_TYPE, MISSING, MISSING_TYPE

Role = Literal["assistant", "user", "system"]

Expand Down Expand Up @@ -89,7 +91,7 @@ class HideToolRequestAction(TypedDict):


class GreetingOptions(TypedDict):
dismissible: NotRequired[bool]
persistent: NotRequired[bool]


class GreetingAction(TypedDict):
Expand Down Expand Up @@ -212,9 +214,23 @@ def __init__(
self,
content: Union[str, HTML, Tag, TagList, "AsyncIterable[str]"],
*,
dismissible: bool = True,
persistent: "bool | MISSING_TYPE" = MISSING,
dismissible: DEPRECATED_TYPE = DEPRECATED,
):
self.dismissible = dismissible
if not isinstance(dismissible, DEPRECATED_TYPE):
if not isinstance(persistent, MISSING_TYPE):
raise ValueError(
"Cannot use both `persistent` and the deprecated `dismissible`. Use `persistent` only."
)
warnings.warn(
"The `dismissible` parameter is deprecated. "
"Use `persistent` (with inverted value) instead. "
"`dismissible=False` is equivalent to `persistent=True`.",
DeprecationWarning,
stacklevel=2,
)
persistent = not dismissible
self.persistent = persistent if not isinstance(persistent, MISSING_TYPE) else False

if isinstance(content, AsyncIterable):
self.content: Union[str, AsyncIterable[str]] = content
Expand All @@ -240,7 +256,8 @@ def __init__(
def chat_greeting(
content: Union[str, HTML, Tag, TagList, "AsyncIterable[str]"],
*,
dismissible: bool = True,
persistent: "bool | MISSING_TYPE" = MISSING,
dismissible: DEPRECATED_TYPE = DEPRECATED,
) -> ChatGreeting:
"""
Create a greeting for a chat UI.
Expand All @@ -256,10 +273,10 @@ def chat_greeting(
:class:`~htmltools.Tag`, :class:`~htmltools.TagList`, or an
:class:`~typing.AsyncIterable` of strings (streaming, only valid via
:meth:`~shinychat.Chat.set_greeting`).
dismissible
Whether the greeting can be dismissed when the user sends a message. When
``True`` (the default), the greeting is hidden once the user sends their first
message. Set to ``False`` to keep the greeting visible throughout the
persistent
Whether the greeting stays visible after the user sends a message. When
``False`` (the default), the greeting is hidden once the user sends their first
message. Set to ``True`` to keep the greeting visible throughout the
conversation, which is useful for persistent instructions or navigation.

Examples
Expand All @@ -272,10 +289,10 @@ def chat_greeting(
chat_greeting("## Welcome!\\n\\nHow can I help you today?")
```

Non-dismissible greeting that stays visible:
Persistent greeting that stays visible:

```python
chat_greeting("Please select a topic to get started.", dismissible=False)
chat_greeting("Please select a topic to get started.", persistent=True)
```

Greeting with suggestion cards (uses ``<span class="suggestion">``):
Expand All @@ -293,9 +310,22 @@ def chat_greeting(
:func:`~shinychat.chat_ui` : Set a static greeting in the UI definition.
:meth:`~shinychat.Chat.set_greeting` : Set or stream a greeting from the server.
"""
if not isinstance(dismissible, DEPRECATED_TYPE):
if not isinstance(persistent, MISSING_TYPE):
raise ValueError(
"Cannot use both `persistent` and the deprecated `dismissible`. Use `persistent` only."
)
warnings.warn(
"The `dismissible` parameter is deprecated. "
"Use `persistent` (with inverted value) instead. "
"`dismissible=False` is equivalent to `persistent=True`.",
DeprecationWarning,
stacklevel=2,
)
persistent = not dismissible
return ChatGreeting(
content,
dismissible=dismissible,
persistent=persistent,
)


Expand Down
Loading
Loading