fix: Change getting focus to chat in editor and name of title

This commit is contained in:
Petr Mironychev
2026-05-21 10:48:11 +02:00
parent 3f4bda51cd
commit b9e0b5a00c
14 changed files with 224 additions and 70 deletions

View File

@@ -44,9 +44,11 @@ qt_add_qml_module(QodeAssistChatView
icons/window-unlock.svg icons/window-unlock.svg
icons/chat-icon.svg icons/chat-icon.svg
icons/chat-pause-icon.svg icons/chat-pause-icon.svg
icons/new-chat-icon.svg
icons/rules-icon.svg icons/rules-icon.svg
icons/context-icon.svg icons/context-icon.svg
icons/open-in-editor.svg icons/open-in-editor.svg
icons/open-in-window.svg
icons/apply-changes-button.svg icons/apply-changes-button.svg
icons/undo-changes-button.svg icons/undo-changes-button.svg
icons/reject-changes-button.svg icons/reject-changes-button.svg

View File

@@ -112,6 +112,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
setRecentFilePath(QString{}); setRecentFilePath(QString{});
m_fileEditController->clearCurrentRequestId(); m_fileEditController->clearCurrentRequestId();
}); });
auto maybeEmitTitle = [this] {
const QString newTitle = computeChatTitle();
if (newTitle == m_cachedChatTitle)
return;
m_cachedChatTitle = newTitle;
emit chatTitleChanged();
};
connect(m_chatModel, &ChatModel::modelReseted, this, maybeEmitTitle);
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]() { connect(this, &ChatRootView::attachmentFilesChanged, this, [this]() {
m_tokenCounter->setAttachments(m_attachmentFiles); m_tokenCounter->setAttachments(m_attachmentFiles);
}); });
@@ -792,6 +804,51 @@ void ChatRootView::triggerOpenChatCommand(Utils::Id commandId)
} }
} }
bool ChatRootView::isInEditor() const
{
return m_isInEditor;
}
void ChatRootView::setInEditor(bool value)
{
if (m_isInEditor == value)
return;
m_isInEditor = value;
emit isInEditorChanged();
}
void ChatRootView::requestNewChat()
{
triggerOpenChatCommand(Constants::QODE_ASSIST_NEW_CHAT_ACTION);
}
QString ChatRootView::chatTitle() const
{
if (m_cachedChatTitle.isEmpty())
m_cachedChatTitle = computeChatTitle();
return m_cachedChatTitle;
}
QString ChatRootView::computeChatTitle() const
{
if (!m_chatModel)
return {};
const auto history = m_chatModel->getChatHistory();
for (const auto &msg : history) {
if (msg.role != ChatModel::User)
continue;
const QString content = msg.content.trimmed();
if (content.isEmpty())
continue;
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
constexpr int maxLen = 60;
if (firstLine.length() > maxLen)
return firstLine.left(maxLen - 1) + QChar(0x2026);
return firstLine;
}
return {};
}
void ChatRootView::handOffSession() void ChatRootView::handOffSession()
{ {
if (m_chatModel->rowCount() > 0) { if (m_chatModel->rowCount() > 0) {
@@ -824,7 +881,7 @@ void ChatRootView::consumePendingChatFile()
void ChatRootView::relocateToSplit() void ChatRootView::relocateToSplit()
{ {
handOffSession(); handOffSession();
triggerOpenChatCommand(Constants::QODE_ASSIST_SHOW_CHAT_ACTION); triggerOpenChatCommand(Constants::QODE_ASSIST_NEW_CHAT_ACTION);
clearMessages(); clearMessages();
clearAttachmentFiles(); clearAttachmentFiles();
emit closeHostRequested(); emit closeHostRequested();

View File

@@ -63,6 +63,8 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL) Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL) Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
QML_ELEMENT QML_ELEMENT
@@ -183,6 +185,13 @@ public:
bool isCompressing() const; bool isCompressing() const;
bool isInEditor() const;
void setInEditor(bool value);
QString chatTitle() const;
Q_INVOKABLE void requestNewChat();
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message);
void copyToClipboard(const QString &text); void copyToClipboard(const QString &text);
@@ -228,11 +237,15 @@ signals:
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error); void compressionFailed(const QString &error);
void isInEditorChanged();
void chatTitleChanged();
void openFilesChanged(); void openFilesChanged();
void closeHostRequested(); void closeHostRequested();
private: private:
QString computeChatTitle() const;
void triggerOpenChatCommand(Utils::Id commandId); void triggerOpenChatCommand(Utils::Id commandId);
void handOffSession(); void handOffSession();
bool deferSendForAutoCompress( bool deferSendForAutoCompress(
@@ -271,6 +284,8 @@ private:
}; };
PendingSend m_pendingSend; PendingSend m_pendingSend;
bool m_isSyncOpenFiles; bool m_isSyncOpenFiles;
bool m_isInEditor = false;
mutable QString m_cachedChatTitle;
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;

View File

@@ -21,6 +21,7 @@ ChatWidget::ChatWidget(
QQmlEngine *engine, QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry, SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager, Skills::SkillsManager *skillsManager,
bool registerOwnContext,
QWidget *parent) QWidget *parent)
: QQuickWidget{engine, parent} : QQuickWidget{engine, parent}
{ {
@@ -37,10 +38,19 @@ ChatWidget::ChatWidget(
setResizeMode(QQuickWidget::SizeRootObjectToView); setResizeMode(QQuickWidget::SizeRootObjectToView);
setFocusPolicy(Qt::StrongFocus); setFocusPolicy(Qt::StrongFocus);
auto ideContext = new Core::IContext{this}; if (registerOwnContext) {
ideContext->setWidget(this); auto ideContext = new Core::IContext{this};
ideContext->setContext(Core::Context{Constants::QODE_ASSIST_CHAT_CONTEXT}); ideContext->setWidget(this);
Core::ICore::addContextObject(ideContext); ideContext->setContext(Core::Context{Constants::QODE_ASSIST_CHAT_CONTEXT});
Core::ICore::addContextObject(ideContext);
}
}
void ChatWidget::focusInEvent(QFocusEvent *event)
{
QQuickWidget::focusInEvent(event);
if (rootObject())
QMetaObject::invokeMethod(rootObject(), "focusInput");
} }
void ChatWidget::clear() void ChatWidget::clear()

View File

@@ -22,6 +22,7 @@ public:
QQmlEngine *engine, QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry, SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager, Skills::SkillsManager *skillsManager,
bool registerOwnContext = true,
QWidget *parent = nullptr); QWidget *parent = nullptr);
~ChatWidget() = default; ~ChatWidget() = default;
@@ -38,6 +39,9 @@ public:
signals: signals:
void clearPressed(); void clearPressed();
protected:
void focusInEvent(QFocusEvent *event) override;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8Z" fill="black"/>
<rect x="11" y="5" width="2" height="9" rx="0.5" fill="black"/>
<rect x="7.5" y="8.5" width="9" height="2" rx="0.5" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -1,17 +1,6 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_74_52)"> <g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round">
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44"> <rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="black"/>
<path d="M44 0H0V44H44V0Z" fill="white"/> <rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="none"/>
</mask> </g>
<g mask="url(#mask0_74_52)">
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
</g>
</g>
<defs>
<clipPath id="clip0_74_52">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -0,0 +1,6 @@
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round">
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="none"/>
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -87,21 +87,25 @@ ChatRootView {
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + 10 Layout.preferredHeight: childrenRect.height + 10
isInEditor: root.isInEditor
saveButton.onClicked: root.showSaveDialog() saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog() loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat() clearButton.onClicked: root.clearChat()
newChatButton.onClicked: root.requestNewChat()
tokensBadge { tokensBadge {
readonly property int sessionCached: root.chatModel.sessionCachedPromptTokens readonly property int sessionPrompt: root.chatModel.sessionPromptTokens || 0
readonly property int sessionCompletion: root.chatModel.sessionCompletionTokens || 0
readonly property int sessionCached: root.chatModel.sessionCachedPromptTokens || 0
text: sessionCached > 0 text: sessionCached > 0
? qsTr("next ~%1 · session ↑%2 ↓%3 ↻%4") ? qsTr("next ~%1 · session ↑%2 ↓%3 ↻%4")
.arg(root.inputTokensCount) .arg(root.inputTokensCount)
.arg(root.chatModel.sessionPromptTokens) .arg(sessionPrompt)
.arg(root.chatModel.sessionCompletionTokens) .arg(sessionCompletion)
.arg(sessionCached) .arg(sessionCached)
: qsTr("next ~%1 · session ↑%2 ↓%3") : qsTr("next ~%1 · session ↑%2 ↓%3")
.arg(root.inputTokensCount) .arg(root.inputTokensCount)
.arg(root.chatModel.sessionPromptTokens) .arg(sessionPrompt)
.arg(root.chatModel.sessionCompletionTokens) .arg(sessionCompletion)
ToolTip.text: sessionCached > 0 ToolTip.text: sessionCached > 0
? qsTr("next request (estimate) · session prompt ↑ / completion ↓ / cached ↻ (provider cache hits)") ? qsTr("next request (estimate) · session prompt ↑ / completion ↓ / cached ↻ (provider cache hits)")
: qsTr("next request (estimate) · session prompt ↑ / completion ↓") : qsTr("next request (estimate) · session prompt ↑ / completion ↓")
@@ -117,8 +121,11 @@ ChatRootView {
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
} }
relocateButton { relocateButton {
icon.source: (typeof _chatview !== 'undefined')
? "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
: "qrc:/qt/qml/ChatView/icons/open-in-window.svg"
ToolTip.text: (typeof _chatview !== 'undefined') ToolTip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor split") ? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window") : qsTr("Move this chat to a separate window")
onClicked: { onClicked: {
if (typeof _chatview !== 'undefined') if (typeof _chatview !== 'undefined')

View File

@@ -10,9 +10,12 @@ import UIControls
Rectangle { Rectangle {
id: root id: root
property bool isInEditor: false
property alias saveButton: saveButtonId property alias saveButton: saveButtonId
property alias loadButton: loadButtonId property alias loadButton: loadButtonId
property alias clearButton: clearButtonId property alias clearButton: clearButtonId
property alias newChatButton: newChatButtonId
property alias tokensBadge: tokensBadgeId property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
@@ -77,6 +80,43 @@ Rectangle {
ToolTip.delay: 250 ToolTip.delay: 250
} }
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: newChatButtonId
visible: root.isInEditor
icon {
source: "qrc:/qt/qml/ChatView/icons/new-chat-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Open new chat in a new tab")
}
QoAComboBox { QoAComboBox {
id: configSelectorId id: configSelectorId
@@ -276,21 +316,6 @@ Rectangle {
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold") ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
} }
QoASeparator {}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
} }
} }
} }

View File

@@ -16,6 +16,7 @@ const char QODE_ASSIST_CHAT_EDITOR_ID[] = "QodeAssist.ChatEditor";
const char QODE_ASSIST_SHOW_CHAT_ACTION[] = "QodeAssist.ShowChatView"; const char QODE_ASSIST_SHOW_CHAT_ACTION[] = "QodeAssist.ShowChatView";
const char QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION[] = "QodeAssist.OpenChatWindow"; const char QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION[] = "QodeAssist.OpenChatWindow";
const char QODE_ASSIST_NEW_CHAT_ACTION[] = "QodeAssist.NewChat";
const char QODE_ASSIST_CHAT_SEND_MESSAGE[] = "QodeAssist.Chat.SendMessage"; const char QODE_ASSIST_CHAT_SEND_MESSAGE[] = "QodeAssist.Chat.SendMessage";
const char QODE_ASSIST_CHAT_CLEAR_SESSION[] = "QodeAssist.Chat.ClearSession"; const char QODE_ASSIST_CHAT_CLEAR_SESSION[] = "QodeAssist.Chat.ClearSession";

View File

@@ -3,17 +3,13 @@
#include "ChatEditor.hpp" #include "ChatEditor.hpp"
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <QAction>
#include "ChatDocument.hpp" #include "ChatDocument.hpp"
#include "ChatView/ChatRootView.hpp" #include "ChatView/ChatRootView.hpp"
#include "ChatView/ChatWidget.hpp" #include "ChatView/ChatWidget.hpp"
#include "QodeAssistConstants.hpp" #include "QodeAssistConstants.hpp"
#include "QodeAssisttr.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -25,26 +21,28 @@ ChatEditor::ChatEditor(
, m_sessionFileRegistry(sessionFileRegistry) , m_sessionFileRegistry(sessionFileRegistry)
, m_skillsManager(skillsManager) , m_skillsManager(skillsManager)
, m_document(new ChatDocument(this)) , m_document(new ChatDocument(this))
, m_chatWidget(new ChatWidget(engine, sessionFileRegistry, skillsManager)) , m_chatWidget(new ChatWidget(engine, sessionFileRegistry, skillsManager, false))
{ {
setWidget(m_chatWidget); setWidget(m_chatWidget);
setContext(Core::Context(Constants::QODE_ASSIST_CHAT_CONTEXT)); setContext(Core::Context(Constants::QODE_ASSIST_CHAT_CONTEXT));
setDuplicateSupported(true); setDuplicateSupported(false);
if (auto rootView = qobject_cast<ChatRootView *>(m_chatWidget->rootObject())) { if (auto rootView = qobject_cast<ChatRootView *>(m_chatWidget->rootObject())) {
rootView->setInEditor(true);
connect( connect(
rootView, rootView,
&ChatRootView::closeHostRequested, &ChatRootView::closeHostRequested,
this, this,
[this] { [this] { Core::EditorManager::closeEditors({this}); },
Core::EditorManager::closeEditors({this});
if (auto command
= Core::ActionManager::command(Core::Constants::REMOVE_CURRENT_SPLIT)) {
if (auto action = command->action(); action && action->isEnabled())
action->trigger();
}
},
Qt::QueuedConnection); Qt::QueuedConnection);
auto syncTitle = [this, rootView] {
const QString title = rootView->chatTitle();
m_document->setPreferredDisplayName(
title.isEmpty() ? Tr::tr("QodeAssist Chat") : QStringLiteral("QodeAssist - ") + title);
};
connect(rootView, &ChatRootView::chatTitleChanged, this, syncTitle);
syncTitle();
} }
} }
@@ -71,7 +69,7 @@ QWidget *ChatEditor::toolBar()
Core::IEditor *ChatEditor::duplicate() Core::IEditor *ChatEditor::duplicate()
{ {
return new ChatEditor(m_engine, m_sessionFileRegistry, m_skillsManager); return nullptr;
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -17,8 +17,6 @@ class ChatDocument;
class ChatWidget; class ChatWidget;
class SessionFileRegistry; class SessionFileRegistry;
// Editor-area host for the chat. Each editor (including a split duplicate) owns its own
// ChatWidget and therefore its own independent chat session.
class ChatEditor : public Core::IEditor class ChatEditor : public Core::IEditor
{ {
Q_OBJECT Q_OBJECT

View File

@@ -10,6 +10,8 @@
#include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h> #include <coreplugin/actionmanager/command.h>
#include <coreplugin/coreconstants.h> #include <coreplugin/coreconstants.h>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icontext.h> #include <coreplugin/icontext.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h> #include <coreplugin/messagemanager.h>
@@ -228,10 +230,10 @@ public:
ActionBuilder showChatViewAction(this, Constants::QODE_ASSIST_SHOW_CHAT_ACTION); ActionBuilder showChatViewAction(this, Constants::QODE_ASSIST_SHOW_CHAT_ACTION);
const QKeySequence showChatViewShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_W); const QKeySequence showChatViewShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_W);
showChatViewAction.setDefaultKeySequence(showChatViewShortcut); showChatViewAction.setDefaultKeySequence(showChatViewShortcut);
showChatViewAction.setToolTip(Tr::tr("Open QodeAssist Chat in an editor split")); showChatViewAction.setToolTip(Tr::tr("Open QodeAssist Chat as an editor tab"));
showChatViewAction.setText(Tr::tr("Show QodeAssist Chat")); showChatViewAction.setText(Tr::tr("Show QodeAssist Chat"));
showChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon()); showChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
showChatViewAction.addOnTriggered(this, [this] { openChatInSplit(); }); showChatViewAction.addOnTriggered(this, [this] { openChatInEditor(); });
m_statusWidget->setChatButtonAction(showChatViewAction.contextAction()); m_statusWidget->setChatButtonAction(showChatViewAction.contextAction());
m_chatButtonMenu = new QMenu(m_statusWidget); m_chatButtonMenu = new QMenu(m_statusWidget);
@@ -260,6 +262,12 @@ public:
openChatWindowAction.setIcon(QCODEASSIST_CHAT_ICON.icon()); openChatWindowAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
openChatWindowAction.addOnTriggered(this, [this] { openChatInWindow(); }); openChatWindowAction.addOnTriggered(this, [this] { openChatInWindow(); });
ActionBuilder newChatAction(this, Constants::QODE_ASSIST_NEW_CHAT_ACTION);
newChatAction.setText(Tr::tr("New QodeAssist Chat"));
newChatAction.setToolTip(Tr::tr("Open a fresh chat in a new editor tab"));
newChatAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
newChatAction.addOnTriggered(this, [this] { openNewChatInEditor(); });
ActionBuilder sendMessageAction(this, Constants::QODE_ASSIST_CHAT_SEND_MESSAGE); ActionBuilder sendMessageAction(this, Constants::QODE_ASSIST_CHAT_SEND_MESSAGE);
sendMessageAction.setContext(Core::Context(Constants::QODE_ASSIST_CHAT_CONTEXT)); sendMessageAction.setContext(Core::Context(Constants::QODE_ASSIST_CHAT_CONTEXT));
sendMessageAction.setText(Tr::tr("Send QodeAssist Chat Message")); sendMessageAction.setText(Tr::tr("Send QodeAssist Chat Message"));
@@ -323,13 +331,14 @@ public:
} }
private: private:
void openChatInSplit() void openChatInEditor()
{ {
if (auto splitCommand if (auto existing = findExistingChatEditor()) {
= Core::ActionManager::command(Core::Constants::SPLIT_SIDE_BY_SIDE)) { Core::EditorManager::activateEditor(existing);
if (auto splitAction = splitCommand->action()) existing->consumePendingChatFile();
splitAction->trigger(); return;
} }
QString title = Tr::tr("QodeAssist Chat"); QString title = Tr::tr("QodeAssist Chat");
Core::IEditor *editor = Core::EditorManager::openEditorWithContents( Core::IEditor *editor = Core::EditorManager::openEditorWithContents(
Constants::QODE_ASSIST_CHAT_EDITOR_ID, &title, {}, QUuid::createUuid().toString()); Constants::QODE_ASSIST_CHAT_EDITOR_ID, &title, {}, QUuid::createUuid().toString());
@@ -337,6 +346,34 @@ private:
chatEditor->consumePendingChatFile(); chatEditor->consumePendingChatFile();
} }
void openNewChatInEditor()
{
QString title = Tr::tr("QodeAssist Chat");
Core::IEditor *editor = Core::EditorManager::openEditorWithContents(
Constants::QODE_ASSIST_CHAT_EDITOR_ID, &title, {}, QUuid::createUuid().toString());
// For the "New Chat" button pending is empty (no-op). For relocate-to-editor it
// carries the handed-off chat file and gets loaded into the freshly opened tab.
if (auto chatEditor = qobject_cast<Chat::ChatEditor *>(editor))
chatEditor->consumePendingChatFile();
}
Chat::ChatEditor *findExistingChatEditor() const
{
const auto entries = Core::DocumentModel::entries();
for (auto *entry : entries) {
if (!entry || !entry->document)
continue;
if (entry->document->id() != Constants::QODE_ASSIST_CHAT_EDITOR_ID)
continue;
const auto editors = Core::DocumentModel::editorsForDocument(entry->document);
for (auto *editor : editors) {
if (auto chatEditor = qobject_cast<Chat::ChatEditor *>(editor))
return chatEditor;
}
}
return nullptr;
}
void openChatInWindow() void openChatInWindow()
{ {
if (!m_chatView) if (!m_chatView)
@@ -400,11 +437,11 @@ private:
m_chatButtonMenu->addSeparator(); m_chatButtonMenu->addSeparator();
if (m_chatView && m_chatView->isVisible()) { if (m_chatView && m_chatView->isVisible()) {
QAction *splitAction = m_chatButtonMenu->addAction(Tr::tr("Open Chat in Split")); QAction *editorAction = m_chatButtonMenu->addAction(Tr::tr("Open Chat in Editor"));
connect(splitAction, &QAction::triggered, this, [this] { connect(editorAction, &QAction::triggered, this, [this] {
if (m_chatView) if (m_chatView)
m_chatView->close(); m_chatView->close();
openChatInSplit(); openChatInEditor();
}); });
} else { } else {
QAction *windowAction QAction *windowAction