[feature][ai-chat] Компактный рендеринг блока «Thinking»: схлопывать пустые строки (\n\n) в reasoning #181
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?
Контекст
В блоке «Thinking» (раскрывающийся reasoning ассистента) текст рассуждений
отображается с очень большими вертикальными отступами: между пунктами
нумерованного/маркированного списка и между абзацами появляется почти по
целой пустой строке. Из-за этого reasoning занимает много места и выглядит
«рыхло».
Источник проблемы — модель присылает reasoning с двойными переносами строк
(
\n\n) между каждым пунктом и абзацем. В Claude Code это же содержимоепоказывается компактно.
Идея (из обсуждения): в reasoning можно убирать
\n\n— схлопывать пустыестроки перед рендером.
Где это происходит (анализ кода)
Рендер блока: reasoning-block.tsx
renderChatMarkdown(markdown.ts)прогоняет текст через общий
markdownToHtml(marked, см.marked.utils.ts).
Две независимые причины «рыхлости»:
Loose lists и отдельные абзацы. Когда пункты списка/абзацы в исходном
markdown разделены пустой строкой (
\n\n),markedрендерит «loose list»:содержимое каждого
<li>оборачивается в<p>, а абзацы становятсяотдельными
<p>. Каждый<p>получает вертикальный margin → большие зазоры.white-space: pre-wrapна markdown-контейнере. В CSS(ai-chat.module.css):
Класс
reasoningTextнавешан и на markdown-<div>(сdangerouslySetInnerHTML),и на текстовый fallback
<Text>. Для fallbackpre-wrapнужен (там сыройтекст с переносами), а вот на сгенерированном HTML он лишний: переносы строк
между блочными тегами (
</li>\n<li>,</p>\n<ol>и т.п.) выводятся каквидимые пустые строки/отступы, добавляя зазоры поверх margin'ов.
Предлагаемое решение
Сделать рендер reasoning компактным, только для блока «Thinking» (ответ
ассистента в message-item.tsx
трогать НЕ нужно — там абзацные отступы уместны):
Схлопывать пустые строки перед рендером. Добавить небольшой util,
например
collapseBlankLines(text), который заменяет последовательности из2+ переносов на один (
/\n{2,}/g → "\n"), и применять его вreasoning-block.tsxкtrimmedдо вызоваrenderChatMarkdown.Так loose lists превращаются в tight lists (без
<p>внутри<li>), а абзацысклеиваются. Важно:
markedсконфигурирован сbreaks: true, поэтомуодиночный
\nвсё равно даёт<br>— переносы строк сохранятся, исчезнуттолько «пустые» зазоры между блоками. Это ровно тот компактный вид, что нужен.
Сузить
white-space: pre-wrapтак, чтобы он применялся только ктекстовому fallback, а не к markdown-
<div>. Например, ввести отдельныйкласс/модификатор для fallback или убрать
pre-wrapиз.reasoningTextиоставить его инлайном только на
<Text>(там он уже задан:style={{ whiteSpace: "pre-wrap" }}).Edge cases / на что обратить внимание
```). Внутри блоков кода пустые строкизначимы — их схлопывать НЕЛЬЗЯ, иначе ломается форматирование кода.
collapseBlankLinesдолжен пропускать содержимое fenced-блоков (в reasoningкод редок, но возможен).
окажутся на соседних строках без пустой строки между ними. Для маркеров
-/*и нумерации, начинающейся с
1.,marked/GFM это обычно корректнораспознаёт как список, но стоит проверить тестом.
(в
reasoning-block.tsxили как опцияrenderChatMarkdown), а не в общемрендере, иначе схлопнутся абзацы и в обычных ответах.
Критерии приёмки
абзацами; reasoning выглядит компактно (как в Claude Code на скриншоте).
<p>внутри<li>), переносы строк внутрисохраняются.
Тесты
collapseBlankLines:\n\n+→\n; пустые строки внутри fenced-кодасохраняются.
reasoning-block: markdown со списком, разделённым пустыми строками,рендерится как tight list (нет
<li><p>), без видимых пустых строк.