kate: fix agent selection, block rendering, project change detection
- newAgent() is idempotent: selects existing agent by cwd if one exists - Use rdwr() to get new agent ID from server instead of ls() ordering - selectAgentForProject() returns bool for caller branching - Connect to Kate project plugin projectChanged signal - Update chat block parser to [[[type]]] / [[[end]]] format - Guard pluginViewCreated for KF5 < 5.78
This commit is contained in:
parent
2c7b5e6fc5
commit
73e262ffaf
|
|
@ -60,19 +60,30 @@ if(OLLIE_KF5)
|
|||
Qt5::Widgets
|
||||
)
|
||||
|
||||
# --- Kate Plugin (disabled — needs rework) ---
|
||||
# kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp kate/chat_renderer.cpp INSTALL_NAMESPACE "ktexteditor")
|
||||
# target_link_libraries(ollie_kate
|
||||
# KF5::TextEditor
|
||||
# KF5::SyntaxHighlighting
|
||||
# Qt5::Core
|
||||
# Qt5::Widgets
|
||||
# )
|
||||
# if(HAVE_KATE_DIFF)
|
||||
# target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
|
||||
# target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
|
||||
# target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
|
||||
# endif()
|
||||
# --- Kate Plugin ---
|
||||
kcoreaddons_add_plugin(ollie_kate SOURCES
|
||||
kate/ollie_kate.cpp
|
||||
kate/ollie_ghost.cpp
|
||||
kate/chat_renderer.cpp
|
||||
gui/lib9pclient.cpp
|
||||
INSTALL_NAMESPACE "ktexteditor"
|
||||
)
|
||||
target_include_directories(ollie_kate PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/gui
|
||||
$ENV{HOME}/.local/include
|
||||
)
|
||||
target_link_libraries(ollie_kate
|
||||
KF5::TextEditor
|
||||
KF5::SyntaxHighlighting
|
||||
Qt5::Core
|
||||
Qt5::Widgets
|
||||
$ENV{HOME}/.local/lib/libollie9p.so
|
||||
)
|
||||
if(HAVE_KATE_DIFF)
|
||||
target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
|
||||
target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
|
||||
target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
|
||||
endif()
|
||||
|
||||
# --- GUI App (9P-based) ---
|
||||
# Strip Qt6-only grouped palette syntax (palette.active.*, palette.inactive.*,
|
||||
|
|
@ -131,22 +142,32 @@ else()
|
|||
Qt6::Widgets
|
||||
)
|
||||
|
||||
# --- Kate Plugin (disabled — needs rework) ---
|
||||
# find_package(KF6TextEditor REQUIRED)
|
||||
# find_package(KF6SyntaxHighlighting REQUIRED)
|
||||
# kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp kate/chat_renderer.cpp INSTALL_NAMESPACE "kf6/ktexteditor")
|
||||
# target_link_libraries(ollie_kate
|
||||
# KF6::TextEditor
|
||||
# KF6::SyntaxHighlighting
|
||||
# Qt6::Core
|
||||
# Qt6::DBus
|
||||
# Qt6::Widgets
|
||||
# )
|
||||
# if(HAVE_KATE_DIFF)
|
||||
# target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
|
||||
# target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
|
||||
# target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
|
||||
# endif()
|
||||
# --- Kate Plugin ---
|
||||
find_package(KF6TextEditor REQUIRED)
|
||||
kcoreaddons_add_plugin(ollie_kate SOURCES
|
||||
kate/ollie_kate.cpp
|
||||
kate/ollie_ghost.cpp
|
||||
kate/chat_renderer.cpp
|
||||
gui/lib9pclient.cpp
|
||||
INSTALL_NAMESPACE "kf6/ktexteditor"
|
||||
)
|
||||
target_include_directories(ollie_kate PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/gui
|
||||
$ENV{HOME}/.local/include
|
||||
)
|
||||
target_link_libraries(ollie_kate
|
||||
KF6::TextEditor
|
||||
KF6::SyntaxHighlighting
|
||||
Qt6::Core
|
||||
Qt6::DBus
|
||||
Qt6::Widgets
|
||||
$ENV{HOME}/.local/lib/libollie9p.so
|
||||
)
|
||||
if(HAVE_KATE_DIFF)
|
||||
target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
|
||||
target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
|
||||
target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
|
||||
endif()
|
||||
|
||||
# --- GUI App (9P-based, no D-Bus) ---
|
||||
find_package(KF6SyntaxHighlighting REQUIRED)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
#!/bin/sh
|
||||
# ollie-session-here: start a session in the given directory
|
||||
# Works from both Dolphin (passes %f) and terminal (uses $PWD).
|
||||
#
|
||||
# Session name is the base directory name (not full path).
|
||||
# Session creation is idempotent — always write to session/new.
|
||||
|
||||
TARGET="$1"
|
||||
[ -f "$TARGET" ] && TARGET="$(dirname "$TARGET")"
|
||||
[ -z "$TARGET" ] && TARGET="$PWD"
|
||||
TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || TARGET="$PWD"
|
||||
|
||||
RESULT=$(dbus-send --session --dest=org.ollie.SessionManager --type=method_call --print-reply \
|
||||
/org/ollie/SessionManager org.ollie.SessionManager.CreateSession \
|
||||
string:"$TARGET" string:"" string:"" string:"default" string:"" string:"" 2>&1)
|
||||
# Extract base directory name for session name
|
||||
SESSION_NAME="$(basename "$TARGET")"
|
||||
|
||||
SESSION_ID=$(echo "$RESULT" | grep 'string "' | head -1 | sed 's/.*string "\(.*\)"/\1/')
|
||||
# Create session (idempotent) and agent via 9P
|
||||
printf 'name=%s\ncwd=%s\n' "$SESSION_NAME" "$TARGET" | ollie-9p write session/new
|
||||
printf '' | ollie-9p write "session/$SESSION_NAME/agent/new"
|
||||
|
||||
if [ -t 1 ]; then
|
||||
# Terminal: print session ID for further use
|
||||
echo "$SESSION_ID"
|
||||
# Terminal: print session info
|
||||
echo "Session: $SESSION_NAME"
|
||||
echo "CWD: $TARGET"
|
||||
else
|
||||
# Dolphin/non-interactive: desktop notification
|
||||
notify-send -t 3000 "Ollie" "Session started in $TARGET"
|
||||
notify-send -t 3000 "Ollie" "Session '$SESSION_NAME' started in $TARGET"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ QList<ChatBlock> ChatRenderer::parseBlocks(const QString &text) const
|
|||
if (text.isEmpty()) return blocks;
|
||||
|
||||
static const QRegularExpression headerRe(
|
||||
R"(^\[(user|assistant|reasoning|call|tool|error|info)(?::([^\]]*))?\](.*)$)",
|
||||
R"(^\[\[\[(user|assistant|reasoning|call|tool|error|info)(?::([^\]]*))?\]\]\](.*)$)",
|
||||
QRegularExpression::MultilineOption);
|
||||
|
||||
static const QRegularExpression endRe(
|
||||
R"(^\[\[\[end\]\]\]\s*$)",
|
||||
QRegularExpression::MultilineOption);
|
||||
|
||||
auto it = headerRe.globalMatch(text);
|
||||
|
|
@ -99,7 +103,19 @@ QList<ChatBlock> ChatRenderer::parseBlocks(const QString &text) const
|
|||
QString inlineContent = match.captured(3);
|
||||
|
||||
int contentStart = match.capturedEnd();
|
||||
int contentEnd = (i + 1 < headers.size()) ? headers[i + 1].first : text.size();
|
||||
|
||||
// Find the [[[end]]] marker after this header (before next header)
|
||||
int searchEnd = (i + 1 < headers.size()) ? headers[i + 1].first : text.size();
|
||||
QString segment = text.mid(contentStart, searchEnd - contentStart);
|
||||
auto endMatch = endRe.match(segment);
|
||||
|
||||
int contentEnd;
|
||||
if (endMatch.hasMatch()) {
|
||||
contentEnd = contentStart + endMatch.capturedStart();
|
||||
} else {
|
||||
// No [[[end]]] found — treat rest until next header as content (streaming)
|
||||
contentEnd = searchEnd;
|
||||
}
|
||||
|
||||
QString content;
|
||||
if (typeStr == "info" && !inlineContent.trimmed().isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -39,16 +39,6 @@
|
|||
|
||||
K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json")
|
||||
|
||||
static QString ninepBin()
|
||||
{
|
||||
QString bin = QStandardPaths::findExecutable("9p");
|
||||
if (!bin.isEmpty()) return bin;
|
||||
QString plan9 = qEnvironmentVariable("PLAN9");
|
||||
if (!plan9.isEmpty() && QFileInfo::exists(plan9 + "/bin/9p"))
|
||||
return plan9 + "/bin/9p";
|
||||
return "9p";
|
||||
}
|
||||
|
||||
// --- Plugin ---
|
||||
|
||||
#if QT_VERSION_MAJOR >= 6
|
||||
|
|
@ -271,10 +261,15 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
, m_plugin(plugin)
|
||||
, m_mainWindow(mainWindow)
|
||||
, m_renderer(new ChatRenderer())
|
||||
, m_9p(new Lib9pClient(this))
|
||||
{
|
||||
m_renderer->setDiffStorage(&m_diffBlocks);
|
||||
m_renderer->setApplyStorage(&m_applyBlocks);
|
||||
// 9P: no interface object needed — we use run9p/write9p helpers
|
||||
|
||||
// Connect to 9P server
|
||||
if (!m_9p->connectDefault()) {
|
||||
qWarning() << "Ollie: failed to connect to 9P server:" << m_9p->lastError();
|
||||
}
|
||||
|
||||
// Create tool view (side panel)
|
||||
m_toolView = m_mainWindow->createToolView(
|
||||
|
|
@ -304,7 +299,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
killBtn->setFlat(true);
|
||||
connect(killBtn, &QPushButton::clicked, this, [this]() {
|
||||
if (m_sessionId.isEmpty()) return;
|
||||
write9p(agentPath() + "/ctl", "kill");
|
||||
m_9p->write(agentPath() + "/ctl", "kill");
|
||||
});
|
||||
headerLayout->addWidget(killBtn);
|
||||
layout->addLayout(headerLayout);
|
||||
|
|
@ -339,25 +334,19 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
m_completionStatus = nullptr;
|
||||
}
|
||||
|
||||
// Button bar
|
||||
// Agent management buttons
|
||||
auto *buttonBar = new QHBoxLayout();
|
||||
|
||||
auto *newBtn = new QPushButton("New", container);
|
||||
newBtn->setToolTip("New session in current file's directory");
|
||||
connect(newBtn, &QPushButton::clicked, this, &OllieKateView::newSession);
|
||||
newBtn->setToolTip("Create a new agent in this session");
|
||||
connect(newBtn, &QPushButton::clicked, this, &OllieKateView::newAgent);
|
||||
buttonBar->addWidget(newBtn);
|
||||
|
||||
auto *pickBtn = new QPushButton("Pick", container);
|
||||
pickBtn->setToolTip("Switch to an existing session matching the project directory");
|
||||
connect(pickBtn, &QPushButton::clicked, this, &OllieKateView::pickSession);
|
||||
pickBtn->setToolTip("Switch to another agent in this session");
|
||||
connect(pickBtn, &QPushButton::clicked, this, &OllieKateView::pickAgent);
|
||||
buttonBar->addWidget(pickBtn);
|
||||
|
||||
auto *renameBtn = new QPushButton("Rename", container);
|
||||
renameBtn->setToolTip("Rename the current session");
|
||||
connect(renameBtn, &QPushButton::clicked, this, &OllieKateView::renameSession);
|
||||
buttonBar->addWidget(renameBtn);
|
||||
|
||||
|
||||
buttonBar->addStretch();
|
||||
layout->addLayout(buttonBar);
|
||||
|
||||
|
|
@ -429,7 +418,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
m_stopBtn->setFixedWidth(30);
|
||||
connect(m_stopBtn, &QPushButton::clicked, this, [this]() {
|
||||
if (!m_sessionId.isEmpty()) {
|
||||
write9p(agentPath() + "/ctl", "stop");
|
||||
m_9p->write(agentPath() + "/ctl", "stop");
|
||||
}
|
||||
});
|
||||
promptLayout->addWidget(m_stopBtn);
|
||||
|
|
@ -440,7 +429,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
startChatStream();
|
||||
startStateStream();
|
||||
|
||||
// Pick up existing session if any
|
||||
// Connect to existing session/agent if any (read-only)
|
||||
refreshSessions();
|
||||
|
||||
// Set up ghost-text completion on active view
|
||||
|
|
@ -449,8 +438,28 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
this, [this](KTextEditor::View *view) {
|
||||
setupGhostProvider();
|
||||
if (view) setupContextMenu(view);
|
||||
// Auto-select agent matching the new project
|
||||
selectAgentForProject();
|
||||
});
|
||||
|
||||
// React to Kate project changes
|
||||
QObject *projectView = mainWindow->pluginView("kateprojectplugin");
|
||||
if (projectView) {
|
||||
QObject::connect(projectView, SIGNAL(projectChanged()),
|
||||
this, SLOT(onProjectChanged()));
|
||||
}
|
||||
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 78, 0)
|
||||
// Also watch for the project plugin appearing later
|
||||
connect(mainWindow, &KTextEditor::MainWindow::pluginViewCreated,
|
||||
this, [this](const QString &name, QObject *view) {
|
||||
if (name == "kateprojectplugin") {
|
||||
QObject::connect(view, SIGNAL(projectChanged()),
|
||||
this, SLOT(onProjectChanged()));
|
||||
selectAgentForProject();
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
// Set up context menu on initial active view
|
||||
if (auto *view = mainWindow->activeView())
|
||||
setupContextMenu(view);
|
||||
|
|
@ -480,8 +489,7 @@ void OllieKateView::onChatUpdated(const QString &session_id, qlonglong offset, c
|
|||
annotateFileReferences(m_rawChat);
|
||||
} else {
|
||||
// Offset mismatch — refetch
|
||||
// Refetch via 9P
|
||||
QByteArray chatData = run9p({"read", agentPath() + "/log"});
|
||||
QByteArray chatData = m_9p->read(agentPath() + "/log");
|
||||
if (!chatData.isEmpty()) {
|
||||
m_rawChat = QString::fromUtf8(chatData);
|
||||
m_chatOffset = chatData.size();
|
||||
|
|
@ -499,7 +507,7 @@ void OllieKateView::onStateChanged(const QString &session_id, const QString &new
|
|||
if (new_state == "idle") dot = "○ ";
|
||||
else if (new_state.startsWith("calling")) dot = "⚡ ";
|
||||
else dot = "● ";
|
||||
m_sessionLabel->setText(dot + m_sessionId + " [" + new_state + "]");
|
||||
m_sessionLabel->setText(dot + "kate/" + m_agentId + " [" + new_state + "]");
|
||||
}
|
||||
|
||||
void OllieKateView::onSessionCreated(const QString &session_id)
|
||||
|
|
@ -535,10 +543,12 @@ void OllieKateView::submitPrompt()
|
|||
QString text = m_promptInput->toPlainText().trimmed();
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
ensureSession();
|
||||
if (m_sessionId.isEmpty()) return;
|
||||
if (m_agentId.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No agent selected. Click 'New' to create one.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
write9p(agentPath() + "/prompt", text.toUtf8());
|
||||
m_9p->write(agentPath() + "/prompt", text.toUtf8());
|
||||
m_promptInput->clear();
|
||||
}
|
||||
|
||||
|
|
@ -550,11 +560,13 @@ void OllieKateView::askAboutFile()
|
|||
QString filePath = view->document()->url().toLocalFile();
|
||||
if (filePath.isEmpty()) return;
|
||||
|
||||
ensureSession();
|
||||
if (m_sessionId.isEmpty()) return;
|
||||
if (m_agentId.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No agent selected. Click 'New' to create one.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString prompt = "Describe and explain this file: " + filePath;
|
||||
write9p(agentPath() + "/prompt", prompt.toUtf8());
|
||||
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
||||
}
|
||||
|
||||
void OllieKateView::explainSelection()
|
||||
|
|
@ -570,112 +582,180 @@ void OllieKateView::explainSelection()
|
|||
}
|
||||
if (selection.isEmpty()) return;
|
||||
|
||||
ensureSession();
|
||||
if (m_sessionId.isEmpty()) return;
|
||||
if (m_agentId.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No agent selected. Click 'New' to create one.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString filePath = view->document()->url().toLocalFile();
|
||||
QString context = filePath.isEmpty() ? "" : " (from " + filePath + ")";
|
||||
QString prompt = "Explain this code" + context + ":\n\n```\n" + selection + "\n```";
|
||||
write9p(agentPath() + "/prompt", prompt.toUtf8());
|
||||
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
||||
}
|
||||
|
||||
void OllieKateView::newSession()
|
||||
void OllieKateView::newAgent()
|
||||
{
|
||||
// Ensure kate session exists
|
||||
if (m_sessionId.isEmpty()) {
|
||||
QString sessionCfg = "name=kate\n";
|
||||
m_9p->write("session/new", sessionCfg.toUtf8());
|
||||
m_sessionId = "kate";
|
||||
}
|
||||
|
||||
// If a matching agent already exists for this project, just select it
|
||||
if (selectAgentForProject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No matching agent — create one
|
||||
QString cwd = projectBaseDir();
|
||||
if (cwd.isEmpty()) {
|
||||
cwd = QDir::homePath();
|
||||
}
|
||||
|
||||
QString backend = m_plugin->defaultBackend();
|
||||
QString model = m_plugin->defaultModel();
|
||||
QString newCfg = "cwd=" + cwd + "\nbackend=" + backend + "\nmodel=" + model + "\n";
|
||||
write9p("session/new", newCfg.toUtf8());
|
||||
QByteArray idxOut = run9p({"read", "session/idx"});
|
||||
QString lastId = QString::fromUtf8(idxOut).trimmed().split(QChar('\n')).last().split(QChar('\t')).value(0);
|
||||
QString reply_value = lastId;
|
||||
if (!lastId.isEmpty()) {
|
||||
m_sessionId = lastId;
|
||||
m_rawChat.clear();
|
||||
m_chatView->clear();
|
||||
m_chatOffset = 0;
|
||||
m_sessionLabel->setText("○ " + m_sessionId + " [idle]");
|
||||
m_cwdLabel->setText(cwd);
|
||||
} else {
|
||||
m_chatView->setHtml("<p style='color:#e53935;'>Error creating session</p>");
|
||||
if (cwd.isEmpty()) cwd = QDir::homePath();
|
||||
|
||||
QString agentCfg = "cwd=" + cwd + "\n";
|
||||
QByteArray resp = m_9p->rdwr("session/kate/agent/new", agentCfg.toUtf8());
|
||||
if (resp.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:red;'>Failed to create agent: ") +
|
||||
m_9p->lastError().toHtmlEscaped() + QStringLiteral("</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString newId = QString::fromUtf8(resp).trimmed();
|
||||
switchToAgent(newId);
|
||||
}
|
||||
|
||||
void OllieKateView::pickSession()
|
||||
void OllieKateView::pickAgent()
|
||||
{
|
||||
QString baseDir = projectBaseDir();
|
||||
if (baseDir.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>Cannot determine project directory.</p>"));
|
||||
if (m_sessionId.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No session. Use 'New' to create one.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList reply;
|
||||
{
|
||||
QByteArray out = run9p({"read", "session/idx"});
|
||||
QString raw = QString::fromUtf8(out).trimmed();
|
||||
if (!raw.isEmpty()) reply = raw.split(QChar('\n'));
|
||||
}
|
||||
if (reply.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No active sessions found.</p>"));
|
||||
|
||||
QStringList agents = m_9p->ls("session/" + m_sessionId + "/agent");
|
||||
agents.removeAll("new"); // Filter out the "new" control file
|
||||
agents.removeAll("idx"); // Filter out the "idx" index file
|
||||
|
||||
if (agents.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No agents exist. Use 'New' to create one.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList entries = reply;
|
||||
entries.sort(Qt::CaseInsensitive);
|
||||
|
||||
// Find first session whose cwd matches the project base dir
|
||||
for (const QString &entry : entries) {
|
||||
QStringList parts = entry.split('\t');
|
||||
if (parts.isEmpty()) continue;
|
||||
QString id = parts[0];
|
||||
|
||||
// Get config for this session via 9P
|
||||
QByteArray agentLs = run9p({"ls", "session/" + id + "/agent"});
|
||||
QString aid = QString::fromUtf8(agentLs).trimmed().split(QChar('\n')).value(0, "0");
|
||||
QString cfgStr = QString::fromUtf8(run9p({"read", "session/" + id + "/agent/" + aid + "/cfg"}));
|
||||
if (cfgStr.isEmpty()) continue;
|
||||
for (const QString &line : cfgStr.split(QChar('\n'))) {
|
||||
|
||||
if (agents.size() == 1) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>Only one agent exists. Use 'New' to create another.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build list of agents with their cwd
|
||||
QStringList items;
|
||||
for (const QString &aid : agents) {
|
||||
QString cfgVal = QString::fromUtf8(m_9p->read("session/" + m_sessionId + "/agent/" + aid + "/cfg")).trimmed();
|
||||
QString cwd;
|
||||
for (const QString &line : cfgVal.split(QChar('\n'))) {
|
||||
if (line.startsWith("cwd=")) {
|
||||
QString cwd = line.mid(4);
|
||||
if (cwd == baseDir) {
|
||||
switchToSession(id);
|
||||
return;
|
||||
}
|
||||
cwd = line.mid(4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
QString marker = (aid == m_agentId) ? " ← current" : "";
|
||||
items << aid + " (" + cwd + ")" + marker;
|
||||
}
|
||||
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No session found with cwd: ") + baseDir.toHtmlEscaped() + QStringLiteral("</p>"));
|
||||
|
||||
bool ok;
|
||||
QString selected = QInputDialog::getItem(
|
||||
m_toolView, "Pick Agent",
|
||||
"Select an agent:",
|
||||
items, agents.indexOf(m_agentId), false, &ok);
|
||||
|
||||
if (!ok || selected.isEmpty()) return;
|
||||
|
||||
// Extract agent ID from selection
|
||||
QString newAgentId = selected.split(' ').first();
|
||||
if (newAgentId == m_agentId) return; // Already on this agent
|
||||
|
||||
switchToAgent(newAgentId);
|
||||
}
|
||||
|
||||
void OllieKateView::renameSession()
|
||||
void OllieKateView::switchToAgent(const QString &agentId)
|
||||
{
|
||||
if (m_sessionId.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No active session to rename.</p>"));
|
||||
return;
|
||||
stopStreams();
|
||||
m_sessionId = "kate";
|
||||
m_agentId = agentId;
|
||||
m_rawChat.clear();
|
||||
m_chatView->clear();
|
||||
m_chatOffset = 0;
|
||||
|
||||
QString stateVal = QString::fromUtf8(m_9p->read(agentPath() + "/state")).trimmed();
|
||||
QString state = stateVal.isEmpty() ? "idle" : stateVal;
|
||||
QString dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● ");
|
||||
m_sessionLabel->setText(dot + "kate/" + m_agentId + " [" + state + "]");
|
||||
|
||||
// Get cwd from agent config
|
||||
QString cfgVal = QString::fromUtf8(m_9p->read(agentPath() + "/cfg")).trimmed();
|
||||
for (const QString &line : cfgVal.split(QChar('\n'))) {
|
||||
if (line.startsWith("cwd=")) {
|
||||
m_cwdLabel->setText(line.mid(4));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(
|
||||
m_toolView, "Rename Session",
|
||||
"New name for session " + m_sessionId + ":",
|
||||
QLineEdit::Normal, m_sessionId, &ok);
|
||||
|
||||
if (!ok || newName.trimmed().isEmpty()) return;
|
||||
newName = newName.trimmed();
|
||||
|
||||
write9p("session/" + m_sessionId + "/ctl", ("rn " + newName).toUtf8());
|
||||
{
|
||||
m_sessionId = newName;
|
||||
QString stateVal = QString::fromUtf8(run9p({"read", agentPath() + "/state"})).trimmed();
|
||||
QString dot = (stateVal == "idle") ? "○ " : (stateVal.startsWith("calling") ? "⚡ " : "● ");
|
||||
m_sessionLabel->setText(dot + m_sessionId + " [" + stateVal + "]");
|
||||
|
||||
// Fetch existing chat
|
||||
QByteArray chatData = m_9p->read(agentPath() + "/log");
|
||||
if (!chatData.isEmpty()) {
|
||||
m_rawChat = QString::fromUtf8(chatData);
|
||||
m_chatOffset = chatData.size();
|
||||
m_renderer->render(m_chatView, m_rawChat);
|
||||
}
|
||||
|
||||
startChatStream();
|
||||
startStateStream();
|
||||
}
|
||||
|
||||
bool OllieKateView::selectAgentForProject()
|
||||
{
|
||||
QString baseDir = projectBaseDir();
|
||||
if (baseDir.isEmpty()) return false;
|
||||
|
||||
// Check if kate session exists
|
||||
QByteArray sessCheck = m_9p->read("session/kate/id");
|
||||
if (sessCheck.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
m_sessionId = "kate";
|
||||
|
||||
// Normalize the project path for comparison
|
||||
QString canonicalBase = QFileInfo(baseDir).canonicalFilePath();
|
||||
if (canonicalBase.isEmpty()) canonicalBase = baseDir;
|
||||
while (canonicalBase.endsWith('/')) canonicalBase.chop(1);
|
||||
|
||||
// Read agent index: name\tid\tcwd\tbackend\tmodel per line
|
||||
QByteArray idxRaw = m_9p->read("session/kate/agent/idx");
|
||||
QString idxData = QString::fromUtf8(idxRaw).trimmed();
|
||||
|
||||
for (const QString &line : idxData.split(QChar('\n'), Qt::SkipEmptyParts)) {
|
||||
QStringList fields = line.split(QChar('\t'));
|
||||
if (fields.size() >= 3) {
|
||||
QString agentId = fields[1];
|
||||
QString agentCwd = fields[2];
|
||||
|
||||
// Normalize agent cwd for comparison
|
||||
QString canonicalCwd = QFileInfo(agentCwd).canonicalFilePath();
|
||||
if (canonicalCwd.isEmpty()) canonicalCwd = agentCwd;
|
||||
while (canonicalCwd.endsWith('/')) canonicalCwd.chop(1);
|
||||
|
||||
if (canonicalCwd == canonicalBase) {
|
||||
if (agentId != m_agentId) {
|
||||
switchToAgent(agentId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OllieKateView::onProjectChanged()
|
||||
{
|
||||
selectAgentForProject();
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
|
@ -706,14 +786,20 @@ void OllieKateView::switchToSession(const QString &sessionId)
|
|||
m_chatView->clear();
|
||||
m_chatOffset = 0;
|
||||
|
||||
// Get agent ID for this session
|
||||
QStringList agents = m_9p->ls("session/" + sessionId + "/agent");
|
||||
agents.removeAll("new");
|
||||
agents.removeAll("idx");
|
||||
m_agentId = agents.value(0);
|
||||
|
||||
// Fetch state
|
||||
QString stateVal = QString::fromUtf8(run9p({"read", agentPath() + "/state"})).trimmed();
|
||||
QString stateVal = QString::fromUtf8(m_9p->read(agentPath() + "/state")).trimmed();
|
||||
QString state = stateVal.isEmpty() ? "idle" : stateVal;
|
||||
QString dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● ");
|
||||
m_sessionLabel->setText(dot + m_sessionId + " [" + state + "]");
|
||||
m_sessionLabel->setText(dot + m_sessionId + "/" + m_agentId + " [" + state + "]");
|
||||
|
||||
// Fetch cwd
|
||||
QString cfgVal = QString::fromUtf8(run9p({"read", agentPath() + "/cfg"})).trimmed();
|
||||
QString cfgVal = QString::fromUtf8(m_9p->read(agentPath() + "/cfg")).trimmed();
|
||||
if (!cfgVal.isEmpty()) {
|
||||
for (const QString &line : cfgVal.split(QChar('\n'))) {
|
||||
if (line.startsWith("cwd=")) {
|
||||
|
|
@ -723,7 +809,7 @@ void OllieKateView::switchToSession(const QString &sessionId)
|
|||
}
|
||||
}
|
||||
// Fetch existing chat via 9P
|
||||
QByteArray chatData = run9p({"read", agentPath() + "/log"});
|
||||
QByteArray chatData = m_9p->read(agentPath() + "/log");
|
||||
if (!chatData.isEmpty()) {
|
||||
m_rawChat = QString::fromUtf8(chatData);
|
||||
m_chatOffset = chatData.size();
|
||||
|
|
@ -736,14 +822,6 @@ void OllieKateView::switchToSession(const QString &sessionId)
|
|||
startStateStream();
|
||||
}
|
||||
|
||||
void OllieKateView::ensureSession()
|
||||
{
|
||||
if (!m_sessionId.isEmpty()) return;
|
||||
// 9P is always reachable — skip daemon check
|
||||
newSession();
|
||||
newSession();
|
||||
}
|
||||
|
||||
QString OllieKateView::activeSessionId()
|
||||
{
|
||||
return m_sessionId;
|
||||
|
|
@ -894,53 +972,8 @@ void OllieKateView::showDiffBlock(int index)
|
|||
|
||||
void OllieKateView::refreshSessions()
|
||||
{
|
||||
QStringList reply;
|
||||
{
|
||||
QByteArray out = run9p({"read", "session/idx"});
|
||||
QString raw = QString::fromUtf8(out).trimmed();
|
||||
if (!raw.isEmpty()) reply = raw.split(QChar('\n'));
|
||||
}
|
||||
if (reply.isEmpty()) return;
|
||||
|
||||
QStringList sessions = reply;
|
||||
sessions.sort(Qt::CaseInsensitive);
|
||||
if (sessions.isEmpty()) {
|
||||
m_sessionId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have a session, keep it
|
||||
if (!m_sessionId.isEmpty()) return;
|
||||
|
||||
// Try to find a session matching the project base dir
|
||||
QString baseDir = projectBaseDir();
|
||||
if (!baseDir.isEmpty()) {
|
||||
for (const QString &entry : sessions) {
|
||||
QStringList parts = entry.split('\t');
|
||||
if (parts.isEmpty()) continue;
|
||||
QString id = parts[0];
|
||||
|
||||
QByteArray agentLs2 = run9p({"ls", "session/" + id + "/agent"});
|
||||
QString aid2 = QString::fromUtf8(agentLs2).trimmed().split(QChar('\n')).value(0, "0");
|
||||
QString cfgStr2 = QString::fromUtf8(run9p({"read", "session/" + id + "/agent/" + aid2 + "/cfg"}));
|
||||
if (cfgStr2.isEmpty()) continue;
|
||||
for (const QString &line : cfgStr2.split(QChar('\n'))) {
|
||||
if (line.startsWith("cwd=")) {
|
||||
if (line.mid(4) == baseDir) {
|
||||
switchToSession(id);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to first session
|
||||
QStringList parts = sessions.first().split('\t');
|
||||
if (!parts.isEmpty()) {
|
||||
switchToSession(parts[0]);
|
||||
}
|
||||
// At startup, try to select agent matching current project
|
||||
selectAgentForProject();
|
||||
}
|
||||
|
||||
void OllieKateView::setupGhostProvider()
|
||||
|
|
@ -954,14 +987,14 @@ void OllieKateView::setupGhostProvider()
|
|||
return;
|
||||
|
||||
// Remove old provider
|
||||
delete m_ghost;
|
||||
m_ghost = nullptr;
|
||||
|
||||
// Create new ghost provider for the active view
|
||||
// TODO: ghost provider needs 9P port
|
||||
m_ghost = nullptr;
|
||||
m_ghost->setEnabled(m_plugin->completionEnabled());
|
||||
if (m_ghost) {
|
||||
delete m_ghost;
|
||||
m_ghost = nullptr;
|
||||
}
|
||||
|
||||
// Ghost provider disabled until 9P completion is implemented
|
||||
// TODO: create ghost provider with 9P-based completion
|
||||
|
||||
// Install event filter on the view widget for Tab interception
|
||||
if (QWidget *proxy = view->focusProxy()) {
|
||||
proxy->installEventFilter(this);
|
||||
|
|
@ -1143,8 +1176,10 @@ void OllieKateView::submitSelectionPrompt(const QString &action)
|
|||
QString selection = view->selectionText();
|
||||
if (selection.isEmpty()) return;
|
||||
|
||||
ensureSession();
|
||||
if (m_sessionId.isEmpty()) return;
|
||||
if (m_agentId.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No agent selected. Click 'New' to create one.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString filePath = view->document()->url().toLocalFile();
|
||||
auto cursor = view->selectionRange().start();
|
||||
|
|
@ -1154,7 +1189,7 @@ void OllieKateView::submitSelectionPrompt(const QString &action)
|
|||
}
|
||||
|
||||
QString prompt = action + " this code" + location + ":\n\n```\n" + selection + "\n```";
|
||||
write9p(agentPath() + "/prompt", prompt.toUtf8());
|
||||
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
||||
}
|
||||
|
||||
void OllieKateView::reviewDiff()
|
||||
|
|
@ -1178,12 +1213,14 @@ void OllieKateView::reviewDiff()
|
|||
|
||||
if (diff.trimmed().isEmpty()) return;
|
||||
|
||||
ensureSession();
|
||||
if (m_sessionId.isEmpty()) return;
|
||||
if (m_agentId.isEmpty()) {
|
||||
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No agent selected. Click 'New' to create one.</p>"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString prompt = QStringLiteral("Review this diff. Point out potential issues, bugs, "
|
||||
"or improvements:\n\n```diff\n") + diff + QStringLiteral("\n```");
|
||||
write9p(agentPath() + "/prompt", prompt.toUtf8());
|
||||
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
||||
}
|
||||
|
||||
// --- Apply Code Blocks ---
|
||||
|
|
@ -1300,7 +1337,24 @@ void OllieKateView::annotateFileReferences(const QString &chatText)
|
|||
|
||||
// --- 9P helpers ---
|
||||
|
||||
QString OllieKateView::serverAddr()
|
||||
QString OllieKateView::agentPath() const
|
||||
{
|
||||
if (m_sessionId.isEmpty() || m_agentId.isEmpty()) return {};
|
||||
return "session/" + m_sessionId + "/agent/" + m_agentId;
|
||||
}
|
||||
|
||||
// ninepBin and serverAddr only used for streaming processes
|
||||
static QString ninepBin()
|
||||
{
|
||||
QString bin = QStandardPaths::findExecutable("9p");
|
||||
if (!bin.isEmpty()) return bin;
|
||||
QString plan9 = qEnvironmentVariable("PLAN9");
|
||||
if (!plan9.isEmpty() && QFileInfo::exists(plan9 + "/bin/9p"))
|
||||
return plan9 + "/bin/9p";
|
||||
return "9p";
|
||||
}
|
||||
|
||||
static QString serverAddr()
|
||||
{
|
||||
QString ns = qEnvironmentVariable("NAMESPACE");
|
||||
if (ns.isEmpty()) {
|
||||
|
|
@ -1311,35 +1365,6 @@ QString OllieKateView::serverAddr()
|
|||
return "unix!" + ns + "/ollie";
|
||||
}
|
||||
|
||||
QString OllieKateView::agentPath()
|
||||
{
|
||||
if (m_sessionId.isEmpty()) return {};
|
||||
// Discover agent ID (cache could be added later)
|
||||
QByteArray out = run9p({"ls", "session/" + m_sessionId + "/agent"});
|
||||
QString aid = QString::fromUtf8(out).trimmed().split('\n').value(0, "0");
|
||||
return "session/" + m_sessionId + "/agent/" + aid;
|
||||
}
|
||||
|
||||
QByteArray OllieKateView::run9p(const QStringList &args)
|
||||
{
|
||||
QProcess proc;
|
||||
QStringList fullArgs = {"-a", serverAddr()};
|
||||
fullArgs.append(args);
|
||||
proc.start(ninepBin(), fullArgs);
|
||||
if (!proc.waitForFinished(10000)) return {};
|
||||
return proc.readAllStandardOutput();
|
||||
}
|
||||
|
||||
void OllieKateView::write9p(const QString &path, const QByteArray &data)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.start(ninepBin(), {"-a", serverAddr(), "write", path});
|
||||
proc.waitForStarted(3000);
|
||||
proc.write(data);
|
||||
proc.closeWriteChannel();
|
||||
proc.waitForFinished(5000);
|
||||
}
|
||||
|
||||
// --- 9P Streaming ---
|
||||
|
||||
void OllieKateView::startChatStream()
|
||||
|
|
@ -1372,7 +1397,7 @@ void OllieKateView::startStateStream()
|
|||
QStringList lines = state.split('\n');
|
||||
state = lines.last().trimmed();
|
||||
QString dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● ");
|
||||
m_sessionLabel->setText(dot + m_sessionId + " [" + state + "]");
|
||||
m_sessionLabel->setText(dot + "kate/" + m_agentId + " [" + state + "]");
|
||||
}
|
||||
// statewait returns once per state change — restart
|
||||
m_stateProc->deleteLater();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <KTextEditor/Attribute>
|
||||
|
||||
#include "chat_renderer.h"
|
||||
#include "lib9pclient.h"
|
||||
|
||||
class QPushButton;
|
||||
class OllieKateView;
|
||||
|
|
@ -90,17 +91,18 @@ private slots:
|
|||
void onStateChanged(const QString &session_id, const QString &new_state);
|
||||
void onSessionCreated(const QString &session_id);
|
||||
void onSessionKilled(const QString &session_id);
|
||||
void onProjectChanged();
|
||||
|
||||
void submitPrompt();
|
||||
void askAboutFile();
|
||||
void explainSelection();
|
||||
void newSession();
|
||||
void pickSession();
|
||||
void renameSession();
|
||||
void newAgent();
|
||||
void pickAgent();
|
||||
void refreshSessions();
|
||||
|
||||
private:
|
||||
void ensureSession();
|
||||
void switchToAgent(const QString &agentId);
|
||||
bool selectAgentForProject(); // Auto-select agent matching project basedir
|
||||
QString activeSessionId();
|
||||
|
||||
OllieKatePlugin *m_plugin;
|
||||
|
|
@ -117,11 +119,9 @@ private:
|
|||
QPushButton *m_stopBtn;
|
||||
QPushButton *m_scrollBottomBtn;
|
||||
|
||||
// 9P helpers
|
||||
QByteArray run9p(const QStringList &args);
|
||||
void write9p(const QString &path, const QByteArray &data);
|
||||
QString serverAddr();
|
||||
QString agentPath();
|
||||
// 9P client (direct connection, no shell-out)
|
||||
Lib9pClient *m_9p;
|
||||
QString agentPath() const; // Returns "session/kate/agent/{m_agentId}"
|
||||
|
||||
// Streaming processes
|
||||
QProcess *m_chatProc = nullptr;
|
||||
|
|
@ -131,6 +131,7 @@ private:
|
|||
void stopStreams();
|
||||
|
||||
QString m_sessionId;
|
||||
QString m_agentId; // Agent within the kate session
|
||||
qlonglong m_chatOffset = 0;
|
||||
QString m_rawChat; // accumulated raw chat text
|
||||
ChatRenderer *m_renderer;
|
||||
|
|
|
|||
Reference in New Issue