import QtQuick import QtQuick.Controls import QtQuick.Layouts // The images a message or a tool call carries: thumbnails from the session's // own store (image://cstattach/, app/src/ui/SessionImages.cpp). The names are // the ones the engine wrote next to the session -- view_image, an MCP image, a // picture the user attached -- never a path the view invented. // // One row of thumbnails: the height is capped so a screenshot cannot push the // conversation off the screen, the width follows the picture's shape. Flow { id: strip objectName: "imageStrip" /* One line per picture, written by the engine side (AgentCore.cpp: * "|||x"). A row carries this as text * because a ListModel role cannot hold a list. */ property string text: "" property var images: decode(text) spacing: 6 function decode(t) { const out = [] const lines = String(t).split("\n") for (let i = 0; i < lines.length; i++) { if (!lines[i].length) continue const f = lines[i].split("|") if (!f[0].length) continue let w = 0, h = 0 const dim = String(f[3] || "").split("x") if (dim.length === 2) { w = parseInt(dim[0], 10) || 0 h = parseInt(dim[1], 10) || 0 } out.push({ url: "image://cstattach/" + f[0], rel: f[0], name: f[1] || f[0], origin: f[2] || "", w: w, h: h }) } return out } Repeater { model: strip.images delegate: Rectangle { id: thumb required property var modelData required property int index readonly property real ratio: { const w = Number(modelData.w), h = Number(modelData.h) return (w > 0 && h > 0) ? w / h : 1.5 } readonly property int cap: 150 // the tallest a thumbnail gets objectName: "imageThumb" + index width: Math.max(56, Math.min(Math.round(cap * ratio), 320)) height: Math.max(40, Math.min(cap, Math.round(width / ratio))) radius: Theme.radiusSm color: Theme.bg border.width: 1 border.color: mouse.containsMouse ? Theme.accentDim : Theme.border clip: true Image { id: pic anchors.fill: parent anchors.margins: 2 source: String(thumb.modelData.url) sourceSize.width: 512 sourceSize.height: 512 fillMode: Image.PreserveAspectFit /* Not asynchronous: the provider already hands back a * thumbnail-sized picture, so there is nothing to wait for. */ asynchronous: false visible: status === Image.Ready } // A file the store no longer has is not worth shouting about: the // frame says which image it was. Text { anchors.centerIn: parent width: parent.width - 8 visible: pic.status === Image.Error text: String(thumb.modelData.name) elide: Text.ElideMiddle horizontalAlignment: Text.AlignHCenter color: Theme.textFaint font.pixelSize: Theme.smallSize } MouseArea { id: mouse anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton } ToolTip.visible: mouse.containsMouse ToolTip.delay: 400 ToolTip.text: { const m = thumb.modelData let s = String(m.name) if (m.w > 0 && m.h > 0) s += " · " + m.w + "×" + m.h if (m.origin && m.origin !== "user") s += " · " + m.origin return s } } } }