From 6d8fa0264d3f4f82f5bcb0055c82c5445a6b0168 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Wed, 10 Jun 2026 17:06:32 +0300 Subject: [PATCH 01/13] feat(ui): add cot_display and show_step_details config options Add two new UI configuration options for controlling Chain of Thought step rendering: - `cot_display`: "list" (default, current behavior) or "compact" (collapses all steps into a single summary accordion line like "Using search_web" / "Used 5 tools") - `show_step_details`: boolean (default true). When false, steps render as flat labels without expandable input/output details. New CompactSteps component groups step-type children under one accordion with an animated "Using X" label while running and a "Used N steps/tools" summary when complete. Includes translations for all 23 supported languages. --- backend/chainlit/config.py | 8 ++ backend/chainlit/translations/ar-SA.json | 4 +- backend/chainlit/translations/bn.json | 4 +- backend/chainlit/translations/da-DK.json | 4 +- backend/chainlit/translations/de-DE.json | 4 +- backend/chainlit/translations/el-GR.json | 4 +- backend/chainlit/translations/en-US.json | 4 +- backend/chainlit/translations/es.json | 4 +- backend/chainlit/translations/fr-FR.json | 4 +- backend/chainlit/translations/gu.json | 4 +- backend/chainlit/translations/he-IL.json | 4 +- backend/chainlit/translations/hi.json | 4 +- backend/chainlit/translations/it.json | 4 +- backend/chainlit/translations/ja.json | 4 +- backend/chainlit/translations/kn.json | 4 +- backend/chainlit/translations/ko.json | 4 +- backend/chainlit/translations/ml.json | 4 +- backend/chainlit/translations/mr.json | 4 +- backend/chainlit/translations/nl.json | 4 +- backend/chainlit/translations/pt-PT.json | 112 +++++++++-------- backend/chainlit/translations/ta.json | 4 +- backend/chainlit/translations/te.json | 4 +- backend/chainlit/translations/zh-CN.json | 4 +- backend/chainlit/translations/zh-TW.json | 4 +- frontend/src/components/ReadOnlyThread.tsx | 4 + .../components/chat/Messages/CompactSteps.tsx | 117 ++++++++++++++++++ .../components/chat/Messages/Message/Step.tsx | 15 ++- .../src/components/chat/Messages/index.tsx | 60 +++++++-- .../chat/MessagesContainer/index.tsx | 4 + frontend/src/contexts/MessageContext.tsx | 4 +- frontend/src/types/messageContext.ts | 2 + libs/react-client/src/types/config.ts | 2 + 32 files changed, 327 insertions(+), 89 deletions(-) create mode 100644 frontend/src/components/chat/Messages/CompactSteps.tsx diff --git a/backend/chainlit/config.py b/backend/chainlit/config.py index a540752b33..96af6e541d 100644 --- a/backend/chainlit/config.py +++ b/backend/chainlit/config.py @@ -190,6 +190,12 @@ # Chain of Thought (CoT) display mode. Can be "hidden", "tool_call" or "full". cot = "full" +# CoT display layout. "list" shows each step individually, "compact" collapses into one summary line. +# cot_display = "list" + +# Whether steps are expandable to show input/output details. +# show_step_details = true + # Specify a CSS file that can be used to customize the user interface. # The CSS file can be served from the public directory or via an external link. # custom_css = "/public/test.css" @@ -351,6 +357,8 @@ class UISettings(BaseModel): name: str description: str = "" cot: Literal["hidden", "tool_call", "full"] = "full" + cot_display: Literal["list", "compact"] = "list" + show_step_details: bool = True default_theme: Optional[Literal["light", "dark"]] = "dark" language: Optional[str] = None layout: Optional[Literal["default", "wide"]] = "default" diff --git a/backend/chainlit/translations/ar-SA.json b/backend/chainlit/translations/ar-SA.json index b98b427d23..5708e5f138 100644 --- a/backend/chainlit/translations/ar-SA.json +++ b/backend/chainlit/translations/ar-SA.json @@ -99,7 +99,9 @@ "messages": { "status": { "using": "يستخدم", - "used": "مستخدم" + "used": "مستخدم", + "usedSteps": "استُخدمت {{count}} خطوات", + "usedTools": "استُخدمت {{count}} أدوات" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/bn.json b/backend/chainlit/translations/bn.json index 432d53bea2..7147b68365 100644 --- a/backend/chainlit/translations/bn.json +++ b/backend/chainlit/translations/bn.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "ব্যবহার করছে", - "used": "ব্যবহৃত" + "used": "ব্যবহৃত", + "usedSteps": "{{count}}টি ধাপ ব্যবহার করেছে", + "usedTools": "{{count}}টি টুল ব্যবহার করেছে" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/da-DK.json b/backend/chainlit/translations/da-DK.json index aba214e617..8ee1520f84 100644 --- a/backend/chainlit/translations/da-DK.json +++ b/backend/chainlit/translations/da-DK.json @@ -99,7 +99,9 @@ "messages": { "status": { "using": "Bruger", - "used": "Brugte" + "used": "Brugte", + "usedSteps": "Brugte {{count}} trin", + "usedTools": "Brugte {{count}} værktøjer" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/de-DE.json b/backend/chainlit/translations/de-DE.json index db5934b6f1..5ec10d2a7d 100644 --- a/backend/chainlit/translations/de-DE.json +++ b/backend/chainlit/translations/de-DE.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "Verwendet", - "used": "Verwendete" + "used": "Verwendete", + "usedSteps": "{{count}} Schritte verwendet", + "usedTools": "{{count}} Tools verwendet" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/el-GR.json b/backend/chainlit/translations/el-GR.json index 54e12265f3..ca2b74f8b2 100644 --- a/backend/chainlit/translations/el-GR.json +++ b/backend/chainlit/translations/el-GR.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "Με τη χρήση", - "used": "Χρησιμοποιήθηκε" + "used": "Χρησιμοποιήθηκε", + "usedSteps": "Χρησιμοποίησε {{count}} βήματα", + "usedTools": "Χρησιμοποίησε {{count}} εργαλεία" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/en-US.json b/backend/chainlit/translations/en-US.json index 5a62b3c254..b7286942e1 100644 --- a/backend/chainlit/translations/en-US.json +++ b/backend/chainlit/translations/en-US.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "Using", - "used": "Used" + "used": "Used", + "usedSteps": "Used {{count}} steps", + "usedTools": "Used {{count}} tools" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/es.json b/backend/chainlit/translations/es.json index f47a53f7c6..b7d624a56d 100644 --- a/backend/chainlit/translations/es.json +++ b/backend/chainlit/translations/es.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "Usando", - "used": "Usado" + "used": "Usado", + "usedSteps": "Usó {{count}} pasos", + "usedTools": "Usó {{count}} herramientas" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/fr-FR.json b/backend/chainlit/translations/fr-FR.json index 163be03afd..677bb13695 100644 --- a/backend/chainlit/translations/fr-FR.json +++ b/backend/chainlit/translations/fr-FR.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "Utilise", - "used": "Utilisé" + "used": "Utilisé", + "usedSteps": "{{count}} étapes utilisées", + "usedTools": "{{count}} outils utilisés" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/gu.json b/backend/chainlit/translations/gu.json index 2abb735f99..5f2a909bf3 100644 --- a/backend/chainlit/translations/gu.json +++ b/backend/chainlit/translations/gu.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "વાપરી રહ્યા છે", - "used": "વપરાયેલ" + "used": "વપરાયેલ", + "usedSteps": "{{count}} પગલાં વાપર્યાં", + "usedTools": "{{count}} ટૂલ્સ વાપર્યાં" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/he-IL.json b/backend/chainlit/translations/he-IL.json index b184890121..319f0b54e1 100644 --- a/backend/chainlit/translations/he-IL.json +++ b/backend/chainlit/translations/he-IL.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "משתמש ב", - "used": "השתמש ב" + "used": "השתמש ב", + "usedSteps": "השתמש ב-{{count}} שלבים", + "usedTools": "השתמש ב-{{count}} כלים" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/hi.json b/backend/chainlit/translations/hi.json index 457d70eea6..15db6ad956 100644 --- a/backend/chainlit/translations/hi.json +++ b/backend/chainlit/translations/hi.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "उपयोग कर रहे हैं", - "used": "उपयोग किया" + "used": "उपयोग किया", + "usedSteps": "{{count}} चरणों का उपयोग किया", + "usedTools": "{{count}} टूल्स का उपयोग किया" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/it.json b/backend/chainlit/translations/it.json index 69021fe630..9479ecb47e 100644 --- a/backend/chainlit/translations/it.json +++ b/backend/chainlit/translations/it.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "In uso", - "used": "Utilizzato" + "used": "Utilizzato", + "usedSteps": "Utilizzati {{count}} passaggi", + "usedTools": "Utilizzati {{count}} strumenti" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/ja.json b/backend/chainlit/translations/ja.json index b1ea49659b..9220addeb5 100644 --- a/backend/chainlit/translations/ja.json +++ b/backend/chainlit/translations/ja.json @@ -99,7 +99,9 @@ "messages": { "status": { "using": "使用中", - "used": "使用済み" + "used": "使用済み", + "usedSteps": "{{count}}個のステップを使用", + "usedTools": "{{count}}個のツールを使用" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/kn.json b/backend/chainlit/translations/kn.json index ef59021765..e3e68d738f 100644 --- a/backend/chainlit/translations/kn.json +++ b/backend/chainlit/translations/kn.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "ಬಳಸುತ್ತಿರುವುದು", - "used": "ಬಳಸಲಾಗಿದೆ" + "used": "ಬಳಸಲಾಗಿದೆ", + "usedSteps": "{{count}} ಹಂತಗಳನ್ನು ಬಳಸಲಾಗಿದೆ", + "usedTools": "{{count}} ಸಾಧನಗಳನ್ನು ಬಳಸಲಾಗಿದೆ" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/ko.json b/backend/chainlit/translations/ko.json index f05622faf1..b26f4a0896 100644 --- a/backend/chainlit/translations/ko.json +++ b/backend/chainlit/translations/ko.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "사용 중", - "used": "사용됨" + "used": "사용됨", + "usedSteps": "{{count}}개의 단계 사용됨", + "usedTools": "{{count}}개의 도구 사용됨" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/ml.json b/backend/chainlit/translations/ml.json index 5e01cd402e..d0aeda2755 100644 --- a/backend/chainlit/translations/ml.json +++ b/backend/chainlit/translations/ml.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "ഉപയോഗിക്കുന്നു", - "used": "ഉപയോഗിച്ചു" + "used": "ഉപയോഗിച്ചു", + "usedSteps": "{{count}} ഘട്ടങ്ങൾ ഉപയോഗിച്ചു", + "usedTools": "{{count}} ടൂളുകൾ ഉപയോഗിച്ചു" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/mr.json b/backend/chainlit/translations/mr.json index c559f83cfc..3aebc2c917 100644 --- a/backend/chainlit/translations/mr.json +++ b/backend/chainlit/translations/mr.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "वापरत आहे", - "used": "वापरले" + "used": "वापरले", + "usedSteps": "{{count}} पायऱ्या वापरल्या", + "usedTools": "{{count}} साधने वापरली" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/nl.json b/backend/chainlit/translations/nl.json index b4760191a2..58e443e1d6 100644 --- a/backend/chainlit/translations/nl.json +++ b/backend/chainlit/translations/nl.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "In gebruik", - "used": "Gebruikt" + "used": "Gebruikt", + "usedSteps": "{{count}} stappen gebruikt", + "usedTools": "{{count}} tools gebruikt" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/pt-PT.json b/backend/chainlit/translations/pt-PT.json index a1f1497059..f0b691444d 100644 --- a/backend/chainlit/translations/pt-PT.json +++ b/backend/chainlit/translations/pt-PT.json @@ -12,44 +12,44 @@ "loading": "A carregar...", "error": { "default": "Ocorreu um erro", - "serverConnection": "N\u00e3o foi poss\u00edvel estabelecer liga\u00e7\u00e3o ao servidor" + "serverConnection": "Não foi possível estabelecer ligação ao servidor" } } }, "auth": { "login": { - "title": "Inicie sess\u00e3o para aceder \u00e0 aplica\u00e7\u00e3o", + "title": "Inicie sessão para aceder à aplicação", "form": { "email": { "label": "E-mail", - "required": "o e-mail \u00e9 obrigat\u00f3rio", + "required": "o e-mail é obrigatório", "placeholder": "me@example.com" }, "password": { "label": "Palavra-passe", - "required": "a palavra-passe \u00e9 obrigat\u00f3ria" + "required": "a palavra-passe é obrigatória" }, "actions": { - "signin": "Iniciar sess\u00e3o" + "signin": "Iniciar sessão" }, "alternativeText": { "or": "Ou" } }, "errors": { - "default": "N\u00e3o foi poss\u00edvel iniciar sess\u00e3o", - "signin": "Tente iniciar sess\u00e3o com outra conta", - "oauthSignin": "Tente iniciar sess\u00e3o com outra conta", - "redirectUriMismatch": "O URI de redirecionamento n\u00e3o corresponde \u00e0 configura\u00e7\u00e3o da aplica\u00e7\u00e3o OAuth", - "oauthCallback": "Tente iniciar sess\u00e3o com outra conta", - "oauthCreateAccount": "Tente iniciar sess\u00e3o com outra conta", - "emailCreateAccount": "Tente iniciar sess\u00e3o com outra conta", - "callback": "Tente iniciar sess\u00e3o com outra conta", - "oauthAccountNotLinked": "Para confirmar a sua identidade, inicie sess\u00e3o com a mesma conta utilizada anteriormente", - "emailSignin": "N\u00e3o foi poss\u00edvel enviar o e-mail", + "default": "Não foi possível iniciar sessão", + "signin": "Tente iniciar sessão com outra conta", + "oauthSignin": "Tente iniciar sessão com outra conta", + "redirectUriMismatch": "O URI de redirecionamento não corresponde à configuração da aplicação OAuth", + "oauthCallback": "Tente iniciar sessão com outra conta", + "oauthCreateAccount": "Tente iniciar sessão com outra conta", + "emailCreateAccount": "Tente iniciar sessão com outra conta", + "callback": "Tente iniciar sessão com outra conta", + "oauthAccountNotLinked": "Para confirmar a sua identidade, inicie sessão com a mesma conta utilizada anteriormente", + "emailSignin": "Não foi possível enviar o e-mail", "emailVerify": "Por favor, verifique o seu e-mail. Foi enviada uma nova mensagem", - "credentialsSignin": "Erro ao iniciar sess\u00e3o. Verifique se os dados fornecidos est\u00e3o corretos", - "sessionRequired": "Por favor, inicie sess\u00e3o para aceder a esta p\u00e1gina" + "credentialsSignin": "Erro ao iniciar sessão. Verifique se os dados fornecidos estão corretos", + "sessionRequired": "Por favor, inicie sessão para aceder a esta página" } }, "provider": { @@ -70,18 +70,18 @@ "headline": "Mensagens favoritas", "remove": "Remover favorito", "empty": { - "title": "Ainda n\u00e3o h\u00e1 prompts guardados", - "description": "Comece por enviar um prompt e marc\u00e1-lo com estrela, ou marque com estrela um prompt de conversas anteriores" + "title": "Ainda não há prompts guardados", + "description": "Comece por enviar um prompt e marcá-lo com estrela, ou marque com estrela um prompt de conversas anteriores" } }, "commands": { "button": "Ferramentas", "changeTool": "Alterar ferramenta", - "availableTools": "Ferramentas dispon\u00edveis" + "availableTools": "Ferramentas disponíveis" }, "speech": { - "start": "Iniciar grava\u00e7\u00e3o", - "stop": "Parar grava\u00e7\u00e3o", + "start": "Iniciar gravação", + "stop": "Parar gravação", "connecting": "A ligar" }, "fileUpload": { @@ -100,39 +100,41 @@ "messages": { "status": { "using": "A utilizar", - "used": "Utilizado" + "used": "Utilizado", + "usedSteps": "{{count}} passos utilizados", + "usedTools": "{{count}} ferramentas utilizadas" }, "actions": { "copy": { - "button": "Copiar para a \u00e1rea de transfer\u00eancia", + "button": "Copiar para a área de transferência", "success": "Copiado!" } }, "feedback": { - "positive": "\u00datil", - "negative": "N\u00e3o \u00fatil", - "edit": "Editar coment\u00e1rio", + "positive": "Útil", + "negative": "Não útil", + "edit": "Editar comentário", "dialog": { - "title": "Adicionar um coment\u00e1rio", - "submit": "Enviar coment\u00e1rio", - "yourFeedback": "O seu coment\u00e1rio..." + "title": "Adicionar um comentário", + "submit": "Enviar comentário", + "yourFeedback": "O seu comentário..." }, "status": { "updating": "A atualizar", - "updated": "Coment\u00e1rio atualizado" + "updated": "Comentário atualizado" } } }, "history": { - "title": "\u00daltimas entradas", - "empty": "Est\u00e1 vazio...", - "show": "Mostrar hist\u00f3rico" + "title": "Últimas entradas", + "empty": "Está vazio...", + "show": "Mostrar histórico" }, "settings": { - "title": "Painel de configura\u00e7\u00f5es", - "customize": "Personalize aqui as configura\u00e7\u00f5es do seu chat" + "title": "Painel de configurações", + "customize": "Personalize aqui as configurações do seu chat" }, - "watermark": "Os modelos de linguagem podem cometer erros. Verifique sempre informa\u00e7\u00f5es importantes." + "watermark": "Os modelos de linguagem podem cometer erros. Verifique sempre informações importantes." }, "threadHistory": { "sidebar": { @@ -144,8 +146,8 @@ "timeframes": { "today": "Hoje", "yesterday": "Ontem", - "previous7days": "\u00daltimos 7 dias", - "previous30days": "\u00daltimos 30 dias" + "previous7days": "Últimos 7 dias", + "previous30days": "Últimos 30 dias" }, "empty": "Nenhuma conversa encontrada", "actions": { @@ -154,7 +156,7 @@ } }, "thread": { - "untitled": "Conversa sem t\u00edtulo", + "untitled": "Conversa sem título", "menu": { "rename": "Renomear", "share": "Partilhar", @@ -162,21 +164,21 @@ }, "actions": { "share": { - "title": "Partilhar liga\u00e7\u00e3o do chat", + "title": "Partilhar ligação do chat", "button": "Partilhar", "status": { - "copied": "Liga\u00e7\u00e3o copiada", - "created": "Liga\u00e7\u00e3o de partilha criada!", + "copied": "Ligação copiada", + "created": "Ligação de partilha criada!", "unshared": "Partilha desativada para esta conversa" }, "error": { - "create": "Erro ao criar liga\u00e7\u00e3o de partilha", + "create": "Erro ao criar ligação de partilha", "unshare": "Erro ao desativar a partilha" } }, "delete": { - "title": "Confirmar elimina\u00e7\u00e3o", - "description": "Ir\u00e1 eliminar a conversa e todos os seus conte\u00fados. Esta a\u00e7\u00e3o n\u00e3o pode ser anulada.", + "title": "Confirmar eliminação", + "description": "Irá eliminar a conversa e todos os seus conteúdos. Esta ação não pode ser anulada.", "success": "Chat eliminado", "inProgress": "A eliminar chat" }, @@ -209,42 +211,42 @@ "button": "Novo chat", "dialog": { "title": "Criar novo chat", - "description": "Isto ir\u00e1 apagar o hist\u00f3rico de chat atual. Tem a certeza de que pretende continuar?", + "description": "Isto irá apagar o histórico de chat atual. Tem a certeza de que pretende continuar?", "tooltip": "Novo chat" } }, "user": { "menu": { - "settings": "Configura\u00e7\u00f5es", + "settings": "Configurações", "settingsKey": "S", "apiKeys": "Chaves API", - "logout": "Terminar sess\u00e3o" + "logout": "Terminar sessão" } } }, "apiKeys": { - "title": "Chaves API necess\u00e1rias", - "description": "Para utilizar esta aplica\u00e7\u00e3o, s\u00e3o necess\u00e1rias as seguintes chaves API. As chaves s\u00e3o guardadas localmente no seu dispositivo.", + "title": "Chaves API necessárias", + "description": "Para utilizar esta aplicação, são necessárias as seguintes chaves API. As chaves são guardadas localmente no seu dispositivo.", "success": { "saved": "Guardado com sucesso" } }, "alerts": { - "info": "Informa\u00e7\u00e3o", + "info": "Informação", "note": "Nota", "tip": "Dica", "important": "Importante", "warning": "Aviso", "caution": "Cuidado", - "debug": "Depura\u00e7\u00e3o", + "debug": "Depuração", "example": "Exemplo", "success": "Sucesso", "help": "Ajuda", "idea": "Ideia", "pending": "Pendente", - "security": "Seguran\u00e7a", + "security": "Segurança", "beta": "Beta", - "best-practice": "Boa pr\u00e1tica" + "best-practice": "Boa prática" }, "components": { "MultiSelectInput": { diff --git a/backend/chainlit/translations/ta.json b/backend/chainlit/translations/ta.json index be051916f7..abc2ee38d7 100644 --- a/backend/chainlit/translations/ta.json +++ b/backend/chainlit/translations/ta.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "பயன்படுத்துகிறது", - "used": "பயன்படுத்தப்பட்டது" + "used": "பயன்படுத்தப்பட்டது", + "usedSteps": "{{count}} படிகள் பயன்படுத்தப்பட்டன", + "usedTools": "{{count}} கருவிகள் பயன்படுத்தப்பட்டன" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/te.json b/backend/chainlit/translations/te.json index 5849a3a1a4..d00c3a617e 100644 --- a/backend/chainlit/translations/te.json +++ b/backend/chainlit/translations/te.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "ఉపయోగిస్తోంది", - "used": "ఉపయోగించబడింది" + "used": "ఉపయోగించబడింది", + "usedSteps": "{{count}} దశలు ఉపయోగించబడ్డాయి", + "usedTools": "{{count}} సాధనాలు ఉపయోగించబడ్డాయి" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/zh-CN.json b/backend/chainlit/translations/zh-CN.json index bce16c1149..5e4f9adeb1 100644 --- a/backend/chainlit/translations/zh-CN.json +++ b/backend/chainlit/translations/zh-CN.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "使用中", - "used": "已使用" + "used": "已使用", + "usedSteps": "使用了 {{count}} 个步骤", + "usedTools": "使用了 {{count}} 个工具" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/zh-TW.json b/backend/chainlit/translations/zh-TW.json index 4c475b61d1..e437a546a5 100644 --- a/backend/chainlit/translations/zh-TW.json +++ b/backend/chainlit/translations/zh-TW.json @@ -100,7 +100,9 @@ "messages": { "status": { "using": "正在使用", - "used": "已使用" + "used": "已使用", + "usedSteps": "使用了 {{count}} 個步驟", + "usedTools": "使用了 {{count}} 個工具" }, "actions": { "copy": { diff --git a/frontend/src/components/ReadOnlyThread.tsx b/frontend/src/components/ReadOnlyThread.tsx index 4036b4ed8c..aae3c1be69 100644 --- a/frontend/src/components/ReadOnlyThread.tsx +++ b/frontend/src/components/ReadOnlyThread.tsx @@ -163,6 +163,8 @@ const ReadOnlyThread = ({ id }: Props) => { showFeedbackButtons: !!config?.dataPersistence, uiName: config?.ui?.name || '', cot: config?.ui?.cot || 'hidden', + cotDisplay: config?.ui?.cot_display || 'list', + showStepDetails: config?.ui?.show_step_details ?? true, onElementRefClick, onError, onFeedbackUpdated, @@ -171,6 +173,8 @@ const ReadOnlyThread = ({ id }: Props) => { }, [ config?.ui?.name, config?.ui?.cot, + config?.ui?.cot_display, + config?.ui?.show_step_details, config?.features?.unsafe_allow_html, config?.features?.user_message_markdown, onElementRefClick, diff --git a/frontend/src/components/chat/Messages/CompactSteps.tsx b/frontend/src/components/chat/Messages/CompactSteps.tsx new file mode 100644 index 0000000000..ff90bc5f03 --- /dev/null +++ b/frontend/src/components/chat/Messages/CompactSteps.tsx @@ -0,0 +1,117 @@ +import { cn } from '@/lib/utils'; +import { MessageContext } from 'contexts/MessageContext'; +import { memo, useContext, useMemo, useState } from 'react'; + +import type { IAction, IMessageElement, IStep } from '@chainlit/react-client'; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger +} from '@/components/ui/accordion'; +import { Translator } from 'components/i18n'; + +import { Messages } from '.'; +import { MessageAvatar } from './Message/Avatar'; + +interface Props { + steps: IStep[]; + elements: IMessageElement[]; + actions: IAction[]; + indent: number; + isRunning?: boolean; + scorableRun?: IStep; +} + +const CompactSteps = memo( + ({ steps, elements, actions, indent, isRunning, scorableRun }: Props) => { + const { cot } = useContext(MessageContext); + const [openValue, setOpenValue] = useState(''); + + // Filter steps based on cot mode (same logic as Message/index.tsx) + const visibleSteps = useMemo(() => { + return steps.filter((s) => { + const isStep = !s.type.includes('message'); + if (!isStep) return false; + if (cot === 'hidden') return false; + if (cot === 'tool_call' && s.type !== 'tool') return false; + return true; + }); + }, [steps, cot]); + + // Show "Using" label while the run is still active (prevents flashing + // the finished label between sequential tool calls) + const showUsing = !!isRunning; + + // Get the last visible step name for the "Using X" label + const lastStep = visibleSteps[visibleSteps.length - 1]; + const lastStepName = lastStep?.name || ''; + + // Determine the label translation key for the finished state + const usedLabelPath = + cot === 'tool_call' + ? 'chat.messages.status.usedTools' + : 'chat.messages.status.usedSteps'; + + const accordionId = 'compact-steps'; + + return ( +
+
+
+ +
+ setOpenValue(val)} + className="w-full" + > + + + {showUsing ? ( + <> + {' '} + {lastStepName} + + ) : ( + + )} + + +
+ +
+
+
+
+
+
+
+
+ ); + } +); + +CompactSteps.displayName = 'CompactSteps'; + +export { CompactSteps }; diff --git a/frontend/src/components/chat/Messages/Message/Step.tsx b/frontend/src/components/chat/Messages/Message/Step.tsx index 96432437bc..7504a0feea 100644 --- a/frontend/src/components/chat/Messages/Message/Step.tsx +++ b/frontend/src/components/chat/Messages/Message/Step.tsx @@ -1,5 +1,12 @@ import { cn } from '@/lib/utils'; -import { PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { MessageContext } from 'contexts/MessageContext'; +import { + PropsWithChildren, + useContext, + useEffect, + useMemo, + useState +} from 'react'; import type { IStep } from '@chainlit/react-client'; @@ -21,6 +28,8 @@ export default function Step({ children, isRunning }: PropsWithChildren) { + const { showStepDetails } = useContext(MessageContext); + const using = useMemo(() => { return isRunning && step.start && !step.end && !step.isError; }, [step, isRunning]); @@ -40,8 +49,8 @@ export default function Step({ } }, [using, step.autoCollapse]); - // If there's no content, just render the status without accordion - if (!hasContent) { + // If there's no content or step details are disabled, just render the status without accordion + if (!hasContent || !showStepDetails) { return (

{ + const isStep = !s.type.includes('message'); + if (!isStep) return false; + if (messageContext.cot === 'tool_call' && s.type !== 'tool') + return false; + return true; + }).length ?? 0) + : 0; + + const showCompact = useCompact && visibleStepCount > 1; + return ( {m.steps?.length ? ( - + showCompact ? ( + <> + + {/* Render message-type children at root level */} + + s.type.includes('message') + )} + elements={elements} + actions={actions} + indent={indent} + isRunning={isRunning} + scorableRun={scorableRun} + /> + + ) : ( + + ) ) : null} {(showToolCoTLoader || showHiddenCoTLoader) && m.name !== 'on_chat_start' ? ( diff --git a/frontend/src/components/chat/MessagesContainer/index.tsx b/frontend/src/components/chat/MessagesContainer/index.tsx index 18899fec92..f89835560e 100644 --- a/frontend/src/components/chat/MessagesContainer/index.tsx +++ b/frontend/src/components/chat/MessagesContainer/index.tsx @@ -164,6 +164,8 @@ const MessagesContainer = ({ navigate }: Props) => { showFeedbackButtons: enableFeedback, uiName: config?.ui?.name || '', cot: config?.ui?.cot || 'hidden', + cotDisplay: config?.ui?.cot_display || 'list', + showStepDetails: config?.ui?.show_step_details ?? true, onElementRefClick, onError, onFeedbackUpdated, @@ -175,6 +177,8 @@ const MessagesContainer = ({ navigate }: Props) => { loading, config?.ui?.name, config?.ui?.cot, + config?.ui?.cot_display, + config?.ui?.show_step_details, config?.features?.unsafe_allow_html, config?.features?.user_message_markdown, onElementRefClick, diff --git a/frontend/src/contexts/MessageContext.tsx b/frontend/src/contexts/MessageContext.tsx index 45735b61f2..e0350db23c 100644 --- a/frontend/src/contexts/MessageContext.tsx +++ b/frontend/src/contexts/MessageContext.tsx @@ -11,7 +11,9 @@ const defaultMessageContext = { showFeedbackButtons: true, onError: () => undefined, uiName: '', - cot: 'hidden' as const + cot: 'hidden' as const, + cotDisplay: 'list' as const, + showStepDetails: true }; const MessageContext = createContext(defaultMessageContext); diff --git a/frontend/src/types/messageContext.ts b/frontend/src/types/messageContext.ts index 87b79f1800..8b81f6c394 100644 --- a/frontend/src/types/messageContext.ts +++ b/frontend/src/types/messageContext.ts @@ -12,6 +12,8 @@ interface IMessageContext { onProgress: (progress: number) => void ) => { xhr: XMLHttpRequest; promise: Promise }; cot: 'hidden' | 'tool_call' | 'full'; + cotDisplay: 'list' | 'compact'; + showStepDetails: boolean; askUser?: IAsk; editable: boolean; loading: boolean; diff --git a/libs/react-client/src/types/config.ts b/libs/react-client/src/types/config.ts index 52a28cc7a3..ed3d2a2a94 100644 --- a/libs/react-client/src/types/config.ts +++ b/libs/react-client/src/types/config.ts @@ -46,6 +46,8 @@ export interface IChainlitConfig { default_chat_settings_open?: boolean; confirm_new_chat?: boolean; cot: 'hidden' | 'tool_call' | 'full'; + cot_display?: 'list' | 'compact'; + show_step_details?: boolean; github?: string; custom_css?: string; custom_js?: string; From bce8e1d08bd96a662d4c895379ba1367b5336879 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Wed, 10 Jun 2026 20:01:00 +0300 Subject: [PATCH 02/13] fix(ui): count steps recursively and fix compact label timing - Add countVisibleSteps helper that walks the full step subtree, fixing compact mode not activating when tools are nested under intermediate agent/run steps (e.g. LangGraph, LangChain). - In CompactSteps, use collectVisible for recursive count/naming and pass all step-type children to so existing skip logic drills through intermediates to reach tools. - Switch showUsing signal from isRunning to !hasAnswer (presence of assistant_message in subtree), preventing both the flash between sequential tools and the stale "Using" label during answer streaming. --- .../components/chat/Messages/CompactSteps.tsx | 45 ++++++++++++++----- .../src/components/chat/Messages/index.tsx | 19 +++++--- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/chat/Messages/CompactSteps.tsx b/frontend/src/components/chat/Messages/CompactSteps.tsx index ff90bc5f03..4a5621b22f 100644 --- a/frontend/src/components/chat/Messages/CompactSteps.tsx +++ b/frontend/src/components/chat/Messages/CompactSteps.tsx @@ -24,25 +24,46 @@ interface Props { scorableRun?: IStep; } +// Recursively collect tool/step-type nodes for counting and naming +const collectVisible = (steps: IStep[], cot: string): IStep[] => { + const result: IStep[] = []; + for (const s of steps) { + if (!s.type.includes('message')) { + if (cot !== 'tool_call' || s.type === 'tool') result.push(s); + } + if (s.steps) result.push(...collectVisible(s.steps, cot)); + } + return result; +}; + const CompactSteps = memo( ({ steps, elements, actions, indent, isRunning, scorableRun }: Props) => { const { cot } = useContext(MessageContext); const [openValue, setOpenValue] = useState(''); - // Filter steps based on cot mode (same logic as Message/index.tsx) + // All non-message step children for rendering (Message skip logic drills through intermediates) + const stepChildren = useMemo(() => { + return steps.filter((s) => !s.type.includes('message')); + }, [steps]); + + // Recursively collected visible steps for count and label const visibleSteps = useMemo(() => { - return steps.filter((s) => { - const isStep = !s.type.includes('message'); - if (!isStep) return false; - if (cot === 'hidden') return false; - if (cot === 'tool_call' && s.type !== 'tool') return false; - return true; - }); + return collectVisible(steps, cot); }, [steps, cot]); - // Show "Using" label while the run is still active (prevents flashing - // the finished label between sequential tool calls) - const showUsing = !!isRunning; + // Show "Using" until an assistant_message appears (meaning tools are done + // and the model is streaming). This avoids flashing between sequential tools + // (no assistant_message yet) while still switching once the answer begins. + const hasAnswer = useMemo(() => { + const check = (items: IStep[]): boolean => + items.some( + (s) => + s.type === 'assistant_message' || (s.steps ? check(s.steps) : false) + ); + return check(steps); + }, [steps]); + + const showUsing = !!isRunning && !hasAnswer; // Get the last visible step name for the "Using X" label const lastStep = visibleSteps[visibleSteps.length - 1]; @@ -93,7 +114,7 @@ const CompactSteps = memo(

{ ); }; +const countVisibleSteps = (steps: IStep[], cot: string): number => { + let count = 0; + for (const s of steps) { + if (!s.type.includes('message')) { + if (cot !== 'tool_call' || s.type === 'tool') count++; + } + if (s.steps) count += countVisibleSteps(s.steps, cot); + } + return count; +}; + const Messages = memo( ({ messages, elements, actions, indent, isRunning, scorableRun }: Props) => { const messageContext = useContext(MessageContext); @@ -85,13 +96,7 @@ const Messages = memo( // Only use compact when there are 2+ steps worth grouping const visibleStepCount = useCompact - ? (m.steps?.filter((s) => { - const isStep = !s.type.includes('message'); - if (!isStep) return false; - if (messageContext.cot === 'tool_call' && s.type !== 'tool') - return false; - return true; - }).length ?? 0) + ? countVisibleSteps(m.steps || [], messageContext.cot) : 0; const showCompact = useCompact && visibleStepCount > 1; From b3983ed72a07712832dc6c5952bfe11674eb1d81 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Wed, 10 Jun 2026 23:58:35 +0300 Subject: [PATCH 03/13] feat(translations): update Dutch translation for 'usedTools' to 'hulpmiddelen' feat(ReadOnlyThread): add support for latex and dataPersistence features --- backend/chainlit/translations/nl.json | 2 +- frontend/src/components/ReadOnlyThread.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/chainlit/translations/nl.json b/backend/chainlit/translations/nl.json index 58e443e1d6..424f2b4434 100644 --- a/backend/chainlit/translations/nl.json +++ b/backend/chainlit/translations/nl.json @@ -102,7 +102,7 @@ "using": "In gebruik", "used": "Gebruikt", "usedSteps": "{{count}} stappen gebruikt", - "usedTools": "{{count}} tools gebruikt" + "usedTools": "{{count}} hulpmiddelen gebruikt" }, "actions": { "copy": { diff --git a/frontend/src/components/ReadOnlyThread.tsx b/frontend/src/components/ReadOnlyThread.tsx index aae3c1be69..b8b70b70f4 100644 --- a/frontend/src/components/ReadOnlyThread.tsx +++ b/frontend/src/components/ReadOnlyThread.tsx @@ -176,7 +176,9 @@ const ReadOnlyThread = ({ id }: Props) => { config?.ui?.cot_display, config?.ui?.show_step_details, config?.features?.unsafe_allow_html, + config?.features?.latex, config?.features?.user_message_markdown, + config?.dataPersistence, onElementRefClick, onError, onFeedbackUpdated, From ea07e4eca642d6f07a4d35ee9d4ba58fa7681e1e Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Tue, 30 Jun 2026 13:09:07 +0300 Subject: [PATCH 04/13] chore: bump to reset stale bot From 0ac90e064f7ea7b6508c1c13799203a904012640 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Tue, 28 Jul 2026 23:11:19 +0300 Subject: [PATCH 05/13] chore: bump to reset stale bot Co-Authored-By: GitHub Copilot From facaed3c4065ee1721ae287943e901a6ebbc55e5 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Wed, 29 Jul 2026 21:47:51 +0300 Subject: [PATCH 06/13] fix(ui): add margin to step and accordion trigger elements for better spacing --- frontend/src/components/chat/Messages/CompactSteps.tsx | 2 +- frontend/src/components/chat/Messages/Message/Step.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/chat/Messages/CompactSteps.tsx b/frontend/src/components/chat/Messages/CompactSteps.tsx index 4a5621b22f..03ebe6fd25 100644 --- a/frontend/src/components/chat/Messages/CompactSteps.tsx +++ b/frontend/src/components/chat/Messages/CompactSteps.tsx @@ -93,7 +93,7 @@ const CompactSteps = memo(

Date: Wed, 29 Jul 2026 23:34:03 +0300 Subject: [PATCH 07/13] test(e2e): add compact CoT display E2E tests Add Cypress coverage for the compact chain-of-thought display: - collapses multiple steps into a single summary and expands on click - leaves a single step uncollapsed - counts nested steps recursively to trigger compact mode Adds data-testid hooks to CompactSteps for stable selection. Co-Authored-By: GitHub Copilot --- .../e2e/compact_steps/.chainlit/config.toml | 14 +++++ cypress/e2e/compact_steps/main.py | 44 +++++++++++++++ cypress/e2e/compact_steps/spec.cy.ts | 54 +++++++++++++++++++ .../components/chat/Messages/CompactSteps.tsx | 3 +- 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/compact_steps/.chainlit/config.toml create mode 100644 cypress/e2e/compact_steps/main.py create mode 100644 cypress/e2e/compact_steps/spec.cy.ts diff --git a/cypress/e2e/compact_steps/.chainlit/config.toml b/cypress/e2e/compact_steps/.chainlit/config.toml new file mode 100644 index 0000000000..e4937335e4 --- /dev/null +++ b/cypress/e2e/compact_steps/.chainlit/config.toml @@ -0,0 +1,14 @@ +[project] +allow_origins = ["*"] + +[UI] +name = "Assistant" + +# Show every step of the chain of thought. +cot = "full" + +# Collapse the chain of thought steps into a single compact summary line. +cot_display = "compact" + +[meta] +generated_by = "2.11.1" diff --git a/cypress/e2e/compact_steps/main.py b/cypress/e2e/compact_steps/main.py new file mode 100644 index 0000000000..887f72216c --- /dev/null +++ b/cypress/e2e/compact_steps/main.py @@ -0,0 +1,44 @@ +import chainlit as cl + + +@cl.step(type="tool", name="tool1") +async def tool1(): + return "Result from tool1" + + +@cl.step(type="tool", name="tool2") +async def tool2(): + return "Result from tool2" + + +@cl.step(type="tool", name="tool3") +async def tool3(): + return "Result from tool3" + + +@cl.step(type="tool", name="agent") +async def agent(): + # Two tools nested under an intermediate step. Compact mode must count the + # nested steps recursively (not just top-level ones) to activate. + await tool1() + await tool2() + return "Result from agent" + + +@cl.on_message +async def main(message: cl.Message): + content = message.content.strip().lower() + + if content == "single": + # A single visible step must not be collapsed into a compact summary. + await tool1() + elif content == "nested": + # Only one top-level step, but it nests two tools -> 3 visible steps. + await agent() + else: + # Three sequential tools -> collapsed into one compact summary. + await tool1() + await tool2() + await tool3() + + await cl.Message(content="Final answer").send() diff --git a/cypress/e2e/compact_steps/spec.cy.ts b/cypress/e2e/compact_steps/spec.cy.ts new file mode 100644 index 0000000000..818123a2e0 --- /dev/null +++ b/cypress/e2e/compact_steps/spec.cy.ts @@ -0,0 +1,54 @@ +import { submitMessage } from '../../support/testUtils'; + +describe('Compact CoT display', () => { + it('collapses multiple steps into a single compact summary', () => { + cy.visit('/'); + submitMessage('multi'); + + // The three tool steps are grouped into a single compact summary line. + cy.get('[data-testid="compact-steps"]').should('have.length', 1); + cy.get('[data-testid="compact-steps-trigger"]').should( + 'contain', + 'Used 3 steps' + ); + + // The final answer renders at the root, outside the collapsed summary. + cy.contains('Final answer').should('be.visible'); + + // Individual steps stay hidden until the summary is expanded. + cy.get('#step-tool1').should('not.exist'); + + // Expanding the summary reveals the individual steps. + cy.get('[data-testid="compact-steps-trigger"]').click(); + cy.get('#step-tool1').should('be.visible'); + cy.get('#step-tool2').should('be.visible'); + cy.get('#step-tool3').should('be.visible'); + }); + + it('does not collapse a single step', () => { + cy.visit('/'); + submitMessage('single'); + + // A lone step is rendered directly, without a compact summary. + cy.get('#step-tool1').should('be.visible'); + cy.get('[data-testid="compact-steps"]').should('not.exist'); + cy.contains('Final answer').should('be.visible'); + }); + + it('counts nested steps recursively to trigger compact mode', () => { + cy.visit('/'); + submitMessage('nested'); + + // Only one top-level step, but it nests two tools, so the recursive count + // reaches three and the compact summary is used. + cy.get('[data-testid="compact-steps"]').should('have.length', 1); + cy.get('[data-testid="compact-steps-trigger"]').should( + 'contain', + 'Used 3 steps' + ); + + // Expanding the summary reveals the intermediate step. + cy.get('[data-testid="compact-steps-trigger"]').click(); + cy.get('#step-agent').should('be.visible'); + }); +}); diff --git a/frontend/src/components/chat/Messages/CompactSteps.tsx b/frontend/src/components/chat/Messages/CompactSteps.tsx index 03ebe6fd25..67f17004f7 100644 --- a/frontend/src/components/chat/Messages/CompactSteps.tsx +++ b/frontend/src/components/chat/Messages/CompactSteps.tsx @@ -78,7 +78,7 @@ const CompactSteps = memo( const accordionId = 'compact-steps'; return ( -

+
@@ -92,6 +92,7 @@ const CompactSteps = memo( > Date: Thu, 20 Aug 2026 22:33:09 +0300 Subject: [PATCH 08/13] fix(ui): surface nested messages and keep step hierarchy in compact/flat CoT Two behavioral asymmetries reported on the compact CoT display: - Compact mode only lifted direct message children of a run to the root, so an assistant message emitted inside a nested step stayed trapped in the collapsed summary. Collect messages recursively and strip them from the accordion body. - show_step_details=false dropped a step's whole subtree. Now it only hides the input/output payload: steps with sub-steps stay collapsible, leaves render as flat labels, preserving the (collapsible) hierarchy. Adds E2E coverage: a nested-message case in compact_steps and a new step_details spec. Co-Authored-By: GitHub Copilot --- cypress/e2e/compact_steps/main.py | 13 +++++++++ cypress/e2e/compact_steps/spec.cy.ts | 21 ++++++++++++++ .../e2e/step_details/.chainlit/config.toml | 12 ++++++++ cypress/e2e/step_details/main.py | 24 ++++++++++++++++ cypress/e2e/step_details/spec.cy.ts | 28 +++++++++++++++++++ .../components/chat/Messages/CompactSteps.tsx | 14 +++++++--- .../components/chat/Messages/Message/Step.tsx | 16 +++++++---- .../chat/Messages/Message/index.tsx | 28 +++++++++++-------- .../src/components/chat/Messages/index.tsx | 17 ++++++++--- 9 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 cypress/e2e/step_details/.chainlit/config.toml create mode 100644 cypress/e2e/step_details/main.py create mode 100644 cypress/e2e/step_details/spec.cy.ts diff --git a/cypress/e2e/compact_steps/main.py b/cypress/e2e/compact_steps/main.py index 887f72216c..a73b019721 100644 --- a/cypress/e2e/compact_steps/main.py +++ b/cypress/e2e/compact_steps/main.py @@ -25,6 +25,14 @@ async def agent(): return "Result from agent" +@cl.step(type="tool", name="answering-agent") +async def answering_agent(): + await tool1() + await tool2() + # Assistant message emitted from inside a nested step. + await cl.Message(content="Nested answer").send() + + @cl.on_message async def main(message: cl.Message): content = message.content.strip().lower() @@ -35,6 +43,11 @@ async def main(message: cl.Message): elif content == "nested": # Only one top-level step, but it nests two tools -> 3 visible steps. await agent() + elif content == "nested_message": + # An assistant message produced inside a nested step must still render + # at the root, not be trapped inside the collapsed compact summary. + await answering_agent() + return else: # Three sequential tools -> collapsed into one compact summary. await tool1() diff --git a/cypress/e2e/compact_steps/spec.cy.ts b/cypress/e2e/compact_steps/spec.cy.ts index 818123a2e0..b0b73d8b26 100644 --- a/cypress/e2e/compact_steps/spec.cy.ts +++ b/cypress/e2e/compact_steps/spec.cy.ts @@ -51,4 +51,25 @@ describe('Compact CoT display', () => { cy.get('[data-testid="compact-steps-trigger"]').click(); cy.get('#step-agent').should('be.visible'); }); + + it('keeps a nested assistant message visible outside the collapsed summary', () => { + cy.visit('/'); + submitMessage('nested_message'); + + // The steps collapse into a summary that stays closed. + cy.get('[data-testid="compact-steps"]').should('have.length', 1); + cy.get('[data-testid="compact-steps-trigger"]').should( + 'have.attr', + 'data-state', + 'closed' + ); + + // The assistant message produced inside a nested step is lifted to the root: + // visible without expanding, and not contained in the compact summary. + cy.contains('Nested answer').should('be.visible'); + cy.get('[data-testid="compact-steps"]').should( + 'not.contain', + 'Nested answer' + ); + }); }); diff --git a/cypress/e2e/step_details/.chainlit/config.toml b/cypress/e2e/step_details/.chainlit/config.toml new file mode 100644 index 0000000000..23aec4e8e2 --- /dev/null +++ b/cypress/e2e/step_details/.chainlit/config.toml @@ -0,0 +1,12 @@ +[project] +allow_origins = ["*"] + +[UI] +name = "Assistant" +cot = "full" + +# Hide input/output detail; the nested step hierarchy must still be visible. +show_step_details = false + +[meta] +generated_by = "2.11.1" diff --git a/cypress/e2e/step_details/main.py b/cypress/e2e/step_details/main.py new file mode 100644 index 0000000000..82d7d2831a --- /dev/null +++ b/cypress/e2e/step_details/main.py @@ -0,0 +1,24 @@ +import chainlit as cl + + +@cl.step(type="tool", name="leaf") +async def leaf(): + return "Leaf result" + + +@cl.step(type="tool", name="branch") +async def branch(): + await leaf() + return "Branch result" + + +@cl.step(type="tool", name="root") +async def root(): + await branch() + return "Root result" + + +@cl.on_message +async def main(message: cl.Message): + await root() + await cl.Message(content="Final answer").send() diff --git a/cypress/e2e/step_details/spec.cy.ts b/cypress/e2e/step_details/spec.cy.ts new file mode 100644 index 0000000000..cbe19ecf59 --- /dev/null +++ b/cypress/e2e/step_details/spec.cy.ts @@ -0,0 +1,28 @@ +import { submitMessage } from '../../support/testUtils'; + +describe('Step details disabled', () => { + it('keeps a collapsible step hierarchy but hides input/output detail', () => { + cy.visit('/'); + submitMessage('go'); + + // Root is a collapsible accordion, collapsed by default. + cy.get('#step-root') + .should('be.visible') + .and('have.attr', 'data-state', 'closed'); + + // Nested steps stay hidden until expanded (hierarchy preserved, not forced open). + cy.get('#step-branch').should('not.exist'); + + // Drilling down reveals the hierarchy one level at a time. + cy.get('#step-root').click(); + cy.get('#step-branch').should('be.visible'); + cy.get('#step-branch').click(); + cy.get('#step-leaf').should('be.visible'); + + // Input/output payloads are hidden: the step return values never render. + cy.contains('Root result').should('not.exist'); + cy.contains('Branch result').should('not.exist'); + + cy.contains('Final answer').should('be.visible'); + }); +}); diff --git a/frontend/src/components/chat/Messages/CompactSteps.tsx b/frontend/src/components/chat/Messages/CompactSteps.tsx index 67f17004f7..3aa7f091b3 100644 --- a/frontend/src/components/chat/Messages/CompactSteps.tsx +++ b/frontend/src/components/chat/Messages/CompactSteps.tsx @@ -36,15 +36,21 @@ const collectVisible = (steps: IStep[], cot: string): IStep[] => { return result; }; +// Assistant messages are lifted to the root by the parent, so drop them at +// every depth to avoid trapping them inside the collapsed accordion. +const stripMessages = (steps: IStep[]): IStep[] => + steps + .filter((s) => !s.type.includes('message')) + .map((s) => (s.steps ? { ...s, steps: stripMessages(s.steps) } : s)); + const CompactSteps = memo( ({ steps, elements, actions, indent, isRunning, scorableRun }: Props) => { const { cot } = useContext(MessageContext); const [openValue, setOpenValue] = useState(''); - // All non-message step children for rendering (Message skip logic drills through intermediates) - const stepChildren = useMemo(() => { - return steps.filter((s) => !s.type.includes('message')); - }, [steps]); + // Steps to render inside the accordion, with assistant messages stripped + // at every depth (they render at the root instead). + const stepChildren = useMemo(() => stripMessages(steps), [steps]); // Recursively collected visible steps for count and label const visibleSteps = useMemo(() => { diff --git a/frontend/src/components/chat/Messages/Message/Step.tsx b/frontend/src/components/chat/Messages/Message/Step.tsx index 56994c5b02..a687565f8d 100644 --- a/frontend/src/components/chat/Messages/Message/Step.tsx +++ b/frontend/src/components/chat/Messages/Message/Step.tsx @@ -2,6 +2,7 @@ import { cn } from '@/lib/utils'; import { MessageContext } from 'contexts/MessageContext'; import { PropsWithChildren, + ReactNode, useContext, useEffect, useMemo, @@ -21,12 +22,14 @@ import { Translator } from 'components/i18n'; interface Props { step: IStep; isRunning?: boolean; + nestedSteps?: ReactNode; } export default function Step({ step, children, - isRunning + isRunning, + nestedSteps }: PropsWithChildren) { const { showStepDetails } = useContext(MessageContext); @@ -34,7 +37,10 @@ export default function Step({ return isRunning && step.start && !step.end && !step.isError; }, [step, isRunning]); - const hasContent = step.input || step.output || step.steps?.length; + const hasSubSteps = !!step.steps?.length; + const hasContent = step.input || step.output || hasSubSteps; + // With details hidden, only sub-steps are worth expanding (input/output is dropped). + const expandable = showStepDetails ? Boolean(hasContent) : hasSubSteps; const isError = step.isError; const stepName = step.name; @@ -49,8 +55,8 @@ export default function Step({ } }, [using, step.autoCollapse]); - // If there's no content or step details are disabled, just render the status without accordion - if (!hasContent || !showStepDetails) { + // Nothing to expand: render a flat status label. + if (!expandable) { return (

- {children} + {showStepDetails ? children : nestedSteps}
diff --git a/frontend/src/components/chat/Messages/Message/index.tsx b/frontend/src/components/chat/Messages/Message/index.tsx index a7ffe81ea4..959e8923d1 100644 --- a/frontend/src/components/chat/Messages/Message/index.tsx +++ b/frontend/src/components/chat/Messages/Message/index.tsx @@ -86,6 +86,16 @@ const Message = memo( ); } + const nestedSteps = message.steps ? ( + !s.type.includes('message'))} + elements={elements} + actions={actions} + indent={indent + 1} + isRunning={isRunning} + /> + ) : null; + return ( <>
@@ -117,7 +127,11 @@ const Message = memo( ) : null} {/* Display the step and its children */} {isStep ? ( - + {showInputSection ? ( ) : null} - {message.steps ? ( - !s.type.includes('message') - )} - elements={elements} - actions={actions} - indent={indent + 1} - isRunning={isRunning} - /> - ) : null} + {nestedSteps} {shouldRenderOutput ? ( { return count; }; +// Assistant messages must surface at the root even when emitted from inside a +// nested step, so collect them recursively for compact mode. +const collectMessages = (steps: IStep[]): IStep[] => { + const out: IStep[] = []; + for (const s of steps) { + if (s.type.includes('message')) out.push(s); + else if (s.steps) out.push(...collectMessages(s.steps)); + } + return out; +}; + const Messages = memo( ({ messages, elements, actions, indent, isRunning, scorableRun }: Props) => { const messageContext = useContext(MessageContext); @@ -114,11 +125,9 @@ const Messages = memo( isRunning={isRunning} scorableRun={scorableRun} /> - {/* Render message-type children at root level */} + {/* Lift assistant messages (at any depth) to the root */} - s.type.includes('message') - )} + messages={collectMessages(m.steps)} elements={elements} actions={actions} indent={indent} From 28cce115cf2c43e5f56a84c899f3e72219be3f15 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 20 Aug 2026 22:45:11 +0300 Subject: [PATCH 09/13] fix(ui): base step expandability on non-message children; tighten E2E assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step: a step whose children are all messages is no longer 'expandable' into an empty accordion — hasSubSteps now matches what the body renders (non-message children), consistent with nestedSteps. - E2E: assert the nested message is absent from the compact summary after expanding it (Radix unmounts closed content, so the collapsed check was vacuous). Co-Authored-By: GitHub Copilot --- cypress/e2e/compact_steps/spec.cy.ts | 11 ++++++++++- .../src/components/chat/Messages/Message/Step.tsx | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/compact_steps/spec.cy.ts b/cypress/e2e/compact_steps/spec.cy.ts index b0b73d8b26..4fefeecefd 100644 --- a/cypress/e2e/compact_steps/spec.cy.ts +++ b/cypress/e2e/compact_steps/spec.cy.ts @@ -65,8 +65,17 @@ describe('Compact CoT display', () => { ); // The assistant message produced inside a nested step is lifted to the root: - // visible without expanding, and not contained in the compact summary. + // visible even though the summary is still collapsed. cy.contains('Nested answer').should('be.visible'); + + // Expanding the summary shows only the steps; the message is not duplicated + // inside (the accordion content is mounted once open, so this is meaningful). + cy.get('[data-testid="compact-steps-trigger"]').click(); + cy.get('[data-testid="compact-steps-trigger"]').should( + 'have.attr', + 'data-state', + 'open' + ); cy.get('[data-testid="compact-steps"]').should( 'not.contain', 'Nested answer' diff --git a/frontend/src/components/chat/Messages/Message/Step.tsx b/frontend/src/components/chat/Messages/Message/Step.tsx index a687565f8d..19d2e2535a 100644 --- a/frontend/src/components/chat/Messages/Message/Step.tsx +++ b/frontend/src/components/chat/Messages/Message/Step.tsx @@ -37,7 +37,10 @@ export default function Step({ return isRunning && step.start && !step.end && !step.isError; }, [step, isRunning]); - const hasSubSteps = !!step.steps?.length; + // Match what the accordion body renders (nestedSteps = non-message children), + // so a step whose only children are messages isn't expandable into empty content. + const hasSubSteps = + step.steps?.some((s) => !s.type.includes('message')) ?? false; const hasContent = step.input || step.output || hasSubSteps; // With details hidden, only sub-steps are worth expanding (input/output is dropped). const expandable = showStepDetails ? Boolean(hasContent) : hasSubSteps; From 4e353b517741a19fb66801389b57a0ac9a7f3248 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Thu, 20 Aug 2026 22:58:56 +0300 Subject: [PATCH 10/13] fix(ui): don't flatten message-only steps in default CoT mode The non-message sub-step filter must only gate flat-mode expandability. Applying it to hasContent regressed default mode: a step whose children are all messages (e.g. a run wrapping assistant messages) flattened and dropped its accordion body, including MessageButtons. Keep hasContent based on any children; use the non-message filter solely for the show_step_details=false branch. Co-Authored-By: GitHub Copilot --- .../src/components/chat/Messages/Message/Step.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/chat/Messages/Message/Step.tsx b/frontend/src/components/chat/Messages/Message/Step.tsx index 19d2e2535a..6d414dc5b9 100644 --- a/frontend/src/components/chat/Messages/Message/Step.tsx +++ b/frontend/src/components/chat/Messages/Message/Step.tsx @@ -37,13 +37,13 @@ export default function Step({ return isRunning && step.start && !step.end && !step.isError; }, [step, isRunning]); - // Match what the accordion body renders (nestedSteps = non-message children), - // so a step whose only children are messages isn't expandable into empty content. - const hasSubSteps = - step.steps?.some((s) => !s.type.includes('message')) ?? false; + const hasSubSteps = !!step.steps?.length; const hasContent = step.input || step.output || hasSubSteps; - // With details hidden, only sub-steps are worth expanding (input/output is dropped). - const expandable = showStepDetails ? Boolean(hasContent) : hasSubSteps; + // Flat mode's accordion body renders only non-message sub-steps, so a step whose + // children are all messages isn't expandable there (it would open to nothing). + const hasVisibleSubSteps = + step.steps?.some((s) => !s.type.includes('message')) ?? false; + const expandable = showStepDetails ? Boolean(hasContent) : hasVisibleSubSteps; const isError = step.isError; const stepName = step.name; From 3ad243bb831c33156b3363d618d5ac5cc2d24c15 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Tue, 1 Sep 2026 21:44:23 +0300 Subject: [PATCH 11/13] fix(ui): respect step icon/avatar in compact CoT summary Compact mode hardcoded the summary avatar to the step name, so custom step icons/avatars and error state were dropped (they showed in list mode only). Pass avatarName/icon/isError from the last step, mirroring the normal message path. Adds an E2E covering the icon case. Co-Authored-By: GitHub Copilot --- cypress/e2e/compact_steps/main.py | 11 +++++++++++ cypress/e2e/compact_steps/spec.cy.ts | 11 +++++++++++ .../src/components/chat/Messages/CompactSteps.tsx | 8 +++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/compact_steps/main.py b/cypress/e2e/compact_steps/main.py index a73b019721..66389d1d02 100644 --- a/cypress/e2e/compact_steps/main.py +++ b/cypress/e2e/compact_steps/main.py @@ -16,6 +16,11 @@ async def tool3(): return "Result from tool3" +@cl.step(type="tool", name="tool_icon", icon="search") +async def tool_icon(): + return "Result from tool_icon" + + @cl.step(type="tool", name="agent") async def agent(): # Two tools nested under an intermediate step. Compact mode must count the @@ -48,6 +53,12 @@ async def main(message: cl.Message): # at the root, not be trapped inside the collapsed compact summary. await answering_agent() return + elif content == "icon": + # The last visible step carries an icon, so the compact summary avatar + # must render that icon instead of a fallback avatar image. + await tool1() + await tool2() + await tool_icon() else: # Three sequential tools -> collapsed into one compact summary. await tool1() diff --git a/cypress/e2e/compact_steps/spec.cy.ts b/cypress/e2e/compact_steps/spec.cy.ts index 4fefeecefd..8d2d14f178 100644 --- a/cypress/e2e/compact_steps/spec.cy.ts +++ b/cypress/e2e/compact_steps/spec.cy.ts @@ -81,4 +81,15 @@ describe('Compact CoT display', () => { 'Nested answer' ); }); + + it("renders the last step's icon in the compact summary avatar", () => { + cy.visit('/'); + submitMessage('icon'); + + cy.get('[data-testid="compact-steps"]').should('have.length', 1); + + // The summary avatar reflects the last step's icon (an ), so no + // fallback avatar is rendered inside the compact summary. + cy.get('[data-testid="compact-steps"]').find('img').should('not.exist'); + }); }); diff --git a/frontend/src/components/chat/Messages/CompactSteps.tsx b/frontend/src/components/chat/Messages/CompactSteps.tsx index 3aa7f091b3..d5be7e6f90 100644 --- a/frontend/src/components/chat/Messages/CompactSteps.tsx +++ b/frontend/src/components/chat/Messages/CompactSteps.tsx @@ -87,7 +87,13 @@ const CompactSteps = memo(
- +
Date: Fri, 18 Sep 2026 20:02:37 +0300 Subject: [PATCH 12/13] chore: bump to reset stale bot Co-Authored-By: GitHub Copilot From 87b737f419950e1b30fe71cef59a7e235075a635 Mon Sep 17 00:00:00 2001 From: "Roy B.a" Date: Sun, 27 Sep 2026 13:45:23 +0300 Subject: [PATCH 13/13] fix(cot): keep compact 'Using' label while tools run and fix i18n/deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CompactSteps: hold 'Using' when a leaf step is still executing so an intermediate assistant message no longer flips the label to 'Used' mid-run - MessagesContainer: add missing useMemo deps (uploadFile, latex, edit_message, onFeedbackDeleted) to avoid stale context values - el-GR: use passive plural 'Χρησιμοποιήθηκαν' for the completed summary - ar-SA: reword to count-agnostic noun phrase to stay grammatical for any count Co-Authored-By: GitHub Copilot --- backend/chainlit/translations/ar-SA.json | 4 ++-- backend/chainlit/translations/el-GR.json | 4 ++-- .../components/chat/Messages/CompactSteps.tsx | 19 +++++++++++++++---- .../chat/MessagesContainer/index.tsx | 6 +++++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/backend/chainlit/translations/ar-SA.json b/backend/chainlit/translations/ar-SA.json index cf31f7ec5d..8a0feae0f2 100644 --- a/backend/chainlit/translations/ar-SA.json +++ b/backend/chainlit/translations/ar-SA.json @@ -100,8 +100,8 @@ "status": { "using": "يستخدم", "used": "مستخدم", - "usedSteps": "استُخدمت {{count}} خطوات", - "usedTools": "استُخدمت {{count}} أدوات" + "usedSteps": "الخطوات المستخدمة: {{count}}", + "usedTools": "الأدوات المستخدمة: {{count}}" }, "actions": { "copy": { diff --git a/backend/chainlit/translations/el-GR.json b/backend/chainlit/translations/el-GR.json index 18e6ecabdf..32f58a1f0e 100644 --- a/backend/chainlit/translations/el-GR.json +++ b/backend/chainlit/translations/el-GR.json @@ -101,8 +101,8 @@ "status": { "using": "Με τη χρήση", "used": "Χρησιμοποιήθηκε", - "usedSteps": "Χρησιμοποίησε {{count}} βήματα", - "usedTools": "Χρησιμοποίησε {{count}} εργαλεία" + "usedSteps": "Χρησιμοποιήθηκαν {{count}} βήματα", + "usedTools": "Χρησιμοποιήθηκαν {{count}} εργαλεία" }, "actions": { "copy": { diff --git a/frontend/src/components/chat/Messages/CompactSteps.tsx b/frontend/src/components/chat/Messages/CompactSteps.tsx index d5be7e6f90..9f05c04f5b 100644 --- a/frontend/src/components/chat/Messages/CompactSteps.tsx +++ b/frontend/src/components/chat/Messages/CompactSteps.tsx @@ -57,9 +57,10 @@ const CompactSteps = memo( return collectVisible(steps, cot); }, [steps, cot]); - // Show "Using" until an assistant_message appears (meaning tools are done - // and the model is streaming). This avoids flashing between sequential tools - // (no assistant_message yet) while still switching once the answer begins. + // Show "Using" until an assistant_message appears (tools done, model + // streaming), which avoids flashing between sequential tools. But if a + // leaf step is still executing keep "Using" too, so an agent that emits an + // intermediate message then keeps working doesn't wrongly read "Used". const hasAnswer = useMemo(() => { const check = (items: IStep[]): boolean => items.some( @@ -69,7 +70,17 @@ const CompactSteps = memo( return check(steps); }, [steps]); - const showUsing = !!isRunning && !hasAnswer; + const hasActiveStep = useMemo(() => { + const check = (items: IStep[]): boolean => + items.some((s) => + s.steps?.length + ? check(s.steps) + : !s.type.includes('message') && !!s.start && !s.end && !s.isError + ); + return check(steps); + }, [steps]); + + const showUsing = !!isRunning && (!hasAnswer || hasActiveStep); // Get the last visible step name for the "Using X" label const lastStep = visibleSteps[visibleSteps.length - 1]; diff --git a/frontend/src/components/chat/MessagesContainer/index.tsx b/frontend/src/components/chat/MessagesContainer/index.tsx index f89835560e..edc20b2ca9 100644 --- a/frontend/src/components/chat/MessagesContainer/index.tsx +++ b/frontend/src/components/chat/MessagesContainer/index.tsx @@ -172,6 +172,7 @@ const MessagesContainer = ({ navigate }: Props) => { onFeedbackDeleted }; }, [ + uploadFile, askUser, enableFeedback, loading, @@ -180,10 +181,13 @@ const MessagesContainer = ({ navigate }: Props) => { config?.ui?.cot_display, config?.ui?.show_step_details, config?.features?.unsafe_allow_html, + config?.features?.latex, config?.features?.user_message_markdown, + config?.features?.edit_message, onElementRefClick, onError, - onFeedbackUpdated + onFeedbackUpdated, + onFeedbackDeleted ]); return (