chainlit
chainlit copied to clipboard
Command field not persisted to frontend when resuming threads
Describe the bug
The command field is not serialized to the frontend when resuming chat threads, causing command badges to disappear when switching between conversations. The database schema includes a command TEXT column that persists correctly, but SQLAlchemyDataLayer.get_thread() does not include this field in the step serialization sent to the React frontend, even though the frontend has rendering logic for it.
To Reproduce
Steps to reproduce the behavior:
- Set up a Chainlit application with PostgreSQL data persistence using
SQLAlchemyDataLayer - Configure slash commands using
cl.context.emitter.set_commands()in@cl.on_chat_start - Start a new chat and send a message using a slash command (e.g.,
/test,/test-command) - Verify the command badge appears next to the user message (showing which command was used)
- Switch to a different conversation or reload the page to trigger
@cl.on_chat_resume - Return to the original conversation
- Observe that the command badge has disappeared from the user message
Expected behavior
Command badges should persist across conversation switches. When resuming a thread via @cl.on_chat_resume, steps loaded from the database should include the command field so the frontend can render the command badge consistently.
Database Evidence
The PostgreSQL database shows that the command field is being saved correctly:
SELECT id, type, command FROM ui.steps WHERE command IS NOT NULL LIMIT 2;
Returns:
id | type | command
--------------------------------------+--------------+--------------
d4e89f12-3456-7890-abcd-ef1234567890 | user_message | test
a1b2c3d4-5678-90ab-cdef-123456789abc | user_message | test-command
However, when SQLAlchemyDataLayer.get_thread() retrieves and serializes these steps, the command field is not included in the ThreadDict response sent to the frontend.
Frontend Evidence
The React frontend (in chainlit/frontend/dist/assets/index-*.js) has the rendering logic for command badges:
e.command?De.jsx("div",{className:"font-bold text-[#08f] command-span",children:e.command}):null
This confirms the frontend expects a command property on step objects and will render it when present.
Current Workaround
We've implemented a workaround by creating a custom data layer:
class CustomSQLAlchemyDataLayer(SQLAlchemyDataLayer):
async def get_thread(self, thread_id: str) -> ThreadDict | None:
thread = await super().get_thread(thread_id)
if thread and "steps" in thread:
for step in thread["steps"]:
metadata = step.get("metadata")
if metadata and "command" in metadata:
step["command"] = metadata["command"]
return thread
This copies the command from metadata.command (which we store separately) to step["command"] so the frontend receives it.
Environment:
- OS: macOS
- Browser: Chrome, Safari (issue occurs on all browsers)
- Chainlit Version: 2.9.0
- Python Version: 3.12
- Database: PostgreSQL (via Docker)
- Data Layer:
SQLAlchemyDataLayerwith PostgreSQL backend
Root Cause Analysis
The issue appears to be in chainlit/data/sql_alchemy.py where SQLAlchemyDataLayer.get_thread() serializes step records from the database. The command column exists in the database schema but is not being included in the step dictionary that gets serialized to ThreadDict and sent to the frontend.
Suggested Fix
Update SQLAlchemyDataLayer._record_to_step_dict() or the relevant serialization method to include the command field from the database column when converting step records to dictionaries.
Additional context
- The Commands API is documented in Chainlit's features, but persistence of command badges across sessions is not working out of the box
- This affects user experience as users lose context about which specialized agent/command was used for previous messages
- The database schema correctly includes the
commandcolumn, suggesting this feature was intended to work but the serialization layer is incomplete - Similar issues may exist with other step fields that are stored in the database but not serialized to the frontend