diff --git a/gui/main.qml b/gui/main.qml index 6d592f7..6e53ef9 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -142,10 +142,10 @@ ApplicationWindow { } Component.onCompleted: { - var log = ollie.readLog() + var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId) if (log) { chatModel.appendData(log) - chatModel.trimFront(30) + chatModel.trimFront(20) } scrollTimer.start() buildSessionTree() @@ -155,10 +155,10 @@ ApplicationWindow { target: ollie function onActiveSessionIdChanged() { chatModel.reset() - var log = ollie.readLog() + var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId) if (log) { chatModel.appendData(log) - chatModel.trimFront(30) + chatModel.trimFront(20) } scrollTimer.start() // Expand the active session in the tree @@ -335,31 +335,32 @@ ApplicationWindow { RowLayout { Layout.fillWidth: true Label { text: "Backend:"; Layout.preferredWidth: 80 } - TextField { - id: backendField + ComboBox { + id: backendCombo Layout.fillWidth: true - placeholderText: "ollama" - } - Button { - text: "..." - onClicked: backendPicker.open() + 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 } - TextField { - id: modelField + ComboBox { + id: modelCombo Layout.fillWidth: true - placeholderText: "(auto-populated)" - enabled: backendField.text.trim() !== "" - } - Button { - text: "..." - enabled: backendField.text.trim() !== "" && ollie.rootModelsLoaded - onClicked: modelPicker.open() + enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded + model: modelList + currentIndex: -1 + editable: true + onAccepted: modelCombo.editText = modelCombo.currentText } } @@ -387,70 +388,28 @@ ApplicationWindow { } } - // Backend picker dialog - Dialog { - id: backendPicker - title: "Select Backend" - anchors.centerIn: parent - modal: true - standardButtons: Dialog.Cancel - - contentItem: ListView { - width: 300 - height: 200 - model: ollie.availableBackends - delegate: ItemDelegate { - width: parent.width - text: modelData - onClicked: { - backendField.text = modelData - backendPicker.close() - } - } - } - } - - // Model picker dialog - Dialog { - id: modelPicker - title: "Select Model" - anchors.centerIn: parent - modal: true - standardButtons: Dialog.Cancel - - contentItem: ListView { - width: 400 - height: 300 - model: ollie.getAvailableModels(backendField.text.trim()) - delegate: ItemDelegate { - width: parent.width - text: modelData - onClicked: { - modelField.text = modelData - modelPicker.close() - } - } - } - } - Component.onCompleted: { if (!ollie.rootBackendsLoaded) ollie.loadRootBackends() if (!ollie.rootAgentsLoaded) ollie.loadRootAgents() } Connections { - target: backendField - function onTextChanged() { - var be = backendField.text.trim() + 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) { - modelField.placeholderText = models[0] + modelCombo.currentIndex = 0 } else { - modelField.placeholderText = "(no models)" + modelCombo.currentIndex = -1 } } else { - modelField.placeholderText = "(auto-populated)" + modelCombo.currentIndex = -1 } } } @@ -463,8 +422,8 @@ ApplicationWindow { return } var name = nameField.text.trim() - var backend = backendField.text.trim() - var model = modelField.text.trim() + var backend = backendCombo.currentText.trim() + var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : "" var agent = agentCombo.currentText var remote = remoteField.text.trim() @@ -647,102 +606,113 @@ ApplicationWindow { } delegate: ColumnLayout { + id: blockItem width: chatView.width - 8 spacing: 2 - // Header decoration - Rectangle { - Layout.fillWidth: true - height: 22 - radius: 3 - color: { - // Derive header colors from base background with subtle tint. - // Always readable because text uses theme.textColor. - var bg = theme.baseColor - if (theme.isDark) { - // Dark theme: lighter tints against dark background - 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) - default: return bg - } - } else { - // Light theme: slightly darker tints - 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) - default: return bg - } - } - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 6 - spacing: 4 - - Label { - text: { - switch (blockType) { - case "user": return "User" - case "assistant": return "Assistant" + (blockName ? " (" + blockName + ")" : "") - case "reasoning": return "Reasoning" - case "call": return "Call: " + blockName - case "tool": return "Tool: " + blockName - case "error": return "Error" - case "info": return "Info" - default: return "" - } - } - font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11 - font.bold: true - color: theme.textColor - opacity: 0.9 - } + property bool blockExpanded: { + switch (blockType) { + case "user": return true + case "assistant": return true + case "tool": return true + default: return false } } - ColumnLayout { + // Clickable header + Rectangle { Layout.fillWidth: true - Layout.leftMargin: 4 - Layout.rightMargin: 4 - Layout.bottomMargin: 4 - spacing: 0 - - // Rendered HTML (fenced code/diff) — shown when available - TextEdit { - Layout.fillWidth: true - visible: blockRenderedHtml && blockRenderedHtml.length > 0 - text: blockRenderedHtml - textFormat: Text.RichText - readOnly: true - wrapMode: TextEdit.Wrap - font.family: theme.fontFamily || "monospace" - font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 - color: theme.textColor + 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) + 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) + default: return bg + } + } } - // Plain text fallback (prose, streaming content) - Label { - Layout.fillWidth: true - visible: (!blockRenderedHtml || blockRenderedHtml.length === 0) - text: blockContent || "" - wrapMode: Text.Wrap - textFormat: Text.PlainText - font.family: theme.fontFamily || "monospace" - font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 - color: theme.textColor + 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 "User" + case "assistant": return "Assistant" + (blockName ? " (" + blockName + ")" : "") + case "reasoning": return "Reasoning" + case "call": return "Call: " + blockName + case "tool": return "Tool: " + blockName + case "error": return "Error" + case "info": return "Info" + default: return "" + } + } + font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11 + font.bold: true + color: theme.textColor + opacity: 0.9 + } + } + } + + // Rendered HTML (fenced code/diff) + TextEdit { + Layout.fillWidth: true + visible: blockItem.blockExpanded && blockRenderedHtml && blockRenderedHtml.length > 0 + text: blockRenderedHtml + textFormat: Text.RichText + readOnly: true + wrapMode: TextEdit.Wrap + font.family: theme.fontFamily || "monospace" + font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 + color: theme.textColor + } + + // Plain text fallback + Label { + Layout.fillWidth: true + visible: blockItem.blockExpanded && (!blockRenderedHtml || blockRenderedHtml.length === 0) + text: blockContent || "" + wrapMode: Text.Wrap + textFormat: Text.PlainText + font.family: theme.fontFamily || "monospace" + font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 + color: theme.textColor } } } diff --git a/gui/ollie9pclient.cpp b/gui/ollie9pclient.cpp index 3db27c3..0fce3db 100644 --- a/gui/ollie9pclient.cpp +++ b/gui/ollie9pclient.cpp @@ -110,6 +110,13 @@ QString Ollie9pClient::readLog() return QString::fromUtf8(out); } +QString Ollie9pClient::readLogForSession(const QString &sessionId, const QString &agentId) +{ + if (sessionId.isEmpty() || agentId.isEmpty()) return {}; + QByteArray out = run9p({"read", "session/" + sessionId + "/agent/" + agentId + "/log"}); + return QString::fromUtf8(out); +} + bool Ollie9pClient::submit(const QString &prompt) { if (m_activeSessionId.isEmpty() || prompt.trimmed().isEmpty()) return false; @@ -244,7 +251,7 @@ bool Ollie9pClient::createSession(const QString &cwd, const QString &name, const if (!remote.isEmpty()) args << "remote=" + remote; QProcess proc; - QStringList fullArgs = {"-a", serverAddr(), "write", "new"}; + QStringList fullArgs = {"-a", serverAddr(), "write", "session/new"}; proc.start(ninepBin(), fullArgs); proc.waitForStarted(3000); @@ -280,7 +287,9 @@ void Ollie9pClient::ensureRootDataLoaded() QString be = line.left(tab).trimmed(); QString mo = line.mid(tab + 1).trimmed(); if (!be.isEmpty() && !mo.isEmpty()) { - m_rootModels[be].toStringList().append(mo); + QStringList list = m_rootModels[be].toStringList(); + list.append(mo); + m_rootModels[be] = QVariant(list); } } m_rootModelsLoaded = true; diff --git a/gui/ollie9pclient.h b/gui/ollie9pclient.h index ed11edc..b028804 100644 --- a/gui/ollie9pclient.h +++ b/gui/ollie9pclient.h @@ -46,6 +46,7 @@ public: Q_INVOKABLE void refreshSessions(); Q_INVOKABLE QString readLog(); + Q_INVOKABLE QString readLogForSession(const QString &sessionId, const QString &agentId); Q_INVOKABLE bool submit(const QString &prompt); Q_INVOKABLE bool interrupt(); Q_INVOKABLE bool kill();