254 lines
8.7 KiB
QML
254 lines
8.7 KiB
QML
// 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) }
|
||
}
|
||
}
|
||
}
|