mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-07 05:29:13 -04:00
fix: Found and fix review mistakes
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace QodeAssist {
|
||||
class SessionManager;
|
||||
class Session;
|
||||
class ConversationHistory;
|
||||
}
|
||||
} // namespace QodeAssist
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user