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)
This commit is contained in:
Levi Neely 2026-07-16 08:43:25 +02:00
parent f546c8cf69
commit f6b218bef7
4 changed files with 450 additions and 30 deletions

View File

@ -31,6 +31,7 @@ if(OLLIE_KF5)
Config Config
Plasma Plasma
TextEditor TextEditor
SyntaxHighlighting
) )
# --- System Tray --- # --- System Tray ---
@ -58,7 +59,7 @@ if(OLLIE_KF5)
) )
# --- Kate Plugin --- # --- 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 target_link_libraries(ollie_kate
KF5::TextEditor KF5::TextEditor
Qt5::Core Qt5::Core
@ -94,6 +95,7 @@ if(OLLIE_KF5)
add_executable(ollie-gui add_executable(ollie-gui
gui/main.cpp gui/main.cpp
gui/thememanager.cpp gui/thememanager.cpp
gui/chatblockmodel.cpp
plasmoid/plugin/olliedbusclient.cpp plasmoid/plugin/olliedbusclient.cpp
${GUI_KF5_QRC} ${GUI_KF5_QRC}
) )
@ -109,6 +111,7 @@ if(OLLIE_KF5)
Qt5::Qml Qt5::Qml
Qt5::QuickControls2 Qt5::QuickControls2
KF5::ConfigCore KF5::ConfigCore
KF5::SyntaxHighlighting
) )
# --- Install --- # --- Install ---
@ -156,7 +159,7 @@ else()
# --- Kate Plugin --- # --- Kate Plugin ---
find_package(KF6TextEditor REQUIRED) 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 target_link_libraries(ollie_kate
KF6::TextEditor KF6::TextEditor
Qt6::Core Qt6::Core

View File

@ -39,9 +39,9 @@ install: build
install -m755 $(BUILD_DIR)/lib/kf6/krunner/krunner_ollie.so \ install -m755 $(BUILD_DIR)/lib/kf6/krunner/krunner_ollie.so \
/usr/lib64/qt6/plugins/kf6/krunner/ /usr/lib64/qt6/plugins/kf6/krunner/
# Kate plugin # Kate plugin
mkdir -p /usr/lib64/qt6/plugins/kf6/ktexteditor mkdir -p /usr/lib64/qt6/plugins/ktexteditor
install -m755 $(BUILD_DIR)/lib/kf6/ktexteditor/ollie_kate.so \ install -m755 $(BUILD_DIR)/lib/ktexteditor/ollie_kate.so \
/usr/lib64/qt6/plugins/kf6/ktexteditor/ /usr/lib64/qt6/plugins/ktexteditor/
# Plasmoid QML plugin library + metadata # Plasmoid QML plugin library + metadata
mkdir -p /usr/lib64/qt6/qml/org/ollie/private mkdir -p /usr/lib64/qt6/qml/org/ollie/private
install -m755 $(BUILD_DIR)/org/ollie/private/ollie-plasmoid-pluginplugin.so \ 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/share/icons/hicolor/scalable/apps/ollie.svg
rm -f /usr/lib64/libollie-plasmoid-plugin.so 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/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 rm -rf /usr/lib64/qt6/qml/org/ollie
kpackagetool6 -g -t Plasma/Applet -r org.ollie.plasmoid 2>/dev/null; true kpackagetool6 -g -t Plasma/Applet -r org.ollie.plasmoid 2>/dev/null; true
rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop
@ -128,6 +128,11 @@ kf5-install:
# Binaries # Binaries
install -m755 $(KF5_BUILD_DIR)/ollie-tray /usr/bin/ install -m755 $(KF5_BUILD_DIR)/ollie-tray /usr/bin/
install -m755 $(KF5_BUILD_DIR)/ollie-gui /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 # KRunner plugin
mkdir -p /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/krunner mkdir -p /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/krunner
install -m755 $(KF5_BUILD_DIR)/lib/krunner_ollie.so \ install -m755 $(KF5_BUILD_DIR)/lib/krunner_ollie.so \

View File

@ -8,6 +8,7 @@ ApplicationWindow {
width: 900 width: 900
height: 600 height: 600
visible: true visible: true
color: palette.window color: palette.window
font.family: theme.fontFamily || Qt.application.font.family font.family: theme.fontFamily || Qt.application.font.family
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
@ -42,6 +43,7 @@ ApplicationWindow {
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: 16 spacing: 16
Layout.margins: 16
Label { Label {
text: "Color Scheme" text: "Color Scheme"
@ -346,43 +348,83 @@ ApplicationWindow {
color: palette.mid color: palette.mid
} }
// Chat view // Chat view — structured blocks
ScrollView { Item {
id: chatScroll
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
property bool atBottom: true property bool atBottom: true
Binding {
target: chatModel
property: "rawText"
value: dbus.chatText || ""
}
Flickable { Flickable {
id: chatFlickable id: chatFlick
contentWidth: chatArea.width anchors.fill: parent
contentHeight: chatArea.implicitHeight anchors.margins: 4
clip: true clip: true
contentWidth: width
contentHeight: chatColumn.implicitHeight
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
onContentYChanged: { onContentYChanged: {
var bottom = contentHeight - height 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: { onContentHeightChanged: {
if (chatScroll.atBottom) { if (parent.atBottom) {
contentY = Math.max(0, contentHeight - height) contentY = Math.max(0, contentHeight - height)
} }
} }
TextArea { Column {
id: chatArea id: chatColumn
readOnly: true width: chatFlick.width
selectByMouse: true spacing: 8
width: chatFlickable.width
wrapMode: TextEdit.Wrap // Placeholder
text: dbus.chatText || (dbus.activeSessionId Label {
? "(waiting for output...)" visible: chatRepeater.count === 0
: "Select or create a session") text: dbus.activeSessionId
font.family: "monospace" ? "(waiting for output...)"
color: dbus.chatText ? palette.text : theme.placeholderTextColor : "Select or create a session"
background: null 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.bottom: parent.bottom
anchors.margins: 6 anchors.margins: 6
icon.name: "go-down" icon.name: "go-down"
visible: !chatScroll.atBottom visible: !parent.atBottom
onClicked: { onClicked: {
chatFlickable.contentY = Math.max(0, chatFlickable.contentHeight - chatFlickable.height) chatFlick.contentY = Math.max(0, chatFlick.contentHeight - chatFlick.height)
chatScroll.atBottom = true parent.atBottom = true
} }
ToolTip.text: "Scroll to bottom" ToolTip.text: "Scroll to bottom"
ToolTip.visible: hovered 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
}
}
}
}
}
}
}
}
} }

View File

@ -742,7 +742,8 @@ ApplicationWindow {
wrapMode: Text.Wrap wrapMode: Text.Wrap
textFormat: Text.MarkdownText textFormat: Text.MarkdownText
color: palette.text color: palette.text
font.pixelSize: Qt.application.font.pixelSize font.pointSize: root.font.pointSize
font.family: root.font.family
lineHeight: 1.3 lineHeight: 1.3
onLinkActivated: function(link) { onLinkActivated: function(link) {
Qt.openUrlExternally(link) Qt.openUrlExternally(link)