feat: add support code completion via acp

This commit is contained in:
Petr Mironychev
2026-07-22 10:27:33 +02:00
parent 442263a6a7
commit 583b5de14c
34 changed files with 2310 additions and 1414 deletions

View File

@@ -0,0 +1,427 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/AcpCompletionEngine.hpp"
#include <chrono>
#include <LLMQore/AcpClient.hpp>
#include <LLMQore/CallbackPermissionProvider.hpp>
#include "acp/AgentSpawn.hpp"
#include "context/DocumentContextReader.hpp"
#include "logger/Logger.hpp"
#include "settings/SettingsConstants.hpp"
namespace QodeAssist {
namespace {
void upsertEnv(QList<Acp::AgentEnvVariable> &env, const QString &name, const QString &value)
{
for (auto &variable : env) {
if (variable.name == name) {
variable.value = value;
return;
}
}
env.append({name, value});
}
LLMQore::Acp::RequestPermissionResult autoAllow(
const QString &,
const LLMQore::Acp::ToolCall &toolCall,
const QList<LLMQore::Acp::PermissionOption> &options)
{
QStringList optionDump;
for (const auto &option : options)
optionDump.append(QString("%1(%2)").arg(option.optionId, option.kind));
const LLMQore::Acp::PermissionOption *chosen = nullptr;
for (const auto &option : options) {
if (option.kind == QLatin1String(LLMQore::Acp::PermissionOptionKind::AllowOnce)) {
chosen = &option;
break;
}
if (!chosen && option.kind == QLatin1String(LLMQore::Acp::PermissionOptionKind::AllowAlways))
chosen = &option;
}
if (!chosen && !options.isEmpty())
chosen = &options.first();
LOG_MESSAGE(
QString("ACP completion: permission requested for tool '%1' (kind=%2); options=[%3]; %4")
.arg(
toolCall.title,
toolCall.kind,
optionDump.join(QStringLiteral(", ")),
chosen ? QString("auto-allowing '%1'").arg(chosen->optionId)
: QStringLiteral("cancelling (no option offered)")));
return chosen ? LLMQore::Acp::RequestPermissionResult::selected(chosen->optionId)
: LLMQore::Acp::RequestPermissionResult::cancelled();
}
} // namespace
AcpCompletionEngine::AcpCompletionEngine(
const Settings::CodeCompletionSettings &completeSettings,
const Settings::GeneralSettings &generalSettings,
AgentResolver agentResolver,
Context::IDocumentReader &documentReader,
Tools::ProposeCompletionTool *proposeTool,
QObject *parent)
: CompletionEngine(parent)
, m_completeSettings(completeSettings)
, m_generalSettings(generalSettings)
, m_agentResolver(std::move(agentResolver))
, m_documentReader(documentReader)
, m_proposeTool(proposeTool)
, m_clientFactory(&Acp::spawnAgent)
, m_completionServer(new Mcp::CompletionMcpServer(this))
, m_permissions(new LLMQore::Acp::CallbackPermissionProvider(&autoAllow, this))
{
if (m_proposeTool) {
connect(
m_proposeTool,
&Tools::ProposeCompletionTool::completionProposed,
this,
&AcpCompletionEngine::handleCompletionProposed);
}
}
void AcpCompletionEngine::setClientFactory(ClientFactory factory)
{
if (factory)
m_clientFactory = std::move(factory);
}
bool AcpCompletionEngine::hasConfiguredAgent() const
{
const QString agentId = m_completeSettings.completionAgentId();
if (agentId.isEmpty() || !m_agentResolver)
return false;
return m_agentResolver(agentId).has_value();
}
void AcpCompletionEngine::request(quint64 requestId, const CompletionContext &context)
{
if (m_active) {
const auto stale = *m_active;
m_active.reset();
if (m_client && !m_sessionId.isEmpty())
m_client->cancel(m_sessionId);
emit completionFailed(stale.requestId, QStringLiteral("Superseded by a newer request"));
}
const QString agentId = m_completeSettings.completionAgentId();
if (agentId.isEmpty()) {
emit completionFailed(
requestId, QStringLiteral("Agentic (ACP agent) completion has no agent selected"));
return;
}
if (!m_agentResolver || !m_agentResolver(agentId).has_value()) {
emit completionFailed(
requestId, QString("ACP completion agent '%1' is not in the catalogue").arg(agentId));
return;
}
auto documentInfo = m_documentReader.readDocument(context.filePath);
if (!documentInfo.document) {
emit completionFailed(
requestId, QString("Document is not available: %1").arg(context.filePath));
return;
}
Context::DocumentContextReader reader(
documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
const auto ctx = reader.prepareContext(context.line, context.column, m_completeSettings);
const QString codeContext = ctx.prefix.value_or("") + "<cursor>" + ctx.suffix.value_or("");
m_active
= ActiveRequest{requestId, documentInfo.filePath, context.line, context.column, codeContext};
LOG_MESSAGE(
QString("ACP completion: request %1 for %2 (line %3, col %4), agent '%5'")
.arg(QString::number(requestId), documentInfo.filePath)
.arg(context.line)
.arg(context.column)
.arg(agentId));
if (m_boundAgentId != agentId) {
if (!m_boundAgentId.isEmpty())
LOG_MESSAGE(
QString("ACP completion: selected agent changed %1 -> %2, releasing client")
.arg(m_boundAgentId, agentId));
releaseClient();
}
m_boundAgentId = agentId;
ensureClientAndPrompt();
}
void AcpCompletionEngine::ensureClientAndPrompt()
{
if (m_client && m_sessionReady) {
LOG_MESSAGE(
QString("ACP completion: reusing live session %1, sending a new prompt")
.arg(m_sessionId));
sendPrompt();
return;
}
if (m_client) {
LOG_MESSAGE("ACP completion: client is still establishing its session, will prompt once ready");
return;
}
const auto agent = m_agentResolver ? m_agentResolver(m_boundAgentId) : std::nullopt;
if (!agent) {
failActive(QString("ACP completion agent '%1' vanished from the catalogue").arg(m_boundAgentId));
return;
}
Acp::AgentDefinition launchAgent = *agent;
const QString model = m_generalSettings.ccModel();
if (!model.isEmpty()) {
upsertEnv(launchAgent.distribution.env, QStringLiteral("ANTHROPIC_MODEL"), model);
LOG_MESSAGE(
QString("ACP completion: injecting ANTHROPIC_MODEL=%1 into agent env").arg(model));
}
const QString cwd = Acp::agentWorkingDirectory();
LOG_MESSAGE(
QString("ACP completion: spawning agent '%1' (%2) in %3")
.arg(launchAgent.id, launchAgent.name, cwd));
const Acp::AgentProcess process = m_clientFactory(launchAgent, cwd, this);
m_client = process.client;
if (!m_client) {
failActive(QStringLiteral("This agent has no launchable distribution"));
return;
}
m_client->setPermissionProvider(m_permissions);
const int generation = ++m_clientGeneration;
connect(m_client, &LLMQore::Acp::AcpClient::errorOccurred, this, [this, generation](const QString &error) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion: agent error: %1").arg(error));
failActive(error);
releaseClient();
});
connect(m_client, &LLMQore::Acp::AcpClient::disconnected, this, [this, generation]() {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE("ACP completion: agent disconnected");
m_sessionReady = false;
m_sessionId.clear();
});
connect(m_client, &LLMQore::Acp::AcpClient::agentStderr, this, [this, generation](const QString &line) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion [agent stderr]: %1").arg(line));
});
connect(
m_client,
&LLMQore::Acp::AcpClient::toolCallStarted,
this,
[this, generation](const QString &, const LLMQore::Acp::ToolCall &call) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(
QString("ACP completion: agent tool call started: title='%1' kind=%2 status=%3")
.arg(call.title, call.kind, call.status));
});
connect(
m_client,
&LLMQore::Acp::AcpClient::toolCallUpdated,
this,
[this, generation](const QString &, const LLMQore::Acp::ToolCall &call) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(
QString("ACP completion: agent tool call updated: title='%1' kind=%2 status=%3")
.arg(call.title, call.kind, call.status));
});
LOG_MESSAGE("ACP completion: connecting and initializing the agent");
m_client->connectAndInitialize(std::chrono::seconds(30))
.then(this, [this, generation, cwd](const LLMQore::Acp::InitializeResult &info) {
if (generation != m_clientGeneration || !m_client)
return;
const bool http = info.agentCapabilities.mcpCapabilities.http;
LOG_MESSAGE(
QString("ACP completion: agent initialized (protocol %1, MCP http=%2 sse=%3)")
.arg(QString::number(info.protocolVersion),
http ? QStringLiteral("true") : QStringLiteral("false"),
info.agentCapabilities.mcpCapabilities.sse ? QStringLiteral("true")
: QStringLiteral("false")));
if (!http) {
LOG_MESSAGE(
"ACP completion: WARNING — this agent does not advertise HTTP MCP support, so "
"it will likely ignore the QodeAssist MCP server and never call "
"propose_completion. Use an agent that accepts HTTP MCP servers.");
}
const QString mcpUrl = m_completionServer->start(m_proposeTool);
if (mcpUrl.isEmpty()) {
failActive(QStringLiteral(
"The QodeAssist completion MCP server could not start; ACP completion needs it"));
releaseClient();
return;
}
LLMQore::Acp::NewSessionParams params;
params.cwd = cwd;
params.mcpServers
= {LLMQore::Acp::McpServer::http(Mcp::CompletionMcpServer::serverName(), mcpUrl)};
LOG_MESSAGE(
QString("ACP completion: creating session, offering MCP server '%1' at %2")
.arg(Mcp::CompletionMcpServer::serverName(), mcpUrl));
m_client->newSession(params, std::chrono::seconds(30))
.then(this, [this, generation](const LLMQore::Acp::NewSessionResult &result) {
if (generation != m_clientGeneration || !m_client)
return;
m_sessionId = result.sessionId;
m_sessionReady = true;
LOG_MESSAGE(QString("ACP completion: session %1 established").arg(m_sessionId));
sendPrompt();
})
.onFailed(this, [this, generation](const std::exception &e) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion: session/new failed: %1")
.arg(QString::fromUtf8(e.what())));
failActive(QString::fromUtf8(e.what()));
releaseClient();
});
})
.onFailed(this, [this, generation](const std::exception &e) {
if (generation != m_clientGeneration)
return;
LOG_MESSAGE(QString("ACP completion: initialize failed: %1")
.arg(QString::fromUtf8(e.what())));
failActive(QString::fromUtf8(e.what()));
releaseClient();
});
}
void AcpCompletionEngine::sendPrompt()
{
if (!m_active || !m_client || m_sessionId.isEmpty())
return;
const int generation = m_clientGeneration;
const quint64 activeId = m_active->requestId;
const QString text = promptText();
LOG_MESSAGE(
QString("ACP completion: prompting session %1 for request %2")
.arg(m_sessionId, QString::number(activeId)));
LOG_MESSAGE(QString("ACP completion: prompt text:\n%1").arg(text));
m_client->prompt(m_sessionId, {LLMQore::Acp::ContentBlock::makeText(text)})
.then(this, [this, generation, activeId](const LLMQore::Acp::PromptResult &result) {
if (generation != m_clientGeneration || !m_active || m_active->requestId != activeId)
return;
LOG_MESSAGE(
QString("ACP completion: prompt for request %1 finished with stopReason '%2' but "
"no proposal arrived")
.arg(QString::number(activeId), result.stopReason));
m_active.reset();
emit completionFailed(
activeId,
QStringLiteral("The agent finished without proposing a completion"));
})
.onFailed(this, [this, generation, activeId](const std::exception &e) {
if (generation != m_clientGeneration || !m_active || m_active->requestId != activeId)
return;
m_active.reset();
LOG_MESSAGE(QString("ACP completion failed: %1").arg(QString::fromUtf8(e.what())));
emit completionFailed(activeId, QString::fromUtf8(e.what()));
});
}
QString AcpCompletionEngine::promptText() const
{
if (!m_active)
return {};
return QString(
"Complete the code at the <cursor> marker in the snippet below. Be fast: do NOT "
"use any other tools, do NOT read files, run commands, or search — work only "
"from this snippet.\n\n"
"<code_context>\n%1\n</code_context>\n\n"
"First work out the exact code that should replace <cursor> (just the "
"continuation — no explanations, no code fences, and do not repeat the code "
"already before or after the cursor). Then make ONE call to propose_completion "
"with that code as the `text` argument. The `text` must be the real completion "
"and must never be empty. file=%2, line=%3, column=%4.")
.arg(m_active->codeContext)
.arg(m_active->filePath)
.arg(m_active->line)
.arg(m_active->column);
}
void AcpCompletionEngine::cancel(quint64 requestId)
{
if (!m_active || m_active->requestId != requestId)
return;
m_active.reset();
if (m_client && !m_sessionId.isEmpty())
m_client->cancel(m_sessionId);
}
void AcpCompletionEngine::handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text)
{
Q_UNUSED(line)
Q_UNUSED(column)
LOG_MESSAGE(
QString("ACP completion: propose_completion called for '%1' (%2 chars)")
.arg(filePath, QString::number(text.size())));
if (!m_active) {
LOG_MESSAGE("ACP completion: ignoring proposal — no completion request is active");
return;
}
if (m_active->filePath != filePath) {
LOG_MESSAGE(
QString("ACP completion: proposal path '%1' differs from the requested '%2'; "
"accepting anyway (single active request)")
.arg(filePath, m_active->filePath));
}
const quint64 externalId = m_active->requestId;
m_active.reset();
LOG_MESSAGE(
QString("ACP completion: rendering proposal for request %1").arg(QString::number(externalId)));
emit completionReady(externalId, text);
}
void AcpCompletionEngine::failActive(const QString &error)
{
if (!m_active)
return;
const auto stale = *m_active;
m_active.reset();
LOG_MESSAGE(QString("ACP completion failed: %1").arg(error));
emit completionFailed(stale.requestId, error);
}
void AcpCompletionEngine::releaseClient()
{
++m_clientGeneration;
m_sessionReady = false;
m_sessionId.clear();
if (m_client) {
m_client->shutdown();
m_client->deleteLater();
m_client = nullptr;
}
if (m_completionServer)
m_completionServer->stop();
}
} // namespace QodeAssist

View File

@@ -0,0 +1,86 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <functional>
#include <optional>
#include <QString>
#include <LLMQore/AcpPermissionProvider.hpp>
#include "acp/AgentDefinition.hpp"
#include "acp/AgentSpawn.hpp"
#include "completion/CompletionEngine.hpp"
#include "context/IDocumentReader.hpp"
#include "mcp/CompletionMcpServer.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "tools/ProposeCompletionTool.hpp"
namespace QodeAssist {
class AcpCompletionEngine : public CompletionEngine
{
Q_OBJECT
public:
using ClientFactory = std::function<
Acp::AgentProcess(const Acp::AgentDefinition &agent, const QString &cwd, QObject *parent)>;
using AgentResolver = std::function<std::optional<Acp::AgentDefinition>(const QString &id)>;
AcpCompletionEngine(
const Settings::CodeCompletionSettings &completeSettings,
const Settings::GeneralSettings &generalSettings,
AgentResolver agentResolver,
Context::IDocumentReader &documentReader,
Tools::ProposeCompletionTool *proposeTool,
QObject *parent = nullptr);
void setClientFactory(ClientFactory factory);
void request(quint64 requestId, const CompletionContext &context) override;
void cancel(quint64 requestId) override;
bool hasConfiguredAgent() const;
private slots:
void handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text);
private:
struct ActiveRequest
{
quint64 requestId = 0;
QString filePath;
int line = 0;
int column = 0;
QString codeContext;
};
void ensureClientAndPrompt();
void sendPrompt();
void failActive(const QString &error);
void releaseClient();
QString promptText() const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
AgentResolver m_agentResolver;
Context::IDocumentReader &m_documentReader;
Tools::ProposeCompletionTool *m_proposeTool = nullptr;
ClientFactory m_clientFactory;
Mcp::CompletionMcpServer *m_completionServer = nullptr;
LLMQore::Acp::AcpPermissionProvider *m_permissions = nullptr;
LLMQore::Acp::AcpClient *m_client = nullptr;
QString m_boundAgentId;
QString m_sessionId;
int m_clientGeneration = 0;
bool m_sessionReady = false;
std::optional<ActiveRequest> m_active;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,249 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/AgenticCompletionEngine.hpp"
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QUrl>
#include "context/DocumentContextReader.hpp"
#include "llmcore/ContextData.hpp"
#include "logger/Logger.hpp"
#include "settings/ToolsSettings.hpp"
namespace QodeAssist {
AgenticCompletionEngine::AgenticCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger,
QObject *parent)
: CompletionEngine(parent)
, m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
{
}
AgenticCompletionEngine::~AgenticCompletionEngine()
{
const auto requests = m_activeRequests;
m_activeRequests.clear();
for (auto it = requests.constBegin(); it != requests.constEnd(); ++it) {
if (it.value().provider)
it.value().provider->cancelRequest(it.key());
}
}
void AgenticCompletionEngine::request(quint64 requestId, const CompletionContext &context)
{
auto documentInfo = m_documentReader.readDocument(context.filePath);
if (!documentInfo.document) {
const QString error = QString("Document is not available: %1").arg(context.filePath);
LOG_MESSAGE("Error: " + error);
emit completionFailed(requestId, error);
return;
}
const auto providerName = m_generalSettings.ccProvider();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
const QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
if (!provider->capabilities().testFlag(Providers::ProviderCapability::Tools)) {
const QString error
= QString("Agentic completion needs tool calling, which provider %1 does not support")
.arg(providerName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
auto promptTemplate = m_promptProvider->getTemplateByName(m_generalSettings.ccTemplate());
if (!promptTemplate) {
const QString error
= QString("No template found with name: %1").arg(m_generalSettings.ccTemplate());
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
if (promptTemplate->type() != Templates::TemplateType::Chat) {
const QString error
= QString("Agentic completion needs a chat-type template; %1 is FIM-only")
.arg(promptTemplate->name());
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
if (auto *toolsManager = provider->toolsManager()) {
auto *proposeTool = qobject_cast<Tools::ProposeCompletionTool *>(
toolsManager->tool(Tools::ProposeCompletionTool::toolId()));
if (!proposeTool) {
proposeTool = new Tools::ProposeCompletionTool();
toolsManager->addTool(proposeTool);
}
connect(
proposeTool,
&Tools::ProposeCompletionTool::completionProposed,
this,
&AgenticCompletionEngine::handleCompletionProposed,
Qt::UniqueConnection);
}
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
auto updatedContext = reader.prepareContext(context.line, context.column, m_completeSettings);
QString systemPrompt = m_completeSettings.systemPromptForNonFimModels();
systemPrompt.append(
QString("\n\nYou are completing code at %1, zero-based line %2, column %3. "
"You may call the read-only project tools to gather context. When you know the "
"completion, call the propose_completion tool exactly once with the completion "
"text for that position; the text is shown to the user as an inline suggestion. "
"Do not answer in prose and do not edit any files.")
.arg(documentInfo.filePath)
.arg(context.line)
.arg(context.column));
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
updatedContext.systemPrompt = systemPrompt;
QString userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
QJsonObject payload{{"model", m_generalSettings.ccModel()}, {"stream", true}};
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion,
true,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&AgenticCompletionEngine::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&AgenticCompletionEngine::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto llmRequestId = provider->sendRequest(
QUrl(m_generalSettings.ccUrl()), payload, promptTemplate->endpoint());
m_activeRequests.insert(llmRequestId, {requestId, documentInfo.filePath, provider});
m_performanceLogger.startTimeMeasurement(llmRequestId);
}
void AgenticCompletionEngine::cancel(quint64 requestId)
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().requestId != requestId)
continue;
const auto llmRequestId = it.key();
auto *provider = it.value().provider;
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(llmRequestId);
if (provider)
provider->cancelRequest(llmRequestId);
return;
}
}
void AgenticCompletionEngine::handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text)
{
Q_UNUSED(line)
Q_UNUSED(column)
auto match = m_activeRequests.end();
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().filePath == filePath) {
match = it;
break;
}
}
if (match == m_activeRequests.end() && m_activeRequests.size() == 1)
match = m_activeRequests.begin();
if (match == m_activeRequests.end()) {
LOG_MESSAGE(
QString("Ignoring completion proposal for %1: no completion request in flight")
.arg(filePath));
return;
}
const quint64 externalId = match.value().requestId;
const auto llmRequestId = match.key();
m_activeRequests.erase(match);
m_performanceLogger.endTimeMeasurement(llmRequestId);
emit completionReady(externalId, text);
}
void AgenticCompletionEngine::handleFullResponse(
const ::LLMQore::RequestID &requestId, const QString &fullText)
{
Q_UNUSED(fullText)
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
const QString error
= QStringLiteral("The model finished without proposing a completion");
LOG_MESSAGE(QString("Agentic completion request %1: %2").arg(requestId, error));
emit completionFailed(active.requestId, error);
}
void AgenticCompletionEngine::handleRequestFailed(
const ::LLMQore::RequestID &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
LOG_MESSAGE(QString("Agentic completion request %1 failed: %2").arg(requestId, error));
emit completionFailed(active.requestId, error);
}
} // namespace QodeAssist

View File

@@ -0,0 +1,61 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QHash>
#include "completion/CompletionEngine.hpp"
#include "context/IDocumentReader.hpp"
#include "logger/IRequestPerformanceLogger.hpp"
#include "providers/IProviderRegistry.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "templates/IPromptProvider.hpp"
#include "tools/ProposeCompletionTool.hpp"
namespace QodeAssist {
class AgenticCompletionEngine : public CompletionEngine
{
Q_OBJECT
public:
AgenticCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger,
QObject *parent = nullptr);
~AgenticCompletionEngine() override;
void request(quint64 requestId, const CompletionContext &context) override;
void cancel(quint64 requestId) override;
private slots:
void handleCompletionProposed(
const QString &filePath, int line, int column, const QString &text);
void handleFullResponse(const ::LLMQore::RequestID &requestId, const QString &fullText);
void handleRequestFailed(const ::LLMQore::RequestID &requestId, const QString &error);
private:
struct ActiveRequest
{
quint64 requestId = 0;
QString filePath;
Providers::Provider *provider = nullptr;
};
const Settings::GeneralSettings &m_generalSettings;
const Settings::CodeCompletionSettings &m_completeSettings;
Providers::IProviderRegistry &m_providerRegistry;
Templates::IPromptProvider *m_promptProvider = nullptr;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QHash<::LLMQore::RequestID, ActiveRequest> m_activeRequests;
};
} // namespace QodeAssist

View File

@@ -24,30 +24,26 @@
* Additional attribution terms under GPLv3 §7(b) apply see LICENSE
*/
#include "completion/QodeAssistClient.hpp"
#include "completion/CodeCompletionController.hpp"
#include <QApplication>
#include <QInputDialog>
#include <QKeyEvent>
#include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/projectmanager.h>
#include "completion/LLMClientInterface.hpp"
#include "completion/LLMSuggestion.hpp"
#include "refactor/RefactorContextHelper.hpp"
#include "refactor/RefactorSuggestion.hpp"
#include "refactor/RefactorSuggestionHoverHandler.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include "refactor/RefactorContextHelper.hpp"
#include "settings/SettingsConstants.hpp"
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol;
using namespace TextEditor;
using namespace Utils;
using namespace ProjectExplorer;
@@ -56,11 +52,6 @@ using namespace Core;
namespace QodeAssist {
namespace {
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
{
return Utils::Text::Position{pos.line, pos.column};
}
bool isIdentifierChar(QChar c)
{
return c.isLetterOrNumber() || c == QLatin1Char('_');
@@ -133,23 +124,32 @@ bool isAfterEagerTrigger(const QTextCursor &cursor)
|| c == QLatin1Char(':') || c == QLatin1Char('>');
}
bool isManualMode()
bool isManualTriggerMode()
{
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
return Settings::codeCompletionSettings().triggerMode.stringValue() == "Manual";
}
bool isAgenticMode()
{
const QString mode = Settings::codeCompletionSettings().completionMode.stringValue();
return mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_LOCAL)
|| mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_ACP);
}
} // anonymous namespace
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
CodeCompletionController::CodeCompletionController(
CompletionEngine *fimEngine,
CompletionEngine *agenticEngine,
CompletionEngine *acpEngine,
Context::ContextManager &contextManager,
QObject *parent)
: QObject(parent)
, m_fimEngine(fimEngine)
, m_agenticEngine(agenticEngine)
, m_acpEngine(acpEngine)
, m_contextManager(contextManager)
, m_recentCharCount(0)
{
setName("QodeAssist");
LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter);
start();
setupConnections();
m_typingTimer.start();
@@ -158,30 +158,33 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}
QodeAssistClient::~QodeAssistClient()
CodeCompletionController::~CodeCompletionController()
{
cleanupConnections();
delete m_refactorHoverHandler;
delete m_refactorWidgetHandler;
}
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
void CodeCompletionController::openDocument(TextEditor::TextDocument *document)
{
if (m_documentConnections.contains(document))
return;
auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project))
return;
Client::openDocument(document);
auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
for (auto *editor : editors) {
if (auto *widget = editor->editorWidget()) {
widget->addHoverHandler(m_refactorHoverHandler);
widget->installEventFilter(this);
if (!m_hoverHandlerWidgets.contains(widget))
m_hoverHandlerWidgets.append(widget);
}
}
connect(
auto connection = connect(
document,
&TextDocument::contentsChangedWithPosition,
this,
@@ -189,7 +192,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (!Settings::codeCompletionSettings().autoCompletion())
return;
if (isManualMode())
if (isManualTriggerMode() || isAgenticMode())
return;
auto project = ProjectManager::projectForFile(document->filePath());
@@ -243,42 +246,47 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
handleAutoRequestTrigger(widget);
});
m_documentConnections.insert(document, connection);
}
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
void CodeCompletionController::closeDocument(TextEditor::TextDocument *document)
{
return isEnabled(project);
auto it = m_documentConnections.find(document);
if (it == m_documentConnections.end())
return;
disconnect(it.value());
m_documentConnections.erase(it);
}
void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
void CodeCompletionController::requestCompletions(TextEditor::TextEditorWidget *editor)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
if (m_contextManager.ignoreManager()->shouldIgnore(
editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
auto *engine = activeEngine();
if (!engine)
return;
MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return;
cancelRunningRequest(editor);
const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
editor->abortAssist();
const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
Position(cursor.mainCursor())}};
if (Settings::codeCompletionSettings().showProgressWidget()) {
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
if (editor) {
@@ -287,16 +295,22 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
});
m_progressHandler.showProgress(editor);
}
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return);
handleCompletions(response, editor);
});
m_runningRequests[editor] = request;
sendMessage(request);
const int position = cursor.mainCursor().position();
const quint64 requestId = ++m_lastRequestId;
m_runningRequests[editor] = {requestId, position, engine};
m_requestEditors[requestId] = editor;
const QTextBlock block = editor->document()->findBlock(position);
CompletionContext context;
context.filePath = editor->textDocument()->filePath().toUrlishString();
context.line = block.blockNumber();
context.column = position - block.position();
engine->request(requestId, context);
}
void QodeAssistClient::requestQuickRefactor(
void CodeCompletionController::requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
@@ -304,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
if (m_contextManager.ignoreManager()->shouldIgnore(
editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
@@ -318,7 +331,7 @@ void QodeAssistClient::requestQuickRefactor(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,
this,
&QodeAssistClient::handleRefactoringResult);
&CodeCompletionController::handleRefactoringResult);
}
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
@@ -331,7 +344,7 @@ void QodeAssistClient::requestQuickRefactor(
m_refactorHandler->sendRefactorRequest(editor, instructions);
}
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
void CodeCompletionController::scheduleRequest(TextEditor::TextEditorWidget *editor)
{
if (m_runningRequests.contains(editor)) {
if (Settings::codeCompletionSettings().cancelOnInput())
@@ -362,25 +375,74 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
}
void QodeAssistClient::handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
void CodeCompletionController::handleCompletionReady(quint64 requestId, const QString &text)
{
auto editorIt = m_requestEditors.find(requestId);
if (editorIt == m_requestEditors.end())
return;
const QPointer<TextEditorWidget> editor = editorIt.value();
m_requestEditors.erase(editorIt);
if (!editor) {
dropRunningRequestById(requestId);
return;
}
auto runningIt = m_runningRequests.find(editor);
if (runningIt == m_runningRequests.end() || runningIt.value().id != requestId)
return;
const int requestPosition = runningIt.value().position;
m_runningRequests.erase(runningIt);
m_progressHandler.hideProgress();
const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
editor->abortAssist();
if (response.error()) {
log(*response.error());
m_errorHandler
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
applyCompletion(editor, text, requestPosition);
}
void CodeCompletionController::handleCompletionFailed(quint64 requestId, const QString &error)
{
auto editorIt = m_requestEditors.find(requestId);
if (editorIt == m_requestEditors.end())
return;
const QPointer<TextEditorWidget> editor = editorIt.value();
m_requestEditors.erase(editorIt);
LOG_MESSAGE(QString("Code completion failed: %1").arg(error));
if (!editor) {
dropRunningRequestById(requestId);
return;
}
int requestPosition = -1;
if (const auto requestParams = m_runningRequests.take(editor).params())
requestPosition = requestParams->position().toPositionInDocument(editor->document());
auto runningIt = m_runningRequests.find(editor);
if (runningIt == m_runningRequests.end() || runningIt.value().id != requestId)
return;
m_runningRequests.erase(runningIt);
m_progressHandler.hideProgress();
m_errorHandler.showError(editor, tr("Code completion failed: %1").arg(error));
}
void CodeCompletionController::dropRunningRequestById(quint64 requestId)
{
for (auto it = m_runningRequests.begin(); it != m_runningRequests.end(); ++it) {
if (it.value().id == requestId) {
m_runningRequests.erase(it);
return;
}
}
}
void CodeCompletionController::applyCompletion(
TextEditor::TextEditorWidget *editor, const QString &text, int requestPosition)
{
const MultiTextCursor cursors = editor->multiTextCursor();
if (cursors.hasMultipleCursors() || cursors.hasSelection())
return;
@@ -401,80 +463,51 @@ void QodeAssistClient::handleCompletions(
}
}
if (const std::optional<GetCompletionResponse> result = response.result()) {
auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty();
};
QList<Completion> completions
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
QString completionText = text;
const int end = int(completionText.size()) - 1;
int delta = 0;
while (delta <= end && completionText[end - delta].isSpace())
++delta;
if (delta > 0)
completionText.chop(delta);
QList<Completion> matchedCompletions;
matchedCompletions.reserve(completions.size());
for (Completion &completion : completions) {
const LanguageServerProtocol::Range range = completion.range();
if (range.start().line() != range.end().line())
continue;
QString completionText = completion.text();
const int end = int(completionText.size()) - 1;
int delta = 0;
while (delta <= end && completionText[end - delta].isSpace())
++delta;
if (delta > 0)
completionText.chop(delta);
if (!typedSinceRequest.isEmpty()) {
if (!completionText.startsWith(typedSinceRequest))
continue;
completionText = completionText.mid(typedSinceRequest.size());
if (completionText.isEmpty())
continue;
}
completion.setText(completionText);
matchedCompletions.append(completion);
}
if (matchedCompletions.isEmpty()) {
LOG_MESSAGE("No valid completions received");
if (!typedSinceRequest.isEmpty()) {
if (!completionText.startsWith(typedSinceRequest)) {
LOG_MESSAGE("Completion no longer matches the typed text");
return;
}
const Text::Position anchor = typedSinceRequest.isEmpty()
? Text::Position{}
: Text::Position::fromPositionInDocument(editor->document(), currentPosition);
const bool useAnchor = !typedSinceRequest.isEmpty();
auto suggestions = Utils::transform(matchedCompletions,
[useAnchor, &anchor](const Completion &c) {
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()};
};
if (useAnchor) {
return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
}
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
completionText = completionText.mid(typedSinceRequest.size());
}
if (completionText.trimmed().isEmpty()) {
LOG_MESSAGE("No valid completions received");
return;
}
const int anchorPosition = typedSinceRequest.isEmpty() ? requestPosition : currentPosition;
const Text::Position anchor
= Text::Position::fromPositionInDocument(editor->document(), anchorPosition);
const TextSuggestion::Data suggestion{Text::Range{anchor, anchor}, anchor, completionText};
editor->insertSuggestion(
std::make_unique<LLMSuggestion>(QList<TextSuggestion::Data>{suggestion}, editor->document()));
}
void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor)
void CodeCompletionController::cancelRunningRequest(TextEditor::TextEditorWidget *editor)
{
const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd())
return;
m_progressHandler.hideProgress();
cancelRequest(it->id());
const quint64 requestId = it->id;
CompletionEngine *engine = it->engine;
m_runningRequests.erase(it);
m_requestEditors.remove(requestId);
m_progressHandler.hideProgress();
if (engine)
engine->cancel(requestId);
}
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
bool CodeCompletionController::isEnabled(ProjectExplorer::Project *project) const
{
if (!project)
return Settings::generalSettings().enableQodeAssist();
@@ -483,8 +516,33 @@ bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
return settings.isEnabled();
}
void QodeAssistClient::setupConnections()
CompletionEngine *CodeCompletionController::activeEngine() const
{
const QString mode = Settings::codeCompletionSettings().completionMode.stringValue();
if (mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_LOCAL))
return m_agenticEngine;
if (mode == QLatin1StringView(Constants::CC_MODE_AGENTIC_ACP))
return m_acpEngine;
return m_fimEngine;
}
void CodeCompletionController::setupConnections()
{
for (CompletionEngine *engine : {m_fimEngine, m_agenticEngine, m_acpEngine}) {
if (!engine)
continue;
connect(
engine,
&CompletionEngine::completionReady,
this,
&CodeCompletionController::handleCompletionReady);
connect(
engine,
&CompletionEngine::completionFailed,
this,
&CodeCompletionController::handleCompletionFailed);
}
auto openDoc = [this](IDocument *document) {
if (auto *textDocument = qobject_cast<TextDocument *>(document))
openDocument(textDocument);
@@ -502,16 +560,26 @@ void QodeAssistClient::setupConnections()
openDoc(doc);
}
void QodeAssistClient::cleanupConnections()
void CodeCompletionController::cleanupConnections()
{
disconnect(m_documentOpenedConnection);
disconnect(m_documentClosedConnection);
for (auto it = m_documentConnections.begin(); it != m_documentConnections.end(); ++it)
disconnect(it.value());
m_documentConnections.clear();
for (const auto &widget : std::as_const(m_hoverHandlerWidgets)) {
if (widget)
widget->removeHoverHandler(m_refactorHoverHandler);
}
m_hoverHandlerWidgets.clear();
qDeleteAll(m_scheduledRequests);
m_scheduledRequests.clear();
}
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
void CodeCompletionController::handleRefactoringResult(const RefactorResult &result)
{
m_progressHandler.hideProgress();
@@ -534,7 +602,7 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
}
int displayMode = Settings::quickRefactorSettings().displayMode();
if (displayMode == 0) {
displayRefactoringWidget(result);
} else {
@@ -542,12 +610,12 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
}
}
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
void CodeCompletionController::displayRefactoringSuggestion(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
Utils::Text::Range range{result.insertRange.begin, result.insertRange.end};
Utils::Text::Position pos = result.insertRange.begin;
int startPos = range.begin.toPositionInDocument(editorWidget->document());
int endPos = range.end.toPositionInDocument(editorWidget->document());
@@ -577,9 +645,7 @@ void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result
}
TextEditor::TextSuggestion::Data suggestionData{
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
pos,
result.newText};
Utils::Text::Range{result.insertRange.begin, result.insertRange.end}, pos, result.newText};
editorWidget->insertSuggestion(
std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));
@@ -599,20 +665,20 @@ void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
}
void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
void CodeCompletionController::displayRefactoringWidget(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
Utils::Text::Range range{result.insertRange.begin, result.insertRange.end};
RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
QString displayOriginal;
QString displayRefactored;
QString textToApply = result.newText;
if (ctx.isInsertion) {
bool isMultiline = result.newText.contains('\n');
if (isMultiline) {
displayOriginal = ctx.textBeforeCursor;
displayRefactored = ctx.textBeforeCursor + result.newText;
@@ -620,7 +686,7 @@ void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
}
if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
textToApply = result.newText;
}
@@ -638,31 +704,30 @@ void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
m_refactorWidgetHandler->showRefactorWidget(
editorWidget, displayOriginal, displayRefactored, range,
ctx.contextBefore, ctx.contextAfter);
m_refactorWidgetHandler->setTextToApply(textToApply);
}
void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &text)
void CodeCompletionController::applyRefactoringEdit(
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text)
{
const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
const QTextCursor endCursor = range.end.toTextCursor(editor->document());
const int startPos = startCursor.position();
const int endPos = endCursor.position();
QTextCursor editCursor(editor->document());
editCursor.beginEditBlock();
if (startPos == endPos) {
bool isMultiline = text.contains('\n');
editCursor.setPosition(startPos);
if (isMultiline) {
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
}
editCursor.insertText(text);
} else {
editCursor.setPosition(startPos);
@@ -674,7 +739,7 @@ void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor
editCursor.endEditBlock();
}
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
void CodeCompletionController::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
{
const QTextCursor cursor = widget->textCursor();
const auto &settings = Settings::codeCompletionSettings();
@@ -690,11 +755,11 @@ void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *wi
scheduleRequest(widget);
}
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
bool CodeCompletionController::eventFilter(QObject *watched, QEvent *event)
{
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
if (!editor)
return LanguageClient::Client::eventFilter(watched, event);
return QObject::eventFilter(watched, event);
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
@@ -719,7 +784,7 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
}
}
return LanguageClient::Client::eventFilter(watched, event);
return QObject::eventFilter(watched, event);
}
} // namespace QodeAssist

View File

@@ -5,31 +5,36 @@
#pragma once
#include <QElapsedTimer>
#include <QHash>
#include <QObject>
#include <QPointer>
#include "completion/LLMClientInterface.hpp"
#include "completion/LSPCompletion.hpp"
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include "completion/CompletionEngine.hpp"
#include "context/ContextManager.hpp"
#include "refactor/QuickRefactorHandler.hpp"
#include "refactor/RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h>
#include "templates/IPromptProvider.hpp"
#include "providers/IProviderRegistry.hpp"
namespace QodeAssist {
class QodeAssistClient : public LanguageClient::Client
class CodeCompletionController : public QObject
{
Q_OBJECT
public:
explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override;
void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override;
public:
CodeCompletionController(
CompletionEngine *fimEngine,
CompletionEngine *agenticEngine,
CompletionEngine *acpEngine,
Context::ContextManager &contextManager,
QObject *parent = nullptr);
~CodeCompletionController() override;
void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
@@ -39,23 +44,45 @@ protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
struct RunningRequest
{
quint64 id = 0;
int position = -1;
CompletionEngine *engine = nullptr;
};
void openDocument(TextEditor::TextDocument *document);
void closeDocument(TextEditor::TextDocument *document);
void scheduleRequest(TextEditor::TextEditorWidget *editor);
void handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
void handleCompletionReady(quint64 requestId, const QString &text);
void handleCompletionFailed(quint64 requestId, const QString &error);
void applyCompletion(
TextEditor::TextEditorWidget *editor, const QString &text, int requestPosition);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
void dropRunningRequestById(quint64 requestId);
bool isEnabled(ProjectExplorer::Project *project) const;
CompletionEngine *activeEngine() const;
void setupConnections();
void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
void displayRefactoringSuggestion(const RefactorResult &result);
void displayRefactoringWidget(const RefactorResult &result);
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
void applyRefactoringEdit(
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
CompletionEngine *m_fimEngine = nullptr;
CompletionEngine *m_agenticEngine = nullptr;
CompletionEngine *m_acpEngine = nullptr;
Context::ContextManager &m_contextManager;
quint64 m_lastRequestId = 0;
QHash<TextEditor::TextEditorWidget *, RunningRequest> m_runningRequests;
QHash<quint64, QPointer<TextEditor::TextEditorWidget>> m_requestEditors;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
QHash<TextEditor::TextDocument *, QMetaObject::Connection> m_documentConnections;
QList<QPointer<TextEditor::TextEditorWidget>> m_hoverHandlerWidgets;
QMetaObject::Connection m_documentOpenedConnection;
QMetaObject::Connection m_documentClosedConnection;
@@ -63,11 +90,9 @@ private:
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,34 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QString>
namespace QodeAssist {
struct CompletionContext
{
QString filePath;
int line = 0;
int column = 0;
};
class CompletionEngine : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
virtual void request(quint64 requestId, const CompletionContext &context) = 0;
virtual void cancel(quint64 requestId) = 0;
signals:
void completionReady(quint64 requestId, const QString &text);
void completionFailed(quint64 requestId, const QString &error);
};
} // namespace QodeAssist

View File

@@ -0,0 +1,292 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/FimCompletionEngine.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QJsonArray>
#include <QUrl>
#include "completion/CodeHandler.hpp"
#include "context/DocumentContextReader.hpp"
#include "llmcore/ContextData.hpp"
#include "logger/Logger.hpp"
#include "settings/SettingsConstants.hpp"
namespace QodeAssist {
namespace {
constexpr int kSemanticContextTokenBudget = 1000;
}
FimCompletionEngine::FimCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
Context::ContextManager &contextManager,
IRequestPerformanceLogger &performanceLogger,
Context::ICompletionEnricher *enricher,
QObject *parent)
: CompletionEngine(parent)
, m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_documentReader(documentReader)
, m_contextManager(contextManager)
, m_performanceLogger(performanceLogger)
, m_enricher(enricher)
{
}
FimCompletionEngine::~FimCompletionEngine()
{
const auto requests = m_activeRequests;
m_activeRequests.clear();
for (auto it = requests.constBegin(); it != requests.constEnd(); ++it) {
if (it.value().provider)
it.value().provider->cancelRequest(it.key());
}
}
void FimCompletionEngine::request(quint64 requestId, const CompletionContext &context)
{
auto documentInfo = m_documentReader.readDocument(context.filePath);
if (!documentInfo.document) {
const QString error = QString("Document is not available: %1").arg(context.filePath);
LOG_MESSAGE("Error: " + error);
emit completionFailed(requestId, error);
return;
}
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
auto updatedContext = reader.prepareContext(context.line, context.column, m_completeSettings);
bool isPreset1Active = m_contextManager.isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
const QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
const QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
emit completionFailed(requestId, error);
return;
}
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == Templates::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == Providers::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager.openedFiles({context.filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager.openedFilesContext({context.filePath}));
}
}
if (m_enricher && promptTemplate->type() == Templates::TemplateType::Chat
&& m_completeSettings.completionMode.stringValue()
== QLatin1StringView(Constants::CC_MODE_FIM_WITH_CONTEXT)) {
const QString enrichment = m_enricher->enrichmentFor(
documentInfo,
updatedContext.prefix.value_or(""),
context.line,
context.column,
kSemanticContextTokenBudget);
if (!enrichment.isEmpty())
systemPrompt.append(enrichment);
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == Templates::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&FimCompletionEngine::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&FimCompletionEngine::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&FimCompletionEngine::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto llmRequestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests.insert(llmRequestId, {requestId, context.filePath, provider});
m_performanceLogger.startTimeMeasurement(llmRequestId);
}
void FimCompletionEngine::cancel(quint64 requestId)
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().requestId != requestId)
continue;
const auto llmRequestId = it.key();
auto *provider = it.value().provider;
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(llmRequestId);
if (provider)
provider->cancelRequest(llmRequestId);
return;
}
}
void FimCompletionEngine::handleFullResponse(
const ::LLMQore::RequestID &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
emit completionReady(active.requestId, postProcess(fullText, active.filePath));
}
void FimCompletionEngine::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void FimCompletionEngine::handleRequestFailed(
const ::LLMQore::RequestID &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const ActiveRequest active = it.value();
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
emit completionFailed(active.requestId, error);
}
QString FimCompletionEngine::resolveEndpoint(
Templates::PromptTemplate *promptTemplate, bool isPreset1Active) const
{
const QString custom = isPreset1Active ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
}
QString FimCompletionEngine::postProcess(const QString &completion, const QString &filePath) const
{
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
const QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
QString processedCompletion;
if (outputHandler == "Raw text") {
processedCompletion = completion;
} else if (outputHandler == "Force processing") {
processedCompletion = CodeHandler::processText(completion, filePath);
} else {
processedCompletion = CodeHandler::hasCodeBlocks(completion)
? CodeHandler::processText(completion, filePath)
: completion;
}
if (processedCompletion.endsWith('\n')) {
QString withoutTrailing = processedCompletion.chopped(1);
if (!withoutTrailing.contains('\n')) {
LOG_MESSAGE(QString("Removed trailing newline from single-line completion"));
processedCompletion = withoutTrailing;
}
}
LOG_MESSAGE(QString("Completion after filter: \n%1").arg(processedCompletion));
return processedCompletion;
}
} // namespace QodeAssist

View File

@@ -0,0 +1,69 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QHash>
#include "completion/CompletionEngine.hpp"
#include "context/CompletionContextEnricher.hpp"
#include "context/ContextManager.hpp"
#include "context/IDocumentReader.hpp"
#include "logger/IRequestPerformanceLogger.hpp"
#include "providers/IProviderRegistry.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "templates/IPromptProvider.hpp"
namespace QodeAssist {
class FimCompletionEngine : public CompletionEngine
{
Q_OBJECT
public:
FimCompletionEngine(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
Context::ContextManager &contextManager,
IRequestPerformanceLogger &performanceLogger,
Context::ICompletionEnricher *enricher = nullptr,
QObject *parent = nullptr);
~FimCompletionEngine() override;
void request(quint64 requestId, const CompletionContext &context) override;
void cancel(quint64 requestId) override;
private slots:
void handleFullResponse(const ::LLMQore::RequestID &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const ::LLMQore::RequestID &requestId, const QString &error);
private:
struct ActiveRequest
{
quint64 requestId = 0;
QString filePath;
Providers::Provider *provider = nullptr;
};
QString resolveEndpoint(Templates::PromptTemplate *promptTemplate, bool isPreset1Active) const;
QString postProcess(const QString &completion, const QString &filePath) const;
const Settings::GeneralSettings &m_generalSettings;
const Settings::CodeCompletionSettings &m_completeSettings;
Providers::IProviderRegistry &m_providerRegistry;
Templates::IPromptProvider *m_promptProvider = nullptr;
Context::IDocumentReader &m_documentReader;
Context::ContextManager &m_contextManager;
IRequestPerformanceLogger &m_performanceLogger;
Context::ICompletionEnricher *m_enricher = nullptr;
QHash<::LLMQore::RequestID, ActiveRequest> m_activeRequests;
};
} // namespace QodeAssist

View File

@@ -1,455 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "completion/LLMClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "completion/CodeHandler.hpp"
#include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
namespace QodeAssist {
LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this))
{
}
LLMClientInterface::~LLMClientInterface()
{
handleCancelRequest();
}
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{
return "QodeAssist";
}
void LLMClientInterface::startImpl()
{
emit started();
}
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
sendCompletionToClient(fullText, ctx.originalRequest, true);
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::sendData(const QByteArray &data)
{
QJsonDocument doc = QJsonDocument::fromJson(data);
if (!doc.isObject())
return;
QJsonObject request = doc.object();
QString method = request["method"].toString();
if (method == "initialize") {
handleInitialize(request);
} else if (method == "initialized") {
// TODO make initilizied handler
} else if (method == "shutdown") {
handleShutdown(request);
} else if (method == "textDocument/didOpen") {
handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") {
handleCompletion(request);
} else if (method == "$/cancelRequest") {
handleCancelRequest();
} else if (method == "exit") {
// TODO make exit handler
} else {
LOG_MESSAGE(QString("Unknown method: %1").arg(method));
}
}
void LLMClientInterface::handleCancelRequest()
{
QSet<Providers::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
void LLMClientInterface::handleInitialize(const QJsonObject &request)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response["id"] = request["id"];
QJsonObject result;
QJsonObject capabilities;
capabilities["textDocumentSync"] = 1;
capabilities["completionProvider"] = QJsonObject{{"resolveProvider", false}};
capabilities["hoverProvider"] = true;
result["capabilities"] = capabilities;
QJsonObject serverInfo;
serverInfo["name"] = "QodeAssist LSP Server";
serverInfo["version"] = "0.1";
result["serverInfo"] = serverInfo;
response["result"] = result;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::handleShutdown(const QJsonObject &request)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response["id"] = request["id"];
response["result"] = QJsonValue();
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::handleTextDocumentDidOpen(const QJsonObject &request) {}
void LLMClientInterface::handleInitialized(const QJsonObject &request)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response["method"] = "initialized";
response["params"] = QJsonObject();
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::handleExit(const QJsonObject &request)
{
emit finished();
}
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = errorMessage;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
// End performance measurement if it was started
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::handleCompletion(const QJsonObject &request)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
if (!documentInfo.document) {
QString error = QString("Document is not available: %1").arg(filePath);
LOG_MESSAGE("Error: " + error);
sendErrorResponse(request, error);
return;
}
auto updatedContext = prepareContext(request, documentInfo);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
}
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
}
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == Templates::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == Providers::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == Templates::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&LLMClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_performanceLogger.startTimeMeasurement(requestId);
}
LLMCore::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].toObject();
int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt();
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
}
QString LLMClientInterface::resolveEndpoint(
Templates::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
{
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
}
Context::ContextManager *LLMClientInterface::contextManager() const
{
return m_contextManager;
}
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject result;
QJsonArray completions;
QJsonObject completionItem;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
QString processedCompletion;
if (outputHandler == "Raw text") {
processedCompletion = completion;
} else if (outputHandler == "Force processing") {
processedCompletion = CodeHandler::processText(completion,
Context::extractFilePathFromRequest(request));
} else { // "Auto"
processedCompletion = CodeHandler::hasCodeBlocks(completion)
? CodeHandler::processText(completion,
Context::extractFilePathFromRequest(
request))
: completion;
}
if (processedCompletion.endsWith('\n')) {
QString withoutTrailing = processedCompletion.chopped(1);
if (!withoutTrailing.contains('\n')) {
LOG_MESSAGE(QString("Removed trailing newline from single-line completion"));
processedCompletion = withoutTrailing;
}
}
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range;
range["start"] = position;
range["end"] = position;
completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem);
result["completions"] = completions;
result[LanguageServerProtocol::isIncompleteKey] = !isComplete;
response[LanguageServerProtocol::resultKey] = result;
LOG_MESSAGE(
QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE(
QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
} // namespace QodeAssist

View File

@@ -1,93 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <LLMQore/BaseClient.hpp>
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include "llmcore/ContextData.hpp"
#include "templates/IPromptProvider.hpp"
#include "providers/IProviderRegistry.hpp"
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
class QNetworkReply;
class QNetworkAccessManager;
namespace QodeAssist {
class LLMClientInterface : public LanguageClient::BaseClientInterface
{
Q_OBJECT
public:
LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
Providers::IProviderRegistry &providerRegistry,
Templates::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override;
Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete);
void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected:
void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private:
void handleInitialize(const QJsonObject &request);
void handleShutdown(const QJsonObject &request);
void handleTextDocumentDidOpen(const QJsonObject &request);
void handleInitialized(const QJsonObject &request);
void handleExit(const QJsonObject &request);
void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
struct RequestContext
{
QJsonObject originalRequest;
Providers::Provider *provider;
};
LLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint(
Templates::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
Templates::IPromptProvider *m_promptProvider = nullptr;
Providers::IProviderRegistry &m_providerRegistry;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager;
QHash<QString, RequestContext> m_activeRequests;
};
} // namespace QodeAssist

View File

@@ -1,147 +0,0 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/
#pragma once
#include <languageserverprotocol/jsonkeys.h>
#include <languageserverprotocol/jsonrpcmessages.h>
#include <languageserverprotocol/lsptypes.h>
namespace QodeAssist {
class Completion : public LanguageServerProtocol::JsonObject
{
static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"};
static constexpr LanguageServerProtocol::Key uuidKey{"uuid"};
public:
using JsonObject::JsonObject;
QString displayText() const { return typedValue<QString>(displayTextKey); }
LanguageServerProtocol::Position position() const
{
return typedValue<LanguageServerProtocol::Position>(LanguageServerProtocol::positionKey);
}
void setRange(const LanguageServerProtocol::Range &range)
{
insert(LanguageServerProtocol::rangeKey, range);
}
LanguageServerProtocol::Range range() const
{
return typedValue<LanguageServerProtocol::Range>(LanguageServerProtocol::rangeKey);
}
QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); }
void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); }
QString uuid() const { return typedValue<QString>(uuidKey); }
bool isValid() const override
{
return contains(LanguageServerProtocol::textKey)
&& contains(LanguageServerProtocol::rangeKey)
&& contains(LanguageServerProtocol::positionKey);
}
};
class GetCompletionParams : public LanguageServerProtocol::JsonObject
{
public:
static constexpr LanguageServerProtocol::Key docKey{"doc"};
GetCompletionParams(
const LanguageServerProtocol::TextDocumentIdentifier &document,
int version,
const LanguageServerProtocol::Position &position)
{
setTextDocument(document);
setVersion(version);
setPosition(position);
}
using JsonObject::JsonObject;
// The text document.
LanguageServerProtocol::TextDocumentIdentifier textDocument() const
{
return typedValue<LanguageServerProtocol::TextDocumentIdentifier>(docKey);
}
void setTextDocument(const LanguageServerProtocol::TextDocumentIdentifier &id)
{
insert(docKey, id);
}
// The position inside the text document.
LanguageServerProtocol::Position position() const
{
return LanguageServerProtocol::fromJsonValue<LanguageServerProtocol::Position>(
value(docKey).toObject().value(LanguageServerProtocol::positionKey));
}
void setPosition(const LanguageServerProtocol::Position &position)
{
QJsonObject result = value(docKey).toObject();
result[LanguageServerProtocol::positionKey] = (QJsonObject) position;
insert(docKey, result);
}
// The version
int version() const { return typedValue<int>(LanguageServerProtocol::versionKey); }
void setVersion(int version)
{
QJsonObject result = value(docKey).toObject();
result[LanguageServerProtocol::versionKey] = version;
insert(docKey, result);
}
bool isValid() const override
{
return contains(docKey)
&& value(docKey).toObject().contains(LanguageServerProtocol::positionKey)
&& value(docKey).toObject().contains(LanguageServerProtocol::versionKey);
}
};
class GetCompletionResponse : public LanguageServerProtocol::JsonObject
{
static constexpr LanguageServerProtocol::Key completionKey{"completions"};
public:
using JsonObject::JsonObject;
LanguageServerProtocol::LanguageClientArray<Completion> completions() const
{
return clientArray<Completion>(completionKey);
}
};
class GetCompletionRequest
: public LanguageServerProtocol::Request<GetCompletionResponse, std::nullptr_t, GetCompletionParams>
{
public:
explicit GetCompletionRequest(const GetCompletionParams &params = {})
: Request(methodName, params)
{}
using Request::Request;
constexpr static const LanguageServerProtocol::Key methodName{"getCompletionsCycling"};
};
} // namespace QodeAssist