feat: Add Claude extended thinking (#254)

* feat: Add Claude extended thinking
* fix: Set 1.0 temperature for thinking mode
This commit is contained in:
Petr Mironychev
2025-11-12 18:33:15 +01:00
committed by GitHub
parent 89797639cf
commit 161d77ac04
23 changed files with 745 additions and 40 deletions

View File

@ -18,6 +18,7 @@ qt_add_qml_module(QodeAssistChatView
qml/parts/AttachedFilesPlace.qml qml/parts/AttachedFilesPlace.qml
qml/parts/Toast.qml qml/parts/Toast.qml
qml/ToolStatusItem.qml qml/ToolStatusItem.qml
qml/ThinkingStatusItem.qml
qml/FileEditItem.qml qml/FileEditItem.qml
qml/parts/RulesViewer.qml qml/parts/RulesViewer.qml
qml/parts/FileEditsActionBar.qml qml/parts/FileEditsActionBar.qml
@ -42,6 +43,8 @@ qt_add_qml_module(QodeAssistChatView
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
icons/thinking-icon-on.svg
icons/thinking-icon-off.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp

View File

@ -81,6 +81,9 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
} }
return filenames; return filenames;
} }
case Roles::IsRedacted: {
return message.isRedacted;
}
default: default:
return QVariant(); return QVariant();
} }
@ -92,6 +95,7 @@ QHash<int, QByteArray> ChatModel::roleNames() const
roles[Roles::RoleType] = "roleType"; roles[Roles::RoleType] = "roleType";
roles[Roles::Content] = "content"; roles[Roles::Content] = "content";
roles[Roles::Attachments] = "attachments"; roles[Roles::Attachments] = "attachments";
roles[Roles::IsRedacted] = "isRedacted";
return roles; return roles;
} }
@ -402,6 +406,57 @@ void ChatModel::updateToolResult(
} }
} }
void ChatModel::addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature)
{
LOG_MESSAGE(QString("Adding thinking block: requestId=%1, thinking length=%2, signature length=%3")
.arg(requestId)
.arg(thinking.length())
.arg(signature.length()));
QString displayContent = thinking;
if (!signature.isEmpty()) {
displayContent += "\n[Signature: " + signature.left(40) + "...]";
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = false;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::addRedactedThinkingBlock(const QString &requestId, const QString &signature)
{
LOG_MESSAGE(
QString("Adding redacted thinking block: requestId=%1, signature length=%2")
.arg(requestId)
.arg(signature.length()));
QString displayContent = "[Thinking content redacted by safety systems]";
if (!signature.isEmpty()) {
displayContent += "\n[Signature: " + signature.left(40) + "...]";
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = true;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added redacted thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent) void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent)
{ {
for (int i = 0; i < m_messages.size(); ++i) { for (int i = 0; i < m_messages.size(); ++i) {

View File

@ -37,10 +37,10 @@ class ChatModel : public QAbstractListModel
QML_ELEMENT QML_ELEMENT
public: public:
enum ChatRole { System, User, Assistant, Tool, FileEdit }; enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { RoleType = Qt::UserRole, Content, Attachments }; enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted };
Q_ENUM(Roles) Q_ENUM(Roles)
struct Message struct Message
@ -48,6 +48,8 @@ public:
ChatRole role; ChatRole role;
QString content; QString content;
QString id; QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments; QList<Context::ContentFile> attachments;
}; };
@ -83,6 +85,9 @@ public:
const QString &toolId, const QString &toolId,
const QString &toolName, const QString &toolName,
const QString &result); const QString &result);
void addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature);
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent); void updateMessageContent(const QString &messageId, const QString &newContent);
void setLoadingFromHistory(bool loading); void setLoadingFromHistory(bool loading);

View File

@ -43,6 +43,8 @@
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
#include "llmcore/RulesLoader.hpp" #include "llmcore/RulesLoader.hpp"
#include "ProvidersManager.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@ -197,12 +199,24 @@ ChatRootView::ChatRootView(QQuickItem *parent)
QSettings appSettings; QSettings appSettings;
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool(); m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
m_isThinkingMode = Settings::chatAssistantSettings().enableThinkingMode();
connect(
&Settings::chatAssistantSettings().enableThinkingMode,
&Utils::BaseAspect::changed,
this,
[this]() { setIsThinkingMode(Settings::chatAssistantSettings().enableThinkingMode()); });
connect( connect(
&Settings::toolsSettings().useTools, &Settings::toolsSettings().useTools,
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
this, this,
&ChatRootView::toolsSupportEnabledChanged); &ChatRootView::toolsSupportEnabledChanged);
connect(
&Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isThinkingSupportChanged);
} }
ChatModel *ChatRootView::chatModel() const ChatModel *ChatRootView::chatModel() const
@ -779,6 +793,22 @@ void ChatRootView::setIsAgentMode(bool newIsAgentMode)
} }
} }
bool ChatRootView::isThinkingMode() const
{
return m_isThinkingMode;
}
void ChatRootView::setIsThinkingMode(bool newIsThinkingMode)
{
if (m_isThinkingMode != newIsThinkingMode) {
m_isThinkingMode = newIsThinkingMode;
Settings::chatAssistantSettings().enableThinkingMode.setValue(newIsThinkingMode);
emit isThinkingModeChanged();
}
}
bool ChatRootView::toolsSupportEnabled() const bool ChatRootView::toolsSupportEnabled() const
{ {
return Settings::toolsSettings().useTools(); return Settings::toolsSettings().useTools();
@ -1087,4 +1117,12 @@ QString ChatRootView::lastInfoMessage() const
return m_lastInfoMessage; return m_lastInfoMessage;
} }
bool ChatRootView::isThinkingSupport() const
{
auto providerName = Settings::generalSettings().caProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->supportThinking();
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@ -49,6 +49,7 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL) Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL) Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL) Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL)
Q_PROPERTY( Q_PROPERTY(
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL) bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
@ -56,6 +57,7 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
QML_ELEMENT QML_ELEMENT
@ -118,6 +120,8 @@ public:
bool isAgentMode() const; bool isAgentMode() const;
void setIsAgentMode(bool newIsAgentMode); void setIsAgentMode(bool newIsAgentMode);
bool isThinkingMode() const;
void setIsThinkingMode(bool newIsThinkingMode);
bool toolsSupportEnabled() const; bool toolsSupportEnabled() const;
Q_INVOKABLE void applyFileEdit(const QString &editId); Q_INVOKABLE void applyFileEdit(const QString &editId);
@ -125,7 +129,6 @@ public:
Q_INVOKABLE void undoFileEdit(const QString &editId); Q_INVOKABLE void undoFileEdit(const QString &editId);
Q_INVOKABLE void openFileEditInEditor(const QString &editId); Q_INVOKABLE void openFileEditInEditor(const QString &editId);
// Mass file edit operations for current message
Q_INVOKABLE void applyAllFileEditsForCurrentMessage(); Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats(); Q_INVOKABLE void updateCurrentMessageEditsStats();
@ -137,6 +140,8 @@ public:
QString lastInfoMessage() const; QString lastInfoMessage() const;
bool isThinkingSupport() const;
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message);
void copyToClipboard(const QString &text); void copyToClipboard(const QString &text);
@ -166,9 +171,12 @@ signals:
void activeRulesCountChanged(); void activeRulesCountChanged();
void isAgentModeChanged(); void isAgentModeChanged();
void isThinkingModeChanged();
void toolsSupportEnabledChanged(); void toolsSupportEnabledChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged();
private: private:
void updateFileEditStatus(const QString &editId, const QString &status); void updateFileEditStatus(const QString &editId, const QString &status);
QString getChatsHistoryDir() const; QString getChatsHistoryDir() const;
@ -189,6 +197,7 @@ private:
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules; QVariantList m_activeRules;
bool m_isAgentMode; bool m_isAgentMode;
bool m_isThinkingMode;
QString m_currentMessageRequestId; QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0}; int m_currentMessageTotalEdits{0};

View File

@ -83,6 +83,10 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
messageObj["role"] = static_cast<int>(message.role); messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content; messageObj["content"] = message.content;
messageObj["id"] = message.id; messageObj["id"] = message.id;
messageObj["isRedacted"] = message.isRedacted;
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
return messageObj; return messageObj;
} }
@ -92,6 +96,8 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt()); message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString(); message.content = json["content"].toString();
message.id = json["id"].toString(); message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
return message; return message;
} }

View File

@ -119,7 +119,15 @@ void ClientInterface::sendMessage(
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) { if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
continue; continue;
} }
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content});
LLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
messages.append(apiMessage);
} }
context.history = messages; context.history = messages;
@ -189,6 +197,18 @@ void ClientInterface::sendMessage(
this, this,
&ClientInterface::handleCleanAccumulatedData, &ClientInterface::handleCleanAccumulatedData,
Qt::UniqueConnection); Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::thinkingBlockReceived,
m_chatModel,
&ChatModel::addThinkingBlock,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::redactedThinkingBlockReceived,
m_chatModel,
&ChatModel::addRedactedThinkingBlock,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest); provider->sendRequest(requestId, config.url, config.providerRequest);
} }

View File

@ -0,0 +1,4 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
<path d="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -91,6 +91,13 @@ ChatRootView {
root.isAgentMode = agentModeSwitch.checked root.isAgentMode = agentModeSwitch.checked
} }
} }
thinkingMode {
checked: root.isThinkingMode
enabled: root.isThinkingSupport
onCheckedChanged: {
root.isThinkingMode = thinkingMode.checked
}
}
} }
ListView { ListView {
@ -116,6 +123,8 @@ ChatRootView {
return toolMessageComponent return toolMessageComponent
} else if (model.roleType === ChatModel.FileEdit) { } else if (model.roleType === ChatModel.FileEdit) {
return fileEditMessageComponent return fileEditMessageComponent
} else if (model.roleType === ChatModel.Thinking) {
return thinkingMessageComponent
} else { } else {
return chatItemComponent return chatItemComponent
} }
@ -199,6 +208,35 @@ ChatRootView {
} }
} }
} }
Component {
id: thinkingMessageComponent
ThinkingStatusItem {
width: parent.width
thinkingContent: {
// Extract thinking content and signature
let content = model.content
let signatureStart = content.indexOf("\n[Signature:")
if (signatureStart >= 0) {
return content.substring(0, signatureStart)
}
return content
}
signature: {
let content = model.content
let signatureStart = content.indexOf("\n[Signature: ")
if (signatureStart >= 0) {
let signatureEnd = content.indexOf("...]", signatureStart)
if (signatureEnd >= 0) {
return content.substring(signatureStart + 13, signatureEnd)
}
}
return ""
}
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
}
}
} }
ScrollView { ScrollView {

View File

@ -0,0 +1,181 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import Qt.labs.platform as Platform
Rectangle {
id: root
property string thinkingContent: ""
property string signature: ""
property bool isRedacted: false
property bool expanded: false
radius: 6
color: palette.base
clip: true
Behavior on implicitHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
MouseArea {
id: header
width: parent.width
height: headerRow.height + 10
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
Row {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 10
}
width: parent.width
spacing: 8
Text {
text: root.isRedacted ? qsTr("Thinking (Redacted)")
: qsTr("Thinking")
font.pixelSize: 13
font.bold: true
color: palette.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
}
}
Column {
id: contentColumn
anchors {
left: parent.left
right: parent.right
top: header.bottom
margins: 10
}
spacing: 8
Text {
visible: root.isRedacted
width: parent.width
text: qsTr("Thinking content was redacted by safety systems")
font.pixelSize: 11
font.italic: true
color: Qt.rgba(0.8, 0.4, 0.4, 1.0)
wrapMode: Text.WordWrap
}
TextEdit {
id: thinkingText
visible: !root.isRedacted
width: parent.width
text: root.thinkingContent
readOnly: true
selectByMouse: true
color: palette.text
wrapMode: Text.WordWrap
font.family: "monospace"
font.pixelSize: 11
selectionColor: palette.highlight
}
// Rectangle {
// visible: root.signature.length > 0 && root.expanded
// width: parent.width
// height: signatureText.height + 10
// color: palette.alternateBase
// radius: 4
// Text {
// id: signatureText
// anchors {
// left: parent.left
// right: parent.right
// verticalCenter: parent.verticalCenter
// margins: 5
// }
// text: qsTr("Signature: %1").arg(root.signature.substring(0, Math.min(40, root.signature.length)) + "...")
// font.pixelSize: 9
// font.family: "monospace"
// color: palette.mid
// elide: Text.ElideRight
// }
// }
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
propagateComposedEvents: true
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
}
Rectangle {
id: thinkingMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: root.isRedacted ? Qt.rgba(0.8, 0.3, 0.3, 0.9)
: (root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
: Qt.lighter(palette.alternateBase, 1.3))
radius: root.radius
}
states: [
State {
when: !root.expanded
PropertyChanges {
target: root
implicitHeight: header.height
}
},
State {
when: root.expanded
PropertyChanges {
target: root
implicitHeight: header.height + contentColumn.height + 20
}
}
]
}

View File

@ -35,6 +35,7 @@ Rectangle {
property alias pinButton: pinButtonId property alias pinButton: pinButtonId
property alias rulesButton: rulesButtonId property alias rulesButton: rulesButtonId
property alias agentModeSwitch: agentModeSwitchId property alias agentModeSwitch: agentModeSwitchId
property alias thinkingMode: thinkingModeId
property alias activeRulesCount: activeRulesCountId.text property alias activeRulesCount: activeRulesCountId.text
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
@ -50,39 +51,70 @@ Rectangle {
} }
spacing: 10 spacing: 10
QoAButton { Row {
id: pinButtonId height: agentModeSwitchId.height
spacing: 10
checkable: true QoAButton {
id: pinButtonId
icon { anchors.verticalCenter: parent.verticalCenter
source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg" checkable: true
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
QoATextSlider { icon {
id: agentModeSwitchId source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg"
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
leftText: "chat" color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
rightText: "AI Agent" height: 15
width: 15
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: {
if (!agentModeSwitchId.enabled) {
return qsTr("Tools are disabled in General Settings")
} }
return checked ToolTip.visible: hovered
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code") ToolTip.delay: 250
: qsTr("Chat Mode: Simple conversation without tool access") ToolTip.text: checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
QoATextSlider {
id: agentModeSwitchId
anchors.verticalCenter: parent.verticalCenter
leftText: "chat"
rightText: "AI Agent"
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: {
if (!agentModeSwitchId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return checked
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
: qsTr("Chat Mode: Simple conversation without tool access")
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
} }
} }

View File

@ -137,4 +137,68 @@ private:
QString m_result; QString m_result;
}; };
class ThinkingContent : public ContentBlock
{
Q_OBJECT
public:
explicit ThinkingContent(const QString &thinking = QString(), const QString &signature = QString())
: ContentBlock()
, m_thinking(thinking)
, m_signature(signature)
{}
QString type() const override { return "thinking"; }
QString thinking() const { return m_thinking; }
QString signature() const { return m_signature; }
void appendThinking(const QString &text) { m_thinking += text; }
void setThinking(const QString &text) { m_thinking = text; }
void setSignature(const QString &signature) { m_signature = signature; }
QJsonValue toJson(ProviderFormat format) const override
{
Q_UNUSED(format);
// Only include signature field if it's not empty
// Empty signature is rejected by API with "Invalid signature" error
// In streaming mode, signature is not provided, so we omit the field entirely
QJsonObject obj{{"type", "thinking"}, {"thinking", m_thinking}};
if (!m_signature.isEmpty()) {
obj["signature"] = m_signature;
}
return obj;
}
private:
QString m_thinking;
QString m_signature;
};
class RedactedThinkingContent : public ContentBlock
{
Q_OBJECT
public:
explicit RedactedThinkingContent(const QString &signature = QString())
: ContentBlock()
, m_signature(signature)
{}
QString type() const override { return "redacted_thinking"; }
QString signature() const { return m_signature; }
void setSignature(const QString &signature) { m_signature = signature; }
QJsonValue toJson(ProviderFormat format) const override
{
Q_UNUSED(format);
// Only include signature field if it's not empty
// Empty signature is rejected by API with "Invalid signature" error
QJsonObject obj{{"type", "redacted_thinking"}};
if (!m_signature.isEmpty()) {
obj["signature"] = m_signature;
}
return obj;
}
private:
QString m_signature;
};
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@ -28,6 +28,9 @@ struct Message
{ {
QString role; QString role;
QString content; QString content;
QString signature;
bool isThinking = false;
bool isRedacted = false;
// clang-format off // clang-format off
bool operator==(const Message&) const = default; bool operator==(const Message&) const = default;

View File

@ -65,6 +65,7 @@ public:
= 0; = 0;
virtual bool supportsTools() const { return false; }; virtual bool supportsTools() const { return false; };
virtual bool supportThinking() const { return false; };
virtual void cancelRequest(const RequestID &requestId); virtual void cancelRequest(const RequestID &requestId);
@ -92,6 +93,9 @@ signals:
const QString &toolName, const QString &toolName,
const QString &result); const QString &result);
void continuationStarted(const QodeAssist::LLMCore::RequestID &requestId); void continuationStarted(const QodeAssist::LLMCore::RequestID &requestId);
void thinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void redactedThinkingBlockReceived(const QString &requestId, const QString &signature);
protected: protected:
QJsonObject parseEventLine(const QString &line); QJsonObject parseEventLine(const QString &line);

View File

@ -46,6 +46,19 @@ void ClaudeMessage::handleContentBlockStart(
addCurrentContent<LLMCore::ToolUseContent>(toolId, toolName, toolInput); addCurrentContent<LLMCore::ToolUseContent>(toolId, toolName, toolInput);
m_pendingToolInputs[index] = ""; m_pendingToolInputs[index] = "";
} else if (blockType == "thinking") {
QString thinking = data["thinking"].toString();
QString signature = data["signature"].toString();
LOG_MESSAGE(QString("ClaudeMessage: Creating thinking block with signature length=%1")
.arg(signature.length()));
addCurrentContent<LLMCore::ThinkingContent>(thinking, signature);
} else if (blockType == "redacted_thinking") {
QString signature = data["signature"].toString();
LOG_MESSAGE(QString("ClaudeMessage: Creating redacted_thinking block with signature length=%1")
.arg(signature.length()));
addCurrentContent<LLMCore::RedactedThinkingContent>(signature);
} }
} }
@ -66,6 +79,24 @@ void ClaudeMessage::handleContentBlockDelta(
if (m_pendingToolInputs.contains(index)) { if (m_pendingToolInputs.contains(index)) {
m_pendingToolInputs[index] += partialJson; m_pendingToolInputs[index] += partialJson;
} }
} else if (deltaType == "thinking_delta") {
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(m_currentBlocks[index])) {
thinkingContent->appendThinking(delta["thinking"].toString());
}
} else if (deltaType == "signature_delta") {
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(m_currentBlocks[index])) {
QString signature = delta["signature"].toString();
thinkingContent->setSignature(signature);
LOG_MESSAGE(QString("Set signature for thinking block %1: length=%2")
.arg(index).arg(signature.length()));
} else if (auto redactedContent = qobject_cast<LLMCore::RedactedThinkingContent *>(m_currentBlocks[index])) {
QString signature = delta["signature"].toString();
redactedContent->setSignature(signature);
LOG_MESSAGE(QString("Set signature for redacted_thinking block %1: length=%2")
.arg(index).arg(signature.length()));
}
} }
} }
@ -104,11 +135,17 @@ QJsonObject ClaudeMessage::toProviderFormat() const
message["role"] = "assistant"; message["role"] = "assistant";
QJsonArray content; QJsonArray content;
for (auto block : m_currentBlocks) { for (auto block : m_currentBlocks) {
content.append(block->toJson(LLMCore::ProviderFormat::Claude)); QJsonValue blockJson = block->toJson(LLMCore::ProviderFormat::Claude);
content.append(blockJson);
} }
message["content"] = content; message["content"] = content;
LOG_MESSAGE(QString("ClaudeMessage::toProviderFormat - message with %1 content block(s)")
.arg(m_currentBlocks.size()));
return message; return message;
} }
@ -138,6 +175,28 @@ QList<LLMCore::ToolUseContent *> ClaudeMessage::getCurrentToolUseContent() const
return toolBlocks; return toolBlocks;
} }
QList<LLMCore::ThinkingContent *> ClaudeMessage::getCurrentThinkingContent() const
{
QList<LLMCore::ThinkingContent *> thinkingBlocks;
for (auto block : m_currentBlocks) {
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(block)) {
thinkingBlocks.append(thinkingContent);
}
}
return thinkingBlocks;
}
QList<LLMCore::RedactedThinkingContent *> ClaudeMessage::getCurrentRedactedThinkingContent() const
{
QList<LLMCore::RedactedThinkingContent *> redactedBlocks;
for (auto block : m_currentBlocks) {
if (auto redactedContent = qobject_cast<LLMCore::RedactedThinkingContent *>(block)) {
redactedBlocks.append(redactedContent);
}
}
return redactedBlocks;
}
void ClaudeMessage::startNewContinuation() void ClaudeMessage::startNewContinuation()
{ {
LOG_MESSAGE(QString("ClaudeMessage: Starting new continuation")); LOG_MESSAGE(QString("ClaudeMessage: Starting new continuation"));

View File

@ -39,6 +39,9 @@ public:
LLMCore::MessageState state() const { return m_state; } LLMCore::MessageState state() const { return m_state; }
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const; QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
QList<LLMCore::ThinkingContent *> getCurrentThinkingContent() const;
QList<LLMCore::RedactedThinkingContent *> getCurrentRedactedThinkingContent() const;
const QList<LLMCore::ContentBlock *> &getCurrentBlocks() const { return m_currentBlocks; }
void startNewContinuation(); void startNewContinuation();

View File

@ -86,7 +86,6 @@ void ClaudeProvider::prepareRequest(
auto applyModelParams = [&request](const auto &settings) { auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens(); request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP()) if (settings.useTopP())
request["top_p"] = settings.topP(); request["top_p"] = settings.topP();
if (settings.useTopK()) if (settings.useTopK())
@ -96,8 +95,21 @@ void ClaudeProvider::prepareRequest(
if (type == LLMCore::RequestType::CodeCompletion) { if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings()); applyModelParams(Settings::codeCompletionSettings());
request["temperature"] = Settings::codeCompletionSettings().temperature();
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); const auto &chatSettings = Settings::chatAssistantSettings();
applyModelParams(chatSettings);
if (chatSettings.enableThinkingMode()) {
QJsonObject thinkingObj;
thinkingObj["type"] = "enabled";
thinkingObj["budget_tokens"] = chatSettings.thinkingBudgetTokens();
request["thinking"] = thinkingObj;
request["max_tokens"] = chatSettings.thinkingMaxTokens();
request["temperature"] = 1.0;
} else {
request["temperature"] = chatSettings.temperature();
}
} }
if (isToolsEnabled) { if (isToolsEnabled) {
@ -169,7 +181,8 @@ QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCo
{"top_k", {}}, {"top_k", {}},
{"stop", QJsonArray{}}, {"stop", QJsonArray{}},
{"stream", {}}, {"stream", {}},
{"tools", {}}}; {"tools", {}},
{"thinking", QJsonObject{{"type", {}}, {"budget_tokens", {}}}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
} }
@ -220,6 +233,10 @@ bool ClaudeProvider::supportsTools() const
return true; return true;
} }
bool ClaudeProvider::supportThinking() const {
return true;
};
void ClaudeProvider::cancelRequest(const LLMCore::RequestID &requestId) void ClaudeProvider::cancelRequest(const LLMCore::RequestID &requestId)
{ {
LOG_MESSAGE(QString("ClaudeProvider: Cancelling request %1").arg(requestId)); LOG_MESSAGE(QString("ClaudeProvider: Cancelling request %1").arg(requestId));
@ -308,7 +325,14 @@ void ClaudeProvider::onToolExecutionComplete(
messages.append(userMessage); messages.append(userMessage);
continuationRequest["messages"] = messages; continuationRequest["messages"] = messages;
if (continuationRequest.contains("thinking")) {
QJsonObject thinkingObj = continuationRequest["thinking"].toObject();
LOG_MESSAGE(QString("Thinking mode preserved for continuation: type=%1, budget=%2 tokens")
.arg(thinkingObj["type"].toString())
.arg(thinkingObj["budget_tokens"].toInt()));
}
LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results")
.arg(requestId) .arg(requestId)
.arg(toolResults.size())); .arg(toolResults.size()));
@ -347,6 +371,13 @@ void ClaudeProvider::processStreamEvent(const QString &requestId, const QJsonObj
LOG_MESSAGE( LOG_MESSAGE(
QString("Adding new content block: type=%1, index=%2").arg(blockType).arg(index)); QString("Adding new content block: type=%1, index=%2").arg(blockType).arg(index));
if (blockType == "thinking" || blockType == "redacted_thinking") {
QJsonDocument eventDoc(event);
LOG_MESSAGE(QString("content_block_start event for %1: %2")
.arg(blockType)
.arg(QString::fromUtf8(eventDoc.toJson(QJsonDocument::Compact))));
}
message->handleContentBlockStart(index, blockType, contentBlock); message->handleContentBlockStart(index, blockType, contentBlock);
@ -362,12 +393,90 @@ void ClaudeProvider::processStreamEvent(const QString &requestId, const QJsonObj
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
buffers.responseContent += text; buffers.responseContent += text;
emit partialResponseReceived(requestId, text); emit partialResponseReceived(requestId, text);
} else if (deltaType == "signature_delta") {
QString signature = delta["signature"].toString();
} }
} else if (eventType == "content_block_stop") { } else if (eventType == "content_block_stop") {
int index = event["index"].toInt(); int index = event["index"].toInt();
auto allBlocks = message->getCurrentBlocks();
if (index < allBlocks.size()) {
QString blockType = allBlocks[index]->type();
if (blockType == "thinking" || blockType == "redacted_thinking") {
QJsonDocument eventDoc(event);
LOG_MESSAGE(QString("content_block_stop event for %1 at index %2: %3")
.arg(blockType)
.arg(index)
.arg(QString::fromUtf8(eventDoc.toJson(QJsonDocument::Compact))));
}
}
if (event.contains("content_block")) {
QJsonObject contentBlock = event["content_block"].toObject();
QString blockType = contentBlock["type"].toString();
if (blockType == "thinking") {
QString signature = contentBlock["signature"].toString();
if (!signature.isEmpty()) {
auto allBlocks = message->getCurrentBlocks();
if (index < allBlocks.size()) {
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(allBlocks[index])) {
thinkingContent->setSignature(signature);
LOG_MESSAGE(
QString("Updated thinking block signature from content_block_stop, "
"signature length=%1")
.arg(signature.length()));
}
}
}
} else if (blockType == "redacted_thinking") {
QString signature = contentBlock["signature"].toString();
if (!signature.isEmpty()) {
auto allBlocks = message->getCurrentBlocks();
if (index < allBlocks.size()) {
if (auto redactedContent = qobject_cast<LLMCore::RedactedThinkingContent *>(allBlocks[index])) {
redactedContent->setSignature(signature);
LOG_MESSAGE(
QString("Updated redacted_thinking block signature from content_block_stop, "
"signature length=%1")
.arg(signature.length()));
}
}
}
}
}
message->handleContentBlockStop(index); message->handleContentBlockStop(index);
auto thinkingBlocks = message->getCurrentThinkingContent();
for (auto thinkingContent : thinkingBlocks) {
auto allBlocks = message->getCurrentBlocks();
if (index < allBlocks.size() && allBlocks[index] == thinkingContent) {
emit thinkingBlockReceived(
requestId, thinkingContent->thinking(), thinkingContent->signature());
LOG_MESSAGE(
QString("Emitted thinking block for request %1, thinking length=%2, signature length=%3")
.arg(requestId)
.arg(thinkingContent->thinking().length())
.arg(thinkingContent->signature().length()));
break;
}
}
auto redactedBlocks = message->getCurrentRedactedThinkingContent();
for (auto redactedContent : redactedBlocks) {
auto allBlocks = message->getCurrentBlocks();
if (index < allBlocks.size() && allBlocks[index] == redactedContent) {
emit redactedThinkingBlockReceived(requestId, redactedContent->signature());
LOG_MESSAGE(
QString("Emitted redacted thinking block for request %1, signature length=%2")
.arg(requestId)
.arg(redactedContent->signature().length()));
break;
}
}
} else if (eventType == "message_delta") { } else if (eventType == "message_delta") {
QJsonObject delta = event["delta"].toObject(); QJsonObject delta = event["delta"].toObject();
if (delta.contains("stop_reason")) { if (delta.contains("stop_reason")) {

View File

@ -53,6 +53,7 @@ public:
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override;
bool supportsTools() const override; bool supportsTools() const override;
bool supportThinking() const override;
void cancelRequest(const LLMCore::RequestID &requestId) override; void cancelRequest(const LLMCore::RequestID &requestId) override;
public slots: public slots:

View File

@ -78,7 +78,7 @@ ChatAssistantSettings::ChatAssistantSettings()
maxTokens.setSettingsKey(Constants::CA_MAX_TOKENS); maxTokens.setSettingsKey(Constants::CA_MAX_TOKENS);
maxTokens.setLabelText(Tr::tr("Max Tokens:")); maxTokens.setLabelText(Tr::tr("Max Tokens:"));
maxTokens.setRange(-1, 10000); maxTokens.setRange(-1, 200000); // -1 for unlimited, 200k max for extended output
maxTokens.setDefaultValue(2000); maxTokens.setDefaultValue(2000);
// Advanced Parameters // Advanced Parameters
@ -144,6 +144,30 @@ ChatAssistantSettings::ChatAssistantSettings()
contextWindow.setRange(-1, 10000); contextWindow.setRange(-1, 10000);
contextWindow.setDefaultValue(2048); contextWindow.setDefaultValue(2048);
// Extended Thinking Settings
enableThinkingMode.setSettingsKey(Constants::CA_ENABLE_THINKING_MODE);
enableThinkingMode.setLabelText(Tr::tr("Enable extended thinking mode (Claude only).\n Temperature is 1.0 accordingly API requerement"));
enableThinkingMode.setToolTip(
Tr::tr("Enable Claude's extended thinking mode for complex reasoning tasks. "
"This provides step-by-step reasoning before the final answer."));
enableThinkingMode.setDefaultValue(false);
thinkingBudgetTokens.setSettingsKey(Constants::CA_THINKING_BUDGET_TOKENS);
thinkingBudgetTokens.setLabelText(Tr::tr("Thinking budget tokens:"));
thinkingBudgetTokens.setToolTip(
Tr::tr("Maximum number of tokens Claude can use for internal reasoning. "
"Larger budgets improve quality but increase latency. Minimum: 1024, Recommended: 10000-16000."));
thinkingBudgetTokens.setRange(1024, 100000);
thinkingBudgetTokens.setDefaultValue(10000);
thinkingMaxTokens.setSettingsKey(Constants::CA_THINKING_MAX_TOKENS);
thinkingMaxTokens.setLabelText(Tr::tr("Thinking mode max output tokens:"));
thinkingMaxTokens.setToolTip(
Tr::tr("Maximum number of tokens for the final response when thinking mode is enabled. "
"Set to -1 to use the default max tokens setting. Recommended: 4096-16000."));
thinkingMaxTokens.setRange(-1, 200000);
thinkingMaxTokens.setDefaultValue(16000);
autosave.setDefaultValue(true); autosave.setDefaultValue(true);
autosave.setLabelText(Tr::tr("Enable autosave when message received")); autosave.setLabelText(Tr::tr("Enable autosave when message received"));
@ -237,6 +261,10 @@ ChatAssistantSettings::ChatAssistantSettings()
ollamaGrid.addRow({ollamaLivetime}); ollamaGrid.addRow({ollamaLivetime});
ollamaGrid.addRow({contextWindow}); ollamaGrid.addRow({contextWindow});
auto thinkingGrid = Grid{};
thinkingGrid.addRow({thinkingBudgetTokens});
thinkingGrid.addRow({thinkingMaxTokens});
auto chatViewSettingsGrid = Grid{}; auto chatViewSettingsGrid = Grid{};
chatViewSettingsGrid.addRow({textFontFamily, textFontSize}); chatViewSettingsGrid.addRow({textFontFamily, textFontSize});
chatViewSettingsGrid.addRow({codeFontFamily, codeFontSize}); chatViewSettingsGrid.addRow({codeFontFamily, codeFontSize});
@ -269,6 +297,9 @@ ChatAssistantSettings::ChatAssistantSettings()
systemPrompt, systemPrompt,
}}, }},
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}}, Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
Group{
title(Tr::tr("Extended Thinking (Claude Only)")),
Column{enableThinkingMode, Row{thinkingGrid, Stretch{1}}}},
Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}}, Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}},
Stretch{1}}; Stretch{1}};
}); });
@ -308,6 +339,9 @@ void ChatAssistantSettings::resetSettingsToDefaults()
resetAspect(systemPrompt); resetAspect(systemPrompt);
resetAspect(ollamaLivetime); resetAspect(ollamaLivetime);
resetAspect(contextWindow); resetAspect(contextWindow);
resetAspect(enableThinkingMode);
resetAspect(thinkingBudgetTokens);
resetAspect(thinkingMaxTokens);
resetAspect(linkOpenFiles); resetAspect(linkOpenFiles);
resetAspect(textFontFamily); resetAspect(textFontFamily);
resetAspect(codeFontFamily); resetAspect(codeFontFamily);

View File

@ -64,6 +64,11 @@ public:
Utils::StringAspect ollamaLivetime{this}; Utils::StringAspect ollamaLivetime{this};
Utils::IntegerAspect contextWindow{this}; Utils::IntegerAspect contextWindow{this};
// Extended Thinking Settings (Claude only)
Utils::BoolAspect enableThinkingMode{this};
Utils::IntegerAspect thinkingBudgetTokens{this};
Utils::IntegerAspect thinkingMaxTokens{this};
// Visuals settings // Visuals settings
Utils::SelectionAspect textFontFamily{this}; Utils::SelectionAspect textFontFamily{this};
Utils::IntegerAspect textFontSize{this}; Utils::IntegerAspect textFontSize{this};

View File

@ -168,6 +168,9 @@ const char CA_USE_FREQUENCY_PENALTY[] = "QodeAssist.chatUseFrequencyPenalty";
const char CA_FREQUENCY_PENALTY[] = "QodeAssist.chatFrequencyPenalty"; const char CA_FREQUENCY_PENALTY[] = "QodeAssist.chatFrequencyPenalty";
const char CA_OLLAMA_LIVETIME[] = "QodeAssist.chatOllamaLivetime"; const char CA_OLLAMA_LIVETIME[] = "QodeAssist.chatOllamaLivetime";
const char CA_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.caOllamaContextWindow"; const char CA_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.caOllamaContextWindow";
const char CA_ENABLE_THINKING_MODE[] = "QodeAssist.caEnableThinkingMode";
const char CA_THINKING_BUDGET_TOKENS[] = "QodeAssist.caThinkingBudgetTokens";
const char CA_THINKING_MAX_TOKENS[] = "QodeAssist.caThinkingMaxTokens";
const char CA_TEXT_FONT_FAMILY[] = "QodeAssist.caTextFontFamily"; const char CA_TEXT_FONT_FAMILY[] = "QodeAssist.caTextFontFamily";
const char CA_TEXT_FONT_SIZE[] = "QodeAssist.caTextFontSize"; const char CA_TEXT_FONT_SIZE[] = "QodeAssist.caTextFontSize";
const char CA_CODE_FONT_FAMILY[] = "QodeAssist.caCodeFontFamily"; const char CA_CODE_FONT_FAMILY[] = "QodeAssist.caCodeFontFamily";

View File

@ -42,7 +42,33 @@ public:
if (context.history) { if (context.history) {
for (const auto &msg : context.history.value()) { for (const auto &msg : context.history.value()) {
if (msg.role != "system") { if (msg.role != "system") {
messages.append(QJsonObject{{"role", msg.role}, {"content", msg.content}}); // Handle thinking blocks with structured content
if (msg.isThinking) {
// Create content array with thinking block
QJsonArray content;
QJsonObject thinkingBlock;
thinkingBlock["type"] = msg.isRedacted ? "redacted_thinking" : "thinking";
// Extract actual thinking text (remove display signature)
QString thinkingText = msg.content;
int signaturePos = thinkingText.indexOf("\n[Signature: ");
if (signaturePos != -1) {
thinkingText = thinkingText.left(signaturePos);
}
if (!msg.isRedacted) {
thinkingBlock["thinking"] = thinkingText;
}
if (!msg.signature.isEmpty()) {
thinkingBlock["signature"] = msg.signature;
}
content.append(thinkingBlock);
messages.append(QJsonObject{{"role", "assistant"}, {"content", content}});
} else {
// Normal message
messages.append(QJsonObject{{"role", msg.role}, {"content", msg.content}});
}
} }
} }
} }