1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
import QtQuick
import ".."
// One turn: the question, then the reply with its thinking line, the tool
// chips, and an error or stopped mark. Both texts go through Segments, so
// the question renders as markdown too.
Column {
id: msg
required property int index
required property string user
required property string reply
required property string think
required property string tools
required property string files // JSON: [{name, kind, pages, truncated}]
required property string error
required property bool live
required property bool stopped
required property bool expanded
required property var model // the row, for setting expanded
property string running: "" // the tool running now, if any
spacing: 6
Rectangle {
width: msg.width
height: question.height + 16
radius: 8
color: Qt.alpha(Theme.surface, 0.7)
Segments {
id: question
anchors { left: parent.left; right: parent.right; top: parent.top; margins: 8 }
text: msg.user
}
}
// The files that went with the question, as the daemon read them.
Flow {
width: msg.width
spacing: 6
visible: msg.files !== "[]"
Repeater {
model: JSON.parse(msg.files)
Rectangle {
required property var modelData
width: f.implicitWidth + 16
height: f.implicitHeight + 6
radius: height / 2
color: Qt.alpha(Theme.surface, 0.6)
Text {
id: f
anchors.centerIn: parent
text: parent.modelData.name
+ (parent.modelData.pages ? ` · ${parent.modelData.pages} pages` : "")
+ (parent.modelData.truncated ? " · truncated" : "")
color: parent.modelData.truncated ? Theme.yellow : Theme.subtext
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
}
}
}
}
// Collapsed by default; a click shows the reasoning as plain text.
Text {
visible: msg.think !== ""
text: (msg.expanded ? "▾ " : "▸ ") + (msg.live && msg.reply === "" ? "thinking…" : "thinking")
color: Theme.overlay
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: msg.model.expanded = !msg.expanded
}
}
Text {
width: msg.width
visible: msg.expanded && msg.think !== ""
text: msg.think
textFormat: Text.PlainText
wrapMode: Text.Wrap
color: Theme.subtext
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
}
Flow {
width: msg.width
spacing: 6
visible: msg.tools !== ""
Repeater {
model: msg.tools ? msg.tools.split(",") : []
Rectangle {
required property string modelData
readonly property bool lit: msg.live && msg.running === modelData
property int elapsed: 0
width: chip.implicitWidth + 14
height: chip.implicitHeight + 6
radius: height / 2
color: Qt.alpha(Theme.surface, 0.9)
opacity: lit ? 1 : 0.55
// A lit chip alone looks frozen on a clone that takes a
// minute, so the running tool counts seconds.
onLitChanged: if (!lit) elapsed = 0
Timer {
running: parent.lit
interval: 1000
repeat: true
onTriggered: parent.elapsed++
}
Text {
id: chip
anchors.centerIn: parent
text: parent.modelData + (parent.lit ? " · " + parent.elapsed + "s" : "")
color: parent.lit ? Theme.text : Theme.subtext
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 }
}
}
}
}
Segments {
width: msg.width
text: msg.reply
visible: msg.reply !== ""
}
Text {
visible: msg.error !== "" || msg.stopped
width: msg.width
text: msg.error !== "" ? msg.error : "stopped"
color: msg.error !== "" ? Theme.red : Theme.overlay
wrapMode: Text.Wrap
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
}
}
|