Files
CST-agent/app/qml/MessageCard.qml
T
WHC 3fd039cb51 v0.8.14: icons from the desktop's theme (Breeze), readable text, composer frame
The window drew its icons with unicode glyphs -- a clip emoji for attach, a
gear, a hamburger, a chevron, a multiplication sign for delete. Those render
in whichever font happens to cover them (colour emoji in a grey toolbar) and
are a box on a machine without that glyph. They now come from the icon theme,
the way every other desktop app gets them.

Icons
- app/src/ui/ThemeIcons.{h,cpp}: a QQuickImageProvider for
  image://csttheme/<name>?color=%23rrggbb -- QIcon::fromTheme() in the
  desktop's theme (whatever the platform theme set, else breeze-dark, breeze,
  hicolor), tinted with QPainter's SourceIn and rasterised at the requested
  size and the screen's device pixel ratio. Qt only builds its theme search
  paths from XDG once a theme name is set, so the provider fills them itself
  (GenericDataLocation/icons, ~/.icons and, on NixOS,
  /run/current-system/sw/share/icons, for a shell started outside the desktop
  session). An unknown name yields a null image, which QML reports as
  Image.Error.
- app/qml/ThemeIcon.qml: the one component that asks for an icon. Every use
  still names the old character as `glyph:`, so a host with no icon theme
  degrades to exactly what the window looked like before instead of going
  blank. installThemeIcons() is called by main() and by --uitest, so the test
  loads the same icons the app does.
- Replaced: nav (application-menu), settings (settings-configure), attach
  (mail-attachment), copy (edit-copy), session delete (edit-delete), new
  session (list-add), expand/collapse (arrow-right/arrow-down), jump to the end
  (go-down), the plan checklist (task-complete/box). A tool card also shows
  what kind of tool ran -- Theme.toolIcon(): pencil for patch/write, text page
  for read, folder for list/glob, magnifier for grep, braces for a shell,
  globe for web, list for a plan. Toolbar icons are 22 px (Breeze's toolbar
  size), card and row icons 14-16 px.

Colours (the "some text is unreadable" part)
- textFaint #6d757b -> #8d959c: ~3.4:1 -> ~5:1 on the card fill. It carries
  hints, provider URLs, session metadata and placeholders, so it is text, not
  decoration.
- A selected provider row and the open session row write on the accent fill;
  they now use accentText / accentTextDim / okOnAccent instead of faint grey
  and green on blue (named "accentText", not "onAccent": QML reads any `onFoo`
  property as a signal handler and refuses the assignment).
- The filled buttons (Send, Use this) take their ink from Theme.accentInk.

Composer
- The frame is one Theme.pad around its column on every side (anchors.fill,
  implicitHeight + 2*pad). With the top margin alone the last row -- the clip
  and Send -- hung over the lower border, which is what put the clip on the
  frame instead of inside it.

app/README.md gains an "Icons, colors" section (the provider, why the theme
search paths are filled by hand, the qtsvg requirement for rasterising Breeze,
the tool map, and the contrast rules).

Verified: --uitest 52/52, --selftest OK, and the 0.8.14 window relaunched in
the Plasma session (no QML warnings).

Version 0.8.14.

mode = coding
2026-10-11 12:53:49 +08:00

110 lines
3.4 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
// One message in the transcript: the prompt, the answer, or an error. The
// answer arrives as plain text while it streams and is rendered from markdown
// (by the engine, through AgentCore::renderMarkdown) once it is complete.
Rectangle {
id: card
property string role: "agent" // user | agent | error
property string text: ""
property string html: ""
property bool complete: true
radius: Theme.radius
color: role === "user" ? Theme.surfaceAlt : Theme.surface
border.width: 1
border.color: role === "error" ? Theme.err
: role === "user" ? Theme.border : Theme.border
implicitHeight: layout.implicitHeight + Theme.pad * 2
ColumnLayout {
id: layout
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: Theme.pad
}
spacing: Theme.gap
RowLayout {
spacing: Theme.gap
Rectangle {
width: 6
height: 6
radius: 3
color: card.role === "user" ? Theme.accent
: card.role === "error" ? Theme.err : Theme.ok
}
Text {
text: card.role === "user" ? "You"
: card.role === "error" ? "Error" : "CST-agent"
color: Theme.textDim
font.pixelSize: Theme.smallSize
font.bold: true
}
Text {
visible: !card.complete
text: "streaming…"
color: Theme.textFaint
font.pixelSize: Theme.smallSize
}
Item { Layout.fillWidth: true }
ToolButton {
implicitWidth: 26
implicitHeight: 22
visible: card.text.length > 0
ToolTip.visible: hovered
ToolTip.text: "Copy"
contentItem: ThemeIcon {
name: "edit-copy"
glyph: "⧉"
size: 14
color: parent.hovered ? Theme.text : Theme.textFaint
}
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered ? Theme.surfaceHi : "transparent"
}
onClicked: {
plain.text = card.text
plain.selectAll()
plain.copy()
}
}
}
// Hidden holder so "copy" always has the plain text, rtf or not.
TextEdit {
id: plain
visible: false
}
TextEdit {
id: streamText
visible: card.html.length === 0
Layout.fillWidth: true
text: card.text
readOnly: true
selectByMouse: true
wrapMode: TextEdit.Wrap
color: card.role === "error" ? Theme.err : Theme.text
font.pixelSize: Theme.bodySize
selectionColor: Theme.accentDim
}
Text {
visible: card.html.length > 0
Layout.fillWidth: true
text: card.html
textFormat: Text.RichText
color: Theme.text
font.pixelSize: Theme.bodySize
wrapMode: Text.Wrap
onLinkActivated: function (link) { Qt.openUrlExternally(link) }
}
}
}