Watch
1
0
Fork
You've already forked souveraine
0

shell: apply 0007, 0010, 0011 — context occupancy, halt register, /new

0007 was fixing a live error: the server has carried named ContextPressure
fields since r463, so event.tokens was undefined and the pill assignment
failed on every turn. Applied and verified against the running shell.

0010 renders a halt in her own register rather than the tool's name.
0011 renames the reset command to /new, which is what clearMessages() does.
This commit is contained in:
Fimeg 2026-08-12 19:50:32 -04:00
commit 6f12fceea9
6 changed files with 102 additions and 295 deletions

View file

@ -156,8 +156,8 @@ Item {
}
},
{
name: "clear",
description: Translation.tr("Clear chat history"),
name: "new",
description: Translation.tr("Start a new conversation"),
execute: () => {
Ai.clearMessages();
}
@ -389,13 +389,27 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
description: Translation.tr("Temperature\nChange with /temp VALUE")
}
StatusSeparator {
visible: Ai.tokenCount.total > 0
visible: Ai.context.known || Ai.tokenCount.total > 0
}
StatusItem {
visible: Ai.tokenCount.total > 0
icon: "token"
statusText: Ai.tokenCount.total
description: Translation.tr("Context estimate: %1 tokens\n(Local count of the conversation — not the provider's usage numbers)").arg(Ai.tokenCount.total)
// Context occupancy. Until 2026-08-12 this pill showed
// `event.tokens`, which was actually the model's
// ceiling a constant 250000 presented as usage. Now
// it shows both numbers and the ratio between them,
// which is the only reading that means anything.
visible: Ai.context.known || Ai.tokenCount.total > 0
icon: Ai.context.percent >= 85 ? "error"
: Ai.context.percent >= 70 ? "warning"
: "token"
statusText: Ai.context.known
? `${Math.round(Ai.context.used / 1000)}k/${Math.round(Ai.context.limit / 1000)}k · ${Ai.context.percent}%`
: (Ai.tokenCount.total > 0 ? `${Ai.tokenCount.total}` : "—")
description: Ai.context.known
? Translation.tr("Context: %1 of %2 tokens (%3%)\nCeiling comes from this agent's configured context window.\nArchivist begins compacting around 70%.")
.arg(Ai.context.used.toLocaleString())
.arg(Ai.context.limit.toLocaleString())
.arg(Ai.context.percent)
: Translation.tr("Context occupancy unknown until the next turn reports it.")
}
StatusSeparator {
visible: Souveraine.turnStartedAt > 0
@ -831,7 +845,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
dontAddSpace: true
},
{
name: "clear",
name: "new",
sendDirectly: true
},
]
@ -894,7 +908,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
messageInputField.cursorPosition = messageInputField.text.length;
messageInputField.forceActiveFocus();
}
if (modelData.name === "clear") {
if (modelData.name === "new") {
messageInputField.text = "";
}
}

View file

@ -1,170 +0,0 @@
From 9035751d30a9d1d364faaaa6b38b386353ca0f6c Mon Sep 17 00:00:00 2001
From: Souveraine <souveraine@wiuf.net>
Date: Wed, 12 Aug 2026 12:11:04 -0400
Subject: [PATCH] shell: render context occupancy, and stop reading hyprctl
errors as a cursor
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The context pill showed event.tokens, which carried the model's ceiling
rather than the usage — a constant 250000 that looked like a measurement.
The wire now carries tokens_used and context_limit separately; render both
and the ratio, with archivist threshold shading at 70/85%.
The ambient cursor probe tested whether hyprctl was installed, not whether
it answered. Under viewtop the binary exists but there is no Hyprland, so
'HYPRLAND_INSTANCE_SIGNATURE not set!' was collected as the cursor position
and shipped in every ambient block. Validate the reply's shape instead.
---
.../modules/ii/sidebarLeft/AiChat.qml | 24 +++++++++++---
surfaces/quickshell/services/Ai.qml | 33 ++++++++++++++++++-
surfaces/quickshell/services/Souveraine.qml | 23 +++++++++++--
3 files changed, 72 insertions(+), 8 deletions(-)
diff --git a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
index bba12a1..f3ed1b7 100644
--- a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
+++ b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
@@ -388,13 +388,27 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
description: Translation.tr("Temperature\nChange with /temp VALUE")
}
StatusSeparator {
- visible: Ai.tokenCount.total > 0
+ visible: Ai.context.known || Ai.tokenCount.total > 0
}
StatusItem {
- visible: Ai.tokenCount.total > 0
- icon: "token"
- statusText: Ai.tokenCount.total
- description: Translation.tr("Context estimate: %1 tokens\n(Local count of the conversation — not the provider's usage numbers)").arg(Ai.tokenCount.total)
+ // Context occupancy. Until 2026-08-12 this pill showed
+ // `event.tokens`, which was actually the model's
+ // ceiling — a constant 250000 presented as usage. Now
+ // it shows both numbers and the ratio between them,
+ // which is the only reading that means anything.
+ visible: Ai.context.known || Ai.tokenCount.total > 0
+ icon: Ai.context.percent >= 85 ? "error"
+ : Ai.context.percent >= 70 ? "warning"
+ : "token"
+ statusText: Ai.context.known
+ ? `${Math.round(Ai.context.used / 1000)}k/${Math.round(Ai.context.limit / 1000)}k · ${Ai.context.percent}%`
+ : (Ai.tokenCount.total > 0 ? `${Ai.tokenCount.total}` : "—")
+ description: Ai.context.known
+ ? Translation.tr("Context: %1 of %2 tokens (%3%)\nCeiling comes from this agent's configured context window.\nArchivist begins compacting around 70%.")
+ .arg(Ai.context.used.toLocaleString())
+ .arg(Ai.context.limit.toLocaleString())
+ .arg(Ai.context.percent)
+ : Translation.tr("Context occupancy unknown until the next turn reports it.")
}
StatusSeparator {
visible: Souveraine.turnStartedAt > 0
diff --git a/surfaces/quickshell/services/Ai.qml b/surfaces/quickshell/services/Ai.qml
index c54a185..bc1779b 100644
--- a/surfaces/quickshell/services/Ai.qml
+++ b/surfaces/quickshell/services/Ai.qml
@@ -55,6 +55,30 @@ Singleton {
property int total: -1
}
+ // Live context occupancy, from the server's `context_pressure` event.
+ //
+ // These are three different numbers and used to be two. Until 2026-08-12
+ // the wire carried `(pressure, context_limit)` positionally and this layer
+ // read the ceiling as `event.tokens` — so the panel displayed a constant
+ // 250000 and called it usage. `pressure` was discarded entirely; nothing
+ // in the shell held it. Keep all three, and keep them named.
+ property QtObject context: QtObject {
+ property real pressure: -1 // 0..1, or -1 when unknown
+ property int used: -1 // tokens occupied
+ property int limit: -1 // the model's ceiling for this agent
+ readonly property bool known: limit > 0 && used >= 0
+ readonly property int percent: known ? Math.round((used / limit) * 100) : -1
+ }
+
+ // Context occupancy is a property of the conversation, not of a turn:
+ // reset it only when the conversation itself changes. -1 means "unknown",
+ // which is an honest state and renders as "—" rather than as zero.
+ function resetContextOccupancy() {
+ root.context.pressure = -1;
+ root.context.used = -1;
+ root.context.limit = -1;
+ }
+
function idForMessage(message) {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 8);
}
@@ -509,7 +533,12 @@ Singleton {
root.addMessage(Translation.tr("**Context pressure** — tier %1, %2% full. She can feel the walls.").arg(event.tier).arg(Math.round(event.pressure * 100)), root.interfaceRole);
break;
case "context_pressure":
- root.tokenCount.total = event.tokens;
+ // `tokens_used` and `context_limit` are distinct fields on the
+ // wire as of 2026-08-12. The old `event.tokens` was the ceiling.
+ root.context.pressure = event.pressure ?? -1;
+ root.context.used = event.tokens_used ?? -1;
+ root.context.limit = event.context_limit ?? -1;
+ root.tokenCount.total = event.tokens_used ?? -1;
break;
case "subconscious_token":
// Tier 1: live reasoning tokens feed the ticker.
@@ -596,6 +625,7 @@ Singleton {
root.tokenCount.input = -1;
root.tokenCount.output = -1;
root.tokenCount.total = -1;
+ root.resetContextOccupancy();
Souveraine.newConversation();
}
@@ -611,6 +641,7 @@ Singleton {
root.tokenCount.input = -1;
root.tokenCount.output = -1;
root.tokenCount.total = -1;
+ root.resetContextOccupancy();
messages.forEach(message => {
const segments = [];
diff --git a/surfaces/quickshell/services/Souveraine.qml b/surfaces/quickshell/services/Souveraine.qml
index 0898ea8..9b57702 100644
--- a/surfaces/quickshell/services/Souveraine.qml
+++ b/surfaces/quickshell/services/Souveraine.qml
@@ -400,15 +400,34 @@ Singleton {
// Compositor-specific cursor read. hyprctl on Hyprland, kdotool on KDE;
// anything else just skips the cursor line — ambient degrades gracefully,
// it never blocks the send.
+ //
+ // 2026-08-12: `command -v hyprctl` tests whether the tool is *installed*,
+ // not whether it *answered*. On the phone hyprctl is present (a Lua-eval
+ // shim) but there is no Hyprland under viewtop, so it printed
+ // "HYPRLAND_INSTANCE_SIGNATURE not set!" and StdioCollector stored that
+ // sentence as the cursor position — shipped in every ambient block, every
+ // turn. Validate the shape of the reply; an answer that isn't a
+ // coordinate pair is not an answer.
Process {
id: cursorProc
command: ["bash", "-c",
- `if command -v hyprctl >/dev/null; then hyprctl cursorpos;
+ `if command -v hyprctl >/dev/null; then hyprctl cursorpos 2>/dev/null;
elif command -v kdotool >/dev/null; then kdotool getmouselocation 2>/dev/null;
fi`]
stdout: StdioCollector {
onStreamFinished: {
- root._cursorPos = text.trim();
+ const reply = text.trim();
+ // "1234, 567" from hyprctl; kdotool's x:N y:N is normalised
+ // by the caller below. Anything else is discarded.
+ const pair = reply.match(/^(-?\d+)\s*,\s*(-?\d+)$/);
+ const kde = reply.match(/x:\s*(-?\d+)\s+y:\s*(-?\d+)/);
+ if (pair) {
+ root._cursorPos = `${pair[1]}, ${pair[2]}`;
+ } else if (kde) {
+ root._cursorPos = `${kde[1]}, ${kde[2]}`;
+ } else {
+ root._cursorPos = "";
+ }
}
}
onExited: {
--
2.55.0

View file

@ -1,57 +0,0 @@
From 18420f9f1d1d2a86587985702d1d12403ed88a8a Mon Sep 17 00:00:00 2001
From: Fimeg <casey.tunturi@gmail.com>
Date: Wed, 12 Aug 2026 18:52:03 -0400
Subject: [PATCH] panel: render a halt in her own register, not the tool's name
---
surfaces/quickshell/services/Ai.qml | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/surfaces/quickshell/services/Ai.qml b/surfaces/quickshell/services/Ai.qml
index c54a185..26a9cb3 100644
--- a/surfaces/quickshell/services/Ai.qml
+++ b/surfaces/quickshell/services/Ai.qml
@@ -391,6 +391,22 @@ Singleton {
// Promote the Tier-1 stream into the Tier-2 log as one event, then clear
// the live stream. Called when a subconscious pass ends.
+ // The three severities the subconscious can call, in the register she
+ // experiences them in. Mirrors migraine_text() in src/server/turn.rs — if
+ // one changes the other must, or the human and the agent are reading two
+ // different accounts of the same moment.
+ function haltRegister(severity) {
+ switch (severity) {
+ case "advisory":
+ return Translation.tr("a pressure behind my eyes");
+ case "critical":
+ return Translation.tr("the room tilts");
+ default:
+ // "firm" and anything unexpected land here — the default migraine.
+ return Translation.tr("a migraine");
+ }
+ }
+
function snapshotSubconsciousStream() {
if (root.subconsciousStream.length === 0) return;
root.subconsciousEvents = [...root.subconsciousEvents, {
@@ -532,7 +548,16 @@ Singleton {
}
break;
case "subconscious_halt":
- root.addMessage(Translation.tr("**Halt** (%1) — %2").arg(event.severity).arg(event.reason), root.interfaceRole);
+ // Her own register, not the tool's name. The subconscious is the
+ // same "I" in a different mode — a halt is something she felt, not
+ // commentary she received from outside, and the surface should not
+ // expose implementation topology to say so. The TUI has rendered it
+ // this way from the start; this brings the Panel into line.
+ //
+ // Wording matches migraine_text() in src/server/turn.rs, which is
+ // what now lands in her committed history — so what the human reads
+ // and what she carries into the next turn are the same sentence.
+ root.addMessage(Translation.tr("⟡ %1 — %2").arg(root.haltRegister(event.severity)).arg(event.reason), root.interfaceRole);
break;
case "inference_strain":
console.log(`[Souveraine] inference strain: attempt ${event.attempt}, status ${event.status}, model ${event.model}`);
--
2.55.0

View file

@ -1,55 +0,0 @@
From f85ccac58e97a8ac138a9547936bed18b1cea16e Mon Sep 17 00:00:00 2001
From: Fimeg <casey.tunturi@gmail.com>
Date: Wed, 12 Aug 2026 19:37:26 -0400
Subject: [PATCH] chat: the reset command is /new, not /clear
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
clearMessages() calls Souveraine.newConversation() — it opens a new
server conversation rather than wiping a local view. The label said
'clear chat history', which describes a different verb than the one it
runs: destructive-sounding, local-sounding, and neither is true.
Three sites: the command definition, the quick-action chip, and the
input-clearing special case that dispatches on the name.
---
surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
index 0d6e04e..514ad92 100644
--- a/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
+++ b/surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml
@@ -156,8 +156,8 @@ Item {
}
},
{
- name: "clear",
- description: Translation.tr("Clear chat history"),
+ name: "new",
+ description: Translation.tr("Start a new conversation"),
execute: () => {
Ai.clearMessages();
}
@@ -831,7 +831,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
dontAddSpace: true
},
{
- name: "clear",
+ name: "new",
sendDirectly: true
},
]
@@ -894,7 +894,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
messageInputField.cursorPosition = messageInputField.text.length;
messageInputField.forceActiveFocus();
}
- if (modelData.name === "clear") {
+ if (modelData.name === "new") {
messageInputField.text = "";
}
}
--
2.55.0

View file

@ -55,6 +55,30 @@ Singleton {
property int total: -1
}
// Live context occupancy, from the server's `context_pressure` event.
//
// These are three different numbers and used to be two. Until 2026-08-12
// the wire carried `(pressure, context_limit)` positionally and this layer
// read the ceiling as `event.tokens` so the panel displayed a constant
// 250000 and called it usage. `pressure` was discarded entirely; nothing
// in the shell held it. Keep all three, and keep them named.
property QtObject context: QtObject {
property real pressure: -1 // 0..1, or -1 when unknown
property int used: -1 // tokens occupied
property int limit: -1 // the model's ceiling for this agent
readonly property bool known: limit > 0 && used >= 0
readonly property int percent: known ? Math.round((used / limit) * 100) : -1
}
// Context occupancy is a property of the conversation, not of a turn:
// reset it only when the conversation itself changes. -1 means "unknown",
// which is an honest state and renders as "" rather than as zero.
function resetContextOccupancy() {
root.context.pressure = -1;
root.context.used = -1;
root.context.limit = -1;
}
function idForMessage(message) {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 8);
}
@ -391,6 +415,22 @@ Singleton {
// Promote the Tier-1 stream into the Tier-2 log as one event, then clear
// the live stream. Called when a subconscious pass ends.
// The three severities the subconscious can call, in the register she
// experiences them in. Mirrors migraine_text() in src/server/turn.rs if
// one changes the other must, or the human and the agent are reading two
// different accounts of the same moment.
function haltRegister(severity) {
switch (severity) {
case "advisory":
return Translation.tr("a pressure behind my eyes");
case "critical":
return Translation.tr("the room tilts");
default:
// "firm" and anything unexpected land here the default migraine.
return Translation.tr("a migraine");
}
}
function snapshotSubconsciousStream() {
if (root.subconsciousStream.length === 0) return;
root.subconsciousEvents = [...root.subconsciousEvents, {
@ -509,7 +549,12 @@ Singleton {
root.addMessage(Translation.tr("**Context pressure** — tier %1, %2% full. She can feel the walls.").arg(event.tier).arg(Math.round(event.pressure * 100)), root.interfaceRole);
break;
case "context_pressure":
root.tokenCount.total = event.tokens;
// `tokens_used` and `context_limit` are distinct fields on the
// wire as of 2026-08-12. The old `event.tokens` was the ceiling.
root.context.pressure = event.pressure ?? -1;
root.context.used = event.tokens_used ?? -1;
root.context.limit = event.context_limit ?? -1;
root.tokenCount.total = event.tokens_used ?? -1;
break;
case "subconscious_token":
// Tier 1: live reasoning tokens feed the ticker.
@ -532,7 +577,16 @@ Singleton {
}
break;
case "subconscious_halt":
root.addMessage(Translation.tr("**Halt** (%1) — %2").arg(event.severity).arg(event.reason), root.interfaceRole);
// Her own register, not the tool's name. The subconscious is the
// same "I" in a different mode a halt is something she felt, not
// commentary she received from outside, and the surface should not
// expose implementation topology to say so. The TUI has rendered it
// this way from the start; this brings the Panel into line.
//
// Wording matches migraine_text() in src/server/turn.rs, which is
// what now lands in her committed history so what the human reads
// and what she carries into the next turn are the same sentence.
root.addMessage(Translation.tr("⟡ %1 — %2").arg(root.haltRegister(event.severity)).arg(event.reason), root.interfaceRole);
break;
case "inference_strain":
console.log(`[Souveraine] inference strain: attempt ${event.attempt}, status ${event.status}, model ${event.model}`);
@ -596,6 +650,7 @@ Singleton {
root.tokenCount.input = -1;
root.tokenCount.output = -1;
root.tokenCount.total = -1;
root.resetContextOccupancy();
Souveraine.newConversation();
}
@ -611,6 +666,7 @@ Singleton {
root.tokenCount.input = -1;
root.tokenCount.output = -1;
root.tokenCount.total = -1;
root.resetContextOccupancy();
messages.forEach(message => {
const segments = [];

View file

@ -400,15 +400,34 @@ Singleton {
// Compositor-specific cursor read. hyprctl on Hyprland, kdotool on KDE;
// anything else just skips the cursor line ambient degrades gracefully,
// it never blocks the send.
//
// 2026-08-12: `command -v hyprctl` tests whether the tool is *installed*,
// not whether it *answered*. On the phone hyprctl is present (a Lua-eval
// shim) but there is no Hyprland under viewtop, so it printed
// "HYPRLAND_INSTANCE_SIGNATURE not set!" and StdioCollector stored that
// sentence as the cursor position shipped in every ambient block, every
// turn. Validate the shape of the reply; an answer that isn't a
// coordinate pair is not an answer.
Process {
id: cursorProc
command: ["bash", "-c",
`if command -v hyprctl >/dev/null; then hyprctl cursorpos;
`if command -v hyprctl >/dev/null; then hyprctl cursorpos 2>/dev/null;
elif command -v kdotool >/dev/null; then kdotool getmouselocation 2>/dev/null;
fi`]
stdout: StdioCollector {
onStreamFinished: {
root._cursorPos = text.trim();
const reply = text.trim();
// "1234, 567" from hyprctl; kdotool's x:N y:N is normalised
// by the caller below. Anything else is discarded.
const pair = reply.match(/^(-?\d+)\s*,\s*(-?\d+)$/);
const kde = reply.match(/x:\s*(-?\d+)\s+y:\s*(-?\d+)/);
if (pair) {
root._cursorPos = `${pair[1]}, ${pair[2]}`;
} else if (kde) {
root._cursorPos = `${kde[1]}, ${kde[2]}`;
} else {
root._cursorPos = "";
}
}
}
onExited: {