diff --git a/ChatView/ChatModel.cpp b/ChatView/ChatModel.cpp index fe50776..d0b2f95 100644 --- a/ChatView/ChatModel.cpp +++ b/ChatView/ChatModel.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -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 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(messageIndex)]; + for (const auto &block : m.blocks()) { + if (auto *tr = dynamic_cast(block.get())) + m_toolResults.insert(tr->toolUseId(), tr->result()); + } +} + +void ChatModel::pruneUsageToHistory() +{ + if (!m_history) { + m_usageByMessageId.clear(); + return; + } + QSet 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 toolResults = buildToolResultMap(); QVector 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}); + } } } diff --git a/ChatView/ChatModel.hpp b/ChatView/ChatModel.hpp index dae0db0..57bf90b 100644 --- a/ChatView/ChatModel.hpp +++ b/ChatView/ChatModel.hpp @@ -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 attachments; QVector images; }; @@ -121,6 +122,8 @@ private: int startMessageIndexFor(int messageIndex) const; int firstRowForMessage(int messageIndex) const; QHash buildToolResultMap() const; + void mergeToolResultsFromMessage(int messageIndex); + void pruneUsageToHistory(); void appendRowsForMessage( int messageIndex, const QHash &toolResults, QVector &out) const; QString overlayFileEditStatus(const QString &content, const QString &editId) const; @@ -129,6 +132,7 @@ private: QPointer m_history; QVector m_rows; + QHash m_toolResults; QHash m_usageByMessageId; QString m_chatFilePath; }; diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index a14e12c..287dba1 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -160,8 +160,8 @@ void ClientInterface::sendMessage( Context::ChangesManager::instance().archiveAllNonArchivedEdits(); - QList imageFiles; - QList textFiles; + QStringList imageFiles; + QStringList textFiles; for (const QString &filePath : attachments) { if (isImageFile(filePath)) imageFiles.append(filePath); diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index 0ffb265..36ea5a6 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -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 {}; diff --git a/QuickRefactorHandler.hpp b/QuickRefactorHandler.hpp index b7fad4a..eeae4ac 100644 --- a/QuickRefactorHandler.hpp +++ b/QuickRefactorHandler.hpp @@ -28,7 +28,7 @@ struct RefactorResult Utils::Text::Range insertRange; bool success; QString errorMessage; - TextEditor::TextEditorWidget *editor{nullptr}; + QPointer editor; }; class QuickRefactorHandler : public QObject @@ -73,7 +73,7 @@ private: QPointer m_sessionManager; QPointer m_agentFactory; QHash m_activeRequests; - TextEditor::TextEditorWidget *m_currentEditor; + QPointer m_currentEditor; Utils::Text::Range m_currentRange; bool m_isRefactoringInProgress; QString m_lastRequestId; diff --git a/context/ChangesManager.h b/context/ChangesManager.h index 73113b2..8f9e4ad 100644 --- a/context/ChangesManager.h +++ b/context/ChangesManager.h @@ -26,7 +26,7 @@ public: QString lineContent; }; - enum FileEditStatus { Pending, Applied, Rejected, Archived }; + enum FileEditStatus : int { Pending, Applied, Rejected, Archived }; struct DiffHunk { diff --git a/context/DocumentContextReader.hpp b/context/DocumentContextReader.hpp index 6b04afb..8372446 100644 --- a/context/DocumentContextReader.hpp +++ b/context/DocumentContextReader.hpp @@ -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; diff --git a/sources/settings/Pill.hpp b/sources/settings/Pill.hpp index cd51b70..3bb33a8 100644 --- a/sources/settings/Pill.hpp +++ b/sources/settings/Pill.hpp @@ -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); diff --git a/sources/settings/PipelinesConfig.cpp b/sources/settings/PipelinesConfig.cpp index 4c1c727..3d5119a 100644 --- a/sources/settings/PipelinesConfig.cpp +++ b/sources/settings/PipelinesConfig.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -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(); diff --git a/sources/settings/PipelinesConfig.hpp b/sources/settings/PipelinesConfig.hpp index 917e548..bfc0547 100644 --- a/sources/settings/PipelinesConfig.hpp +++ b/sources/settings/PipelinesConfig.hpp @@ -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( diff --git a/test/SystemPromptBuilderTest.cpp b/test/SystemPromptBuilderTest.cpp index 1177f14..30a1452 100644 --- a/test/SystemPromptBuilderTest.cpp +++ b/test/SystemPromptBuilderTest.cpp @@ -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); }