GUI: add Ctrl+B2 'Send to' menu for cross-agent messaging

- 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'
This commit is contained in:
Levi Neely 2026-10-12 14:30:37 +02:00
parent 9449bd2ba4
commit dd6755befa
6 changed files with 78 additions and 25 deletions

View File

@ -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) }
}
}
}

View File

@ -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 }

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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;"));
}
}