ollie/kde/gui/ChatBlockDelegate.qml

254 lines
8.7 KiB
QML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ChatBlockDelegate.qml — Delegate for rendering chat blocks in a ListView
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Column {
id: root
spacing: 2
width: parent ? parent.width : 400
// Required properties from model
required property string blockType
required property string blockContent
required property int index
// For "Send to" menu
property string textToSend: ""
// Expand state based on block type
property bool blockExpanded: {
switch (blockType) {
case "user":
case "assistant":
case "tool":
case "info":
case "error":
return true
default:
return false
}
}
// Header color based on block type and theme
function headerColor() {
var bg = theme.baseColor
if (theme.isDark) {
switch (blockType) {
case "user": return Qt.lighter(bg, 1.8)
case "assistant": return Qt.lighter(bg, 1.5)
case "call": return Qt.lighter(bg, 1.7)
case "tool": return Qt.lighter(bg, 1.2)
case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7)
case "info": return Qt.lighter(bg, 1.4)
case "retry": return Qt.rgba(0.6, 0.5, 0.0, 0.5)
case "bypass": return "#3d2e00"
default: return bg
}
} else {
switch (blockType) {
case "user": return Qt.darker(bg, 1.12)
case "assistant": return Qt.darker(bg, 1.06)
case "call": return Qt.darker(bg, 1.1)
case "tool": return Qt.darker(bg, 1.04)
case "error": return Qt.rgba(1.0, 0.8, 0.8, 0.7)
case "info": return Qt.darker(bg, 1.02)
case "retry": return Qt.rgba(1.0, 0.9, 0.6, 0.5)
case "bypass": return "#fff8e1"
default: return bg
}
}
}
// Header label text based on block type
function headerText() {
switch (blockType) {
case "user": return "🧑 You"
case "assistant": return "🤖 Ollie"
case "call": return "▶️ Call"
case "tool": return "🖥️ Tool"
case "error": return "❌ Error"
case "info": return "ℹ️ Info"
case "retry": return "🔄 Retry"
case "bypass": return "⚠️ Bypass"
case "stalled": return "⏸️ Stalled"
default: return blockType
}
}
// Clickable header
Rectangle {
width: root.width
height: 28
radius: 3
color: root.headerColor()
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.blockExpanded = !root.blockExpanded
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 6
anchors.rightMargin: 6
spacing: 4
Label {
text: root.blockExpanded ? "▼" : "▶"
font.pixelSize: 8
opacity: 0.6
}
Label {
Layout.fillWidth: true
text: root.headerText()
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
font.bold: true
color: theme.textColor
opacity: 0.9
}
// Copy button
ToolButton {
text: "📋"
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
implicitWidth: 24
implicitHeight: 24
onClicked: clipboard.copyText(root.blockContent)
ToolTip.visible: hovered
ToolTip.text: "Copy"
}
}
}
// Block content — editable (local only, non-persistent)
TextArea {
id: contentArea
width: root.width
visible: root.blockExpanded && root.blockContent.length > 0
text: root.blockContent
textFormat: TextEdit.PlainText
readOnly: false
selectByMouse: true
persistentSelection: true
wrapMode: TextEdit.Wrap
font.family: "monospace"
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
color: theme.textColor
selectionColor: theme.highlightColor
selectedTextColor: theme.highlightedTextColor
background: null
leftPadding: 8
rightPadding: 8
topPadding: 4
bottomPadding: 4
// Acme-style mouse handling: B2=execute, B3=plumb, Ctrl+B2=send to
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.MiddleButton | Qt.RightButton
cursorShape: Qt.IBeamCursor
onPressed: function(mouse) {
// Chord: B1+B2=cut, B1+B3=paste
if (mouse.buttons & Qt.LeftButton) {
if (mouse.button === Qt.MiddleButton) {
contentArea.cut()
} else if (mouse.button === Qt.RightButton) {
contentArea.paste()
}
mouse.accepted = true
return
}
if (mouse.button === Qt.MiddleButton) {
var sel = contentArea.selectedText
if (sel && sel.trim() !== "") {
// Ctrl+B2 = Send to another agent
if (mouse.modifiers & Qt.ControlModifier) {
root.textToSend = sel.trim()
sendToMenu.popup()
} else {
ollie.submit(sel.trim())
}
}
mouse.accepted = true
} else if (mouse.button === Qt.RightButton) {
if (plumber.available()) {
var sel = contentArea.selectedText
if (sel && sel.trim() !== "") {
plumber.plumb(sel.trim(), root.sessionCwd())
} else {
// Plumb word at cursor
var pos = contentArea.positionAt(mouse.x, mouse.y)
var word = root.wordAtCursor(contentArea, pos)
if (word) plumber.plumb(word, root.sessionCwd())
}
}
mouse.accepted = true
}
}
}
}
// Get session cwd for plumbing
function sessionCwd() {
if (!ollie.activeSessionId) return ""
var cfg = ollie.readSessionConfig(ollie.activeSessionId)
return cfg ? (cfg.cwd || "") : ""
}
// Extract word at cursor for plumbing (file paths, etc.)
function wordAtCursor(textEdit, cursorPos) {
var text = textEdit.getText(0, textEdit.length)
if (cursorPos < 0 || cursorPos > text.length) return ""
function isfilec(ch) {
return /[a-zA-Z0-9.\-+\/:@_]/.test(ch)
}
var start = cursorPos
while (start > 0 && isfilec(text.charAt(start - 1))) start--
var end = cursorPos
while (end < text.length && isfilec(text.charAt(end))) end++
if (start === end) return ""
return text.substring(start, end)
}
// "Send to" cascading menu — session → agent
Menu {
id: sendToMenu
title: "Send to..."
Instantiator {
model: ollie.sessions
delegate: Menu {
id: sessionMenu
required property var modelData
title: modelData.name || modelData.id
property string sessionId: modelData.id
property var agentList: modelData.agents || []
Instantiator {
model: sessionMenu.agentList
delegate: MenuItem {
required property var modelData
text: modelData.name || modelData.id
onTriggered: {
ollie.submitTo(sessionMenu.sessionId, modelData.id, root.textToSend)
}
}
onObjectAdded: function(index, object) { sessionMenu.insertItem(index, object) }
onObjectRemoved: function(index, object) { sessionMenu.removeItem(object) }
}
}
onObjectAdded: function(index, object) { sendToMenu.insertMenu(index, object) }
onObjectRemoved: function(index, object) { sendToMenu.removeMenu(object) }
}
}
}