Watch
1
0
Fork
You've already forked souveraine
0

shell: offer resume instead of forcing it

This commit is contained in:
souveraine 2026-08-10 23:04:36 -04:00 committed by Fimeg
commit ec87e6a9dc
2 changed files with 58 additions and 132 deletions

View file

@ -1,123 +0,0 @@
From 377a7542fef4422e1e3232a0c1d5f93ca6aaf1f5 Mon Sep 17 00:00:00 2001
From: souveraine <souveraine@wiuf.net>
Date: Mon, 10 Aug 2026 23:04:36 -0400
Subject: [PATCH] shell: offer resume instead of forcing it
---
surfaces/quickshell/services/Souveraine.qml | 67 ++++++++++++++++++---
1 file changed, 58 insertions(+), 9 deletions(-)
diff --git a/surfaces/quickshell/services/Souveraine.qml b/surfaces/quickshell/services/Souveraine.qml
index fe5e08e..6d27cc1 100644
--- a/surfaces/quickshell/services/Souveraine.qml
+++ b/surfaces/quickshell/services/Souveraine.qml
@@ -44,17 +44,36 @@ Singleton {
property var agents: ({})
property var agentList: Object.keys(agents)
property string currentAgentId: ""
- // Auto-resume: whenever the active agent is (re)established — shell start,
- // reboot, agent switch — re-attach her latest server-persisted conversation
- // so the next message continues the thread instead of minting a fresh one.
- // Without this every shell reload starts amnesiac, and the shell reloads
- // often (a deploy, a lock, a crash). The server is the source of truth here;
- // the surface deliberately keeps no agent → conversation map of its own.
+ // On agent (re)establishment — shell start, reboot, agent switch — we look
+ // up her latest server-persisted conversation. What we do with it depends
+ // on autoResume.
+ //
+ // autoResume false (default): we *offer* it. `resumeOffered` fires with
+ // the thread's id and metadata; nothing is attached. Doing nothing
+ // starts fresh, which is what a reload should do. Continuing is one
+ // deliberate act (acceptOfferedResume), not a default.
+ // autoResume true: legacy behaviour — attach it silently.
+ //
+ // The distinction matters because the shell reloads often (a deploy, a
+ // lock, a crash) and silent re-attachment makes every one of those look
+ // like a continuation of a conversation the human may have finished with.
+ // Explicit resume paths (/resume, the Face control) are unaffected: they
+ // call resumeLatestConversation() with no argument and still load.
+ //
+ // Note this is NOT the amnesia fix from e4e6594 — that one stopped
+ // selectAgent clearing conversationId on an unchanged agent, and stays.
// Gated so it never clobbers a live turn or an already-attached thread.
+ property bool autoResume: Config.options?.ai?.autoResume ?? false
+
+ // Emitted when a resumable thread exists and we chose not to attach it.
+ // agentId/conversationId identify it; the rest is for drawing the offer.
+ signal resumeOffered(string agentId, string conversationId, string title, string updatedAt)
+ property string offeredConversationId: ""
+
onCurrentAgentIdChanged: {
if (root.currentAgentId.length > 0 && root.serverUp
&& root.conversationId.length === 0 && !root.turnActive) {
- root.resumeLatestConversation();
+ root.resumeLatestConversation(!root.autoResume);
}
}
property string conversationId: ""
@@ -214,6 +233,9 @@ Singleton {
Process {
id: listConversations
property string agentId: ""
+ // Set by resumeLatestConversation(true): announce the thread rather
+ // than loading it.
+ property bool offerOnly: false
stdout: StdioCollector {
onStreamFinished: {
if (listConversations.agentId !== root.currentAgentId) return;
@@ -240,7 +262,16 @@ Singleton {
return;
}
// Server sorts by updated_at descending — [0] is her latest.
- root._loadConversation(listConversations.agentId, conversations[0].id);
+ const latest = conversations[0];
+ if (listConversations.offerOnly) {
+ // Announce, don't attach. conversationId stays empty, so a
+ // send without accepting mints a fresh thread on purpose.
+ root.offeredConversationId = latest.id;
+ root.resumeOffered(listConversations.agentId, latest.id,
+ latest.title ?? "", latest.updated_at ?? "");
+ return;
+ }
+ root._loadConversation(listConversations.agentId, latest.id);
}
}
onExited: exitCode => {
@@ -287,8 +318,11 @@ Singleton {
}
}
- function resumeLatestConversation() {
+ // offerOnly: fetch the latest thread but announce it instead of attaching.
+ // Defaults to false so every existing caller keeps its old behaviour.
+ function resumeLatestConversation(offerOnly) {
if (!root.serverUp || root.currentAgentId.length === 0 || root.turnActive) return false;
+ listConversations.offerOnly = (offerOnly === true);
listConversations.agentId = root.currentAgentId;
listConversations.command = [
"curl", "-sf", "--max-time", "5",
@@ -298,6 +332,21 @@ Singleton {
return true;
}
+ // Take up an offer made by resumeOffered. No-op if the offer has gone
+ // stale (a turn started, or something else attached in the meantime).
+ function acceptOfferedResume() {
+ if (root.offeredConversationId.length === 0 || root.turnActive) return false;
+ if (root.conversationId.length > 0) return false;
+ root._loadConversation(root.currentAgentId, root.offeredConversationId);
+ root.offeredConversationId = "";
+ return true;
+ }
+
+ // Decline. The thread stays on the server; we simply start fresh.
+ function dismissOfferedResume() {
+ root.offeredConversationId = "";
+ }
+
function _loadConversation(agentId, conversationId) {
loadConversation.agentId = agentId;
loadConversation.requestedConversationId = conversationId;
--
2.55.0

View file

@ -44,17 +44,36 @@ Singleton {
property var agents: ({})
property var agentList: Object.keys(agents)
property string currentAgentId: ""
// Auto-resume: whenever the active agent is (re)established shell start,
// reboot, agent switch re-attach her latest server-persisted conversation
// so the next message continues the thread instead of minting a fresh one.
// Without this every shell reload starts amnesiac, and the shell reloads
// often (a deploy, a lock, a crash). The server is the source of truth here;
// the surface deliberately keeps no agent conversation map of its own.
// On agent (re)establishment shell start, reboot, agent switch we look
// up her latest server-persisted conversation. What we do with it depends
// on autoResume.
//
// autoResume false (default): we *offer* it. `resumeOffered` fires with
// the thread's id and metadata; nothing is attached. Doing nothing
// starts fresh, which is what a reload should do. Continuing is one
// deliberate act (acceptOfferedResume), not a default.
// autoResume true: legacy behaviour attach it silently.
//
// The distinction matters because the shell reloads often (a deploy, a
// lock, a crash) and silent re-attachment makes every one of those look
// like a continuation of a conversation the human may have finished with.
// Explicit resume paths (/resume, the Face control) are unaffected: they
// call resumeLatestConversation() with no argument and still load.
//
// Note this is NOT the amnesia fix from e4e6594 that one stopped
// selectAgent clearing conversationId on an unchanged agent, and stays.
// Gated so it never clobbers a live turn or an already-attached thread.
property bool autoResume: Config.options?.ai?.autoResume ?? false
// Emitted when a resumable thread exists and we chose not to attach it.
// agentId/conversationId identify it; the rest is for drawing the offer.
signal resumeOffered(string agentId, string conversationId, string title, string updatedAt)
property string offeredConversationId: ""
onCurrentAgentIdChanged: {
if (root.currentAgentId.length > 0 && root.serverUp
&& root.conversationId.length === 0 && !root.turnActive) {
root.resumeLatestConversation();
root.resumeLatestConversation(!root.autoResume);
}
}
property string conversationId: ""
@ -214,6 +233,9 @@ Singleton {
Process {
id: listConversations
property string agentId: ""
// Set by resumeLatestConversation(true): announce the thread rather
// than loading it.
property bool offerOnly: false
stdout: StdioCollector {
onStreamFinished: {
if (listConversations.agentId !== root.currentAgentId) return;
@ -240,7 +262,16 @@ Singleton {
return;
}
// Server sorts by updated_at descending [0] is her latest.
root._loadConversation(listConversations.agentId, conversations[0].id);
const latest = conversations[0];
if (listConversations.offerOnly) {
// Announce, don't attach. conversationId stays empty, so a
// send without accepting mints a fresh thread on purpose.
root.offeredConversationId = latest.id;
root.resumeOffered(listConversations.agentId, latest.id,
latest.title ?? "", latest.updated_at ?? "");
return;
}
root._loadConversation(listConversations.agentId, latest.id);
}
}
onExited: exitCode => {
@ -292,8 +323,11 @@ Singleton {
}
}
function resumeLatestConversation() {
// offerOnly: fetch the latest thread but announce it instead of attaching.
// Defaults to false so every existing caller keeps its old behaviour.
function resumeLatestConversation(offerOnly) {
if (!root.serverUp || root.currentAgentId.length === 0 || root.turnActive) return false;
listConversations.offerOnly = (offerOnly === true);
listConversations.agentId = root.currentAgentId;
listConversations.command = [
"curl", "-sf", "--max-time", "5",
@ -303,6 +337,21 @@ Singleton {
return true;
}
// Take up an offer made by resumeOffered. No-op if the offer has gone
// stale (a turn started, or something else attached in the meantime).
function acceptOfferedResume() {
if (root.offeredConversationId.length === 0 || root.turnActive) return false;
if (root.conversationId.length > 0) return false;
root._loadConversation(root.currentAgentId, root.offeredConversationId);
root.offeredConversationId = "";
return true;
}
// Decline. The thread stays on the server; we simply start fresh.
function dismissOfferedResume() {
root.offeredConversationId = "";
}
function _loadConversation(agentId, conversationId) {
loadConversation.agentId = agentId;
loadConversation.requestedConversationId = conversationId;