// TODO #include "ollie_kate.h" #include "ollie_ghost.h" #ifdef HAVE_KATE_DIFF #include "ktexteditor_utils.h" #endif #include #include #include #include #include #if KTEXTEDITOR_VERSION_MAJOR < 6 #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json") // --- Plugin --- #if QT_VERSION_MAJOR >= 6 OllieKatePlugin::OllieKatePlugin(QObject *parent, const KPluginMetaData &data, const QVariantList &args) : KTextEditor::Plugin(parent) { Q_UNUSED(data); Q_UNUSED(args); loadConfig(); } #else OllieKatePlugin::OllieKatePlugin(QObject *parent, const QVariantList &args) : KTextEditor::Plugin(parent) { Q_UNUSED(args); loadConfig(); } #endif QObject *OllieKatePlugin::createView(KTextEditor::MainWindow *mainWindow) { return new OllieKateView(this, mainWindow); } int OllieKatePlugin::configPages() const { return 1; } KTextEditor::ConfigPage *OllieKatePlugin::configPage(int number, QWidget *parent) { if (number != 0) return nullptr; // Inline config page class OllieConfigPage : public KTextEditor::ConfigPage { public: OllieConfigPage(OllieKatePlugin *plugin, QWidget *parent) : KTextEditor::ConfigPage(parent), m_plugin(plugin) { auto *layout = new QFormLayout(this); // Default session backend/model m_defaultBackendEdit = new QLineEdit(this); m_defaultBackendEdit->setPlaceholderText(QStringLiteral("(use OLLIE_BACKEND)")); m_defaultModelEdit = new QLineEdit(this); m_defaultModelEdit->setPlaceholderText(QStringLiteral("(use OLLIE_MODEL)")); layout->addRow(QStringLiteral("Default backend:"), m_defaultBackendEdit); layout->addRow(QStringLiteral("Default model:"), m_defaultModelEdit); auto *defaultNote = new QLabel( QStringLiteral("Override OLLIE_BACKEND / OLLIE_MODEL for chat sessions."), this); defaultNote->setWordWrap(true); defaultNote->setStyleSheet(QStringLiteral("color: gray;")); layout->addRow(defaultNote); // Completion backend/model m_backendEdit = new QLineEdit(this); m_backendEdit->setPlaceholderText(QStringLiteral("(use OLLIE_COMPLETE_BACKEND)")); m_modelEdit = new QLineEdit(this); m_modelEdit->setPlaceholderText(QStringLiteral("(use OLLIE_COMPLETE_MODEL)")); layout->addRow(QStringLiteral("Completion backend:"), m_backendEdit); layout->addRow(QStringLiteral("Completion model:"), m_modelEdit); auto *note = new QLabel( QStringLiteral("Override OLLIE_COMPLETE_BACKEND / OLLIE_COMPLETE_MODEL for ghost text."), this); note->setWordWrap(true); note->setStyleSheet(QStringLiteral("color: gray;")); layout->addRow(note); m_enabledCheck = new QCheckBox(QStringLiteral("Completion enabled on startup"), this); layout->addRow(m_enabledCheck); // Diff viewer mode auto *diffSep = new QLabel(QString(), this); layout->addRow(diffSep); m_diffModeCombo = new QComboBox(this); m_diffModeCombo->addItem(QStringLiteral("Unified"), QStringLiteral("unified")); m_diffModeCombo->addItem(QStringLiteral("Side by Side"), QStringLiteral("side-by-side")); m_diffModeCombo->addItem(QStringLiteral("Raw"), QStringLiteral("raw")); layout->addRow(QStringLiteral("Diff view style:"), m_diffModeCombo); auto *diffNote = new QLabel( QStringLiteral("How diffs are displayed in Kate's diff viewer."), this); diffNote->setWordWrap(true); diffNote->setStyleSheet(QStringLiteral("color: gray;")); layout->addRow(diffNote); reset(); connect(m_enabledCheck, &QCheckBox::toggled, this, &ConfigPage::changed); connect(m_backendEdit, &QLineEdit::textChanged, this, &ConfigPage::changed); connect(m_modelEdit, &QLineEdit::textChanged, this, &ConfigPage::changed); connect(m_defaultBackendEdit, &QLineEdit::textChanged, this, &ConfigPage::changed); connect(m_defaultModelEdit, &QLineEdit::textChanged, this, &ConfigPage::changed); connect(m_diffModeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ConfigPage::changed); } QString name() const override { return QStringLiteral("Ollie"); } QString fullName() const override { return QStringLiteral("Ollie Completion Settings"); } QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("system-run")); } void apply() override { m_plugin->setDefaultBackend(m_defaultBackendEdit->text().trimmed()); m_plugin->setDefaultModel(m_defaultModelEdit->text().trimmed()); m_plugin->setCompletionBackend(m_backendEdit->text().trimmed()); m_plugin->setCompletionModel(m_modelEdit->text().trimmed()); m_plugin->setCompletionEnabled(m_enabledCheck->isChecked()); m_plugin->setDiffMode(m_diffModeCombo->currentData().toString()); } void reset() override { m_defaultBackendEdit->setText(m_plugin->defaultBackend()); m_defaultModelEdit->setText(m_plugin->defaultModel()); m_backendEdit->setText(m_plugin->completionBackend()); m_modelEdit->setText(m_plugin->completionModel()); m_enabledCheck->setChecked(m_plugin->completionEnabled()); int idx = m_diffModeCombo->findData(m_plugin->diffMode()); m_diffModeCombo->setCurrentIndex(idx >= 0 ? idx : 0); } void defaults() override { m_defaultBackendEdit->clear(); m_defaultModelEdit->clear(); m_backendEdit->clear(); m_modelEdit->clear(); m_enabledCheck->setChecked(true); m_diffModeCombo->setCurrentIndex(0); } private: OllieKatePlugin *m_plugin; QLineEdit *m_defaultBackendEdit; QLineEdit *m_defaultModelEdit; QLineEdit *m_backendEdit; QLineEdit *m_modelEdit; QCheckBox *m_enabledCheck; QComboBox *m_diffModeCombo; }; return new OllieConfigPage(this, parent); } QString OllieKatePlugin::completionBackend() const { return m_completionBackend; } QString OllieKatePlugin::completionModel() const { return m_completionModel; } void OllieKatePlugin::setCompletionBackend(const QString &backend) { m_completionBackend = backend; saveConfig(); } void OllieKatePlugin::setCompletionModel(const QString &model) { m_completionModel = model; saveConfig(); } bool OllieKatePlugin::completionEnabled() const { return m_completionEnabled; } void OllieKatePlugin::setCompletionEnabled(bool enabled) { m_completionEnabled = enabled; saveConfig(); } QString OllieKatePlugin::defaultBackend() const { return m_defaultBackend; } QString OllieKatePlugin::defaultModel() const { return m_defaultModel; } QString OllieKatePlugin::diffMode() const { return m_diffMode; } void OllieKatePlugin::setDefaultBackend(const QString &backend) { m_defaultBackend = backend; saveConfig(); } void OllieKatePlugin::setDefaultModel(const QString &model) { m_defaultModel = model; saveConfig(); } void OllieKatePlugin::setDiffMode(const QString &mode) { m_diffMode = mode; saveConfig(); } void OllieKatePlugin::loadConfig() { KConfigGroup grp(KSharedConfig::openConfig(), QStringLiteral("Ollie")); m_completionBackend = grp.readEntry("CompletionBackend", QString()); m_completionModel = grp.readEntry("CompletionModel", QString()); m_completionEnabled = grp.readEntry("CompletionEnabled", true); m_defaultBackend = grp.readEntry("DefaultBackend", QString()); m_defaultModel = grp.readEntry("DefaultModel", QString()); m_diffMode = grp.readEntry("DiffMode", QStringLiteral("unified")); } void OllieKatePlugin::saveConfig() { KConfigGroup grp(KSharedConfig::openConfig(), QStringLiteral("Ollie")); grp.writeEntry("CompletionBackend", m_completionBackend); grp.writeEntry("CompletionModel", m_completionModel); grp.writeEntry("CompletionEnabled", m_completionEnabled); grp.writeEntry("DefaultBackend", m_defaultBackend); grp.writeEntry("DefaultModel", m_defaultModel); grp.writeEntry("DiffMode", m_diffMode); grp.sync(); } // --- View --- OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *mainWindow) : QObject(mainWindow) , 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); // 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( plugin, "ollie_panel", KTextEditor::MainWindow::Right, QIcon::fromTheme("system-run"), "Ollie"); auto *container = new QWidget(m_toolView); auto *layout = new QVBoxLayout(container); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(2); // Session indicator + kill button auto *headerLayout = new QHBoxLayout(); headerLayout->setContentsMargins(0, 0, 0, 0); m_sessionLabel = new QLabel("No session", container); m_sessionLabel->setStyleSheet(QStringLiteral("color: gray; padding: 2px;")); m_sessionLabel->setFont(QApplication::font()); headerLayout->addWidget(m_sessionLabel); headerLayout->addStretch(); auto *killBtn = new QPushButton("×", container); killBtn->setFixedSize(20, 20); killBtn->setToolTip("Kill session"); killBtn->setFlat(true); connect(killBtn, &QPushButton::clicked, this, [this]() { if (m_sessionId.isEmpty()) return; m_9p->write(agentPath() + "/ctl", "kill"); }); headerLayout->addWidget(killBtn); layout->addLayout(headerLayout); // CWD label m_cwdLabel = new QLabel("", container); QFont smallFont = QApplication::font(); smallFont.setPointSizeF(smallFont.pointSizeF() * 0.85); m_cwdLabel->setStyleSheet(QStringLiteral("color: gray; padding: 0px 2px;")); m_cwdLabel->setFont(smallFont); m_cwdLabel->setWordWrap(true); layout->addWidget(m_cwdLabel); // Completion status indicator (in the main window status bar) bool startEnabled = m_plugin->completionEnabled(); if (auto *win = qobject_cast(m_mainWindow->window())) { // Reuse existing widget if another view already created it m_completionStatus = win->statusBar()->findChild(QStringLiteral("ollie_completion_status")); if (!m_completionStatus) { m_completionStatus = new QLabel(); m_completionStatus->setObjectName(QStringLiteral("ollie_completion_status")); m_completionStatus->setFont(smallFont); win->statusBar()->addPermanentWidget(m_completionStatus); } m_completionStatus->setText(startEnabled ? QStringLiteral("✦ completion on") : QStringLiteral("✦ completion off")); m_completionStatus->setStyleSheet(startEnabled ? QStringLiteral("color: green; padding: 0px 4px;") : QStringLiteral("color: gray; padding: 0px 4px;")); } else { m_completionStatus = nullptr; } // Agent management buttons auto *buttonBar = new QHBoxLayout(); auto *newBtn = new QPushButton("New", container); 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 another agent in this session"); connect(pickBtn, &QPushButton::clicked, this, &OllieKateView::pickAgent); buttonBar->addWidget(pickBtn); buttonBar->addStretch(); layout->addLayout(buttonBar); // Chat view m_chatView = new QTextBrowser(container); m_chatView->setReadOnly(true); m_chatView->setOpenLinks(false); m_chatView->setOpenExternalLinks(false); QFont chatFont(QApplication::font()); chatFont.setPointSize(qMax(12, QApplication::font().pointSize() + 2)); m_chatView->setFont(chatFont); m_chatView->setPlaceholderText("No active session.\nClick 'New' or use context actions."); connect(m_chatView, &QTextBrowser::anchorClicked, this, &OllieKateView::onAnchorClicked); layout->addWidget(m_chatView); // Scroll-to-bottom button (floats over chat view) m_scrollBottomBtn = new QPushButton("↓", container); m_scrollBottomBtn->setFixedSize(28, 28); m_scrollBottomBtn->setToolTip("Scroll to bottom"); m_scrollBottomBtn->hide(); connect(m_scrollBottomBtn, &QPushButton::clicked, this, [this]() { auto *sb = m_chatView->verticalScrollBar(); sb->setValue(sb->maximum()); m_scrollBottomBtn->hide(); }); // Position over bottom-right of chat view, updated on resize auto repositionBtn = [this]() { if (!m_chatView) return; QPoint pos = m_chatView->geometry().bottomRight() - QPoint(36, 36); m_scrollBottomBtn->move(pos); }; m_chatView->installEventFilter(this); // handle resize in eventFilter repositionBtn(); // Also show/hide on manual scroll + lazy-load older messages connect(m_chatView->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) { auto *sb = m_chatView->verticalScrollBar(); if (value >= sb->maximum() - 4) { m_scrollBottomBtn->hide(); } else if (!m_chatView->toPlainText().isEmpty()) { m_scrollBottomBtn->show(); } // Lazy-load: when scrolled near the top, prepend older blocks if (value <= 50 && m_renderer->hasOlderBlocks() && !m_loadingOlder) { m_loadingOlder = true; m_renderer->prependOlder(m_chatView, 20); m_loadingOlder = false; } }); // Prompt input auto *promptLayout = new QHBoxLayout(); m_promptInput = new QPlainTextEdit(container); m_promptInput->setPlaceholderText("Message..."); m_promptInput->setMaximumHeight(80); m_promptInput->setTabChangesFocus(true); m_promptInput->installEventFilter(this); connect(m_promptInput, &QPlainTextEdit::textChanged, this, &OllieKateView::processEmojis); promptLayout->addWidget(m_promptInput); auto *sendBtn = new QPushButton("⏎", container); sendBtn->setFixedWidth(30); connect(sendBtn, &QPushButton::clicked, this, &OllieKateView::submitPrompt); promptLayout->addWidget(sendBtn); m_stopBtn = new QPushButton("■", container); m_stopBtn->setToolTip("Stop (interrupt the current agent turn)"); m_stopBtn->setFixedWidth(30); connect(m_stopBtn, &QPushButton::clicked, this, [this]() { if (!m_sessionId.isEmpty()) { m_9p->write(agentPath() + "/ctl", "stop"); } }); promptLayout->addWidget(m_stopBtn); layout->addLayout(promptLayout); // Start 9P streaming processes startChatStream(); startStateStream(); // Connect to existing session/agent if any (read-only) refreshSessions(); // Set up ghost-text completion on active view setupGhostProvider(); connect(mainWindow, &KTextEditor::MainWindow::viewChanged, 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); // Register "Review Diff" action with Git++ plugin (kate-worktree) setupGitPlusPlusDiffAction(); } OllieKateView::~OllieKateView() { clearAnnotations(); delete m_renderer; delete m_ghost; delete m_toolView; } // --- D-Bus signal handlers --- void OllieKateView::onChatUpdated(const QString &session_id, qlonglong offset, const QString &new_text) { if (session_id != m_sessionId) return; if (offset == m_chatOffset) { m_rawChat += new_text; m_chatOffset += new_text.size(); m_renderer->render(m_chatView, m_rawChat); annotateFileReferences(m_rawChat); } else { // Offset mismatch — refetch 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); annotateFileReferences(m_rawChat); m_scrollBottomBtn->hide(); } } } void OllieKateView::onStateChanged(const QString &session_id, const QString &new_state) { if (session_id != m_sessionId) return; QString dot; if (new_state == "idle") dot = "○ "; else if (new_state.startsWith("calling")) dot = "⚡ "; else dot = "● "; m_sessionLabel->setText(dot + "kate/" + m_agentId + " [" + new_state + "]"); } void OllieKateView::onSessionCreated(const QString &session_id) { // If we have no session, adopt this one if (m_sessionId.isEmpty()) { m_sessionId = session_id; m_chatView->clear(); m_chatOffset = 0; } } void OllieKateView::onSessionKilled(const QString &session_id) { if (session_id == m_sessionId) { m_sessionId.clear(); m_chatOffset = 0; m_rawChat.clear(); m_chatView->clear(); m_sessionLabel->setText("No session"); m_cwdLabel->setText(""); // Defer session switch to let Qt finish processing the D-Bus signal. // Calling render() synchronously here can crash because the QTextDocument // is still tearing down resources from the clear() above. QTimer::singleShot(0, this, &OllieKateView::refreshSessions); } } // --- Actions --- void OllieKateView::submitPrompt() { QString text = m_promptInput->toPlainText().trimmed(); if (text.isEmpty()) return; if (m_agentId.isEmpty()) { m_chatView->setHtml(QStringLiteral("

No agent selected. Click 'New' to create one.

")); return; } m_9p->write(agentPath() + "/prompt", text.toUtf8()); m_promptInput->clear(); } void OllieKateView::askAboutFile() { auto *view = m_mainWindow->activeView(); if (!view || !view->document()) return; QString filePath = view->document()->url().toLocalFile(); if (filePath.isEmpty()) return; if (m_agentId.isEmpty()) { m_chatView->setHtml(QStringLiteral("

No agent selected. Click 'New' to create one.

")); return; } QString prompt = "Describe and explain this file: " + filePath; m_9p->write(agentPath() + "/prompt", prompt.toUtf8()); } void OllieKateView::explainSelection() { auto *view = m_mainWindow->activeView(); if (!view) return; QString selection = view->selectionText(); if (selection.isEmpty()) { // No selection — explain current line auto cursor = view->cursorPosition(); selection = view->document()->line(cursor.line()); } if (selection.isEmpty()) return; if (m_agentId.isEmpty()) { m_chatView->setHtml(QStringLiteral("

No agent selected. Click 'New' to create one.

")); return; } QString filePath = view->document()->url().toLocalFile(); QString context = filePath.isEmpty() ? "" : " (from " + filePath + ")"; QString prompt = "Explain this code" + context + ":\n\n```\n" + selection + "\n```"; m_9p->write(agentPath() + "/prompt", prompt.toUtf8()); } 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 agentCfg = "cwd=" + cwd + "\n"; QByteArray resp = m_9p->rdwr("session/kate/agent/new", agentCfg.toUtf8()); if (resp.isEmpty()) { m_chatView->setHtml(QStringLiteral("

Failed to create agent: ") + m_9p->lastError().toHtmlEscaped() + QStringLiteral("

")); return; } QString newId = QString::fromUtf8(resp).trimmed(); switchToAgent(newId); } void OllieKateView::pickAgent() { if (m_sessionId.isEmpty()) { m_chatView->setHtml(QStringLiteral("

No session. Use 'New' to create one.

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

No agents exist. Use 'New' to create one.

")); return; } if (agents.size() == 1) { m_chatView->setHtml(QStringLiteral("

Only one agent exists. Use 'New' to create another.

")); 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=")) { cwd = line.mid(4); break; } } QString marker = (aid == m_agentId) ? " ← current" : ""; items << aid + " (" + cwd + ")" + marker; } 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::switchToAgent(const QString &agentId) { 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; } } // 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 --- QString OllieKateView::projectBaseDir() { // Try Kate's project plugin first QObject *projectView = m_mainWindow->pluginView("kateprojectplugin"); if (projectView) { QString dir = projectView->property("projectBaseDir").toString(); if (!dir.isEmpty()) return dir; } // Fall back to active file's directory auto *view = m_mainWindow->activeView(); if (view && view->document() && !view->document()->url().toLocalFile().isEmpty()) { return QFileInfo(view->document()->url().toLocalFile()).absolutePath(); } return QString(); } void OllieKateView::switchToSession(const QString &sessionId) { stopStreams(); m_sessionId = sessionId; m_rawChat.clear(); 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(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 + "/" + m_agentId + " [" + state + "]"); // Fetch cwd 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=")) { m_cwdLabel->setText(line.mid(4)); break; } } } // Fetch existing chat via 9P 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); m_scrollBottomBtn->hide(); } // Start streaming for new data startChatStream(); startStateStream(); } QString OllieKateView::activeSessionId() { return m_sessionId; } void OllieKateView::processEmojis() { static const QHash quickEmojis = { {";)", QStringLiteral("\U0001F609")}, {":)", QStringLiteral("\U0001F642")}, {":D", QStringLiteral("\U0001F603")}, {":(", QStringLiteral("\U0001F61E")}, {":P", QStringLiteral("\U0001F61B")}, {":p", QStringLiteral("\U0001F61B")}, {"<3", QStringLiteral("\u2764\uFE0F")}, {":O", QStringLiteral("\U0001F62E")}, {":o", QStringLiteral("\U0001F62E")}, {"B)", QStringLiteral("\U0001F60E")}, {":*", QStringLiteral("\U0001F618")}, {"XD", QStringLiteral("\U0001F606")}, {"xD", QStringLiteral("\U0001F606")}, {":|", QStringLiteral("\U0001F610")}, {">:(", QStringLiteral("\U0001F620")}, {"'(", QStringLiteral("\U0001F622")}, }; static const QHash shortcodes = { {"thumbsup", QStringLiteral("\U0001F44D")}, {"thumbsdown", QStringLiteral("\U0001F44E")}, {"fire", QStringLiteral("\U0001F525")}, {"rocket", QStringLiteral("\U0001F680")}, {"check", QStringLiteral("\u2705")}, {"x", QStringLiteral("\u274C")}, {"eyes", QStringLiteral("\U0001F440")}, {"thinking", QStringLiteral("\U0001F914")}, {"shrug", QStringLiteral("\U0001F937")}, {"wave", QStringLiteral("\U0001F44B")}, {"clap", QStringLiteral("\U0001F44F")}, {"pray", QStringLiteral("\U0001F64F")}, {"heart", QStringLiteral("\u2764\uFE0F")}, {"broken_heart", QStringLiteral("\U0001F494")}, {"star", QStringLiteral("\u2B50")}, {"sparkles", QStringLiteral("\u2728")}, {"zap", QStringLiteral("\u26A1")}, {"100", QStringLiteral("\U0001F4AF")}, {"tada", QStringLiteral("\U0001F389")}, {"party", QStringLiteral("\U0001F389")}, {"beer", QStringLiteral("\U0001F37A")}, {"beers", QStringLiteral("\U0001F37B")}, {"coffee", QStringLiteral("\u2615")}, {"wine", QStringLiteral("\U0001F377")}, {"pizza", QStringLiteral("\U0001F355")}, {"taco", QStringLiteral("\U0001F32E")}, {"cookie", QStringLiteral("\U0001F36A")}, {"cake", QStringLiteral("\U0001F382")}, {"sun", QStringLiteral("\u2600\uFE0F")}, {"moon", QStringLiteral("\U0001F319")}, {"rainbow", QStringLiteral("\U0001F308")}, {"ocean", QStringLiteral("\U0001F30A")}, {"dog", QStringLiteral("\U0001F436")}, {"cat", QStringLiteral("\U0001F431")}, {"bug", QStringLiteral("\U0001F41B")}, {"snake", QStringLiteral("\U0001F40D")}, {"unicorn", QStringLiteral("\U0001F984")}, {"skull", QStringLiteral("\U0001F480")}, {"ghost", QStringLiteral("\U0001F47B")}, {"robot", QStringLiteral("\U0001F916")}, {"poop", QStringLiteral("\U0001F4A9")}, {"brain", QStringLiteral("\U0001F9E0")}, {"bulb", QStringLiteral("\U0001F4A1")}, {"gear", QStringLiteral("\u2699\uFE0F")}, {"lock", QStringLiteral("\U0001F512")}, {"key", QStringLiteral("\U0001F511")}, {"warning", QStringLiteral("\u26A0\uFE0F")}, {"no_entry", QStringLiteral("\u26D4")}, {"muscle", QStringLiteral("\U0001F4AA")}, {"ok_hand", QStringLiteral("\U0001F44C")}, {"fist", QStringLiteral("\u270A")}, {"v", QStringLiteral("\u270C\uFE0F")}, {"metal", QStringLiteral("\U0001F918")}, {"middle_finger", QStringLiteral("\U0001F595")}, {"smile", QStringLiteral("\U0001F604")}, {"joy", QStringLiteral("\U0001F602")}, {"rofl", QStringLiteral("\U0001F923")}, {"wink", QStringLiteral("\U0001F609")}, {"blush", QStringLiteral("\U0001F60A")}, {"heart_eyes", QStringLiteral("\U0001F60D")}, {"cool", QStringLiteral("\U0001F60E")}, {"angry", QStringLiteral("\U0001F620")}, {"cry", QStringLiteral("\U0001F622")}, {"sob", QStringLiteral("\U0001F62D")}, {"scream", QStringLiteral("\U0001F631")}, {"facepalm", QStringLiteral("\U0001F926")}, {"gift", QStringLiteral("\U0001F381")}, {"balloon", QStringLiteral("\U0001F388")}, {"trophy", QStringLiteral("\U0001F3C6")}, {"crown", QStringLiteral("\U0001F451")}, {"gem", QStringLiteral("\U0001F48E")}, {"money", QStringLiteral("\U0001F4B0")}, {"computer", QStringLiteral("\U0001F4BB")}, {"phone", QStringLiteral("\U0001F4F1")}, {"earth", QStringLiteral("\U0001F30D")}, {"airplane", QStringLiteral("\u2708\uFE0F")}, }; static bool processing = false; if (processing) return; processing = true; QTextCursor cursor = m_promptInput->textCursor(); int pos = cursor.position(); QString text = m_promptInput->toPlainText(); // Quick emoticons (check last 2-4 chars before cursor) for (int len = 2; len <= qMin(4, pos); ++len) { QString candidate = text.mid(pos - len, len); auto it = quickEmojis.find(candidate); if (it != quickEmojis.end()) { cursor.setPosition(pos - len); cursor.setPosition(pos, QTextCursor::KeepAnchor); cursor.insertText(it.value()); processing = false; return; } } // :shortcode: (find closing : at cursor, opening : before it) if (pos >= 3 && text[pos - 1] == ':') { int start = text.lastIndexOf(':', pos - 2); if (start >= 0 && start < pos - 2) { QString code = text.mid(start + 1, pos - start - 2); auto it = shortcodes.find(code); if (it != shortcodes.end()) { cursor.setPosition(start); cursor.setPosition(pos, QTextCursor::KeepAnchor); cursor.insertText(it.value()); } } } processing = false; } void OllieKateView::onAnchorClicked(const QUrl &url) { if (url.scheme() == QStringLiteral("ollie-diff")) { bool ok; int index = url.path().mid(1).toInt(&ok); // path is "/N" if (ok) { showDiffBlock(index); } } else if (url.scheme() == QStringLiteral("ollie-apply")) { bool ok; int index = url.path().mid(1).toInt(&ok); // path is "/N" if (ok) { applyCodeBlock(index); } } } void OllieKateView::showDiffBlock(int index) { if (index < 0 || index >= m_diffBlocks.size()) return; QByteArray diffData = m_diffBlocks[index].toUtf8(); QString mode = m_plugin->diffMode(); // Set Kate's diff style preference before opening { KConfigGroup grp(KSharedConfig::openConfig(QStringLiteral("katerc")), QStringLiteral("Diff")); // Kate uses: 0 = Side By Side, 1 = Unified, 2 = Raw int style = 1; // unified default if (mode == QStringLiteral("side-by-side")) style = 0; else if (mode == QStringLiteral("raw")) style = 2; grp.writeEntry("DiffStyle", style); grp.sync(); } #ifdef HAVE_KATE_DIFF DiffParams params; params.tabTitle = QStringLiteral("Ollie Diff %1").arg(index + 1); params.flags = DiffParams::Flag(DiffParams::ShowFileName); Utils::showDiff(diffData, params, m_mainWindow); #else // Fallback: open as a new document with Diff highlighting auto *view = m_mainWindow->openUrl(QUrl()); if (view) { view->document()->setText(QString::fromUtf8(diffData)); view->document()->setHighlightingMode(QStringLiteral("Diff")); } #endif } void OllieKateView::refreshSessions() { // At startup, try to select agent matching current project selectAgentForProject(); } void OllieKateView::setupGhostProvider() { auto *view = m_mainWindow->activeView(); if (!view) return; // If ghost provider is already on this view, keep it if (m_ghost && m_ghost->view() == view) return; // Remove old provider 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); } } void OllieKateView::setupGitPlusPlusDiffAction() { QObject *gitView = m_mainWindow->pluginView(QStringLiteral("gitplusplus")); if (!gitView) return; auto *action = new QAction(QIcon::fromTheme(QStringLiteral("document-preview")), QStringLiteral("Ollie: Review Diff"), this); connect(action, &QAction::triggered, this, &OllieKateView::reviewDiff); QMetaObject::invokeMethod(gitView, "registerDiffAction", Q_ARG(QAction *, action)); } bool OllieKateView::eventFilter(QObject *obj, QEvent *event) { // Reposition scroll-to-bottom button when chat view resizes if (obj == m_chatView && event->type() == QEvent::Resize) { QPoint pos = m_chatView->geometry().bottomRight() - QPoint(36, 36); m_scrollBottomBtn->move(pos); } // Prompt input: Enter sends, Shift+Enter / Ctrl+J inserts newline if (obj == m_promptInput && event->type() == QEvent::KeyPress) { auto *ke = static_cast(event); if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { if (ke->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier)) { m_promptInput->insertPlainText("\n"); } else { submitPrompt(); } return true; } if (ke->key() == Qt::Key_J && (ke->modifiers() & Qt::ControlModifier)) { m_promptInput->insertPlainText("\n"); return true; } } if (event->type() == QEvent::KeyPress && m_ghost) { auto *keyEvent = static_cast(event); // Second key of Meta+O chord if (m_ghostPrefix) { m_ghostPrefix = false; switch (keyEvent->key()) { case Qt::Key_N: m_ghost->cycle(+1); return true; case Qt::Key_P: m_ghost->cycle(-1); return true; case Qt::Key_C: m_ghost->setEnabled(!m_ghost->isEnabled()); if (m_ghost->isEnabled()) { m_completionStatus->setText(QStringLiteral("\u2726 completion on")); m_completionStatus->setStyleSheet(QStringLiteral("color: green; padding: 0px 2px;")); } else { m_completionStatus->setText(QStringLiteral("\u2726 completion off")); m_completionStatus->setStyleSheet(QStringLiteral("color: gray; padding: 0px 2px;")); } return true; case Qt::Key_D: m_ghost->dismiss(); return true; case Qt::Key_Tab: m_ghost->requestCompletion(); return true; default: // Not a recognized chord — pass through break; } } // Meta+O starts the chord prefix if (keyEvent->key() == Qt::Key_O && keyEvent->modifiers() == Qt::MetaModifier) { m_ghostPrefix = true; return true; } if (m_ghost->hasSuggestion()) { // Don't intercept keys while a popup or completion widget is active auto *activeView = m_mainWindow->activeView(); #if KTEXTEDITOR_VERSION_MAJOR >= 6 if (QApplication::activePopupWidget() || (activeView && activeView->isCompletionActive())) return QObject::eventFilter(obj, event); #else auto *ccIface = activeView ? dynamic_cast(activeView) : nullptr; if (QApplication::activePopupWidget() || (ccIface && ccIface->isCompletionActive())) return QObject::eventFilter(obj, event); #endif if (keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) { if (m_ghost->accept()) { return true; // consumed } } } } return QObject::eventFilter(obj, event); } // --- Context Menu Integration --- void OllieKateView::setupContextMenu(KTextEditor::View *view) { if (!view) return; // Connect directly to the view's contextMenuAboutToShow signal. // This signal is emitted by KateView before showing the context menu. connect(view, &KTextEditor::View::contextMenuAboutToShow, this, [this](KTextEditor::View *v, QMenu *menu) { // Remove previous ollie actions to avoid duplicates const auto actions = menu->actions(); for (auto *a : actions) { if (a->objectName().startsWith(QStringLiteral("ollie_ctx_"))) { menu->removeAction(a); delete a; } } auto *sep = menu->addSeparator(); sep->setObjectName(QStringLiteral("ollie_ctx_sep")); auto *askAction = menu->addAction(QIcon::fromTheme(QStringLiteral("help-about")), QStringLiteral("Ollie: Ask about file")); askAction->setObjectName(QStringLiteral("ollie_ctx_ask")); connect(askAction, &QAction::triggered, this, &OllieKateView::askAboutFile); // "Review Diff" — visible when the document is a diff if (v->document()->highlightingMode() == QStringLiteral("Diff")) { auto *reviewDiffAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-preview")), QStringLiteral("Ollie: Review Diff")); reviewDiffAction->setObjectName(QStringLiteral("ollie_ctx_review_diff")); connect(reviewDiffAction, &QAction::triggered, this, &OllieKateView::reviewDiff); } if (!v->selection()) return; auto *explainAction = menu->addAction(QIcon::fromTheme(QStringLiteral("help-contents")), QStringLiteral("Ollie: Explain this")); explainAction->setObjectName(QStringLiteral("ollie_ctx_explain")); connect(explainAction, &QAction::triggered, this, &OllieKateView::explainSelection); auto *fixAction = menu->addAction(QIcon::fromTheme(QStringLiteral("tools-wizard")), QStringLiteral("Ollie: Fix this")); fixAction->setObjectName(QStringLiteral("ollie_ctx_fix")); connect(fixAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Fix")); }); auto *refactorAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), QStringLiteral("Ollie: Refactor this")); refactorAction->setObjectName(QStringLiteral("ollie_ctx_refactor")); connect(refactorAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Refactor")); }); auto *testAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), QStringLiteral("Ollie: Add tests for this")); testAction->setObjectName(QStringLiteral("ollie_ctx_test")); connect(testAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Write unit tests for")); }); auto *docAction = menu->addAction(QIcon::fromTheme(QStringLiteral("documentation")), QStringLiteral("Ollie: Document this")); docAction->setObjectName(QStringLiteral("ollie_ctx_doc")); connect(docAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Add documentation comments to")); }); }); } void OllieKateView::submitSelectionPrompt(const QString &action) { auto *view = m_mainWindow->activeView(); if (!view || !view->selection()) return; QString selection = view->selectionText(); if (selection.isEmpty()) return; if (m_agentId.isEmpty()) { m_chatView->setHtml(QStringLiteral("

No agent selected. Click 'New' to create one.

")); return; } QString filePath = view->document()->url().toLocalFile(); auto cursor = view->selectionRange().start(); QString location; if (!filePath.isEmpty()) { location = " in `" + filePath + ":" + QString::number(cursor.line() + 1) + "`"; } QString prompt = action + " this code" + location + ":\n\n```\n" + selection + "\n```"; m_9p->write(agentPath() + "/prompt", prompt.toUtf8()); } void OllieKateView::reviewDiff() { QString diff; // Try to get diff from Git++ plugin (kate-worktree) QObject *gitView = m_mainWindow->pluginView(QStringLiteral("gitplusplus")); if (gitView) { QByteArray data = gitView->property("lastDiff").toByteArray(); if (!data.isEmpty()) diff = QString::fromUtf8(data); } // Fall back to active document content (e.g. diff opened as plain text) if (diff.isEmpty()) { auto *view = m_mainWindow->activeView(); if (view && view->document()) diff = view->document()->text(); } if (diff.trimmed().isEmpty()) return; if (m_agentId.isEmpty()) { m_chatView->setHtml(QStringLiteral("

No agent selected. Click 'New' to create one.

")); return; } QString prompt = QStringLiteral("Review this diff. Point out potential issues, bugs, " "or improvements:\n\n```diff\n") + diff + QStringLiteral("\n```"); m_9p->write(agentPath() + "/prompt", prompt.toUtf8()); } // --- Apply Code Blocks --- void OllieKateView::applyCodeBlock(int index) { if (index < 0 || index >= m_applyBlocks.size()) return; const auto &block = m_applyBlocks[index]; if (block.code.trimmed().isEmpty()) return; KTextEditor::View *view = nullptr; KTextEditor::Document *doc = nullptr; if (!block.filePath.isEmpty()) { // Open the target file in Kate view = m_mainWindow->openUrl(QUrl::fromLocalFile(block.filePath)); } else { // No file path detected — apply to the currently active document view = m_mainWindow->activeView(); } if (!view || !view->document()) return; doc = view->document(); // Line-range replace mode: replace lines startLine through endLine if (block.startLine > 0 && block.endLine > 0) { int start = block.startLine - 1; // convert to 0-indexed int end = block.endLine - 1; if (end >= doc->lines()) end = doc->lines() - 1; if (start > end) start = end; KTextEditor::Range range( KTextEditor::Cursor(start, 0), KTextEditor::Cursor(end, doc->lineLength(end))); doc->replaceText(range, block.code); view->setCursorPosition(KTextEditor::Cursor(start, 0)); return; } // If there's a selection in the target view, replace it with the code block if (view->selection()) { doc->replaceText(view->selectionRange(), block.code); return; } // Otherwise insert at cursor position KTextEditor::Cursor cursor = view->cursorPosition(); doc->insertText(cursor, block.code); } // --- Inline Annotations --- void OllieKateView::clearAnnotations() { if (m_annotationDoc) { // Document still alive — safe to delete its MovingRanges qDeleteAll(m_annotations); } // If document is gone, ranges are already destroyed — just drop pointers m_annotations.clear(); m_annotationDoc = nullptr; } void OllieKateView::annotateFileReferences(const QString &chatText) { clearAnnotations(); auto *view = m_mainWindow->activeView(); if (!view || !view->document()) return; QString currentFile = view->document()->url().toLocalFile(); if (currentFile.isEmpty()) return; // Match patterns like /path/to/file.ext:123 or /path/to/file.ext:123:45 static const QRegularExpression fileRefRe( R"((?:^|[\s`])(/[^\s:]+):(\d+)(?::(\d+))?)", QRegularExpression::MultilineOption); auto *doc = view->document(); m_annotationDoc = doc; #if KTEXTEDITOR_VERSION_MAJOR < 6 auto *movingIface = qobject_cast(doc); #else auto *movingIface = doc; #endif if (!movingIface) return; KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); attr->setBackground(m_renderer ? QColor(255, 255, 0, 40) : QColor(255, 255, 0, 40)); attr->setFontBold(false); QSet annotatedLines; auto it = fileRefRe.globalMatch(chatText); while (it.hasNext()) { auto match = it.next(); QString path = match.captured(1); int line = match.captured(2).toInt() - 1; // 0-indexed // Only annotate if it references the currently open file if (path != currentFile) continue; if (line < 0 || line >= doc->lines()) continue; if (annotatedLines.contains(line)) continue; annotatedLines.insert(line); KTextEditor::Range range(line, 0, line, doc->lineLength(line)); auto *movingRange = movingIface->newMovingRange(range); movingRange->setAttribute(attr); movingRange->setZDepth(-100.0); // behind text m_annotations.append(movingRange); } } // --- 9P helpers --- 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()) { QString user = qEnvironmentVariable("USER"); QString display = qEnvironmentVariable("DISPLAY"); ns = "/tmp/ns." + user + "." + display; } return "unix!" + ns + "/ollie"; } // --- 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 + "kate/" + m_agentId + " [" + 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"