Watch
1
0
Fork
You've already forked souveraine
0

panel: a message typed mid-turn reaches her

Souveraine.send returns false for a busy turn exactly as it does for a dead
server, and the sidebar rendered every false as "server unreachable" — so
typing while she worked showed the words, then a lie, and dropped the text.
The verbs existed on both sides since 93fe30a and nothing called them.

Type-while-busy now interjects, as the TUI has always done
(src/ui/app/mod.rs:1006). Esc with nothing attached raises a hand. A queued
count sits in the status row and says sent, not read, because 202 from
/interject means only that it was taken.

Deliberately unchanged: the two drain sites and their system/user registers.
Providers disagree about which one they want and that choice is not ready to
be made. Latency is unchanged too — turn.rs:600 runs a whole tool round
without looking, so a note sent into a fifteen-call round waits for it.
This commit is contained in:
Fimeg 2026-08-17 09:16:11 -04:00
commit da8d69af2f
2 changed files with 59 additions and 2 deletions

View file

@ -425,6 +425,15 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
? Translation.tr("Waiting on the agent\nSent at %1").arg(root.formatClockTime(Souveraine.turnStartedAt))
: Translation.tr("Last turn took %1\nSent at %2").arg(root.formatTurnDuration(Souveraine.turnElapsedMs)).arg(root.formatClockTime(Souveraine.turnStartedAt))
}
StatusSeparator {
visible: Ai.queuedInterjections > 0
}
StatusItem {
visible: Ai.queuedInterjections > 0
icon: "pan_tool"
statusText: Translation.tr("%1 queued").arg(Ai.queuedInterjections)
description: Translation.tr("Notes handed to the turn in flight.\nShe reads them at the next round boundary — a long tool round finishes first.\nQueued means sent, not yet read.")
}
}
}
@ -790,10 +799,19 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
}
event.accepted = false; // No image, let text pasting proceed
} else if (event.key === Qt.Key_Escape) {
// Esc to detach file
// Esc detaches a file first that is the
// older, narrower meaning and it keeps it.
if (Ai.pendingFilePath.length > 0) {
Ai.attachFile("");
event.accepted = true;
} else if (Souveraine.turnActive) {
// Nothing attached and a turn is running:
// Esc is the raised hand. Cancellation is
// read at a round boundary, not mid-tool
// (`src/server/turn.rs:248`), so partial
// work survives.
Souveraine.cancelTurn();
event.accepted = true;
} else {
event.accepted = false;
}

View file

@ -32,6 +32,13 @@ Singleton {
property Component aiModelComponent: AiModel {}
readonly property string interfaceRole: "interface"
// Notes handed to the turn already running, still waiting to be read.
// The server takes them at a round boundary (`src/server/turn.rs:260`
// and `:453`), and 202 from `/interject` means *queued*, never *read*
// so this counts what was sent, not what landed. Cleared when the turn
// ends, by which point the drain has run.
property int queuedInterjections: 0
signal responseFinished()
property var messageIDs: []
@ -121,6 +128,10 @@ Singleton {
Connections {
target: Souveraine
function onTurnActiveChanged() {
if (!Souveraine.turnActive) root.queuedInterjections = 0;
}
function onAgentsRefreshed() {
const map = {};
Souveraine.agentList.forEach(id => {
@ -717,6 +728,18 @@ Singleton {
function sendUserMessage(message) {
if (message.length === 0) return;
// A turn is already running: this is an interjection, not a new turn.
// The TUI has always worked this way `src/ui/app/mod.rs:1006`,
// "Submit always when busy, this becomes an interjection". The
// Panel instead dropped the text on the floor and reported the server
// unreachable, because `Souveraine.send` returns false for a busy turn
// exactly as it does for a dead server.
if (Souveraine.turnActive) {
root.interjectUserMessage(message);
return;
}
root.addMessage(message, "user");
const result = Souveraine.send(message);
if (result === "step-up") {
@ -740,12 +763,28 @@ Singleton {
return;
}
if (!result) {
root.addMessage(Translation.tr("Souveraine server unreachable at %1 — start it with `souveraine server`").arg(Souveraine.serverBase), root.interfaceRole);
root.addMessage(Souveraine.serverUp
? Translation.tr("No agent selected — pick one before sending.")
: Translation.tr("Souveraine server unreachable at %1 — start it with `souveraine server`").arg(Souveraine.serverBase),
root.interfaceRole);
return;
}
root._startStreaming();
}
/* Slip a note into the turn already in flight.
It is read at the next round boundary, so a note sent during a long
tool round waits for that round to finish `src/server/turn.rs:600`
runs every tool in a round without checking. That latency is real and
is not something this function can fix. */
function interjectUserMessage(message) {
if (message.length === 0) return;
root.addMessage(message, "user");
root.queuedInterjections = root.queuedInterjections + 1;
Souveraine.interject(message);
}
// Set up the streaming assistant message container. Called after a
// successful send (or after a step-up auth retry succeeds).
function _startStreaming() {