From dd6755befa5b13683068953be6dcaf485025c98d Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Mon, 12 Oct 2026 14:30:37 +0200 Subject: [PATCH] GUI: add Ctrl+B2 'Send to' menu for cross-agent messaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ctrl+middle-click on selection opens cascading menu - Session list → Agent list navigation - ollie.submitTo(sessionId, agentId, prompt) via 9P - Fixed nested Instantiator scope with explicit property capture - Theme colors for ProcManager/PeerManager windows - Kate plugin uses 'default' session instead of 'kate' --- kde/gui/ChatBlockDelegate.qml | 45 +++++++++++++++++++++++++++++++++-- kde/gui/PeerManagerWindow.qml | 19 ++++++++------- kde/gui/ProcManagerWindow.qml | 3 ++- kde/gui/ollie9pclient.cpp | 9 +++++++ kde/gui/ollie9pclient.h | 1 + kde/kate/ollie_kate.cpp | 26 ++++++++++---------- 6 files changed, 78 insertions(+), 25 deletions(-) diff --git a/kde/gui/ChatBlockDelegate.qml b/kde/gui/ChatBlockDelegate.qml index 9344814..f8c68a0 100644 --- a/kde/gui/ChatBlockDelegate.qml +++ b/kde/gui/ChatBlockDelegate.qml @@ -13,6 +13,9 @@ Column { 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) { @@ -142,7 +145,7 @@ Column { topPadding: 4 bottomPadding: 4 - // Acme-style mouse handling: B2=execute, B3=plumb + // Acme-style mouse handling: B2=execute, B3=plumb, Ctrl+B2=send to MouseArea { anchors.fill: parent acceptedButtons: Qt.MiddleButton | Qt.RightButton @@ -163,7 +166,13 @@ Column { if (mouse.button === Qt.MiddleButton) { var sel = contentArea.selectedText if (sel && sel.trim() !== "") { - ollie.submit(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) { @@ -209,4 +218,36 @@ Column { 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) } + } + } } diff --git a/kde/gui/PeerManagerWindow.qml b/kde/gui/PeerManagerWindow.qml index 0c9d7d3..dec3b20 100644 --- a/kde/gui/PeerManagerWindow.qml +++ b/kde/gui/PeerManagerWindow.qml @@ -10,6 +10,7 @@ Window { height: 500 minimumWidth: 500 minimumHeight: 400 + color: theme.baseColor property string currentSessionId: ollie.activeSessionId property string currentAgentId: ollie.activeAgentId @@ -138,8 +139,8 @@ Window { Rectangle { Layout.fillWidth: true Layout.fillHeight: true - color: palette.base - border.color: palette.mid + color: theme.baseColor + border.color: theme.midColor border.width: 1 clip: true @@ -158,7 +159,7 @@ Window { onPaint: { var ctx = getContext("2d") ctx.reset() - ctx.fillStyle = palette.base + ctx.fillStyle = theme.baseColor ctx.fillRect(0, 0, width, height) ctx.save() @@ -168,7 +169,7 @@ Window { var nodeRadius = 30 // Draw edges first - ctx.strokeStyle = palette.highlight + ctx.strokeStyle = theme.highlightColor ctx.lineWidth = 2 for (var i = 0; i < agents.length; i++) { var a = agents[i] @@ -221,19 +222,19 @@ Window { // Border if (isSelected || isConnecting) { - ctx.strokeStyle = palette.highlightedText + ctx.strokeStyle = theme.highlightedTextColor ctx.lineWidth = 3 } else if (isActive) { - ctx.strokeStyle = palette.highlight + ctx.strokeStyle = theme.highlightColor ctx.lineWidth = 2 } else { - ctx.strokeStyle = palette.text + ctx.strokeStyle = theme.textColor ctx.lineWidth = 1 } ctx.stroke() // Label - ctx.fillStyle = palette.text + ctx.fillStyle = theme.textColor ctx.font = "12px sans-serif" ctx.textAlign = "center" ctx.textBaseline = "middle" @@ -458,7 +459,7 @@ Window { Label { visible: connectingFrom !== null text: "Click another node to connect, or empty space to cancel" - color: palette.highlight + color: theme.highlightColor } Item { Layout.fillWidth: true } diff --git a/kde/gui/ProcManagerWindow.qml b/kde/gui/ProcManagerWindow.qml index 47ca36b..55c62a0 100644 --- a/kde/gui/ProcManagerWindow.qml +++ b/kde/gui/ProcManagerWindow.qml @@ -10,6 +10,7 @@ Window { height: 450 minimumWidth: 400 minimumHeight: 300 + color: theme.baseColor property var procs: [] property string outputText: "" @@ -127,7 +128,7 @@ Window { delegate: Rectangle { width: procList.width height: 44 - color: selectedPid === modelData.pid ? palette.highlight : (index % 2 ? palette.alternateBase : palette.base) + color: selectedPid === modelData.pid ? theme.highlightColor : (index % 2 ? Qt.darker(theme.baseColor, 1.05) : theme.baseColor) MouseArea { anchors.fill: parent diff --git a/kde/gui/ollie9pclient.cpp b/kde/gui/ollie9pclient.cpp index ab037e9..2a34491 100644 --- a/kde/gui/ollie9pclient.cpp +++ b/kde/gui/ollie9pclient.cpp @@ -480,6 +480,15 @@ bool Ollie9pClient::submit(const QString &prompt) return m_9p->write(agentPath() + "/prompt", prompt.toUtf8()); } +bool Ollie9pClient::submitTo(const QString &sessionId, const QString &agentId, const QString &prompt) +{ + if (sessionId.isEmpty() || agentId.isEmpty() || prompt.trimmed().isEmpty()) return false; + if (!m_9p || !m_9p->isConnected()) return false; + + QString path = "session/" + sessionId + "/agent/" + agentId + "/prompt"; + return m_9p->write(path, prompt.toUtf8()); +} + bool Ollie9pClient::interrupt() { if (m_activeSessionId.isEmpty() || m_agentId.isEmpty()) return false; diff --git a/kde/gui/ollie9pclient.h b/kde/gui/ollie9pclient.h index f58a94d..c480c1b 100644 --- a/kde/gui/ollie9pclient.h +++ b/kde/gui/ollie9pclient.h @@ -71,6 +71,7 @@ public: Q_INVOKABLE QString readLog(); Q_INVOKABLE QString readLogForSession(const QString &sessionId, const QString &agentId); Q_INVOKABLE bool submit(const QString &prompt); + Q_INVOKABLE bool submitTo(const QString &sessionId, const QString &agentId, const QString &prompt); Q_INVOKABLE bool interrupt(); Q_INVOKABLE bool kill(); Q_INVOKABLE bool killSession(const QString &sessionId); diff --git a/kde/kate/ollie_kate.cpp b/kde/kate/ollie_kate.cpp index c5ad51a..11f3ec8 100644 --- a/kde/kate/ollie_kate.cpp +++ b/kde/kate/ollie_kate.cpp @@ -225,26 +225,26 @@ void OllieKateView::ensureAgent() QString agentName = agentNameFromCwd(cwd); - // Ensure kate session exists + // Ensure default session exists if (m_sessionId.isEmpty()) { - QByteArray resp = m_9p->rdwr("session/new", ("name=kate\ncwd=" + cwd + "\n").toUtf8()); + QByteArray resp = m_9p->rdwr("session/new", ("name=default\ncwd=" + cwd + "\n").toUtf8()); if (resp.isEmpty()) { qWarning() << "Ollie Kate: failed to create session"; return; } - m_sessionId = "kate"; + m_sessionId = "default"; } // Check if agent with this name already exists - QByteArray check = m_9p->read("session/kate/agent/" + agentName + "/cfg"); + QByteArray check = m_9p->read("session/default/agent/" + agentName + "/cfg"); if (!check.isEmpty()) { m_agentId = agentName; // Ensure cwd is up to date - m_9p->rdwr("session/kate/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8()); + m_9p->rdwr("session/default/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8()); } else { // Create new agent with computed name QString agentCfg = "name=" + agentName + "\ncwd=" + cwd + "\n"; - QByteArray resp = m_9p->rdwr("session/kate/agent/new", agentCfg.toUtf8()); + QByteArray resp = m_9p->rdwr("session/default/agent/new", agentCfg.toUtf8()); if (!resp.isEmpty()) { m_agentId = agentName; } @@ -254,10 +254,10 @@ void OllieKateView::ensureAgent() bool OllieKateView::selectAgentForProject() { - // Check if kate session exists - QByteArray sessCheck = m_9p->read("session/kate/id"); + // Check if default session exists + QByteArray sessCheck = m_9p->read("session/default/id"); if (sessCheck.isEmpty()) return false; - m_sessionId = "kate"; + m_sessionId = "default"; QString cwd = projectBaseDir(); if (cwd.isEmpty()) return false; @@ -265,11 +265,11 @@ bool OllieKateView::selectAgentForProject() QString agentName = agentNameFromCwd(cwd); // Check if agent exists - QByteArray check = m_9p->read("session/kate/agent/" + agentName + "/cfg"); + QByteArray check = m_9p->read("session/default/agent/" + agentName + "/cfg"); if (!check.isEmpty()) { m_agentId = agentName; // Ensure cwd is up to date - m_9p->rdwr("session/kate/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8()); + m_9p->rdwr("session/default/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8()); updateStatusLabel(); return true; } @@ -282,7 +282,7 @@ bool OllieKateView::selectAgentForProject() void OllieKateView::switchToAgent(const QString &agentId) { - m_sessionId = "kate"; + m_sessionId = "default"; m_agentId = agentId; updateStatusLabel(); } @@ -338,7 +338,7 @@ void OllieKateView::updateStatusLabel() m_statusLabel->setText(QStringLiteral("○ ollie: no agent")); m_statusLabel->setStyleSheet(QStringLiteral("color: gray; padding: 0px 4px;")); } else { - m_statusLabel->setText(QStringLiteral("● ollie: kate/") + m_agentId); + m_statusLabel->setText(QStringLiteral("● ollie: default/") + m_agentId); m_statusLabel->setStyleSheet(QStringLiteral("color: green; padding: 0px 4px;")); } }