iOS remote control: permission prompt buttons prone to mis-taps, no correction mechanism
Problem
When approving tool permissions via the iOS Claude app (remote control), the vertically stacked buttons are narrow with minimal spacing between them.
The most critical adjacency problem: "Allow for session" sits directly above "Deny" — these are opposite actions with the highest consequence difference. A mis-tap denies the agent's work entirely with no way to correct it.
By contrast, mis-tapping between "Allow once" and "Allow for session" is low-consequence — you just get prompted again.
There is currently no UI element to change a permission decision after it's been made.
Proposed Solutions
A) Reorder and group by intent — separate destructive action
import SwiftUI
struct PermissionPromptView: View {
let toolName: String
let onAllowOnce: () -> Void
let onAllowSession: () -> Void
let onDeny: () -> Void
var body: some View {
VStack(spacing: 20) {
Text("Allow \(toolName)?")
.font(.headline)
// Affirmative actions grouped horizontally
HStack(spacing: 12) {
Button(action: onAllowOnce) {
Text("Allow Once")
.frame(maxWidth: .infinity, minHeight: 44)
}
.buttonStyle(.bordered)
Button(action: onAllowSession) {
Text("Allow for Session")
.frame(maxWidth: .infinity, minHeight: 44)
}
.buttonStyle(.borderedProminent)
}
// Deny isolated with distinct styling and spacing
Button(action: onDeny) {
Text("Deny")
.frame(maxWidth: .infinity, minHeight: 44)
}
.buttonStyle(.bordered)
.tint(.red)
}
.padding()
}
}
// MARK: - Preview
struct PermissionPromptView_Previews: PreviewProvider {
static var previews: some View {
PermissionPromptView(
toolName: "Bash(git status)",
onAllowOnce: {},
onAllowSession: {},
onDeny: {}
)
.previewLayout(.sizeThatFits)
// Test on smallest iPhone screen
PermissionPromptView(
toolName: "Bash(npm run build)",
onAllowOnce: {},
onAllowSession: {},
onDeny: {}
)
.previewDevice("iPhone SE (3rd generation)")
}
}
B) Post-decision correction UI
There is currently no UI element after a permission choice is made. This proposes adding one — a tappable permission status badge near the tool call output that allows changing the decision:
import SwiftUI
struct PermissionBadgeView: View {
@State private var showingOptions = false
@State var currentPermission: PermissionLevel
let onPermissionChanged: (PermissionLevel) -> Void
enum PermissionLevel: String, CaseIterable {
case allowedOnce = "Allowed once"
case allowedForSession = "Allowed for session"
case denied = "Denied"
var icon: String {
switch self {
case .allowedOnce: return "checkmark.circle"
case .allowedForSession: return "checkmark.circle.fill"
case .denied: return "xmark.circle.fill"
}
}
var tint: Color {
switch self {
case .allowedOnce: return .green
case .allowedForSession: return .blue
case .denied: return .red
}
}
}
var body: some View {
Button {
showingOptions = true
} label: {
HStack(spacing: 4) {
Image(systemName: currentPermission.icon)
Text(currentPermission.rawValue)
.font(.caption)
Image(systemName: "chevron.down")
.font(.caption2)
}
.foregroundColor(currentPermission.tint)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(currentPermission.tint.opacity(0.1))
.cornerRadius(6)
}
.confirmationDialog(
"Change permission",
isPresented: $showingOptions,
titleVisibility: .visible
) {
ForEach(PermissionLevel.allCases, id: \.self) { level in
if level != currentPermission {
Button(level.rawValue) {
currentPermission = level
onPermissionChanged(level)
}
}
}
}
}
}
// MARK: - Tests (Preview-based)
#if DEBUG
struct PermissionBadgeView_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 16) {
PermissionBadgeView(
currentPermission: .allowedOnce,
onPermissionChanged: { _ in }
)
PermissionBadgeView(
currentPermission: .allowedForSession,
onPermissionChanged: { _ in }
)
PermissionBadgeView(
currentPermission: .denied,
onPermissionChanged: { _ in }
)
}
.padding()
.previewLayout(.sizeThatFits)
}
}
#endif
C) Unit tests
import XCTest
@testable import ClaudeCode
final class PermissionPromptTests: XCTestCase {
func testButtonMinimumTouchTarget() {
// Apple HIG: minimum 44pt touch targets
let minTouchTarget: CGFloat = 44
// All interactive elements must meet 44pt minimum height
// Implementation should enforce .frame(minHeight: 44) on all buttons
}
func testDenyButtonIsSeparatedFromAllowButtons() {
// Deny must not be adjacent to Allow for Session
// Layout: [Allow Once | Allow for Session] then gap then [Deny]
// The VStack spacing between allow group and deny (20pt)
// should exceed HStack spacing between allow buttons (12pt)
}
func testPermissionBadgeChangesLevel() {
var result: PermissionBadgeView.PermissionLevel?
let badge = PermissionBadgeView(
currentPermission: .allowedOnce,
onPermissionChanged: { result = $0 }
)
badge.onPermissionChanged(.allowedForSession)
XCTAssertEqual(result, .allowedForSession)
}
func testAllPermissionLevelsDisplayCorrectIcon() {
for level in PermissionBadgeView.PermissionLevel.allCases {
XCTAssertFalse(level.icon.isEmpty)
XCTAssertFalse(level.rawValue.isEmpty)
}
}
}
Summary
| Issue | Impact | Fix |
|---|---|---|
| Deny adjacent to Allow for Session | Opposite actions, highest mis-tap cost | Reorder: group allows horizontally, isolate deny below |
| Buttons below 44pt HIG minimum | Hard to tap accurately on phone | Enforce minHeight: 44 on all buttons |
| No post-decision correction | Mis-taps are permanent | New tappable permission badge with change dialog |
Environment
- Claude Code 2.1.56
- iOS Claude app via remote control
- iPhone standard screen size
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗