diff --git a/surfaces/quickshell/deploy.sh b/surfaces/quickshell/deploy.sh index 8ae91bd..d3ccc62 100755 --- a/surfaces/quickshell/deploy.sh +++ b/surfaces/quickshell/deploy.sh @@ -89,6 +89,8 @@ modules/souveraine/island/AgentSessionPanel.qml souveraine/modules/souveraine/is modules/souveraine/island/qmldir souveraine/modules/souveraine/island/qmldir modules/souveraine/agent/ToolVocabulary.qml souveraine/modules/souveraine/agent/ToolVocabulary.qml modules/souveraine/agent/ToolCard.qml souveraine/modules/souveraine/agent/ToolCard.qml +modules/souveraine/agent/ThinkingCard.qml souveraine/modules/souveraine/agent/ThinkingCard.qml +modules/souveraine/agent/AgentMessage.qml souveraine/modules/souveraine/agent/AgentMessage.qml modules/souveraine/agent/qmldir souveraine/modules/souveraine/agent/qmldir services/SessionEvents.qml souveraine/services/SessionEvents.qml services/SessiondBridge.qml souveraine/services/SessiondBridge.qml diff --git a/surfaces/quickshell/modules/souveraine/agent/AgentMessage.qml b/surfaces/quickshell/modules/souveraine/agent/AgentMessage.qml new file mode 100644 index 0000000..26dd960 --- /dev/null +++ b/surfaces/quickshell/modules/souveraine/agent/AgentMessage.qml @@ -0,0 +1,221 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import qs.modules.ii.sidebarLeft.aiChat +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +/* + * One message, drawn from typed segments. + * + * Owned Souveraine surface (TASK-72 step 2). This replaces the vendor + * snapshot's AiMessage as the agent-surface delegate. + * + * The structural point: it consumes the segment list, never a flattened + * markdown blob. `services/Ai.qml` stays the data boundary — it preserves wire + * segment type and call identity and does not choose pixels; this file chooses + * pixels and reads no state. + * + * Deliberately reused from the vendor snapshot: MessageTextBlock and + * MessageCodeBlock. Those are markdown renderers, not agent vocabulary — the + * ii-base rule is that no further *agent-surface feature* lands in the vendor + * tree, not that we owe ourselves a second markdown engine. Reasoning and tool + * calls are ours because those are the parts that are about her. + * + * Known parity gaps against the vendor delegate, stated rather than hidden: + * - in-place message editing (Ctrl+S save) is not carried over; + * - the attached-file indicator is not drawn yet — it belongs with the image + * intake half of TASK-72, which has no picker yet either. + */ +Rectangle { + id: root + + property int messageIndex + property var messageData + property var messageInputField + + property real messagePadding: 7 + property real contentSpacing: 3 + property bool renderMarkdown: true + property bool enableMouseSelection: false + + // Typed segments are the contract. The markdown splitter is the fallback + // for legacy/provider messages that never carried segments at all. + readonly property var messageBlocks: { + const segments = root.messageData ? root.messageData.segments : []; + return (segments && segments.length > 0) + ? segments + : StringUtils.splitMarkdownBlocks(root.messageData?.content); + } + + readonly property bool isAssistant: (root.messageData?.role ?? "") === "assistant" + readonly property bool done: root.messageData?.done ?? false + + anchors.left: parent?.left + anchors.right: parent?.right + + // While streaming, never let the bubble shrink: markdown reflow transiently + // collapses height and jolts the auto-scroll. Floor it until done. + readonly property real naturalHeight: columnLayout.implicitHeight + root.messagePadding * 2 + property real streamingHeightFloor: 0 + implicitHeight: !root.done ? Math.max(naturalHeight, streamingHeightFloor) : naturalHeight + onNaturalHeightChanged: if (!root.done) streamingHeightFloor = Math.max(streamingHeightFloor, naturalHeight) + onMessageDataChanged: streamingHeightFloor = 0 + + radius: Appearance.rounding.normal + color: Appearance.colors.colLayer1 + + ColumnLayout { + id: columnLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: root.messagePadding + spacing: root.contentSpacing + + Rectangle { // Header + Layout.fillWidth: true + implicitHeight: headerRow.implicitHeight + 8 + radius: Appearance.rounding.small + color: Appearance.colors.colSecondaryContainer + + RowLayout { + id: headerRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 8 + + MaterialSymbol { + text: root.isAssistant ? "auto_awesome" : "person" + iconSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnSecondaryContainer + } + + StyledText { + Layout.fillWidth: true + text: root.messageData?.name ?? (root.isAssistant ? Translation.tr("Souveraine") : Translation.tr("You")) + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnSecondaryContainer + elide: Text.ElideRight + } + } + } + + Item { // Waiting, before any segment has arrived + Layout.fillWidth: true + implicitHeight: waitingLoader.shown ? waitingLoader.implicitHeight : 0 + visible: implicitHeight > 0 + + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + FadeLoader { + id: waitingLoader + anchors.centerIn: parent + shown: (root.messageBlocks.length < 1) && !root.done + sourceComponent: MaterialLoadingIndicator { + loading: true + } + } + } + + Repeater { + model: ScriptModel { + values: root.messageBlocks + } + + delegate: DelegateChooser { + role: "type" + + DelegateChoice { + roleValue: "tool" + ToolCard { + required property var modelData + segment: modelData + } + } + + DelegateChoice { + roleValue: "think" + ThinkingCard { + required property var modelData + segmentContent: modelData.content ?? "" + renderMarkdown: root.renderMarkdown + enableMouseSelection: root.enableMouseSelection + done: root.done + completed: modelData.completed ?? root.done + } + } + + DelegateChoice { + roleValue: "code" + MessageCodeBlock { + required property var modelData + renderMarkdown: root.renderMarkdown + enableMouseSelection: root.enableMouseSelection + segmentContent: modelData.content ?? "" + segmentLang: modelData.lang ?? "" + messageData: root.messageData + } + } + + DelegateChoice { + roleValue: "text" + MessageTextBlock { + required property var modelData + renderMarkdown: root.renderMarkdown + enableMouseSelection: root.enableMouseSelection + segmentContent: modelData.content ?? "" + messageData: root.messageData + done: root.done + forceDisableChunkSplitting: root.messageData?.content?.includes("```") ?? true + } + } + } + } + + Flow { // Annotations + visible: (root.messageData?.annotationSources?.length ?? 0) > 0 + spacing: 5 + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + + Repeater { + model: ScriptModel { + values: root.messageData?.annotationSources ?? [] + } + delegate: AnnotationSourceButton { + required property var modelData + displayText: modelData.text + url: modelData.url + } + } + } + + Flow { // Search queries + visible: (root.messageData?.searchQueries?.length ?? 0) > 0 + spacing: 5 + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + + Repeater { + model: ScriptModel { + values: root.messageData?.searchQueries ?? [] + } + delegate: SearchQueryButton { + required property var modelData + query: modelData + } + } + } + } +} diff --git a/surfaces/quickshell/modules/souveraine/agent/ThinkingCard.qml b/surfaces/quickshell/modules/souveraine/agent/ThinkingCard.qml new file mode 100644 index 0000000..c88c1f8 --- /dev/null +++ b/surfaces/quickshell/modules/souveraine/agent/ThinkingCard.qml @@ -0,0 +1,109 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +/* + * Reasoning, drawn as itself. + * + * Owned Souveraine surface. The thing this replaces marked reasoning with + * literal fences inside one flattened markdown string, which is how a + * think-fence collision could eat a reply (renderer-think-fence, 2026-08-11). + * A segment kind cannot collide with punctuation, so the whole class of bug + * goes away by construction rather than by escaping harder. + * + * Visual intent: quieter than speech, and quieter than any tool. Thinking is + * not addressed to anyone. It is legible when wanted and out of the way + * otherwise — open while it is still happening, folded once it is done. + */ +Item { + id: root + + property string segmentContent: "" + property bool done: false + property bool completed: false + property bool renderMarkdown: true + property bool enableMouseSelection: false + + // Open while the thought is still forming; fold it once it has landed. + // Deliberately not user-sticky yet: a fold state that survives a delegate + // recycle needs to live in the model, not here. + property bool expanded: !root.completed + + onCompletedChanged: if (root.completed) root.expanded = false + + readonly property color accent: Appearance.colors.colSubtext + + Layout.fillWidth: true + implicitHeight: body.implicitHeight + + Rectangle { + id: body + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: column.implicitHeight + 12 + radius: Appearance.rounding.small + color: Appearance.colors.colLayer2 + border.width: 1 + border.color: ColorUtils.transparentize(root.accent, 0.75) + + ColumnLayout { + id: column + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 6 + spacing: 4 + + MouseArea { + Layout.fillWidth: true + implicitHeight: header.implicitHeight + cursorShape: Qt.PointingHandCursor + onClicked: root.expanded = !root.expanded + + RowLayout { + id: header + anchors.left: parent.left + anchors.right: parent.right + spacing: 6 + + MaterialSymbol { + text: "neurology" + iconSize: Appearance.font.pixelSize.normal + color: root.accent + } + + StyledText { + Layout.fillWidth: true + text: root.completed ? Translation.tr("Thought") : Translation.tr("Thinking\u2026") + font.pixelSize: Appearance.font.pixelSize.smaller + font.italic: true + color: root.accent + elide: Text.ElideRight + } + + MaterialSymbol { + text: root.expanded ? "expand_less" : "expand_more" + iconSize: Appearance.font.pixelSize.normal + color: root.accent + } + } + } + + StyledText { + Layout.fillWidth: true + visible: root.expanded && root.segmentContent.length > 0 + text: root.segmentContent + font.pixelSize: Appearance.font.pixelSize.smaller + font.italic: true + color: Appearance.colors.colSubtext + wrapMode: Text.Wrap + textFormat: Text.PlainText + } + } + } +} diff --git a/surfaces/quickshell/modules/souveraine/agent/qmldir b/surfaces/quickshell/modules/souveraine/agent/qmldir index 7d20774..1a16f56 100644 --- a/surfaces/quickshell/modules/souveraine/agent/qmldir +++ b/surfaces/quickshell/modules/souveraine/agent/qmldir @@ -1,2 +1,4 @@ singleton ToolVocabulary 1.0 ToolVocabulary.qml ToolCard 1.0 ToolCard.qml +ThinkingCard 1.0 ThinkingCard.qml +AgentMessage 1.0 AgentMessage.qml diff --git a/surfaces/quickshell/patches/0008-agent-surface-owned-message-delegate.patch b/surfaces/quickshell/patches/0008-agent-surface-owned-message-delegate.patch new file mode 100644 index 0000000..03827e6 --- /dev/null +++ b/surfaces/quickshell/patches/0008-agent-surface-owned-message-delegate.patch @@ -0,0 +1,41 @@ +From f76c911a2f37352b0846b2ec70a96126ac275ee2 Mon Sep 17 00:00:00 2001 +From: Souveraine +Date: Wed, 12 Aug 2026 13:09:27 -0400 +Subject: [PATCH] shell: render the agent surface through the owned message + delegate + +The sidebar's message list drew through ii-base's AiMessage, so every tool +call was rendered by the vendor snapshot's ToolCallBlock, which knows 8 of +the registry's 19 tools. The 11 it cannot name are exactly the interiority +verbs. TASK-72 step 2. + +Requires modules/souveraine/agent/ composed (deploy.sh MANIFEST, already in +tree). One line reverts it. +--- + surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml +index bba12a1..0d6e04e 100644 +--- a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml ++++ b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml +@@ -6,6 +6,7 @@ import qs.modules.common.functions + import qs.modules.souveraine.subconscious + import qs.modules.ii.sidebarLeft.aiChat + import qs.modules.souveraine.island ++import qs.modules.souveraine.agent + import QtQuick + import QtQuick.Controls + import QtQuick.Layouts +@@ -446,7 +447,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) + return message?.visibleToUser ?? true; + }) + } +- delegate: AiMessage { ++ delegate: AgentMessage { + required property var modelData + required property int index + messageIndex: index +-- +2.55.0 +