Mount a message widget to the messages area. This method also stores the message data and handles pruning when the widget count exceeds the maximum. If the `#messages` container is not present (e.g. the screen has been torn down during an interruption), the call is
(
self,
widget: Static | AssistantMessage | ToolCallMessage | SkillMessage,
)
| 11043 | ) |
| 11044 | |
| 11045 | async def _mount_message( |
| 11046 | self, |
| 11047 | widget: Static | AssistantMessage | ToolCallMessage | SkillMessage, |
| 11048 | ) -> None: |
| 11049 | """Mount a message widget to the messages area. |
| 11050 | |
| 11051 | This method also stores the message data and handles pruning |
| 11052 | when the widget count exceeds the maximum. |
| 11053 | |
| 11054 | If the `#messages` container is not present (e.g. the screen has |
| 11055 | been torn down during an interruption), the call is silently skipped |
| 11056 | to avoid cascading `NoMatches` errors. |
| 11057 | |
| 11058 | Args: |
| 11059 | widget: The message widget to mount |
| 11060 | """ |
| 11061 | try: |
| 11062 | messages = self.query_one("#messages", Container) |
| 11063 | except NoMatches: |
| 11064 | return |
| 11065 | |
| 11066 | # During shutdown (e.g. Ctrl+D mid-stream) the container may still |
| 11067 | # be in the DOM tree but already detached, so mount() would raise |
| 11068 | # MountError. Bail out silently — the app is exiting anyway. |
| 11069 | if not messages.is_attached: |
| 11070 | return |
| 11071 | |
| 11072 | if isinstance(widget, QueuedUserMessage): |
| 11073 | # Queued placeholders mount at the bottom and stay out of the |
| 11074 | # message store; drain remounts them as real UserMessage widgets. |
| 11075 | await messages.mount(widget) |
| 11076 | try: |
| 11077 | input_container = self.query_one("#bottom-app-container", Container) |
| 11078 | input_container.scroll_visible() |
| 11079 | except NoMatches: |
| 11080 | pass |
| 11081 | return |
| 11082 | |
| 11083 | # Eagerly fold tool calls into a single live summary so they are |
| 11084 | # collapsed from the moment they start, rather than rendering verbose |
| 11085 | # then snapping shut. A groupable tool joins (or opens) the current |
| 11086 | # step's group; a diff folds into it; anything else is a step boundary |
| 11087 | # that closes the group. |
| 11088 | is_groupable_tool = ( |
| 11089 | isinstance(widget, ToolCallMessage) and widget.tool_name != "ask_user" |
| 11090 | ) |
| 11091 | is_diff = isinstance(widget, DiffMessage) |
| 11092 | |
| 11093 | # Store message data for virtualization |
| 11094 | message_data = MessageData.from_widget(widget) |
| 11095 | if not widget.id: |
| 11096 | # Keep the widget DOM id == store id so pruning can locate a |
| 11097 | # mounted widget (and its timestamp footer) from its MessageData. |
| 11098 | widget.id = message_data.id |
| 11099 | footer = self._build_message_timestamp_footer( |
| 11100 | message_data, visible=self._message_timestamps_visible |
| 11101 | ) |
| 11102 |