fix: Found and fix review mistakes

This commit is contained in:
Petr Mironychev
2026-07-02 22:26:07 +02:00
parent c070d65366
commit 35bbaa1af0
139 changed files with 2032 additions and 1573 deletions

View File

@@ -23,6 +23,12 @@ ChatAgentController::ChatAgentController(QObject *parent)
{
if (auto *settings = Core::ICore::settings())
m_currentAgent = settings->value(kChatAgentKey).toString();
connect(
Settings::PipelinesNotifier::instance(),
&Settings::PipelinesNotifier::pipelinesChanged,
this,
&ChatAgentController::reload);
}
void ChatAgentController::setAgentFactory(AgentFactory *factory)

View File

@@ -12,6 +12,7 @@
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp"
#include "logger/Logger.hpp"
@@ -47,8 +48,7 @@ void ChatCompressor::setActiveAgent(const QString &agentName)
m_activeAgent = agentName;
}
void ChatCompressor::startCompression(
const QString &chatFilePath, ConversationHistory *sourceHistory)
void ChatCompressor::startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory)
{
if (m_isCompressing) {
emit compressionFailed(tr("Compression already in progress"));
@@ -73,8 +73,7 @@ void ChatCompressor::startCompression(
QString sessionError;
Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
if (!session) {
emit compressionFailed(
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
emit compressionFailed(sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
return;
}
@@ -117,11 +116,13 @@ void ChatCompressor::startCompression(
const QString transcript = transcriptParts.join(QStringLiteral("\n\n"));
connect(session, &Session::finished, this, [this](const LLMQore::RequestID &id, const QString &) {
onCompressionFinished(id);
});
connect(
session, &Session::finished, this,
[this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); });
connect(
session, &Session::failed, this,
session,
&Session::failed,
this,
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onCompressionFailed(id, error.message);
});
@@ -134,8 +135,8 @@ void ChatCompressor::startCompression(
m_currentRequestId = session->send(std::move(blocks));
if (m_currentRequestId.isEmpty()) {
handleCompressionError(tr("Failed to start compression request: %1")
.arg(session->lastError().message));
handleCompressionError(
tr("Failed to start compression request: %1").arg(session->lastError().message));
return;
}
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
@@ -169,6 +170,11 @@ void ChatCompressor::onCompressionFinished(const QString &requestId)
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
if (summary.trimmed().isEmpty()) {
handleCompressionError(tr("Compression produced an empty summary"));
return;
}
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
const QString sourcePath = m_originalChatPath;
@@ -209,23 +215,8 @@ QString ChatCompressor::createCompressedChatPath(const QString &originalPath) co
bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary)
{
QFile sourceFile(sourcePath);
if (!sourceFile.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
return false;
}
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
sourceFile.close();
if (doc.isNull() || !doc.isObject()) {
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
.arg(sourcePath, parseError.errorString()));
return false;
}
QJsonObject root = doc.object();
QJsonObject root;
root["version"] = ChatSerializer::VERSION;
QJsonObject summaryMessage;
summaryMessage["role"] = "assistant";

View File

@@ -13,7 +13,7 @@ namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
} // namespace QodeAssist
namespace QodeAssist::Chat {

View File

@@ -22,14 +22,17 @@
#include <LLMQore/ContentBlocks.hpp>
#include "ChatModel.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
ChatHistoryStore::ChatHistoryStore(
ConversationHistory *history, ChatModel *chatModel, QObject *parent)
: QObject(parent)
, m_history(history)
, m_chatModel(chatModel)
{}
QString ChatHistoryStore::historyDir() const
@@ -118,12 +121,17 @@ QString ChatHistoryStore::autosaveFilePath(
SerializationResult ChatHistoryStore::save(const QString &filePath) const
{
return ChatSerializer::saveToFile(m_history, filePath);
return ChatSerializer::saveToFile(
m_history, filePath, m_chatModel ? m_chatModel->usageToJson() : QJsonObject{});
}
SerializationResult ChatHistoryStore::load(const QString &filePath) const
{
return ChatSerializer::loadFromFile(m_history, filePath);
QJsonObject usage;
const SerializationResult result = ChatSerializer::loadFromFile(m_history, filePath, &usage);
if (result.success && m_chatModel)
m_chatModel->restoreUsageFromJson(usage);
return result;
}
void ChatHistoryStore::showSaveDialog()

View File

@@ -15,12 +15,15 @@ class ConversationHistory;
namespace QodeAssist::Chat {
class ChatModel;
class ChatHistoryStore : public QObject
{
Q_OBJECT
public:
explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
explicit ChatHistoryStore(
ConversationHistory *history, ChatModel *chatModel, QObject *parent = nullptr);
QString historyDir() const;
QString suggestedFileName() const;
@@ -45,6 +48,7 @@ private:
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
ConversationHistory *m_history;
ChatModel *m_chatModel;
};
} // namespace QodeAssist::Chat

View File

@@ -28,10 +28,14 @@ const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
QString changesStatusToString(Context::ChangesManager::FileEditStatus status)
{
switch (status) {
case Context::ChangesManager::Pending: return QStringLiteral("pending");
case Context::ChangesManager::Applied: return QStringLiteral("applied");
case Context::ChangesManager::Rejected: return QStringLiteral("rejected");
case Context::ChangesManager::Archived: return QStringLiteral("archived");
case Context::ChangesManager::Pending:
return QStringLiteral("pending");
case Context::ChangesManager::Applied:
return QStringLiteral("applied");
case Context::ChangesManager::Rejected:
return QStringLiteral("rejected");
case Context::ChangesManager::Archived:
return QStringLiteral("archived");
}
return QStringLiteral("pending");
}
@@ -80,17 +84,25 @@ ChatModel::ChatModel(QObject *parent)
{
auto &changes = Context::ChangesManager::instance();
connect(
&changes, &Context::ChangesManager::fileEditApplied,
this, &ChatModel::onFileEditStatusChanged);
&changes,
&Context::ChangesManager::fileEditApplied,
this,
&ChatModel::onFileEditStatusChanged);
connect(
&changes, &Context::ChangesManager::fileEditRejected,
this, &ChatModel::onFileEditStatusChanged);
&changes,
&Context::ChangesManager::fileEditRejected,
this,
&ChatModel::onFileEditStatusChanged);
connect(
&changes, &Context::ChangesManager::fileEditUndone,
this, &ChatModel::onFileEditStatusChanged);
&changes,
&Context::ChangesManager::fileEditUndone,
this,
&ChatModel::onFileEditStatusChanged);
connect(
&changes, &Context::ChangesManager::fileEditArchived,
this, &ChatModel::onFileEditStatusChanged);
&changes,
&Context::ChangesManager::fileEditArchived,
this,
&ChatModel::onFileEditStatusChanged);
}
void ChatModel::setHistory(ConversationHistory *history)
@@ -105,11 +117,12 @@ void ChatModel::setHistory(ConversationHistory *history)
if (m_history) {
connect(
m_history, &ConversationHistory::messageAdded,
this, &ChatModel::onHistoryMessageAdded);
m_history, &ConversationHistory::messageAdded, this, &ChatModel::onHistoryMessageAdded);
connect(
m_history, &ConversationHistory::messageUpdated,
this, &ChatModel::onHistoryMessageUpdated);
m_history,
&ConversationHistory::messageUpdated,
this,
&ChatModel::onHistoryMessageUpdated);
connect(m_history, &ConversationHistory::cleared, this, &ChatModel::onHistoryCleared);
connect(m_history, &ConversationHistory::reset, this, &ChatModel::onHistoryReset);
}
@@ -244,8 +257,7 @@ QString ChatModel::overlayFileEditStatus(const QString &content, const QString &
obj["status_message"] = edit.statusMessage;
}
}
return kFileEditMarker
+ QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
return kFileEditMarker + QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
}
QHash<QString, QString> ChatModel::buildToolResultMap() const
@@ -328,8 +340,7 @@ void ChatModel::appendRowsForMessage(
row.messageId = id;
row.content = content;
out.append(std::move(row));
} else if (
auto *rth = dynamic_cast<LLMQore::RedactedThinkingContent *>(block.get())) {
} else if (auto *rth = dynamic_cast<LLMQore::RedactedThinkingContent *>(block.get())) {
QString content = QStringLiteral("[Thinking content redacted by safety systems]");
if (!rth->signature().isEmpty())
content += QStringLiteral("\n[Signature: ") + rth->signature().left(40)
@@ -644,6 +655,36 @@ void ChatModel::setMessageUsage(
emit sessionUsageChanged();
}
QJsonObject ChatModel::usageToJson() const
{
QJsonObject out;
for (auto it = m_usageByMessageId.cbegin(); it != m_usageByMessageId.cend(); ++it) {
const Usage &u = it.value();
if (u.prompt == 0 && u.completion == 0 && u.cached == 0 && u.reasoning == 0)
continue;
QJsonObject entry;
entry["prompt"] = u.prompt;
entry["completion"] = u.completion;
entry["cached"] = u.cached;
entry["reasoning"] = u.reasoning;
out.insert(it.key(), entry);
}
return out;
}
void ChatModel::restoreUsageFromJson(const QJsonObject &usage)
{
for (auto it = usage.constBegin(); it != usage.constEnd(); ++it) {
const QJsonObject entry = it.value().toObject();
setMessageUsage(
it.key(),
entry.value("prompt").toInt(),
entry.value("completion").toInt(),
entry.value("cached").toInt(),
entry.value("reasoning").toInt());
}
}
int ChatModel::sessionPromptTokens() const
{
int total = 0;

View File

@@ -26,7 +26,8 @@ class ChatModel : public QAbstractListModel
Q_OBJECT
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(
int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
QML_ELEMENT
public:
@@ -67,6 +68,9 @@ public:
int cachedPromptTokens,
int reasoningTokens);
QJsonObject usageToJson() const;
void restoreUsageFromJson(const QJsonObject &usage);
int sessionPromptTokens() const;
int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const;

View File

@@ -41,12 +41,12 @@
#include "FileEditController.hpp"
#include "GeneralSettings.hpp"
#include "InputTokenCounter.hpp"
#include "SettingsConstants.hpp"
#include "Logger.hpp"
#include "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp"
#include "ProjectSettings.hpp"
#include "SessionFileRegistry.hpp"
#include "SettingsConstants.hpp"
#include "SkillsSettings.hpp"
#include "context/ContextManager.hpp"
#include "sources/skills/SkillsManager.hpp"
namespace QodeAssist::Chat {
@@ -85,7 +85,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
, m_agentController(new ChatAgentController(this))
, m_fileEditController(new FileEditController(this))
, m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_history, this))
, m_historyStore(new ChatHistoryStore(m_history, m_chatModel, this))
{
m_chatModel->setHistory(m_history);
m_clientInterface->setHistory(m_history);
@@ -145,7 +145,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(m_chatModel, &QAbstractItemModel::modelReset, this, maybeEmitTitle);
connect(m_chatModel, &QAbstractItemModel::rowsInserted, this, maybeEmitTitle);
connect(m_chatModel, &QAbstractItemModel::rowsRemoved, this, maybeEmitTitle);
connect(m_chatModel, &QAbstractItemModel::dataChanged, this, maybeEmitTitle);
connect(this, &ChatRootView::attachmentFilesChanged, this, [this]() {
m_tokenCounter->setAttachments(m_attachmentFiles);
});
@@ -174,6 +173,14 @@ ChatRootView::ChatRootView(QQuickItem *parent)
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useToolsChanged);
connect(
Settings::PipelinesNotifier::instance(),
&Settings::PipelinesNotifier::pipelinesChanged,
this,
[this]() {
emit availableChatAgentsChanged();
emit useToolsChanged();
});
auto editors = Core::EditorManager::instance();
@@ -223,10 +230,14 @@ ChatRootView::ChatRootView(QQuickItem *parent)
});
connect(
m_clientInterface,
&ClientInterface::requestStarted,
this,
[this](const QString &requestId) { m_fileEditController->setCurrentRequestId(requestId); });
m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
m_fileEditController->setCurrentRequestId(requestId);
setRequestProgressStatus(true);
});
connect(m_clientInterface, &ClientInterface::requestCancelled, this, [this]() {
setRequestProgressStatus(false);
});
connect(
m_clientInterface,
@@ -428,6 +439,11 @@ ChatModel *ChatRootView::chatModel() const
void ChatRootView::sendMessage(const QString &message)
{
if (message.trimmed().isEmpty() && m_attachmentFiles.isEmpty())
return;
if (m_chatCompressor->isCompressing())
return;
const QStringList attachments = m_attachmentFiles;
const QStringList linkedFiles = m_linkedFiles;
@@ -438,9 +454,7 @@ void ChatRootView::sendMessage(const QString &message)
}
bool ChatRootView::deferSendForAutoCompress(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles)
const QString &message, const QStringList &attachments, const QStringList &linkedFiles)
{
auto &settings = Settings::chatAssistantSettings();
if (!settings.autoCompress())
@@ -456,6 +470,9 @@ bool ChatRootView::deferSendForAutoCompress(
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments);
if (auto registry = sessionFileRegistry()) {
filePath = registry->uniqueFreePath(filePath);
}
if (filePath.isEmpty())
return false;
setRecentFilePath(filePath);
@@ -475,9 +492,7 @@ bool ChatRootView::deferSendForAutoCompress(
}
void ChatRootView::dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles)
const QString &message, const QStringList &attachments, const QStringList &linkedFiles)
{
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments);
@@ -500,7 +515,6 @@ void ChatRootView::dispatchSend(
m_fileManager->clearIntermediateStorage();
clearAttachmentFiles();
setRequestProgressStatus(true);
}
void ChatRootView::copyToClipboard(const QString &text)
@@ -568,10 +582,17 @@ void ChatRootView::loadHistory(const QString &filePath)
return;
}
m_clientInterface->cancelRequest();
auto result = m_historyStore->load(filePath);
if (!result.success) {
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
} else {
if (!result.errorMessage.isEmpty()) {
m_lastInfoMessage = result.errorMessage;
emit lastInfoMessageChanged();
LOG_MESSAGE(QString("Chat history loaded with issues: %1").arg(result.errorMessage));
}
setRecentFilePath(filePath);
}
@@ -591,6 +612,12 @@ void ChatRootView::showSaveDialog()
m_historyStore->showSaveDialog();
}
void ChatRootView::resetChatTo(int index)
{
m_clientInterface->cancelRequest();
m_chatModel->resetModelTo(index);
}
void ChatRootView::showLoadDialog()
{
m_historyStore->showLoadDialog();
@@ -939,8 +966,6 @@ void ChatRootView::relocateToWindow()
clearAttachmentFiles();
emit closeHostRequested();
// Closing the source split raises the main window; re-raise the chat window once that
// queued teardown has run. The registry outlives this view, which the split close deletes.
if (auto registry = sessionFileRegistry()) {
QMetaObject::invokeMethod(
registry,
@@ -1210,7 +1235,13 @@ void ChatRootView::compressCurrentChat()
if (compressionAgent.isEmpty())
return;
autosave();
const auto saveResult = m_historyStore->save(m_recentFilePath);
if (!saveResult.success) {
m_lastErrorMessage
= tr("Failed to save chat before compression: %1").arg(saveResult.errorMessage);
emit lastErrorMessageChanged();
return;
}
m_chatCompressor->setSessionManager(sessionManager());
m_chatCompressor->setActiveAgent(compressionAgent);

View File

@@ -22,7 +22,7 @@ class AgentFactory;
class SessionManager;
class ConversationHistory;
class Session;
}
} // namespace QodeAssist
namespace QodeAssist::Chat {
@@ -57,8 +57,12 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
Q_PROPERTY(
QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged
FINAL)
Q_PROPERTY(
QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY
currentChatAgentChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool canCompress READ canCompress NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
@@ -77,6 +81,7 @@ public:
Q_INVOKABLE void showSaveDialog();
Q_INVOKABLE void showLoadDialog();
Q_INVOKABLE void resetChatTo(int index);
void autosave();
QString getAutosaveFilePath() const;
@@ -220,13 +225,9 @@ private:
void triggerOpenChatCommand(Utils::Id commandId);
void handOffSession();
bool deferSendForAutoCompress(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles);
const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
void dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles);
const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
QString configuredCompressionAgent() const;
bool hasImageAttachments(const QStringList &attachments) const;

View File

@@ -29,7 +29,6 @@ namespace {
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
void registerEditFromResult(const QString &result)
@@ -51,8 +50,21 @@ void registerEditFromResult(const QString &result)
filePath,
obj.value("old_content").toString(),
obj.value("new_content").toString(),
/*autoApply=*/false,
/*isFromHistory=*/true);
false,
true);
}
void appendMergingAssistants(std::vector<Message> &out, Message message)
{
if (message.role() == Message::Role::Assistant && !out.empty()
&& out.back().role() == Message::Role::Assistant) {
if (out.back().id().isEmpty() && !message.id().isEmpty())
out.back().setId(message.id());
for (auto &block : message.takeBlocks())
out.back().appendBlock(std::move(block));
return;
}
out.push_back(std::move(message));
}
} // namespace
@@ -60,7 +72,7 @@ void registerEditFromResult(const QString &result)
const QString ChatSerializer::VERSION = "0.3";
SerializationResult ChatSerializer::saveToFile(
const ConversationHistory *history, const QString &filePath)
const ConversationHistory *history, const QString &filePath, const QJsonObject &usage)
{
if (!history)
return {false, "No conversation history"};
@@ -74,7 +86,7 @@ SerializationResult ChatSerializer::saveToFile(
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
}
QJsonDocument doc(serializeChat(history));
QJsonDocument doc(serializeChat(history, usage));
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
}
@@ -83,7 +95,7 @@ SerializationResult ChatSerializer::saveToFile(
}
SerializationResult ChatSerializer::loadFromFile(
ConversationHistory *history, const QString &filePath)
ConversationHistory *history, const QString &filePath, QJsonObject *usageOut)
{
if (!history)
return {false, "No conversation history"};
@@ -106,11 +118,12 @@ SerializationResult ChatSerializer::loadFromFile(
}
if (version == VERSION)
return loadCurrent(history, root);
return loadLegacy(history, root);
return loadCurrent(history, root, usageOut);
return loadLegacy(history, root, usageOut);
}
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
QJsonObject ChatSerializer::serializeChat(
const ConversationHistory *history, const QJsonObject &usage)
{
QJsonArray messagesArray;
for (const auto &message : history->messages())
@@ -119,29 +132,57 @@ QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
if (!usage.isEmpty())
root["usage"] = usage;
return root;
}
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
SerializationResult ChatSerializer::loadCurrent(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut)
{
history->clear();
int skipped = 0;
const QJsonArray messagesArray = root["messages"].toArray();
for (const auto &value : messagesArray) {
bool ok = false;
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
if (ok)
history->append(std::move(message));
else
++skipped;
}
if (usageOut)
*usageOut = root["usage"].toObject();
registerHistoricalFileEdits(history);
if (skipped > 0) {
return {true, QString("%1 message(s) could not be parsed and were skipped").arg(skipped)};
}
return {true, QString()};
}
SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root)
SerializationResult ChatSerializer::loadLegacy(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut)
{
history->clear();
QJsonObject usage;
const auto collectUsage = [&usage](const QJsonObject &mj) {
const QString id = mj["id"].toString();
const QJsonObject legacyUsage = mj["usage"].toObject();
if (id.isEmpty() || legacyUsage.isEmpty())
return;
QJsonObject entry;
entry["prompt"] = legacyUsage["promptTokens"].toInt();
entry["completion"] = legacyUsage["completionTokens"].toInt();
entry["cached"] = legacyUsage["cachedPromptTokens"].toInt();
entry["reasoning"] = legacyUsage["reasoningTokens"].toInt();
usage.insert(id, entry);
};
std::vector<Message> merged;
const QJsonArray arr = root["messages"].toArray();
int i = 0;
while (i < arr.size()) {
@@ -152,21 +193,23 @@ SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, con
Message assistant(Message::Role::Assistant);
Message toolResults(Message::Role::User);
while (i < arr.size()
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) {
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt())
== LegacyRole::Tool) {
const QJsonObject tj = arr[i].toObject();
const QString toolName = tj["toolName"].toString();
const QString id = tj["id"].toString();
if (!toolName.isEmpty()) {
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
id, toolName, tj["toolArguments"].toObject()));
toolResults.appendBlock(std::make_unique<LLMQore::ToolResultContent>(
id, tj["toolResult"].toString()));
assistant.appendBlock(
std::make_unique<LLMQore::ToolUseContent>(
id, toolName, tj["toolArguments"].toObject()));
toolResults.appendBlock(
std::make_unique<LLMQore::ToolResultContent>(id, tj["toolResult"].toString()));
}
++i;
}
if (!assistant.blocks().empty()) {
history->append(std::move(assistant));
history->append(std::move(toolResults));
appendMergingAssistants(merged, std::move(assistant));
merged.push_back(std::move(toolResults));
}
continue;
}
@@ -174,22 +217,21 @@ SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, con
++i;
if (role == LegacyRole::FileEdit)
continue; // derived from the tool result in the new model
continue;
if (role == LegacyRole::Thinking) {
const QString content = mj["content"].toString();
const QString signature = mj["signature"].toString();
Message assistant(Message::Role::Assistant);
if (mj["isRedacted"].toBool(false)) {
assistant.appendBlock(
std::make_unique<LLMQore::RedactedThinkingContent>(signature));
assistant.appendBlock(std::make_unique<LLMQore::RedactedThinkingContent>(signature));
} else {
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
assistant.appendBlock(
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
}
history->append(std::move(assistant));
appendMergingAssistants(merged, std::move(assistant));
continue;
}
@@ -198,29 +240,41 @@ SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, con
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
for (const auto &a : mj["attachments"].toArray()) {
const QJsonObject ao = a.toObject();
user.appendBlock(std::make_unique<StoredAttachmentContent>(
ao["fileName"].toString(), ao["storedPath"].toString()));
user.appendBlock(
std::make_unique<StoredAttachmentContent>(
ao["fileName"].toString(), ao["storedPath"].toString()));
}
for (const auto &im : mj["images"].toArray()) {
const QJsonObject io = im.toObject();
user.appendBlock(std::make_unique<StoredImageContent>(
io["fileName"].toString(),
io["storedPath"].toString(),
io["mediaType"].toString()));
user.appendBlock(
std::make_unique<StoredImageContent>(
io["fileName"].toString(),
io["storedPath"].toString(),
io["mediaType"].toString()));
}
history->append(std::move(user));
merged.push_back(std::move(user));
} else {
const QString content = mj["content"].toString();
if (content.trimmed().isEmpty())
continue;
const Message::Role mapped
= role == LegacyRole::System ? Message::Role::System : Message::Role::Assistant;
const Message::Role mapped = role == LegacyRole::System ? Message::Role::System
: Message::Role::Assistant;
Message message(mapped, mj["id"].toString());
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
history->append(std::move(message));
collectUsage(mj);
if (mapped == Message::Role::Assistant)
appendMergingAssistants(merged, std::move(message));
else
merged.push_back(std::move(message));
}
}
for (auto &message : merged)
history->append(std::move(message));
if (usageOut)
*usageOut = usage;
registerHistoricalFileEdits(history);
return {true, QString()};
}

View File

@@ -33,11 +33,13 @@ using StoredContentCache = QHash<QString, StoredContentEntry>;
class ChatSerializer
{
public:
static SerializationResult saveToFile(
const ConversationHistory *history, const QString &filePath);
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
static const QString VERSION;
static SerializationResult saveToFile(
const ConversationHistory *history, const QString &filePath, const QJsonObject &usage = {});
static SerializationResult loadFromFile(
ConversationHistory *history, const QString &filePath, QJsonObject *usageOut = nullptr);
// Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage(
const QString &chatFilePath,
@@ -45,16 +47,14 @@ public:
const QString &base64Data,
QString &storedPath);
static QString loadContentFromStorage(
const QString &chatFilePath,
const QString &storedPath,
StoredContentCache *cache = nullptr);
const QString &chatFilePath, const QString &storedPath, StoredContentCache *cache = nullptr);
private:
static const QString VERSION;
static QJsonObject serializeChat(const ConversationHistory *history);
static SerializationResult loadCurrent(ConversationHistory *history, const QJsonObject &root);
static SerializationResult loadLegacy(ConversationHistory *history, const QJsonObject &root);
static QJsonObject serializeChat(const ConversationHistory *history, const QJsonObject &usage);
static SerializationResult loadCurrent(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
static SerializationResult loadLegacy(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
static void registerHistoricalFileEdits(const ConversationHistory *history);
static bool ensureDirectoryExists(const QString &filePath);

View File

@@ -32,8 +32,8 @@
#include <QUuid>
#include <AgentFactory.hpp>
#include <ConversationHistory.hpp>
#include <ContextRenderer.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <Session.hpp>
@@ -122,6 +122,9 @@ void ClientInterface::ensureSession()
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onSessionFailed(id, error);
});
connect(m_session, &Session::cancelled, this, [this](const LLMQore::RequestID &id) {
onSessionCancelled(id);
});
}
bool ClientInterface::ensureAgentBound()
@@ -147,9 +150,7 @@ bool ClientInterface::ensureAgentBound()
}
void ClientInterface::sendMessage(
const QString &message,
const QList<QString> &attachments,
const QList<QString> &linkedFiles)
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
{
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message");
@@ -299,13 +300,13 @@ void ClientInterface::sendMessage(
}
for (const auto &image : storedImages) {
blocks.push_back(std::make_unique<StoredImageContent>(
image.fileName, image.storedPath, image.mediaType));
blocks.push_back(
std::make_unique<StoredImageContent>(image.fileName, image.storedPath, image.mediaType));
}
if (!m_chatFilePath.isEmpty()) {
if (auto *todoTool
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
client->tools()->tool("todo_tool"))) {
todoTool->setCurrentSessionId(m_chatFilePath);
}
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
@@ -396,14 +397,19 @@ void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist
m_activeRequests.erase(it);
}
void ClientInterface::onSessionCancelled(const QString &requestId)
{
m_activeRequests.remove(requestId);
emit requestCancelled();
}
QStringList ClientInterface::invokedSkillNames(const QString &message) const
{
QStringList names;
if (!m_skillsManager || !Settings::skillsSettings().enableSkills())
return names;
static const QRegularExpression skillCommand(
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
static const QRegularExpression skillCommand(QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
auto skillMatch = skillCommand.globalMatch(message);
while (skillMatch.hasNext()) {
const QString skillName = skillMatch.next().captured(1);
@@ -415,16 +421,16 @@ QStringList ClientInterface::invokedSkillNames(const QString &message) const
QString ClientInterface::buildChatContextLayer() const
{
QString context
= Context::EnvBlockFormatter::formatProject(Context::EnvBlockFormatter::currentProject());
QString context = Context::EnvBlockFormatter::formatProject(
Context::EnvBlockFormatter::currentProject());
auto *project = ProjectExplorer::ProjectManager::startupProject();
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
projectSkillDirs
= Settings::SkillsSettings::splitLines(projectSettings.projectSkillDirs());
projectSkillDirs = Settings::SkillsSettings::splitLines(
projectSettings.projectSkillDirs());
}
m_skillsManager->configure(
project ? project->projectDirectory().toFSPathString() : QString(),

View File

@@ -22,7 +22,7 @@ namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
} // namespace QodeAssist
namespace QodeAssist::Skills {
class SkillsManager;
@@ -62,6 +62,7 @@ signals:
void errorOccurred(const QString &error);
void messageReceivedCompletely();
void requestStarted(const QString &requestId);
void requestCancelled();
void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
@@ -71,6 +72,7 @@ private:
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
void onSessionFinished(const QString &requestId);
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
void onSessionCancelled(const QString &requestId);
QStringList invokedSkillNames(const QString &message) const;
QString buildChatContextLayer() const;

View File

@@ -87,7 +87,7 @@ void InputTokenCounter::recompute()
if (m_history) {
for (const auto &message : m_history->messages()) {
inputTokens += Context::TokenUtils::estimateTokens(message.text());
inputTokens += 4; // + role
inputTokens += 4;
}
}

View File

@@ -15,9 +15,6 @@ class Session;
namespace QodeAssist::Chat {
// Shared registry mapping each chat (autosave) file to the live Session that owns it, so a
// file is busy only while its owning Session is alive (a destroyed Session frees it — the
// QPointer goes null). Keeps two chat views from autosaving into the same path.
class SessionFileRegistry : public QObject
{
Q_OBJECT
@@ -33,8 +30,6 @@ public:
QString uniqueFreePath(const QString &desiredPath) const;
// Handoff slot for relocating a live chat between hosts (split <-> window): the source
// chat stores its history file here, the freshly created host picks it up exactly once.
void setPendingChatFile(const QString &path);
QString takePendingChatFile();

View File

@@ -317,7 +317,7 @@ ChatRootView {
onResetChatToMessage: function(idx) {
messageInput.text = model.content
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx)
root.resetChatTo(idx)
}
onOpenFileRequested: function(filePath) {
@@ -659,6 +659,10 @@ ChatRootView {
}
function sendChatMessage() {
if (root.isCompressing)
return
if (messageInput.text.trim() === "" && root.attachmentFiles.length === 0)
return
root.hasActiveError = false
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = ""

View File

@@ -185,7 +185,7 @@ Rectangle {
id: sendButtonId
anchors.fill: parent
enabled: root.isProcessing || root.canSend
enabled: root.isProcessing || (root.canSend && !root.isCompressing)
leftPadding: root.isProcessing ? 22 : 4
icon {