This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-kde/gui/ChatPane.qml

269 lines
11 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Item {
id: chatPane
property bool sidebarVisible: true
signal toggleSidebar()
ColumnLayout {
anchors.fill: parent
spacing: 0
// Header - shows active session/agent and state
RowLayout {
Layout.fillWidth: true
Layout.margins: 6
spacing: 8
// Sidebar toggle button
ToolButton {
text: chatPane.sidebarVisible ? "◀" : "▶"
font.pixelSize: 12
implicitWidth: 24
implicitHeight: 24
onClicked: chatPane.toggleSidebar()
ToolTip.text: chatPane.sidebarVisible ? "Hide sidebar (Ctrl+B)" : "Show sidebar (Ctrl+B)"
ToolTip.visible: hovered
}
// Active session/agent label
Label {
text: {
var sessionName = ""
var agentName = ""
for (var i = 0; i < sessionModel.count; ++i) {
var item = sessionModel.get(i)
if (item.type === "session" && item.id === ollie.activeSessionId) {
sessionName = item.displayName || item.id
}
if (item.type === "agent" && item.agentId === ollie.activeAgentId && item.parentId === ollie.activeSessionId) {
agentName = item.displayName || item.agentId
}
}
if (sessionName && agentName) {
return sessionName + " / " + agentName
} else if (sessionName) {
return sessionName + " (no agent)"
}
return "(no session)"
}
font.bold: true
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
elide: Text.ElideMiddle
Layout.fillWidth: true
}
// State indicator
Rectangle {
width: 10; height: 10; radius: 5
color: {
if (!ollie.activeAgentId) return "#9e9e9e" // gray when no agent
if (ollie.activeSessionId && ollie.isSessionPaused(ollie.activeSessionId)) return "#9e9e9e" // gray when paused
if (ollie.activeState === "idle") return "#4caf50"
if (ollie.activeState.startsWith("calling")) return "#ff9800"
return "#2196f3"
}
}
Label {
text: {
if (!ollie.activeAgentId) return ""
if (ollie.activeSessionId && ollie.isSessionPaused(ollie.activeSessionId)) return "inactive"
return ollie.activeState
}
opacity: 0.7
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
// Chat display — block-based ListView with scroll-to-bottom button
Item {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
// Placeholder when no agent selected or disconnected
Label {
anchors.centerIn: parent
text: {
if (!ollie.daemonConnected) return "Not connected"
if (!ollie.activeSessionId) return "Select a session"
return "Select an agent to view chat"
}
color: theme.midColor
font.italic: true
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
visible: !ollie.activeAgentId || !ollie.daemonConnected
}
ListView {
id: chatView
model: chatModel
spacing: 4
anchors.fill: parent
anchors.margins: 4
boundsBehavior: Flickable.StopAtBounds
visible: ollie.daemonConnected && ollie.activeAgentId !== ""
property bool followTail: true
reuseItems: true
onCountChanged: {
if (followTail)
scrollTimer.restart()
}
onContentHeightChanged: {
if (followTail)
scrollTimer.restart()
}
onMovementEnded: followTail = atYEnd
Timer {
id: scrollTimer
interval: 16
onTriggered: {
if (chatView.followTail && chatView.contentHeight > chatView.height)
chatView.positionViewAtEnd()
}
}
delegate: ChatBlockDelegate {
width: chatView.width - 8
parentListView: chatView
}
}
// Scroll to bottom button
Button {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 12
text: "↓"
font.pixelSize: 16
implicitWidth: 36
implicitHeight: 36
visible: chatView.visible && !chatView.followTail
onClicked: {
chatView.followTail = true
chatView.positionViewAtEnd()
}
ToolTip.text: "Scroll to bottom"
ToolTip.visible: hovered
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
// Input
RowLayout {
Layout.fillWidth: true
Layout.margins: 6
spacing: 4
// TextArea in ScrollView for overflow, with context menu overlay
Item {
Layout.fillWidth: true
Layout.maximumHeight: 200
Layout.preferredHeight: Math.min(promptInput.contentHeight + 16, 200)
clip: true
ScrollView {
id: promptScroll
anchors.fill: parent
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
TextArea {
id: promptInput
width: promptScroll.width
placeholderText: {
if (!ollie.daemonConnected) return "Not connected"
if (!ollie.activeSessionId) return "No session"
if (!ollie.activeAgentId) return "No agent"
if (ollie.isSessionPaused(ollie.activeSessionId)) return "Session paused"
return "Message..."
}
enabled: ollie.daemonConnected && ollie.activeSessionId !== "" && ollie.activeAgentId !== "" && !ollie.isSessionPaused(ollie.activeSessionId)
wrapMode: TextEdit.Wrap
selectByMouse: true
persistentSelection: true
property string _lastText: ""
property int _lastCursorPos: 0
onTextChanged: {
_lastText = text
_lastCursorPos = cursorPosition
}
Keys.onReturnPressed: function(event) {
if (event.modifiers & Qt.ShiftModifier) {
event.accepted = false
} else {
if (text.trim() !== "") {
ollie.submit(text.trim())
text = ""
}
event.accepted = true
}
}
}
}
// Context menu overlay - only catches right-click, on top of ScrollView
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
cursorShape: Qt.IBeamCursor
onClicked: promptContextMenu.popup()
}
Menu {
id: promptContextMenu
MenuItem {
text: "Cut"
enabled: promptInput.selectedText.length > 0
onTriggered: promptInput.cut()
}
MenuItem {
text: "Copy"
enabled: promptInput.selectedText.length > 0
onTriggered: promptInput.copy()
}
MenuItem {
text: "Paste"
enabled: promptInput.canPaste
onTriggered: promptInput.paste()
}
MenuSeparator {}
MenuItem {
text: "Select All"
onTriggered: promptInput.selectAll()
}
}
}
}
}
Connections {
target: ollie
function loadChat() {
chatModel.reset()
if (ollie.activeSessionId && ollie.activeAgentId) {
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
if (log) { chatModel.appendData(log); chatModel.trimFront(20) }
}
}
function onActiveSessionIdChanged() { loadChat() }
function onActiveAgentIdChanged() { loadChat() }
}
}