Watch
1
0
Fork
You've already forked souveraine
0

agent sessions: one collector, one island

Collector emits a single JSON envelope for every agent session (Souveraine,
Claude Code, Codex) and always exits 0; a failing provider reports
available:false rather than taking the envelope down.

AgentSessions.qml owns the cadence and projects the envelope. Transport is not
the contract: when the TASK-69 daemon lands, only the source changes.

Island is a morph host for the bar - dot on a cramped bar, pill where there is
room, derived from the same threshold BarContent uses.

Inert until registered in services/qmldir and mounted; see patches/0005.
This commit is contained in:
Fimeg 2026-08-11 14:19:45 -04:00
commit 47ce2d1970
5 changed files with 742 additions and 0 deletions

View file

@ -0,0 +1,188 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
// Island the agent surface for the top bar. TASK-70, fed by TASK-69.
//
// A morph host: one element that changes shape with what the agents are doing,
// rather than a fixed widget that is mostly empty. States, in order of weight:
//
// hidden no sessions at all occupy nothing, not a placeholder
// dot something is active; a pulse and nothing else
// pill the primary session: provider glyph, label, state
// expanded tapped open every session, grouped
//
// DEVICE TYPES ARE A REAL DIFFERENCE, NOT A SETTING
// The Pixel 3 bar is ~1080px with a carrier readout already competing for the
// left side, and ii-phone/BarContent drops the entire leftCenterGroup for that
// reason. So the island's *resting* state is device-dependent: `dot` on a
// narrow bar, `pill` where there is room. That is derived from the same
// threshold the rest of the bar uses (Appearance.sizes.barShorten...), so the
// island narrows exactly when its neighbours do. One authority for "is this
// bar cramped"; the island is a rendering of it, not a second opinion.
//
// HOST-AGNOSTIC, like SubconsciousTicker
// This owns no overlay state and reaches into no manager. Expansion is a local
// state change; anything larger is a signal for whoever mounted it to handle.
// That is what lets the same file serve the desktop bar, the phone bar, and
// later a viewtop node without a fork.
Item {
id: root
// The host may pin a form; otherwise it is derived from the bar's own
// cramped-ness. Values: "auto" | "dot" | "pill".
property string restingForm: "auto"
property var screen: root.QsWindow.window?.screen
// Same test BarContent uses, so island and neighbours narrow together.
readonly property bool narrowBar: (Appearance.sizes.barShortenScreenWidthThreshold >= (screen?.width ?? 99999))
readonly property string form: {
if (root.restingForm !== "auto")
return root.restingForm;
return root.narrowBar ? "dot" : "pill";
}
// Nothing to say occupy nothing. An always-present empty chip trains the
// eye to ignore the spot, which costs us the one thing the island is for.
readonly property bool hasContent: AgentSessions.sessions.length > 0
property bool expanded: false
// For a host that wants to put the full session list somewhere better than
// an inline popout (a sidebar, a sheet, a notch overlay). If nobody
// connects it, the inline expansion below is the fallback the component
// is useful alone and better when hosted.
signal requestOpenPanel
visible: hasContent
implicitWidth: visible ? content.implicitWidth : 0
implicitHeight: Appearance.sizes.baseBarHeight
readonly property var primary: AgentSessions.primarySession
// Colour carries the state, so the island reads pre-attentively you know
// something is running before you read a word of it.
readonly property color stateColor: {
if (AgentSessions.stale)
return Appearance.colors.colOutlineVariant;
if (AgentSessions.anyActive)
return Appearance.colors.colPrimary;
if (root.primary?.state === "recent")
return Appearance.colors.colOnLayer0;
return Appearance.colors.colOutlineVariant;
}
Rectangle {
id: content
anchors.centerIn: parent
implicitWidth: row.implicitWidth + 16
implicitHeight: Math.max(20, Appearance.sizes.baseBarHeight - 10)
radius: height / 2
color: root.expanded ? Appearance.colors.colLayer2
: mouse.containsMouse ? Appearance.colors.colLayer1
: "transparent"
Behavior on implicitWidth {
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
}
Behavior on color {
ColorAnimation { duration: 120 }
}
RowLayout {
id: row
anchors.centerIn: parent
spacing: 6
// The pulse. Present in every form in `dot` it IS the island.
Rectangle {
id: pulse
Layout.alignment: Qt.AlignVCenter
implicitWidth: 8
implicitHeight: 8
radius: 4
color: root.stateColor
// Only animate while something is genuinely active. A dot that
// always breathes is decoration; a dot that breathes only when
// an agent is working is information.
SequentialAnimation on opacity {
running: AgentSessions.anyActive && !AgentSessions.stale
loops: Animation.Infinite
NumberAnimation { to: 0.35; duration: 900; easing.type: Easing.InOutSine }
NumberAnimation { to: 1.0; duration: 900; easing.type: Easing.InOutSine }
}
// Leaving the loop mid-fade would strand it dim.
onOpacityChanged: if (!AgentSessions.anyActive && opacity !== 1) opacity = 1
}
// Multiple agents at once is the case the old per-provider widgets
// could not show at all. A count is the cheapest honest summary.
StyledText {
Layout.alignment: Qt.AlignVCenter
visible: AgentSessions.activeCount > 1
text: AgentSessions.activeCount
color: root.stateColor
font.pixelSize: Appearance.font.pixelSize.smaller
font.weight: Font.DemiBold
}
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
visible: root.form === "pill" && root.primary !== null
text: AgentSessions.providerIcon(root.primary?.provider ?? "")
iconSize: Appearance.font.pixelSize.normal
color: root.stateColor
}
StyledText {
Layout.alignment: Qt.AlignVCenter
visible: root.form === "pill" && root.primary !== null
text: AgentSessions.sessionLabel(root.primary)
color: Appearance.colors.colOnLayer0
font.pixelSize: Appearance.font.pixelSize.smaller
elide: Text.ElideRight
Layout.maximumWidth: 110
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (ev) => {
if (ev.button === Qt.RightButton) {
root.requestOpenPanel();
return;
}
root.expanded = !root.expanded;
}
}
StyledToolTip {
// Stale is worth saying out loud rather than only dimming: a dim
// island and a quiet one look identical at a glance.
content: AgentSessions.stale
? qsTr("Agent sessions — stale (%1)").arg(AgentSessions.lastError)
: AgentSessions.available
? qsTr("%1 agent session(s), %2 active").arg(AgentSessions.sessions.length).arg(AgentSessions.activeCount)
: qsTr("Agent sessions — collecting…")
extraVisibleCondition: mouse.containsMouse && !root.expanded
}
}
IslandExpansion {
id: expansion
anchors.top: content.bottom
anchors.topMargin: 6
anchors.horizontalCenter: content.horizontalCenter
visible: root.expanded
onDismissed: root.expanded = false
}
}

View file

@ -0,0 +1,151 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
// The island opened: every session the collector sees, newest first.
//
// Deliberately a flat list rather than provider-grouped tabs. The question this
// answers is "what is running right now", and that is chronological, not
// taxonomic grouping by provider would bury a live Codex run under three idle
// Souveraine threads. The provider is a glyph on each row instead.
Rectangle {
id: root
signal dismissed
implicitWidth: 320
implicitHeight: Math.min(column.implicitHeight + 16, 360)
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2
border.width: 1
border.color: Appearance.colors.colLayer0Border
opacity: visible ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 140 } }
ColumnLayout {
id: column
anchors.fill: parent
anchors.margins: 8
spacing: 4
RowLayout {
Layout.fillWidth: true
spacing: 6
StyledText {
Layout.fillWidth: true
text: qsTr("Agent sessions")
color: Appearance.colors.colOnLayer0
font.pixelSize: Appearance.font.pixelSize.smaller
font.weight: Font.DemiBold
}
// Provider health belongs here, not on a row: a provider that is
// unavailable has no rows to hang the message on, and "no sessions"
// must never be confused with "not looking". Same family as every
// empty-result bug this project has hit.
Repeater {
model: ["souveraine", "claude", "codex"]
delegate: MaterialSymbol {
required property string modelData
readonly property var p: AgentSessions.providers?.[modelData] ?? null
visible: p !== null && p.available === false
text: AgentSessions.providerIcon(modelData)
iconSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOutlineVariant
StyledToolTip {
content: qsTr("%1 unavailable: %2")
.arg(AgentSessions.providerLabel(parent.modelData))
.arg(parent.p?.error ?? "unknown")
}
}
}
}
StyledText {
Layout.fillWidth: true
visible: AgentSessions.stale
text: qsTr("stale — %1").arg(AgentSessions.lastError)
color: Appearance.colors.colOutlineVariant
font.pixelSize: Appearance.font.pixelSize.smallest
elide: Text.ElideRight
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: contentHeight
clip: true
spacing: 2
model: AgentSessions.sessions
delegate: Item {
required property var modelData
width: ListView.view.width
implicitHeight: 30
RowLayout {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
spacing: 6
Rectangle {
Layout.alignment: Qt.AlignVCenter
implicitWidth: 6
implicitHeight: 6
radius: 3
color: modelData.state === "active" ? Appearance.colors.colPrimary
: modelData.state === "recent" ? Appearance.colors.colOnLayer0
: Appearance.colors.colOutlineVariant
}
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
text: AgentSessions.providerIcon(modelData.provider)
iconSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnLayer0
}
StyledText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: AgentSessions.sessionLabel(modelData)
color: Appearance.colors.colOnLayer0
font.pixelSize: Appearance.font.pixelSize.smaller
elide: Text.ElideRight
}
// "" where a provider does not report tokens. Rendering a
// 0 would be a measurement claim we cannot back: the
// Souveraine substrate records TokenUsage but nothing calls
// assistant_with_usage yet (TASK-67). An em dash is the
// honest glyph for "not measured".
StyledText {
Layout.alignment: Qt.AlignVCenter
readonly property int tok: AgentSessions.sessionTokens(modelData)
text: tok < 0 ? "—"
: tok > 1000 ? (Math.round(tok / 100) / 10) + "k"
: String(tok)
color: Appearance.colors.colOutlineVariant
font.pixelSize: Appearance.font.pixelSize.smallest
}
}
}
}
StyledText {
Layout.fillWidth: true
visible: AgentSessions.sessions.length === 0
text: AgentSessions.available ? qsTr("No recent sessions")
: qsTr("Collecting…")
color: Appearance.colors.colOutlineVariant
font.pixelSize: Appearance.font.pixelSize.smaller
horizontalAlignment: Text.AlignHCenter
}
}
}

View file

@ -0,0 +1,2 @@
Island 1.0 Island.qml
IslandExpansion 1.0 IslandExpansion.qml