AI-чат: показывать текст запроса/аргументы в карточке вызова инструмента #392
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Проблема
В панели AI-агента каждый вызов инструмента отображается карточкой-логом (
ToolCallCard). Сейчас карточка показывает только человекочитаемую метку действия. Для инструментов без специальной метки (в частности — внешние MCP-инструменты вродеSearch_web_search,Search_read_pages) карточка выводит родовую надпись «Ran tool Search_web_search» и не показывает, что именно искали.На скриншоте от пользователя видна длинная серия одинаковых карточек
Ran tool Search_web_search— по ним невозможно понять, какие запросы делал агент. Хочется, чтобы в карточке показывался текст запроса (аргументы вызова).Как устроено сейчас
apps/client/src/features/ai-chat/components/tool-call-card.tsx. Выводится толькоt(key, values)— метка, полученная изtoolLabelKey(toolName).apps/client/src/features/ai-chat/utils/tool-parts.tsx, функцияtoolLabelKey(). Известные Docmost-инструменты получают дружелюбные метки («Searched pages», «Created page» и т.д.), всё остальное падает вdefault→"Ran tool {{name}}". Внешние MCP-инструменты всегда попадают в этотdefault.part.input(объект). Например, для web-поиска это{ query: "..." }; для DocmostsearchPages—{ query }; для операций со страницей —{ pageId, title, ... }.parts(включаяinput) вmetadata.parts. Более того, экспорт в Markdown (apps/server/src/core/ai-chat/chat-markdown.util.ts, строки ~209–211) уже рендеритpart.inputкак fenced-блок JSON. То есть прецедент показа входных данных есть, иinputдоступен и в лайве, и в открытой заново истории.Предлагаемое решение
Добавить рядом с меткой компактную строку-саммари аргументов (одна строка, dimmed, с обрезкой по длине).
1. Хелпер
toolInputSummary(part)вtool-parts.tsxЧистая функция, извлекающая короткую человекочитаемую строку из
part.input:где
collapseсхлопывает пробелы/переводы строк в один пробел,clampобрезает до ~140 символов с «…».Открытый вопрос (см. ниже) — нужен ли фолбэк на компактный JSON-превью, когда primary-поле не найдено. Дефолт предложения: не показывать (меньше шума), т.к. у известных инструментов primary-поле обычно есть.
2. Рендер в
tool-call-card.tsxПод строкой с меткой добавить (когда саммари непустое):
Стиль — как у блока цитат:
size="xs",c="dimmed", обрезка строк. Рендер через Mantine<Text>(React экранирует) — никакогоdangerouslySetInnerHTML, поэтому XSS невозможен, даже если запрос содержит HTML/markdown.Файлы к изменению
apps/client/src/features/ai-chat/utils/tool-parts.tsx— новый хелперtoolInputSummary(+collapse/clamp).apps/client/src/features/ai-chat/components/tool-call-card.tsx— рендер саммари под меткой.apps/client/src/features/ai-chat/utils/tool-parts.test.tsx— юнит-тесты хелпера.apps/client/src/features/ai-chat/components/ai-chat.module.css— если понадобится отдельный класс.chat-markdown.util.ts) менять не нужно — экспорт уже показываетinput.Тонкие места (обязательно учесть)
messageSignature.message-signature.tsНЕ включаетinputв сигнатуру строки, аMessageItemмемоизирован по ней. Во времяinput-streamingобъектinputрастёт, ноstateне меняется → строка не перерендерится. Поэтому саммари показываем только после финализации входа (гейтstate !== "input-streaming"в хелпере): переходinput-streaming → input-availableменяетstate, а значит и сигнатуру, → строка перерисуется с полным значением. Так мы избегаем «замороженной» строки без правки load-bearing функцииmessageSignature. Альтернатива (если захотим показывать запрос живьём по мере стриминга) — расширитьmessageSignatureсигналом отinput(например, длинойJSON.stringify(input)); это дороже и трогает критичную функцию, поэтому не рекомендуется для v1.<Text>. Не интерпретировать markdown/HTML.text/promptне должны ломать вёрстку карточки.ru-RU/translation.jsonуже отсутствуют ключи меток инструментов (Ran tool {{name}},Searched pagesи др.) — есть толькоOpen page, поэтому русскоязычные пользователи и сейчас видят английские метки. Можно заодно добавить недостающие RU-ключи, но это отдельный мелкий фикс, не блокирующий эту задачу.Крайние случаи
inputотсутствует / не объект / пустой → саммариundefined, ничего не рендерим.urls) → показываем первый элемент +(+N).inputдоступен и там; проверить оба сценария (см. вопрос про шэр ниже).Тесты
Добавить в
tool-parts.test.tsxпокрытиеtoolInputSummary:query→ строка запроса;urls→ первый +(+N);titleдля операции со страницей;input/ пустой / не-объект →undefined;state: "input-streaming"→undefined(не показываем недостроенный ввод).Вопросы к решению (перед реализацией)
showCitations={false}). Показывать ли текст запроса анонимному читателю? Варианты: (а) показывать всегда — это часть лога действий агента; (б) скрывать по аналогии с цитатами (новый пропshowInput, по умолчаниюtrue, в шэреfalse). Предлагаю (а), но нужно подтверждение — вдруг запросы могут содержать чувствительный контекст.🤖 Оформлено при участии Claude Code (архитектурный разбор кода; код не менялся).