diff --git a/surfaces/quickshell/patches/0001-resume-offer.patch b/surfaces/quickshell/patches/0001-resume-offer.patch deleted file mode 100644 index 61f2fcd..0000000 --- a/surfaces/quickshell/patches/0001-resume-offer.patch +++ /dev/null @@ -1,123 +0,0 @@ -From 377a7542fef4422e1e3232a0c1d5f93ca6aaf1f5 Mon Sep 17 00:00:00 2001 -From: souveraine -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 - diff --git a/surfaces/quickshell/services/Souveraine.qml b/surfaces/quickshell/services/Souveraine.qml index 9b57702..a50edd5 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 => { @@ -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;