From 2d6e0ed7873b2f37895a1acbc8a89e6420e37820 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 7 Aug 2026 17:40:09 +0200 Subject: [PATCH] gui: extract dialogs and chat delegate into components Phase 1 of GUI cleanup - component extraction: - SettingsDialog.qml: theme and font settings (118 lines) - NewSessionDialog.qml: session creation form (79 lines) - NewAgentDialog.qml: agent creation form (174 lines) - ChatBlockDelegate.qml: chat block with text/diagram segments (322 lines) main.qml reduced from 1685 to 1030 lines (39% reduction). Updated CMakeLists.txt for both Qt6 and KF5 builds. --- CMakeLists.txt | 12 +- gui/ChatBlockDelegate.qml | 322 ++++++++++++++++++ gui/NewAgentDialog.qml | 174 ++++++++++ gui/NewSessionDialog.qml | 79 +++++ gui/SettingsDialog.qml | 118 +++++++ gui/main.qml | 669 +------------------------------------- gui/qml.qrc | 4 + 7 files changed, 715 insertions(+), 663 deletions(-) create mode 100644 gui/ChatBlockDelegate.qml create mode 100644 gui/NewAgentDialog.qml create mode 100644 gui/NewSessionDialog.qml create mode 100644 gui/SettingsDialog.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index a7dc83c..925e662 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,11 @@ if(OLLIE_KF5) file(READ ${CMAKE_CURRENT_SOURCE_DIR}/gui/main.qml _QML_SRC) string(REGEX REPLACE "[ \t]*(//[^\n]*palette[^\n]*\n|palette\\.(active|inactive|disabled)\\.[^\n]*\n)" "" _QML_SRC "${_QML_SRC}") file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/main.qml "${_QML_SRC}") + # Copy component QML files (no palette stripping needed - they don't use grouped palette) + configure_file(gui/SettingsDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/SettingsDialog.qml COPYONLY) + configure_file(gui/NewSessionDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/NewSessionDialog.qml COPYONLY) + configure_file(gui/NewAgentDialog.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/NewAgentDialog.qml COPYONLY) + configure_file(gui/ChatBlockDelegate.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/ChatBlockDelegate.qml COPYONLY) configure_file(gui/qml.qrc ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/qml.qrc COPYONLY) qt5_add_resources(GUI_KF5_QRC ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/qml.qrc) add_executable(ollie-gui @@ -170,7 +175,12 @@ else() qt_add_resources(ollie-gui "gui" PREFIX "/" BASE gui - FILES gui/main.qml + FILES + gui/main.qml + gui/SettingsDialog.qml + gui/NewSessionDialog.qml + gui/NewAgentDialog.qml + gui/ChatBlockDelegate.qml ) # --- Install --- diff --git a/gui/ChatBlockDelegate.qml b/gui/ChatBlockDelegate.qml new file mode 100644 index 0000000..bee6f2e --- /dev/null +++ b/gui/ChatBlockDelegate.qml @@ -0,0 +1,322 @@ +// ChatBlockDelegate.qml — Delegate for rendering chat blocks in the ListView +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +ColumnLayout { + id: root + spacing: 2 + + // Required properties from model + required property string blockType + required property string blockName + required property var blockSegments + + // Reference to parent ListView for scroll control + property var chatView: null + + // Expand state based on block type + property bool blockExpanded: { + switch (blockType) { + case "user": + case "assistant": + case "tool": + case "info": + case "retry": + case "interrupt": + 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 "reasoning": return Qt.lighter(bg, 1.3) + 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 "interrupt": 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 "reasoning": return Qt.darker(bg, 1.03) + 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 "interrupt": return "#fff8e1" + default: return bg + } + } + } + + // Header label text based on block type + function headerText() { + switch (blockType) { + case "user": return "\uD83E\uDDD1 You" + case "assistant": return "\uD83E\uDD16 Ollie" + case "reasoning": return "Reasoning" + case "call": return "\u25B6\uFE0F " + blockName + case "tool": return "\uD83D\uDDA5\uFE0F " + blockName + case "error": return "\u274C Error" + case "info": return "\u2139\uFE0F Info" + case "retry": return "Retry" + case "interrupt": return "\u26A0\uFE0F User Interrupt" + default: return "" + } + } + + // Clickable header + Rectangle { + Layout.fillWidth: true + height: 22 + radius: 3 + color: root.headerColor() + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.blockExpanded = !root.blockExpanded + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 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 + } + } + } + + // Block content rendered as segments + Column { + Layout.fillWidth: true + visible: root.blockExpanded + spacing: 4 + + Repeater { + model: root.blockSegments || [] + + delegate: Loader { + width: parent ? parent.width : 0 + active: modelData !== null && modelData !== undefined + sourceComponent: (modelData && modelData.type === "diagram") ? diagramComponent : textComponent + property var segmentData: modelData || {} + } + } + } + + // Text segment component + Component { + id: textComponent + + TextEdit { + id: textEdit + width: parent ? parent.width : 100 + text: segmentData.html || "" + textFormat: Text.RichText + readOnly: true + selectByMouse: true + persistentSelection: true + wrapMode: TextEdit.Wrap + font.family: theme.fontFamily || Qt.application.font.family + font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 + selectionColor: theme.highlightColor + selectedTextColor: theme.highlightedTextColor + + onActiveFocusChanged: { + if (!activeFocus && root.chatView) + root.chatView.interactive = true + } + + Menu { + id: contextMenu + MenuItem { + text: "Copy" + enabled: textEdit.selectedText.length > 0 + onTriggered: textEdit.copy() + } + MenuItem { + text: "Select All" + onTriggered: textEdit.selectAll() + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + cursorShape: Qt.IBeamCursor + onPressed: function(mouse) { + if (root.chatView) root.chatView.interactive = false + mouse.accepted = false + } + onReleased: function(mouse) { + if (root.chatView) root.chatView.interactive = true + mouse.accepted = false + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + cursorShape: Qt.IBeamCursor + onClicked: contextMenu.popup() + } + } + } + + // Diagram segment component + Component { + id: diagramComponent + + Rectangle { + id: diagramRect + width: parent ? parent.width : 100 + height: diagramContent.height + diagramHeader.height + 8 + color: "transparent" + border.color: theme.midColor + border.width: 1 + radius: 4 + + property bool showSource: false + + RowLayout { + id: diagramHeader + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 4 + height: 24 + + Label { + text: segmentData.lang || "diagram" + font.pointSize: theme.fontSize > 0 ? theme.fontSize - 1 : 11 + color: theme.textColor + opacity: 0.7 + } + + Item { Layout.fillWidth: true } + + Button { + text: diagramRect.showSource ? "📊" : "" + font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10 + implicitWidth: 32 + implicitHeight: 20 + ToolTip.visible: hovered + ToolTip.text: diagramRect.showSource ? "Show diagram" : "Show source" + onClicked: diagramRect.showSource = !diagramRect.showSource + } + + Button { + text: "📋" + font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10 + implicitWidth: 32 + implicitHeight: 20 + ToolTip.visible: hovered + ToolTip.text: diagramRect.showSource ? "Copy source" : "Copy image" + onClicked: { + if (diagramRect.showSource) { + clipboard.copyText(segmentData.source) + } else if (segmentData.svg) { + clipboard.copyImage(segmentData.svg) + } + } + } + } + + Item { + id: diagramContent + anchors.top: diagramHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 4 + height: diagramRect.showSource ? sourceEdit.height : imageColumn.height + + TextEdit { + id: sourceEdit + width: parent.width + visible: diagramRect.showSource + text: segmentData.html || "" + textFormat: Text.RichText + readOnly: true + selectByMouse: true + persistentSelection: true + wrapMode: TextEdit.Wrap + font.family: theme.fontFamily || Qt.application.font.family + font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 + selectionColor: theme.highlightColor + selectedTextColor: theme.highlightedTextColor + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + cursorShape: Qt.IBeamCursor + onPressed: function(mouse) { + if (root.chatView) root.chatView.interactive = false + mouse.accepted = false + } + onReleased: function(mouse) { + if (root.chatView) root.chatView.interactive = true + mouse.accepted = false + } + } + } + + Column { + id: imageColumn + width: parent.width + visible: !diagramRect.showSource + + Rectangle { + visible: segmentData.svg && !segmentData.loading + color: "white" + width: diagramImg.width + height: diagramImg.height + + Image { + id: diagramImg + source: segmentData.svg || "" + fillMode: Image.PreserveAspectFit + width: Math.min(implicitWidth, imageColumn.width) + } + } + + Label { + visible: segmentData.loading + text: "Loading " + (segmentData.lang || "diagram") + "..." + font.italic: true + color: theme.textColor + opacity: 0.7 + } + } + } + } + } +} diff --git a/gui/NewAgentDialog.qml b/gui/NewAgentDialog.qml new file mode 100644 index 0000000..76d5b1f --- /dev/null +++ b/gui/NewAgentDialog.qml @@ -0,0 +1,174 @@ +// NewAgentDialog.qml — Dialog for creating a new agent in a session +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Dialog { + id: root + title: "New Agent" + anchors.centerIn: parent + width: 420 + modal: true + + // Session ID to create the agent in + property string targetSessionId: "" + + signal agentCreated() + + contentItem: ColumnLayout { + spacing: 16 + Layout.margins: 16 + + Label { + text: "Agent" + font.bold: true + font.pointSize: theme.fontSize > 0 ? theme.fontSize + 1 : Qt.application.font.pointSize + 1 + } + + Label { + id: errorLabel + Layout.fillWidth: true + visible: text !== "" + color: "red" + wrapMode: Text.Wrap + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Directory:"; Layout.preferredWidth: 80 } + TextField { + id: dirField + Layout.fillWidth: true + placeholderText: "/path/to/project" + } + Button { + text: "Browse..." + onClicked: { + var folder = theme.chooseFolder("Select Project Directory", dirField.text || "") + if (folder !== "") + dirField.text = folder + } + } + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Backend:"; Layout.preferredWidth: 80 } + ComboBox { + id: backendCombo + Layout.fillWidth: true + enabled: ollie.rootBackendsLoaded + model: ollie.availableBackends + currentIndex: 0 + editable: true + onAccepted: backendCombo.editText = backendCombo.currentText + } + } + + ListModel { id: modelList } + + RowLayout { + Layout.fillWidth: true + Label { text: "Model:"; Layout.preferredWidth: 80 } + ComboBox { + id: modelCombo + Layout.fillWidth: true + enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded + model: modelList + currentIndex: -1 + editable: true + onAccepted: modelCombo.editText = modelCombo.currentText + } + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Profile:"; Layout.preferredWidth: 80 } + ComboBox { + id: agentCombo + Layout.fillWidth: true + enabled: ollie.rootAgentsLoaded + model: ollie.availableAgents + currentIndex: 0 + } + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Name:"; Layout.preferredWidth: 80 } + TextField { + id: aliasField + Layout.fillWidth: true + placeholderText: "(auto-generated)" + } + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Remote:"; Layout.preferredWidth: 80 } + TextField { + id: remoteField + Layout.fillWidth: true + placeholderText: "user@host:port" + } + } + + Component.onCompleted: { + if (!ollie.rootBackendsLoaded) ollie.loadRootBackends() + if (!ollie.rootAgentsLoaded) ollie.loadRootAgents() + } + + Connections { + target: backendCombo + function onCurrentTextChanged() { + var be = backendCombo.currentText.trim() + modelList.clear() + if (be !== "" && ollie.rootModelsLoaded) { + var models = ollie.getAvailableModels(be) + for (var i = 0; i < models.length; ++i) { + modelList.append({ modelData: models[i] }) + } + modelCombo.currentIndex = models.length > 0 ? 0 : -1 + } else { + modelCombo.currentIndex = -1 + } + } + } + } + + footer: RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + Button { + text: "OK" + onClicked: { + errorLabel.text = "" + var cwd = dirField.text.trim() + if (cwd === "") { + errorLabel.text = "Directory is required" + return + } + var backend = backendCombo.currentText.trim() + var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : "" + var agent = agentCombo.currentText + var remote = remoteField.text.trim() + var agentAlias = aliasField.text.trim() + + var err = ollie.createAgent(root.targetSessionId, cwd, backend, model, agent, remote, agentAlias) + if (err === "") { + ollie.refreshSessions() + root.agentCreated() + root.close() + } else { + errorLabel.text = err + } + } + } + Button { + text: "Cancel" + onClicked: root.close() + } + } + + onOpened: errorLabel.text = "" +} diff --git a/gui/NewSessionDialog.qml b/gui/NewSessionDialog.qml new file mode 100644 index 0000000..4cdd374 --- /dev/null +++ b/gui/NewSessionDialog.qml @@ -0,0 +1,79 @@ +// NewSessionDialog.qml — Dialog for creating a new session +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Dialog { + id: root + title: "New Session" + anchors.centerIn: parent + width: 380 + modal: true + + signal sessionCreated() + + contentItem: ColumnLayout { + spacing: 16 + Layout.margins: 16 + + Label { + id: errorLabel + Layout.fillWidth: true + visible: text !== "" + color: "red" + wrapMode: Text.Wrap + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Name:"; Layout.preferredWidth: 80 } + TextField { + id: nameField + Layout.fillWidth: true + placeholderText: "(auto-generated)" + } + } + + RowLayout { + Layout.fillWidth: true + Label { text: "Remote:"; Layout.preferredWidth: 80 } + TextField { + id: remoteField + Layout.fillWidth: true + placeholderText: "user@host (optional)" + } + } + } + + footer: RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + Button { + text: "OK" + onClicked: { + errorLabel.text = "" + var name = nameField.text.trim() + var remote = remoteField.text.trim() + + var err = ollie.createSession(name, remote) + if (err === "") { + ollie.refreshSessions() + root.sessionCreated() + root.close() + } else { + errorLabel.text = err + } + } + } + Button { + text: "Cancel" + onClicked: root.close() + } + } + + onOpened: { + errorLabel.text = "" + nameField.text = "" + remoteField.text = "" + } +} diff --git a/gui/SettingsDialog.qml b/gui/SettingsDialog.qml new file mode 100644 index 0000000..04221de --- /dev/null +++ b/gui/SettingsDialog.qml @@ -0,0 +1,118 @@ +// SettingsDialog.qml — Theme and font settings dialog +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Dialog { + id: root + 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 + } + + Label { + text: "Uses KDE Breeze color schemes from /usr/share/color-schemes/" + opacity: 0.6 + wrapMode: Text.Wrap + Layout.fillWidth: true + font.pixelSize: Qt.application.font.pixelSize * 0.9 + } + + ColumnLayout { + spacing: 8 + + RadioButton { + text: "Use system theme" + checked: theme.mode === "system" + onClicked: theme.mode = "system" + } + RadioButton { + text: "Breeze Light" + checked: theme.mode === "light" + onClicked: theme.mode = "light" + } + RadioButton { + text: "Breeze Dark" + checked: theme.mode === "dark" + onClicked: theme.mode = "dark" + } + } + + Label { + text: theme.isDark ? "Current: dark" : "Current: light" + opacity: 0.5 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + 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() + } + } +} diff --git a/gui/main.qml b/gui/main.qml index 826cc96..6e275c9 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -531,371 +531,19 @@ ApplicationWindow { } // --- Settings Dialog --- - Dialog { + SettingsDialog { 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 - } - - Label { - text: "Uses KDE Breeze color schemes from /usr/share/color-schemes/" - opacity: 0.6 - wrapMode: Text.Wrap - Layout.fillWidth: true - font.pixelSize: Qt.application.font.pixelSize * 0.9 - } - - ColumnLayout { - spacing: 8 - - RadioButton { - id: systemRadio - text: "Use system theme" - checked: theme.mode === "system" - onClicked: theme.mode = "system" - } - RadioButton { - id: lightRadio - text: "Breeze Light" - checked: theme.mode === "light" - onClicked: theme.mode = "light" - } - RadioButton { - id: darkRadio - text: "Breeze Dark" - checked: theme.mode === "dark" - onClicked: theme.mode = "dark" - } - } - - Label { - text: theme.isDark ? "Current: dark" : "Current: light" - opacity: 0.5 - font.pixelSize: Qt.application.font.pixelSize * 0.85 - } - - 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() - } - } } // --- New Session Dialog --- - Dialog { + NewSessionDialog { id: newSessionDialog - title: "New Session" - anchors.centerIn: parent - width: 380 - modal: true - - contentItem: ColumnLayout { - spacing: 16 - Layout.margins: 16 - - Label { - id: newSessionErrorLabel - Layout.fillWidth: true - visible: text !== "" - color: "red" - wrapMode: Text.Wrap - } - - // Name - RowLayout { - Layout.fillWidth: true - Label { text: "Name:"; Layout.preferredWidth: 80 } - TextField { - id: sessionNameField - Layout.fillWidth: true - placeholderText: "(auto-generated)" - } - } - - // Remote - RowLayout { - Layout.fillWidth: true - Label { text: "Remote:"; Layout.preferredWidth: 80 } - TextField { - id: sessionRemoteField - Layout.fillWidth: true - placeholderText: "user@host (optional)" - } - } - } - - footer: RowLayout { - Layout.fillWidth: true - Item { Layout.fillWidth: true } - Button { - text: "OK" - onClicked: { - newSessionErrorLabel.text = "" - var name = sessionNameField.text.trim() - var remote = sessionRemoteField.text.trim() - - var err = ollie.createSession(name, remote) - if (err === "") { - ollie.refreshSessions() - newSessionDialog.close() - } else { - newSessionErrorLabel.text = err - } - } - } - Button { - text: "Cancel" - onClicked: newSessionDialog.close() - } - } - - onOpened: { - newSessionErrorLabel.text = "" - sessionNameField.text = "" - sessionRemoteField.text = "" - } } // --- New Agent Dialog --- - Dialog { + NewAgentDialog { id: newAgentDialog - title: "New Agent" - anchors.centerIn: parent - width: 420 - modal: true - - contentItem: ColumnLayout { - spacing: 16 - Layout.margins: 16 - - // --- Agent --- - Label { text: "Agent"; font.bold: true; font.pointSize: theme.fontSize > 0 ? theme.fontSize + 1 : Qt.application.font.pointSize + 1 } - - // Error label - Label { - id: newAgentErrorLabel - Layout.fillWidth: true - visible: text !== "" - color: "red" - wrapMode: Text.Wrap - } - - // Directory - RowLayout { - Layout.fillWidth: true - Label { text: "Directory:"; Layout.preferredWidth: 80 } - TextField { - id: dirField - Layout.fillWidth: true - placeholderText: "/path/to/project" - readOnly: false - } - Button { - text: "Browse..." - onClicked: { - var folder = theme.chooseFolder("Select Project Directory", dirField.text || "") - if (folder !== "") - dirField.text = folder - } - } - } - - // Backend - RowLayout { - Layout.fillWidth: true - Label { text: "Backend:"; Layout.preferredWidth: 80 } - ComboBox { - id: backendCombo - Layout.fillWidth: true - enabled: ollie.rootBackendsLoaded - model: ollie.availableBackends - currentIndex: 0 - editable: true - onAccepted: backendCombo.editText = backendCombo.currentText - } - } - - // Model list (populated reactively from backend selection) - ListModel { id: modelList } - - // Model - RowLayout { - Layout.fillWidth: true - Label { text: "Model:"; Layout.preferredWidth: 80 } - ComboBox { - id: modelCombo - Layout.fillWidth: true - enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded - model: modelList - currentIndex: -1 - editable: true - onAccepted: modelCombo.editText = modelCombo.currentText - } - } - - // Profile - RowLayout { - Layout.fillWidth: true - Label { text: "Profile:"; Layout.preferredWidth: 80 } - ComboBox { - id: agentCombo - Layout.fillWidth: true - enabled: ollie.rootAgentsLoaded - model: ollie.availableAgents - currentIndex: 0 - } - } - - // Alias - RowLayout { - Layout.fillWidth: true - Label { text: "Name:"; Layout.preferredWidth: 80 } - TextField { - id: agentAliasField - Layout.fillWidth: true - placeholderText: "(auto-generated)" - } - } - - // Remote - RowLayout { - Layout.fillWidth: true - Label { text: "Remote:"; Layout.preferredWidth: 80 } - TextField { - id: remoteField - Layout.fillWidth: true - placeholderText: "user@host:port" - } - } - - Component.onCompleted: { - if (!ollie.rootBackendsLoaded) ollie.loadRootBackends() - if (!ollie.rootAgentsLoaded) ollie.loadRootAgents() - } - - Connections { - target: backendCombo - function onCurrentTextChanged() { - var be = backendCombo.currentText.trim() - modelList.clear() - if (be !== "" && ollie.rootModelsLoaded) { - var models = ollie.getAvailableModels(be) - for (var i = 0; i < models.length; ++i) { - modelList.append({ modelData: models[i] }) - } - if (models.length > 0) { - modelCombo.currentIndex = 0 - } else { - modelCombo.currentIndex = -1 - } - } else { - modelCombo.currentIndex = -1 - } - } - } - } - - footer: RowLayout { - Layout.fillWidth: true - Item { Layout.fillWidth: true } - Button { - text: "OK" - onClicked: { - newAgentErrorLabel.text = "" - var cwd = dirField.text.trim() - if (cwd === "") { - newAgentErrorLabel.text = "Directory is required" - return - } - var backend = backendCombo.currentText.trim() - var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : "" - var agent = agentCombo.currentText - var remote = remoteField.text.trim() - var agentAlias = agentAliasField.text.trim() - - var err = ollie.createAgent(root.newAgentTargetSessionId, cwd, backend, model, agent, remote, agentAlias) - if (err === "") { - ollie.refreshSessions() - newAgentDialog.close() - } else { - newAgentErrorLabel.text = err - } - } - } - Button { - text: "Cancel" - onClicked: newAgentDialog.close() - } - } - - onOpened: newAgentErrorLabel.text = "" + targetSessionId: root.newAgentTargetSessionId } SplitView { @@ -1279,313 +927,10 @@ ApplicationWindow { } } -delegate: ColumnLayout { - id: blockItem - width: chatView.width - 8 - spacing: 2 - -property bool blockExpanded: { - switch (blockType) { - case "user": return true - case "assistant": return true - case "tool": return true - case "info": return true - case "retry": return true - case "interrupt": return true - default: return false + delegate: ChatBlockDelegate { + width: chatView.width - 8 + chatView: chatView } - } - - // Clickable header - Rectangle { - Layout.fillWidth: true - height: 22 - radius: 3 - color: { - 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 "reasoning": return Qt.lighter(bg, 1.3) - 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 "interrupt": 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 "reasoning": return Qt.darker(bg, 1.03) - 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 "interrupt": return "#fff8e1" - default: return bg - } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: blockItem.blockExpanded = !blockItem.blockExpanded - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 6 - spacing: 4 - - Label { - text: blockItem.blockExpanded ? "▼" : "▶" - font.pixelSize: 8 - opacity: 0.6 - } - - Label { - Layout.fillWidth: true - text: { - switch (blockType) { - case "user": return "\uD83E\uDDD1 You" - case "assistant": return "\uD83E\uDD16 Ollie" - case "reasoning": return "Reasoning" - case "call": return "\u25B6\uFE0F " + blockName - case "tool": return "\uD83D\uDDA5\uFE0F " + blockName - case "error": return "\u274C Error" - case "info": return "\u2139\uFE0F Info" - case "retry": return "Retry" - case "interrupt": return "\u26A0\uFE0F User Interrupt" - default: return "" - } - } - font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11 - font.bold: true - color: theme.textColor - opacity: 0.9 - } - } - } - - // Block content rendered as segments (text and diagrams) - Column { - Layout.fillWidth: true - visible: blockItem.blockExpanded - spacing: 4 - - Repeater { - model: blockSegments || [] - - delegate: Loader { - width: parent ? parent.width : 0 - active: modelData !== null && modelData !== undefined - sourceComponent: (modelData && modelData.type === "diagram") ? diagramComponent : textComponent - - property var segmentData: modelData || {} - } - } - } - - // Text segment component - Component { - id: textComponent - - TextEdit { - id: textSegmentEdit - width: parent ? parent.width : 100 - text: segmentData.html || "" - textFormat: Text.RichText - readOnly: true - selectByMouse: true - persistentSelection: true - wrapMode: TextEdit.Wrap - font.family: theme.fontFamily || Qt.application.font.family - font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 - selectionColor: theme.highlightColor - selectedTextColor: theme.highlightedTextColor - - onActiveFocusChanged: { - if (!activeFocus) - chatView.interactive = true - } - - Menu { - id: textSegmentMenu - MenuItem { - text: "Copy" - enabled: textSegmentEdit.selectedText.length > 0 - onTriggered: textSegmentEdit.copy() - } - MenuItem { - text: "Select All" - onTriggered: textSegmentEdit.selectAll() - } - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - propagateComposedEvents: true - cursorShape: Qt.IBeamCursor - onPressed: function(mouse) { - chatView.interactive = false - mouse.accepted = false - } - onReleased: function(mouse) { - chatView.interactive = true - mouse.accepted = false - } - } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - cursorShape: Qt.IBeamCursor - onClicked: textSegmentMenu.popup() - } - } - } - - // Diagram segment component with source/image toggle - Component { - id: diagramComponent - - Rectangle { - width: parent ? parent.width : 100 - height: diagramContent.height + diagramHeader.height + 8 - color: "transparent" - border.color: theme.midColor - border.width: 1 - radius: 4 - - property bool showSource: false - - // Header with language label and toggle button - RowLayout { - id: diagramHeader - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 4 - height: 24 - - Label { - text: segmentData.lang || "diagram" - font.pointSize: theme.fontSize > 0 ? theme.fontSize - 1 : 11 - color: theme.textColor - opacity: 0.7 - } - - Item { Layout.fillWidth: true } - - Button { - text: parent.parent.showSource ? "📊" : "" - font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10 - implicitWidth: 32 - implicitHeight: 20 - ToolTip.visible: hovered - ToolTip.text: parent.parent.showSource ? "Show diagram" : "Show source" - onClicked: parent.parent.showSource = !parent.parent.showSource - } - - Button { - text: "📋" - font.pointSize: theme.fontSize > 0 ? theme.fontSize - 2 : 10 - implicitWidth: 32 - implicitHeight: 20 - ToolTip.visible: hovered - ToolTip.text: parent.parent.showSource ? "Copy source" : "Copy image" - onClicked: { - if (parent.parent.showSource) { - // Copy source code as plain text - clipboard.copyText(segmentData.source) - } else if (segmentData.svg) { - // Copy image - clipboard.copyImage(segmentData.svg) - } - } - } - } - - // Content area - Item { - id: diagramContent - anchors.top: diagramHeader.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 4 - height: showSource ? diagramSourceEdit.height : diagramImage.height - - // Source view - TextEdit { - id: diagramSourceEdit - width: parent.width - visible: showSource - text: segmentData.html || "" - textFormat: Text.RichText - readOnly: true - selectByMouse: true - persistentSelection: true - wrapMode: TextEdit.Wrap - font.family: theme.fontFamily || Qt.application.font.family - font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 - selectionColor: theme.highlightColor - selectedTextColor: theme.highlightedTextColor - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - propagateComposedEvents: true - cursorShape: Qt.IBeamCursor - onPressed: function(mouse) { - chatView.interactive = false - mouse.accepted = false - } - onReleased: function(mouse) { - chatView.interactive = true - mouse.accepted = false - } - } - } - - // Image view - Column { - id: diagramImage - width: parent.width - visible: !showSource - - Rectangle { - visible: segmentData.svg && !segmentData.loading - color: "white" - width: diagramImg.width - height: diagramImg.height - - Image { - id: diagramImg - source: segmentData.svg || "" - fillMode: Image.PreserveAspectFit - width: Math.min(implicitWidth, diagramImage.width) - } - } - - Label { - visible: segmentData.loading - text: "Loading " + (segmentData.lang || "diagram") + "..." - font.italic: true - color: theme.textColor - opacity: 0.7 - } - } - } - } - } - } } } diff --git a/gui/qml.qrc b/gui/qml.qrc index 5f6483a..fc3fe43 100644 --- a/gui/qml.qrc +++ b/gui/qml.qrc @@ -1,5 +1,9 @@ main.qml + SettingsDialog.qml + NewSessionDialog.qml + NewAgentDialog.qml + ChatBlockDelegate.qml