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/chat-icon.svg
icons/chat-pause-icon.svg
icons/new-chat-icon.svg
icons/rules-icon.svg
icons/context-icon.svg
icons/open-in-editor.svg
icons/open-in-window.svg
icons/apply-changes-button.svg
icons/undo-changes-button.svg
icons/reject-changes-button.svg

View File

@@ -112,6 +112,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
setRecentFilePath(QString{});
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]() {
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()
{
if (m_chatModel->rowCount() > 0) {
@@ -824,7 +881,7 @@ void ChatRootView::consumePendingChatFile()
void ChatRootView::relocateToSplit()
{
handOffSession();
triggerOpenChatCommand(Constants::QODE_ASSIST_SHOW_CHAT_ACTION);
triggerOpenChatCommand(Constants::QODE_ASSIST_NEW_CHAT_ACTION);
clearMessages();
clearAttachmentFiles();
emit closeHostRequested();

View File

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

View File

@@ -21,6 +21,7 @@ ChatWidget::ChatWidget(
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager,
bool registerOwnContext,
QWidget *parent)
: QQuickWidget{engine, parent}
{
@@ -37,10 +38,19 @@ ChatWidget::ChatWidget(
setResizeMode(QQuickWidget::SizeRootObjectToView);
setFocusPolicy(Qt::StrongFocus);
auto ideContext = new Core::IContext{this};
ideContext->setWidget(this);
ideContext->setContext(Core::Context{Constants::QODE_ASSIST_CHAT_CONTEXT});
Core::ICore::addContextObject(ideContext);
if (registerOwnContext) {
auto ideContext = new Core::IContext{this};
ideContext->setWidget(this);
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()

View File

@@ -22,6 +22,7 @@ public:
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager,
bool registerOwnContext = true,
QWidget *parent = nullptr);
~ChatWidget() = default;
@@ -38,6 +39,9 @@ public:
signals:
void clearPressed();
protected:
void focusInEvent(QFocusEvent *event) override;
};
} // 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">
<g clip-path="url(#clip0_74_52)">
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<path d="M44 0H0V44H44V0Z" fill="white"/>
</mask>
<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 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="black"/>
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="none"/>
</g>
</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.preferredHeight: childrenRect.height + 10
isInEditor: root.isInEditor
saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
newChatButton.onClicked: root.requestNewChat()
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
? qsTr("next ~%1 · session ↑%2 ↓%3 ↻%4")
.arg(root.inputTokensCount)
.arg(root.chatModel.sessionPromptTokens)
.arg(root.chatModel.sessionCompletionTokens)
.arg(sessionPrompt)
.arg(sessionCompletion)
.arg(sessionCached)
: qsTr("next ~%1 · session ↑%2 ↓%3")
.arg(root.inputTokensCount)
.arg(root.chatModel.sessionPromptTokens)
.arg(root.chatModel.sessionCompletionTokens)
.arg(sessionPrompt)
.arg(sessionCompletion)
ToolTip.text: sessionCached > 0
? qsTr("next request (estimate) · session prompt ↑ / completion ↓ / cached ↻ (provider cache hits)")
: qsTr("next request (estimate) · session prompt ↑ / completion ↓")
@@ -117,8 +121,11 @@ ChatRootView {
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
}
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')
? qsTr("Move this chat to an editor split")
? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window")
onClicked: {
if (typeof _chatview !== 'undefined')

View File

@@ -10,9 +10,12 @@ import UIControls
Rectangle {
id: root
property bool isInEditor: false
property alias saveButton: saveButtonId
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias newChatButton: newChatButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
@@ -77,6 +80,43 @@ Rectangle {
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 {
id: configSelectorId
@@ -276,21 +316,6 @@ Rectangle {
ToolTip.delay: 250
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_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_CLEAR_SESSION[] = "QodeAssist.Chat.ClearSession";

View File

@@ -3,17 +3,13 @@
#include "ChatEditor.hpp"
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/editormanager/editormanager.h>
#include <QAction>
#include "ChatDocument.hpp"
#include "ChatView/ChatRootView.hpp"
#include "ChatView/ChatWidget.hpp"
#include "QodeAssistConstants.hpp"
#include "QodeAssisttr.h"
namespace QodeAssist::Chat {
@@ -25,26 +21,28 @@ ChatEditor::ChatEditor(
, m_sessionFileRegistry(sessionFileRegistry)
, m_skillsManager(skillsManager)
, m_document(new ChatDocument(this))
, m_chatWidget(new ChatWidget(engine, sessionFileRegistry, skillsManager))
, m_chatWidget(new ChatWidget(engine, sessionFileRegistry, skillsManager, false))
{
setWidget(m_chatWidget);
setContext(Core::Context(Constants::QODE_ASSIST_CHAT_CONTEXT));
setDuplicateSupported(true);
setDuplicateSupported(false);
if (auto rootView = qobject_cast<ChatRootView *>(m_chatWidget->rootObject())) {
rootView->setInEditor(true);
connect(
rootView,
&ChatRootView::closeHostRequested,
this,
[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();
}
},
[this] { Core::EditorManager::closeEditors({this}); },
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()
{
return new ChatEditor(m_engine, m_sessionFileRegistry, m_skillsManager);
return nullptr;
}
} // namespace QodeAssist::Chat

View File

@@ -17,8 +17,6 @@ class ChatDocument;
class ChatWidget;
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
{
Q_OBJECT

View File

@@ -10,6 +10,8 @@
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
@@ -228,10 +230,10 @@ public:
ActionBuilder showChatViewAction(this, Constants::QODE_ASSIST_SHOW_CHAT_ACTION);
const QKeySequence showChatViewShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_W);
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.setIcon(QCODEASSIST_CHAT_ICON.icon());
showChatViewAction.addOnTriggered(this, [this] { openChatInSplit(); });
showChatViewAction.addOnTriggered(this, [this] { openChatInEditor(); });
m_statusWidget->setChatButtonAction(showChatViewAction.contextAction());
m_chatButtonMenu = new QMenu(m_statusWidget);
@@ -260,6 +262,12 @@ public:
openChatWindowAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
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);
sendMessageAction.setContext(Core::Context(Constants::QODE_ASSIST_CHAT_CONTEXT));
sendMessageAction.setText(Tr::tr("Send QodeAssist Chat Message"));
@@ -323,13 +331,14 @@ public:
}
private:
void openChatInSplit()
void openChatInEditor()
{
if (auto splitCommand
= Core::ActionManager::command(Core::Constants::SPLIT_SIDE_BY_SIDE)) {
if (auto splitAction = splitCommand->action())
splitAction->trigger();
if (auto existing = findExistingChatEditor()) {
Core::EditorManager::activateEditor(existing);
existing->consumePendingChatFile();
return;
}
QString title = Tr::tr("QodeAssist Chat");
Core::IEditor *editor = Core::EditorManager::openEditorWithContents(
Constants::QODE_ASSIST_CHAT_EDITOR_ID, &title, {}, QUuid::createUuid().toString());
@@ -337,6 +346,34 @@ private:
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()
{
if (!m_chatView)
@@ -400,11 +437,11 @@ private:
m_chatButtonMenu->addSeparator();
if (m_chatView && m_chatView->isVisible()) {
QAction *splitAction = m_chatButtonMenu->addAction(Tr::tr("Open Chat in Split"));
connect(splitAction, &QAction::triggered, this, [this] {
QAction *editorAction = m_chatButtonMenu->addAction(Tr::tr("Open Chat in Editor"));
connect(editorAction, &QAction::triggered, this, [this] {
if (m_chatView)
m_chatView->close();
openChatInSplit();
openChatInEditor();
});
} else {
QAction *windowAction