gui: split-pane sessions (Ctrl+* to split, Ctrl+W to close)
Major refactor: extracted chat UI into ChatPane.qml (self-contained with own ChatBlockModel, D-Bus signal filtering, prompt input). Main content area is now a SplitView with dynamic pane creation. Auto-layout: splits horizontal if window wider than tall. Sidebar click updates the focused pane session. New OllieDBusClient methods: getChat, getConfig, getState (per-session). New signal: chatAppended (emitted for all sessions, not just active).
This commit is contained in:
parent
16c195b1a8
commit
2699044b3b
|
|
@ -247,7 +247,7 @@ else()
|
|||
)
|
||||
qt_add_resources(ollie-gui "gui"
|
||||
PREFIX "/"
|
||||
FILES gui/main.qml gui/SessionGraph.qml gui/emojis.js
|
||||
FILES gui/main.qml gui/ChatPane.qml gui/SessionGraph.qml gui/emojis.js
|
||||
)
|
||||
|
||||
# --- Install ---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,788 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.ollie.private 1.0
|
||||
import "emojis.js" as Emojis
|
||||
|
||||
// Self-contained chat pane with own session binding
|
||||
Item {
|
||||
id: paneRoot
|
||||
|
||||
property string sessionId: ""
|
||||
property bool isFocused: false
|
||||
|
||||
// Pane-local chat model
|
||||
ChatBlockModel { id: paneModel }
|
||||
|
||||
// Track chat state for this pane's session
|
||||
property string paneState: "idle"
|
||||
property string paneCwd: ""
|
||||
property int chatOffset: 0
|
||||
|
||||
// Fetch chat when session changes
|
||||
onSessionIdChanged: {
|
||||
paneModel.rawText = ""
|
||||
chatOffset = 0
|
||||
paneState = "idle"
|
||||
paneCwd = ""
|
||||
if (sessionId !== "") {
|
||||
// Fetch initial chat and config
|
||||
let chat = dbus.getChat(sessionId, 0)
|
||||
if (chat) {
|
||||
paneModel.rawText = chat.text
|
||||
chatOffset = chat.offset
|
||||
}
|
||||
let cfg = dbus.getConfig(sessionId)
|
||||
if (cfg && cfg.cwd) paneCwd = cfg.cwd
|
||||
let st = dbus.getState(sessionId)
|
||||
if (st) paneState = st
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for D-Bus signals filtered to our session
|
||||
Connections {
|
||||
target: dbus
|
||||
function onChatAppended(sid, offset, text) {
|
||||
if (sid !== paneRoot.sessionId) return
|
||||
if (offset === paneRoot.chatOffset) {
|
||||
paneModel.rawText = paneModel.rawText + text
|
||||
paneRoot.chatOffset += text.length
|
||||
} else {
|
||||
// Offset mismatch - refetch
|
||||
let chat = dbus.getChat(paneRoot.sessionId, 0)
|
||||
if (chat) {
|
||||
paneModel.rawText = chat.text
|
||||
paneRoot.chatOffset = chat.offset
|
||||
}
|
||||
}
|
||||
}
|
||||
function onSessionStateUpdated(sid, newState) {
|
||||
if (sid === paneRoot.sessionId)
|
||||
paneRoot.paneState = newState
|
||||
}
|
||||
}
|
||||
|
||||
// Visual focus indicator
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: paneRoot.isFocused ? palette.highlight : "transparent"
|
||||
border.width: 2
|
||||
z: 100
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
spacing: 0
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 6
|
||||
|
||||
Rectangle {
|
||||
width: 8; height: 8; radius: 4
|
||||
color: {
|
||||
if (paneRoot.paneState === "idle") return "#4caf50"
|
||||
if (paneRoot.paneState.startsWith("calling")) return "#ff9800"
|
||||
return "#2196f3"
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: paneRoot.paneCwd || paneRoot.sessionId || "No session"
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideMiddle
|
||||
font.bold: true
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.9
|
||||
}
|
||||
|
||||
Label {
|
||||
text: paneRoot.paneState
|
||||
opacity: 0.6
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.8
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: palette.mid }
|
||||
|
||||
// Chat view
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
property bool atBottom: true
|
||||
|
||||
Flickable {
|
||||
id: paneFlick
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: paneColumn.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
property bool _settingY: false
|
||||
|
||||
onContentYChanged: {
|
||||
if (_settingY) return
|
||||
let bottom = contentHeight - height
|
||||
parent.atBottom = (bottom <= 0 || contentY >= bottom - 4)
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (parent.atBottom) {
|
||||
_settingY = true
|
||||
contentY = Math.max(0, contentHeight - height)
|
||||
_settingY = false
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheel) {
|
||||
paneFlick._settingY = true
|
||||
paneFlick.contentY = Math.max(0,
|
||||
Math.min(paneFlick.contentY - wheel.angleDelta.y,
|
||||
paneFlick.contentHeight - paneFlick.height))
|
||||
paneFlick._settingY = false
|
||||
let bottom = paneFlick.contentHeight - paneFlick.height
|
||||
paneFlick.parent.atBottom = (bottom <= 0 || paneFlick.contentY >= bottom - 4)
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: paneColumn
|
||||
width: paneFlick.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
visible: paneChatRepeater.count === 0
|
||||
text: paneRoot.sessionId ? "(waiting...)" : "No session"
|
||||
color: palette.placeholderText
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: paneChatRepeater
|
||||
model: paneModel
|
||||
delegate: Loader {
|
||||
width: paneColumn.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: {
|
||||
paneFlick.contentY = Math.max(0, paneFlick.contentHeight - paneFlick.height)
|
||||
parent.atBottom = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: palette.mid }
|
||||
|
||||
// Prompt
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 6
|
||||
spacing: 4
|
||||
|
||||
TextField {
|
||||
id: panePrompt
|
||||
Layout.fillWidth: true
|
||||
placeholderText: paneRoot.sessionId ? "Message..." : "No session"
|
||||
enabled: paneRoot.sessionId !== ""
|
||||
onAccepted: {
|
||||
if (text.trim() !== "" && paneRoot.sessionId !== "") {
|
||||
dbus.submit(paneRoot.sessionId, text.trim())
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.name: "media-playback-stop"
|
||||
enabled: paneRoot.paneState !== "idle" && paneRoot.sessionId !== ""
|
||||
onClicked: dbus.interrupt(paneRoot.sessionId)
|
||||
ToolTip.text: "Interrupt"
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
// Status
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 6
|
||||
Layout.bottomMargin: 2
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.75
|
||||
opacity: 0.4
|
||||
text: paneRoot.sessionId || "no session"
|
||||
}
|
||||
}
|
||||
|
||||
// Click to focus
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
onPressed: function(mouse) {
|
||||
paneRoot.forceActiveFocus()
|
||||
mouse.accepted = false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userDelegate
|
||||
Item {
|
||||
implicitHeight: userBubble.implicitHeight + userLabel.implicitHeight + 4
|
||||
|
||||
Rectangle {
|
||||
id: userBubble
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: Math.max(160, 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
|
||||
|
||||
HoverHandler { id: userHover }
|
||||
|
||||
ToolButton {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 4
|
||||
icon.name: "edit-copy"
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
opacity: userHover.hovered ? 0.8 : 0
|
||||
onClicked: paneModel.copyToClipboard(block.blockContent)
|
||||
ToolTip.text: "Copy"
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: userContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: block ? block.subBlocks : []
|
||||
delegate: Loader {
|
||||
Layout.fillWidth: true
|
||||
property var subBlock: modelData
|
||||
sourceComponent: subBlock.type === "code" ? codeSubDelegate
|
||||
: subBlock.type === "diff" ? diffSubDelegate
|
||||
: subBlock.type === "interrupt" ? interruptSubDelegate
|
||||
: 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: parent.width * 0.95
|
||||
implicitHeight: assistantContent.implicitHeight + 16
|
||||
color: Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.06)
|
||||
radius: 12
|
||||
|
||||
HoverHandler { id: assistantHover }
|
||||
|
||||
ToolButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 4
|
||||
icon.name: "edit-copy"
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
opacity: assistantHover.hovered ? 0.8 : 0
|
||||
onClicked: paneModel.copyToClipboard(block.blockContent)
|
||||
ToolTip.text: "Copy"
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: assistantContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: block ? block.subBlocks : []
|
||||
delegate: Loader {
|
||||
Layout.fillWidth: true
|
||||
property var subBlock: modelData
|
||||
sourceComponent: subBlock.type === "code" ? codeSubDelegate
|
||||
: subBlock.type === "diff" ? diffSubDelegate
|
||||
: subBlock.type === "interrupt" ? interruptSubDelegate
|
||||
: 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 ? block.blockContent : "")
|
||||
wrapMode: Text.Wrap
|
||||
font.family: "monospace"
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
||||
opacity: 0.5
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: callDelegate
|
||||
Item {
|
||||
implicitHeight: callLayout.implicitHeight + 12
|
||||
|
||||
HoverHandler { id: callHover }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.03)
|
||||
|
||||
Rectangle {
|
||||
width: 3
|
||||
height: parent.height
|
||||
color: "#ff9800"
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 4
|
||||
icon.name: "edit-copy"
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
opacity: callHover.hovered ? 0.8 : 0
|
||||
onClicked: paneModel.copyToClipboard(block.blockContent)
|
||||
ToolTip.text: "Copy"
|
||||
ToolTip.visible: hovered
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: callLayout
|
||||
x: 10
|
||||
width: parent.width - 20
|
||||
y: 6
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
text: "\u26A1 call: " + (block ? (block.blockName || "") : "")
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.8
|
||||
color: "#ff9800"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: block ? block.subBlocks : []
|
||||
delegate: Loader {
|
||||
Layout.fillWidth: true
|
||||
property var subBlock: modelData
|
||||
sourceComponent: subBlock.type === "code" ? codeSubDelegate
|
||||
: subBlock.type === "diff" ? diffSubDelegate
|
||||
: subBlock.type === "interrupt" ? interruptSubDelegate
|
||||
: plainSubDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: toolDelegate
|
||||
Item {
|
||||
implicitHeight: toolLayout.implicitHeight + 12
|
||||
|
||||
HoverHandler { id: toolHover }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(palette.text.r, palette.text.g, palette.text.b, 0.03)
|
||||
|
||||
Rectangle {
|
||||
width: 3
|
||||
height: parent.height
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 4
|
||||
icon.name: "edit-copy"
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
opacity: toolHover.hovered ? 0.8 : 0
|
||||
onClicked: paneModel.copyToClipboard(block.blockContent)
|
||||
ToolTip.text: "Copy"
|
||||
ToolTip.visible: hovered
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: toolLayout
|
||||
x: 10
|
||||
width: parent.width - 20
|
||||
y: 6
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
text: "\u25C6 tool: " + (block ? (block.blockName || "") : "")
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.8
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: block ? block.subBlocks : []
|
||||
delegate: Loader {
|
||||
Layout.fillWidth: true
|
||||
property var subBlock: modelData
|
||||
sourceComponent: subBlock.type === "code" ? codeSubDelegate
|
||||
: subBlock.type === "diff" ? diffSubDelegate
|
||||
: subBlock.type === "interrupt" ? interruptSubDelegate
|
||||
: plainSubDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ? block.blockContent : "")
|
||||
wrapMode: Text.Wrap
|
||||
color: "#e53935"
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: infoDelegate
|
||||
Label {
|
||||
text: (block ? 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: paneModel.linkifyFilePaths(subBlock.content || "")
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.MarkdownText
|
||||
color: palette.text
|
||||
// Qt 6 renders Markdown text noticeably smaller than Controls text.
|
||||
font.pointSize: root.font.pointSize + 2
|
||||
font.family: root.font.family
|
||||
lineHeight: 1.3
|
||||
onLinkActivated: function(link) {
|
||||
paneModel.openLink(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: plainSubDelegate
|
||||
Text {
|
||||
width: parent ? parent.width : implicitWidth
|
||||
text: subBlock.content || ""
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.PlainText
|
||||
color: palette.text
|
||||
font.family: "monospace"
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.9
|
||||
lineHeight: 1.3
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: interruptSubDelegate
|
||||
Rectangle {
|
||||
width: parent ? parent.width : 200
|
||||
height: interruptCol.implicitHeight + 16
|
||||
color: theme.isDark ? "#3d2e00" : "#fff8e1"
|
||||
radius: 4
|
||||
|
||||
Rectangle {
|
||||
width: 3
|
||||
height: parent.height
|
||||
color: "#ffc107"
|
||||
radius: 2
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: interruptCol
|
||||
x: 10
|
||||
y: 8
|
||||
width: parent.width - 20
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
text: "\u26A0 user interrupt"
|
||||
font.pixelSize: Qt.application.font.pixelSize * 0.8
|
||||
font.bold: true
|
||||
color: "#ffc107"
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: codeSubDelegate
|
||||
Rectangle {
|
||||
implicitHeight: codeCol.implicitHeight
|
||||
implicitWidth: Math.max(280, codeCol.implicitWidth)
|
||||
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/exec buttons
|
||||
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: "media-playback-start"
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
visible: {
|
||||
let lang = (subBlock.language || "").toLowerCase()
|
||||
return ["bash", "sh", "zsh", "fish", "python", "python3",
|
||||
"py", "perl", "ruby", "rb", "lua", "node",
|
||||
"javascript", "js", "php"].indexOf(lang) >= 0
|
||||
}
|
||||
onClicked: paneModel.executeInTerminal(subBlock.content, subBlock.language,
|
||||
dbus.activeCwd)
|
||||
ToolTip.text: "Execute in terminal"
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.name: "edit-copy"
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
onClicked: {
|
||||
paneModel.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 = paneModel.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: 32
|
||||
implicitHeight: 32
|
||||
onClicked: {
|
||||
paneModel.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -120,6 +120,9 @@ int main(int argc, char *argv[])
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Register types for QML instantiation
|
||||
qmlRegisterType<ChatBlockModel>("org.ollie.private", 1, 0, "ChatBlockModel");
|
||||
|
||||
// Register OllieDBusClient as a singleton
|
||||
auto *dbus = new OllieDBusClient(&app);
|
||||
auto *chatModel = new ChatBlockModel(&app);
|
||||
|
|
|
|||
1020
gui/main.qml
1020
gui/main.qml
File diff suppressed because it is too large
Load Diff
|
|
@ -349,6 +349,43 @@ QStringList OllieDBusClient::peerList(const QString &sessionId)
|
|||
return {};
|
||||
}
|
||||
|
||||
QVariantMap OllieDBusClient::getChat(const QString &sessionId, int offset)
|
||||
{
|
||||
QDBusMessage msg = QDBusMessage::createMethodCall(
|
||||
"org.ollie.SessionManager", "/org/ollie/SessionManager",
|
||||
"org.ollie.SessionManager", "GetChat");
|
||||
msg << sessionId << (qlonglong)offset;
|
||||
QDBusMessage reply = QDBusConnection::sessionBus().call(msg);
|
||||
QVariantMap result;
|
||||
if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() >= 2) {
|
||||
result["text"] = reply.arguments()[0].toString();
|
||||
result["offset"] = reply.arguments()[1].toInt();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariantMap OllieDBusClient::getConfig(const QString &sessionId)
|
||||
{
|
||||
QDBusReply<QString> reply = m_iface->call("GetConfig", sessionId);
|
||||
QVariantMap result;
|
||||
if (reply.isValid()) {
|
||||
for (const QString &line : reply.value().split('\n')) {
|
||||
int eq = line.indexOf('=');
|
||||
if (eq > 0)
|
||||
result[line.left(eq)] = line.mid(eq + 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString OllieDBusClient::getState(const QString &sessionId)
|
||||
{
|
||||
QDBusReply<QString> reply = m_iface->call("GetState", sessionId);
|
||||
if (reply.isValid())
|
||||
return reply.value();
|
||||
return "idle";
|
||||
}
|
||||
|
||||
bool OllieDBusClient::killSession(const QString &sessionId)
|
||||
{
|
||||
if (sessionId == m_activeSessionId) {
|
||||
|
|
@ -453,6 +490,9 @@ void OllieDBusClient::onStateChanged(const QString &session_id, const QString &n
|
|||
|
||||
void OllieDBusClient::onChatUpdated(const QString &session_id, qlonglong offset, const QString &new_text)
|
||||
{
|
||||
// Emit for all panes listening to any session
|
||||
emit chatAppended(session_id, static_cast<int>(offset), new_text);
|
||||
|
||||
if (session_id != m_activeSessionId) return;
|
||||
|
||||
// Append directly from the signal — no need to poll
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ public:
|
|||
QVariantList sessions() const { return m_sessions; }
|
||||
|
||||
Q_INVOKABLE QStringList peerList(const QString &sessionId);
|
||||
Q_INVOKABLE QVariantMap getChat(const QString &sessionId, int offset);
|
||||
Q_INVOKABLE QVariantMap getConfig(const QString &sessionId);
|
||||
Q_INVOKABLE QString getState(const QString &sessionId);
|
||||
QString activeSessionId() const { return m_activeSessionId; }
|
||||
void setActiveSessionId(const QString &id);
|
||||
QString chatText() const { return m_chatText; }
|
||||
|
|
@ -61,6 +64,7 @@ signals:
|
|||
void activeModelChanged();
|
||||
void activeBackendChanged();
|
||||
void sessionStateUpdated(const QString &sessionId, const QString &newState);
|
||||
void chatAppended(const QString &sessionId, int offset, const QString &text);
|
||||
|
||||
private slots:
|
||||
void onSessionCreated(const QString &session_id);
|
||||
|
|
|
|||
Reference in New Issue