patches: correct the queue; retire 0002 as obsolete, clear landed 0003/0004
The queue section claimed 0001-0004 were all unapplied. Verified with git apply --check in both directions: 0003 landed as8c434df, 0004 as23cebc5, and 0002 can no longer apply at all. 0002 patched inThinkBlock in services/Ai.qml. That variable no longer exists - typed segments made the think-fence collision impossible by construction rather than by escaping harder. Retired, not abandoned. 0001 and 0006 were on disk and undescribed. Both now carry their reasoning here. 0007's block records the specific installed version it waits on, since 'needs the server first' is not actionable a month later. A queue is not an archive; git keeps history.
This commit is contained in:
parent
ba13d56afc
commit
cb3785bcbd
4 changed files with 48 additions and 657 deletions
|
|
@ -1,46 +0,0 @@
|
|||
From 03bd4f14a10d7703ba8d61fa66fdb2c97e630862 Mon Sep 17 00:00:00 2001
|
||||
From: Fimeg <casey.tunturi@gmail.com>
|
||||
Date: Tue, 11 Aug 2026 08:09:36 -0400
|
||||
Subject: [PATCH] shell: stop sensor calls closing an open reasoning fence
|
||||
|
||||
A tool_call/tool_return arriving while a <think> block was open emitted its
|
||||
own self-closing fence, whose </think> closed the outer block early. Every
|
||||
reasoning token after that point rendered as prose, and the stray close left
|
||||
the trailing assistant text mis-segmented. Append inside the open fence
|
||||
instead; only open one when arriving in prose.
|
||||
---
|
||||
surfaces/quickshell/services/Ai.qml | 13 +++++++++++--
|
||||
1 file changed, 11 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/surfaces/quickshell/services/Ai.qml b/surfaces/quickshell/services/Ai.qml
|
||||
index 0f64b86..cd065c8 100644
|
||||
--- a/surfaces/quickshell/services/Ai.qml
|
||||
+++ b/surfaces/quickshell/services/Ai.qml
|
||||
@@ -356,13 +356,22 @@ Singleton {
|
||||
root.appendToStreaming(event.content);
|
||||
break;
|
||||
case "tool_call_message": {
|
||||
+ // A sensor firing mid-reasoning must not close the reasoning
|
||||
+ // fence. When a <think> block is already open we append inside it;
|
||||
+ // only a sensor arriving in prose opens a fence of its own.
|
||||
const call = event.tool_call;
|
||||
- root.appendToStreaming(`\n\n<think>\nsensor: ${call.function.name}(${call.function.arguments})\n</think>\n`);
|
||||
+ const body = `sensor: ${call.function.name}(${call.function.arguments})`;
|
||||
+ root.appendToStreaming(root.inThinkBlock
|
||||
+ ? `\n${body}\n`
|
||||
+ : `\n\n<think>\n${body}\n</think>\n`);
|
||||
break;
|
||||
}
|
||||
case "tool_return_message": {
|
||||
const ret = event.tool_return;
|
||||
- root.appendToStreaming(`\n<think>\n[${ret.status}] ${ret.output}\n</think>\n`);
|
||||
+ const body = `[${ret.status}] ${ret.output}`;
|
||||
+ root.appendToStreaming(root.inThinkBlock
|
||||
+ ? `\n${body}\n`
|
||||
+ : `\n<think>\n${body}\n</think>\n`);
|
||||
break;
|
||||
}
|
||||
case "interstitial":
|
||||
--
|
||||
2.55.0
|
||||
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
From f297008054bea93babc5c56d1669f33f5803473e Mon Sep 17 00:00:00 2001
|
||||
From: Fimeg <casey.tunturi@gmail.com>
|
||||
Date: Tue, 11 Aug 2026 08:54:36 -0400
|
||||
Subject: [PATCH] shell: render typed chat segments
|
||||
|
||||
Keep assistant, reasoning, tool-call, and tool-return frames structured through the sidebar delegate. Tool cards bind returns by wire id and fold their output by default.
|
||||
|
||||
Reasoned from the SSE contract and existing TUI renderer. Diff check passed. Untested against a running shell: Casey applies and reloads staged patches.
|
||||
---
|
||||
.../ii/sidebarLeft/aiChat/AiMessage.qml | 15 +-
|
||||
.../ii/sidebarLeft/aiChat/ToolCallBlock.qml | 168 ++++++++++++++++++
|
||||
.../ii-base/services/ai/AiMessageData.qml | 4 +
|
||||
surfaces/quickshell/services/Ai.qml | 154 ++++++++++++----
|
||||
4 files changed, 299 insertions(+), 42 deletions(-)
|
||||
create mode 100644 surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/ToolCallBlock.qml
|
||||
|
||||
diff --git a/surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/AiMessage.qml b/surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/AiMessage.qml
|
||||
index 8a6dc2f..03cf283 100644
|
||||
--- a/surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/AiMessage.qml
|
||||
+++ b/surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/AiMessage.qml
|
||||
@@ -20,7 +20,14 @@ Rectangle {
|
||||
property bool renderMarkdown: true
|
||||
property bool editing: false
|
||||
|
||||
- property list<var> messageBlocks: StringUtils.splitMarkdownBlocks(root.messageData?.content)
|
||||
+ // Souveraine messages arrive as typed blocks. Legacy/provider messages
|
||||
+ // still use ii's markdown splitter, so the override remains compatible.
|
||||
+ property var messageBlocks: {
|
||||
+ const segments = root.messageData ? root.messageData.segments : [];
|
||||
+ return segments.length > 0
|
||||
+ ? segments
|
||||
+ : StringUtils.splitMarkdownBlocks(root.messageData?.content);
|
||||
+ }
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
@@ -358,7 +365,10 @@ Rectangle {
|
||||
segmentContent: modelData.content
|
||||
messageData: root.messageData
|
||||
done: root.messageData?.done ?? false
|
||||
- completed: modelData.completed ?? false
|
||||
+ completed: modelData.completed ?? (root.messageData?.done ?? false)
|
||||
+ } }
|
||||
+ DelegateChoice { roleValue: "tool"; ToolCallBlock {
|
||||
+ segment: modelData
|
||||
} }
|
||||
DelegateChoice { roleValue: "text"; MessageTextBlock {
|
||||
editing: root.editing
|
||||
@@ -410,4 +420,3 @@ Rectangle {
|
||||
|
||||
}
|
||||
}
|
||||
-
|
||||
diff --git a/surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/ToolCallBlock.qml b/surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/ToolCallBlock.qml
|
||||
new file mode 100644
|
||||
index 0000000..452eac3
|
||||
--- /dev/null
|
||||
+++ b/surfaces/quickshell/ii-base/modules/ii/sidebarLeft/aiChat/ToolCallBlock.qml
|
||||
@@ -0,0 +1,168 @@
|
||||
+pragma ComponentBehavior: Bound
|
||||
+
|
||||
+import qs.modules.common
|
||||
+import qs.modules.common.widgets
|
||||
+import QtQuick
|
||||
+import QtQuick.Controls
|
||||
+import QtQuick.Layouts
|
||||
+
|
||||
+Item {
|
||||
+ id: root
|
||||
+
|
||||
+ property var segment: ({})
|
||||
+ property bool expanded: segment.status === "running" || segment.failed === true
|
||||
+ property string name: String(segment.name ?? "tool")
|
||||
+ property string status: String(segment.status ?? "running")
|
||||
+ property string output: String(segment.output ?? "")
|
||||
+ property bool failed: segment.failed === true
|
||||
+ property string summary: toolSummary()
|
||||
+
|
||||
+ Layout.fillWidth: true
|
||||
+ implicitHeight: card.implicitHeight
|
||||
+
|
||||
+ function clip(text, max) {
|
||||
+ const value = String(text ?? "");
|
||||
+ return value.length > max ? value.slice(0, Math.max(0, max - 1)) + "…" : value;
|
||||
+ }
|
||||
+
|
||||
+ function argumentsObject() {
|
||||
+ try {
|
||||
+ const parsed = JSON.parse(String(segment.arguments ?? "{}"));
|
||||
+ return parsed && typeof parsed === "object" ? parsed : {};
|
||||
+ } catch (error) {
|
||||
+ return {};
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ function toolSummary() {
|
||||
+ const args = argumentsObject();
|
||||
+ switch (name) {
|
||||
+ case "bash": return "$ " + clip(args.command ?? segment.arguments, 88);
|
||||
+ case "read": return clip(args.path ?? segment.arguments, 88);
|
||||
+ case "write": return "write → " + clip(args.path ?? segment.arguments, 78);
|
||||
+ case "edit": return "edit → " + clip(args.path ?? segment.arguments, 80);
|
||||
+ case "grep": return "\"" + clip(args.pattern ?? segment.arguments, 48) + "\" " + clip(args.path ?? "", 30);
|
||||
+ case "glob": return clip(args.pattern ?? segment.arguments, 88);
|
||||
+ case "list_dir": return clip(args.path ?? ".", 88);
|
||||
+ case "memory": return clip((args.command ?? "list") + " " + (args.path ?? ""), 88);
|
||||
+ default: return clip(segment.arguments ?? "", 88);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ function iconForTool() {
|
||||
+ switch (name) {
|
||||
+ case "bash": return "terminal";
|
||||
+ case "read": return "article";
|
||||
+ case "write":
|
||||
+ case "edit": return "edit_note";
|
||||
+ case "grep": return "manage_search";
|
||||
+ case "glob":
|
||||
+ case "list_dir": return "folder";
|
||||
+ case "memory": return "psychology";
|
||||
+ default: return "sensors";
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ function statusLabel() {
|
||||
+ if (status === "running") return Translation.tr("running");
|
||||
+ if (status === "unresolved") return Translation.tr("unresolved");
|
||||
+ if (failed) return Translation.tr("failed");
|
||||
+ return Translation.tr("done");
|
||||
+ }
|
||||
+
|
||||
+ Rectangle {
|
||||
+ id: card
|
||||
+ width: parent.width
|
||||
+ implicitHeight: cardLayout.implicitHeight + 14
|
||||
+ radius: Appearance.rounding.small
|
||||
+ color: root.failed ? Appearance.colors.colErrorContainer : Appearance.colors.colLayer2
|
||||
+ border.width: 1
|
||||
+ border.color: root.failed ? Appearance.colors.colError : Appearance.colors.colOutlineVariant
|
||||
+
|
||||
+ ColumnLayout {
|
||||
+ id: cardLayout
|
||||
+ anchors.fill: parent
|
||||
+ anchors.margins: 7
|
||||
+ spacing: 5
|
||||
+
|
||||
+ MouseArea {
|
||||
+ id: header
|
||||
+ Layout.fillWidth: true
|
||||
+ implicitHeight: headerRow.implicitHeight
|
||||
+ hoverEnabled: true
|
||||
+ enabled: root.status !== "running" || root.output.length > 0
|
||||
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
+ onClicked: root.expanded = !root.expanded
|
||||
+
|
||||
+ RowLayout {
|
||||
+ id: headerRow
|
||||
+ anchors.fill: parent
|
||||
+ spacing: 7
|
||||
+
|
||||
+ MaterialSymbol {
|
||||
+ text: root.iconForTool()
|
||||
+ iconSize: Appearance.font.pixelSize.large
|
||||
+ color: root.failed ? Appearance.colors.colError : Appearance.colors.colPrimary
|
||||
+ }
|
||||
+ StyledText {
|
||||
+ Layout.fillWidth: false
|
||||
+ font.pixelSize: Appearance.font.pixelSize.small
|
||||
+ font.bold: true
|
||||
+ text: root.name
|
||||
+ color: Appearance.colors.colOnLayer2
|
||||
+ }
|
||||
+ StyledText {
|
||||
+ Layout.fillWidth: true
|
||||
+ elide: Text.ElideRight
|
||||
+ font.pixelSize: Appearance.font.pixelSize.small
|
||||
+ text: root.summary
|
||||
+ color: Appearance.colors.colSubtext
|
||||
+ }
|
||||
+ MaterialSymbol {
|
||||
+ visible: root.status === "running"
|
||||
+ text: "sync"
|
||||
+ iconSize: Appearance.font.pixelSize.normal
|
||||
+ color: Appearance.colors.colPrimary
|
||||
+ RotationAnimation on rotation {
|
||||
+ running: root.status === "running"
|
||||
+ from: 0
|
||||
+ to: 360
|
||||
+ duration: 900
|
||||
+ loops: Animation.Infinite
|
||||
+ }
|
||||
+ }
|
||||
+ StyledText {
|
||||
+ visible: root.status !== "running"
|
||||
+ font.pixelSize: Appearance.font.pixelSize.small
|
||||
+ text: root.statusLabel()
|
||||
+ color: root.failed ? Appearance.colors.colError : Appearance.colors.colSubtext
|
||||
+ }
|
||||
+ MaterialSymbol {
|
||||
+ visible: header.enabled
|
||||
+ text: root.expanded ? "expand_less" : "expand_more"
|
||||
+ iconSize: Appearance.font.pixelSize.normal
|
||||
+ color: Appearance.colors.colSubtext
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ TextArea {
|
||||
+ Layout.fillWidth: true
|
||||
+ visible: root.expanded && root.output.length > 0
|
||||
+ implicitHeight: visible ? Math.min(contentHeight + topPadding + bottomPadding, 240) : 0
|
||||
+ readOnly: true
|
||||
+ selectByMouse: true
|
||||
+ wrapMode: TextArea.Wrap
|
||||
+ textFormat: TextEdit.PlainText
|
||||
+ text: root.output
|
||||
+ font.family: Appearance.font.family.monospace
|
||||
+ font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
+ color: root.failed ? Appearance.colors.colOnErrorContainer : Appearance.colors.colOnLayer2
|
||||
+ background: Rectangle {
|
||||
+ radius: Appearance.rounding.small / 2
|
||||
+ color: Appearance.colors.colLayer1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/surfaces/quickshell/ii-base/services/ai/AiMessageData.qml b/surfaces/quickshell/ii-base/services/ai/AiMessageData.qml
|
||||
index 3db787b..9c3b5eb 100644
|
||||
--- a/surfaces/quickshell/ii-base/services/ai/AiMessageData.qml
|
||||
+++ b/surfaces/quickshell/ii-base/services/ai/AiMessageData.qml
|
||||
@@ -7,6 +7,10 @@ QtObject {
|
||||
property string role
|
||||
property string content
|
||||
property string rawContent
|
||||
+ // First-class stream blocks. The Souveraine adapter fills these from the
|
||||
+ // typed SSE events; legacy/provider messages leave this empty and the
|
||||
+ // existing markdown splitter remains their renderer.
|
||||
+ property var segments: []
|
||||
property string fileMimeType
|
||||
property string fileUri
|
||||
property string localFilePath
|
||||
diff --git a/surfaces/quickshell/services/Ai.qml b/surfaces/quickshell/services/Ai.qml
|
||||
index cd065c8..aaec22f 100644
|
||||
--- a/surfaces/quickshell/services/Ai.qml
|
||||
+++ b/surfaces/quickshell/services/Ai.qml
|
||||
@@ -230,7 +230,6 @@ Singleton {
|
||||
|
||||
// ── Streaming message shaping ────────────────────────────────────────
|
||||
property AiMessageData streamingMessage
|
||||
- property bool inThinkBlock: false
|
||||
|
||||
// ── Subconscious three-tier visibility ───────────────────────────────
|
||||
// See docs/tasks/subconscious-surfacing-threshold.md. The subconscious's
|
||||
@@ -253,6 +252,89 @@ Singleton {
|
||||
root.streamingMessage.content += text;
|
||||
}
|
||||
|
||||
+ // The server already says what each frame is. Keep that fact attached to
|
||||
+ // the message instead of smuggling it through markdown fences and asking
|
||||
+ // the delegate to parse it back out again.
|
||||
+ function appendStreamingTextSegment(type, content) {
|
||||
+ if (!root.streamingMessage) return;
|
||||
+ const text = String(content ?? "");
|
||||
+ if (text.length === 0) return;
|
||||
+ const segments = root.streamingMessage.segments ?? [];
|
||||
+ const last = segments.length > 0 ? segments[segments.length - 1] : null;
|
||||
+ if (last && last.type === type) {
|
||||
+ root.streamingMessage.segments = [
|
||||
+ ...segments.slice(0, -1),
|
||||
+ { ...last, content: String(last.content ?? "") + text }
|
||||
+ ];
|
||||
+ } else {
|
||||
+ root.streamingMessage.segments = [...segments, { type, content: text }];
|
||||
+ }
|
||||
+ // `content` remains a plain compatibility projection for Copy, TTS,
|
||||
+ // and old snapshot consumers. It no longer drives the renderer.
|
||||
+ root.appendToStreaming(text);
|
||||
+ }
|
||||
+
|
||||
+ function appendToolCallSegment(call, round) {
|
||||
+ if (!root.streamingMessage) return;
|
||||
+ const tool = call ?? {};
|
||||
+ const fn = tool.function ?? {};
|
||||
+ const name = String(fn.name ?? "tool");
|
||||
+ const arguments = String(fn.arguments ?? "");
|
||||
+ const id = String(tool.id ?? "");
|
||||
+ root.streamingMessage.segments = [...root.streamingMessage.segments, {
|
||||
+ type: "tool",
|
||||
+ id,
|
||||
+ name,
|
||||
+ arguments,
|
||||
+ round: Number(round ?? 0),
|
||||
+ status: "running",
|
||||
+ output: "",
|
||||
+ failed: false,
|
||||
+ }];
|
||||
+ root.appendToStreaming(`\n${name}(${arguments})\n`);
|
||||
+ }
|
||||
+
|
||||
+ function bindToolReturnSegment(toolReturn) {
|
||||
+ if (!root.streamingMessage) return;
|
||||
+ const result = toolReturn ?? {};
|
||||
+ const segments = root.streamingMessage.segments ?? [];
|
||||
+ const resultId = String(result.id ?? "");
|
||||
+ let index = -1;
|
||||
+ if (resultId.length > 0) {
|
||||
+ index = segments.findIndex(segment => segment.type === "tool" && segment.id === resultId);
|
||||
+ }
|
||||
+ // Older servers did not include the tool id. Bind their return to the
|
||||
+ // newest still-running call rather than silently inventing a second one.
|
||||
+ if (index < 0) {
|
||||
+ for (let i = segments.length - 1; i >= 0; i--) {
|
||||
+ if (segments[i].type === "tool" && segments[i].status === "running") {
|
||||
+ index = i;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ const status = String(result.status ?? "done");
|
||||
+ const failed = /error|fail/i.test(status);
|
||||
+ const output = String(result.output ?? "");
|
||||
+ if (index >= 0) {
|
||||
+ root.streamingMessage.segments = segments.map((segment, i) => i === index
|
||||
+ ? { ...segment, status, output, failed }
|
||||
+ : segment);
|
||||
+ } else {
|
||||
+ root.streamingMessage.segments = [...segments, {
|
||||
+ type: "tool",
|
||||
+ id: resultId,
|
||||
+ name: String(result.name ?? "tool"),
|
||||
+ arguments: "",
|
||||
+ round: 0,
|
||||
+ status,
|
||||
+ output,
|
||||
+ failed,
|
||||
+ }];
|
||||
+ }
|
||||
+ root.appendToStreaming(`\n[${status}] ${output}\n`);
|
||||
+ }
|
||||
+
|
||||
// Push a line onto the Tier-1 live stream. kind ∈ token|tool_call|tool_result.
|
||||
// Consecutive tokens are coalesced onto the current line so the ticker reads
|
||||
// as a thought forming (a sentence), not a single flickering word replaced
|
||||
@@ -308,10 +390,12 @@ Singleton {
|
||||
|
||||
function finishStreaming() {
|
||||
if (!root.streamingMessage) return;
|
||||
- if (root.inThinkBlock) {
|
||||
- root.appendToStreaming("\n</think>\n");
|
||||
- root.inThinkBlock = false;
|
||||
- }
|
||||
+ root.streamingMessage.segments = (root.streamingMessage.segments ?? []).map(segment => {
|
||||
+ if (segment.type === "tool" && segment.status === "running") {
|
||||
+ return { ...segment, status: "unresolved", failed: true };
|
||||
+ }
|
||||
+ return segment;
|
||||
+ });
|
||||
root.streamingMessage.thinking = false;
|
||||
root.streamingMessage.done = true;
|
||||
// If the turn finished while the session is locked, the finished
|
||||
@@ -342,36 +426,17 @@ Singleton {
|
||||
|
||||
switch (event.message_type) {
|
||||
case "assistant_message":
|
||||
- if (root.inThinkBlock) {
|
||||
- root.appendToStreaming("\n</think>\n");
|
||||
- root.inThinkBlock = false;
|
||||
- }
|
||||
- root.appendToStreaming(event.content);
|
||||
+ root.appendStreamingTextSegment("text", event.content);
|
||||
break;
|
||||
case "reasoning_message":
|
||||
- if (!root.inThinkBlock) {
|
||||
- root.appendToStreaming("\n<think>\n");
|
||||
- root.inThinkBlock = true;
|
||||
- }
|
||||
- root.appendToStreaming(event.content);
|
||||
+ root.appendStreamingTextSegment("think", event.content);
|
||||
break;
|
||||
case "tool_call_message": {
|
||||
- // A sensor firing mid-reasoning must not close the reasoning
|
||||
- // fence. When a <think> block is already open we append inside it;
|
||||
- // only a sensor arriving in prose opens a fence of its own.
|
||||
- const call = event.tool_call;
|
||||
- const body = `sensor: ${call.function.name}(${call.function.arguments})`;
|
||||
- root.appendToStreaming(root.inThinkBlock
|
||||
- ? `\n${body}\n`
|
||||
- : `\n\n<think>\n${body}\n</think>\n`);
|
||||
+ root.appendToolCallSegment(event.tool_call, event.round);
|
||||
break;
|
||||
}
|
||||
case "tool_return_message": {
|
||||
- const ret = event.tool_return;
|
||||
- const body = `[${ret.status}] ${ret.output}`;
|
||||
- root.appendToStreaming(root.inThinkBlock
|
||||
- ? `\n${body}\n`
|
||||
- : `\n<think>\n${body}\n</think>\n`);
|
||||
+ root.bindToolReturnSegment(event.tool_return);
|
||||
break;
|
||||
}
|
||||
case "interstitial":
|
||||
@@ -455,13 +520,14 @@ Singleton {
|
||||
}
|
||||
|
||||
// ── Message store ────────────────────────────────────────────────────
|
||||
- function addMessage(message, role) {
|
||||
+ function addMessage(message, role, segments = []) {
|
||||
if (message.length === 0) return;
|
||||
const aiMessage = aiMessageComponent.createObject(root, {
|
||||
"role": role,
|
||||
"model": Souveraine.currentAgentId,
|
||||
"content": message,
|
||||
"rawContent": message,
|
||||
+ "segments": segments,
|
||||
"thinking": false,
|
||||
"done": true,
|
||||
});
|
||||
@@ -494,7 +560,6 @@ Singleton {
|
||||
root.messageIDs = [];
|
||||
root.messageByID = ({});
|
||||
root.streamingMessage = null;
|
||||
- root.inThinkBlock = false;
|
||||
root.subconsciousActive = false;
|
||||
root.subconsciousStream = [];
|
||||
root.tokenCount.input = -1;
|
||||
@@ -502,21 +567,32 @@ Singleton {
|
||||
root.tokenCount.total = -1;
|
||||
|
||||
messages.forEach(message => {
|
||||
- const content = (message.blocks ?? []).map(block => {
|
||||
+ const segments = (message.blocks ?? []).map(block => {
|
||||
switch (block.type) {
|
||||
- case "text": return block.text ?? "";
|
||||
- case "reasoning": return `<think>\n${block.reasoning ?? ""}\n</think>`;
|
||||
- case "tool_use": return `sensor: ${block.name ?? "?"}(${block.input ?? ""})`;
|
||||
- case "tool_result": return `[${block.tool_name ?? "tool"}] ${block.output ?? ""}`;
|
||||
- case "image": return "[image]";
|
||||
- default: return "";
|
||||
+ case "text": return { type: "text", content: block.text ?? "" };
|
||||
+ case "reasoning": return { type: "think", content: block.reasoning ?? "" };
|
||||
+ case "tool_use": return {
|
||||
+ type: "tool", id: block.id ?? "", name: block.name ?? "tool",
|
||||
+ arguments: block.input ?? "", round: 0, status: "done", output: "", failed: false,
|
||||
+ };
|
||||
+ case "tool_result": return {
|
||||
+ type: "tool", id: block.tool_use_id ?? "", name: block.tool_name ?? "tool",
|
||||
+ arguments: "", round: 0, status: block.is_error ? "error" : "done",
|
||||
+ output: block.output ?? "", failed: block.is_error ?? false,
|
||||
+ };
|
||||
+ case "image": return { type: "text", content: "[image]" };
|
||||
+ default: return null;
|
||||
}
|
||||
+ }).filter(Boolean);
|
||||
+ const content = segments.map(segment => {
|
||||
+ if (segment.type === "tool") return `${segment.name}(${segment.arguments})\n${segment.output}`;
|
||||
+ return segment.content;
|
||||
}).filter(Boolean).join("\n");
|
||||
if (content.length === 0) return;
|
||||
const role = message.role === "user"
|
||||
? "user"
|
||||
: message.role === "assistant" ? "assistant" : root.interfaceRole;
|
||||
- root.addMessage(content, role);
|
||||
+ root.addMessage(content, role, segments);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -554,12 +630,12 @@ Singleton {
|
||||
// Set up the streaming assistant message container. Called after a
|
||||
// successful send (or after a step-up auth retry succeeds).
|
||||
function _startStreaming() {
|
||||
- root.inThinkBlock = false;
|
||||
root.streamingMessage = root.aiMessageComponent.createObject(root, {
|
||||
"role": "assistant",
|
||||
"model": Souveraine.currentAgentId,
|
||||
"content": "",
|
||||
"rawContent": "",
|
||||
+ "segments": [],
|
||||
"thinking": true,
|
||||
"done": false,
|
||||
});
|
||||
--
|
||||
2.55.0
|
||||
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
From 23cebc531dd40ac23c31472214fb93eb28ea2342 Mon Sep 17 00:00:00 2001
|
||||
From: Fimeg <casey.tunturi@gmail.com>
|
||||
Date: Tue, 11 Aug 2026 09:35:53 -0400
|
||||
Subject: [PATCH] shell: keep typed segments QML-compatible
|
||||
|
||||
---
|
||||
surfaces/quickshell/services/Ai.qml | 59 +++++++++++++++++++++--------
|
||||
1 file changed, 43 insertions(+), 16 deletions(-)
|
||||
|
||||
diff --git a/surfaces/quickshell/services/Ai.qml b/surfaces/quickshell/services/Ai.qml
|
||||
index aaec22f..64f269a 100644
|
||||
--- a/surfaces/quickshell/services/Ai.qml
|
||||
+++ b/surfaces/quickshell/services/Ai.qml
|
||||
@@ -262,12 +262,13 @@ Singleton {
|
||||
const segments = root.streamingMessage.segments ?? [];
|
||||
const last = segments.length > 0 ? segments[segments.length - 1] : null;
|
||||
if (last && last.type === type) {
|
||||
- root.streamingMessage.segments = [
|
||||
- ...segments.slice(0, -1),
|
||||
- { ...last, content: String(last.content ?? "") + text }
|
||||
- ];
|
||||
+ const replacement = {
|
||||
+ type: last.type,
|
||||
+ content: String(last.content ?? "") + text,
|
||||
+ };
|
||||
+ root.streamingMessage.segments = segments.slice(0, -1).concat([replacement]);
|
||||
} else {
|
||||
- root.streamingMessage.segments = [...segments, { type, content: text }];
|
||||
+ root.streamingMessage.segments = segments.concat([{ type, content: text }]);
|
||||
}
|
||||
// `content` remains a plain compatibility projection for Copy, TTS,
|
||||
// and old snapshot consumers. It no longer drives the renderer.
|
||||
@@ -281,7 +282,8 @@ Singleton {
|
||||
const name = String(fn.name ?? "tool");
|
||||
const arguments = String(fn.arguments ?? "");
|
||||
const id = String(tool.id ?? "");
|
||||
- root.streamingMessage.segments = [...root.streamingMessage.segments, {
|
||||
+ const nextSegments = root.streamingMessage.segments.slice();
|
||||
+ nextSegments.push({
|
||||
type: "tool",
|
||||
id,
|
||||
name,
|
||||
@@ -290,7 +292,8 @@ Singleton {
|
||||
status: "running",
|
||||
output: "",
|
||||
failed: false,
|
||||
- }];
|
||||
+ });
|
||||
+ root.streamingMessage.segments = nextSegments;
|
||||
root.appendToStreaming(`\n${name}(${arguments})\n`);
|
||||
}
|
||||
|
||||
@@ -317,11 +320,22 @@ Singleton {
|
||||
const failed = /error|fail/i.test(status);
|
||||
const output = String(result.output ?? "");
|
||||
if (index >= 0) {
|
||||
- root.streamingMessage.segments = segments.map((segment, i) => i === index
|
||||
- ? { ...segment, status, output, failed }
|
||||
- : segment);
|
||||
+ const nextSegments = segments.slice();
|
||||
+ const previous = segments[index];
|
||||
+ nextSegments[index] = {
|
||||
+ type: previous.type,
|
||||
+ id: previous.id,
|
||||
+ name: previous.name,
|
||||
+ arguments: previous.arguments,
|
||||
+ round: previous.round,
|
||||
+ status,
|
||||
+ output,
|
||||
+ failed,
|
||||
+ };
|
||||
+ root.streamingMessage.segments = nextSegments;
|
||||
} else {
|
||||
- root.streamingMessage.segments = [...segments, {
|
||||
+ const nextSegments = segments.slice();
|
||||
+ nextSegments.push({
|
||||
type: "tool",
|
||||
id: resultId,
|
||||
name: String(result.name ?? "tool"),
|
||||
@@ -330,7 +344,8 @@ Singleton {
|
||||
status,
|
||||
output,
|
||||
failed,
|
||||
- }];
|
||||
+ });
|
||||
+ root.streamingMessage.segments = nextSegments;
|
||||
}
|
||||
root.appendToStreaming(`\n[${status}] ${output}\n`);
|
||||
}
|
||||
@@ -390,12 +405,24 @@ Singleton {
|
||||
|
||||
function finishStreaming() {
|
||||
if (!root.streamingMessage) return;
|
||||
- root.streamingMessage.segments = (root.streamingMessage.segments ?? []).map(segment => {
|
||||
+ const resolvedSegments = [];
|
||||
+ for (const segment of (root.streamingMessage.segments ?? [])) {
|
||||
if (segment.type === "tool" && segment.status === "running") {
|
||||
- return { ...segment, status: "unresolved", failed: true };
|
||||
+ resolvedSegments.push({
|
||||
+ type: segment.type,
|
||||
+ id: segment.id,
|
||||
+ name: segment.name,
|
||||
+ arguments: segment.arguments,
|
||||
+ round: segment.round,
|
||||
+ status: "unresolved",
|
||||
+ output: segment.output,
|
||||
+ failed: true,
|
||||
+ });
|
||||
+ } else {
|
||||
+ resolvedSegments.push(segment);
|
||||
}
|
||||
- return segment;
|
||||
- });
|
||||
+ }
|
||||
+ root.streamingMessage.segments = resolvedSegments;
|
||||
root.streamingMessage.thinking = false;
|
||||
root.streamingMessage.done = true;
|
||||
// If the turn finished while the session is locked, the finished
|
||||
--
|
||||
2.55.0
|
||||
|
||||
|
|
@ -75,13 +75,45 @@ again.
|
|||
|
||||
## In the queue
|
||||
|
||||
`0001`–`0004` are still on disk and still unapplied — resume-offer,
|
||||
think-fence collision, typed chat segments, and the QML-compat follow-up. They
|
||||
carry their own notes in their commit messages; none has been described here.
|
||||
Anything applied or retired is deleted from this directory by design.
|
||||
Status below is from `git apply --check` in **both directions**, not from
|
||||
memory: forward-applies means pending, reverse-applies means already landed.
|
||||
Run it before trusting this list — a queue file is a claim with a shelf life.
|
||||
|
||||
for p in surfaces/quickshell/patches/000*.patch; do
|
||||
git apply --check "$p" 2>/dev/null && echo "$p PENDING"
|
||||
git apply --check --reverse "$p" 2>/dev/null && echo "$p LANDED"
|
||||
done
|
||||
|
||||
**Apply order.** 0009 before or with 0008; 0007 last, after the server upgrade.
|
||||
The rest are independent.
|
||||
|
||||
- **0001 — resume offer.** Pending since 2026-08-10. The agent-established hook
|
||||
*offers* the latest thread instead of silently attaching it; default becomes
|
||||
start-fresh, `ai.autoResume: true` restores the old behaviour. Explicit
|
||||
resume paths are unchanged. The second half is undrawn — `resumeOffered`
|
||||
fires and nothing renders it, so with no UI the behaviour is still correct
|
||||
(doing nothing starts fresh) but the affordance to continue is missing.
|
||||
- **0006 — step-up authenticates through PAM.** Rook's, not mine.
|
||||
`StepUpAuth` called `souveraine-pam-auth` — a binary that was never written —
|
||||
and fell back to `pkcheck` against an action that was never shipped, so
|
||||
**every step-up grant request was silently denied**. Runs a real `PamContext`
|
||||
against `souveraine-stepup`, which `cc541d1` now ships. Note
|
||||
`fpc-polkit-pam.c:86` refuses every service except `polkit-1`, so step-up is
|
||||
password rather than finger until that check widens — one line, and a
|
||||
security decision that is Casey's.
|
||||
- **0007 — render context occupancy; stop reading hyprctl errors as cursor.**
|
||||
Needs the server carrying `f266136` first, or the pill honestly shows `—`.
|
||||
**Blocked on the server.** Needs `f266136` running, not merely installed.
|
||||
As of 2026-08-12 the box runs `souveraine 0.1.r442.gfb5e9bb80cac-1`, and
|
||||
`fb5e9bb` *predates* `f266136` — edge has not rebuilt. Applied early the pill
|
||||
honestly shows `—`, which looks exactly like the patch failing.
|
||||
The bug it fixes: `ContextPressure` crossed a module boundary as a
|
||||
**positional tuple**, so every consumer guessed what the second element was.
|
||||
The TUI guessed `limit` and was right; the HTTP layer named it `tokens` and
|
||||
was wrong, so the Panel printed the *ceiling* (250000) as the *usage*.
|
||||
Also: `command -v hyprctl` succeeds on the phone (installed as a Lua-eval
|
||||
shim), so it runs under viewtop, fails, and its error text is captured **as
|
||||
the cursor position** — the probe tested whether the tool was installed, not
|
||||
whether it answered.
|
||||
- **0008 — owned message delegate.** Parity resolved by `df11bba`; see the
|
||||
header. Apply with or after 0009.
|
||||
- **0009 — speech: make stop actually stop, and re-synthesis actually
|
||||
|
|
@ -104,6 +136,16 @@ Anything applied or retired is deleted from this directory by design.
|
|||
|
||||
## Recently closed
|
||||
|
||||
- **0002 — think-fence collision. Retired 2026-08-12, obsolete not abandoned.**
|
||||
It patched `inThinkBlock` in `services/Ai.qml`; `grep -c inThinkBlock` on
|
||||
that file now returns **0**. Typed chat segments (0003) made the collision
|
||||
impossible *by construction* — a sensor firing mid-reasoning lands in its own
|
||||
segment and cannot close a fence, because there is no fence. Escaping harder
|
||||
was the right fix for the string design; the design changed underneath it.
|
||||
A queued patch that can no longer apply reads as "not done yet" forever, so
|
||||
it goes.
|
||||
- **0003 — typed chat segments + tool cards.** Landed as `8c434df`.
|
||||
- **0004 — keep typed segments QML-compatible.** Landed as `23cebc5`.
|
||||
- **0005 — config: never store a null option.** Applied and committed as
|
||||
`f7e0e51`. Null-valued options serialised into `config.json` and segfaulted
|
||||
`JsonAdapter::deserializeRec` on the *next* launch — the session that wrote
|
||||
|
|
|
|||
Loading…
Reference in a new issue