diff --git a/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 78e347e..1d23168 100644 --- a/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "997519e7bf6b3bb52f6f766bab10b0bf950c6beb140aa897dc50ba77eaaa5801", + "originHash" : "fa70224ebd92e3bfa55fbdf5bbea5d2a51a1f6402e805a443a323509fbc905ed", "pins" : [ { "identity" : "async-http-client", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "7744c2a035c68ec14726c709f031835e3e30bde1", - "version" : "1.34.0" + "revision" : "4603a8036d921ea999fadb742931546c341f4bd7", + "version" : "1.35.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift.git", "state" : { - "revision" : "9ed8c8457e00ff9c7aedb3bf213f20a2cfdf509e", - "version" : "7.11.0" + "revision" : "b83108d10f42680d78f23fe4d4d80fc88dab3212", + "version" : "7.11.1" } }, { @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift-2.git", "state" : { - "revision" : "21fe69ab7ce0e87ac089534733c52f037e74a3eb", - "version" : "2.4.1" + "revision" : "28cdd63ef88583ddc67d7bb179eab46fab465ce9", + "version" : "2.4.2" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift-nio-transport.git", "state" : { - "revision" : "e7d463749f9037b047dcd6da4e633718ddc432b8", - "version" : "2.8.0" + "revision" : "2ca31f06658ed288a2560e23ad649acbb3d6b3a3", + "version" : "2.9.0" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/grpc/grpc-swift-protobuf.git", "state" : { - "revision" : "b05885fa9bdd88f1eab2e7162f1ee81340b0da33", - "version" : "2.4.0" + "revision" : "176c5a434fd76f6f479848d1a8f7d44967534168", + "version" : "2.4.1" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "d46d456107feacc80711b21847b82b07bd9fb46e", - "version" : "2.9.3" + "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", + "version" : "2.9.4" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-algorithms.git", "state" : { - "revision" : "d0b4a06d0f173a2f3be27d3ea21b3c3aa18db440", - "version" : "1.1.4" + "revision" : "3da39bbc4e687d4192af7c9cf4eab805745a0b9c", + "version" : "1.1.5" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-certificates.git", "state" : { - "revision" : "bde8ca32a096825dfce37467137c903418c1893d", - "version" : "1.19.1" + "revision" : "89fbc3714264cce8db8e4ec51b64e01c3e28c6c5", + "version" : "1.19.3" } }, { @@ -168,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "92448c359f00ebe36ae97d3bd9086f13c7692b5a", - "version" : "1.13.2" + "revision" : "a878e7f8f46cfc0e1125e565b5c08e7d5272dc9a", + "version" : "1.14.0" } }, { @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "77b84ac2cd2ac9e4ac67d19f045fd5b434f56967", - "version" : "2.101.0" + "revision" : "cd3e1152083706d77b223fb29110e590efcc70c0", + "version" : "2.101.2" } }, { @@ -186,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "d2eeec0339074034f11a040a74aa2a341a2c4506", - "version" : "1.34.1" + "revision" : "88a51340f59cf181ebde888bd1b749296b3ec029", + "version" : "1.34.3" } }, { @@ -258,8 +258,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "7502b711c92a17741fa625d722b0ccbd595d8ed1", - "version" : "1.7.2" + "revision" : "b5544ba79a70a0cb3563e75bf26dc198d6b40ed3", + "version" : "1.7.4" } }, { diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift index 0566120..6b24d46 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift @@ -204,7 +204,10 @@ enum SubagentRunState { init(group: ToolGroup) { if !group.finished { self = .running } - else if group.isError { self = .failed } + // A `Task` subagent surfaces failure as an error result. A `nucleic_subagent` worker + // instead returns a *non-error* result whose JSON envelope carries `{"ok": false}` (or + // `{"denied": true}`) in-band, so read that too — otherwise a failed worker reads "Done". + else if group.isError || Subagent.workerFailed(group) { self = .failed } else { self = .done } } @@ -234,9 +237,14 @@ enum Subagent { return (raw?.isEmpty == false) ? raw : nil } - /// The one-line task the parent handed the subagent (`description`), falling back to the - /// full `prompt`, then a generic label. + /// The one-line task the parent handed the subagent: a `nucleic_subagent` worker's `task` + /// label, or a `Task` subagent's `description`, falling back to the full `prompt`, then a + /// generic label. static func taskLabel(_ group: ToolGroup) -> String { + if let task = group.input["task"]?.stringValue? + .trimmingCharacters(in: .whitespacesAndNewlines), !task.isEmpty { + return task + } if let description = group.input["description"]?.stringValue? .trimmingCharacters(in: .whitespacesAndNewlines), !description.isEmpty { return description @@ -258,9 +266,48 @@ enum Subagent { /// The subagent's returned report, or nil while it's still working / reported nothing. static func report(_ group: ToolGroup) -> String? { guard let result = group.result else { return nil } + // A `nucleic_subagent` worker returns a JSON envelope — surface its actual `output` (or + // failure `message`) rather than a one-line gist of the raw `{ok,session_id,…}` object. A + // `Task` subagent returns prose, which falls through to the gist unchanged. + if let worker = workerText(result.resultText), !worker.isEmpty { return worker } let text = result.compactSummary return text.isEmpty ? nil : text } + + /// Whether a `nucleic_subagent` worker result reports failure — a `{"ok": false}` completion + /// or a `{"denied": true}` refusal, both delivered as non-error tool results. False for any + /// other tool (a `Task` subagent, whose failure is an error result handled separately). + static func workerFailed(_ group: ToolGroup) -> Bool { + guard let result = group.result, + let envelope = workerEnvelope(result.resultText) else { return false } + if let denied = envelope["denied"] as? Bool, denied { return true } + if let ok = envelope["ok"] as? Bool { return !ok } + return false + } + + /// The human-facing text of a `nucleic_subagent` worker envelope — the worker's `output` on + /// success, or its `message` on failure/denial — or nil when `text` isn't such an envelope + /// (so a `Task` subagent's prose report falls through unchanged). + private static func workerText(_ text: String) -> String? { + guard let envelope = workerEnvelope(text) else { return nil } + if let denied = envelope["denied"] as? Bool, denied { + return envelope["message"] as? String ?? "" + } + guard let ok = envelope["ok"] as? Bool else { return nil } + return ok ? (envelope["output"] as? String ?? "") + : (envelope["message"] as? String ?? "") + } + + /// Parses a `nucleic_subagent` result string into its JSON envelope, gated on the envelope's + /// signature keys (`ok` / `denied`) so an arbitrary JSON-shaped tool result isn't mistaken for + /// one. Nil for anything that isn't a worker envelope. + private static func workerEnvelope(_ text: String) -> [String: Any]? { + guard let data = text.data(using: .utf8), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + object["ok"] != nil || object["denied"] != nil + else { return nil } + return object + } } /// A small leading status glyph for a subagent — a gold spinner while it works, a green check or @@ -531,7 +578,7 @@ enum ToolGlyph { case "Edit", "Write", "MultiEdit", "NotebookEdit": return "pencil" case "Grep", "Glob", "Search": return "magnifyingglass" case "WebFetch", "WebSearch": return "globe" - case "Task", "Agent": return "person.2" + case "Task", "Agent", ToolGroup.orchestraSubagentToolName: return "person.2" case "TodoWrite": return "checklist" case "AskUserQuestion": return "questionmark.bubble" case HostCommandSummary.hostExecToolName: return "desktopcomputer" diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift index f53cb91..2b60f2e 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift @@ -76,8 +76,16 @@ struct ToolGroup: Equatable { struct FilePatch: Equatable { let path: String; let change: FileChange.ChangeKind } - /// Subagent orchestration (`Task`/`Agent`) gets the gold card treatment, like the Mac. - var isOrchestration: Bool { name == "Task" || name == "Agent" } + /// Nucleic's own Orchestra worker spawn — the qualified `nucleic_subagent` MCP wire name. + /// Unlike a `Task` subagent, a worker runs as its own observed session, so it carries no + /// nested `children` here; its card shows the final output it returned instead. + static let orchestraSubagentToolName = "mcp__nucleic__nucleic_subagent" + + /// Subagent orchestration — a `Task`/`Agent` spawn or a `nucleic_subagent` worker — gets the + /// gold card treatment, like the Mac. + var isOrchestration: Bool { + name == "Task" || name == "Agent" || name == Self.orchestraSubagentToolName + } var isAskUserQuestion: Bool { name == "AskUserQuestion" } /// The literal shell command a `Bash` call ran, if any (nil for every other tool). Used to