Files
WHCandCursor 5538ed3412
ci / tcc-check (push) Successful in 1m8s
ci / linux (aarch64, 0) (push) Failing after 1h0m3s
ci / linux (x86, 1) (push) Failing after 1h0m3s
ci / linux (x86_64, 1) (push) Failing after 1h0m2s
ci / windows (push) Failing after 50s
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>
2026-10-30 22:49:23 +08:00

346 lines
14 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
// What the engine is set to: where it runs, how it frames itself, when it
// asks, whether it is planning. Everything here writes straight into the
// engine's config (AgentCore), never into a copy the engine cannot see.
//
// The panel scrolls (a phone-sized window cannot show it all) and every
// control keeps a finger-sized hit area.
Rectangle {
id: side
property string workspace: ""
property string provider: ""
property string model: ""
property string mode: "coding"
property int approval: 1 // cst_approval_t
property bool planMode: false
property bool running: false
property string status: ""
property string todo: ""
property var sessions: [] // what the store holds (AgentCore::sessions)
property string sessionId: ""
signal modeRequested(string mode)
signal approvalRequested(int approval)
signal planModeRequested(bool on)
signal sessionRequested(string id)
signal sessionNewRequested()
signal sessionDeleteRequested(string id)
readonly property alias contentColumn: col
color: Theme.surface
border.width: 1
border.color: Theme.border
implicitHeight: col.implicitHeight + Theme.pad * 2
// One selectable row of a segmented control.
component Seg: Button {
id: seg
property bool current: false
flat: true
implicitHeight: Theme.segmentHeight
padding: 0
background: Rectangle {
radius: Theme.radiusSm
color: seg.current ? Theme.accentDim : (seg.hovered ? Theme.surfaceAlt : "transparent")
border.width: 1
border.color: seg.current ? Theme.accent : Theme.border
}
contentItem: Text {
text: seg.text
color: seg.current ? Theme.text : Theme.textDim
font.pixelSize: Theme.smallSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Flickable {
anchors.fill: parent
contentWidth: width
contentHeight: col.implicitHeight + Theme.pad * 2
clip: true
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {}
ColumnLayout {
id: col
x: Theme.pad
y: Theme.pad
width: parent.width - Theme.pad * 2
spacing: Theme.gapL
// --- where ---
ColumnLayout {
Layout.fillWidth: true
spacing: 4
RowLayout {
spacing: Theme.gap
Rectangle { width: 8; height: 8; radius: 4; color: Theme.accent }
Text {
text: "CST-agent"
color: Theme.text
font.pixelSize: Theme.bodySize
font.bold: true
}
Item { Layout.fillWidth: true }
Text {
visible: side.running
text: "turn running"
color: Theme.warn
font.pixelSize: Theme.smallSize
}
}
Text {
Layout.fillWidth: true
text: side.workspace.length ? side.workspace : "(no workspace)"
color: Theme.textDim
font.pixelSize: Theme.smallSize
elide: Text.ElideMiddle
ToolTip.visible: wsHover.hovered
ToolTip.text: side.workspace
HoverHandler { id: wsHover }
}
Text {
Layout.fillWidth: true
visible: side.provider.length > 0 || side.model.length > 0
text: (side.provider.length ? side.provider : "?") +
(side.model.length ? " · " + side.model : "")
color: Theme.textFaint
font.pixelSize: Theme.smallSize
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
visible: side.sessionId.length > 0
text: "session " + side.sessionId
color: Theme.textFaint
font.pixelSize: Theme.smallSize
elide: Text.ElideMiddle
}
Text {
Layout.fillWidth: true
visible: side.status.length > 0
text: side.status
color: Theme.textFaint
font.pixelSize: Theme.smallSize
elide: Text.ElideRight
}
}
// --- which conversation ---
ColumnLayout {
Layout.fillWidth: true
spacing: 4
RowLayout {
Layout.fillWidth: true
spacing: Theme.gap
Text {
text: "Sessions"
color: Theme.textDim
font.pixelSize: Theme.smallSize
font.bold: true
}
Item { Layout.fillWidth: true }
// Starting a session while a turn runs would move the
// engine out from under it: the whole block is off then.
ToolButton {
id: newSession
objectName: "newSessionButton"
implicitWidth: 68
implicitHeight: Theme.segmentHeight
enabled: !side.running
ToolTip.visible: hovered
ToolTip.text: "Start a new session"
contentItem: RowLayout {
spacing: 4
ThemeIcon {
name: "list-add"
glyph: "+"
size: 14
color: newSession.enabled
? (newSession.hovered ? Theme.text : Theme.textDim)
: Theme.textFaint
}
Text {
text: "new"
color: newSession.enabled
? (newSession.hovered ? Theme.text : Theme.textDim)
: Theme.textFaint
font.pixelSize: Theme.smallSize
}
}
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered && parent.enabled ? Theme.surfaceHi
: "transparent"
border.width: 1
border.color: Theme.border
}
onClicked: side.sessionNewRequested()
}
}
Repeater {
model: side.sessions
delegate: SessionRow {
Layout.fillWidth: true
entry: modelData
onActivated: function (id) { side.sessionRequested(id) }
onRemoveRequested: function (id) { side.sessionDeleteRequested(id) }
}
}
Text {
Layout.fillWidth: true
visible: side.sessions.length === 0
text: "nothing stored here yet"
color: Theme.textFaint
font.pixelSize: Theme.smallSize
wrapMode: Text.Wrap
}
}
// --- how it frames itself ---
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Mode"; color: Theme.textDim; font.pixelSize: Theme.smallSize; font.bold: true }
RowLayout {
Layout.fillWidth: true
spacing: 4
Seg {
objectName: "modeCoding"
Layout.fillWidth: true
text: "coding"
current: side.mode !== "general"
onClicked: side.modeRequested("coding")
}
Seg {
objectName: "modeGeneral"
Layout.fillWidth: true
text: "general"
current: side.mode === "general"
onClicked: side.modeRequested("general")
}
}
Text {
Layout.fillWidth: true
text: side.mode === "general" ? "documents, data, everyday files"
: "code framed: repos, builds, tests"
color: Theme.textFaint
font.pixelSize: Theme.smallSize
wrapMode: Text.Wrap
}
}
// --- when it asks ---
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Approval"; color: Theme.textDim; font.pixelSize: Theme.smallSize; font.bold: true }
RowLayout {
Layout.fillWidth: true
spacing: 4
Seg {
Layout.fillWidth: true
text: "ask"
current: side.approval === 0
onClicked: side.approvalRequested(0)
}
Seg {
Layout.fillWidth: true
text: "auto"
current: side.approval === 1
onClicked: side.approvalRequested(1)
}
Seg {
Layout.fillWidth: true
text: "yolo"
current: side.approval === 2
onClicked: side.approvalRequested(2)
}
}
Text {
Layout.fillWidth: true
text: side.approval === 0 ? "every tool asks first"
: side.approval === 2 ? "never asks — writes go through"
: "sandbox and rules decide, asks on doubt"
color: Theme.textFaint
font.pixelSize: Theme.smallSize
wrapMode: Text.Wrap
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 2
spacing: Theme.gap
Switch {
id: planSwitch
objectName: "planSwitch"
checked: side.planMode
implicitHeight: Theme.touchTarget
onClicked: side.planModeRequested(checked)
/* The Basic style's indicator is a dark pill with a dark
* knob here, so it is drawn: thumb-sized, accent when
* on, clearly off when not. */
indicator: Rectangle {
implicitWidth: 44
implicitHeight: 22
x: planSwitch.leftPadding
y: parent.height / 2 - height / 2
radius: 11
color: planSwitch.checked ? Theme.accentDim : Theme.surfaceAlt
border.width: 1
border.color: planSwitch.checked ? Theme.accent : Theme.border
Rectangle {
width: 16
height: 16
y: 2
x: planSwitch.checked ? parent.width - width - 3 : 3
radius: 8
color: planSwitch.checked ? Theme.text : Theme.textDim
Behavior on x {
NumberAnimation { duration: 120; easing.type: Easing.OutCubic }
}
}
}
}
Text {
text: "Plan mode"
color: Theme.textDim
font.pixelSize: Theme.smallSize
}
Item { Layout.fillWidth: true }
}
Text {
Layout.fillWidth: true
visible: side.planMode
text: "read-only tools and a planning note"
color: Theme.textFaint
font.pixelSize: Theme.smallSize
wrapMode: Text.Wrap
}
}
// --- the plan the engine keeps ---
TodoPanel {
Layout.fillWidth: true
text: side.todo
}
Text {
Layout.fillWidth: true
Layout.topMargin: Theme.gap
text: "engine in-process · v" + Qt.application.version
color: Theme.textFaint
font.pixelSize: Theme.smallSize
elide: Text.ElideRight
}
}
}
}