Reference in New Issue
Block a user
Delete Branch "fix/ai-chat-copy-chat-wysiwyg"
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?
Closes #160. Closes #174.
Проблема (#160)
«Copy chat» строил Markdown из persisted-строк + живого «хвоста», который подмешивался ТОЛЬКО при
isStreaming. При обрыве хода (Lost connection / dropped stream)isStreamingстановился false → живой хвост отбрасывался, а частичный ответ ассистента (видимый на экране, чья строка часто так и не сохранилась) пропадал из выгрузки — оставались только сообщения пользователя.Решение #160 (WYSIWYG)
buildChatMarkdownтеперь live-first: живые (on-screen) сообщения становятся документом. Каждое матчится к persisted-строке поidдля обогащения токенами/ошибкой/таймстампом; авторитетныеusage/error, уже лежащие на живом сообщении (сервер кладёт usage на границе шага), приоритетнее строки. При пустомlive— fallback на persisted-строки (старый формат сохранён). Пометка «still generating» — только на ассистенте, который реально является хвостом стрима (в окне submitted, где хвост — user-сообщение, предыдущий завершённый ответ больше не помечается).ChatThread, зеркалится вliveStateRefрядом с messages/isStreaming и добавляется в конец выгрузки.handleCopyмаппит живые сообщения и передаётlive/rows/isStreaming/banner.Проблема (#174)
Кнопка «Copy chat» была скрыта на САМОМ первом ходе нового, ещё не сохранённого чата: и гейт
canExport, и ранний возвратhandleCopyтребовалиactiveChatIdИ persisted-строк (messageRows), которых на первом ходе ещё нет (он стримится или оборвался до сохранения любой строки). Это ровно тот edge-case, что был задокументирован как вне scope #160.Решение #174
ChatThreadтеперь репортит реактивный сигналonLiveContentChange(messages.length > 0)— живой снапшот лежит в нереактивномliveStateRef, поэтому для ре-рендера кнопки нужен отдельный реактивный флаг. Родитель держит его вhasLiveContent.canExport = hasLiveContent || (есть persisted-строки); ранний возвратhandleCopyсрабатывает только когда пусто И live, И rows.handleCopyпередаёт placeholder"unsaved"в качестве chat id, когда настоящего ещё нет; live-first билдер сериализует on-screen тред WYSIWYG.falseдо эффекта нового маунта → корректныйhasLiveContent, без stale-true для реально пустого чата. ИдентичныйsetState— no-op, поэтому per-delta ре-рендеров нет.Тесты
chat-markdown.test.ts: live/enrichment/fallback/banner + регресс на окно submitted + 2 новых теста на экспорт первого хода без persisted-базы (#174) — 28 зелёных. Полный набор ai-chat зелёный (185).tscчисто,prettier --checkчисто.Ревью
#160: два прохода ревью-сабагента (первый нашёл реальный баг ложной пометки «генерируется» — исправлено + регресс-тест; повторный — APPROVE). #174: проход ревью-сабагента — APPROVE WITH SUGGESTIONS (поправлены кавычки
"unsaved"под prettier; реактивность/отсутствие per-delta ре-рендеров/зависимостиhandleCopy— без замечаний).🤖 Generated with Claude Code
039071852etodf81851eb3fix(ai-chat): WYSIWYG Copy chat export keeps the on-screen partial reply (#160)to fix(ai-chat): WYSIWYG Copy chat export + first-turn export (#160, #174)