From 1fcd963019c7d343122493f6ee2bf96ceb58d088 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Sat, 25 Apr 2026 09:04:33 +0200 Subject: [PATCH] feat(chat): numbered shortcuts on permission sheet (Phase 2.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hermes v2026.4.23's TUI rewrite added 1-9 numbered shortcuts on approval prompts so power users approve/deny without reaching for the mouse. Mirror the pattern in Scarf: Mac PermissionApprovalView: - Each option button gets a "1. ", "2. ", … prefix on its label. - New private View extension `applyingNumberShortcut(index:)` binds the digit `idx + 1` (no modifiers) via .keyboardShortcut. Capped at 9; extra options stay tappable but unbound. iOS PermissionSheet: - Each row gets a monospaced "1." / "2." prefix as a hierarchy hint. - No keyboard binding (phones don't have hardware keyboards), but the numbering matches the Mac pattern so users transitioning between platforms see the same visual structure. Verified: Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- scarf/Scarf iOS/Chat/ChatView.swift | 12 ++++- .../scarf/Features/Chat/Views/ChatView.swift | 50 ++++++++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/scarf/Scarf iOS/Chat/ChatView.swift b/scarf/Scarf iOS/Chat/ChatView.swift index 5cddf27..483b1a3 100644 --- a/scarf/Scarf iOS/Chat/ChatView.swift +++ b/scarf/Scarf iOS/Chat/ChatView.swift @@ -1270,7 +1270,12 @@ private struct PermissionSheet: View { } Section("Your response") { - ForEach(permission.options, id: \.optionId) { opt in + // Visual numbering 1-9 matches the Mac sheet's + // keyboard shortcuts; on iPhone the numbers serve + // as a hierarchy hint rather than an accelerator + // (no hardware keyboard binding). Mirrors the new + // Hermes v2026.4.23 TUI pattern. + ForEach(Array(permission.options.enumerated()), id: \.element.optionId) { idx, opt in Button { Task { await onRespond(opt.optionId) @@ -1278,6 +1283,11 @@ private struct PermissionSheet: View { } } label: { HStack { + if idx < 9 { + Text("\(idx + 1).") + .font(.body.monospaced()) + .foregroundStyle(.secondary) + } Text(opt.name) Spacer() Image(systemName: "chevron.right") diff --git a/scarf/scarf/Features/Chat/Views/ChatView.swift b/scarf/scarf/Features/Chat/Views/ChatView.swift index 5011258..9d48610 100644 --- a/scarf/scarf/Features/Chat/Views/ChatView.swift +++ b/scarf/scarf/Features/Chat/Views/ChatView.swift @@ -444,21 +444,33 @@ struct PermissionApprovalView: View { .multilineTextAlignment(.center) .padding(.horizontal) + // Numbered keyboard shortcuts (1–9) on the option buttons. + // Mirrors the new TUI pattern Hermes v2026.4.23 ships — + // power users approve / deny without reaching for the + // mouse. Visible "1." prefixes act as discoverability + // hints; the actual key binding goes through + // `.keyboardShortcut`. Capped at 9 — extra options stay + // tappable but unbound (they'd need modifiers to + // disambiguate beyond 9, which isn't worth it). HStack(spacing: 12) { - ForEach(options, id: \.optionId) { option in - if option.optionId == "deny" { - Button(option.name) { - onRespond(option.optionId) - dismiss() + ForEach(Array(options.enumerated()), id: \.element.optionId) { idx, option in + let label = idx < 9 ? "\(idx + 1). \(option.name)" : option.name + Group { + if option.optionId == "deny" { + Button(label) { + onRespond(option.optionId) + dismiss() + } + .buttonStyle(.bordered) + } else { + Button(label) { + onRespond(option.optionId) + dismiss() + } + .buttonStyle(.borderedProminent) } - .buttonStyle(.bordered) - } else { - Button(option.name) { - onRespond(option.optionId) - dismiss() - } - .buttonStyle(.borderedProminent) } + .applyingNumberShortcut(index: idx) } } } @@ -484,3 +496,17 @@ struct PermissionApprovalView: View { } } } + +private extension View { + /// Bind the digit `idx + 1` (1-9) to this view as a no-modifier + /// keyboard shortcut. Indices ≥ 9 silently skip — there are only + /// nine numeric shortcut keys without modifier conflicts. + @ViewBuilder + func applyingNumberShortcut(index idx: Int) -> some View { + if idx < 9, let scalar = Unicode.Scalar(48 + idx + 1) { + self.keyboardShortcut(KeyEquivalent(Character(scalar)), modifiers: []) + } else { + self + } + } +}