refactor: archive GUI clients; keep minimal TUI with hooks/API
Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
// The transcript: cards for the messages, a card per tool call, folded
|
||||
// thinking blocks and one-line notes. Follows the tail of the conversation
|
||||
// until the user scrolls away from it.
|
||||
Item {
|
||||
id: view
|
||||
property var model: null
|
||||
property bool running: false
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
objectName: "transcript"
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.gapL
|
||||
anchors.rightMargin: Theme.gapL
|
||||
model: view.model
|
||||
clip: true
|
||||
spacing: Theme.gap
|
||||
topMargin: Theme.gapL
|
||||
bottomMargin: Theme.gapL
|
||||
cacheBuffer: 2000
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
// The delegate picks the card by row kind; the components below read
|
||||
// the ListModel roles directly, which is why they are declared in the
|
||||
// delegate's scope.
|
||||
delegate: Loader {
|
||||
id: rowLoader
|
||||
width: list.width
|
||||
sourceComponent: model.kind === "tool" ? toolComp
|
||||
: model.kind === "think" ? thinkComp
|
||||
: model.kind === "note" ? noteComp : msgComp
|
||||
|
||||
Component {
|
||||
id: msgComp
|
||||
MessageCard {
|
||||
role: model.role
|
||||
text: model.text
|
||||
complete: model.complete
|
||||
images: model.images !== undefined ? model.images : ""
|
||||
/* html comes from the streaming renderer (Main.qml) or,
|
||||
* for a replayed session, from here once. */
|
||||
html: model.html.length > 0 ? model.html
|
||||
: (model.role === "agent" || model.role === "error") &&
|
||||
model.complete && model.text.length > 0
|
||||
? agent.renderMarkdown(model.text) : ""
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: thinkComp
|
||||
ThinkBlock {
|
||||
text: model.text
|
||||
complete: model.complete
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: toolComp
|
||||
ToolCard {
|
||||
objectName: "toolCard"
|
||||
name: model.name
|
||||
state: model.state
|
||||
detail: model.detail
|
||||
output: model.output
|
||||
exitCode: model.exitCode
|
||||
hasResult: model.complete
|
||||
images: model.images !== undefined ? model.images : ""
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: noteComp
|
||||
NoteLine {
|
||||
text: model.text
|
||||
level: model.state === "warn" || model.state === "error" ? model.state : "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMovementEnded: view.stickBottom = atYEnd
|
||||
onCountChanged: if (view.stickBottom) Qt.callLater(list.positionViewAtEnd)
|
||||
onContentHeightChanged: if (view.stickBottom) Qt.callLater(list.positionViewAtEnd)
|
||||
}
|
||||
|
||||
// Follow the tail unless the user asked otherwise.
|
||||
property bool stickBottom: true
|
||||
|
||||
// Empty state: what this window is and what to do with it.
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width - 80, 520)
|
||||
spacing: Theme.gap
|
||||
visible: view.model ? view.model.count === 0 : true
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "CST-agent"
|
||||
color: Theme.text
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "The engine runs in this window — no server, no second process."
|
||||
color: Theme.textDim
|
||||
font.pixelSize: Theme.bodySize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Theme.gap
|
||||
text: "Ask for a change, a command to run, a file to explain.\n" +
|
||||
"Every write, delete and command asks first: Deny / Once / Always.\n" +
|
||||
"Enter sends, Shift+Enter adds a line, Esc stops a running turn."
|
||||
color: Theme.textFaint
|
||||
font.pixelSize: Theme.smallSize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Jump back to the live end of the conversation.
|
||||
RoundButton {
|
||||
id: jump
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.padL
|
||||
width: Theme.jumpButton
|
||||
height: Theme.jumpButton
|
||||
visible: !view.stickBottom && list.contentHeight > list.height
|
||||
contentItem: ThemeIcon {
|
||||
name: "go-down"
|
||||
glyph: "↓"
|
||||
size: 20
|
||||
color: Theme.text
|
||||
}
|
||||
background: Rectangle {
|
||||
radius: width / 2
|
||||
color: jump.hovered ? Theme.surfaceHi : Theme.surfaceAlt
|
||||
border.width: 1
|
||||
border.color: Theme.border
|
||||
}
|
||||
onClicked: {
|
||||
view.stickBottom = true
|
||||
list.positionViewAtEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user