|
|
|
@@ -29,7 +29,7 @@ enum ToolRailMetrics {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The hairline a run of tool lines hangs off. Draws no fill and no border: nested inside a gold
|
|
|
|
|
/// subagent card it adds a rule, not another box.
|
|
|
|
|
/// delegation card it adds a rule, not another box.
|
|
|
|
|
struct ToolRail<Content: View>: View {
|
|
|
|
|
/// Overrides the hairline colour — the danger tint when a call in the run failed.
|
|
|
|
|
var tint: Color? = nil
|
|
|
|
@@ -228,22 +228,24 @@ struct ToolBlockCard: View {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Subagent & multi-agent cards
|
|
|
|
|
// MARK: - Delegation & multi-agent cards
|
|
|
|
|
//
|
|
|
|
|
// Claude Code spawns subagents through the `Task` tool. Nucleic surfaces that delegated work in
|
|
|
|
|
// the orchestra gold so it reads distinctly from the agent's own tool calls: a lone `Task`
|
|
|
|
|
// becomes a `SubagentCard`, and a parallel wave of them becomes an `OrchestrationCard`. Both
|
|
|
|
|
// build from the projection's nested `children` (a subagent's inner Read/Bash/Edit/thinking),
|
|
|
|
|
// mirroring the Mac at phone fidelity.
|
|
|
|
|
// Two things land here: Claude Code's own subagents, spawned through the `Task` tool, and
|
|
|
|
|
// Nucleic's managed workers, spawned through `nucleic_worker`. Both are delegated work, so both
|
|
|
|
|
// get the orchestra gold that sets them apart from the agent's own tool calls: a lone spawn
|
|
|
|
|
// becomes a `SubagentCard` (which names itself Worker or Subagent accordingly), and a parallel
|
|
|
|
|
// wave becomes an `OrchestrationCard`. Both build from the projection's nested `children` (the
|
|
|
|
|
// delegate's inner Read/Bash/Edit/thinking), mirroring the Mac at phone fidelity.
|
|
|
|
|
|
|
|
|
|
/// The lifecycle state of a spawned subagent, read from its `Task` group: not finished → still
|
|
|
|
|
/// working; an error result → failed; otherwise → done.
|
|
|
|
|
enum SubagentRunState {
|
|
|
|
|
/// The lifecycle state of a spawned agent, read from its group: not finished → still working;
|
|
|
|
|
/// an error result → failed; otherwise → done. Shared by the worker and `Task` cards, so the
|
|
|
|
|
/// two speak one status vocabulary.
|
|
|
|
|
enum WorkerRunState {
|
|
|
|
|
case running, done, failed
|
|
|
|
|
|
|
|
|
|
init(group: ToolGroup) {
|
|
|
|
|
if !group.finished { self = .running }
|
|
|
|
|
// A `Task` subagent surfaces failure as an error result. A `nucleic_subagent` worker
|
|
|
|
|
// A `Task` subagent surfaces failure as an error result. A `nucleic_worker` 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 }
|
|
|
|
@@ -276,7 +278,7 @@ enum Subagent {
|
|
|
|
|
return (raw?.isEmpty == false) ? raw : nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The one-line task the parent handed the subagent: a `nucleic_subagent` worker's `task`
|
|
|
|
|
/// The one-line task the parent handed the subagent: a `nucleic_worker` 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 {
|
|
|
|
@@ -305,7 +307,7 @@ 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
|
|
|
|
|
// A `nucleic_worker` 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 unchanged — via `resultText`, so a
|
|
|
|
|
// report delivered as an MCP content array reads as the report and not as "[1 items]".
|
|
|
|
@@ -314,7 +316,7 @@ enum Subagent {
|
|
|
|
|
return text.isEmpty ? nil : text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether a `nucleic_subagent` worker result reports failure — a `{"ok": false}` completion
|
|
|
|
|
/// Whether a `nucleic_worker` 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 {
|
|
|
|
@@ -325,7 +327,7 @@ enum Subagent {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The human-facing text of a `nucleic_subagent` worker envelope — the worker's `output` on
|
|
|
|
|
/// The human-facing text of a `nucleic_worker` 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? {
|
|
|
|
@@ -338,7 +340,7 @@ enum Subagent {
|
|
|
|
|
: (envelope["message"] as? String ?? "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parses a `nucleic_subagent` result string into its JSON envelope, gated on the envelope's
|
|
|
|
|
/// Parses a `nucleic_worker` 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]? {
|
|
|
|
@@ -350,10 +352,10 @@ enum Subagent {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A small leading status glyph for a subagent — a gold spinner while it works, a green check or
|
|
|
|
|
/// A small leading status glyph for a worker — a gold spinner while it works, a green check or
|
|
|
|
|
/// red cross once it settles.
|
|
|
|
|
struct SubagentStatusGlyph: View {
|
|
|
|
|
let state: SubagentRunState
|
|
|
|
|
struct WorkerStatusGlyph: View {
|
|
|
|
|
let state: WorkerRunState
|
|
|
|
|
var body: some View {
|
|
|
|
|
Group {
|
|
|
|
|
switch state {
|
|
|
|
@@ -366,12 +368,12 @@ struct SubagentStatusGlyph: View {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A status pill (spinner / check / cross + word) for a subagent card header.
|
|
|
|
|
struct SubagentStatusChip: View {
|
|
|
|
|
let state: SubagentRunState
|
|
|
|
|
/// A status pill (spinner / check / cross + word) for the header of a worker card.
|
|
|
|
|
struct WorkerStatusChip: View {
|
|
|
|
|
let state: WorkerRunState
|
|
|
|
|
var body: some View {
|
|
|
|
|
HStack(spacing: 4) {
|
|
|
|
|
SubagentStatusGlyph(state: state).frame(width: 14)
|
|
|
|
|
WorkerStatusGlyph(state: state).frame(width: 14)
|
|
|
|
|
Text(state.label)
|
|
|
|
|
}
|
|
|
|
|
.font(.caption2.weight(.medium))
|
|
|
|
@@ -381,14 +383,16 @@ struct SubagentStatusChip: View {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One delegated subagent (`Task`), as a gold card: its type and the task it was given, a live
|
|
|
|
|
/// Working / Done / Failed status, and — on expand — its nested activity stream (or, for a
|
|
|
|
|
/// replayed transcript with none, its returned report). Mirrors the Mac's `SubagentCard`.
|
|
|
|
|
/// One delegated agent as a gold card — a Nucleic-managed worker (`nucleic_worker`) or a
|
|
|
|
|
/// provider-native subagent (`Task`), which the header names accordingly: its type and the task
|
|
|
|
|
/// it was given, a live Working / Done / Failed status, and — on expand — its nested activity
|
|
|
|
|
/// stream (or, for a replayed transcript with none, its returned report). Mirrors the Mac's
|
|
|
|
|
/// `SubagentCard`.
|
|
|
|
|
struct SubagentCard: View {
|
|
|
|
|
let group: ToolGroup
|
|
|
|
|
@State private var expanded = false
|
|
|
|
|
|
|
|
|
|
private var state: SubagentRunState { SubagentRunState(group: group) }
|
|
|
|
|
private var state: WorkerRunState { WorkerRunState(group: group) }
|
|
|
|
|
private var report: String? { Subagent.report(group) }
|
|
|
|
|
private var hasDetail: Bool { !group.children.isEmpty || report != nil }
|
|
|
|
|
|
|
|
|
@@ -415,11 +419,11 @@ struct SubagentCard: View {
|
|
|
|
|
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
|
|
|
|
Image(systemName: "person.2.fill")
|
|
|
|
|
.font(.callout).foregroundStyle(Palette.orchestra).frame(width: 18)
|
|
|
|
|
(Text("Subagent")
|
|
|
|
|
(Text(group.isManagedWorker ? "Worker" : "Subagent")
|
|
|
|
|
+ (Subagent.type(group).map { Text(" · \($0)").foregroundColor(.secondary) } ?? Text("")))
|
|
|
|
|
.font(.callout.weight(.semibold))
|
|
|
|
|
Spacer(minLength: 6)
|
|
|
|
|
SubagentStatusChip(state: state)
|
|
|
|
|
WorkerStatusChip(state: state)
|
|
|
|
|
if hasDetail {
|
|
|
|
|
Image(systemName: expanded ? "chevron.up" : "chevron.down")
|
|
|
|
|
.font(.caption2).foregroundStyle(.secondary)
|
|
|
|
@@ -434,15 +438,20 @@ struct SubagentCard: View {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A multi-agent fan-out: a wave of two or more subagents (`Task` calls) spawned together. The
|
|
|
|
|
/// header sums up the wave (count + overall status); each subagent is its own row that expands
|
|
|
|
|
/// independently to reveal its activity stream and report. Mirrors the Mac's `OrchestrationCard`.
|
|
|
|
|
/// A multi-agent fan-out: a wave of two or more delegates spawned together. The header sums up
|
|
|
|
|
/// the wave (count + overall status); each one is its own row that expands independently to
|
|
|
|
|
/// reveal its activity stream and report. Mirrors the Mac's `OrchestrationCard`.
|
|
|
|
|
struct OrchestrationCard: View {
|
|
|
|
|
let groups: [ToolGroup]
|
|
|
|
|
|
|
|
|
|
/// Whether every call in the wave is a Nucleic-managed worker spawn, which is what entitles
|
|
|
|
|
/// the card to say "Workers". A mixed wave takes the broader word.
|
|
|
|
|
private var allManaged: Bool { groups.allSatisfy(\.isManagedWorker) }
|
|
|
|
|
private var noun: String { allManaged ? "worker" : "subagent" }
|
|
|
|
|
|
|
|
|
|
/// Overall wave status: failed if any failed, else running if any is still working, else done.
|
|
|
|
|
private var overall: SubagentRunState {
|
|
|
|
|
let states = groups.map { SubagentRunState(group: $0) }
|
|
|
|
|
private var overall: WorkerRunState {
|
|
|
|
|
let states = groups.map { WorkerRunState(group: $0) }
|
|
|
|
|
if states.contains(where: { if case .failed = $0 { true } else { false } }) { return .failed }
|
|
|
|
|
if states.contains(where: { if case .running = $0 { true } else { false } }) { return .running }
|
|
|
|
|
return .done
|
|
|
|
@@ -467,25 +476,26 @@ struct OrchestrationCard: View {
|
|
|
|
|
Image(systemName: "rectangle.3.group.fill")
|
|
|
|
|
.font(.callout).foregroundStyle(Palette.orchestra).frame(width: 18)
|
|
|
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
|
|
|
Text("Multiple Subagents").font(.callout.weight(.semibold))
|
|
|
|
|
Text("\(groups.count) subagents · \(doneCount) done")
|
|
|
|
|
Text(allManaged ? "Multiple Workers" : "Multiple Subagents")
|
|
|
|
|
.font(.callout.weight(.semibold))
|
|
|
|
|
Text("\(groups.count) \(noun)s · \(doneCount) done")
|
|
|
|
|
.font(.caption2).foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
Spacer(minLength: 6)
|
|
|
|
|
SubagentStatusChip(state: overall)
|
|
|
|
|
WorkerStatusChip(state: overall)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 12).padding(.vertical, 10)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One subagent within a fan-out: a one-line status header that expands on tap to reveal that
|
|
|
|
|
/// One agent within a fan-out: a one-line status header that expands on tap to reveal that
|
|
|
|
|
/// agent's own activity stream — and, failing that, its returned report. Each row tracks its own
|
|
|
|
|
/// expansion so agents open independently.
|
|
|
|
|
private struct OrchestrationAgentRow: View {
|
|
|
|
|
let group: ToolGroup
|
|
|
|
|
@State private var expanded = false
|
|
|
|
|
|
|
|
|
|
private var state: SubagentRunState { SubagentRunState(group: group) }
|
|
|
|
|
private var state: WorkerRunState { WorkerRunState(group: group) }
|
|
|
|
|
private var report: String? { Subagent.report(group) }
|
|
|
|
|
private var hasDetail: Bool { !group.children.isEmpty || report != nil }
|
|
|
|
|
|
|
|
|
@@ -505,7 +515,7 @@ private struct OrchestrationAgentRow: View {
|
|
|
|
|
|
|
|
|
|
private var header: some View {
|
|
|
|
|
HStack(alignment: .firstTextBaseline, spacing: 9) {
|
|
|
|
|
SubagentStatusGlyph(state: state)
|
|
|
|
|
WorkerStatusGlyph(state: state)
|
|
|
|
|
Text(Subagent.headline(group))
|
|
|
|
|
.font(.caption).lineLimit(1).truncationMode(.tail)
|
|
|
|
|
Spacer(minLength: 6)
|
|
|
|
@@ -518,8 +528,8 @@ private struct OrchestrationAgentRow: View {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A subagent's nested activity — the calls, thinking, and prose it produced — rendered with the
|
|
|
|
|
/// same row views the main transcript uses, recursing into nested subagents' own cards. Falls
|
|
|
|
|
/// A delegate's nested activity — the calls, thinking, and prose it produced — rendered with the
|
|
|
|
|
/// same row views the main transcript uses, recursing into nested delegates' own cards. Falls
|
|
|
|
|
/// back to the returned report when there's no captured inner activity (a replayed transcript).
|
|
|
|
|
private struct SubagentActivityStream: View {
|
|
|
|
|
let children: [TranscriptItem]
|
|
|
|
@@ -784,7 +794,8 @@ enum ToolGlyph {
|
|
|
|
|
case "Edit", "Write", "MultiEdit", "NotebookEdit": return "pencil"
|
|
|
|
|
case "Grep", "Glob", "Search": return "magnifyingglass"
|
|
|
|
|
case "WebFetch", "WebSearch": return "globe"
|
|
|
|
|
case "Task", "Agent", ToolGroup.orchestraSubagentToolName: return "person.2"
|
|
|
|
|
case "Task", "Agent", ToolGroup.orchestraWorkerToolName,
|
|
|
|
|
ToolGroup.legacyOrchestraWorkerToolName: return "person.2"
|
|
|
|
|
case "TodoWrite": return "checklist"
|
|
|
|
|
case ExitPlanMode.toolName, PlanReview.toolName, PlanReview.qualifiedToolName:
|
|
|
|
|
return "list.bullet.clipboard"
|
|
|
|
|