[bug][ai-chat] Нельзя скопировать незавершённое сообщение: кнопка «Copy chat» скрыта во время первого стрима нового чата #174
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?
Кратко
Кнопку «Copy chat» нельзя использовать для незавершённого сообщения. В самый частый момент — пока идёт первый стрим ответа в новом чате (агент ещё думает/вызывает инструменты, активна красная кнопка Stop) — иконка копирования в шапке окна вообще не показывается, хотя на экране уже есть и сообщение пользователя, и ответ агента (текст плана, блоки «Ran tool …», размышления).
Нужно: разрешить копирование того, что показано на экране, даже когда turn ещё не завершён и чат ещё не сохранён.
Воспроизведение
Ожидается: доступна кнопка «Copy chat», по нажатию в буфер попадает то, что видно на экране (сообщение пользователя + частичный ответ ассистента с текстом и блоками инструментов, помеченный как генерирующийся).
Фактически: кнопки «Copy chat» в шапке нет вообще. Она появляется только после завершения первого хода.
Корень проблемы
Видимость кнопки и сам экспорт завязаны на persisted-состояние, а не на то, что реально отрисовано:
canExport— ai-chat-window.tsx#L510:handleCopyдополнительно выходит по тому же условию — ai-chat-window.tsx#L252:А то, что на экране, рендерится из живого in-memory треда
useChat, а не изmessageRows:MessageListотрисовывает livemessages— chat-thread.tsx#L423-L425;liveStateRefвсегда — chat-thread.tsx#L318-L323.Для первого, ещё не завершённого хода нового чата оба слагаемых
canExportложны:activeChatId === nullдо конца хода — реальный id «усыновляется» только вonTurnFinished(use-chat-session.ts#L119-L129, канон — adopt-chat-id.ts);messageRowsберётся изuseAiChatMessagesQuery(activeChatId ?? undefined)(ai-chat-window.tsx#L148-L149) и рефетчится тоже только вonTurnFinished(use-chat-session.ts#L146-L148).Итог: во время первого стрима контент существует только в live-треде →
canExport === false→ кнопка скрыта, аhandleCopyвсё равно сделал бы no-op.Связь с #160
Это ровно тот «смежный момент вне scope», который отмечен в #160 (раздел «Заметки и риски»):
canExportтребуетmessageRows.length > 0, поэтому для первого хода нового чата кнопка скрыта. #160 чинит потерю содержимого при экспорте оборванного хода уже существующего чата; этот issue — про видимость и работу кнопки для незавершённого хода, когда persisted-базы ещё нет вовсе. Решения пересекаются (live-first построение Markdown), поэтому делать желательно поверх/вместе с #160.Предлагаемое решение (направление)
activeChatId/messageRows. Оставить кнопку скрытой только для по-настоящему пустого чата (экран выбора роли, ноль сообщений).handleCopy: когда persisted-базы нет (activeChatId === nullилиmessageRowsпуст), строить Markdown только из live-треда (live-first), переиспользуя WYSIWYG-логику из #160.chatIdможет бытьnull/плейсхолдер,titleможет отсутствовать; помечать ассистентский ответ как генерирующийся (generating).Подводный камень реактивности:
liveStateRef— этоref, его изменение само по себе не вызывает ре-рендер шапки. Во время стрима шапка и так перерисовывается из-заliveTurnTokens(~8 Гц), но чтобы кнопка стабильно появлялась/исчезала, для гейта нужен реактивный сигнал «есть live-контент» (отдельный state/проп изchat-thread), а не чтениеliveStateRef.currentнапрямую.Затронутые файлы
canExport/гейт кнопки +handleCopy(ветка без persisted-базы).chatId === null(пересечение с #160).Критерии приёмки
activeChatId, нет persisted-строк) кнопка «Copy chat» видна.apps/client→pnpm testзелёный; добавлены тесты на копирование незавершённого хода.Ghost referenced this issue2026-06-25 03:52:53 +03:00
Ghost referenced this issue2026-06-25 11:03:55 +03:00
Ghost referenced this issue2026-06-25 11:56:26 +03:00