Skip to content

Commit 361db53

Browse files
authored
chore: prepare release (#2107)
* chore: prepare release * fix: test
1 parent e824635 commit 361db53

File tree

9 files changed

+91
-186
lines changed

9 files changed

+91
-186
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@ All notable changes to Chainlit will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7-
## Unreleased
7+
## [2.5.5] - 2025-04-14
88

99
### Added
1010

1111
- Avatars now support `.` in their name (will be replaced with `_`).
12+
- Typed session accessors for user session
13+
- Allow set attributes for the tags of the custom_js or custom_css
1214
- Hovering a past chat in the sidebar will display the full title of the chat in a tooltip
15+
- The `X-Chainlit-Session-id` header is now automatically set to facilitate sticky sessions with websockets
1316
- `cl.ErrorMessage` now have a different avatar
1417
- The copy button is now only displayed on the final message of a run, like feedback buttons
18+
- CopilotFunction is now usable in custom JS
19+
- Header link now have an optional `display_name` to display text next to the icon
20+
- The default .env file loaded by chainlit is now configurable with `CHAINLIT_ENV_FILE`
1521

1622
### Changed
1723

18-
- **[breaking]**: `http_referer`, `http_cookie` and `languages` are no longer directly available in the session object. Instead, `environ` is available containing all of those plus other HTTP headers.
24+
- **[breaking]**: `http_referer`, `http_cookie` and `languages` are no longer directly available in the session object. Instead, `environ` is available containing all of those plus other HTTP headers
25+
- The scroll to the bottom animation is now smooth
1926

2027
## [2.4.400] - 2025-03-29
2128

backend/chainlit/server.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -717,28 +717,10 @@ async def get_user(current_user: UserParam) -> GenericUser:
717717

718718

719719
@router.post("/set-session-cookie")
720-
async def set_session_cookie(
721-
request: Request, response: Response, current_user: UserParam
722-
):
720+
async def set_session_cookie(request: Request, response: Response):
723721
body = await request.json()
724722
session_id = body.get("session_id")
725723

726-
from chainlit.session import WebsocketSession
727-
728-
session = WebsocketSession.get_by_id(session_id)
729-
if not session:
730-
raise HTTPException(
731-
status_code=404,
732-
detail="Session not found",
733-
)
734-
735-
if current_user:
736-
if not session.user or session.user.identifier != current_user.identifier:
737-
raise HTTPException(
738-
status_code=401,
739-
detail="You are not authorized to set a session cookie for this session",
740-
)
741-
742724
is_local = request.client and request.client.host in ["127.0.0.1", "localhost"]
743725

744726
response.set_cookie(

backend/chainlit/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
except metadata.PackageNotFoundError:
66
# Case where package metadata is not available, default to a 'non-outdated' version.
77
# Ref: config.py::load_settings()
8-
__version__ = "2.4.400"
8+
__version__ = "2.5.5"

backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "chainlit"
3-
version = "2.4.400"
3+
version = "2.5.5"
44
keywords = [
55
'LLM',
66
'Agents',

backend/tests/test_server.py

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -687,93 +687,3 @@ def test_project_translations_file_path_traversal(
687687

688688
# Should give error status
689689
assert response.status_code == 422
690-
691-
692-
def test_set_session_cookie_authorized(
693-
test_client, mock_get_current_user, mock_session_get_by_id_patched
694-
):
695-
"""
696-
Test that a valid session cookie is set when the current user is authorized.
697-
Using the existing fixtures, the patched WebsocketSession returns a session
698-
when the session_id is "test_session_id".
699-
"""
700-
# Create an authorized user object.
701-
authorized_user = type("User", (), {"identifier": "user123"})()
702-
# Set the session's user to match the authorized user.
703-
mock_session_get_by_id_patched.user = authorized_user
704-
# Override current user dependency.
705-
mock_get_current_user.return_value = authorized_user
706-
707-
response = test_client.post(
708-
"/set-session-cookie", json={"session_id": "test_session_id"}
709-
)
710-
assert response.status_code == 200, response.text
711-
data = response.json()
712-
assert data.get("message") == "Session cookie set"
713-
714-
set_cookie = response.headers.get("set-cookie")
715-
assert "X-Chainlit-Session-id=test_session_id" in set_cookie
716-
assert "HttpOnly" in set_cookie
717-
assert "Secure" in set_cookie
718-
assert "SameSite=none" in set_cookie
719-
720-
721-
def test_set_session_cookie_session_not_found(test_client, mock_get_current_user):
722-
"""
723-
Test that a POST with an unknown session_id returns a 404 error.
724-
This is simulated by providing a session_id different from "test_session_id",
725-
which the existing mock_session_get_by_id_patched fixture uses.
726-
"""
727-
# Set a valid current user.
728-
current_user = type("User", (), {"identifier": "user123"})()
729-
mock_get_current_user.return_value = current_user
730-
731-
response = test_client.post(
732-
"/set-session-cookie", json={"session_id": "invalid_session"}
733-
)
734-
assert response.status_code == 404
735-
736-
737-
def test_set_session_cookie_unauthorized(
738-
test_client, mock_get_current_user, mock_session_get_by_id_patched
739-
):
740-
"""
741-
Test that when the current user does not match the session's user,
742-
a 401 error is returned.
743-
"""
744-
# Create a session with a specific user.
745-
session_user = type("User", (), {"identifier": "user123"})()
746-
# Create a different (unauthorized) current user.
747-
unauthorized_user = type("User", (), {"identifier": "different"})()
748-
mock_session_get_by_id_patched.user = session_user
749-
mock_get_current_user.return_value = unauthorized_user
750-
751-
response = test_client.post(
752-
"/set-session-cookie", json={"session_id": "test_session_id"}
753-
)
754-
assert response.status_code == 401
755-
756-
757-
def test_set_session_cookie_no_current_user(
758-
test_client, mock_get_current_user, mock_session_get_by_id_patched
759-
):
760-
"""
761-
Test that if current_user is None (i.e. no user provided), the authorization check is skipped,
762-
and the cookie is set.
763-
"""
764-
# Set the session's user to any value.
765-
session_user = type("User", (), {"identifier": "user123"})()
766-
mock_session_get_by_id_patched.user = session_user
767-
# Simulate absence of a current user.
768-
mock_get_current_user.return_value = None
769-
770-
response = test_client.post(
771-
"/set-session-cookie", json={"session_id": "test_session_id"}
772-
)
773-
assert response.status_code == 200, response.text
774-
data = response.json()
775-
assert data.get("message") == "Session cookie set"
776-
set_cookie = response.headers.get("set-cookie")
777-
assert "X-Chainlit-Session-id=test_session_id" in set_cookie
778-
assert "Secure" in set_cookie
779-
assert "SameSite=none" in set_cookie

frontend/src/components/LeftSidebar/ThreadList.tsx

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import {
4747
SidebarMenuButton,
4848
SidebarMenuItem
4949
} from '@/components/ui/sidebar';
50-
5150
import {
5251
Tooltip,
5352
TooltipContent,
@@ -284,61 +283,68 @@ export function ThreadList({
284283
</DialogContent>
285284
</Dialog>
286285
<TooltipProvider delayDuration={300}>
287-
{sortedTimeGroupKeys.map((group) => {
288-
const items = threadHistory!.timeGroupedThreads![group];
289-
return (
290-
<SidebarGroup key={group}>
291-
<SidebarGroupLabel>{getTimeGroupLabel(group)}</SidebarGroupLabel>
292-
<SidebarGroupContent>
293-
<SidebarMenu>
294-
{items.map((thread) => {
295-
const isResumed =
296-
idToResume === thread.id && !threadHistory!.currentThreadId;
297-
const isSelected =
298-
isResumed || threadHistory!.currentThreadId === thread.id;
299-
return (
300-
<SidebarMenuItem key={thread.id} id={`thread-${thread.id}`}>
301-
<Tooltip>
286+
{sortedTimeGroupKeys.map((group) => {
287+
const items = threadHistory!.timeGroupedThreads![group];
288+
return (
289+
<SidebarGroup key={group}>
290+
<SidebarGroupLabel>{getTimeGroupLabel(group)}</SidebarGroupLabel>
291+
<SidebarGroupContent>
292+
<SidebarMenu>
293+
{items.map((thread) => {
294+
const isResumed =
295+
idToResume === thread.id &&
296+
!threadHistory!.currentThreadId;
297+
const isSelected =
298+
isResumed || threadHistory!.currentThreadId === thread.id;
299+
return (
300+
<SidebarMenuItem
301+
key={thread.id}
302+
id={`thread-${thread.id}`}
303+
>
304+
<Tooltip>
302305
<TooltipTrigger asChild>
303-
<Link to={isResumed ? '' : `/thread/${thread.id}`}>
304-
<SidebarMenuButton
305-
isActive={isSelected}
306-
className="relative truncate h-9 group/thread"
307-
>
308-
{thread.name || (
309-
<Translator path="threadHistory.thread.untitled" />
310-
)}
311-
<div
312-
className={cn(
313-
'absolute w-10 bottom-0 top-0 right-0 bg-gradient-to-l from-[hsl(var(--sidebar-background))] to-transparent'
314-
)}
315-
/>
316-
<ThreadOptions
317-
onDelete={() => setThreadIdToDelete(thread.id)}
318-
onRename={() => {
319-
setThreadIdToRename(thread.id);
320-
setThreadNewName(thread.name);
321-
}}
322-
className={cn(
323-
'absolute z-20 bottom-0 top-0 right-0 bg-sidebar-accent hover:bg-sidebar-accent hover:text-primary flex opacity-0 group-hover/thread:opacity-100',
324-
isSelected && 'bg-sidebar-accent opacity-100'
325-
)}
326-
/>
327-
</SidebarMenuButton>
328-
</Link>
329-
</TooltipTrigger>
330-
<TooltipContent side="right" align="center">
306+
<Link to={isResumed ? '' : `/thread/${thread.id}`}>
307+
<SidebarMenuButton
308+
isActive={isSelected}
309+
className="relative truncate h-9 group/thread"
310+
>
311+
{thread.name || (
312+
<Translator path="threadHistory.thread.untitled" />
313+
)}
314+
<div
315+
className={cn(
316+
'absolute w-10 bottom-0 top-0 right-0 bg-gradient-to-l from-[hsl(var(--sidebar-background))] to-transparent'
317+
)}
318+
/>
319+
<ThreadOptions
320+
onDelete={() =>
321+
setThreadIdToDelete(thread.id)
322+
}
323+
onRename={() => {
324+
setThreadIdToRename(thread.id);
325+
setThreadNewName(thread.name);
326+
}}
327+
className={cn(
328+
'absolute z-20 bottom-0 top-0 right-0 bg-sidebar-accent hover:bg-sidebar-accent hover:text-primary flex opacity-0 group-hover/thread:opacity-100',
329+
isSelected &&
330+
'bg-sidebar-accent opacity-100'
331+
)}
332+
/>
333+
</SidebarMenuButton>
334+
</Link>
335+
</TooltipTrigger>
336+
<TooltipContent side="right" align="center">
331337
<p>{thread.name}</p>
332338
</TooltipContent>
333-
</Tooltip>
334-
</SidebarMenuItem>
335-
);
336-
})}
337-
</SidebarMenu>
338-
</SidebarGroupContent>
339-
</SidebarGroup>
340-
);
341-
})}
339+
</Tooltip>
340+
</SidebarMenuItem>
341+
);
342+
})}
343+
</SidebarMenu>
344+
</SidebarGroupContent>
345+
</SidebarGroup>
346+
);
347+
})}
342348
</TooltipProvider>
343349
{isLoadingMore ? (
344350
<div className="flex items-center justify-center p-2">

libs/react-client/src/api/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ export class ChainlitAPI extends APIBase {
184184
return res.json();
185185
}
186186

187+
async stickyCookie(sessionId: string) {
188+
const res = await this.fetch('POST', '/set-session-cookie', {
189+
session_id: sessionId
190+
});
191+
return res.json();
192+
}
193+
187194
async passwordAuth(data: FormData) {
188195
const res = await this.post(`/login`, data);
189196
return res.json();

libs/react-client/src/types/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ export interface IChainlitConfig {
4444
login_page_image_filter?: string;
4545
login_page_image_dark_filter?: string;
4646
custom_meta_image_url?: string;
47-
header_links?: { name: string; display_name: string; icon_url: string; url: string }[];
47+
header_links?: {
48+
name: string;
49+
display_name: string;
50+
icon_url: string;
51+
url: string;
52+
}[];
4853
};
4954
features: {
5055
spontaneous_file_upload?: {

libs/react-client/src/useChatSession.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const useChatSession = () => {
9494
}, [currentThreadId]);
9595

9696
const _connect = useCallback(
97-
({
97+
async ({
9898
transports,
9999
userEnv
100100
}: {
@@ -108,6 +108,12 @@ const useChatSession = () => {
108108
? `${pathname}/ws/socket.io`
109109
: '/ws/socket.io';
110110

111+
try {
112+
await client.stickyCookie(sessionId);
113+
} catch (err) {
114+
console.error(`Failed to set sticky session cookie: ${err}`);
115+
}
116+
111117
const socket = io(uri, {
112118
path,
113119
withCredentials: true,
@@ -129,24 +135,6 @@ const useChatSession = () => {
129135
});
130136

131137
socket.on('connect', () => {
132-
fetch(`${uri}/set-session-cookie`, {
133-
method: 'POST',
134-
credentials: 'include',
135-
headers: {
136-
'Content-Type': 'application/json'
137-
},
138-
body: JSON.stringify({ session_id: sessionId })
139-
})
140-
.then((response) => {
141-
if (!response.ok) {
142-
console.error('Failed to set session cookie');
143-
} else {
144-
console.log('Session cookie set successfully');
145-
}
146-
})
147-
.catch((error) => {
148-
console.error('Error setting session cookie:', error);
149-
});
150138
socket.emit('connection_successful');
151139
setSession((s) => ({ ...s!, error: false }));
152140
setMcps((prev) =>

0 commit comments

Comments
 (0)