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

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