fix: Merging tool result

This commit is contained in:
Petr Mironychev
2026-06-29 13:28:26 +02:00
parent 86c537477d
commit d66c714a28
11 changed files with 93 additions and 27 deletions

View File

@@ -8,6 +8,7 @@
#include <QFileInfo> #include <QFileInfo>
#include <QJsonDocument> #include <QJsonDocument>
#include <QRegularExpression> #include <QRegularExpression>
#include <QSet>
#include <QUrl> #include <QUrl>
#include <LLMQore/ContentBlocks.hpp> #include <LLMQore/ContentBlocks.hpp>
@@ -114,6 +115,7 @@ void ChatModel::setHistory(ConversationHistory *history)
} }
beginResetModel(); beginResetModel();
m_usageByMessageId.clear();
rebuildAll(); rebuildAll();
endResetModel(); endResetModel();
emit sessionUsageChanged(); emit sessionUsageChanged();
@@ -137,7 +139,7 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue(row.kind); return QVariant::fromValue(row.kind);
case Roles::Content: case Roles::Content:
if (row.kind == ChatRole::FileEdit) if (row.kind == ChatRole::FileEdit)
return overlayFileEditStatus(row.content, row.editId); return row.fileEditDisplay;
return row.content; return row.content;
case Roles::Attachments: case Roles::Attachments:
return buildAttachmentList(row.attachments); return buildAttachmentList(row.attachments);
@@ -364,6 +366,7 @@ void ChatModel::appendRowsForMessage(
editRow.messageId = id; editRow.messageId = id;
editRow.content = result; editRow.content = result;
editRow.editId = parseEditId(result); editRow.editId = parseEditId(result);
editRow.fileEditDisplay = overlayFileEditStatus(result, editRow.editId);
out.append(std::move(editRow)); out.append(std::move(editRow));
} }
} }
@@ -376,20 +379,54 @@ void ChatModel::appendRowsForMessage(
void ChatModel::rebuildAll() void ChatModel::rebuildAll()
{ {
m_rows.clear(); m_rows.clear();
m_toolResults.clear();
if (!m_history) if (!m_history)
return; return;
const QHash<QString, QString> toolResults = buildToolResultMap(); m_toolResults = buildToolResultMap();
for (int mi = 0; mi < m_history->size(); ++mi) for (int mi = 0; mi < m_history->size(); ++mi)
appendRowsForMessage(mi, toolResults, m_rows); appendRowsForMessage(mi, m_toolResults, m_rows);
}
void ChatModel::mergeToolResultsFromMessage(int messageIndex)
{
if (!m_history || messageIndex < 0 || messageIndex >= m_history->size())
return;
const Message &m = m_history->messages()[static_cast<size_t>(messageIndex)];
for (const auto &block : m.blocks()) {
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
m_toolResults.insert(tr->toolUseId(), tr->result());
}
}
void ChatModel::pruneUsageToHistory()
{
if (!m_history) {
m_usageByMessageId.clear();
return;
}
QSet<QString> liveIds;
for (const auto &m : m_history->messages())
liveIds.insert(m.id());
for (auto it = m_usageByMessageId.begin(); it != m_usageByMessageId.end();) {
if (!liveIds.contains(it.key()))
it = m_usageByMessageId.erase(it);
else
++it;
}
} }
int ChatModel::firstRowForMessage(int messageIndex) const int ChatModel::firstRowForMessage(int messageIndex) const
{ {
for (int i = 0; i < m_rows.size(); ++i) { int lo = 0;
if (m_rows[i].messageIndex >= messageIndex) int hi = m_rows.size();
return i; while (lo < hi) {
const int mid = lo + (hi - lo) / 2;
if (m_rows[mid].messageIndex < messageIndex)
lo = mid + 1;
else
hi = mid;
} }
return m_rows.size(); return lo;
} }
int ChatModel::startMessageIndexFor(int messageIndex) const int ChatModel::startMessageIndexFor(int messageIndex) const
@@ -414,11 +451,10 @@ void ChatModel::reprojectTail(int startMessageIndex)
return; return;
const int oldStart = firstRowForMessage(startMessageIndex); const int oldStart = firstRowForMessage(startMessageIndex);
const QHash<QString, QString> toolResults = buildToolResultMap();
QVector<Row> newTail; QVector<Row> newTail;
for (int mi = startMessageIndex; mi < m_history->size(); ++mi) for (int mi = startMessageIndex; mi < m_history->size(); ++mi)
appendRowsForMessage(mi, toolResults, newTail); appendRowsForMessage(mi, m_toolResults, newTail);
const int oldCount = m_rows.size() - oldStart; const int oldCount = m_rows.size() - oldStart;
const int newCount = newTail.size(); const int newCount = newTail.size();
@@ -443,11 +479,13 @@ void ChatModel::reprojectTail(int startMessageIndex)
void ChatModel::onHistoryMessageAdded(int index) void ChatModel::onHistoryMessageAdded(int index)
{ {
mergeToolResultsFromMessage(index);
reprojectTail(startMessageIndexFor(index)); reprojectTail(startMessageIndexFor(index));
} }
void ChatModel::onHistoryMessageUpdated(int index) void ChatModel::onHistoryMessageUpdated(int index)
{ {
mergeToolResultsFromMessage(index);
reprojectTail(startMessageIndexFor(index)); reprojectTail(startMessageIndexFor(index));
} }
@@ -455,6 +493,7 @@ void ChatModel::onHistoryCleared()
{ {
beginResetModel(); beginResetModel();
m_rows.clear(); m_rows.clear();
m_toolResults.clear();
m_usageByMessageId.clear(); m_usageByMessageId.clear();
endResetModel(); endResetModel();
emit modelReseted(); emit modelReseted();
@@ -465,6 +504,7 @@ void ChatModel::onHistoryReset()
{ {
beginResetModel(); beginResetModel();
rebuildAll(); rebuildAll();
pruneUsageToHistory();
endResetModel(); endResetModel();
emit sessionUsageChanged(); emit sessionUsageChanged();
} }
@@ -472,8 +512,10 @@ void ChatModel::onHistoryReset()
void ChatModel::onFileEditStatusChanged(const QString &editId) void ChatModel::onFileEditStatusChanged(const QString &editId)
{ {
for (int i = 0; i < m_rows.size(); ++i) { for (int i = 0; i < m_rows.size(); ++i) {
if (m_rows[i].kind == ChatRole::FileEdit && m_rows[i].editId == editId) if (m_rows[i].kind == ChatRole::FileEdit && m_rows[i].editId == editId) {
m_rows[i].fileEditDisplay = overlayFileEditStatus(m_rows[i].content, editId);
emit dataChanged(index(i), index(i), {Roles::Content}); emit dataChanged(index(i), index(i), {Roles::Content});
}
} }
} }

View File

@@ -30,10 +30,10 @@ class ChatModel : public QAbstractListModel
QML_ELEMENT QML_ELEMENT
public: public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking }; enum ChatRole : int { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { enum Roles : int {
RoleType = Qt::UserRole, RoleType = Qt::UserRole,
Content, Content,
Attachments, Attachments,
@@ -105,6 +105,7 @@ private:
QString content; QString content;
bool isRedacted = false; bool isRedacted = false;
QString editId; QString editId;
QString fileEditDisplay;
QVector<AttachmentRef> attachments; QVector<AttachmentRef> attachments;
QVector<ImageRef> images; QVector<ImageRef> images;
}; };
@@ -121,6 +122,8 @@ private:
int startMessageIndexFor(int messageIndex) const; int startMessageIndexFor(int messageIndex) const;
int firstRowForMessage(int messageIndex) const; int firstRowForMessage(int messageIndex) const;
QHash<QString, QString> buildToolResultMap() const; QHash<QString, QString> buildToolResultMap() const;
void mergeToolResultsFromMessage(int messageIndex);
void pruneUsageToHistory();
void appendRowsForMessage( void appendRowsForMessage(
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const; int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
QString overlayFileEditStatus(const QString &content, const QString &editId) const; QString overlayFileEditStatus(const QString &content, const QString &editId) const;
@@ -129,6 +132,7 @@ private:
QPointer<ConversationHistory> m_history; QPointer<ConversationHistory> m_history;
QVector<Row> m_rows; QVector<Row> m_rows;
QHash<QString, QString> m_toolResults;
QHash<QString, Usage> m_usageByMessageId; QHash<QString, Usage> m_usageByMessageId;
QString m_chatFilePath; QString m_chatFilePath;
}; };

View File

@@ -160,8 +160,8 @@ void ClientInterface::sendMessage(
Context::ChangesManager::instance().archiveAllNonArchivedEdits(); Context::ChangesManager::instance().archiveAllNonArchivedEdits();
QList<QString> imageFiles; QStringList imageFiles;
QList<QString> textFiles; QStringList textFiles;
for (const QString &filePath : attachments) { for (const QString &filePath : attachments) {
if (isImageFile(filePath)) if (isImageFile(filePath))
imageFiles.append(filePath); imageFiles.append(filePath);

View File

@@ -292,7 +292,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
if (requestId.isEmpty()) { if (requestId.isEmpty()) {
QString error = QString("Failed to start completion request for agent '%1': %2") QString error = QString("Failed to start completion request for agent '%1': %2")
.arg(agentName, session->lastError().message); .arg(agentName, session->lastError().message);
session->deleteLater(); m_sessionManager.removeSession(session);
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
@@ -304,7 +304,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
{ {
const QStringList roster = Settings::PipelinesConfig::load().rosters.codeCompletion; const QStringList roster = Settings::PipelinesConfig::loadCached().rosters.codeCompletion;
if (roster.isEmpty()) if (roster.isEmpty())
return {}; return {};

View File

@@ -28,7 +28,7 @@ struct RefactorResult
Utils::Text::Range insertRange; Utils::Text::Range insertRange;
bool success; bool success;
QString errorMessage; QString errorMessage;
TextEditor::TextEditorWidget *editor{nullptr}; QPointer<TextEditor::TextEditorWidget> editor;
}; };
class QuickRefactorHandler : public QObject class QuickRefactorHandler : public QObject
@@ -73,7 +73,7 @@ private:
QPointer<SessionManager> m_sessionManager; QPointer<SessionManager> m_sessionManager;
QPointer<AgentFactory> m_agentFactory; QPointer<AgentFactory> m_agentFactory;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor; QPointer<TextEditor::TextEditorWidget> m_currentEditor;
Utils::Text::Range m_currentRange; Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress; bool m_isRefactoringInProgress;
QString m_lastRequestId; QString m_lastRequestId;

View File

@@ -26,7 +26,7 @@ public:
QString lineContent; QString lineContent;
}; };
enum FileEditStatus { Pending, Applied, Rejected, Archived }; enum FileEditStatus : int { Pending, Applied, Rejected, Archived };
struct DiffHunk struct DiffHunk
{ {

View File

@@ -59,7 +59,7 @@ public:
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const; int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
private: private:
TextEditor::TextDocument *m_textDocument; TextEditor::TextDocument *m_textDocument = nullptr;
QTextDocument *m_document; QTextDocument *m_document;
QString m_mimeType; QString m_mimeType;
QString m_filePath; QString m_filePath;

View File

@@ -15,7 +15,7 @@ class Pill : public QLabel
{ {
Q_OBJECT Q_OBJECT
public: public:
enum Kind { Neutral, Accent, On, Off, User, Tag, Active, Match }; enum Kind : int { Neutral, Accent, On, Off, User, Tag, Active, Match };
explicit Pill(Kind kind, const QString &text = {}, QWidget *parent = nullptr); explicit Pill(Kind kind, const QString &text = {}, QWidget *parent = nullptr);

View File

@@ -6,6 +6,7 @@
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <QDateTime>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -203,6 +204,23 @@ PipelinesLoadResult PipelinesConfig::load()
return result; return result;
} }
PipelinesLoadResult PipelinesConfig::loadCached()
{
static PipelinesLoadResult cached;
static QDateTime cachedMTime;
static bool valid = false;
const QFileInfo info(filePath());
const QDateTime mtime = info.exists() ? info.lastModified() : QDateTime();
if (valid && mtime == cachedMTime)
return cached;
cached = load();
cachedMTime = mtime;
valid = true;
return cached;
}
bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut) bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut)
{ {
const QString path = filePath(); const QString path = filePath();

View File

@@ -44,6 +44,8 @@ public:
[[nodiscard]] static PipelinesLoadResult load(); [[nodiscard]] static PipelinesLoadResult load();
[[nodiscard]] static PipelinesLoadResult loadCached();
[[nodiscard]] static bool save(const PipelineRosters &rosters, QString *errorOut = nullptr); [[nodiscard]] static bool save(const PipelineRosters &rosters, QString *errorOut = nullptr);
[[nodiscard]] static bool validate( [[nodiscard]] static bool validate(

View File

@@ -92,7 +92,7 @@ TEST(SystemPromptBuilderTest, IdenticalSetLayerEmitsNoSignal)
QSignalSpy spy(&builder, &SystemPromptBuilder::layersChanged); QSignalSpy spy(&builder, &SystemPromptBuilder::layersChanged);
builder.setLayer(QStringLiteral("a"), QStringLiteral("A"), 10); builder.setLayer(QStringLiteral("a"), QStringLiteral("A"), 10);
EXPECT_EQ(spy.count(), 0); EXPECT_EQ(spy.size(), 0);
} }
TEST(SystemPromptBuilderTest, ChangingSetLayerEmitsSignal) TEST(SystemPromptBuilderTest, ChangingSetLayerEmitsSignal)
@@ -103,7 +103,7 @@ TEST(SystemPromptBuilderTest, ChangingSetLayerEmitsSignal)
QSignalSpy spy(&builder, &SystemPromptBuilder::layersChanged); QSignalSpy spy(&builder, &SystemPromptBuilder::layersChanged);
builder.setLayer(QStringLiteral("a"), QStringLiteral("A"), 20); builder.setLayer(QStringLiteral("a"), QStringLiteral("A"), 20);
EXPECT_EQ(spy.count(), 1); EXPECT_EQ(spy.size(), 1);
} }
TEST(SystemPromptBuilderTest, ClearLayerRemovesAndSignals) TEST(SystemPromptBuilderTest, ClearLayerRemovesAndSignals)
@@ -115,7 +115,7 @@ TEST(SystemPromptBuilderTest, ClearLayerRemovesAndSignals)
builder.clearLayer(QStringLiteral("a")); builder.clearLayer(QStringLiteral("a"));
EXPECT_TRUE(builder.isEmpty()); EXPECT_TRUE(builder.isEmpty());
EXPECT_EQ(spy.count(), 1); EXPECT_EQ(spy.size(), 1);
} }
TEST(SystemPromptBuilderTest, ClearMissingLayerEmitsNoSignal) TEST(SystemPromptBuilderTest, ClearMissingLayerEmitsNoSignal)
@@ -127,7 +127,7 @@ TEST(SystemPromptBuilderTest, ClearMissingLayerEmitsNoSignal)
builder.clearLayer(QStringLiteral("nope")); builder.clearLayer(QStringLiteral("nope"));
EXPECT_FALSE(builder.isEmpty()); EXPECT_FALSE(builder.isEmpty());
EXPECT_EQ(spy.count(), 0); EXPECT_EQ(spy.size(), 0);
} }
TEST(SystemPromptBuilderTest, ClearEmptiesAndSignals) TEST(SystemPromptBuilderTest, ClearEmptiesAndSignals)
@@ -140,7 +140,7 @@ TEST(SystemPromptBuilderTest, ClearEmptiesAndSignals)
builder.clear(); builder.clear();
EXPECT_TRUE(builder.isEmpty()); EXPECT_TRUE(builder.isEmpty());
EXPECT_EQ(spy.count(), 1); EXPECT_EQ(spy.size(), 1);
} }
TEST(SystemPromptBuilderTest, ClearWhenAlreadyEmptyEmitsNoSignal) TEST(SystemPromptBuilderTest, ClearWhenAlreadyEmptyEmitsNoSignal)
@@ -150,5 +150,5 @@ TEST(SystemPromptBuilderTest, ClearWhenAlreadyEmptyEmitsNoSignal)
QSignalSpy spy(&builder, &SystemPromptBuilder::layersChanged); QSignalSpy spy(&builder, &SystemPromptBuilder::layersChanged);
builder.clear(); builder.clear();
EXPECT_EQ(spy.count(), 0); EXPECT_EQ(spy.size(), 0);
} }