Files
dotfiles/shell/plugins/agents/Panel.qml
T

386 lines
10 KiB
QML

import QtQuick
import Quickshell.Io
import qs.Commons
import qs.Ui
import "Model.js" as Model
Panel {
id: root
moduleName: "blob.agents"
ipcTarget: "blob.agents"
manageIpc: false
readonly property color foreground: bar ? bar.foreground : Color.foreground
readonly property color urgent: Color.urgent
readonly property color dim: Qt.darker(foreground, 1.55)
readonly property color track: Style.selectedFillFor(foreground, Color.accent)
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
readonly property string barGlyph: "󱚣"
readonly property string launchCommand: "blob-launch-terminal claude"
readonly property var record: usage.record
readonly property var windows: {
var revision = usage.dataRevision
return Model.displayWindows(record)
}
readonly property var phrases: {
var revision = usage.dataRevision
return Model.summaryPhrases(record)
}
readonly property bool alarming: {
var fullest = Model.fullestWindow(record)
return !!fullest && fullest.percent >= 0.9
}
property double nowMs: Date.now()
property int phraseIndex: 0
property string shownPhrase: ""
function showPhrase() {
shownPhrase = phrases.length > 0 ? phrases[phraseIndex % phrases.length] : ""
}
function launchAgent() {
if (root.bar) root.bar.run(root.launchCommand)
root.close()
}
onPhrasesChanged: if (shownPhrase === "" || phrases.length <= 1) showPhrase()
onOpenedChanged: if (opened) {
nowMs = Date.now()
usage.refreshLimits()
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Main {
id: usage
settings: root.settings
}
Timer {
interval: 30000
running: root.opened
repeat: true
onTriggered: root.nowMs = Date.now()
}
Timer {
interval: 2800
running: root.opened && root.phrases.length > 1
repeat: true
onTriggered: phraseSwap.restart()
}
SequentialAnimation {
id: phraseSwap
PropertyAnimation {
target: hero
property: "metaOpacity"
to: 0.0
duration: 180
easing.type: Easing.OutQuad
}
ScriptAction {
script: {
root.phraseIndex = (root.phraseIndex + 1) % Math.max(1, root.phrases.length)
root.showPhrase()
}
}
PropertyAnimation {
target: hero
property: "metaOpacity"
to: 1.0
duration: 260
easing.type: Easing.InQuad
}
}
IpcHandler {
target: root.ipcTarget
function open(): void { root.open() }
function close(): void { root.close() }
function show(): void { root.open() }
function hide(): void { root.close() }
function toggle(): void { root.toggle() }
function refresh(): void { usage.refresh() }
}
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.barGlyph
active: root.alarming
onPressed: function(mouseButton) {
if (mouseButton === Qt.RightButton) root.launchAgent()
else if (mouseButton === Qt.MiddleButton) usage.refresh()
else root.toggle()
}
}
KeyboardPanel {
id: panel
anchorItem: button
owner: root
bar: root.bar
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(380))
contentHeight: panel.fittedContentHeight(column.implicitHeight)
PanelKeyCatcher {
id: keyCatcher
anchors.fill: parent
onActivateRequested: root.launchAgent()
onCloseRequested: root.close()
onTabRequested: function(direction) { root.switchPanel(direction) }
onTextKey: function(key) { if (key === "r" || key === "R") usage.refresh() }
Column {
id: column
width: parent.width
spacing: Style.space(16)
PanelHero {
id: hero
width: parent.width
title: "Claude Code"
meta: root.shownPhrase !== "" ? root.shownPhrase : "Usage"
detail: Model.planLabel(root.record)
foreground: root.foreground
fontFamily: root.fontFamily
iconComponent: Component {
ClaudeMark {
size: Style.font.display
}
}
trailingControl: Component {
HeroButton {
glyph: "󰞷"
tooltip: "Start Claude Code"
onClicked: root.launchAgent()
}
}
}
PanelSeparator {
foreground: root.foreground
}
Text {
visible: text !== ""
width: parent.width
textFormat: Text.PlainText
text: Model.troubleText(root.record)
color: root.urgent
font.family: root.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
Column {
visible: root.windows.length > 0
width: parent.width
spacing: Style.space(12)
Repeater {
model: root.windows
CompactLimit {
required property var modelData
width: column.width
window: modelData
stale: !!root.record && root.record.limitsStale === true
fetchedAt: root.record ? Number(root.record.limitsFetchedAt || 0) : 0
}
}
}
Text {
visible: !root.record
width: parent.width
text: "Loading usage..."
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
}
}
}
}
component ClaudeMark: Item {
id: mark
property real size: Style.font.heading
width: size
height: size
Image {
id: markImage
anchors.fill: parent
source: Qt.resolvedUrl("assets/claude.svg")
sourceSize.width: mark.size * 2
sourceSize.height: mark.size * 2
fillMode: Image.PreserveAspectFit
}
Text {
anchors.centerIn: parent
visible: markImage.status !== Image.Ready
textFormat: Text.PlainText
text: root.barGlyph
color: root.foreground
font.family: root.fontFamily
font.pixelSize: mark.size
}
}
component HeroButton: Rectangle {
id: heroButton
signal clicked()
property string glyph: ""
property string tooltip: ""
implicitWidth: Style.space(34)
implicitHeight: implicitWidth
radius: Style.cornerRadius
color: Util.alpha(Color.accent, heroMouse.containsMouse ? 0.3 : 0.12)
border.width: heroMouse.containsMouse ? Math.max(1, Style.hoverBorderWidth) : 0
border.color: Color.accent
Text {
anchors.centerIn: parent
textFormat: Text.PlainText
text: heroButton.glyph
color: Color.accent
font.family: root.fontFamily
font.pixelSize: Style.font.heading
}
MouseArea {
id: heroMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: heroButton.clicked()
}
PanelToolTip {
visible: heroMouse.containsMouse
text: heroButton.tooltip
}
}
component CompactLimit: Item {
id: compact
property var window: null
property bool stale: false
property real fetchedAt: 0
readonly property real resetMs: Model.resetMillisecondsFor(window, root.nowMs)
opacity: stale ? 0.5 : 1.0
implicitHeight: Math.max(compactTitle.implicitHeight, compactValue.implicitHeight)
HoverHandler { id: compactHover }
PanelToolTip {
visible: compactHover.hovered && !!compact.window
text: Model.limitTooltip(compact.window, compact.stale, compact.fetchedAt, root.nowMs)
}
Text {
id: compactTitle
textFormat: Text.PlainText
width: parent.width * 0.3
anchors.verticalCenter: parent.verticalCenter
text: compact.window ? compact.window.title : ""
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
elide: Text.ElideRight
}
Meter {
anchors.left: compactTitle.right
anchors.right: compactValue.left
anchors.rightMargin: Style.space(20)
anchors.verticalCenter: parent.verticalCenter
value: compact.window ? compact.window.percent : -1
alarming: !!compact.window && compact.window.percent >= 0.9
markers: compact.window && compact.window.scoped ? compact.window.scoped : []
}
TextMetrics {
id: compactValueMetrics
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
text: "00h 00m"
}
Text {
id: compactValue
width: Math.ceil(compactValueMetrics.advanceWidth)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
textFormat: Text.PlainText
horizontalAlignment: Text.AlignRight
text: compact.resetMs > 0 ? Model.formatDuration(compact.resetMs) : ""
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
}
}
component Meter: Item {
id: meter
property real value: -1
property bool alarming: false
property real thickness: Math.max(Style.space(4), Math.round(Style.spacing.controlHeight * 0.14))
property var markers: []
implicitHeight: thickness
Rectangle {
id: meterTrack
anchors.fill: parent
radius: height / 2
color: root.track
}
Rectangle {
anchors.left: meterTrack.left
anchors.verticalCenter: meterTrack.verticalCenter
height: meterTrack.height
radius: meterTrack.radius
width: meterTrack.width * Model.clamp(meter.value, 0, 1)
color: meter.alarming ? root.urgent : root.foreground
Behavior on width {
NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
}
}
Repeater {
model: meter.markers
Rectangle {
required property var modelData
width: Math.max(2, Math.round(meter.thickness * 0.5))
height: meter.thickness * 2.5
radius: width / 2
anchors.verticalCenter: meterTrack.verticalCenter
x: Model.clamp(meterTrack.width * Model.clamp(Number(modelData.percent), 0, 1) - width / 2, 0, meterTrack.width - width)
color: meter.alarming ? root.urgent : root.foreground
}
}
}
}