866 lines
31 KiB
QML
866 lines
31 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
|
|
ApplicationWindow {
|
|
id: root
|
|
title: dbus.activeCwd ? "Ollie — " + dbus.activeCwd : "Ollie"
|
|
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
|
|
|
|
palette {
|
|
window: theme.windowColor
|
|
windowText: theme.windowTextColor
|
|
base: theme.baseColor
|
|
alternateBase: theme.alternateBaseColor
|
|
text: theme.textColor
|
|
button: theme.buttonColor
|
|
buttonText: theme.buttonTextColor
|
|
highlight: theme.highlightColor
|
|
highlightedText: theme.highlightedTextColor
|
|
toolTipBase: theme.tooltipBaseColor
|
|
toolTipText: theme.tooltipTextColor
|
|
link: theme.linkColor
|
|
mid: theme.midColor
|
|
dark: theme.darkColor
|
|
light: theme.lightColor
|
|
shadow: theme.shadowColor
|
|
placeholderText: theme.placeholderTextColor
|
|
}
|
|
|
|
// --- Settings Dialog ---
|
|
Dialog {
|
|
id: settingsDialog
|
|
title: "Settings"
|
|
anchors.centerIn: parent
|
|
width: 380
|
|
modal: true
|
|
standardButtons: Dialog.Close
|
|
|
|
contentItem: ColumnLayout {
|
|
spacing: 16
|
|
Layout.margins: 16
|
|
|
|
Label {
|
|
text: "Color Scheme"
|
|
font.bold: true
|
|
}
|
|
|
|
ComboBox {
|
|
id: schemeCombo
|
|
Layout.fillWidth: true
|
|
model: ["System"].concat(theme.availableSchemes)
|
|
currentIndex: {
|
|
if (theme.mode === "system") return 0
|
|
let idx = theme.availableSchemes.indexOf(theme.mode)
|
|
return idx >= 0 ? idx + 1 : 0
|
|
}
|
|
onActivated: {
|
|
if (currentIndex === 0) {
|
|
theme.mode = "system"
|
|
} else {
|
|
theme.mode = theme.availableSchemes[currentIndex - 1]
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: theme.isDark ? "Current: dark" : "Current: light"
|
|
opacity: 0.5
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
}
|
|
|
|
// --- Font section ---
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: palette.mid
|
|
Layout.topMargin: 8
|
|
}
|
|
|
|
Label {
|
|
text: "Font"
|
|
font.bold: true
|
|
}
|
|
|
|
GridLayout {
|
|
columns: 2
|
|
columnSpacing: 12
|
|
rowSpacing: 8
|
|
Layout.fillWidth: true
|
|
|
|
Label { text: "Family:" }
|
|
ComboBox {
|
|
id: fontFamilyCombo
|
|
Layout.fillWidth: true
|
|
model: theme.availableFamilies
|
|
editable: true
|
|
currentIndex: {
|
|
let fam = theme.fontFamily || Qt.application.font.family
|
|
let idx = model.indexOf(fam)
|
|
return idx >= 0 ? idx : 0
|
|
}
|
|
onActivated: theme.fontFamily = currentText
|
|
onAccepted: theme.fontFamily = editText
|
|
}
|
|
|
|
Label { text: "Style:" }
|
|
ComboBox {
|
|
id: fontStyleCombo
|
|
Layout.fillWidth: true
|
|
model: theme.availableStyles
|
|
currentIndex: {
|
|
let st = theme.fontStyle
|
|
let idx = model.indexOf(st)
|
|
return idx >= 0 ? idx : 0
|
|
}
|
|
onActivated: theme.fontStyle = currentText
|
|
}
|
|
|
|
Label { text: "Size:" }
|
|
SpinBox {
|
|
id: fontSizeSpin
|
|
from: 6
|
|
to: 72
|
|
value: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
onValueModified: theme.fontSize = value
|
|
}
|
|
}
|
|
|
|
Button {
|
|
text: "Reset to system font"
|
|
onClicked: theme.resetFont()
|
|
}
|
|
}
|
|
}
|
|
|
|
function openNewSessionDialog() {
|
|
let path = theme.chooseFolder("Choose working directory for new session")
|
|
if (path !== "")
|
|
dbus.createSession(path)
|
|
}
|
|
|
|
function openCwdDialog() {
|
|
let path = theme.chooseFolder("Change working directory",
|
|
dbus.activeCwd || "")
|
|
if (path !== "")
|
|
dbus.setConfig(dbus.activeSessionId, "cwd", path)
|
|
}
|
|
|
|
SplitView {
|
|
anchors.fill: parent
|
|
orientation: Qt.Horizontal
|
|
|
|
// --- Session sidebar ---
|
|
Pane {
|
|
SplitView.preferredWidth: 220
|
|
SplitView.minimumWidth: 160
|
|
padding: 0
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// Toolbar
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
|
|
Label {
|
|
text: "Sessions"
|
|
font.bold: true
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "list-add"
|
|
onClicked: root.openNewSessionDialog()
|
|
ToolTip.text: "New session"
|
|
ToolTip.visible: hovered
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "configure"
|
|
onClicked: settingsDialog.open()
|
|
ToolTip.text: "Settings"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
|
|
// Session list
|
|
ListView {
|
|
id: sessionList
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
model: dbus.sessions
|
|
|
|
delegate: ItemDelegate {
|
|
id: sessionDelegate
|
|
width: sessionList.width
|
|
highlighted: modelData.id === dbus.activeSessionId
|
|
|
|
property bool renaming: false
|
|
|
|
background: Rectangle {
|
|
color: sessionDelegate.highlighted
|
|
? palette.highlight : "transparent"
|
|
radius: 4
|
|
}
|
|
|
|
contentItem: RowLayout {
|
|
spacing: 6
|
|
|
|
Rectangle {
|
|
width: 8
|
|
height: 8
|
|
radius: 4
|
|
color: {
|
|
let state = modelData.state
|
|
if (state === "idle") return "#4caf50"
|
|
if (state.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
id: sessionLabel
|
|
text: modelData.id
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
font.family: "monospace"
|
|
color: sessionDelegate.highlighted
|
|
? palette.highlightedText : palette.text
|
|
visible: !sessionDelegate.renaming
|
|
}
|
|
|
|
TextField {
|
|
id: renameField
|
|
Layout.fillWidth: true
|
|
font.family: "monospace"
|
|
visible: sessionDelegate.renaming
|
|
onAccepted: {
|
|
let newName = text.trim()
|
|
if (newName !== "" && newName !== modelData.id) {
|
|
dbus.renameSession(modelData.id, newName)
|
|
}
|
|
sessionDelegate.renaming = false
|
|
}
|
|
Keys.onEscapePressed: sessionDelegate.renaming = false
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
text = modelData.id
|
|
selectAll()
|
|
forceActiveFocus()
|
|
}
|
|
}
|
|
onActiveFocusChanged: {
|
|
if (!activeFocus && sessionDelegate.renaming) {
|
|
sessionDelegate.renaming = false
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: modelData.agent
|
|
opacity: 0.6
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
color: sessionDelegate.highlighted
|
|
? palette.highlightedText : palette.text
|
|
visible: !sessionDelegate.renaming
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "process-stop"
|
|
implicitWidth: 24
|
|
implicitHeight: 24
|
|
visible: !sessionDelegate.renaming
|
|
onClicked: dbus.killSession(modelData.id)
|
|
ToolTip.text: "Kill session"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
|
|
onClicked: dbus.activeSessionId = modelData.id
|
|
onDoubleClicked: sessionDelegate.renaming = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Main content ---
|
|
Pane {
|
|
SplitView.fillWidth: true
|
|
padding: 0
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// Header bar
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 8
|
|
|
|
Rectangle {
|
|
width: 10
|
|
height: 10
|
|
radius: 5
|
|
color: {
|
|
if (dbus.sessions.length === 0) return "gray"
|
|
let state = dbus.activeState
|
|
if (state === "idle") return "#4caf50"
|
|
if (state.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: dbus.activeCwd || "No session selected"
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideMiddle
|
|
font.bold: true
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onDoubleClicked: {
|
|
if (dbus.activeSessionId !== "") {
|
|
root.openCwdDialog()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: dbus.activeState
|
|
opacity: 0.6
|
|
}
|
|
}
|
|
|
|
// Separator
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: palette.mid
|
|
}
|
|
|
|
// Chat view
|
|
// 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: chatFlick
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: chatColumn.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
onContentYChanged: {
|
|
let bottom = contentHeight - height
|
|
parent.atBottom = (bottom <= 0 || contentY >= bottom - 4)
|
|
|
|
// Load earlier messages when scrolled near top
|
|
if (contentY < 100 && contentHeight > height) {
|
|
let oldHeight = contentHeight
|
|
if (chatModel.loadEarlier()) {
|
|
Qt.callLater(function() {
|
|
chatFlick.contentY += (chatFlick.contentHeight - oldHeight)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
onContentHeightChanged: {
|
|
if (parent.atBottom) {
|
|
contentY = Math.max(0, contentHeight - height)
|
|
}
|
|
}
|
|
|
|
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: palette.placeholderText
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ToolButton {
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
anchors.margins: 6
|
|
icon.name: "go-down"
|
|
visible: !parent.atBottom
|
|
onClicked: {
|
|
chatFlick.contentY = Math.max(0, chatFlick.contentHeight - chatFlick.height)
|
|
parent.atBottom = true
|
|
}
|
|
ToolTip.text: "Scroll to bottom"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
|
|
// Separator
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: palette.mid
|
|
}
|
|
|
|
// Prompt input
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 8
|
|
spacing: 6
|
|
|
|
ScrollView {
|
|
Layout.fillWidth: true
|
|
Layout.maximumHeight: 100
|
|
|
|
TextArea {
|
|
id: promptInput
|
|
placeholderText: dbus.activeSessionId
|
|
? "Message..."
|
|
: "No active session"
|
|
enabled: dbus.activeSessionId !== ""
|
|
wrapMode: TextEdit.Wrap
|
|
Keys.onReturnPressed: function(event) {
|
|
if (event.modifiers & (Qt.ShiftModifier | Qt.ControlModifier)) {
|
|
promptInput.insert(promptInput.cursorPosition, "\n")
|
|
} else {
|
|
if (text.trim() !== "" && dbus.activeSessionId !== "") {
|
|
dbus.submit(dbus.activeSessionId, text.trim())
|
|
text = ""
|
|
}
|
|
}
|
|
event.accepted = true
|
|
}
|
|
Keys.onPressed: function(event) {
|
|
if (event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) {
|
|
promptInput.insert(promptInput.cursorPosition, "\n")
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: forceActiveFocus()
|
|
}
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "media-playback-stop"
|
|
enabled: dbus.activeState !== "idle" && dbus.activeSessionId !== ""
|
|
onClicked: dbus.interrupt(dbus.activeSessionId)
|
|
ToolTip.text: "Interrupt"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
|
|
// Status bar
|
|
Label {
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: 8
|
|
Layout.bottomMargin: 4
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
opacity: 0.5
|
|
text: {
|
|
let id = dbus.activeSessionId
|
|
if (!id) return "ollie"
|
|
return dbus.activeState + " │ " + id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Chat block delegates ---
|
|
|
|
function linkifyPaths(text) {
|
|
// Escape HTML entities first
|
|
let escaped = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
// Inline code: `text` → monospace styled span
|
|
escaped = escaped.replace(/`([^`]+)`/g, '<code style="font-family:monospace;background:rgba(128,128,128,0.15);padding:1px 4px;border-radius:3px;">$1</code>')
|
|
// Convert newlines to <br>
|
|
escaped = escaped.replace(/\n/g, '<br>')
|
|
// Match /absolute/path or /absolute/path:line
|
|
escaped = escaped.replace(/(\/[\w\-.\/]+(?::\d+)?)/g, '<a href="file://$1">$1</a>')
|
|
return escaped
|
|
}
|
|
|
|
Component {
|
|
id: userDelegate
|
|
ColumnLayout {
|
|
spacing: 4
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
implicitHeight: userContent.implicitHeight + 12
|
|
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.08)
|
|
radius: 6
|
|
|
|
ColumnLayout {
|
|
id: userContent
|
|
anchors.fill: parent
|
|
anchors.margins: 6
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: assistantDelegate
|
|
ColumnLayout {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
Label {
|
|
text: (callText.visible ? "\u25BC" : "\u25B6") + " call: " + (block.blockName || "")
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.8
|
|
opacity: 0.5
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: callText.visible = !callText.visible
|
|
}
|
|
}
|
|
Text {
|
|
id: callText
|
|
Layout.fillWidth: true
|
|
text: visible ? chatModel.prettyJson(String(block.blockContent || "")) : ""
|
|
textFormat: Text.RichText
|
|
wrapMode: Text.Wrap
|
|
font.family: "monospace"
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
opacity: 0.7
|
|
visible: false
|
|
color: palette.text
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: toolDelegate
|
|
ColumnLayout {
|
|
spacing: 2
|
|
Label {
|
|
text: (toolText.visible ? "\u25BC" : "\u25B6") + " tool: " + (block.blockName || "")
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.8
|
|
opacity: 0.5
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: toolText.visible = !toolText.visible
|
|
}
|
|
}
|
|
Text {
|
|
id: toolText
|
|
Layout.fillWidth: true
|
|
text: {
|
|
if (!visible) return ""
|
|
let content = String(block.blockContent || "")
|
|
if (!content) return ""
|
|
let trimmed = content.trim()
|
|
if (trimmed.charAt(0) === '{' || trimmed.charAt(0) === '[')
|
|
return chatModel.prettyJson(content)
|
|
if (trimmed.indexOf("---") === 0 || trimmed.indexOf("diff ") === 0)
|
|
return chatModel.highlightedHtml(content, "diff")
|
|
return content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')
|
|
}
|
|
textFormat: Text.RichText
|
|
wrapMode: Text.Wrap
|
|
font.family: "monospace"
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
opacity: 0.7
|
|
visible: false
|
|
color: palette.text
|
|
maximumLineCount: 80
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
|
|
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: root.linkifyPaths(subBlock.content)
|
|
wrapMode: Text.Wrap
|
|
textFormat: Text.RichText
|
|
color: palette.text
|
|
font.pixelSize: Qt.application.font.pixelSize
|
|
lineHeight: 1.4
|
|
onLinkActivated: function(link) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|