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

View File

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

View File

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

View File

@@ -292,7 +292,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
if (requestId.isEmpty()) {
QString error = QString("Failed to start completion request for agent '%1': %2")
.arg(agentName, session->lastError().message);
session->deleteLater();
m_sessionManager.removeSession(session);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
@@ -304,7 +304,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
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())
return {};

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ class Pill : public QLabel
{
Q_OBJECT
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);

View File

@@ -6,6 +6,7 @@
#include <coreplugin/icore.h>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
@@ -203,6 +204,23 @@ PipelinesLoadResult PipelinesConfig::load()
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)
{
const QString path = filePath();

View File

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

View File

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