Class ChatTranscriptControl
- Namespace
- SharpConsoleUI.Controls
- Assembly
- SharpConsoleUI.dll
A chat transcript control that presents an ordered list of role-tagged messages, with per-message streaming (token append), collapsible verbose roles, thinking indicators and themed per-role styling.
public class ChatTranscriptControl : ScrollablePanelControl, IDOMPaintable, INotifyPropertyChanged, IInteractiveControl, IFocusableControl, IMouseAwareControl, IWindowControl, IDisposable, IContainer, IContainerControl, IControlHost, IScrollableContainer, IFocusScope, ILogicalCursorProvider, IColorRoleableControl
- Inheritance
-
ChatTranscriptControl
- Implements
- Inherited Members
- Extension Methods
Remarks
ChatTranscriptControl is an honest composition: it subclasses ScrollablePanelControl and hosts one real CollapsiblePanel per message, each containing a MarkupControl body (or a started SpinnerControl while a message is "thinking"). Scrolling, wrapping, markdown rendering, selection and the collapse animation are all provided by those child controls — none of it is re-implemented here.
Auto-scroll stickiness (following the newest content while pinned to the bottom) is inherited from the base AutoScroll flag, which this control enables by default.
Constructors
ChatTranscriptControl()
Initialises a new ChatTranscriptControl with themed per-role defaults and auto-scroll enabled (so the transcript stays pinned to the newest message while at the bottom).
public ChatTranscriptControl()
Properties
AnimateMessages
Gets or sets whether message panels animate their expand/collapse (height tween). When
true (the default) collapsible message panels use
Height; otherwise None.
Only affects messages added after the value changes.
public bool AnimateMessages { get; set; }
Property Value
CollapsedPreview
Gets or sets whether a collapsed message shows a one-line "peek" preview row directly below its
header. When true (the default), each collapsed message renders the first line of its hidden
content — faded left→right into a dim, clickable expand… cue — as a sibling row that clicking
expands the message. When false, collapsed messages show only their header.
public bool CollapsedPreview { get; set; }
Property Value
Remarks
The peek row is a real child MarkupControl inserted after the message panel; it is added and removed automatically as the panel collapses and expands. Changing this value affects messages that collapse/expand afterwards; it does not retroactively add or remove peek rows on already-collapsed messages.
CollapsedPreviewFadeWidth
Gets or sets the number of trailing columns over which a collapsed-message peek row fades its
foreground from opaque to transparent before the dim expand… cue. Defaults to
ChatCollapsedPreviewFadeWidth (10). Affects peek rows built afterwards.
public int CollapsedPreviewFadeWidth { get; set; }
Property Value
MessageIds
Gets the ids of all messages currently in the transcript, in display order.
public IReadOnlyList<ChatMessageId> MessageIds { get; }
Property Value
MessageRailColor
Gets or sets an explicit rail color. When null (the default), the rail derives a dimmed
version of the message's role color.
public Color? MessageRailColor { get; set; }
Property Value
MessageRailEnabled
Gets or sets whether a message that has a footer shows a dim role-tinted left rail down its
body and footer. Defaults to true. The rail is footer-gated: plain (no-footer) messages
are unaffected.
public bool MessageRailEnabled { get; set; }
Property Value
MessageRailGlyph
Gets or sets the glyph painted down a railed message's left gutter. Defaults to
'│' (U+2502).
public char MessageRailGlyph { get; set; }
Property Value
MessageRailGutterWidth
Gets or sets the reserved left gutter width, in columns, for a railed message (rail glyph plus
gap). Defaults to 2.
public int MessageRailGutterWidth { get; set; }
Property Value
MessagesSelectable
Gets or sets the control-level baseline controlling whether message bodies in this transcript
can be selected (and, with copy enabled, copied). Defaults to true. Each message resolves
its selectability as role.Selectable ?? MessagesSelectable: a role whose
Selectable is null inherits this baseline, while a role that
sets it to true or false overrides the baseline in either direction. Changing this
value updates existing message bodies (respecting each role's override) as well as any added
afterwards.
public bool MessagesSelectable { get; set; }
Property Value
ThinkingSpinnerStyle
Gets or sets the spinner style used for thinking messages. Defaults to Dots. Only affects thinking messages added after the change.
public SpinnerStyle ThinkingSpinnerStyle { get; set; }
Property Value
Methods
AddAction(ChatMessageId, ChatMessageAction)
Appends a single action to a message's actions row, creating the row if needed. No-op if the id is unknown.
public void AddAction(ChatMessageId id, ChatMessageAction action)
Parameters
idChatMessageIdThe target message id.
actionChatMessageActionThe action to append.
AddMessage(ChatRole, string, string?, bool)
Adds a message to the transcript and returns its id.
public ChatMessageId AddMessage(ChatRole role, string content, string? author = null, bool thinking = false)
Parameters
roleChatRoleThe role of the message author.
contentstringThe initial message content (markdown or plain text per the role style).
authorstringAn optional author name that overrides the role's default header label.
thinkingboolWhen
true, the message initially shows a spinner (a "thinking" indicator) instead of a text body. The spinner is cleared automatically on the first Append(ChatMessageId, string) or UpdateMessage(ChatMessageId, string) for the message.
Returns
- ChatMessageId
The id of the newly added message.
Remarks
This mutates the control's children and MUST be called on the UI thread. When streaming from
a background thread (e.g. an agent producing tokens off-thread), marshal the call via
windowSystem.EnqueueOnUIThread(() => chat.AddMessage(...)) — see CLAUDE.md Rule 13.
AddMessage(ChatRole, string, string?, IEnumerable<ChatMessageAction>?, ChatMessageStatus?)
Adds a new message and seeds its footer actions and/or status row at creation time.
public ChatMessageId AddMessage(ChatRole role, string content, string? author, IEnumerable<ChatMessageAction>? actions, ChatMessageStatus? status)
Parameters
roleChatRoleThe role of the message author.
contentstringThe initial message content (markdown or plain text per the role style).
authorstringAn optional author name that overrides the role's default header label.
actionsIEnumerable<ChatMessageAction>Explicit footer actions for this message. When non-
nullthese override the role's DefaultActions; whennullthe role defaults are kept.statusChatMessageStatusAn optional initial status row for this message.
nulladds no status row.
Returns
- ChatMessageId
The id of the newly added message.
Remarks
This overload delegates to AddMessage(ChatRole, string, string?, bool) (which already
seeds the role's DefaultActions). When actions is non-null
it replaces those defaults; when null the role defaults are kept. When status is
non-null the message's status row is set.
Like the other reactive methods, this must be called on the UI thread; background callers should marshal via
windowSystem.EnqueueOnUIThread(...) — see CLAUDE.md Rule 13.
Append(ChatMessageId, string)
Appends a token/chunk to a specific message, growing its body. If the message was "thinking", the spinner is cleared and replaced by a text body on the first token.
public void Append(ChatMessageId id, string token)
Parameters
idChatMessageIdThe target message id.
tokenstringThe text chunk to append.
Remarks
Mutates child-control state and MUST run on the UI thread. Callers streaming tokens from a
background thread MUST marshal via windowSystem.EnqueueOnUIThread(() => chat.Append(id, ...))
(CLAUDE.md Rule 13).
Exceptions
- KeyNotFoundException
No message with the id exists.
Append(string)
Appends a token/chunk to the most recently added message. Convenience for the common single-message streaming case.
public void Append(string token)
Parameters
tokenstringThe text chunk to append.
Remarks
Mutates child-control state and MUST run on the UI thread. Callers streaming tokens from a
background thread MUST marshal via windowSystem.EnqueueOnUIThread(() => chat.Append(...))
(CLAUDE.md Rule 13).
Exceptions
- InvalidOperationException
The transcript is empty.
Clear()
Removes all messages from the transcript.
public void Clear()
ClearActions(ChatMessageId)
Removes a message's actions row entirely (and the footer, if no status row remains). No-op if the id is unknown.
public void ClearActions(ChatMessageId id)
Parameters
idChatMessageIdThe target message id.
ClearStatus(ChatMessageId)
Removes the status row from a message, and removes the footer if no other footer row (actions) remains. No-op if the id is unknown or the message has no status row.
public void ClearStatus(ChatMessageId id)
Parameters
idChatMessageIdThe target message id.
Remarks
Mutates the control's children and MUST run on the UI thread (see CLAUDE.md Rule 13).
GetRole(ChatMessageId)
Gets the role of the message with the given id.
public ChatRole GetRole(ChatMessageId id)
Parameters
idChatMessageIdThe message id.
Returns
Exceptions
- KeyNotFoundException
No message with the id exists.
GetRoleStyle(ChatRole)
Gets the visual style currently associated with the given role. Every role has a themed
default, so this never returns null.
public ChatRoleStyle GetRoleStyle(ChatRole role)
Parameters
roleChatRoleThe role whose style is requested.
Returns
- ChatRoleStyle
The ChatRoleStyle for the role.
IsExpanded(ChatMessageId)
Gets whether the message with the given id is currently expanded.
public bool IsExpanded(ChatMessageId id)
Parameters
idChatMessageIdThe message id.
Returns
- bool
truewhen the message body is visible;falsewhen it is collapsed.
Exceptions
- KeyNotFoundException
No message with the id exists.
IsThinking(ChatMessageId)
Gets whether the message with the given id is still showing its thinking indicator.
public bool IsThinking(ChatMessageId id)
Parameters
idChatMessageIdThe message id.
Returns
- bool
truewhile the message is thinking (no content yet); otherwisefalse.
Exceptions
- KeyNotFoundException
No message with the id exists.
PaintDOM(CharacterBuffer, LayoutRect, LayoutRect, Color, Color)
Paints the transcript chrome (via the base ScrollablePanelControl), then, when the message rail is enabled, overlays the dim role-tinted left rail down each footered message's body and footer. The rail is drawn AFTER the base paint (which arranges and paints the children), so it lands on top of the reserved gutter column the children were inset past.
public override void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipRect, Color defaultFg, Color defaultBg)
Parameters
bufferCharacterBufferboundsLayoutRectclipRectLayoutRectdefaultFgColordefaultBgColor
RemoveAction(ChatMessageId, string)
Removes the action(s) with the given action id from a message's actions row. Removes the row (and footer) if it becomes empty. No-op if the id is unknown.
public void RemoveAction(ChatMessageId id, string actionId)
Parameters
idChatMessageIdThe target message id.
actionIdstringThe Id of the action(s) to remove.
RemoveMessage(ChatMessageId)
Removes the message with the given id from the transcript. No-op if the id is unknown.
public void RemoveMessage(ChatMessageId id)
Parameters
idChatMessageIdThe message id to remove.
SetActionEnabled(ChatMessageId, string, bool)
Enables or disables the action(s) with the given action id, rebuilding the row. No-op if the id is unknown.
public void SetActionEnabled(ChatMessageId id, string actionId, bool enabled)
Parameters
idChatMessageIdThe target message id.
actionIdstringThe Id of the action(s) to toggle.
enabledboolThe new enabled state.
SetActionState(ChatMessageId, string, bool)
Sets a toggle action's pressed state programmatically. Restyles the button and raises ActionToggled, but does not run the action's click handler (unlike a real click) — this is a state restore, not a user gesture. No-op if the id or action id is unknown.
public void SetActionState(ChatMessageId id, string actionId, bool pressed)
Parameters
idChatMessageIdThe target message id.
actionIdstringThe id of the toggle action to update.
pressedboolThe new pressed state.
Remarks
Mutates the control's children (rebuilds the actions row) and MUST run on the UI thread (see CLAUDE.md Rule 13).
SetActions(ChatMessageId, IEnumerable<ChatMessageAction>)
Replaces the actions row of a message with the given set of actions, rebuilding the toolbar. An empty set removes the actions row (and the footer, if no status row remains). No-op if the id is unknown.
public void SetActions(ChatMessageId id, IEnumerable<ChatMessageAction> actions)
Parameters
idChatMessageIdThe target message id.
actionsIEnumerable<ChatMessageAction>The actions to display; an empty sequence clears the row.
Remarks
The actions row is a toolbar of buttons rendered as a sibling of the message panel, inserted immediately after it and above any status row. Mutates the control's children and MUST run on the UI thread (see CLAUDE.md Rule 13).
SetCompactFooter(ChatMessageId, bool)
Renders ONE message's footer compactly: no separator rule above the status/actions rows. The trailing blank line below them is KEPT — it separates consecutive rows.
public void SetCompactFooter(ChatMessageId id, bool compact)
Parameters
idChatMessageIdThe message id.
compactbooltrueto drop the rule and the trailing blank.
Remarks
For hosts that log many short, low-information rows — a per-step tool trace, a file list — where the divider and the trailing blank cost more vertical space than the row's content. A five-step sequence otherwise pays ten lines of chrome for five lines of substance.
Per-message rather than a property on the rows themselves, because
ApplyFooterSeparator and ApplyFooterSpacer re-derive both on every status
update: setting ShowAboveLine or a margin directly is silently overwritten on the
next SetStatus(ChatMessageId, string, NotificationSeverity?) call.
Off by default, so existing transcripts render unchanged. Apply it before or after setting a status; the next footer rebuild honours it either way.
Exceptions
- KeyNotFoundException
No message with the id exists.
SetExpanded(ChatMessageId, bool)
Expands or collapses ONE message, overriding its role's StartCollapsed default.
public void SetExpanded(ChatMessageId id, bool expanded)
Parameters
idChatMessageIdThe message id.
expandedbooltrueto expand;falseto collapse.
Remarks
Collapse state was configurable per ROLE but not per MESSAGE, and the two are not the same need. A role like "System" is usually worth collapsing — startup notes, diagnostics, noise — yet the occasional System line matters: a goal reporting that it finished, a warning the user must act on. Those arrived collapsed behind an "expand…" nobody opens, which made a finished run look identical to a stalled one.
The capability already existed internally: every message owns a
CollapsiblePanel whose IsExpanded is public and
settable, and this control already listens for its changes. Only the accessor was missing —
PanelForTest was marked test-only — so callers had no way to reach it.
Exceptions
- KeyNotFoundException
No message with the id exists.
SetHeader(ChatMessageId, string)
Replaces ONE message's header text after creation.
public void SetHeader(ChatMessageId id, string header)
Parameters
idChatMessageIdThe message id.
headerstringThe new header text. Markup is honoured.
Remarks
The header is otherwise composed once, when the message is added
(ComposeHeader), from the role style's factory and the author string — so a host
whose header should reflect CHANGING state (a step's status, its elapsed time, a live
spinner) had no way to express it and was forced into a separate status row below the
content.
Folding that state into the header is what turns a three-line step — header,
result, status — into two. Combined with an inline [spinner] tag the same header
carries the running indicator, so a step does not change shape when it finishes.
The text is used verbatim: the role style's header factory is NOT re-applied, since the caller supplying a full header already knows what it wants to say.
Exceptions
- KeyNotFoundException
No message with the id exists.
SetRoleStyle(ChatRole, ChatRoleStyle)
Sets the visual style used for messages of the given role.
public void SetRoleStyle(ChatRole role, ChatRoleStyle style)
Parameters
roleChatRoleThe role whose style is being set.
styleChatRoleStyleThe style to apply. Must not be
null.
Remarks
This affects messages added after the call only; already-added message panels keep the style they were built with and are not retroactively restyled. To restyle an existing conversation, set the role styles first and re-add the messages.
SetStatus(ChatMessageId, ChatMessageStatus)
Sets (or replaces) the status row for a message from a full ChatMessageStatus (text + severity plus optional left/center/right region items). No-op if the id is unknown.
public void SetStatus(ChatMessageId id, ChatMessageStatus status)
Parameters
idChatMessageIdThe target message id.
statusChatMessageStatusThe status content to display.
Remarks
The status row is rendered as a non-sticky, transparent StatusBarControl inserted as a sibling of the message panel (right after it), so it survives the message body collapsing. Mutates the control's children and MUST run on the UI thread (see CLAUDE.md Rule 13).
SetStatus(ChatMessageId, string, NotificationSeverity?)
Sets (or replaces) the status row for a message: a single left-aligned status line tinted by the optional severity. Creates the message's footer lazily if needed. No-op if the id is unknown.
public void SetStatus(ChatMessageId id, string text, NotificationSeverity? severity = null)
Parameters
idChatMessageIdThe target message id.
textstringThe status text.
severityNotificationSeverityOptional severity that tints the status text color.
Remarks
The status row is rendered as a non-sticky, transparent StatusBarControl inserted as a sibling of the message panel (right after it), so it survives the message body collapsing. Mutates the control's children and MUST run on the UI thread (see CLAUDE.md Rule 13).
UpdateMessage(ChatMessageId, string)
Replaces the entire body of a message with the given content. If the message was "thinking", the spinner is cleared first.
public void UpdateMessage(ChatMessageId id, string content)
Parameters
idChatMessageIdThe target message id.
contentstringThe new full content (markdown or plain text per the role style).
Remarks
Mutates child-control state and MUST run on the UI thread. Callers updating from a background
thread MUST marshal via windowSystem.EnqueueOnUIThread(() => chat.UpdateMessage(id, ...))
(CLAUDE.md Rule 13).
Exceptions
- KeyNotFoundException
No message with the id exists.
Events
ActionInvoked
Raised after a non-toggle message action has been dispatched (its click handler has run).
Toggle-variant actions raise ActionToggled instead.
public event EventHandler<ChatActionEventArgs>? ActionInvoked
Event Type
ActionToggled
Raised after a Toggle action's pressed state changes — whether via a click, a handler's SetPressed(bool), or a programmatic SetActionState(ChatMessageId, string, bool) call. The event args carry the new pressed state.
public event EventHandler<ChatActionToggledEventArgs>? ActionToggled