Skip to content

Preserve textFormat through TeamsStreamingWriter's intermediate and final activities - #661

Open
Copilot wants to merge 5 commits into
mainfrom
copilot/preserve-textformat-streaming-writer
Open

Preserve textFormat through TeamsStreamingWriter's intermediate and final activities#661
Copilot wants to merge 5 commits into
mainfrom
copilot/preserve-textformat-streaming-writer

Conversation

Copilot AI commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Mirrors microsoft/teams.ts#762, applied to the .NET streaming abstraction (TeamsStreamingWriter).

The .NET streamer has a different shape than the TS HttpStream (raw string deltas are accumulated via AppendResponseAsync, with a structured MessageActivityInput only supplied at FinalizeResponseAsync), so the bug shows up slightly differently here: StreamingActivityInput (the typing-type chunk used for informative/intermediate updates) never modeled textFormat at all, so there was no way to make intermediate streamed chunks render as anything other than the default format — only the final message (built directly from the caller-supplied MessageActivityInput) could carry it. In practice this meant content intended to stream as Extended Markdown (task lists, strikethrough, etc.) rendered as plain markdown while streaming and only switched format once the final message landed.

Fix

  • Modeled TextFormat on StreamingActivityInput (the typing-type chunk used for informative/intermediate updates), with a WithTextFormat() builder mirroring MessageActivityInput.
  • Added TeamsStreamingWriter.WithTextFormat(TextFormat): sets the format applied to every subsequent informative/intermediate typing chunk, and used as the final message's default format unless the caller sets TextFormat explicitly on the activity passed to FinalizeResponseAsync (explicit final value wins).
  • The tracked format resets when the writer is reused for a new streamed message after FinalizeResponseAsync, consistent with how the rest of the per-stream state (accumulated text, sequence, etc.) resets.

Note

Extended Markdown is available in public developer preview, which is why the corresponding TextFormats.ExtendedMarkdown constant remains behind the ExperimentalTeamsExtendedMarkdown diagnostic.

Testing

  • Added unit tests in TeamsStreamingWriterTests.cs covering: format applied to informative + streaming chunks + final message, no format sent when unset, explicit final-activity format overriding the writer's tracked format, and reset on stream reuse.
  • Full Microsoft.Teams.Apps.UnitTests suite passes (532/532).

Created from a Microsoft Teams conversation.

I attest that I have verified

…inal activities

Mirrors microsoft/teams.ts PR #762 for the .NET streaming writer.

- StreamingActivityInput (the typing-type chunks used for informative
  and intermediate streaming updates) now models textFormat and gets a
  WithTextFormat builder method, matching MessageActivityInput.
- TeamsStreamingWriter.WithTextFormat(TextFormat) sets the format
  applied to every subsequent intermediate chunk, and is used as the
  final message's default format unless the caller sets TextFormat
  explicitly on the activity passed to FinalizeResponseAsync.
- The tracked format resets when the writer is reused for a new
  streamed message after finalize, matching how other per-stream state
  is reset.

Without this, streamed extended markdown content (task lists,
strikethrough, etc.) rendered as plain markdown during intermediate
typing chunks and only switched to the intended format on the final
message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI requested a lite review from Copilot and removed request for Copilot August 27, 2026 20:04
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@singhk97

Copy link
Copy Markdown
Collaborator

After fix demo:

after-fix-cs.mp4

Copilot AI lite review requested due to automatic review settings August 28, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a formatting inconsistency in TeamsStreamingWriter so that TextFormat (notably Extended Markdown) is preserved across intermediate “typing” chunks and the final message, rather than only being applied at finalize time.

Changes:

  • Adds TextFormat support to StreamingActivityInput plus a fluent WithTextFormat() builder API.
  • Adds TeamsStreamingWriter.WithTextFormat(TextFormat) to track/apply a per-stream format to informative + streaming chunks, and as the default for the final message unless explicitly overridden.
  • Adds unit tests covering propagation, unset behavior, final override precedence, and state reset on writer reuse.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
test/Microsoft.Teams.Apps.UnitTests/TeamsStreamingWriterTests.cs Adds coverage for TextFormat propagation/override/reset behaviors in streaming.
src/Microsoft.Teams.Apps/TeamsStreamingWriter.cs Tracks a per-stream TextFormat and applies it to intermediate chunks and (as a default) to the final message.
src/Microsoft.Teams.Apps/TeamsStreamingWriter.Activity.cs Models textFormat on StreamingActivityInput and exposes it via the builder.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/Microsoft.Teams.Apps.UnitTests/TeamsStreamingWriterTests.cs Outdated
/// Cleared when the stream is reused for a new streamed message after
/// <see cref="FinalizeResponseAsync"/>; call this again for each new streamed message.
/// </remarks>
public TeamsStreamingWriter WithTextFormat(TextFormat textFormat)

@MehakBindra Mehak Bindra (MehakBindra) Aug 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like having this both in activity and writer is confusing, if both are filled, which one is honored? Let's just keep it contained to streaming activity?

Replace writer-level WithTextFormat with per-chunk overloads:
- Add AppendResponseAsync(MessageActivityInput) honoring Text + TextFormat
- Add SendInformativeUpdateAsync(string, TextFormat) for per-informative format
- Track last-streamed format for the final message's default (explicit final wins)

Also update the StreamingBot sample: make Azure OpenAI optional (canned
streamed fallback when unconfigured), demonstrate the per-chunk API in the
extended-markdown path, and simplify to a single 'extended markdown' command.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf9dec16-4ade-483a-b60f-88d651e345f4
@singhk97

Copy link
Copy Markdown
Collaborator

I attest that I have verified

.AsIChatClient());
// Azure OpenAI is optional. When all three settings are present the default message path streams
// live model output; otherwise it falls back to a canned streamed response so the sample still runs.
string? endpoint = builder.Configuration["AzureOpenAI:Endpoint"];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated sample to not require aoai keys

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf9dec16-4ade-483a-b60f-88d651e345f4
@singhk97

Copy link
Copy Markdown
Collaborator

I attest that I have verified

_sequence++;
_logger.LogDebug("Sending informative streaming update (sequence {Sequence}).", _sequence);
SendActivityResponse? response = await TrySendChunkAsync(BuildActivity(text, StreamTypes.Informative), cancellationToken).ConfigureAwait(false);
SendActivityResponse? response = await TrySendChunkAsync(BuildActivity(text, StreamTypes.Informative, textFormat), cancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TrySendChunkAsync doesn't seem modified

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants