You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Images pasted into the queue composer and images returned by chat providers are already persisted into MongoDB as inline base64 bytes on DataContent items inside serialized ChatMessage payloads. However, the HTML agent-chat view renders those items as a plain text media-type label (e.g. image/png) — the user never actually sees the image. This enhancement adds a small clickable image preview inside the chat HTML by (1) introducing a new HTTP image-streaming endpoint on the in-app web host that reads the bytes back from the persistence store, and (2) updating the HTML renderer to emit an <img> tag pointing at that endpoint (rather than inlining a giant base64 data URI into the HTML). The same design covers both user-pasted images and provider-generated images because both flow through identical AIContent / persistence / rendering paths.
Ingest (user paste).QueueComposerControl.axaml.cs:314-318 captures the pasted bitmap as bytes and calls vm.AppendImageAttachment(bytes, "image/png", w, h). QueueComposerViewModel.cs:164 wraps that into new DataContent(imageData, mediaType) and appends it to a List<AIContent> attached to the user's message.
Persistence (write).Phantom.Workspaces.Data.MongoDB/MongoDbAgentPersistenceStore.cs:~99 (inside StoreAsync) serializes the entire ChatMessage (including all AIContent items — text, DataContent, function-call/result, etc.) via JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions) and stores it as a BsonDocument on MongoDbPersistedMessageDocument with { AgentSessionId, Sequence, Payload }:
Persistence (read).MongoDbAgentPersistenceStore.ReadMessagesAsync (around :165-182) selects by AgentSessionId, sorts by Sequence, and deserializes each payload back into a ChatMessage[]. This is the authoritative source of persisted image bytes.
Web host & routing.Phantom.Workspaces/Services/WorkspacesWebHost.cs:84-90 and Phantom.Workspaces.Web.Server/Program.cs:53-59 both compose the same route surface via extension methods: MapWebDataAccessEndpoints, MapAgentEndpoints, MapTransportReverseEndpoints, MapWorkspaceToolEndpoints, MapAgentPersistenceEndpoints (the last of which currently exposes POST /agent/persistence/store, .../restore, .../messages, .../sub-agent-links/add, .../sub-agent-links/read — see AgentPersistenceEndpointRouteBuilderExtensions.cs:106-132).
HTML rendering. The chat WebView loads embedded Phantom.Workspaces.Agent.Gui/Assets/chat-output-shell.html from AgentChatOutputControl.axaml.cs:189-199 and receives per-content HTML fragments through IChatOutputHtmlSink.UpdateContent(path, location, content) (AgentChatOutputControl.axaml.cs:128-130). The switch on AIContent type lives in Phantom.Workspaces.Agent.Gui/ViewModels/DocumentModels/ChatOutputHtmlRenderer.cs in RenderContent(...) around :439-508. Today the DataContent case (:497-500) only renders the media-type as a text label:
Content ids are already stable within a rendered session: ChatOutputHtmlRenderer.MessageId(historyIndex) => "history-{i}" and ContentId(messageId, subIndex) => "{messageId}-{subIndex}" (:60, :68).
Answers to the design questions
Q1 — Identity: does DataContent uniquely identify itself?
No.Microsoft.Extensions.AI.DataContent exposes only Data (ReadOnlyMemory<byte>) and MediaType (string?); there is no Id, no Name, no annotation field on the base AIContent, and ChatMessage.MessageId is never set or persisted anywhere in the codebase (grep confirms zero assignments). Confirmed via ChatHistoryItemViewModel.cs:140-141 (reads Data, MediaType), CopilotSdkChatClient.cs:1043-1050 (same), and ChatOutputHtmlRenderer.cs:523 (uses a hash-like tuple "data:{MediaType}\^A{Data.Length}" purely for streaming dedup).
AgentSessionId is the session partition key on MongoDbPersistedMessageDocument,
Sequence is the monotonically increasing per-session integer already assigned at write time (MongoDbPersistedMessageDocument.Sequence, long),
ContentIndex is the position of the DataContent inside ChatMessage.Contents.
This tuple is stable under reloads because Mongo owns Sequence and content order within a single stored message is fixed by serialization. Optional hardening: append a short content fingerprint sha256(Data)[0..8] as a query-string parameter (?v=…) so the browser cache is invalidated if a message is ever edited to swap image bytes at the same index; the server MAY validate that the requested fingerprint matches the resolved bytes and 404 otherwise.
Q2 — Retrieval source and endpoint
Recommendation: read from IAgentPersistenceStore (Mongo) rather than AgentChatSessionCache / AgentChatHistoryCollection.
Rationale:
AgentChatSessionCache (Phantom.Workspaces.Llm.Core/AgentChatSessionCache.cs:20-35, registered as a singleton at Phantom.Workspaces.Web.Server/Program.cs:38) only holds liveAgentChat objects for currently-open sessions. After a reload, restart, or when opening a historical session from a different host process, the cache miss would mean image tags in the HTML break.
The persistence store is the durable source of truth. ReadMessagesAsync(ReadMessagesRequest) already exists (MongoDbAgentPersistenceStore.cs:165-182) and returns ChatMessage[] sorted by Sequence, so ContentIndex on a given Sequence is well-defined.
The persistence store is already reachable from web endpoints — see AgentPersistenceEndpointRouteBuilderExtensions.cs:110-113 (RequestServices.GetService<IAgentPersistenceStore>()).
Proposed endpoint — add to AgentPersistenceEndpointRouteBuilderExtensions so it is picked up by both WorkspacesWebHost and Phantom.Workspaces.Web.Server (both call MapAgentPersistenceEndpoints()):
GET /agent/persistence/{sessionId}/message/{sequence:long}/content/{contentIndex:int}/image
Response: raw image bytes, Content-Type set from DataContent.MediaType (fallback application/octet-stream), Cache-Control: private, max-age=…, plus Content-Length. 404 if the message/index does not exist or the content at that index is not a DataContent image. Reject on missing/whitespace sessionId.
Optimization note (non-blocking): a first cut can call the existing ReadMessagesAsync and index into the array; a follow-up may introduce ReadMessageBySequenceAsync(sessionId, sequence) on IAgentPersistenceStore to avoid deserializing the whole session for a single <img>.
Q3 — HTML chat view rendering
Change the DataContent image branch in ChatOutputHtmlRenderer.RenderContent (:497-500) so that when IsImageMediaType(data.MediaType) is true and rendering context includes the AgentSessionId + current message Sequence + contentIndex, it emits:
Click-to-full is handled by the existing anchor-click interceptor already exercised by AgentChatOutputControlTests.ChatOutputShellHtml_ContainsAnchorClickInterceptor (AgentChatOutputControlTests.cs:23), which routes anchor clicks through the WebView's URL-open path (raising UrlNavigationRequested). No new JavaScript is required for the MVP; a follow-up may add an in-shell lightbox overlay.
Explicitly avoided: inlining the base64 bytes into the HTML fragment. The current wire size for a single 4K-screenshot paste can exceed 5–10 MB; multiplying by every render/replace via IChatOutputHtmlSink.UpdateContent and by DOM parsing time in the WebView is prohibitive. The endpoint keeps HTML fragments small (a <img src="…"> URL) and streams the bytes once, cached by the WebView.
Threading identity into the renderer.RenderContent currently takes only contentId and the AIContent. ChatMessageHtmlModel.Render (ChatOutputHtmlModels.cs:195-210) already knows the message index; we thread AgentSessionId (already available from the owning session) and the message's Sequence (from AgentChatHistoryItem / the persistence write path — new field or lookup) through to RenderContent, and pass the loop subIndex as contentIndex. If Sequence is not yet available on the in-memory history item, add it to AgentChatHistoryItem at message-store time.
Q4 — Generated (assistant) images
Confirmed: ChatMessageHtmlModel.Render (ChatOutputHtmlModels.cs:201-210) uses Role only to pick styling flags (isDiagnostic, isHelp) — it does not filter by content type. Every AIContent on every role runs through the same ChatOutputHtmlRenderer.RenderContent switch, and MongoDbAgentPersistenceStore.StoreAsync serializes assistant ChatMessages the same way as user messages. Therefore provider-generated DataContent images in assistant messages are covered by the same identity scheme and the same endpoint. The only observable difference is the message Role label around the fragment; the image branch itself is role-agnostic.
Endpoint is picked up automatically via application.MapAgentPersistenceEndpoints() chain (adjacent to :84-90 composition — extend the same call if not already invoked).
Design / Fix
Content-identity scheme
(AgentSessionId, Sequence, ContentIndex). AgentSessionId is the existing session key; Sequence is MongoDbPersistedMessageDocument.Sequence assigned at write; ContentIndex is the position of the DataContent within ChatMessage.Contents. Optional ?v={sha256(Data)[..16]} for cache-busting and integrity checks.
New HTTP image endpoint (sketch)
endpointRouteBuilder.MapGet("/agent/persistence/{sessionId}/message/{sequence:long}/content/{contentIndex:int}/image",async(HttpContexthttp,stringsessionId,longsequence,intcontentIndex)=>{if(string.IsNullOrWhiteSpace(sessionId))returnResults.BadRequest();varstore=http.RequestServices.GetService<IAgentPersistenceStore>();if(storeisnull)returnResults.StatusCode(StatusCodes.Status503ServiceUnavailable);// MVP: single-message read via existing ReadMessagesAsync; follow-up: ReadMessageBySequenceAsync.varmessages=awaitstore.ReadMessagesAsync(newReadMessagesRequest{AgentSessionId=sessionId},http.RequestAborted).ConfigureAwait(false);if(sequence<0||sequence>=messages.Length)returnResults.NotFound();varmsg=messages[sequence];if(contentIndex<0||contentIndex>=msg.Contents.Count)returnResults.NotFound();if(msg.Contents[contentIndex]is not DataContentdata||string.IsNullOrEmpty(data.MediaType)||!data.MediaType.StartsWith("image/",StringComparison.OrdinalIgnoreCase)){returnResults.NotFound();}http.Response.Headers.CacheControl="private, max-age=3600";returnResults.File(fileContents:data.Data.ToArray(),contentType:data.MediaType,fileDownloadName:null,enableRangeProcessing:false);});
Notes:
Content-Type is taken directly from DataContent.MediaType (already validated to start with image/).
Base64-in-HTML avoidance. Do NOT emit src="data:image/png;base64,…" — pasted screenshots are routinely multi-megabyte, and IChatOutputHtmlSink.UpdateContent re-marshals the entire fragment string across the WebView boundary on every change. Using an HTTP URL keeps the HTML fragment small, lets the WebView cache, and defers the payload to a background GET.
Coverage for generated images. Because ChatMessageHtmlModel.Render (ChatOutputHtmlModels.cs:201-210) invokes RenderContent for every AIContent regardless of Role, and MongoDbAgentPersistenceStore.StoreAsync persists assistant messages the same way, provider-generated DataContent items land on this same branch with no additional code.
Expected Tests
Class names and naming style mirror existing suites (Subject_Scenario_ExpectedOutcome, per AgentChatOutputControlTests, RunningSubAgentsHtmlTransformerTests, WebAgentPersistenceStoreTests, AgentPersistenceEndpointTests).
Click on the <a class="chat-image-link"> triggers the existing anchor interceptor, delivering the endpoint URL through UrlNavigationRequested (companion to existing AgentChatOutputControl_OpenUrlMessage_RaisesUrlNavigationRequested).
Preview persisted image
DataContentin the agent-chat HTML view via a hosted image endpointImplementation Sub-Items
Complete in this order:
DataContentby(AgentSessionId, Sequence, ContentIndex)(no dependencies)GET /agent/persistence/{sessionId}/message/{sequence}/content/{contentIndex}/imageendpoint (depends on Persistence: read-by-index for image DataContent by (AgentSessionId, Sequence, ContentIndex) #1217)DataContentas<img>pointing at hosted image endpoint (depends on Web host: GET /agent/persistence/{sessionId}/message/{sequence}/content/{contentIndex}/image endpoint #1218)Summary
Images pasted into the queue composer and images returned by chat providers are already persisted into MongoDB as inline base64 bytes on
DataContentitems inside serializedChatMessagepayloads. However, the HTML agent-chat view renders those items as a plain text media-type label (e.g.image/png) — the user never actually sees the image. This enhancement adds a small clickable image preview inside the chat HTML by (1) introducing a new HTTP image-streaming endpoint on the in-app web host that reads the bytes back from the persistence store, and (2) updating the HTML renderer to emit an<img>tag pointing at that endpoint (rather than inlining a giant base64 data URI into the HTML). The same design covers both user-pasted images and provider-generated images because both flow through identicalAIContent/ persistence / rendering paths.Cross-references:
/,/data/*, agent endpoints onWorkspacesWebHostandPhantom.Workspaces.Web.Server). The new image endpoint is registered at the same seam.RunningSubAgentsHtmlTransformer,ChatOutputHtmlRenderer, andchat-output-shell.html. The rendering change lives in the same pipeline.Design Context / Current Behavior
Ingest (user paste).
QueueComposerControl.axaml.cs:314-318captures the pasted bitmap as bytes and callsvm.AppendImageAttachment(bytes, "image/png", w, h).QueueComposerViewModel.cs:164wraps that intonew DataContent(imageData, mediaType)and appends it to aList<AIContent>attached to the user's message.Persistence (write).
Phantom.Workspaces.Data.MongoDB/MongoDbAgentPersistenceStore.cs:~99(insideStoreAsync) serializes the entireChatMessage(including allAIContentitems — text,DataContent, function-call/result, etc.) viaJsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions)and stores it as aBsonDocumentonMongoDbPersistedMessageDocumentwith{ AgentSessionId, Sequence, Payload }:Persistence (read).
MongoDbAgentPersistenceStore.ReadMessagesAsync(around:165-182) selects byAgentSessionId, sorts bySequence, and deserializes each payload back into aChatMessage[]. This is the authoritative source of persisted image bytes.Web host & routing.
Phantom.Workspaces/Services/WorkspacesWebHost.cs:84-90andPhantom.Workspaces.Web.Server/Program.cs:53-59both compose the same route surface via extension methods:MapWebDataAccessEndpoints,MapAgentEndpoints,MapTransportReverseEndpoints,MapWorkspaceToolEndpoints,MapAgentPersistenceEndpoints(the last of which currently exposesPOST /agent/persistence/store,.../restore,.../messages,.../sub-agent-links/add,.../sub-agent-links/read— seeAgentPersistenceEndpointRouteBuilderExtensions.cs:106-132).HTML rendering. The chat WebView loads embedded
Phantom.Workspaces.Agent.Gui/Assets/chat-output-shell.htmlfromAgentChatOutputControl.axaml.cs:189-199and receives per-content HTML fragments throughIChatOutputHtmlSink.UpdateContent(path, location, content)(AgentChatOutputControl.axaml.cs:128-130). The switch onAIContenttype lives inPhantom.Workspaces.Agent.Gui/ViewModels/DocumentModels/ChatOutputHtmlRenderer.csinRenderContent(...)around:439-508. Today theDataContentcase (:497-500) only renders the media-type as a text label:Content ids are already stable within a rendered session:
ChatOutputHtmlRenderer.MessageId(historyIndex) => "history-{i}"andContentId(messageId, subIndex) => "{messageId}-{subIndex}"(:60,:68).Answers to the design questions
Q1 — Identity: does
DataContentuniquely identify itself?No.
Microsoft.Extensions.AI.DataContentexposes onlyData(ReadOnlyMemory<byte>) andMediaType(string?); there is noId, noName, no annotation field on the baseAIContent, andChatMessage.MessageIdis never set or persisted anywhere in the codebase (grep confirms zero assignments). Confirmed viaChatHistoryItemViewModel.cs:140-141(readsData,MediaType),CopilotSdkChatClient.cs:1043-1050(same), andChatOutputHtmlRenderer.cs:523(uses a hash-like tuple"data:{MediaType}\^A{Data.Length}"purely for streaming dedup).Proposed stable composite key (persistence-anchored):
AgentSessionId + Sequence + ContentIndex, where:AgentSessionIdis the session partition key onMongoDbPersistedMessageDocument,Sequenceis the monotonically increasing per-session integer already assigned at write time (MongoDbPersistedMessageDocument.Sequence,long),ContentIndexis the position of theDataContentinsideChatMessage.Contents.This tuple is stable under reloads because Mongo owns
Sequenceand content order within a single stored message is fixed by serialization. Optional hardening: append a short content fingerprintsha256(Data)[0..8]as a query-string parameter (?v=…) so the browser cache is invalidated if a message is ever edited to swap image bytes at the same index; the server MAY validate that the requested fingerprint matches the resolved bytes and 404 otherwise.Q2 — Retrieval source and endpoint
Recommendation: read from
IAgentPersistenceStore(Mongo) rather thanAgentChatSessionCache/AgentChatHistoryCollection.Rationale:
AgentChatSessionCache(Phantom.Workspaces.Llm.Core/AgentChatSessionCache.cs:20-35, registered as a singleton atPhantom.Workspaces.Web.Server/Program.cs:38) only holds liveAgentChatobjects for currently-open sessions. After a reload, restart, or when opening a historical session from a different host process, the cache miss would mean image tags in the HTML break.ReadMessagesAsync(ReadMessagesRequest)already exists (MongoDbAgentPersistenceStore.cs:165-182) and returnsChatMessage[]sorted bySequence, soContentIndexon a givenSequenceis well-defined.AgentPersistenceEndpointRouteBuilderExtensions.cs:110-113(RequestServices.GetService<IAgentPersistenceStore>()).Proposed endpoint — add to
AgentPersistenceEndpointRouteBuilderExtensionsso it is picked up by bothWorkspacesWebHostandPhantom.Workspaces.Web.Server(both callMapAgentPersistenceEndpoints()):Response: raw image bytes,
Content-Typeset fromDataContent.MediaType(fallbackapplication/octet-stream),Cache-Control: private, max-age=…, plusContent-Length. 404 if the message/index does not exist or the content at that index is not aDataContentimage. Reject on missing/whitespacesessionId.Optimization note (non-blocking): a first cut can call the existing
ReadMessagesAsyncand index into the array; a follow-up may introduceReadMessageBySequenceAsync(sessionId, sequence)onIAgentPersistenceStoreto avoid deserializing the whole session for a single<img>.Q3 — HTML chat view rendering
Change the
DataContentimage branch inChatOutputHtmlRenderer.RenderContent(:497-500) so that whenIsImageMediaType(data.MediaType)is true and rendering context includes theAgentSessionId+ current messageSequence+contentIndex, it emits:with CSS in
chat-output-shell.html:Click-to-full is handled by the existing anchor-click interceptor already exercised by
AgentChatOutputControlTests.ChatOutputShellHtml_ContainsAnchorClickInterceptor(AgentChatOutputControlTests.cs:23), which routes anchor clicks through the WebView's URL-open path (raisingUrlNavigationRequested). No new JavaScript is required for the MVP; a follow-up may add an in-shell lightbox overlay.Explicitly avoided: inlining the base64 bytes into the HTML fragment. The current wire size for a single 4K-screenshot paste can exceed 5–10 MB; multiplying by every render/replace via
IChatOutputHtmlSink.UpdateContentand by DOM parsing time in the WebView is prohibitive. The endpoint keeps HTML fragments small (a<img src="…">URL) and streams the bytes once, cached by the WebView.Threading identity into the renderer.
RenderContentcurrently takes onlycontentIdand theAIContent.ChatMessageHtmlModel.Render(ChatOutputHtmlModels.cs:195-210) already knows the message index; we threadAgentSessionId(already available from the owning session) and the message'sSequence(fromAgentChatHistoryItem/ the persistence write path — new field or lookup) through toRenderContent, and pass the loopsubIndexascontentIndex. IfSequenceis not yet available on the in-memory history item, add it toAgentChatHistoryItemat message-store time.Q4 — Generated (assistant) images
Confirmed:
ChatMessageHtmlModel.Render(ChatOutputHtmlModels.cs:201-210) usesRoleonly to pick styling flags (isDiagnostic,isHelp) — it does not filter by content type. EveryAIContenton every role runs through the sameChatOutputHtmlRenderer.RenderContentswitch, andMongoDbAgentPersistenceStore.StoreAsyncserializes assistantChatMessages the same way as user messages. Therefore provider-generatedDataContentimages in assistant messages are covered by the same identity scheme and the same endpoint. The only observable difference is the messageRolelabel around the fragment; the image branch itself is role-agnostic.Affected Files
features/Phantom.Workspaces.Web.Server/AgentPersistenceEndpointRouteBuilderExtensions.csGET /agent/persistence/{sessionId}/message/{sequence}/content/{contentIndex}/imageendpoint.features/Phantom.Workspaces.Llm.Core/IAgentPersistenceStore.cs(or sibling helper)ReadMessageBySequenceAsyncfor single-message reads; MVP can compose from existingReadMessagesAsync.features/Phantom.Workspaces.Data.MongoDB/MongoDbAgentPersistenceStore.csReadMessageBySequenceAsync(BSON filter onAgentSessionId + Sequence).features/Phantom.Workspaces.Agent.Gui/ViewModels/DocumentModels/ChatOutputHtmlRenderer.csRenderContentsignature to acceptagentSessionId+messageSequence; changeDataContentimage branch (:497-500) to emit<a><img src="…"></a>.features/Phantom.Workspaces.Agent.Gui/ViewModels/DocumentModels/ChatOutputHtmlModels.csagentSessionId+ messageSequencefromChatMessageHtmlModel.Render(:195-210) intoRenderContent.features/Phantom.Workspaces.Llm.Core/AgentChatHistoryItem.cslong? Sequence { get; init; }so the in-memory item knows its persistence key.features/Phantom.Workspaces.Agent.Gui/Assets/chat-output-shell.html.chat-image-preview/.chat-image-linkCSS (max-width/max-height 240px, cursor: zoom-in).features/Phantom.Workspaces.Web.Server/Program.cs(existing wiring)app.MapAgentPersistenceEndpoints()(:59).features/Phantom.Workspaces/Services/WorkspacesWebHost.csapplication.MapAgentPersistenceEndpoints()chain (adjacent to:84-90composition — extend the same call if not already invoked).Design / Fix
Content-identity scheme
(AgentSessionId, Sequence, ContentIndex).AgentSessionIdis the existing session key;SequenceisMongoDbPersistedMessageDocument.Sequenceassigned at write;ContentIndexis the position of theDataContentwithinChatMessage.Contents. Optional?v={sha256(Data)[..16]}for cache-busting and integrity checks.New HTTP image endpoint (sketch)
Notes:
Content-Typeis taken directly fromDataContent.MediaType(already validated to start withimage/).AgentPersistenceEndpointRouteBuilderExtensions.MapAgentPersistenceEndpointsmeans both hosts (WorkspacesWebHostandPhantom.Workspaces.Web.Server) expose the route automatically — see WorkspacesWebHost missing /transport/connect mapping breaks reverse-HTTP hub registration (404 instead of 101) #1209.IAgentPersistenceStore(notAgentChatSessionCache) so it survives reload and works for closed sessions.HTML renderer change (sketch)
ChatOutputHtmlRenderer.RenderContentgainsstring agentSessionId, long messageSequence, int contentIndex(already implicit from the caller loop):CSS added to
chat-output-shell.html:Base64-in-HTML avoidance. Do NOT emit
src="data:image/png;base64,…"— pasted screenshots are routinely multi-megabyte, andIChatOutputHtmlSink.UpdateContentre-marshals the entire fragment string across the WebView boundary on every change. Using an HTTP URL keeps the HTML fragment small, lets the WebView cache, and defers the payload to a background GET.Coverage for generated images. Because
ChatMessageHtmlModel.Render(ChatOutputHtmlModels.cs:201-210) invokesRenderContentfor everyAIContentregardless ofRole, andMongoDbAgentPersistenceStore.StoreAsyncpersists assistant messages the same way, provider-generatedDataContentitems land on this same branch with no additional code.Expected Tests
Class names and naming style mirror existing suites (
Subject_Scenario_ExpectedOutcome, perAgentChatOutputControlTests,RunningSubAgentsHtmlTransformerTests,WebAgentPersistenceStoreTests,AgentPersistenceEndpointTests).AgentImageEndpoint_WhenRequestedByMessageAndIndex_ReturnsImageBytesWithMediaTypeAgentPersistenceEndpointTestsDataContent.Data,Content-TypeequalsDataContent.MediaType.AgentImageEndpoint_WhenSequenceOutOfRange_Returns404AgentPersistenceEndpointTestssequence→ 404.AgentImageEndpoint_WhenContentIndexOutOfRange_Returns404AgentPersistenceEndpointTestsAgentImageEndpoint_WhenContentIsNotImage_Returns404AgentPersistenceEndpointTestsDataContentwith non-image media type orTextContentat index → 404.AgentImageEndpoint_WhenPersistenceStoreUnavailable_Returns503AgentPersistenceEndpointTestsStoreAsync_WhenServerReturns503_ThrowsHttpRequestExceptionshape.AgentImageEndpoint_WhenSessionIdMissing_Returns400AgentPersistenceEndpointTestssessionIdroute param → 400.ReadMessagesAsync_WithImageDataContent_RoundTripsBytesAndMediaTypeWebAgentPersistenceStoreTestsReadMessagesAsync_WithComplexMessageContents_RoundTripwithDataContent(image/png)payload.MongoDbAgentPersistenceStore_ReadMessageBySequence_ReturnsSingleMessageMongoDbAgentPersistenceStoreSlowTestsAgentSessionId + Sequence.ChatHtml_WhenMessageHasImageContent_EmitsImgTagPointingAtHostEndpointChatOutputHtmlRendererTests(new) orAgentChatOutputControlTests<img class="chat-image-preview" src="/agent/persistence/{sessionId}/message/{seq}/content/{idx}/image".ChatHtml_WhenMessageHasImageContent_WrapsImgInAnchorForFullSize<a class="chat-image-link" href="{url}">.ChatHtml_WhenMessageHasImageContent_DoesNotInlineBase64data:image/or the base64 payload.ChatHtml_WhenAssistantGeneratesImage_RendersPreviewChatRole.Assistant+DataContent(image/png), same<img>output is produced (role-agnostic).ChatHtml_WhenDataContentIsNonImage_RendersMonospaceLabelDataContentstill uses the existing[mediaType]monospace branch.ChatOutputShellHtml_HasImagePreviewMaxSizeCssAgentChatOutputControlTests.chat-image-preview { max-width: 240px; max-height: 240px; ... }.AgentChatOutputControl_ImagePreviewClick_RaisesUrlNavigationRequestedAgentChatOutputControlTests<a class="chat-image-link">triggers the existing anchor interceptor, delivering the endpoint URL throughUrlNavigationRequested(companion to existingAgentChatOutputControl_OpenUrlMessage_RaisesUrlNavigationRequested).