Watch
1
0
Fork
You've already forked souveraine
0

shell: own the agent message delegate and the reasoning card

TASK-72 step 2. AgentMessage consumes the typed segment list and dispatches
to owned primitives; ThinkingCard draws reasoning as a segment kind rather
than as <think> punctuation inside a flattened string, which removes the
think-fence collision class by construction.

MessageTextBlock and MessageCodeBlock are deliberately reused from the vendor
snapshot: they are markdown renderers, not agent vocabulary.

Composed by MANIFEST, imported by nothing. The activating edit is queued as
patch 0008. Qt6 qmllint: zero syntax findings; remaining warnings are the
widget-resolution class the vendor AiMessage also produces (38 vs 46).

Not verified: nothing has loaded this. No real tool round has rendered here.
This commit is contained in:
Souveraine 2026-08-12 13:11:19 -04:00
commit 6d0f891d6e
5 changed files with 375 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,41 @@
From f76c911a2f37352b0846b2ec70a96126ac275ee2 Mon Sep 17 00:00:00 2001
From: Souveraine <souveraine@wiuf.net>
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