From f6b218bef757ad258bad48f3423c6e75f8f806a5 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 16 Jul 2026 08:43:25 +0200 Subject: [PATCH] kf5: sync gui with kf6 features, fix install paths - main-kf5.qml: port all KF6 gui enhancements (chat bubbles, structured blocks, markdown, syntax highlighting, diff coloring, collapsible call/tool blocks, lazy-load scrolling) - CMakeLists.txt: add chatblockmodel.cpp and KF5::SyntaxHighlighting to KF5 gui target; fix Kate plugin INSTALL_NAMESPACE (ktexteditor, not kf5/ktexteditor or kf6/ktexteditor) - Makefile: fix KF6 kate install path; add icon+desktop to kf5-install - main.qml + main-kf5.qml: fix prose font size (use root.font.pointSize instead of Qt.application.font.pixelSize) --- CMakeLists.txt | 7 +- Makefile | 13 +- gui/main-kf5.qml | 457 ++++++++++++++++++++++++++++++++++++++++++++--- gui/main.qml | 3 +- 4 files changed, 450 insertions(+), 30 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9567afa..52dffe8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ if(OLLIE_KF5) Config Plasma TextEditor + SyntaxHighlighting ) # --- System Tray --- @@ -58,7 +59,7 @@ if(OLLIE_KF5) ) # --- Kate Plugin --- - kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp INSTALL_NAMESPACE "kf5/ktexteditor") + kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp INSTALL_NAMESPACE "ktexteditor") target_link_libraries(ollie_kate KF5::TextEditor Qt5::Core @@ -94,6 +95,7 @@ if(OLLIE_KF5) add_executable(ollie-gui gui/main.cpp gui/thememanager.cpp + gui/chatblockmodel.cpp plasmoid/plugin/olliedbusclient.cpp ${GUI_KF5_QRC} ) @@ -109,6 +111,7 @@ if(OLLIE_KF5) Qt5::Qml Qt5::QuickControls2 KF5::ConfigCore + KF5::SyntaxHighlighting ) # --- Install --- @@ -156,7 +159,7 @@ else() # --- Kate Plugin --- find_package(KF6TextEditor REQUIRED) - kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp INSTALL_NAMESPACE "kf6/ktexteditor") + kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp INSTALL_NAMESPACE "ktexteditor") target_link_libraries(ollie_kate KF6::TextEditor Qt6::Core diff --git a/Makefile b/Makefile index ca24272..cf50730 100644 --- a/Makefile +++ b/Makefile @@ -39,9 +39,9 @@ install: build install -m755 $(BUILD_DIR)/lib/kf6/krunner/krunner_ollie.so \ /usr/lib64/qt6/plugins/kf6/krunner/ # Kate plugin - mkdir -p /usr/lib64/qt6/plugins/kf6/ktexteditor - install -m755 $(BUILD_DIR)/lib/kf6/ktexteditor/ollie_kate.so \ - /usr/lib64/qt6/plugins/kf6/ktexteditor/ + mkdir -p /usr/lib64/qt6/plugins/ktexteditor + install -m755 $(BUILD_DIR)/lib/ktexteditor/ollie_kate.so \ + /usr/lib64/qt6/plugins/ktexteditor/ # Plasmoid QML plugin library + metadata mkdir -p /usr/lib64/qt6/qml/org/ollie/private install -m755 $(BUILD_DIR)/org/ollie/private/ollie-plasmoid-pluginplugin.so \ @@ -74,7 +74,7 @@ uninstall: rm -f /usr/share/icons/hicolor/scalable/apps/ollie.svg rm -f /usr/lib64/libollie-plasmoid-plugin.so rm -f /usr/lib64/qt6/plugins/kf6/krunner/krunner_ollie.so - rm -f /usr/lib64/qt6/plugins/kf6/ktexteditor/ollie_kate.so + rm -f /usr/lib64/qt6/plugins/ktexteditor/ollie_kate.so rm -rf /usr/lib64/qt6/qml/org/ollie kpackagetool6 -g -t Plasma/Applet -r org.ollie.plasmoid 2>/dev/null; true rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop @@ -128,6 +128,11 @@ kf5-install: # Binaries install -m755 $(KF5_BUILD_DIR)/ollie-tray /usr/bin/ install -m755 $(KF5_BUILD_DIR)/ollie-gui /usr/bin/ + # Desktop file and icon + mkdir -p /usr/share/applications + install -m644 gui/org.ollie.gui.desktop /usr/share/applications/ + mkdir -p /usr/share/icons/hicolor/scalable/apps + install -m644 gui/ollie.svg /usr/share/icons/hicolor/scalable/apps/ollie.svg # KRunner plugin mkdir -p /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/krunner install -m755 $(KF5_BUILD_DIR)/lib/krunner_ollie.so \ diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index 454255c..0196d88 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -8,6 +8,7 @@ ApplicationWindow { width: 900 height: 600 visible: true + color: palette.window font.family: theme.fontFamily || Qt.application.font.family font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize @@ -42,6 +43,7 @@ ApplicationWindow { contentItem: ColumnLayout { spacing: 16 + Layout.margins: 16 Label { text: "Color Scheme" @@ -346,43 +348,83 @@ ApplicationWindow { color: palette.mid } - // Chat view - ScrollView { - id: chatScroll + // Chat view — structured blocks + Item { Layout.fillWidth: true Layout.fillHeight: true property bool atBottom: true + Binding { + target: chatModel + property: "rawText" + value: dbus.chatText || "" + } + Flickable { - id: chatFlickable - contentWidth: chatArea.width - contentHeight: chatArea.implicitHeight + id: chatFlick + anchors.fill: parent + anchors.margins: 4 clip: true + contentWidth: width + contentHeight: chatColumn.implicitHeight boundsBehavior: Flickable.StopAtBounds onContentYChanged: { var bottom = contentHeight - height - chatScroll.atBottom = (bottom <= 0 || contentY >= bottom - 4) + parent.atBottom = (bottom <= 0 || contentY >= bottom - 4) + + // Load earlier messages when scrolled near top + if (contentY < 100 && contentHeight > height) { + var oldHeight = contentHeight + if (chatModel.loadEarlier()) { + Qt.callLater(function() { + chatFlick.contentY += (chatFlick.contentHeight - oldHeight) + }) + } + } } onContentHeightChanged: { - if (chatScroll.atBottom) { + if (parent.atBottom) { contentY = Math.max(0, contentHeight - height) } } - TextArea { - id: chatArea - readOnly: true - selectByMouse: true - width: chatFlickable.width - wrapMode: TextEdit.Wrap - text: dbus.chatText || (dbus.activeSessionId - ? "(waiting for output...)" - : "Select or create a session") - font.family: "monospace" - color: dbus.chatText ? palette.text : theme.placeholderTextColor - background: null + Column { + id: chatColumn + width: chatFlick.width + spacing: 8 + + // Placeholder + Label { + visible: chatRepeater.count === 0 + text: dbus.activeSessionId + ? "(waiting for output...)" + : "Select or create a session" + color: theme.placeholderTextColor + } + + 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 + } + } + } + } } } @@ -391,10 +433,10 @@ ApplicationWindow { anchors.bottom: parent.bottom anchors.margins: 6 icon.name: "go-down" - visible: !chatScroll.atBottom + visible: !parent.atBottom onClicked: { - chatFlickable.contentY = Math.max(0, chatFlickable.contentHeight - chatFlickable.height) - chatScroll.atBottom = true + chatFlick.contentY = Math.max(0, chatFlick.contentHeight - chatFlick.height) + parent.atBottom = true } ToolTip.text: "Scroll to bottom" ToolTip.visible: hovered @@ -472,4 +514,373 @@ ApplicationWindow { } } } + + // --- Chat block delegates --- + + Component { + id: userDelegate + Item { + implicitHeight: userBubble.implicitHeight + userLabel.implicitHeight + 4 + + Rectangle { + id: userBubble + anchors.top: parent.top + anchors.right: parent.right + width: Math.min(userContent.implicitWidth + 24, parent.width * 0.85) + implicitHeight: userContent.implicitHeight + 16 + color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.15) + radius: 12 + + ColumnLayout { + id: userContent + anchors.fill: parent + anchors.margins: 12 + spacing: 4 + + Repeater { + model: block.subBlocks + delegate: Loader { + Layout.fillWidth: true + property var subBlock: modelData + sourceComponent: subBlock.type === "code" ? codeSubDelegate + : subBlock.type === "diff" ? diffSubDelegate + : proseSubDelegate + } + } + } + } + + Label { + id: userLabel + text: "You" + font.pixelSize: Qt.application.font.pixelSize * 0.75 + opacity: 0.5 + anchors.right: parent.right + anchors.top: userBubble.bottom + anchors.topMargin: 2 + } + } + } + + Component { + id: assistantDelegate + Item { + implicitHeight: assistantBubble.implicitHeight + assistantLabel.implicitHeight + 4 + + Rectangle { + id: assistantBubble + anchors.top: parent.top + anchors.left: parent.left + width: Math.min(assistantContent.implicitWidth + 24, parent.width * 0.95) + implicitHeight: assistantContent.implicitHeight + 16 + color: Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.06) + radius: 12 + + ColumnLayout { + id: assistantContent + anchors.fill: parent + anchors.margins: 12 + spacing: 4 + + Repeater { + model: block.subBlocks + delegate: Loader { + Layout.fillWidth: true + property var subBlock: modelData + sourceComponent: subBlock.type === "code" ? codeSubDelegate + : subBlock.type === "diff" ? diffSubDelegate + : proseSubDelegate + } + } + } + } + + Label { + id: assistantLabel + text: "Ollie" + font.pixelSize: Qt.application.font.pixelSize * 0.75 + opacity: 0.5 + anchors.left: parent.left + anchors.top: assistantBubble.bottom + anchors.topMargin: 2 + } + } + } + + Component { + id: reasoningDelegate + ColumnLayout { + spacing: 2 + Label { + text: (reasoningText.visible ? "\u25BC" : "\u25B6") + " reasoning" + font.pixelSize: Qt.application.font.pixelSize * 0.8 + opacity: 0.5 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: reasoningText.visible = !reasoningText.visible + } + } + Label { + id: reasoningText + Layout.fillWidth: true + text: block.blockContent + wrapMode: Text.Wrap + font.family: "monospace" + font.pixelSize: Qt.application.font.pixelSize * 0.85 + opacity: 0.5 + visible: false + } + } + } + + Component { + id: callDelegate + ColumnLayout { + spacing: 2 + + property bool expanded: false + + Label { + text: (parent.expanded ? "\u25BC" : "\u25B6") + " call: " + (block.blockName || "") + font.pixelSize: Qt.application.font.pixelSize * 0.8 + opacity: 0.5 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: parent.parent.expanded = !parent.parent.expanded + } + } + + // Call args — pretty-printed highlighted HTML + Text { + Layout.fillWidth: true + text: block.formattedContent || "" + textFormat: Text.RichText + wrapMode: Text.Wrap + font.family: "monospace" + font.pixelSize: Qt.application.font.pixelSize * 0.85 + opacity: 0.7 + visible: parent.expanded + color: palette.text + } + } + } + + Component { + id: toolDelegate + ColumnLayout { + spacing: 2 + + property bool expanded: false + + Label { + text: (parent.expanded ? "\u25BC" : "\u25B6") + " tool: " + (block.blockName || "") + font.pixelSize: Qt.application.font.pixelSize * 0.8 + opacity: 0.5 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: parent.parent.expanded = !parent.parent.expanded + } + } + + // Render sub-blocks (same as assistant) when expanded + Repeater { + model: parent.expanded ? block.subBlocks : [] + delegate: Loader { + Layout.fillWidth: true + property var subBlock: modelData + sourceComponent: subBlock.type === "code" ? codeSubDelegate + : subBlock.type === "diff" ? diffSubDelegate + : proseSubDelegate + } + } + } + } + + Component { + id: errorDelegate + Rectangle { + implicitHeight: errorLabel.implicitHeight + 12 + color: Qt.rgba(1, 0, 0, 0.08) + radius: 4 + Label { + id: errorLabel + anchors.fill: parent + anchors.margins: 6 + text: block.blockContent + wrapMode: Text.Wrap + color: "#e53935" + font.family: "monospace" + } + } + } + + Component { + id: infoDelegate + Label { + text: block.blockContent + wrapMode: Text.Wrap + font.pixelSize: Qt.application.font.pixelSize * 0.85 + opacity: 0.6 + font.italic: true + } + } + + // --- Sub-block delegates --- + + Component { + id: proseSubDelegate + Text { + width: parent ? parent.width : implicitWidth + text: subBlock.content || "" + wrapMode: Text.Wrap + textFormat: Text.MarkdownText + color: palette.text + font.pointSize: root.font.pointSize + font.family: root.font.family + lineHeight: 1.3 + onLinkActivated: Qt.openUrlExternally(link) + } + } + + Component { + id: codeSubDelegate + Rectangle { + implicitHeight: codeCol.implicitHeight + color: Qt.rgba(palette.base.r, palette.base.g, palette.base.b, 0.6) + radius: 4 + border.width: 1 + border.color: palette.mid + + ColumnLayout { + id: codeCol + anchors.fill: parent + spacing: 0 + + // Header with language + copy button + RowLayout { + Layout.fillWidth: true + Layout.margins: 4 + + Label { + text: subBlock.language || "code" + font.pixelSize: Qt.application.font.pixelSize * 0.8 + opacity: 0.6 + Layout.fillWidth: true + } + + ToolButton { + icon.name: "edit-copy" + implicitWidth: 22 + implicitHeight: 22 + onClicked: { + chatModel.copyToClipboard(subBlock.content) + } + ToolTip.text: "Copy" + ToolTip.visible: hovered + } + } + + // Code content — show plain first, highlight lazily + Text { + id: codeText + Layout.fillWidth: true + Layout.margins: 8 + text: subBlock.content + textFormat: Text.PlainText + wrapMode: Text.Wrap + font.family: "monospace" + font.pixelSize: Qt.application.font.pixelSize * 0.9 + color: palette.text + + property bool highlighted: false + Timer { + interval: 150 + running: true + onTriggered: { + codeText.text = chatModel.highlightedHtml(subBlock.content, subBlock.language) + codeText.textFormat = Text.RichText + codeText.highlighted = true + } + } + } + } + } + } + + Component { + id: diffSubDelegate + Rectangle { + implicitHeight: diffCol.implicitHeight + color: Qt.rgba(palette.base.r, palette.base.g, palette.base.b, 0.6) + radius: 4 + border.width: 1 + border.color: palette.mid + + ColumnLayout { + id: diffCol + anchors.fill: parent + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.margins: 4 + + Label { + text: "diff" + font.pixelSize: Qt.application.font.pixelSize * 0.8 + opacity: 0.6 + Layout.fillWidth: true + } + + ToolButton { + icon.name: "edit-copy" + implicitWidth: 22 + implicitHeight: 22 + onClicked: { + chatModel.copyToClipboard(subBlock.content) + } + ToolTip.text: "Copy" + ToolTip.visible: hovered + } + } + + // Diff lines with coloring + Column { + Layout.fillWidth: true + Layout.margins: 8 + spacing: 0 + + Repeater { + model: subBlock.content.split("\n") + Rectangle { + width: parent.width + implicitHeight: diffLineText.implicitHeight + color: { + if (modelData.startsWith("+")) return Qt.rgba(0.3, 0.8, 0.3, 0.08) + if (modelData.startsWith("-")) return Qt.rgba(0.8, 0.3, 0.3, 0.08) + return "transparent" + } + Text { + id: diffLineText + width: parent.width + text: modelData + font.family: "monospace" + font.pixelSize: Qt.application.font.pixelSize * 0.9 + wrapMode: Text.Wrap + color: { + if (modelData.startsWith("+")) return "#4caf50" + if (modelData.startsWith("-")) return "#f44336" + if (modelData.startsWith("@@")) return "#2196f3" + return palette.text + } + } + } + } + } + } + } + } } diff --git a/gui/main.qml b/gui/main.qml index 3c76d5d..97dab17 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -742,7 +742,8 @@ ApplicationWindow { wrapMode: Text.Wrap textFormat: Text.MarkdownText color: palette.text - font.pixelSize: Qt.application.font.pixelSize + font.pointSize: root.font.pointSize + font.family: root.font.family lineHeight: 1.3 onLinkActivated: function(link) { Qt.openUrlExternally(link)