mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-09 06:29:12 -04:00
fix: Remove isUserSource from tests
This commit is contained in:
@@ -357,6 +357,15 @@ SessionManager *ChatRootView::sessionManager() const
|
|||||||
return m_sessionManager;
|
return m_sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QodeAssist::Session *ChatRootView::ownerSession()
|
||||||
|
{
|
||||||
|
if (!m_clientInterface)
|
||||||
|
return nullptr;
|
||||||
|
m_clientInterface->setSessionManager(sessionManager());
|
||||||
|
m_clientInterface->ensureSession();
|
||||||
|
return m_clientInterface->session();
|
||||||
|
}
|
||||||
|
|
||||||
void ChatRootView::loadAvailableChatAgents()
|
void ChatRootView::loadAvailableChatAgents()
|
||||||
{
|
{
|
||||||
m_agentController->setAgentFactory(agentFactory());
|
m_agentController->setAgentFactory(agentFactory());
|
||||||
@@ -535,13 +544,12 @@ void ChatRootView::clearMessages()
|
|||||||
|
|
||||||
void ChatRootView::saveHistory(const QString &filePath)
|
void ChatRootView::saveHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (filePath != m_recentFilePath) {
|
if (auto registry = sessionFileRegistry();
|
||||||
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
|
registry && registry->isLockedByOther(filePath, ownerSession())) {
|
||||||
m_lastErrorMessage
|
m_lastErrorMessage = tr(
|
||||||
= tr("This chat file is already in use by another QodeAssist chat session.");
|
"This chat file is already in use by another QodeAssist chat session.");
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = m_historyStore->save(filePath);
|
auto result = m_historyStore->save(filePath);
|
||||||
@@ -554,13 +562,11 @@ void ChatRootView::saveHistory(const QString &filePath)
|
|||||||
|
|
||||||
void ChatRootView::loadHistory(const QString &filePath)
|
void ChatRootView::loadHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (filePath != m_recentFilePath) {
|
if (auto registry = sessionFileRegistry();
|
||||||
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
|
registry && registry->isLockedByOther(filePath, ownerSession())) {
|
||||||
m_lastErrorMessage
|
m_lastErrorMessage = tr("This chat is already open in another QodeAssist chat session.");
|
||||||
= tr("This chat is already open in another QodeAssist chat session.");
|
emit lastErrorMessageChanged();
|
||||||
emit lastErrorMessageChanged();
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = m_historyStore->load(filePath);
|
auto result = m_historyStore->load(filePath);
|
||||||
@@ -1032,7 +1038,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
registry->release(m_recentFilePath);
|
registry->release(m_recentFilePath);
|
||||||
}
|
}
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty()) {
|
||||||
registry->lock(filePath);
|
registry->lock(filePath, ownerSession());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace QodeAssist {
|
|||||||
class AgentFactory;
|
class AgentFactory;
|
||||||
class SessionManager;
|
class SessionManager;
|
||||||
class ConversationHistory;
|
class ConversationHistory;
|
||||||
|
class Session;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@@ -233,6 +234,7 @@ private:
|
|||||||
Skills::SkillsManager *skillsManager() const;
|
Skills::SkillsManager *skillsManager() const;
|
||||||
AgentFactory *agentFactory() const;
|
AgentFactory *agentFactory() const;
|
||||||
SessionManager *sessionManager() const;
|
SessionManager *sessionManager() const;
|
||||||
|
QodeAssist::Session *ownerSession();
|
||||||
|
|
||||||
QodeAssist::ConversationHistory *m_history;
|
QodeAssist::ConversationHistory *m_history;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
|
|||||||
@@ -97,6 +97,55 @@ void ClientInterface::setActiveAgent(const QString &agentName)
|
|||||||
m_activeAgent = agentName;
|
m_activeAgent = agentName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Session *ClientInterface::session() const
|
||||||
|
{
|
||||||
|
return m_session;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::ensureSession()
|
||||||
|
{
|
||||||
|
if (m_session || !m_sessionManager || !m_history)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_session = m_sessionManager->createDetachedSession(m_history, this);
|
||||||
|
|
||||||
|
connect(m_session, &Session::event, this, [this](const QodeAssist::ResponseEvent &ev) {
|
||||||
|
onSessionEvent(m_session, ev);
|
||||||
|
});
|
||||||
|
connect(m_session, &Session::finished, this, [this](const LLMQore::RequestID &id, const QString &) {
|
||||||
|
onSessionFinished(id);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
m_session,
|
||||||
|
&Session::failed,
|
||||||
|
this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onSessionFailed(id, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ClientInterface::ensureAgentBound()
|
||||||
|
{
|
||||||
|
if (m_session->hasAgent() && m_boundAgent == m_activeAgent)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
QString agentError;
|
||||||
|
if (!m_sessionManager->rebindAgentByName(m_session, m_activeAgent, &agentError)) {
|
||||||
|
m_boundAgent.clear();
|
||||||
|
const QString error = agentError.isEmpty() ? QStringLiteral("No chat agent selected")
|
||||||
|
: agentError;
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_boundAgent = m_activeAgent;
|
||||||
|
|
||||||
|
if (auto *client = m_session->client())
|
||||||
|
m_sessionManager->toolContributors().contribute(client->tools());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ClientInterface::sendMessage(
|
void ClientInterface::sendMessage(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments,
|
const QList<QString> &attachments,
|
||||||
@@ -173,22 +222,21 @@ void ClientInterface::sendMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString sessionError;
|
ensureSession();
|
||||||
Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError);
|
if (!m_session) {
|
||||||
if (!session) {
|
const QString error = QStringLiteral("Failed to create chat session");
|
||||||
const QString error = sessionError.isEmpty()
|
|
||||||
? QStringLiteral("No chat agent selected")
|
|
||||||
: sessionError;
|
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
emit errorOccurred(error);
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *client = session->client();
|
if (!ensureAgentBound())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto *client = m_session->client();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
const QString error = QStringLiteral("Chat agent has no live client");
|
const QString error = QStringLiteral("Chat agent has no live client");
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
m_sessionManager->removeSession(session);
|
|
||||||
emit errorOccurred(error);
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,26 +245,27 @@ void ClientInterface::sendMessage(
|
|||||||
Templates::ContextRenderer::Bindings bindings;
|
Templates::ContextRenderer::Bindings bindings;
|
||||||
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
||||||
bindings.configDir = AgentFactory::userConfigDir();
|
bindings.configDir = AgentFactory::userConfigDir();
|
||||||
session->setContextBindings(bindings);
|
m_session->setContextBindings(bindings);
|
||||||
|
|
||||||
const QString chatFilePath = m_chatFilePath;
|
const QString chatFilePath = m_chatFilePath;
|
||||||
session->setContentLoader([chatFilePath, cache = m_contentCache](const QString &storedPath) {
|
m_session->setContentLoader([chatFilePath, cache = m_contentCache](const QString &storedPath) {
|
||||||
return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath, cache.get());
|
return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath, cache.get());
|
||||||
});
|
});
|
||||||
|
|
||||||
m_sessionManager->toolContributors().contribute(client->tools());
|
|
||||||
client->toolLoop()->setMaxRounds(Settings::toolsSettings().maxToolContinuations());
|
client->toolLoop()->setMaxRounds(Settings::toolsSettings().maxToolContinuations());
|
||||||
client->setTransferTimeout(
|
client->setTransferTimeout(
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
|
|
||||||
const QString chatContext = buildChatContextLayer();
|
const QString chatContext = buildChatContextLayer();
|
||||||
if (!chatContext.isEmpty())
|
if (chatContext.isEmpty())
|
||||||
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
m_session->systemPrompt()->clearLayer(QStringLiteral("chat.context"));
|
||||||
|
else
|
||||||
|
m_session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
||||||
|
|
||||||
if (linkedFiles.isEmpty()) {
|
if (linkedFiles.isEmpty()) {
|
||||||
session->unpinContext(QStringLiteral("chat.files"));
|
m_session->unpinContext(QStringLiteral("chat.files"));
|
||||||
} else {
|
} else {
|
||||||
session->pinContext(
|
m_session->pinContext(
|
||||||
QStringLiteral("chat.files"),
|
QStringLiteral("chat.files"),
|
||||||
[contextManager = QPointer<Context::ContextManager>(m_contextManager),
|
[contextManager = QPointer<Context::ContextManager>(m_contextManager),
|
||||||
linkedFiles]() -> QString {
|
linkedFiles]() -> QString {
|
||||||
@@ -265,29 +314,16 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(session, &Session::event, this, [this, session](const QodeAssist::ResponseEvent &ev) {
|
const LLMQore::RequestID requestId = m_session->send(std::move(blocks));
|
||||||
onSessionEvent(session, ev);
|
|
||||||
});
|
|
||||||
connect(
|
|
||||||
session, &Session::finished, this,
|
|
||||||
[this](const LLMQore::RequestID &id, const QString &) { onSessionFinished(id); });
|
|
||||||
connect(
|
|
||||||
session, &Session::failed, this,
|
|
||||||
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
|
||||||
onSessionFailed(id, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
|
||||||
if (requestId.isEmpty()) {
|
if (requestId.isEmpty()) {
|
||||||
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
|
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
|
||||||
.arg(m_activeAgent, session->lastError().message);
|
.arg(m_activeAgent, m_session->lastError().message);
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
m_sessionManager->removeSession(session);
|
|
||||||
emit errorOccurred(error);
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, m_session};
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
emit requestStarted(requestId);
|
||||||
}
|
}
|
||||||
@@ -337,8 +373,6 @@ void ClientInterface::onSessionFinished(const QString &requestId)
|
|||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Session *session = it.value().session;
|
|
||||||
|
|
||||||
QString applyError;
|
QString applyError;
|
||||||
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
|
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
|
||||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||||
@@ -348,9 +382,6 @@ void ClientInterface::onSessionFinished(const QString &requestId)
|
|||||||
emit messageReceivedCompletely();
|
emit messageReceivedCompletely();
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
if (session && m_sessionManager)
|
|
||||||
m_sessionManager->removeSession(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
|
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||||
@@ -359,15 +390,10 @@ void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist
|
|||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Session *session = it.value().session;
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
|
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
|
||||||
emit errorOccurred(error.message);
|
emit errorOccurred(error.message);
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
if (session && m_sessionManager)
|
|
||||||
m_sessionManager->removeSession(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList ClientInterface::invokedSkillNames(const QString &message) const
|
QStringList ClientInterface::invokedSkillNames(const QString &message) const
|
||||||
@@ -419,20 +445,18 @@ QString ClientInterface::buildChatContextLayer() const
|
|||||||
|
|
||||||
void ClientInterface::clearMessages()
|
void ClientInterface::clearMessages()
|
||||||
{
|
{
|
||||||
|
if (m_session)
|
||||||
|
m_session->cancel();
|
||||||
|
m_activeRequests.clear();
|
||||||
if (m_history)
|
if (m_history)
|
||||||
m_history->clear();
|
m_history->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
{
|
{
|
||||||
const auto requests = m_activeRequests;
|
|
||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
|
if (m_session)
|
||||||
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
m_session->cancel();
|
||||||
Session *session = it.value().session;
|
|
||||||
if (session && m_sessionManager)
|
|
||||||
m_sessionManager->removeSession(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ public:
|
|||||||
void setChatFilePath(const QString &filePath);
|
void setChatFilePath(const QString &filePath);
|
||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
|
|
||||||
|
void ensureSession();
|
||||||
|
Session *session() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
void messageReceivedCompletely();
|
void messageReceivedCompletely();
|
||||||
@@ -63,6 +66,8 @@ signals:
|
|||||||
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool ensureAgentBound();
|
||||||
|
|
||||||
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
|
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
|
||||||
void onSessionFinished(const QString &requestId);
|
void onSessionFinished(const QString &requestId);
|
||||||
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||||
@@ -85,7 +90,9 @@ private:
|
|||||||
QPointer<ConversationHistory> m_history;
|
QPointer<ConversationHistory> m_history;
|
||||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||||
QPointer<SessionManager> m_sessionManager;
|
QPointer<SessionManager> m_sessionManager;
|
||||||
|
QPointer<Session> m_session;
|
||||||
QString m_activeAgent;
|
QString m_activeAgent;
|
||||||
|
QString m_boundAgent;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
std::shared_ptr<StoredContentCache> m_contentCache;
|
std::shared_ptr<StoredContentCache> m_contentCache;
|
||||||
|
|
||||||
|
|||||||
@@ -8,29 +8,43 @@
|
|||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
#include <Session.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
SessionFileRegistry::~SessionFileRegistry() = default;
|
||||||
|
|
||||||
bool SessionFileRegistry::isLocked(const QString &path) const
|
bool SessionFileRegistry::isLocked(const QString &path) const
|
||||||
{
|
{
|
||||||
return !path.isEmpty() && m_lockedPaths.contains(path);
|
return !path.isEmpty() && !m_locks.value(path).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SessionFileRegistry::lock(const QString &path)
|
bool SessionFileRegistry::isLockedByOther(const QString &path, QodeAssist::Session *self) const
|
||||||
{
|
{
|
||||||
if (path.isEmpty() || m_lockedPaths.contains(path)) {
|
if (path.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
}
|
const auto owner = m_locks.value(path);
|
||||||
m_lockedPaths.insert(path);
|
return !owner.isNull() && owner != self;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SessionFileRegistry::lock(const QString &path, QodeAssist::Session *owner)
|
||||||
|
{
|
||||||
|
if (path.isEmpty())
|
||||||
|
return false;
|
||||||
|
const auto existing = m_locks.value(path);
|
||||||
|
if (!existing.isNull() && existing != owner)
|
||||||
|
return false;
|
||||||
|
m_locks.insert(path, owner);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionFileRegistry::release(const QString &path)
|
void SessionFileRegistry::release(const QString &path)
|
||||||
{
|
{
|
||||||
m_lockedPaths.remove(path);
|
m_locks.remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
||||||
@@ -45,7 +59,7 @@ QString SessionFileRegistry::takePendingChatFile()
|
|||||||
|
|
||||||
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
||||||
{
|
{
|
||||||
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
|
if (desiredPath.isEmpty() || !isLocked(desiredPath)) {
|
||||||
return desiredPath;
|
return desiredPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +73,7 @@ QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
|||||||
if (!suffix.isEmpty()) {
|
if (!suffix.isEmpty()) {
|
||||||
candidate += '.' + suffix;
|
candidate += '.' + suffix;
|
||||||
}
|
}
|
||||||
if (!m_lockedPaths.contains(candidate)) {
|
if (!isLocked(candidate)) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,31 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSet>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class Session;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
// Shared registry of chat session (autosave) file paths that are currently held by a live
|
// Shared registry mapping each chat (autosave) file to the live Session that owns it, so a
|
||||||
// chat instance. Lets every chat view — bottom pane, navigation panel, editor split — claim
|
// file is busy only while its owning Session is alive (a destroyed Session frees it — the
|
||||||
// a unique history file so two sessions never autosave into the same path.
|
// QPointer goes null). Keeps two chat views from autosaving into the same path.
|
||||||
class SessionFileRegistry : public QObject
|
class SessionFileRegistry : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SessionFileRegistry(QObject *parent = nullptr);
|
explicit SessionFileRegistry(QObject *parent = nullptr);
|
||||||
|
~SessionFileRegistry() override;
|
||||||
|
|
||||||
bool isLocked(const QString &path) const;
|
bool isLocked(const QString &path) const;
|
||||||
bool lock(const QString &path);
|
bool isLockedByOther(const QString &path, QodeAssist::Session *self) const;
|
||||||
|
bool lock(const QString &path, QodeAssist::Session *owner);
|
||||||
void release(const QString &path);
|
void release(const QString &path);
|
||||||
|
|
||||||
QString uniqueFreePath(const QString &desiredPath) const;
|
QString uniqueFreePath(const QString &desiredPath) const;
|
||||||
@@ -32,7 +39,7 @@ public:
|
|||||||
QString takePendingChatFile();
|
QString takePendingChatFile();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSet<QString> m_lockedPaths;
|
QHash<QString, QPointer<QodeAssist::Session>> m_locks;
|
||||||
QString m_pendingChatFile;
|
QString m_pendingChatFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,33 @@ Session::Session(Agent *agent, QObject *parent)
|
|||||||
|
|
||||||
Session::Session(Agent *agent, ConversationHistory *externalHistory, QObject *parent)
|
Session::Session(Agent *agent, ConversationHistory *externalHistory, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_agent(agent)
|
|
||||||
, m_history(externalHistory ? externalHistory : new ConversationHistory(this))
|
, m_history(externalHistory ? externalHistory : new ConversationHistory(this))
|
||||||
, m_systemPrompt(new SystemPromptBuilder(this))
|
, m_systemPrompt(new SystemPromptBuilder(this))
|
||||||
{
|
{
|
||||||
if (!m_agent) {
|
if (agent)
|
||||||
m_invalidReason = QStringLiteral("Session: agent is null");
|
setAgent(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::setAgent(Agent *agent)
|
||||||
|
{
|
||||||
|
if (agent == m_agent)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (isInFlight())
|
||||||
|
teardownInFlight();
|
||||||
|
|
||||||
|
if (m_router) {
|
||||||
|
delete m_router;
|
||||||
|
m_router = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete m_agent;
|
||||||
|
m_agent = agent;
|
||||||
|
m_invalidReason.clear();
|
||||||
|
|
||||||
|
if (!m_agent)
|
||||||
|
return;
|
||||||
|
|
||||||
m_agent->setParent(this);
|
m_agent->setParent(this);
|
||||||
|
|
||||||
if (!m_agent->isValid()) {
|
if (!m_agent->isValid()) {
|
||||||
@@ -55,8 +74,7 @@ Session::Session(Agent *agent, ConversationHistory *externalHistory, QObject *pa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!m_agent->promptTemplate()) {
|
if (!m_agent->promptTemplate()) {
|
||||||
m_invalidReason
|
m_invalidReason = QStringLiteral("Session: agent has no inline prompt template");
|
||||||
= QStringLiteral("Session: agent has no inline prompt template");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +103,16 @@ bool Session::isInFlight() const noexcept
|
|||||||
return !m_inFlight.isEmpty();
|
return !m_inFlight.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Session::hasAgent() const noexcept
|
||||||
|
{
|
||||||
|
return m_agent != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::canSend() const noexcept
|
||||||
|
{
|
||||||
|
return isValid() && m_agent != nullptr && client() != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const ErrorInfo &Session::lastError() const noexcept
|
const ErrorInfo &Session::lastError() const noexcept
|
||||||
{
|
{
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
@@ -128,8 +156,12 @@ void Session::unpinContext(const QString &id)
|
|||||||
|
|
||||||
LLMQore::RequestID Session::send(std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks)
|
LLMQore::RequestID Session::send(std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks)
|
||||||
{
|
{
|
||||||
if (!isValid()) {
|
if (!canSend()) {
|
||||||
m_lastError = makeError(ErrorCategory::Config, invalidReason());
|
const QString reason = m_agent ? (invalidReason().isEmpty()
|
||||||
|
? QStringLiteral("Session: agent has no live client")
|
||||||
|
: invalidReason())
|
||||||
|
: QStringLiteral("Session: no agent bound");
|
||||||
|
m_lastError = makeError(ErrorCategory::Config, reason);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (userBlocks.empty() || !m_history) {
|
if (userBlocks.empty() || !m_history) {
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ public:
|
|||||||
bool isValid() const noexcept;
|
bool isValid() const noexcept;
|
||||||
QString invalidReason() const;
|
QString invalidReason() const;
|
||||||
bool isInFlight() const noexcept;
|
bool isInFlight() const noexcept;
|
||||||
|
bool hasAgent() const noexcept;
|
||||||
|
bool canSend() const noexcept;
|
||||||
const ErrorInfo &lastError() const noexcept;
|
const ErrorInfo &lastError() const noexcept;
|
||||||
|
|
||||||
using ContentLoader = ContextAssembler::ContentLoader;
|
using ContentLoader = ContextAssembler::ContentLoader;
|
||||||
@@ -56,6 +58,7 @@ public:
|
|||||||
void unpinContext(const QString &id);
|
void unpinContext(const QString &id);
|
||||||
|
|
||||||
Agent *agent() noexcept { return m_agent; }
|
Agent *agent() noexcept { return m_agent; }
|
||||||
|
void setAgent(Agent *agent);
|
||||||
ConversationHistory *history() const noexcept { return m_history; }
|
ConversationHistory *history() const noexcept { return m_history; }
|
||||||
SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; }
|
SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; }
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,43 @@ Session *SessionManager::createSession(
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Session *SessionManager::createDetachedSession(ConversationHistory *externalHistory, QObject *parent)
|
||||||
|
{
|
||||||
|
return new Session(/*agent=*/nullptr, externalHistory, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SessionManager::rebindAgentByName(Session *session, const QString &agentName, QString *errorOut)
|
||||||
|
{
|
||||||
|
if (!session) {
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = QStringLiteral("SessionManager: null session");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!m_agentFactory) {
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = QStringLiteral("SessionManager: no AgentFactory bound");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString agentErr;
|
||||||
|
Agent *agent = m_agentFactory->create(agentName, /*parent=*/nullptr, &agentErr);
|
||||||
|
if (!agent) {
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = agentErr.isEmpty()
|
||||||
|
? QStringLiteral("SessionManager: agent '%1' not found").arg(agentName)
|
||||||
|
: agentErr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
session->setAgent(agent);
|
||||||
|
if (!session->isValid()) {
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = session->invalidReason();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Session *SessionManager::acquire(const QString &agentName, QString *errorOut)
|
Session *SessionManager::acquire(const QString &agentName, QString *errorOut)
|
||||||
{
|
{
|
||||||
auto &bucket = m_pool[agentName];
|
auto &bucket = m_pool[agentName];
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ public:
|
|||||||
ConversationHistory *externalHistory,
|
ConversationHistory *externalHistory,
|
||||||
QString *errorOut = nullptr);
|
QString *errorOut = nullptr);
|
||||||
|
|
||||||
|
Session *createDetachedSession(ConversationHistory *externalHistory, QObject *parent);
|
||||||
|
|
||||||
|
bool rebindAgentByName(Session *session, const QString &agentName, QString *errorOut = nullptr);
|
||||||
|
|
||||||
Session *acquire(const QString &agentName, QString *errorOut = nullptr);
|
Session *acquire(const QString &agentName, QString *errorOut = nullptr);
|
||||||
void release(Session *session);
|
void release(Session *session);
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ TEST(AgentLoaderTest, UserAgentCollidingWithBundledNameIsRejected)
|
|||||||
const AgentConfig *cfg = findConfig(result, QStringLiteral("A"));
|
const AgentConfig *cfg = findConfig(result, QStringLiteral("A"));
|
||||||
ASSERT_NE(cfg, nullptr);
|
ASSERT_NE(cfg, nullptr);
|
||||||
EXPECT_EQ(cfg->description, QStringLiteral("base"));
|
EXPECT_EQ(cfg->description, QStringLiteral("base"));
|
||||||
EXPECT_FALSE(cfg->isUserSource());
|
EXPECT_TRUE(cfg->sourcePath.startsWith(bundled.path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(AgentLoaderTest, HiddenIsNotInherited)
|
TEST(AgentLoaderTest, HiddenIsNotInherited)
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
|
|||||||
(QodeAssist::Templates::ContextData{
|
(QodeAssist::Templates::ContextData{
|
||||||
.prefix = "Line 0\nLine 1\nLin",
|
.prefix = "Line 0\nLine 1\nLin",
|
||||||
.suffix = "e 2\nLine 3\nLine 4",
|
.suffix = "e 2\nLine 3\nLine 4",
|
||||||
.fileContext = "\n Language: (MIME: text/python) filepath: /path/to/file()\n\n"
|
.fileContext = "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||||
"Recent Project Changes Context:\n "}));
|
"Recent Project Changes Context:\n "}));
|
||||||
|
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
@@ -378,7 +378,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
|
|||||||
(QodeAssist::Templates::ContextData{
|
(QodeAssist::Templates::ContextData{
|
||||||
.prefix = "Line 1\nLin",
|
.prefix = "Line 1\nLin",
|
||||||
.suffix = "e 2\nLine 3",
|
.suffix = "e 2\nLine 3",
|
||||||
.fileContext = "\n Language: (MIME: text/python) filepath: /path/to/file()\n\n"
|
.fileContext = "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||||
"Recent Project Changes Context:\n "}));
|
"Recent Project Changes Context:\n "}));
|
||||||
|
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
@@ -386,7 +386,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
|
|||||||
(QodeAssist::Templates::ContextData{
|
(QodeAssist::Templates::ContextData{
|
||||||
.prefix = "Line 0\nLine 1\nLin",
|
.prefix = "Line 0\nLine 1\nLin",
|
||||||
.suffix = "e 2\nLine 3\nLine 4",
|
.suffix = "e 2\nLine 3\nLine 4",
|
||||||
.fileContext = "\n Language: (MIME: text/python) filepath: /path/to/file()\n\n"
|
.fileContext = "\nFile information:\nMIME type: text/python\nFile path: /path/to/file\n\n"
|
||||||
"Recent Project Changes Context:\n "}));
|
"Recent Project Changes Context:\n "}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user