mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-23 19:51:05 -04:00
feat: add support acp in common chat (#369)
This commit is contained in:
@@ -47,23 +47,16 @@ ChatRootView {
|
||||
color: palette.window
|
||||
}
|
||||
|
||||
SplitDropZone {
|
||||
DropZone {
|
||||
anchors.fill: parent
|
||||
z: 99
|
||||
|
||||
onFilesDroppedToAttach: (urlStrings) => {
|
||||
onFilesDropped: (urlStrings) => {
|
||||
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
||||
if (localPaths.length > 0) {
|
||||
root.addFilesToAttachList(localPaths)
|
||||
}
|
||||
}
|
||||
|
||||
onFilesDroppedToLink: (urlStrings) => {
|
||||
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
||||
if (localPaths.length > 0) {
|
||||
root.addFilesToLinkList(localPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QoABusyOverlay {
|
||||
@@ -76,7 +69,7 @@ ChatRootView {
|
||||
anchors.bottomMargin: bottomBar.height
|
||||
|
||||
active: root.isCompressing
|
||||
text: qsTr("Compressing chat…")
|
||||
text: root.isAgentBound ? qsTr("Preparing the handover summary…") : qsTr("Compressing chat…")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -138,19 +131,6 @@ ChatRootView {
|
||||
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
||||
? qsTr("Move this chat to an editor tab")
|
||||
: qsTr("Move this chat to a separate window")
|
||||
toolsButton {
|
||||
checked: root.useTools
|
||||
onCheckedChanged: {
|
||||
root.useTools = toolsButton.checked
|
||||
}
|
||||
}
|
||||
thinkingMode {
|
||||
checked: root.useThinking
|
||||
enabled: root.isThinkingSupport
|
||||
onCheckedChanged: {
|
||||
root.useThinking = thinkingMode.checked
|
||||
}
|
||||
}
|
||||
settingsButton.onClicked: root.openSettings()
|
||||
configSelector {
|
||||
model: root.availableConfigurations
|
||||
@@ -165,18 +145,6 @@ ChatRootView {
|
||||
root.loadAvailableConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
roleSelector {
|
||||
model: root.availableAgentRoles
|
||||
displayText: root.currentAgentRole
|
||||
onActivated: function(index) {
|
||||
root.applyAgentRole(root.availableAgentRoles[index])
|
||||
}
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableAgentRoles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -248,6 +216,10 @@ ChatRootView {
|
||||
return fileEditMessageComponent
|
||||
} else if (model.roleType === ChatModel.Thinking) {
|
||||
return thinkingMessageComponent
|
||||
} else if (model.roleType === ChatModel.Permission) {
|
||||
return permissionMessageComponent
|
||||
} else if (model.roleType === ChatModel.Plan) {
|
||||
return planMessageComponent
|
||||
} else {
|
||||
return chatItemComponent
|
||||
}
|
||||
@@ -357,7 +329,20 @@ ChatRootView {
|
||||
|
||||
ToolBlock {
|
||||
width: parent.width
|
||||
toolContent: model.content
|
||||
toolName: model.toolName || ""
|
||||
toolResult: model.toolResult || ""
|
||||
toolKind: model.toolKind || ""
|
||||
toolStatus: model.toolStatus || ""
|
||||
toolDetails: model.toolDetails || ({})
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: planMessageComponent
|
||||
|
||||
PlanBlock {
|
||||
width: parent.width
|
||||
planContent: model.content
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +371,19 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: permissionMessageComponent
|
||||
|
||||
PermissionBlock {
|
||||
width: parent.width
|
||||
permissionContent: model.content
|
||||
|
||||
onRespond: function(requestId, optionId) {
|
||||
root.respondToPermission(requestId, optionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: thinkingMessageComponent
|
||||
|
||||
@@ -405,12 +403,76 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: agentSessionBanner
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 5
|
||||
visible: root.agentSessionIssue.length > 0
|
||||
implicitHeight: bannerLayout.implicitHeight + 16
|
||||
radius: 4
|
||||
color: palette.base
|
||||
border.width: 1
|
||||
border.color: palette.mid
|
||||
|
||||
ColumnLayout {
|
||||
id: bannerLayout
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: 8
|
||||
}
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.agentSessionIssue
|
||||
textFormat: Text.PlainText
|
||||
color: palette.text
|
||||
font.pixelSize: 12
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.canStartNewAgentSession
|
||||
spacing: 8
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Continue with a new session")
|
||||
onClicked: root.startNewAgentSession()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: handoverButton
|
||||
|
||||
text: qsTr("Continue with a summary")
|
||||
enabled: root.canHandOverSummary
|
||||
|
||||
onClicked: root.startNewAgentSessionWithSummary()
|
||||
|
||||
QoAToolTip {
|
||||
visible: handoverButton.hovered
|
||||
&& root.summaryHandoverTooltip.length > 0
|
||||
text: root.summaryHandoverTooltip
|
||||
delay: 300
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 30
|
||||
Layout.maximumHeight: root.height / 2
|
||||
enabled: root.agentSessionIssue.length === 0
|
||||
|
||||
QQC.TextArea {
|
||||
id: messageInput
|
||||
@@ -452,20 +514,7 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
fileMentionPopup.dismiss()
|
||||
|
||||
const slashIndex = textBefore.lastIndexOf('/')
|
||||
if (slashIndex >= 0) {
|
||||
const beforeSlash = slashIndex === 0
|
||||
? ' '
|
||||
: textBefore.charAt(slashIndex - 1)
|
||||
const skillQuery = textBefore.substring(slashIndex + 1)
|
||||
if ((beforeSlash === ' ' || beforeSlash === '\n')
|
||||
&& /^[a-z0-9-]*$/.test(skillQuery)) {
|
||||
skillCommandPopup.updateSearch(skillQuery)
|
||||
return
|
||||
}
|
||||
}
|
||||
skillCommandPopup.dismiss()
|
||||
root.refreshSlashPopup()
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
@@ -561,17 +610,6 @@ ChatRootView {
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
|
||||
}
|
||||
|
||||
AttachedFilesPlace {
|
||||
id: linkedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
attachedFilesModel: root.linkedFiles
|
||||
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
|
||||
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||
}
|
||||
|
||||
FileEditsActionBar {
|
||||
id: fileEditsActionBar
|
||||
|
||||
@@ -593,6 +631,7 @@ ChatRootView {
|
||||
|
||||
isCompressing: root.isCompressing
|
||||
isProcessing: root.isRequestInProgress
|
||||
agentMode: root.isAgentBound
|
||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||
: root.cancelRequest()
|
||||
sendButton.icon.source: root.isRequestInProgress
|
||||
@@ -608,14 +647,12 @@ ChatRootView {
|
||||
? root.lastErrorMessage
|
||||
: qsTr("Send message to LLM %1").arg(root.sendShortcutText))
|
||||
compressButton.onClicked: compressConfirmDialog.open()
|
||||
compressButton.enabled: root.canShrinkContext
|
||||
compressButton.text: root.isAgentBound ? qsTr("Hand over") : qsTr("Compress")
|
||||
compressTooltip.text: root.shrinkContextTooltip
|
||||
cancelCompressButton.onClicked: root.cancelCompression()
|
||||
syncOpenFiles {
|
||||
checked: root.isSyncOpenFiles
|
||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||
}
|
||||
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||
attachImages.onClicked: root.showAddImageDialog()
|
||||
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,13 +693,36 @@ ChatRootView {
|
||||
|
||||
function applyMentionSelection() {
|
||||
var result = fileMentionPopup.applyCurrentSelection(
|
||||
messageInput.text, messageInput.cursorPosition, root.useTools)
|
||||
messageInput.text, messageInput.cursorPosition)
|
||||
if (result.text !== undefined) {
|
||||
messageInput.text = result.text
|
||||
messageInput.cursorPosition = result.cursorPosition
|
||||
}
|
||||
}
|
||||
|
||||
onSlashCommandsChanged: {
|
||||
if (skillCommandPopup.visible)
|
||||
refreshSlashPopup()
|
||||
}
|
||||
|
||||
function refreshSlashPopup() {
|
||||
const cursorPos = messageInput.cursorPosition
|
||||
const textBefore = messageInput.text.substring(0, cursorPos)
|
||||
const slashIndex = textBefore.lastIndexOf('/')
|
||||
if (slashIndex >= 0) {
|
||||
const beforeSlash = slashIndex === 0
|
||||
? ' '
|
||||
: textBefore.charAt(slashIndex - 1)
|
||||
const skillQuery = textBefore.substring(slashIndex + 1)
|
||||
if ((beforeSlash === ' ' || beforeSlash === '\n')
|
||||
&& /^\S*$/.test(skillQuery)) {
|
||||
skillCommandPopup.updateSearch(skillQuery)
|
||||
return
|
||||
}
|
||||
}
|
||||
skillCommandPopup.dismiss()
|
||||
}
|
||||
|
||||
function applySkillSelection() {
|
||||
const name = skillCommandPopup.currentName()
|
||||
if (name === "")
|
||||
@@ -688,16 +748,42 @@ ChatRootView {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
onChatTargetSwitchNeedsNewChat: function(targetName) {
|
||||
chatTargetSwitchDialog.targetName = targetName
|
||||
chatTargetSwitchDialog.open()
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: compressConfirmDialog
|
||||
id: chatTargetSwitchDialog
|
||||
|
||||
property string targetName: ""
|
||||
|
||||
anchors.centerIn: parent
|
||||
title: qsTr("Compress Chat")
|
||||
title: qsTr("Start a New Conversation")
|
||||
modal: true
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
Label {
|
||||
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
|
||||
text: qsTr("A conversation stays with the kind it started with, so switching to %1 needs a new one.\n\nClear this chat and switch?").arg(chatTargetSwitchDialog.targetName)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
onAccepted: root.confirmChatTargetSwitch()
|
||||
onRejected: root.cancelChatTargetSwitch()
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: compressConfirmDialog
|
||||
|
||||
anchors.centerIn: parent
|
||||
title: root.isAgentBound ? qsTr("Hand Over Session") : qsTr("Compress Chat")
|
||||
modal: true
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
Label {
|
||||
text: root.isAgentBound
|
||||
? qsTr("Summarise this conversation and continue in a fresh agent session?\n\nThe summary will be generated by the LLM chat configuration and given to the new session as context.")
|
||||
: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
@@ -840,19 +926,8 @@ ChatRootView {
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
baseSystemPrompt: root.baseSystemPrompt
|
||||
currentAgentRole: root.currentAgentRole
|
||||
currentAgentRoleDescription: root.currentAgentRoleDescription
|
||||
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
||||
activeRules: root.activeRules
|
||||
activeRulesCount: root.activeRulesCount
|
||||
|
||||
onOpenSettings: root.openSettings()
|
||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||
onOpenRulesFolder: root.openRulesFolder()
|
||||
onRefreshRules: root.refreshRules()
|
||||
onRuleSelected: function(index) {
|
||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
29
sources/ChatView/qml/chatparts/BlockPayload.js
Normal file
29
sources/ChatView/qml/chatparts/BlockPayload.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
.pragma library
|
||||
|
||||
function parseMarkerPayload(marker, content) {
|
||||
if (!content || !content.startsWith(marker))
|
||||
return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(content.substring(marker.length));
|
||||
return (parsed && typeof parsed === "object") ? parsed : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFileEdit(content) {
|
||||
return parseMarkerPayload("QODEASSIST_FILE_EDIT:", content);
|
||||
}
|
||||
|
||||
function parsePermission(content) {
|
||||
return parseMarkerPayload("QODEASSIST_PERMISSION:", content);
|
||||
}
|
||||
|
||||
function parsePlan(content) {
|
||||
return parseMarkerPayload("QODEASSIST_PLAN:", content);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import QtQuick.Layouts
|
||||
import UIControls
|
||||
import ChatView
|
||||
import Qt.labs.platform as Platform
|
||||
import "BlockPayload.js" as BlockPayload
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@@ -39,13 +40,11 @@ Rectangle {
|
||||
readonly property bool isArchived: editStatus === "archived"
|
||||
|
||||
readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
||||
readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
||||
readonly property color rejectedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
|
||||
readonly property color archivedColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
|
||||
readonly property color pendingColor: palette.highlight
|
||||
|
||||
readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
||||
readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3)
|
||||
readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3)
|
||||
readonly property color archivedBgColor: Qt.rgba(0.5, 0.5, 0.5, 0.3)
|
||||
|
||||
@@ -84,23 +83,15 @@ Rectangle {
|
||||
readonly property int removedLines: countLines(oldContent)
|
||||
|
||||
function parseEditData(content) {
|
||||
try {
|
||||
const marker = "QODEASSIST_FILE_EDIT:";
|
||||
let jsonStr = content;
|
||||
if (content.indexOf(marker) >= 0) {
|
||||
jsonStr = content.substring(content.indexOf(marker) + marker.length);
|
||||
}
|
||||
return JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
return {
|
||||
edit_id: "",
|
||||
file: "",
|
||||
old_content: "",
|
||||
new_content: "",
|
||||
status: "error",
|
||||
status_message: ""
|
||||
};
|
||||
}
|
||||
const parsed = BlockPayload.parseFileEdit(content);
|
||||
return parsed !== null ? parsed : {
|
||||
edit_id: "",
|
||||
file: "",
|
||||
old_content: "",
|
||||
new_content: "",
|
||||
status: "error",
|
||||
status_message: ""
|
||||
};
|
||||
}
|
||||
|
||||
function getFileName(path) {
|
||||
|
||||
208
sources/ChatView/qml/chatparts/PermissionBlock.qml
Normal file
208
sources/ChatView/qml/chatparts/PermissionBlock.qml
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import UIControls
|
||||
import "BlockPayload.js" as BlockPayload
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string permissionContent: ""
|
||||
|
||||
readonly property var permissionData: parsePermissionData(permissionContent)
|
||||
readonly property bool isMalformed: permissionData === null
|
||||
readonly property string requestId: isMalformed ? "" : (permissionData.requestId || "")
|
||||
readonly property string title: isMalformed
|
||||
? qsTr("Unreadable permission request")
|
||||
: (permissionData.title || qsTr("The agent asks for permission"))
|
||||
readonly property string toolKind: isMalformed ? "" : (permissionData.toolKind || "")
|
||||
readonly property var options: isMalformed ? [] : (permissionData.options || [])
|
||||
readonly property string status: isMalformed ? "error" : (permissionData.status || "pending")
|
||||
readonly property string selectedOptionId: isMalformed ? "" : (permissionData.selectedOptionId || "")
|
||||
readonly property bool automatic: !isMalformed && permissionData.automatic === true
|
||||
|
||||
readonly property bool isPending: status === "pending" && options.length > 0
|
||||
readonly property bool isCancelled: status === "cancelled"
|
||||
readonly property bool wasDeclinedUnpresentable: isCancelled && options.length === 0
|
||||
|
||||
readonly property var selectedOption: findOption(selectedOptionId)
|
||||
readonly property bool wasAllowed: selectedOption !== null
|
||||
&& selectedOption.allows === true
|
||||
|
||||
readonly property int borderRadius: 6
|
||||
readonly property int contentMargin: 10
|
||||
readonly property int badgeRadius: 3
|
||||
readonly property int badgePaddingH: 12
|
||||
readonly property int badgePaddingV: 6
|
||||
readonly property int titleMaxLines: 4
|
||||
readonly property int rowSpacing: 8
|
||||
|
||||
readonly property color allowedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
||||
readonly property color deniedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
|
||||
readonly property color cancelledColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
|
||||
|
||||
readonly property color statusColor: {
|
||||
if (isMalformed)
|
||||
return deniedColor;
|
||||
if (isPending)
|
||||
return palette.highlight;
|
||||
if (isCancelled)
|
||||
return cancelledColor;
|
||||
return wasAllowed ? allowedColor : deniedColor;
|
||||
}
|
||||
|
||||
readonly property string statusText: {
|
||||
if (isMalformed)
|
||||
return qsTr("UNREADABLE");
|
||||
if (isPending)
|
||||
return qsTr("WAITING FOR YOU");
|
||||
if (wasDeclinedUnpresentable)
|
||||
return qsTr("DECLINED");
|
||||
if (isCancelled)
|
||||
return qsTr("NO LONGER AVAILABLE");
|
||||
if (automatic)
|
||||
return wasAllowed ? qsTr("ALLOWED AUTOMATICALLY") : qsTr("DENIED AUTOMATICALLY");
|
||||
return wasAllowed ? qsTr("ALLOWED") : qsTr("DENIED");
|
||||
}
|
||||
|
||||
readonly property string explanation: {
|
||||
if (isMalformed)
|
||||
return qsTr("This permission record could not be read, so it cannot be answered.");
|
||||
if (wasDeclinedUnpresentable)
|
||||
return qsTr("The agent did not offer a set of options this chat could present safely, so the request was declined.");
|
||||
if (isCancelled)
|
||||
return qsTr("This request ended before it was answered.");
|
||||
if (isPending)
|
||||
return "";
|
||||
if (selectedOption === null)
|
||||
return "";
|
||||
return automatic
|
||||
? qsTr("Answered automatically with \"%1\", because you chose that for this action type for the rest of the conversation.").arg(selectedOption.name)
|
||||
: qsTr("You answered \"%1\".").arg(selectedOption.name);
|
||||
}
|
||||
|
||||
signal respond(string requestId, string optionId)
|
||||
|
||||
implicitHeight: layout.implicitHeight + 2 * contentMargin
|
||||
radius: borderRadius
|
||||
color: palette.base
|
||||
border.width: 1
|
||||
border.color: statusColor
|
||||
|
||||
function parsePermissionData(content) {
|
||||
return BlockPayload.parsePermission(content);
|
||||
}
|
||||
|
||||
function findOption(optionId) {
|
||||
if (!optionId)
|
||||
return null;
|
||||
for (let i = 0; i < root.options.length; ++i) {
|
||||
if (root.options[i].id === optionId)
|
||||
return root.options[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: root.contentMargin
|
||||
}
|
||||
spacing: root.rowSpacing
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: root.rowSpacing
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: statusLabel.implicitWidth + root.badgePaddingH
|
||||
Layout.preferredHeight: statusLabel.implicitHeight + root.badgePaddingV
|
||||
Layout.alignment: Qt.AlignTop
|
||||
radius: root.badgeRadius
|
||||
color: root.statusColor
|
||||
|
||||
Text {
|
||||
id: statusLabel
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: root.statusText
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
color: palette.base
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.title
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: root.titleMaxLines
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: root.toolKind.length > 0
|
||||
text: qsTr("Action type: %1").arg(root.toolKind)
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 11
|
||||
color: palette.placeholderText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: text.length > 0
|
||||
text: root.explanation
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 11
|
||||
color: palette.placeholderText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
visible: root.isPending
|
||||
spacing: root.rowSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.options
|
||||
|
||||
delegate: QoAButton {
|
||||
id: optionButton
|
||||
|
||||
required property var modelData
|
||||
|
||||
text: optionButton.modelData.name || optionButton.modelData.id
|
||||
accentColor: optionButton.modelData.allows === true
|
||||
? root.allowedColor
|
||||
: root.deniedColor
|
||||
|
||||
contentItem: Text {
|
||||
text: optionButton.text
|
||||
textFormat: Text.PlainText
|
||||
color: palette.buttonText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
onClicked: root.respond(root.requestId, optionButton.modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
sources/ChatView/qml/chatparts/PlanBlock.qml
Normal file
151
sources/ChatView/qml/chatparts/PlanBlock.qml
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "BlockPayload.js" as BlockPayload
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string planContent: ""
|
||||
property bool expanded: true
|
||||
|
||||
readonly property var planData: parsePlanData(planContent)
|
||||
readonly property var entries: planData === null ? [] : (planData.entries || [])
|
||||
readonly property int completedCount: countCompleted()
|
||||
readonly property bool isComplete: entries.length > 0 && completedCount === entries.length
|
||||
|
||||
readonly property int borderRadius: 6
|
||||
readonly property int contentMargin: 10
|
||||
readonly property int rowSpacing: 6
|
||||
|
||||
readonly property color completedColor: Qt.rgba(0.2, 0.8, 0.2, 0.9)
|
||||
readonly property color activeColor: palette.highlight
|
||||
readonly property color pendingColor: palette.placeholderText
|
||||
|
||||
implicitHeight: layout.implicitHeight + 2 * contentMargin
|
||||
radius: borderRadius
|
||||
color: palette.base
|
||||
border.width: 1
|
||||
border.color: isComplete ? completedColor : palette.mid
|
||||
|
||||
function parsePlanData(content) {
|
||||
return BlockPayload.parsePlan(content);
|
||||
}
|
||||
|
||||
function countCompleted() {
|
||||
let done = 0;
|
||||
for (let i = 0; i < root.entries.length; ++i) {
|
||||
if (root.entries[i].status === "completed")
|
||||
++done;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
function entryColor(status) {
|
||||
if (status === "completed")
|
||||
return root.completedColor;
|
||||
if (status === "in_progress")
|
||||
return root.activeColor;
|
||||
return root.pendingColor;
|
||||
}
|
||||
|
||||
function entryMarker(status) {
|
||||
if (status === "completed")
|
||||
return "✓";
|
||||
if (status === "in_progress")
|
||||
return "▶";
|
||||
return "○";
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: root.contentMargin
|
||||
}
|
||||
spacing: root.rowSpacing
|
||||
|
||||
MouseArea {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: header.implicitHeight
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.expanded = !root.expanded
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: root.rowSpacing
|
||||
|
||||
Text {
|
||||
text: qsTr("Plan")
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("%1 of %2 done").arg(root.completedCount).arg(root.entries.length)
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 11
|
||||
color: palette.placeholderText
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.expanded ? "▼" : "▶"
|
||||
font.pixelSize: 10
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.expanded ? root.entries : []
|
||||
|
||||
delegate: RowLayout {
|
||||
id: entryRow
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: root.rowSpacing
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: root.entryMarker(entryRow.modelData.status)
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 12
|
||||
color: root.entryColor(entryRow.modelData.status)
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: entryRow.modelData.content || ""
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 12
|
||||
font.strikeout: entryRow.modelData.status === "completed"
|
||||
color: entryRow.modelData.status === "completed" ? palette.placeholderText
|
||||
: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
visible: entryRow.modelData.priority === "high"
|
||||
text: qsTr("high")
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 10
|
||||
color: palette.highlight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,52 @@ import Qt.labs.platform as Platform
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string toolContent: ""
|
||||
property string toolName: ""
|
||||
property string toolResult: ""
|
||||
property string toolKind: ""
|
||||
property string toolStatus: ""
|
||||
property var toolDetails: ({})
|
||||
property bool expanded: false
|
||||
|
||||
property alias headerOpacity: headerRow.opacity
|
||||
|
||||
readonly property int firstNewline: toolContent.indexOf('\n')
|
||||
readonly property string toolName: firstNewline > 0 ? toolContent.substring(0, firstNewline) : toolContent
|
||||
readonly property string toolResult: firstNewline > 0 ? toolContent.substring(firstNewline + 1) : ""
|
||||
readonly property int legacyNewline: toolName === "" ? toolResult.indexOf('\n') : -1
|
||||
readonly property string headerName: {
|
||||
if (toolName !== "")
|
||||
return toolName;
|
||||
return legacyNewline > 0 ? toolResult.substring(0, legacyNewline) : toolResult;
|
||||
}
|
||||
readonly property string resultText: {
|
||||
if (toolName !== "")
|
||||
return toolResult;
|
||||
return legacyNewline > 0 ? toolResult.substring(legacyNewline + 1) : "";
|
||||
}
|
||||
|
||||
readonly property var locations: (toolDetails && toolDetails.locations) ? toolDetails.locations : []
|
||||
readonly property var diffs: (toolDetails && toolDetails.diffs) ? toolDetails.diffs : []
|
||||
|
||||
readonly property bool isRunning: toolStatus === "pending" || toolStatus === "in_progress"
|
||||
readonly property bool hasFailed: toolStatus === "failed"
|
||||
|
||||
readonly property color statusColor: {
|
||||
if (hasFailed)
|
||||
return Qt.rgba(0.8, 0.2, 0.2, 0.9);
|
||||
if (isRunning)
|
||||
return palette.highlight;
|
||||
if (toolStatus === "completed")
|
||||
return Qt.rgba(0.2, 0.8, 0.2, 0.9);
|
||||
return palette.mid;
|
||||
}
|
||||
|
||||
readonly property string statusMarker: {
|
||||
if (hasFailed)
|
||||
return "✗";
|
||||
if (toolStatus === "completed")
|
||||
return "✓";
|
||||
if (isRunning)
|
||||
return "…";
|
||||
return "";
|
||||
}
|
||||
|
||||
radius: 6
|
||||
color: palette.base
|
||||
@@ -45,10 +83,26 @@ Rectangle {
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: qsTr("Tool: %1").arg(root.toolName)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.statusMarker
|
||||
textFormat: Text.PlainText
|
||||
visible: text.length > 0
|
||||
font.pixelSize: 12
|
||||
color: root.statusColor
|
||||
}
|
||||
|
||||
Text {
|
||||
id: headerTitle
|
||||
|
||||
width: headerRow.width - x - 30
|
||||
text: root.toolKind.length > 0
|
||||
? root.toolKind + ": " + root.headerName
|
||||
: qsTr("Tool: %1").arg(root.headerName)
|
||||
textFormat: Text.PlainText
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
color: root.hasFailed ? root.statusColor : palette.text
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
@@ -60,8 +114,8 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
@@ -69,20 +123,88 @@ Rectangle {
|
||||
top: header.bottom
|
||||
margins: 10
|
||||
}
|
||||
spacing: 8
|
||||
active: root.expanded
|
||||
sourceComponent: contentComponent
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: resultText
|
||||
Component {
|
||||
id: contentComponent
|
||||
|
||||
width: parent.width
|
||||
text: root.toolResult
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
selectionColor: palette.highlight
|
||||
Column {
|
||||
property alias resultEditor: resultEditorItem
|
||||
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: root.locations
|
||||
|
||||
delegate: Text {
|
||||
id: locationEntry
|
||||
|
||||
required property var modelData
|
||||
|
||||
width: contentLoader.width
|
||||
text: locationEntry.modelData.line !== undefined
|
||||
? "→ " + locationEntry.modelData.path + ":" + locationEntry.modelData.line
|
||||
: "→ " + locationEntry.modelData.path
|
||||
textFormat: Text.PlainText
|
||||
color: palette.placeholderText
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.diffs
|
||||
|
||||
delegate: Column {
|
||||
id: diffEntry
|
||||
|
||||
required property var modelData
|
||||
|
||||
width: contentLoader.width
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: qsTr("Diff") + ": " + (diffEntry.modelData.path || "")
|
||||
textFormat: Text.PlainText
|
||||
color: palette.text
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
width: parent.width
|
||||
text: (diffEntry.modelData.oldText || "") + "\n" + (diffEntry.modelData.newText || "")
|
||||
textFormat: TextEdit.PlainText
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: resultEditorItem
|
||||
|
||||
width: parent.width
|
||||
visible: text.length > 0
|
||||
text: root.resultText
|
||||
textFormat: TextEdit.PlainText
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: palette.text
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,14 +220,14 @@ Rectangle {
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: resultText.selectedText.length > 0
|
||||
onTriggered: resultText.copy()
|
||||
enabled: contentLoader.item && contentLoader.item.resultEditor.selectedText.length > 0
|
||||
onTriggered: contentLoader.item.resultEditor.copy()
|
||||
}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Select All")
|
||||
enabled: resultText.text.length > 0
|
||||
onTriggered: resultText.selectAll()
|
||||
enabled: contentLoader.item && contentLoader.item.resultEditor.text.length > 0
|
||||
onTriggered: contentLoader.item.resultEditor.selectAll()
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
@@ -140,7 +262,7 @@ Rectangle {
|
||||
when: root.expanded
|
||||
PropertyChanges {
|
||||
target: root
|
||||
implicitHeight: header.height + contentColumn.height + 20
|
||||
implicitHeight: header.height + contentLoader.height + 20
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -12,15 +12,15 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property alias sendButton: sendButtonId
|
||||
property alias syncOpenFiles: syncOpenFilesId
|
||||
property alias attachFiles: attachFilesId
|
||||
property alias attachImages: attachImagesId
|
||||
property alias linkFiles: linkFilesId
|
||||
property alias compressButton: compressButtonId
|
||||
property alias compressTooltip: compressTooltipId
|
||||
property alias cancelCompressButton: cancelCompressButtonId
|
||||
|
||||
property bool isCompressing: false
|
||||
property bool isProcessing: false
|
||||
property bool agentMode: false
|
||||
property alias sendButtonTooltip: sendButtonTooltipId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
@@ -72,33 +72,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: linkFilesId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
visible: linkFilesId.hovered
|
||||
delay: 250
|
||||
text: qsTr("Link file to context")
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: syncOpenFilesId
|
||||
|
||||
text: qsTr("Sync open files")
|
||||
|
||||
QoAToolTip {
|
||||
visible: syncOpenFilesId.hovered
|
||||
text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
@@ -119,7 +92,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Compressing...")
|
||||
text: root.agentMode ? qsTr("Preparing summary...") : qsTr("Compressing...")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: palette.text
|
||||
font.pixelSize: 12
|
||||
@@ -134,7 +107,7 @@ Rectangle {
|
||||
QoAToolTip {
|
||||
visible: cancelCompressButtonId.hovered
|
||||
delay: 250
|
||||
text: qsTr("Cancel compression")
|
||||
text: root.agentMode ? qsTr("Cancel the summary") : qsTr("Cancel compression")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +116,7 @@ Rectangle {
|
||||
id: compressButtonId
|
||||
|
||||
visible: !root.isCompressing
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
text: qsTr("Compress")
|
||||
|
||||
icon {
|
||||
@@ -152,9 +126,10 @@ Rectangle {
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
id: compressTooltipId
|
||||
|
||||
visible: compressButtonId.hovered
|
||||
delay: 250
|
||||
text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,8 @@ Popup {
|
||||
id: root
|
||||
|
||||
property string baseSystemPrompt
|
||||
property string currentAgentRole
|
||||
property string currentAgentRoleDescription
|
||||
property string currentAgentRoleSystemPrompt
|
||||
property var activeRules
|
||||
property int activeRulesCount
|
||||
property string selectedRuleContent
|
||||
|
||||
signal openSettings()
|
||||
signal openAgentRolesSettings()
|
||||
signal openRulesFolder()
|
||||
signal refreshRules()
|
||||
signal ruleSelected(int index)
|
||||
|
||||
modal: true
|
||||
focus: true
|
||||
@@ -59,11 +49,6 @@ Popup {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Refresh")
|
||||
onClicked: root.refreshRules()
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Close")
|
||||
onClicked: root.close()
|
||||
@@ -159,281 +144,6 @@ Popup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: agentRoleSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Agent Role")
|
||||
badge: root.currentAgentRole
|
||||
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
|
||||
|
||||
sectionContent: ColumnLayout {
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.currentAgentRoleDescription
|
||||
font.pixelSize: 11
|
||||
font.italic: true
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
visible: root.currentAgentRoleDescription.length > 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
visible: root.currentAgentRoleSystemPrompt.length > 0
|
||||
|
||||
Flickable {
|
||||
id: agentPromptFlickable
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
contentHeight: agentPromptText.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
TextEdit {
|
||||
id: agentPromptText
|
||||
|
||||
width: agentPromptFlickable.width
|
||||
text: root.currentAgentRoleSystemPrompt
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
color: palette.text
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("No role selected. Using base system prompt only.")
|
||||
font.pixelSize: 11
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
visible: root.currentAgentRoleSystemPrompt.length === 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Copy")
|
||||
enabled: root.currentAgentRoleSystemPrompt.length > 0
|
||||
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Manage Roles")
|
||||
onClicked: {
|
||||
root.openAgentRolesSettings()
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: projectRulesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Project Rules")
|
||||
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
|
||||
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
|
||||
|
||||
sectionContent: ColumnLayout {
|
||||
spacing: 8
|
||||
|
||||
SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 220
|
||||
orientation: Qt.Horizontal
|
||||
visible: root.activeRulesCount > 0
|
||||
|
||||
Rectangle {
|
||||
SplitView.minimumWidth: 120
|
||||
SplitView.preferredWidth: 180
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Rules (%1)").arg(rulesList.count)
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: rulesList
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: root.activeRules
|
||||
currentIndex: 0
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
delegate: ItemDelegate {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
height: ruleItemContent.implicitHeight + 8
|
||||
highlighted: ListView.isCurrentItem
|
||||
|
||||
background: Rectangle {
|
||||
color: {
|
||||
if (parent.highlighted)
|
||||
return palette.highlight
|
||||
if (parent.hovered)
|
||||
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
||||
return "transparent"
|
||||
}
|
||||
radius: 2
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: ruleItemContent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: modelData.fileName
|
||||
font.pixelSize: 10
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.category
|
||||
font.pixelSize: 9
|
||||
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
rulesList.currentIndex = index
|
||||
root.ruleSelected(index)
|
||||
}
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumWidth: 200
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 2
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 5
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: qsTr("Content")
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Copy")
|
||||
enabled: root.selectedRuleContent.length > 0
|
||||
onClicked: utils.copyToClipboard(root.selectedRuleContent)
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: ruleContentFlickable
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
contentHeight: ruleContentArea.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
TextEdit {
|
||||
id: ruleContentArea
|
||||
|
||||
width: ruleContentFlickable.width
|
||||
text: root.selectedRuleContent
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
|
||||
font.pixelSize: 11
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
visible: root.activeRulesCount === 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
QoAButton {
|
||||
text: qsTr("Open Rules Folder")
|
||||
onClicked: root.openRulesFolder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||
@@ -448,7 +158,7 @@ Popup {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
|
||||
text: qsTr("Final prompt: Base System Prompt + Project Info + Skills")
|
||||
font.pixelSize: 9
|
||||
color: palette.mid
|
||||
wrapMode: Text.WordWrap
|
||||
@@ -534,10 +244,4 @@ Popup {
|
||||
active: sectionRoot.expanded
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
if (root.activeRulesCount > 0) {
|
||||
root.ruleSelected(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
148
sources/ChatView/qml/controls/DropZone.qml
Normal file
148
sources/ChatView/qml/controls/DropZone.qml
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal filesDropped(var urlStrings)
|
||||
|
||||
property int filesCount: 0
|
||||
property bool isDragActive: false
|
||||
|
||||
Item {
|
||||
id: dropOverlay
|
||||
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
z: 999
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: 30
|
||||
}
|
||||
width: fileCountText.width + 40
|
||||
height: 50
|
||||
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
|
||||
radius: 25
|
||||
visible: root.filesCount > 0
|
||||
|
||||
Text {
|
||||
id: fileCountText
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("%n file(s) to drop", "", root.filesCount)
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: palette.highlightedText
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.isDragActive
|
||||
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
|
||||
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
|
||||
border.width: root.isDragActive ? 3 : 2
|
||||
border.color: root.isDragActive
|
||||
? palette.highlight
|
||||
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 15
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Attach")
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: root.isDragActive ? palette.highlightedText : palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Images & Text Files")
|
||||
font.pixelSize: 14
|
||||
color: root.isDragActive ? palette.highlightedText : palette.text
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: globalDropArea
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onEntered: (drag) => {
|
||||
if (drag.hasUrls) {
|
||||
root.isDragActive = true
|
||||
root.filesCount = drag.urls.length
|
||||
dropOverlay.visible = true
|
||||
dropOverlay.opacity = 1
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
root.isDragActive = false
|
||||
root.filesCount = 0
|
||||
dropOverlay.opacity = 0
|
||||
|
||||
Qt.callLater(function() {
|
||||
if (!root.isDragActive) {
|
||||
dropOverlay.visible = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onDropped: (drop) => {
|
||||
root.isDragActive = false
|
||||
root.filesCount = 0
|
||||
dropOverlay.opacity = 0
|
||||
|
||||
Qt.callLater(function() {
|
||||
dropOverlay.visible = false
|
||||
})
|
||||
|
||||
if (!drop.hasUrls || drop.urls.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
var urlStrings = []
|
||||
for (var i = 0; i < drop.urls.length; i++) {
|
||||
var urlString = drop.urls[i].toString()
|
||||
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
|
||||
urlStrings.push(urlString)
|
||||
}
|
||||
}
|
||||
|
||||
if (urlStrings.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
drop.accept(Qt.CopyAction)
|
||||
|
||||
root.filesDropped(urlStrings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import QtQuick.Layouts
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// Object exposing Q_INVOKABLE QVariantList searchSkills(query).
|
||||
property var skillProvider: null
|
||||
property var searchResults: []
|
||||
property int currentIndex: 0
|
||||
@@ -25,7 +24,7 @@ Rectangle {
|
||||
radius: 4
|
||||
|
||||
function updateSearch(query) {
|
||||
searchResults = skillProvider ? skillProvider.searchSkills(query) : []
|
||||
searchResults = skillProvider ? skillProvider.searchSlashCommands(query) : []
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
@@ -87,19 +86,55 @@ Rectangle {
|
||||
anchors.bottomMargin: 4
|
||||
spacing: 1
|
||||
|
||||
Text {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
text: "/" + delegateItem.modelData.name
|
||||
color: delegateItem.index === root.currentIndex
|
||||
? palette.highlightedText
|
||||
: palette.text
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "/" + delegateItem.modelData.name
|
||||
textFormat: Text.PlainText
|
||||
color: delegateItem.index === root.currentIndex
|
||||
? palette.highlightedText
|
||||
: palette.text
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: sourceBadge.text.length > 0
|
||||
implicitWidth: Math.min(sourceBadge.implicitWidth + 10, 140)
|
||||
implicitHeight: sourceBadge.implicitHeight + 2
|
||||
radius: height / 2
|
||||
color: delegateItem.index === root.currentIndex
|
||||
? Qt.rgba(palette.highlightedText.r,
|
||||
palette.highlightedText.g,
|
||||
palette.highlightedText.b, 0.2)
|
||||
: palette.alternateBase
|
||||
|
||||
Text {
|
||||
id: sourceBadge
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 5
|
||||
anchors.rightMargin: 5
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: delegateItem.modelData.source || ""
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
color: delegateItem.index === root.currentIndex
|
||||
? palette.highlightedText
|
||||
: palette.mid
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: delegateItem.modelData.description
|
||||
textFormat: Text.PlainText
|
||||
color: delegateItem.index === root.currentIndex
|
||||
? Qt.rgba(palette.highlightedText.r,
|
||||
palette.highlightedText.g,
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal filesDroppedToAttach(var urlStrings)
|
||||
signal filesDroppedToLink(var urlStrings)
|
||||
|
||||
property string activeZone: ""
|
||||
property int filesCount: 0
|
||||
property bool isDragActive: false
|
||||
|
||||
Item {
|
||||
id: splitDropOverlay
|
||||
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
z: 999
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: 30
|
||||
}
|
||||
width: fileCountText.width + 40
|
||||
height: 50
|
||||
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
|
||||
radius: 25
|
||||
visible: root.filesCount > 0
|
||||
|
||||
Text {
|
||||
id: fileCountText
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("%n file(s) to drop", "", root.filesCount)
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: palette.highlightedText
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: leftZone
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: parent.width / 2
|
||||
color: root.activeZone === "left"
|
||||
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
|
||||
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
|
||||
border.width: root.activeZone === "left" ? 3 : 2
|
||||
border.color: root.activeZone === "left"
|
||||
? palette.highlight
|
||||
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 15
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Attach")
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Images & Text Files")
|
||||
font.pixelSize: 14
|
||||
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("(for one-time use)")
|
||||
font.pixelSize: 12
|
||||
font.italic: true
|
||||
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rightZone
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: parent.width / 2
|
||||
color: root.activeZone === "right"
|
||||
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
|
||||
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
|
||||
border.width: root.activeZone === "right" ? 3 : 2
|
||||
border.color: root.activeZone === "right"
|
||||
? palette.highlight
|
||||
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 15
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("LINK")
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Text Files")
|
||||
font.pixelSize: 14
|
||||
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("(added to context)")
|
||||
font.pixelSize: 12
|
||||
font.italic: true
|
||||
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: 2
|
||||
color: palette.mid
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: leftDropArea
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: parent.width / 2
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
root.activeZone = "left"
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rightDropArea
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: parent.width / 2
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
root.activeZone = "right"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: globalDropArea
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onEntered: (drag) => {
|
||||
if (drag.hasUrls) {
|
||||
root.isDragActive = true
|
||||
root.filesCount = drag.urls.length
|
||||
splitDropOverlay.visible = true
|
||||
splitDropOverlay.opacity = 1
|
||||
root.activeZone = ""
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
root.isDragActive = false
|
||||
root.filesCount = 0
|
||||
splitDropOverlay.opacity = 0
|
||||
|
||||
Qt.callLater(function() {
|
||||
if (!root.isDragActive) {
|
||||
splitDropOverlay.visible = false
|
||||
root.activeZone = ""
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onPositionChanged: (drag) => {
|
||||
if (drag.hasUrls) {
|
||||
root.activeZone = drag.x < globalDropArea.width / 2 ? "left" : "right"
|
||||
}
|
||||
}
|
||||
|
||||
onDropped: (drop) => {
|
||||
const targetZone = root.activeZone
|
||||
root.isDragActive = false
|
||||
root.filesCount = 0
|
||||
splitDropOverlay.opacity = 0
|
||||
|
||||
Qt.callLater(function() {
|
||||
splitDropOverlay.visible = false
|
||||
root.activeZone = ""
|
||||
})
|
||||
|
||||
if (!drop.hasUrls || drop.urls.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
var urlStrings = []
|
||||
for (var i = 0; i < drop.urls.length; i++) {
|
||||
var urlString = drop.urls[i].toString()
|
||||
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
|
||||
urlStrings.push(urlString)
|
||||
}
|
||||
}
|
||||
|
||||
if (urlStrings.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
drop.accept(Qt.CopyAction)
|
||||
|
||||
if (targetZone === "right") {
|
||||
root.filesDroppedToLink(urlStrings)
|
||||
} else {
|
||||
root.filesDroppedToAttach(urlStrings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,8 @@ Rectangle {
|
||||
property alias pinButton: pinButtonId
|
||||
property alias relocateButton: relocateButtonId
|
||||
property alias contextButton: contextButtonId
|
||||
property alias toolsButton: toolsButtonId
|
||||
property alias thinkingMode: thinkingModeId
|
||||
property alias settingsButton: settingsButtonId
|
||||
property alias configSelector: configSelectorId
|
||||
property alias roleSelector: roleSelector
|
||||
property alias relocateTooltip: relocateTooltipId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
@@ -147,82 +144,11 @@ Rectangle {
|
||||
text: qsTr("Switch saved AI configuration")
|
||||
}
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: roleSelector
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
model: []
|
||||
currentIndex: 0
|
||||
|
||||
QoAToolTip {
|
||||
visible: roleSelector.hovered
|
||||
delay: 250
|
||||
text: qsTr("Switch agent role (different system prompts)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: toolsButtonId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
checkable: true
|
||||
opacity: enabled ? 1.0 : 0.2
|
||||
|
||||
icon {
|
||||
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
visible: toolsButtonId.hovered
|
||||
delay: 250
|
||||
text: {
|
||||
if (!toolsButtonId.enabled) {
|
||||
return qsTr("Tools are disabled in General Settings")
|
||||
}
|
||||
return toolsButtonId.checked
|
||||
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
||||
: qsTr("Tools disabled: 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
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
visible: thinkingModeId.hovered
|
||||
delay: 250
|
||||
text: thinkingModeId.enabled
|
||||
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
|
||||
: qsTr("Thinking Mode disabled"))
|
||||
: qsTr("Thinking Mode is not available for this provider")
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: settingsButtonId
|
||||
|
||||
@@ -345,7 +271,7 @@ Rectangle {
|
||||
QoAToolTip {
|
||||
visible: contextButtonId.hovered
|
||||
delay: 250
|
||||
text: qsTr("View chat context (system prompt, role, rules)")
|
||||
text: qsTr("View chat context (system prompt)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user