feat(ai-chat): показывать текст запроса/аргументы в карточке вызова инструмента (#392) #393
Reference in New Issue
Block a user
Delete Branch "feat/392-tool-input-summary"
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?
Summary
Карточка вызова инструмента в AI-чате теперь показывает текст запроса/аргументы вместо безликого «Ran tool Search_web_search». closes #392
Как сделано (по предложенному в тикете решению):
toolInputSummary(part)вutils/tool-parts.tsx: берёт первое присутствующее «primary»-поле (query/q/searchQuery/url/urls/title/name/text/prompt), схлопывает пробелы/переводы строк, обрезает до ~140 символов с «…»; массив (urls) →первый (+N).tool-call-card.tsx— одна строка под меткой:<Text size="xs" c="dimmed" lineClamp={2}>. Только plain text через Mantine<Text>(React экранирует) — никакогоdangerouslySetInnerHTML, XSS невозможен даже если запрос содержит HTML/markdown/бэктики.Тонкие места из тикета — учтены:
messageSignature. Сводку показываю только после финализации входа (гейтstate === "input-streaming" → undefined).message-signature.tsне трогал: переходinput-streaming → input-availableменяетstate, который входит в сигнатуру, → строка перерисовывается с полным значением. «Замороженной» сводки нет.Решения по двум вопросам тикета (подтверждены владельцем — «оба ок»):
showInput(по аналогии сshowCitations),MessageList → MessageItem (+memo) → ToolCallCard, дефолтtrue, аshare-ai-widget.tsxпередаётshowInput={false}.How verified
Прогонял на стенде из чистого состояния (rebase на текущий develop):
vitest src/features/ai-chat/utils/tool-parts.test.tsx→ 21 passed (11 прежних + 10 новых): query-строка, массивurls→«a (+2)», одиночный элемент без суффикса, фолбэк наtitle, обрезка+«…», схлопывание пробелов, нет/пустой/не-объект input →undefined,input-streaming+полный input →undefined.tsc --noEmit— по всем затронутым файлам без ошибок.Внутренний цикл: 1 проход (general-purpose ревьюер, read-only) — блокеров нет; отдельно перепроверены XSS (только React-escaped
<Text>), гейт стриминга (сигнатура строки меняется на финализации), пробросshowInputво все компоненты + наличие вarePropsEqualмемо, независимость отshowCitations.Checklist
For tools without a friendly label (esp. external MCP tools like Search_web_search) the card showed a generic "Ran tool <name>" with no sense of what was searched. Add a compact one-line, dimmed summary of the call's arguments under the label, pulled from part.input. New pure toolInputSummary(part): picks the first present "primary" field (query/q/searchQuery/url/urls/title/name/text/prompt), collapses whitespace, clamps to ~140 chars; arrays render "first (+N)". It returns undefined during input-streaming (the input grows while state is fixed and messageSignature doesn't track input, so a live summary would freeze) — the state flip to input-available re-renders the row with the final value, so message-signature.ts is left untouched. The value renders ONLY through Mantine <Text> (React-escaped) — no markdown/HTML, no XSS. A showInput prop (default true) is threaded MessageList -> MessageItem (+memo) -> ToolCallCard; the public share widget passes showInput={false} so an anonymous reader never sees the agent's raw query text (mirrors showCitations). No JSON fallback when no primary field is present. closes #392 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Передаю на ревью. Реализация по тикету, оба открытых вопроса — как подтвердил владелец («оба ок»): в публичном шэре текст запроса скрыт (showInput=false, как цитаты), JSON-фолбэка нет. Ключевое: сводка — только plain-text через Mantine (XSS невозможен), message-signature.ts не тронут (гейт на input-streaming), showInput в мемо-сравнении MessageItem. Прогоны: 21/21 тестов хелпера, tsc чист по затронутым файлам. Внутренний цикл: 1 проход, блокеров нет.
🤖 Авто-ревью — feat(ai-chat): текст запроса/аргументы в карточке вызова инструмента (#392)
Вердикт: CHANGES (2 DO — оба дешёвые тест-локи на核-логику хелпера; безопасность и приватность подтверждены). Гейт зелёный (чистый клон-рум): client
tsc0, vitest 21 passed.✅ Что проверено независимо (9 аспектов; приватность/XSS — построчно)
toolInputSummaryрендерится ровно в одном месте (tool-call-card.tsx:71), под гейтомshowInput ? … : undefined. Консьюмеров<MessageList>ровно ДВА:chat-thread.tsx:815(внутренний чат, дефолтtrue) иshare-ai-widget.tsx:163(аноним,showInput={false}).showInput— единственный гейт,falseна единственном анонимном пути. Второе чтениеpart.input(toolCitations) берёт толькоpageId/titleи гейтится отдельнымshowCitations={false}. Непокрытого пути нет.<Text>→ React-экранирует.dangerouslySetInnerHTMLв дифф-е нет; два существующих сайта (answer/reasoning) — DOMPurify-санитайзнутый markdown, к сводке отношения не имеют.url/urlsне попадают вhref/атрибут.message-signature.tsне тронут и включаетstate; хелпер отдаётundefinedпокаinput-streaming, а флипinput-streaming → input-availableменяет сигнатуру → строка перерисовывается полным значением.arePropsEqualтеперь сравниваетshowInput. «Замороженной» сводки нет. Регрессий у существующих консьюмеров нет (showInputопционален, дефолтtrue= новое поведение фичи).message-signature.ts(кортеж[type, len, state, error?, output?]—stateтрекается,inputнет) — точен.🔧 DO — доработать и перезапросить ревью (2)
PRIMARY_INPUT_FIELDS) не проверен —tool-parts.test.tsx. Тесты покрывают одиночные поля (query→query,title-fallback когда query отсутствует), но НИ ОДИН не подаёт ≥2 primary-поля сразу и не проверяет, что раннее выигрывает. Сам смысл упорядоченного списка (query→q→searchQuery→url→urls→title→name→text→prompt) не залочен — перестановка полей прошла бы молча. Фикс: кейс вроде{ query: "Q", title: "T" } → "Q".clamp=s.length > MAX ? slice(0,140)+"…" : s. Единственный тест — 300 символов (endsWith("…")+≤141); собственно граница не запиннена (off-by-one/>↔>=не поймается). Фикс:140 символов → без изменений/без "…",141 → длина 141 и кончается "…".⛔ DROP / вне scope (кодеру НЕ делать в рамках DO)
[below-threshold]suggestion/low[stability]String(value[0])на не-строковом элементе массива →"[object Object]"/"null"/"42". Для документированной формыurls: string[]не срабатывает — косметика.[below-threshold]suggestion/low[conventions]toolInputSummaryинлайнитtypeof value === "string" && value.length > 0вместо переиспользования хелпераasString— косметический DRY-нит.tool-call-card.tsx:77-81рендеритpart.errorTextБЕЗ гейтаshowInput→ серверное сообщение об ошибке инструмента показывается и в публичном шэре. Пред-существующее (не введено #393, касаетсяerrorText, а не добавляемогоinput). Учитывая, что PR аккуратно гейтитinputот анонима, стоит рассмотреть такой же гейт дляerrorTextотдельным тикетом.🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge. Гейт (чистый клон-рум): client
tsc0 · vitest 21 passed. Независимо: перечислил всех консьюмеров<MessageList>(2, шэр гейтнут), проверил отсутствиеdangerouslySetInnerHTML-стока. База developcafe29b1.Review round: add the two missing test-locks the reviewer asked for — (1) priority order of PRIMARY_INPUT_FIELDS is now pinned (`{query,title}` -> "Q", so a reordering breaks the test); (2) the clamp boundary is pinned exactly (140 chars -> unchanged, no ellipsis; 141 -> 140 + "…"), catching an off-by-one / `>` vs `>=` regression. Test-only, no production change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Поправил оба DO — перезапрашиваю ревью. Оба тест-локи на логику хелпера, продакшен-код не менялся.
DO-1 (приоритет полей). Добавил кейс
{ query: "Q", title: "T" } → "Q"— теперь упорядоченностьPRIMARY_INPUT_FIELDSзалочена, перестановка полей уронит тест.DO-2 (граница clamp). Запинил ровно границу: 140 символов → без изменений и без «…» (длина 140); 141 →
"a"×140 + "…"(длина 141). Off-by-one />vs>=теперь ловится.Гейт:
vitest tool-parts.test.tsx→ 24 passed (21 прежних + 3 новых), tsc чист.Про DROP-заметку (errorText инструмента показывается в публичном шэре без гейта showInput) — согласен: предсуществующее, не про добавленный input; вынесу отдельным тикетом, в этот PR не тащу.
(прошлый мой коммент #7113 обрезался из-за экранирования — это исправленная версия.)
🤖 Авто-ревью (фикс-раунд) — оба DO закрыты → PASS ✅
Фикс-раунд
cb445f09 → 7f88b0b4: +36 строк, только тесты (tool-parts.test.tsx), прод-код не тронут. Гейт: vitest 24 passed (было 21 + 3 новых).Адверсариальная проверка обоих DO
{ query: "Q", title: "T" } → "Q"с комментарием «reordering must break this test». Мутация: переставилtitleпередqueryвPRIMARY_INPUT_FIELDS→ тест падает. Порядок-контракт залочен. ✓140 симв → без изменений/без "…"/len 140и141 → "a"*140+"…"/len 141. Мутация>→>=→ 140-точный тест падает. Граница залочена. ✓Безопасность/приватность/XSS/мемоизация были подтверждены в прошлом раунде (текст запроса гейтится
showInput, в публичном шэреfalse; plain-text через<Text>). Approve.Напоминание (вне scope, отдельным тикетом):
part.errorTextв публичном шэре по-прежнему без гейтаshowInput— пред-существующее, к этому PR не относится.Гейт: vitest 24 passed. Мутации: reorder
PRIMARY_INPUT_FIELDS(precedence-тест роняет),>→>=(140-boundary роняет). Диф фикса от прошлого headcb445f09(только тесты). База developcafe29b1.