pragma ComponentBehavior: Bound import QtQuick import ZShell.Config import ZShell.Llm import qs.Components import qs.Services MouseArea { id: root readonly property ChatGeneration current: modelData.activeGeneration required property int index readonly property bool isUser: modelData.role === ChatMessage.Role.User required property ChatMessage modelData property bool reasoningExpanded: false property real savedContentY: -1 function handleReasoningToggle(expanded: bool): void { const view = root.ListView.view; if (!view) return; if (expanded) { root.savedContentY = view.contentY; root.reasoningExpanded = true; } else { root.reasoningExpanded = false; if (root.savedContentY !== -1) { restoreAnim.to = root.savedContentY; restoreAnim.restart(); root.savedContentY = -1; } } } anchors.left: parent.left anchors.leftMargin: root.isUser ? 0 : Tokens.spacing.extraSmall anchors.right: parent.right anchors.rightMargin: root.isUser ? Tokens.spacing.extraSmall : 0 hoverEnabled: true // Explicit, synchronous height — sum of children, no Layout polish involved. implicitHeight: reasoningLoader.implicitHeight + (reasoningLoader.active ? Tokens.spacing.medium : 0) + bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin preventStealing: true Binding { property: "contentY" restoreMode: Binding.RestoreNone target: root.ListView.view value: root.y - Tokens.padding.large * 2 when: root.reasoningExpanded && root.ListView.view && ((root.y - Tokens.padding.large * 2) < root.ListView.view.contentY) } Anim { id: restoreAnim property: "contentY" target: root.ListView.view type: Anim.DefaultEffects } Loader { id: reasoningLoader readonly property bool shouldBeActive: root.current.reasoningActive || root.current.content === "" active: !root.isUser anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: Tokens.spacing.extraLarge anchors.top: parent.top sourceComponent: Reasoning { current: root.current onExpandedChanged: { root.handleReasoningToggle(expanded); } } } CustomRect { id: bubble anchors.left: root.isUser ? undefined : parent.left anchors.right: root.isUser ? parent.right : undefined anchors.top: reasoningLoader.active ? reasoningLoader.bottom : parent.top anchors.topMargin: reasoningLoader.active ? Tokens.spacing.medium : 0 color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer implicitHeight: root.current.content !== "" ? msgText.contentHeight + Tokens.padding.medium * 2 : 0 implicitWidth: Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge) radius: Tokens.rounding.medium visible: !root.current.reasoningActive TextEditBase { id: msgText anchors.left: parent.left anchors.margins: Tokens.padding.medium anchors.right: parent.right anchors.top: parent.top animateCursor: false color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary font.pointSize: Tokens.font.size.smaller readOnly: true selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4) text: root.current.content textFormat: CustomText.MarkdownText wrapMode: Text.WordWrap Keys.onPressed: event => { if (event.key == Qt.Key_Return) { if (!(event.modifiers & Qt.ShiftModifier)) { readOnly = true; event.accepted = true; } } else if (event.key == Qt.Key_Escape) { text = root.current.content; readOnly = true; event.accepted = true; } } onEditingFinished: { const old = root.current.content; if (old !== text) { root.modelData.edit(text); if (root.isUser) root.modelData.generate(); } readOnly = true; } onReadOnlyChanged: { if (readOnly) { animateCursor = false; root.forceActiveFocus(); textFormat = CustomText.MarkdownText; } else { var raw = root.current.content; textFormat = CustomText.PlainText; text = raw; forceActiveFocus(); cursorPosition = text.length; animateCursor = true; } } } } Actions { id: actionsRow anchors.left: parent.left anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right anchors.top: bubble.bottom anchors.topMargin: Tokens.spacing.medium current: root.current edit: msgText hovered: root.containsMouse message: root.modelData } }