From e31b6a87990cd6d1e6663688d08c5453e9b49b2f Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Tue, 16 Jun 2026 00:10:38 -0700 Subject: [PATCH 1/2] Show full untruncated content in approval requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permission requests only ever displayed a summary that was hard-capped: RiskClassifier.title() slices to 80 chars, and the iOS card used a lossy compactSummary clipped to 8 lines. The full tool input was present on the request but never shown, so a long Bash command, file path, or URL was cut off with "…" and the user couldn't see what they were granting. - Add RiskClassifier.detail(toolName:input:): the full, untruncated content (full command/path/url/query), falling back to the pretty-printed input so nothing about an unrecognized tool is hidden. - Add JSONValue.prettyString() for indented, multi-line display. - macOS ApprovalBar: render the detail in a bounded, scrollable, selectable monospaced block (shown only when it adds beyond the already-shown title). - iOS ApprovalCardView: replace lossy compactSummary/lineLimit(8) with the full content in a bounded scroll view via JSONValue.approvalDetail. - Tests: assert detail is full and untruncated, with pretty-input fallback. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Views/ApprovalCardView.swift | 18 ++++++++++----- .../NucleicRemote/Views/TranscriptRow.swift | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift index c1405eb..f07b28c 100644 --- a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift +++ b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift @@ -33,13 +33,19 @@ struct ApprovalCardView: View { .background(approval.risk.color.opacity(0.2), in: Capsule()) .foregroundStyle(approval.risk.color) } - Text(approval.input.compactSummary) - .font(.caption.monospaced()) - .lineLimit(8) - .textSelection(.enabled) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) + // Full, untruncated content in a bounded scroll view — the user must be + // able to read exactly what they are granting, so long commands/paths/inputs + // scroll instead of being clipped to a few lines. + if !approval.input.approvalDetail.isEmpty { + ScrollView([.horizontal, .vertical]) { + Text(approval.input.approvalDetail) + .font(.caption.monospaced()) + .textSelection(.enabled) + .padding(8) + } + .frame(maxWidth: .infinity, maxHeight: 200, alignment: .leading) .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8)) + } if requiresBiometric && !biometricPassed { Button { diff --git a/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift b/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift index aaac024..a15be01 100644 --- a/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift +++ b/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift @@ -82,4 +82,27 @@ extension JSONValue { case .null: return "null" } } + + /// The full, **untruncated** content of a tool input for the approval card — + /// the user must see exactly what they are granting before allowing. Unlike + /// `compactSummary` (a one-line gist that drops values for complex objects) this + /// returns the primary field verbatim, falling back to the pretty-printed input + /// so nothing is hidden. The caller scrolls; it never clips. + var approvalDetail: String { + switch self { + case .string(let s): return s + case .object(let o): + if let primary = o["command"]?.stringValue + ?? o["file_path"]?.stringValue + ?? o["path"]?.stringValue + ?? o["notebook_path"]?.stringValue + ?? o["url"]?.stringValue + ?? o["query"]?.stringValue { + return primary + } + return o.isEmpty ? "" : prettyString() + case .null: return "" + default: return prettyString() + } + } } From 3954ff13b7b34d75d87ab03862d9579647621190 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Tue, 16 Jun 2026 02:00:05 -0700 Subject: [PATCH 2/2] Wrap approval detail and size the box to its content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass put the detail in a ScrollView with fixedSize(horizontal:) and a fixed maxHeight, so the command was forced onto one long line that scrolled horizontally and the box always claimed full height — a tall box with a lot of empty space. Replace it with an ApprovalDetailBox (macOS + iOS) that wraps the text and measures its content height via a PreferenceKey, sizing the box to fit and only scrolling vertically once the content exceeds the max height. No more horizontal scroll; short commands sit in a snug box. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Views/ApprovalCardView.swift | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift index f07b28c..bc65cae 100644 --- a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift +++ b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift @@ -33,18 +33,11 @@ struct ApprovalCardView: View { .background(approval.risk.color.opacity(0.2), in: Capsule()) .foregroundStyle(approval.risk.color) } - // Full, untruncated content in a bounded scroll view — the user must be - // able to read exactly what they are granting, so long commands/paths/inputs - // scroll instead of being clipped to a few lines. + // Full, untruncated content — the command wraps and the box fits its + // content, only scrolling vertically once it's genuinely tall, so the user + // can read exactly what they are granting without a giant half-empty box. if !approval.input.approvalDetail.isEmpty { - ScrollView([.horizontal, .vertical]) { - Text(approval.input.approvalDetail) - .font(.caption.monospaced()) - .textSelection(.enabled) - .padding(8) - } - .frame(maxWidth: .infinity, maxHeight: 200, alignment: .leading) - .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8)) + ApprovalDetailBox(text: approval.input.approvalDetail) } if requiresBiometric && !biometricPassed { @@ -118,3 +111,36 @@ struct ApprovalCardView: View { } } } + +/// The full content of an approval, wrapped and selectable. The box sizes to its +/// content and only begins scrolling once the content exceeds `maxHeight`, so a short +/// command sits in a snug box while a large paste stays bounded. The text wraps; it +/// never scrolls horizontally. +private struct ApprovalDetailBox: View { + let text: String + private let maxHeight: CGFloat = 200 + @State private var contentHeight: CGFloat = 0 + + var body: some View { + ScrollView(.vertical) { + Text(text) + .font(.caption.monospaced()) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .background(GeometryReader { geo in + Color.clear.preference(key: ApprovalDetailHeightKey.self, value: geo.size.height) + }) + } + .frame(height: contentHeight == 0 ? nil : min(contentHeight, maxHeight)) + .onPreferenceChange(ApprovalDetailHeightKey.self) { contentHeight = $0 } + .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8)) + } +} + +private struct ApprovalDetailHeightKey: PreferenceKey { + static var defaultValue: CGFloat { 0 } + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +}