feat: add support acp in common chat (#369)

This commit is contained in:
Petr Mironychev
2026-07-21 13:10:00 +02:00
committed by GitHub
parent af85efb554
commit 442263a6a7
158 changed files with 14270 additions and 4118 deletions

View File

@@ -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 {

View 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);
}

View File

@@ -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) {

View 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)
}
}
}
}
}

View 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
}
}
}
}
}

View File

@@ -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
}
}
]

View File

@@ -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)")
}
}

View File

@@ -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)
}
}
}

View 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)
}
}
}

View File

@@ -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,

View File

@@ -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)
}
}
}
}

View File

@@ -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)")
}
}