Show full untruncated content in approval requests

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) <[email protected]>
This commit is contained in:
2026-06-16 00:10:38 -07:00
co-authored by Claude Opus 4.8
parent 136a22eb89
commit e31b6a8799
2 changed files with 35 additions and 6 deletions
@@ -33,13 +33,19 @@ struct ApprovalCardView: View {
.background(approval.risk.color.opacity(0.2), in: Capsule()) .background(approval.risk.color.opacity(0.2), in: Capsule())
.foregroundStyle(approval.risk.color) .foregroundStyle(approval.risk.color)
} }
Text(approval.input.compactSummary) // Full, untruncated content in a bounded scroll view — the user must be
.font(.caption.monospaced()) // able to read exactly what they are granting, so long commands/paths/inputs
.lineLimit(8) // scroll instead of being clipped to a few lines.
.textSelection(.enabled) if !approval.input.approvalDetail.isEmpty {
.padding(8) ScrollView([.horizontal, .vertical]) {
.frame(maxWidth: .infinity, alignment: .leading) 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)) .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8))
}
if requiresBiometric && !biometricPassed { if requiresBiometric && !biometricPassed {
Button { Button {
@@ -82,4 +82,27 @@ extension JSONValue {
case .null: return "null" 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()
}
}
} }