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:
parent
3def2920b8
commit
6d0f891d6e
5 changed files with 375 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
221
surfaces/quickshell/modules/souveraine/agent/AgentMessage.qml
Normal file
221
surfaces/quickshell/modules/souveraine/agent/AgentMessage.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
surfaces/quickshell/modules/souveraine/agent/ThinkingCard.qml
Normal file
109
surfaces/quickshell/modules/souveraine/agent/ThinkingCard.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
Loading…
Reference in a new issue