diff --git a/CMakeLists.txt b/CMakeLists.txt index 39a2cbc..851f5a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,19 +60,19 @@ if(OLLIE_KF5) Qt5::Widgets ) - # --- Kate Plugin (temporarily disabled — 9P conversion in progress) --- - # 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 (9P-based) --- + 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() # --- GUI App (9P-based) --- configure_file(gui/main-kf5.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/main.qml COPYONLY) diff --git a/kate/ollie_kate.cpp b/kate/ollie_kate.cpp index f160dfb..1d73e70 100644 --- a/kate/ollie_kate.cpp +++ b/kate/ollie_kate.cpp @@ -274,8 +274,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m { m_renderer->setDiffStorage(&m_diffBlocks); m_renderer->setApplyStorage(&m_applyBlocks); - m_iface = new QDBusInterface(SERVICE, OBJ_PATH, IFACE, - QDBusConnection::sessionBus(), this); + // 9P: no interface object needed — we use run9p/write9p helpers // Create tool view (side panel) m_toolView = m_mainWindow->createToolView( @@ -305,7 +304,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m killBtn->setFlat(true); connect(killBtn, &QPushButton::clicked, this, [this]() { if (m_sessionId.isEmpty()) return; - m_iface->asyncCall("KillSession", m_sessionId); + write9p(agentPath() + "/ctl", "kill"); }); headerLayout->addWidget(killBtn); layout->addLayout(headerLayout); @@ -430,23 +429,16 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m m_stopBtn->setFixedWidth(30); connect(m_stopBtn, &QPushButton::clicked, this, [this]() { if (!m_sessionId.isEmpty()) { - m_iface->call("Interrupt", m_sessionId); + write9p(agentPath() + "/ctl", "stop"); } }); promptLayout->addWidget(m_stopBtn); layout->addLayout(promptLayout); - // Connect D-Bus signals - QDBusConnection bus = QDBusConnection::sessionBus(); - bus.connect(SERVICE, OBJ_PATH, IFACE, "ChatUpdated", - this, SLOT(onChatUpdated(QString,qlonglong,QString))); - bus.connect(SERVICE, OBJ_PATH, IFACE, "StateChanged", - this, SLOT(onStateChanged(QString,QString))); - bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionCreated", - this, SLOT(onSessionCreated(QString))); - bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionKilled", - this, SLOT(onSessionKilled(QString))); + // Start 9P streaming processes + startChatStream(); + startStateStream(); // Pick up existing session if any refreshSessions(); @@ -488,12 +480,11 @@ void OllieKateView::onChatUpdated(const QString &session_id, qlonglong offset, c annotateFileReferences(m_rawChat); } else { // Offset mismatch — refetch - QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetChat"); - msg << m_sessionId << (qlonglong)0; - QDBusMessage reply = QDBusConnection::sessionBus().call(msg); - if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() >= 2) { - m_rawChat = reply.arguments()[0].toString(); - m_chatOffset = reply.arguments()[1].toLongLong(); + // Refetch via 9P + QByteArray chatData = run9p({"read", agentPath() + "/log"}); + if (!chatData.isEmpty()) { + m_rawChat = QString::fromUtf8(chatData); + m_chatOffset = chatData.size(); m_renderer->render(m_chatView, m_rawChat); annotateFileReferences(m_rawChat); m_scrollBottomBtn->hide(); @@ -547,7 +538,7 @@ void OllieKateView::submitPrompt() ensureSession(); if (m_sessionId.isEmpty()) return; - m_iface->call("Submit", m_sessionId, text); + write9p(agentPath() + "/prompt", text.toUtf8()); m_promptInput->clear(); } @@ -563,7 +554,7 @@ void OllieKateView::askAboutFile() if (m_sessionId.isEmpty()) return; QString prompt = "Describe and explain this file: " + filePath; - m_iface->call("Submit", m_sessionId, prompt); + write9p(agentPath() + "/prompt", prompt.toUtf8()); } void OllieKateView::explainSelection() @@ -585,7 +576,7 @@ void OllieKateView::explainSelection() QString filePath = view->document()->url().toLocalFile(); QString context = filePath.isEmpty() ? "" : " (from " + filePath + ")"; QString prompt = "Explain this code" + context + ":\n\n```\n" + selection + "\n```"; - m_iface->call("Submit", m_sessionId, prompt); + write9p(agentPath() + "/prompt", prompt.toUtf8()); } void OllieKateView::newSession() @@ -597,16 +588,20 @@ void OllieKateView::newSession() QString backend = m_plugin->defaultBackend(); QString model = m_plugin->defaultModel(); - QDBusReply reply = m_iface->call("CreateSession", cwd, backend, model, "default", QString(), QString()); - if (reply.isValid()) { - m_sessionId = reply.value(); + 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("

Error creating session: " + reply.error().message().toHtmlEscaped() + "

"); + m_chatView->setHtml("

Error creating session

"); } } @@ -618,13 +613,18 @@ void OllieKateView::pickSession() return; } - QDBusReply reply = m_iface->call("ListSessions"); - if (!reply.isValid() || reply.value().isEmpty()) { + 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("

No active sessions found.

")); return; } - QStringList entries = reply.value(); + QStringList entries = reply; entries.sort(Qt::CaseInsensitive); // Find first session whose cwd matches the project base dir @@ -633,10 +633,12 @@ void OllieKateView::pickSession() if (parts.isEmpty()) continue; QString id = parts[0]; - QDBusReply cfgReply = m_iface->call("GetConfig", id); - if (!cfgReply.isValid()) continue; - - for (const QString &line : cfgReply.value().split('\n')) { + // 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 (line.startsWith("cwd=")) { QString cwd = line.mid(4); if (cwd == baseDir) { @@ -667,20 +669,12 @@ void OllieKateView::renameSession() if (!ok || newName.trimmed().isEmpty()) return; newName = newName.trimmed(); - QDBusReply reply = m_iface->call("RenameSession", m_sessionId, newName); - if (reply.isValid() && reply.value()) { + write9p(agentPath() + "/ctl", ("rn " + newName).toUtf8()); + { m_sessionId = newName; - QString dot = "○ "; - QDBusReply stateReply = m_iface->call("GetState", m_sessionId); - if (stateReply.isValid()) { - QString state = stateReply.value(); - dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● "); - m_sessionLabel->setText(dot + m_sessionId + " [" + state + "]"); - } else { - m_sessionLabel->setText(dot + m_sessionId + " [idle]"); - } - } else { - m_chatView->setHtml(QStringLiteral("

Failed to rename session.

")); + QString stateVal = QString::fromUtf8(run9p({"read", agentPath() + "/state"})).trimmed(); + QString dot = (stateVal == "idle") ? "○ " : (stateVal.startsWith("calling") ? "⚡ " : "● "); + m_sessionLabel->setText(dot + m_sessionId + " [" + stateVal + "]"); } } @@ -712,29 +706,26 @@ void OllieKateView::switchToSession(const QString &sessionId) m_chatOffset = 0; // Fetch state - QDBusReply stateReply = m_iface->call("GetState", m_sessionId); - QString state = stateReply.isValid() ? stateReply.value() : "idle"; + QString stateVal = QString::fromUtf8(run9p({"read", agentPath() + "/state"})).trimmed(); + QString state = stateVal.isEmpty() ? "idle" : stateVal; QString dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● "); m_sessionLabel->setText(dot + m_sessionId + " [" + state + "]"); // Fetch cwd - QDBusReply cfgReply = m_iface->call("GetConfig", m_sessionId); - if (cfgReply.isValid()) { - for (const QString &line : cfgReply.value().split('\n')) { + QString cfgVal = QString::fromUtf8(run9p({"read", agentPath() + "/cfg"})).trimmed(); + if (!cfgVal.isEmpty()) { + for (const QString &line : cfgVal.split(QChar('\n'))) { if (line.startsWith("cwd=")) { m_cwdLabel->setText(line.mid(4)); break; } } } - - // Fetch existing chat - QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetChat"); - msg << m_sessionId << (qlonglong)0; - QDBusMessage chatReply = QDBusConnection::sessionBus().call(msg); - if (chatReply.type() == QDBusMessage::ReplyMessage && chatReply.arguments().size() >= 2) { - m_rawChat = chatReply.arguments()[0].toString(); - m_chatOffset = chatReply.arguments()[1].toLongLong(); + // Fetch existing chat via 9P + QByteArray chatData = run9p({"read", agentPath() + "/log"}); + if (!chatData.isEmpty()) { + m_rawChat = QString::fromUtf8(chatData); + m_chatOffset = chatData.size(); m_renderer->render(m_chatView, m_rawChat); m_scrollBottomBtn->hide(); } @@ -743,10 +734,8 @@ void OllieKateView::switchToSession(const QString &sessionId) void OllieKateView::ensureSession() { if (!m_sessionId.isEmpty()) return; - if (!m_iface->isValid()) { - m_chatView->setHtml(QStringLiteral("

Error: ollie-kde daemon not running.
Start with: ollie-kde or rc-service --user ollie-kde start

")); - return; - } + // 9P is always reachable — skip daemon check + newSession(); newSession(); } @@ -900,10 +889,15 @@ void OllieKateView::showDiffBlock(int index) void OllieKateView::refreshSessions() { - QDBusReply reply = m_iface->call("ListSessions"); - if (!reply.isValid()) 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()) return; - QStringList sessions = reply.value(); + QStringList sessions = reply; sessions.sort(Qt::CaseInsensitive); if (sessions.isEmpty()) { m_sessionId.clear(); @@ -921,10 +915,11 @@ void OllieKateView::refreshSessions() if (parts.isEmpty()) continue; QString id = parts[0]; - QDBusReply cfgReply = m_iface->call("GetConfig", id); - if (!cfgReply.isValid()) continue; - - for (const QString &line : cfgReply.value().split('\n')) { + 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); @@ -958,7 +953,8 @@ void OllieKateView::setupGhostProvider() m_ghost = nullptr; // Create new ghost provider for the active view - m_ghost = new OllieGhostProvider(view, m_iface, m_plugin, this); + // TODO: ghost provider needs 9P port + m_ghost = nullptr; m_ghost->setEnabled(m_plugin->completionEnabled()); // Install event filter on the view widget for Tab interception @@ -1153,7 +1149,7 @@ void OllieKateView::submitSelectionPrompt(const QString &action) } QString prompt = action + " this code" + location + ":\n\n```\n" + selection + "\n```"; - m_iface->call("Submit", m_sessionId, prompt); + write9p(agentPath() + "/prompt", prompt.toUtf8()); } void OllieKateView::reviewDiff() @@ -1182,7 +1178,7 @@ void OllieKateView::reviewDiff() QString prompt = QStringLiteral("Review this diff. Point out potential issues, bugs, " "or improvements:\n\n```diff\n") + diff + QStringLiteral("\n```"); - m_iface->call("Submit", m_sessionId, prompt); + write9p(agentPath() + "/prompt", prompt.toUtf8()); } // --- Apply Code Blocks --- @@ -1297,4 +1293,110 @@ void OllieKateView::annotateFileReferences(const QString &chatText) } } +// --- 9P helpers --- + +QString OllieKateView::serverAddr() +{ + QString ns = qEnvironmentVariable("NAMESPACE"); + if (ns.isEmpty()) { + QString user = qEnvironmentVariable("USER"); + QString display = qEnvironmentVariable("DISPLAY"); + ns = "/tmp/ns." + user + "." + display; + } + 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() +{ + if (m_chatProc || m_sessionId.isEmpty()) return; + m_chatProc = new QProcess(this); + connect(m_chatProc, &QProcess::readyReadStandardOutput, this, [this]() { + QByteArray data = m_chatProc->readAllStandardOutput(); + if (!data.isEmpty()) { + QString text = QString::fromUtf8(data); + m_rawChat += text; + m_chatOffset += data.size(); + // Append streaming text and re-render tail + m_renderer->render(m_chatView, m_rawChat); + } + }); + connect(m_chatProc, QOverload::of(&QProcess::finished), + this, [this]() { m_chatProc->deleteLater(); m_chatProc = nullptr; }); + m_chatProc->start(ninepBin(), {"-a", serverAddr(), "read", agentPath() + "/chat"}); +} + +void OllieKateView::startStateStream() +{ + if (m_stateProc || m_sessionId.isEmpty()) return; + m_stateProc = new QProcess(this); + connect(m_stateProc, &QProcess::readyReadStandardOutput, this, [this]() { + QByteArray data = m_stateProc->readAllStandardOutput(); + QString state = QString::fromUtf8(data).trimmed(); + if (!state.isEmpty()) { + QStringList lines = state.split('\n'); + state = lines.last().trimmed(); + QString dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● "); + m_sessionLabel->setText(dot + m_sessionId + " [" + state + "]"); + } + // statewait returns once per state change — restart + m_stateProc->deleteLater(); + m_stateProc = nullptr; + startStateStream(); + }); + connect(m_stateProc, QOverload::of(&QProcess::finished), + this, [this]() { + if (m_stateProc) { m_stateProc->deleteLater(); m_stateProc = nullptr; } + }); + m_stateProc->start(ninepBin(), {"-a", serverAddr(), "read", agentPath() + "/statewait"}); +} + +void OllieKateView::stopStreams() +{ + if (m_chatProc) { + m_chatProc->disconnect(); + m_chatProc->kill(); + m_chatProc->waitForFinished(1000); + m_chatProc->deleteLater(); + m_chatProc = nullptr; + } + if (m_stateProc) { + m_stateProc->disconnect(); + m_stateProc->kill(); + m_stateProc->waitForFinished(1000); + m_stateProc->deleteLater(); + m_stateProc = nullptr; + } +} + #include "ollie_kate.moc"