From d7a66f1827e0e6f4fb793bf3368b96a85da18e25 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:51:37 +0200 Subject: [PATCH] replace ListView with Flickable+Column+Repeater ListView.positionViewAtEnd() causes freezes by forcing instantiation of all delegates to measure total height. Flickable+Column avoids this - contentHeight is computed directly from Column.implicitHeight - Scroll to bottom is just contentY = contentHeight - height - No virtualization needed with only 20 blocks - Eliminates all positionViewAtEnd and _scrolling guard complexity --- gui/main.qml | 86 +++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/gui/main.qml b/gui/main.qml index 2d6231e..ddc069c 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -363,24 +363,16 @@ ApplicationWindow { value: dbus.chatText || "" } - ListView { - id: chatList + Flickable { + id: chatFlick anchors.fill: parent anchors.margins: 4 clip: true - spacing: 8 - cacheBuffer: 200 - model: chatModel - verticalLayoutDirection: ListView.TopToBottom - - property bool _scrolling: false - - Component.onCompleted: { - Qt.callLater(function() { chatList.positionViewAtEnd() }) - } + contentWidth: width + contentHeight: chatColumn.implicitHeight + boundsBehavior: Flickable.StopAtBounds onContentYChanged: { - if (_scrolling) return let bottom = contentHeight - height parent.atBottom = (bottom <= 0 || contentY >= bottom - 4) @@ -388,47 +380,51 @@ ApplicationWindow { if (contentY < 100 && contentHeight > height) { let oldHeight = contentHeight if (chatModel.loadEarlier()) { - // Maintain scroll position after prepend Qt.callLater(function() { - chatList.contentY += (chatList.contentHeight - oldHeight) + chatFlick.contentY += (chatFlick.contentHeight - oldHeight) }) } } } onContentHeightChanged: { - if (parent.atBottom && !_scrolling) { - _scrolling = true - Qt.callLater(function() { - chatList.positionViewAtEnd() - chatList._scrolling = false - }) + if (parent.atBottom) { + contentY = Math.max(0, contentHeight - height) } } - // Placeholder when empty - Label { - anchors.centerIn: parent - visible: chatList.count === 0 - text: dbus.activeSessionId - ? "(waiting for output...)" - : "Select or create a session" - color: palette.placeholderText - } + Column { + id: chatColumn + width: chatFlick.width + spacing: 8 - delegate: Loader { - width: chatList.width - height: item ? item.implicitHeight : 0 - property var block: model - sourceComponent: { - switch (blockType) { - case "user": return userDelegate - case "assistant": return assistantDelegate - case "reasoning": return reasoningDelegate - case "call": return callDelegate - case "tool": return toolDelegate - case "error": return errorDelegate - case "info": return infoDelegate - default: return assistantDelegate + // Placeholder + Label { + visible: chatRepeater.count === 0 + text: dbus.activeSessionId + ? "(waiting for output...)" + : "Select or create a session" + color: palette.placeholderText + } + + Repeater { + id: chatRepeater + model: chatModel + + delegate: Loader { + width: chatColumn.width + property var block: model + sourceComponent: { + switch (blockType) { + case "user": return userDelegate + case "assistant": return assistantDelegate + case "reasoning": return reasoningDelegate + case "call": return callDelegate + case "tool": return toolDelegate + case "error": return errorDelegate + case "info": return infoDelegate + default: return assistantDelegate + } + } } } } @@ -441,7 +437,7 @@ ApplicationWindow { icon.name: "go-down" visible: !parent.atBottom onClicked: { - chatList.contentY = Math.max(0, chatList.contentHeight - chatList.height) + chatFlick.contentY = Math.max(0, chatFlick.contentHeight - chatFlick.height) parent.atBottom = true } ToolTip.text: "Scroll to bottom"