mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-06-03 09:08:21 -04:00
feat: Add auto sync open files with model context
This commit is contained in:
parent
9add61c805
commit
bf3c0b3aa0
@ -29,6 +29,7 @@
|
|||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
@ -45,6 +46,13 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||||
{
|
{
|
||||||
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
|
connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
[this](){
|
||||||
|
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
|
||||||
|
});
|
||||||
|
|
||||||
auto &settings = Settings::generalSettings();
|
auto &settings = Settings::generalSettings();
|
||||||
|
|
||||||
connect(&settings.caModel,
|
connect(&settings.caModel,
|
||||||
@ -52,11 +60,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::currentTemplateChanged);
|
&ChatRootView::currentTemplateChanged);
|
||||||
|
|
||||||
connect(&Settings::chatAssistantSettings().sharingCurrentFile,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::isSharingCurrentFileChanged);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
&ClientInterface::messageReceivedCompletely,
|
&ClientInterface::messageReceivedCompletely,
|
||||||
@ -76,6 +79,13 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this, &ChatRootView::updateInputTokensCount);
|
this, &ChatRootView::updateInputTokensCount);
|
||||||
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
|
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
|
||||||
this, &ChatRootView::updateInputTokensCount);
|
this, &ChatRootView::updateInputTokensCount);
|
||||||
|
|
||||||
|
auto editors = Core::EditorManager::instance();
|
||||||
|
|
||||||
|
connect(editors, &Core::EditorManager::editorOpened, this, &ChatRootView::onEditorOpened);
|
||||||
|
connect(editors, &Core::EditorManager::editorAboutToClose, this, &ChatRootView::onEditorAboutToClose);
|
||||||
|
connect(editors, &Core::EditorManager::editorsClosed, this, &ChatRootView::onEditorsClosed);
|
||||||
|
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +94,7 @@ ChatModel *ChatRootView::chatModel() const
|
|||||||
return m_chatModel;
|
return m_chatModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile)
|
void ChatRootView::sendMessage(const QString &message)
|
||||||
{
|
{
|
||||||
if (m_inputTokensCount > m_chatModel->tokensThreshold()) {
|
if (m_inputTokensCount > m_chatModel->tokensThreshold()) {
|
||||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||||
@ -102,7 +112,7 @@ void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles, sharingCurrentFile);
|
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||||
clearAttachmentFiles();
|
clearAttachmentFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,11 +168,6 @@ QString ChatRootView::currentTemplate() const
|
|||||||
return settings.caModel();
|
return settings.caModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::isSharingCurrentFile() const
|
|
||||||
{
|
|
||||||
return Settings::chatAssistantSettings().sharingCurrentFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::saveHistory(const QString &filePath)
|
void ChatRootView::saveHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
|
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||||
@ -362,6 +367,14 @@ void ChatRootView::calculateMessageTokensCount(const QString &message)
|
|||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatRootView::setIsSyncOpenFiles(bool state)
|
||||||
|
{
|
||||||
|
if (m_isSyncOpenFiles != state) {
|
||||||
|
m_isSyncOpenFiles = state;
|
||||||
|
emit isSyncOpenFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ChatRootView::updateInputTokensCount()
|
void ChatRootView::updateInputTokensCount()
|
||||||
{
|
{
|
||||||
int inputTokens = m_messageTokensCount;
|
int inputTokens = m_messageTokensCount;
|
||||||
@ -396,4 +409,42 @@ int ChatRootView::inputTokensCount() const
|
|||||||
return m_inputTokensCount;
|
return m_inputTokensCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ChatRootView::isSyncOpenFiles() const
|
||||||
|
{
|
||||||
|
return m_isSyncOpenFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::onEditorOpened(Core::IEditor *editor)
|
||||||
|
{
|
||||||
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||||
|
QString filePath = document->filePath().toString();
|
||||||
|
if (!m_linkedFiles.contains(filePath)) {
|
||||||
|
m_linkedFiles.append(filePath);
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
||||||
|
{
|
||||||
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||||
|
QString filePath = document->filePath().toString();
|
||||||
|
m_linkedFiles.removeOne(filePath);
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::onEditorsClosed(QList<Core::IEditor *> editors)
|
||||||
|
{
|
||||||
|
if (isSyncOpenFiles()) {
|
||||||
|
for (Core::IEditor *editor : editors) {
|
||||||
|
if (auto document = editor->document()) {
|
||||||
|
QString filePath = document->filePath().toString();
|
||||||
|
m_linkedFiles.removeOne(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@ -31,8 +32,7 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||||
Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
|
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||||
isSharingCurrentFileChanged FINAL)
|
|
||||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||||
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||||
@ -45,8 +45,6 @@ public:
|
|||||||
ChatModel *chatModel() const;
|
ChatModel *chatModel() const;
|
||||||
QString currentTemplate() const;
|
QString currentTemplate() const;
|
||||||
|
|
||||||
bool isSharingCurrentFile() const;
|
|
||||||
|
|
||||||
void saveHistory(const QString &filePath);
|
void saveHistory(const QString &filePath);
|
||||||
void loadHistory(const QString &filePath);
|
void loadHistory(const QString &filePath);
|
||||||
|
|
||||||
@ -64,12 +62,19 @@ public:
|
|||||||
Q_INVOKABLE void showLinkFilesDialog();
|
Q_INVOKABLE void showLinkFilesDialog();
|
||||||
Q_INVOKABLE void removeFileFromLinkList(int index);
|
Q_INVOKABLE void removeFileFromLinkList(int index);
|
||||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||||
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
|
|
||||||
void updateInputTokensCount();
|
void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
|
|
||||||
|
bool isSyncOpenFiles() const;
|
||||||
|
|
||||||
|
void onEditorOpened(Core::IEditor *editor);
|
||||||
|
void onEditorAboutToClose(Core::IEditor *editor);
|
||||||
|
void onEditorsClosed(QList<Core::IEditor *> editors);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message, bool sharingCurrentFile = false);
|
void sendMessage(const QString &message);
|
||||||
void copyToClipboard(const QString &text);
|
void copyToClipboard(const QString &text);
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
void clearAttachmentFiles();
|
void clearAttachmentFiles();
|
||||||
@ -78,10 +83,10 @@ public slots:
|
|||||||
signals:
|
signals:
|
||||||
void chatModelChanged();
|
void chatModelChanged();
|
||||||
void currentTemplateChanged();
|
void currentTemplateChanged();
|
||||||
void isSharingCurrentFileChanged();
|
|
||||||
void attachmentFilesChanged();
|
void attachmentFilesChanged();
|
||||||
void linkedFilesChanged();
|
void linkedFilesChanged();
|
||||||
void inputTokensCountChanged();
|
void inputTokensCountChanged();
|
||||||
|
void isSyncOpenFilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
@ -95,6 +100,7 @@ private:
|
|||||||
QStringList m_linkedFiles;
|
QStringList m_linkedFiles;
|
||||||
int m_messageTokensCount{0};
|
int m_messageTokensCount{0};
|
||||||
int m_inputTokensCount{0};
|
int m_inputTokensCount{0};
|
||||||
|
bool m_isSyncOpenFiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -66,7 +66,7 @@ ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
|||||||
ClientInterface::~ClientInterface() = default;
|
ClientInterface::~ClientInterface() = default;
|
||||||
|
|
||||||
void ClientInterface::sendMessage(
|
void ClientInterface::sendMessage(
|
||||||
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles, bool includeCurrentFile)
|
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
|
|
||||||
@ -100,13 +100,6 @@ void ClientInterface::sendMessage(
|
|||||||
if (chatAssistantSettings.useSystemPrompt())
|
if (chatAssistantSettings.useSystemPrompt())
|
||||||
systemPrompt = chatAssistantSettings.systemPrompt();
|
systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
|
|
||||||
if (includeCurrentFile) {
|
|
||||||
QString fileContext = getCurrentFileContext();
|
|
||||||
if (!fileContext.isEmpty()) {
|
|
||||||
systemPrompt = systemPrompt.append(fileContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
if (!linkedFiles.isEmpty()) {
|
||||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,7 @@ public:
|
|||||||
void sendMessage(
|
void sendMessage(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments = {},
|
const QList<QString> &attachments = {},
|
||||||
const QList<QString> &linkedFiles = {},
|
const QList<QString> &linkedFiles = {});
|
||||||
bool includeCurrentFile = false);
|
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
|
@ -188,7 +188,10 @@ ChatRootView {
|
|||||||
|
|
||||||
sendButton.onClicked: root.sendChatMessage()
|
sendButton.onClicked: root.sendChatMessage()
|
||||||
stopButton.onClicked: root.cancelRequest()
|
stopButton.onClicked: root.cancelRequest()
|
||||||
sharingCurrentFile.checked: root.isSharingCurrentFile
|
syncOpenFiles {
|
||||||
|
checked: root.isSyncOpenFiles
|
||||||
|
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||||
|
}
|
||||||
attachFiles.onClicked: root.showAttachFilesDialog()
|
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||||
linkFiles.onClicked: root.showLinkFilesDialog()
|
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||||
}
|
}
|
||||||
@ -197,7 +200,6 @@ ChatRootView {
|
|||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.chatModel.clear()
|
root.chatModel.clear()
|
||||||
root.clearAttachmentFiles()
|
root.clearAttachmentFiles()
|
||||||
root.clearLinkedFiles()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
@ -205,7 +207,7 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.sendMessage(messageInput.text, bottomBar.sharingCurrentFile.checked)
|
root.sendMessage(messageInput.text)
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ Rectangle {
|
|||||||
|
|
||||||
property alias sendButton: sendButtonId
|
property alias sendButton: sendButtonId
|
||||||
property alias stopButton: stopButtonId
|
property alias stopButton: stopButtonId
|
||||||
property alias sharingCurrentFile: sharingCurrentFileId
|
property alias syncOpenFiles: syncOpenFilesId
|
||||||
property alias attachFiles: attachFilesId
|
property alias attachFiles: attachFilesId
|
||||||
property alias linkFiles: linkFilesId
|
property alias linkFiles: linkFilesId
|
||||||
|
|
||||||
@ -61,9 +61,9 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: sharingCurrentFileId
|
id: syncOpenFilesId
|
||||||
|
|
||||||
text: qsTr("Share current file with models")
|
text: qsTr("Sync open files with model context")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
|
@ -44,15 +44,15 @@ ChatAssistantSettings::ChatAssistantSettings()
|
|||||||
|
|
||||||
// Chat Settings
|
// Chat Settings
|
||||||
chatTokensThreshold.setSettingsKey(Constants::CA_TOKENS_THRESHOLD);
|
chatTokensThreshold.setSettingsKey(Constants::CA_TOKENS_THRESHOLD);
|
||||||
chatTokensThreshold.setLabelText(Tr::tr("Chat History Token Limit:"));
|
chatTokensThreshold.setLabelText(Tr::tr("Chat history token limit:"));
|
||||||
chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When "
|
chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When "
|
||||||
"exceeded, oldest messages will be removed."));
|
"exceeded, oldest messages will be removed."));
|
||||||
chatTokensThreshold.setRange(1, 900000);
|
chatTokensThreshold.setRange(1, 900000);
|
||||||
chatTokensThreshold.setDefaultValue(8000);
|
chatTokensThreshold.setDefaultValue(8000);
|
||||||
|
|
||||||
sharingCurrentFile.setSettingsKey(Constants::CA_SHARING_CURRENT_FILE);
|
linkOpenFiles.setSettingsKey(Constants::CA_LINK_OPEN_FILES);
|
||||||
sharingCurrentFile.setLabelText(Tr::tr("Share Current File With Assistant by Default"));
|
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default"));
|
||||||
sharingCurrentFile.setDefaultValue(true);
|
linkOpenFiles.setDefaultValue(false);
|
||||||
|
|
||||||
stream.setSettingsKey(Constants::CA_STREAM);
|
stream.setSettingsKey(Constants::CA_STREAM);
|
||||||
stream.setDefaultValue(true);
|
stream.setDefaultValue(true);
|
||||||
@ -171,7 +171,7 @@ ChatAssistantSettings::ChatAssistantSettings()
|
|||||||
Space{8},
|
Space{8},
|
||||||
Group{
|
Group{
|
||||||
title(Tr::tr("Chat Settings")),
|
title(Tr::tr("Chat Settings")),
|
||||||
Column{Row{chatTokensThreshold, Stretch{1}}, sharingCurrentFile, stream, autosave}},
|
Column{Row{chatTokensThreshold, Stretch{1}}, linkOpenFiles, stream, autosave}},
|
||||||
Space{8},
|
Space{8},
|
||||||
Group{
|
Group{
|
||||||
title(Tr::tr("General Parameters")),
|
title(Tr::tr("General Parameters")),
|
||||||
@ -227,6 +227,7 @@ void ChatAssistantSettings::resetSettingsToDefaults()
|
|||||||
resetAspect(systemPrompt);
|
resetAspect(systemPrompt);
|
||||||
resetAspect(ollamaLivetime);
|
resetAspect(ollamaLivetime);
|
||||||
resetAspect(contextWindow);
|
resetAspect(contextWindow);
|
||||||
|
resetAspect(linkOpenFiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ public:
|
|||||||
|
|
||||||
// Chat settings
|
// Chat settings
|
||||||
Utils::IntegerAspect chatTokensThreshold{this};
|
Utils::IntegerAspect chatTokensThreshold{this};
|
||||||
Utils::BoolAspect sharingCurrentFile{this};
|
Utils::BoolAspect linkOpenFiles{this};
|
||||||
Utils::BoolAspect stream{this};
|
Utils::BoolAspect stream{this};
|
||||||
Utils::BoolAspect autosave{this};
|
Utils::BoolAspect autosave{this};
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ const char CC_STREAM[] = "QodeAssist.ccStream";
|
|||||||
const char CC_SMART_PROCESS_INSTRUCT_TEXT[] = "QodeAssist.ccSmartProcessInstructText";
|
const char CC_SMART_PROCESS_INSTRUCT_TEXT[] = "QodeAssist.ccSmartProcessInstructText";
|
||||||
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
|
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
|
||||||
const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold";
|
const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold";
|
||||||
const char CA_SHARING_CURRENT_FILE[] = "QodeAssist.caSharingCurrentFile";
|
const char CA_LINK_OPEN_FILES[] = "QodeAssist.caLinkOpenFiles";
|
||||||
const char CA_STREAM[] = "QodeAssist.caStream";
|
const char CA_STREAM[] = "QodeAssist.caStream";
|
||||||
const char CA_AUTOSAVE[] = "QodeAssist.caAutosave";
|
const char CA_AUTOSAVE[] = "QodeAssist.caAutosave";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user