Table of Contents

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

bool

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

bool

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

int

MessageIds

Gets the ids of all messages currently in the transcript, in display order.

public IReadOnlyList<ChatMessageId> MessageIds { get; }

Property Value

IReadOnlyList<ChatMessageId>

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

Color?

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

bool

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

char

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

int

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

bool

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

SpinnerStyle

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

id ChatMessageId

The target message id.

action ChatMessageAction

The 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

role ChatRole

The role of the message author.

content string

The initial message content (markdown or plain text per the role style).

author string

An optional author name that overrides the role's default header label.

thinking bool

When 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

role ChatRole

The role of the message author.

content string

The initial message content (markdown or plain text per the role style).

author string

An optional author name that overrides the role's default header label.

actions IEnumerable<ChatMessageAction>

Explicit footer actions for this message. When non-null these override the role's DefaultActions; when null the role defaults are kept.

status ChatMessageStatus

An optional initial status row for this message. null adds 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

id ChatMessageId

The target message id.

token string

The 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

token string

The 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

id ChatMessageId

The 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

id ChatMessageId

The 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

id ChatMessageId

The message id.

Returns

ChatRole

The message's ChatRole.

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

role ChatRole

The 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

id ChatMessageId

The message id.

Returns

bool

true when the message body is visible; false when 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

id ChatMessageId

The message id.

Returns

bool

true while the message is thinking (no content yet); otherwise false.

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

buffer CharacterBuffer
bounds LayoutRect
clipRect LayoutRect
defaultFg Color
defaultBg Color

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

id ChatMessageId

The target message id.

actionId string

The 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

id ChatMessageId

The 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

id ChatMessageId

The target message id.

actionId string

The Id of the action(s) to toggle.

enabled bool

The 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

id ChatMessageId

The target message id.

actionId string

The id of the toggle action to update.

pressed bool

The 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

id ChatMessageId

The target message id.

actions IEnumerable<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

id ChatMessageId

The message id.

compact bool

true to 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

id ChatMessageId

The message id.

expanded bool

true to expand; false to 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

id ChatMessageId

The message id.

header string

The 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

role ChatRole

The role whose style is being set.

style ChatRoleStyle

The 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

id ChatMessageId

The target message id.

status ChatMessageStatus

The 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

id ChatMessageId

The target message id.

text string

The status text.

severity NotificationSeverity

Optional 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

id ChatMessageId

The target message id.

content string

The 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

EventHandler<ChatActionEventArgs>

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

Event Type

EventHandler<ChatActionToggledEventArgs>