Preserve textFormat through HttpStream's intermediate and final activities - #762
Conversation
…ities
HttpStream rebuilds outbound wire activities (TypingActivityInput chunks and
the final MessageActivityInput) from scratch each flush/close cycle. It
already carries forward attachments, entities, suggested actions, and
channelData from the last emitted message ("last message wins"), but did not
carry forward textFormat, so `stream.emit({ type: "message", text,
textFormat: "extendedmarkdown" })` was silently dropped from every
intermediate typing chunk and the final message. The timeout/plain-final
fallback (sendFinal) had the same gap.
- Model textFormat on ITypingActivity/TypingActivityInput (previously only
modeled on message activities), with a withTextFormat() builder method,
mirroring MessageActivityInput.
- HttpStream now reads the last emitted message's textFormat off
this.finalActivity (the existing last-message-wins state) and applies it to
every cumulative typing chunk, informative typing updates, the normal final
message, and the timed-out plain-final fallback.
- Add unit tests covering textFormat propagation across intermediate chunks,
the final message, and the timeout fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
|
|
Before fix: before-fix.mp4After fix: after-fix.mp4 |
…mple Adds an 'extended-markdown' trigger that streams a release-status update where each delta sets textFormat: 'extendedmarkdown', so task lists and strikethrough render while streaming. Also trims verbose textFormat comments per review feedback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0073e3f0-b7b4-464a-9bdf-30f97d8081ec
There was a problem hiding this comment.
Pull request overview
This PR fixes a streaming regression where HttpStream rebuilt intermediate/final activities without carrying forward the last emitted message’s textFormat, causing streamed Extended Markdown to render as plain markdown. It extends typing activity models/builders to support textFormat, and ensures HttpStream propagates that value consistently across intermediate typing chunks, final close, and timeout fallback.
Changes:
- Add
textFormatsupport (andwithTextFormat()builder) to typing activity interfaces and builders in@microsoft/teams.api. - Update
HttpStreamto apply the last emitted message’stextFormatto intermediate typing chunks, the final message sent onclose(), and thesendFinal()timeout fallback. - Add unit tests covering typing
textFormatmodeling and end-to-end propagation through streaming behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/apps/src/http/http-stream.ts | Propagates finalActivity.textFormat into typing chunks and final/timeout message sends. |
| packages/apps/src/http/http-stream.spec.ts | Adds tests verifying textFormat is retained for intermediate chunks, final message, and timeout fallback. |
| packages/api/src/activities/typing.ts | Models textFormat on typing activities and adds withTextFormat() builders. |
| packages/api/src/activities/typing.spec.ts | Adds unit tests for TypingActivityInput and TypingActivity textFormat behavior. |
| examples/stream/src/index.ts | Extends the streaming example to demonstrate Extended Markdown deltas via textFormat: 'extendedmarkdown'. |
| examples/stream/README.md | Documents how to trigger the Extended Markdown streaming demo. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…vities (#581) ## Summary Ports [microsoft/teams.ts#762](microsoft/teams.ts#762) and its follow-up [#784](microsoft/teams.ts#784) to the Python SDK. On every `flush()`, `HttpStream` rebuilds the outbound typing chunk and forwards the last emitted message's attachments, entities, suggested actions, and channel data ("last message wins" via `self._final_activity`) — but not `text_format`. So `stream.emit(MessageActivityInput(text=...).with_text_format("extendedmarkdown"))` was dropped from every intermediate chunk, and streamed Extended Markdown (task lists, strikethrough) rendered as plain markdown until the message closed. The final message and timeout fallback (`_send_final`) already preserved `text_format`, since they reuse the real `_final_activity` object rather than rebuilding a copy (unlike TS). Only the streamed chunks needed fixing. ## Changes - Add `text_format` + a `with_text_format()` builder to `_TypingBase` (shared by `TypingActivity` / `TypingActivityInput`), mirroring `MessageActivityInput`. - `HttpStream._flush()` applies the last emitted message's `text_format` to the combined streamed-text chunk (last-message-wins). - **Informative updates keep their own `text_format`.** `_flush()` previously overwrote each informative update's format with `_final_activity.text_format`; it now leaves the update's own value intact (`_final_activity` isn't even set when informative updates are sent). - **`update()` gains an optional `text_format`.** `StreamerProtocol.update` / `HttpStream.update` now accept `text_format`; `None` → Teams default (`markdown`), an explicit value → that format. Previously the only way to format an informative update was to hand-build a typing activity and `emit()` it. - `examples/stream`: refresh the `extended-markdown` scenario and demo the new `update(text, "markdown")` overload. - Unit tests for `text_format` on typing activities, its propagation across streamed chunks / final message / timeout fallback, informative-update format independence (red/green against the old overwrite), and the `update()` overload. ## Testing - `ruff format --check`, `ruff check`, `pyright`, `pytest packages` (1107 passed) — all pass. - Verified live in Teams: streaming a message with `text_format="extendedmarkdown"` renders task lists and strikethrough on each intermediate chunk. _Created from a [Microsoft Teams conversation](https://teams.microsoft.com/l/message/19%3AeXvmZeTKJ1Myy2_qfz4vp4-YjeIqCMJ4xF6poI2GxtU1%40thread.tacv2/1787691375435?tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47&groupId=1314f851-c930-4caa-b3e0-dbe9b8fe2737&parentMessageId=1787691375435)._ --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Kavin Singh <kavinsingh@microsoft.com> Co-authored-by: Kavin <115390646+singhk97@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
HttpStreamrebuilds outbound activities on every flush/close and forwarded the last emitted message's attachments, entities, suggested actions, and channelData — but nottextFormat. As a result,stream.emit({ text, textFormat: 'extendedmarkdown' })was accepted but dropped on every intermediate typing chunk and on the final message, so streamed Extended Markdown rendered as plain markdown.Fix
textFormatonITypingActivity/ITypingActivityInput/TypingActivityInputwith awithTextFormat()builder (mirroringMessageActivityInput).HttpStreamreads the last emitted message'stextFormat(same "last message wins" state as attachments/entities/etc.) and applies it to the cumulative typing chunks and informative updates inflush(), the final message inclose(), and the timeout fallback insendFinal().Testing
packages/api/src/activities/typing.spec.tsandpackages/apps/src/http/http-stream.spec.tscoveringtextFormaton typing activities and propagation across intermediate chunks, the final message, and the timeout fallback.Note on C#
This repo only contains the TypeScript SDK; the .NET streaming abstraction is out of scope here.