From 415443e8ce527289b44717fb4e22d0aa06b729c7 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 27 Jun 2026 20:24:15 -0700 Subject: [PATCH] nvrsion: promote trunk to dev Nucleic-Promote: 1 Co-authored-by: Nucleic --- .../NucleicRemote/Views/HomeView.swift | 26 +++-- .../Views/Transcript/HostExecCard.swift | 94 ++++++++++++++++--- .../Views/Transcript/ToolGroupRow.swift | 2 +- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/HomeView.swift b/NucleicRemote/NucleicRemote/Views/HomeView.swift index 5f06fd7..773b729 100644 --- a/NucleicRemote/NucleicRemote/Views/HomeView.swift +++ b/NucleicRemote/NucleicRemote/Views/HomeView.swift @@ -96,7 +96,7 @@ struct HomeView: View { StatCard(value: running.count, label: "In progress", icon: "circle.lefthalf.filled") StatCard(value: counts.activeChats, label: "Active", icon: "bolt") StatCard(value: counts.messages, label: "Messages", icon: "paperplane") - StatCard(value: TokenCount.abbreviated(counts.tokens), label: "Tokens", icon: "number") + StatCard(values: TokenCount.adaptiveCandidates(counts.tokens), label: "Tokens", icon: "text.word.spacing") } } } @@ -128,18 +128,23 @@ private struct InProgressSessions: View { } private struct StatCard: View { - let value: String + /// Candidate renderings of the value, widest first: the card shows the fullest one its width + /// allows (via `ViewThatFits`) and falls back to the narrowest. A single entry for the plain + /// integer cards; several for the token card, so a wide card shows more digits than "1.6B". + let values: [String] let label: String let icon: String init(value: Int, label: String, icon: String) { - self.value = "\(value)" - self.label = label - self.icon = icon + self.init(values: ["\(value)"], label: label, icon: icon) } init(value: String, label: String, icon: String) { - self.value = value + self.init(values: [value], label: label, icon: icon) + } + + init(values: [String], label: String, icon: String) { + self.values = values.isEmpty ? [""] : values self.label = label self.icon = icon } @@ -147,7 +152,14 @@ private struct StatCard: View { var body: some View { VStack(alignment: .leading, spacing: 4) { Image(systemName: icon).foregroundStyle(Palette.accent) - Text(value).font(.title2.weight(.semibold).monospacedDigit()) + // Show the fullest value the card's width fits, shrinking toward the compact form when + // narrow; ViewThatFits renders the last candidate if none fit, so it never overflows. + ViewThatFits(in: .horizontal) { + ForEach(Array(values.enumerated()), id: \.offset) { _, value in + Text(value).font(.title2.weight(.semibold).monospacedDigit()) + .lineLimit(1) + } + } Text(label).font(.caption).foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/HostExecCard.swift b/NucleicRemote/NucleicRemote/Views/Transcript/HostExecCard.swift index 3699bf0..f502a28 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/HostExecCard.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/HostExecCard.swift @@ -59,17 +59,23 @@ struct HostExecToolCard: View { .font(.caption).foregroundStyle(Palette.accent).frame(width: 16) VStack(alignment: .leading, spacing: 3) { HStack(alignment: .firstTextBaseline, spacing: 6) { - Text("Host").font(.caption.weight(.semibold)) + Text("HOST").font(.caption2.weight(.bold)).foregroundStyle(Palette.accent) if let parsed { + // The command-derived purpose leads as the bold headline; "HOST" is just a + // quiet tag (the desktop glyph already marks it as a host call). Text(MessageBubble.markdown(parsed.purpose)) - .font(.caption).lineLimit(1).truncationMode(.tail) + .font(.callout.weight(.semibold)).lineLimit(1).truncationMode(.tail) + } else { + Text("Host").font(.callout.weight(.semibold)) } } HStack(alignment: .firstTextBaseline, spacing: 5) { Text("$").font(.caption.monospaced()).foregroundStyle(Palette.accent.opacity(0.7)) Text(command) .font(.caption.monospaced()).foregroundStyle(.secondary) - .lineLimit(expanded ? nil : 1).truncationMode(.middle) + // Expanded, the literal stays bounded — a long chain's command is itself + // long, and the compact step list above carries the structure. + .lineLimit(expanded ? 6 : 1).truncationMode(.middle) .textSelection(.enabled) } } @@ -106,29 +112,91 @@ struct HostCommandBreakdown: View { if let dir = summary.workingDirectory { detailRow(icon: "folder", lines: ["in \(dir)"]) } - ForEach(Array(summary.invocations.enumerated()), id: \.element.id) { index, inv in - if index > 0 { Divider().overlay(Color.primary.opacity(0.08)) } - invocationView(inv) - } + steps } .frame(maxWidth: .infinity, alignment: .leading) } + /// At most this many steps render in the compact chain list before the rest fold into a + /// "+N more" tail — so a long `cmd1 && cmd2 && …` pipeline can't grow the card unbounded + /// (the exact command shown alongside is always the complete source of truth). + private static let maxVisibleSteps = 7 + + /// The invocations, rendered to fit the chain length: a lone command gets the full block + /// (program + actions + every env/flag/operand spelled out); a *chain* collapses to one + /// compact line per command so a long pipeline stays short, not a tall stack of blocks. + @ViewBuilder private var steps: some View { + if summary.invocations.count <= 1 { + if let only = summary.invocations.first { invocationView(only) } + } else { + VStack(alignment: .leading, spacing: 6) { + ForEach(Array(summary.invocations.prefix(Self.maxVisibleSteps))) { inv in + stepRow(inv) + } + let hidden = summary.invocations.count - Self.maxVisibleSteps + if hidden > 0 { + Text("+\(hidden) more step\(hidden == 1 ? "" : "s")") + .font(.caption2).foregroundStyle(.tertiary) + .padding(.leading, 22) + } + } + } + } + + /// One command in a chain, compacted to a single line: a step glyph, the program + actions + /// emphasized, then a truncated tail of its env/flags/operands. A destructive/elevated step is + /// tinted and glyphed in the danger color so it still stands out. + private func stepRow(_ inv: HostCommandSummary.Invocation) -> some View { + let risky = inv.destructive || inv.elevated + let tail = stepTail(inv) + return HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(systemName: stepGlyph(inv)) + .font(.caption2).foregroundStyle(risky ? Palette.danger : accent).frame(width: 14) + (Text(inv.headline) + .font(.caption.monospaced().weight(.semibold)) + .foregroundColor(.primary) + + Text(tail.isEmpty ? "" : " " + tail) + .font(.caption.monospaced()) + .foregroundColor(.secondary)) + .lineLimit(1).truncationMode(.middle) + .textSelection(.enabled) + Spacer(minLength: 0) + } + } + + /// The trailing detail of a compacted step — its env, flags, and operands joined on one line, + /// truncated by the row. The literal command carries the untruncated form. + private func stepTail(_ inv: HostCommandSummary.Invocation) -> String { + (inv.env.map(\.display) + inv.flags.map(\.display) + inv.arguments).joined(separator: " ") + } + + /// The leading glyph for a compacted step: a delete/elevation marker takes priority so the + /// risky step is unmistakable; otherwise a plain terminal glyph. + private func stepGlyph(_ inv: HostCommandSummary.Invocation) -> String { + if inv.destructive { return "trash" } + if inv.elevated { return "lock.shield" } + return "terminal" + } + /// The deterministic, command-derived purpose — labeled so the user knows it's *Nucleic's* /// reading of the command, not the agent's claim about it. private var purposeRow: some View { - HStack(alignment: .top, spacing: 9) { - Image(systemName: "sparkles").font(.callout).foregroundStyle(accent).frame(width: 16) - VStack(alignment: .leading, spacing: 2) { - Text("Likely purpose · inferred from the command") - .font(.caption2.weight(.semibold)).foregroundStyle(.secondary) + HStack(alignment: .top, spacing: 10) { + Image(systemName: "sparkles").font(.title3).foregroundStyle(accent).frame(width: 22) + VStack(alignment: .leading, spacing: 3) { Text(MessageBubble.markdown(summary.purpose)) - .font(.callout.weight(.semibold)) + .font(.title3.weight(.semibold)) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) + Text("Likely purpose · inferred from the command") + .font(.caption2.weight(.medium)).foregroundStyle(.secondary) } Spacer(minLength: 0) } + .padding(.horizontal, 11).padding(.vertical, 9) + .frame(maxWidth: .infinity, alignment: .leading) + .background(accent.opacity(0.12), in: RoundedRectangle(cornerRadius: 8)) + .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(accent.opacity(0.22), lineWidth: 1)) } /// A red banner for the two things a host command most needs flagged before "Allow": running diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift index b96c8e7..6e5da10 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift @@ -338,7 +338,7 @@ struct OrchestrationCard: View { Image(systemName: "rectangle.3.group.fill") .font(.callout).foregroundStyle(Palette.orchestra).frame(width: 18) VStack(alignment: .leading, spacing: 1) { - Text("Multi-agent fan-out").font(.callout.weight(.semibold)) + Text("Multiple Subagents").font(.callout.weight(.semibold)) Text("\(groups.count) subagents · \(doneCount) done") .font(.caption2).foregroundStyle(.secondary) }