Files
Jarvis/App/Wizard/WizardView.swift
james.maesandClaude Opus 4.7 8ebdf76980 fix(hud): wrap text in wizard + banner instead of tail-truncation
Both the first-launch wizard's TCC stage and the HUD banner panel were
showing tail-truncated body text (Grant Permissions descriptions ending
in "...", Limited hotkey mode body cut at "The hotke..."). Two layered
SwiftUI layout bugs:

1. The body Text inside row HStacks (`VStack(title+body) + Spacer +
   button`) didn't have `.fixedSize(horizontal: false, vertical: true)`,
   so when the trailing button was wide ("Denied — Open System Settings")
   SwiftUI's HStack layout compressed Text and chose tail-truncation
   over multi-line wrap.

2. The wizard's outer frame was `560×440`, leaving only ~318h for
   stageBody after header(64) + dividers(2) + footer(56). The TCC stage
   with 4 permission rows needed ≥520h. The vertical squeeze cascaded
   into more aggressive Text compression and clipped the bottom Back row.

Fixes:
- WizardView: bump frame to 600×640.
- WizardStageTCCView: `.fixedSize(horizontal: false, vertical: true)` on
  description + each row's body. `.frame(maxWidth: .infinity, alignment:
  .leading)` on inner row VStacks so they receive the available width
  before the trailing button claims it. Spacer minLength bumped to 16
  for breathing room between body text and button.
- HUDBanner: same `.fixedSize` + `.frame(maxWidth:)` pair on the title
  +body VStack. The 360pt-wide banner needed it most because
  `HUDBannerPanel.positionTopTrailing` re-reads the SwiftUI fittingSize
  on host — without wrapping, the panel was sized to a single-line body
  and forced all subsequent body text to truncate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 16:08:02 -05:00

101 lines
3.1 KiB
Swift

import SwiftUI
import Keychain
import Shell
/// Three-stage root view per UI-SPEC Surface 1 (api key → tcc → hotkey).
/// Header shows stage dots; footer offers Back (disabled on Stage 1). The
/// actual modal/non-modal distinction lives at the hosting
/// `OnboardingWizardController`, not here.
public struct WizardView: View {
@ObservedObject var state: WizardState
let keychain: any KeychainStore
let validator: AnthropicKeyValidator
let onGrantInputMonitoring: () -> Bool
let onComplete: () -> Void
public init(
state: WizardState,
keychain: any KeychainStore,
validator: AnthropicKeyValidator,
onGrantInputMonitoring: @escaping () -> Bool,
onComplete: @escaping () -> Void
) {
self.state = state
self.keychain = keychain
self.validator = validator
self.onGrantInputMonitoring = onGrantInputMonitoring
self.onComplete = onComplete
}
public var body: some View {
VStack(spacing: 0) {
header
Divider()
stageBody
Divider()
footer
}
// 440h was too short for the TCC stage (4 permission rows + descriptions
// + buttons ≈ 520h needed); SwiftUI compressed Text first, producing the
// tail-truncation visible on first-launch and clipping the Back row.
.frame(width: 600, height: 640)
}
private var header: some View {
HStack(spacing: 8) {
ForEach(WizardStage.allCases.filter { $0 != .complete }, id: \.self) { stage in
Circle()
.fill(state.currentStage == stage
? Color(NSColor.controlAccentColor)
: Color(NSColor.separatorColor))
.frame(width: 10, height: 10)
}
}
.frame(height: 64)
.frame(maxWidth: .infinity)
}
@ViewBuilder
private var stageBody: some View {
switch state.currentStage {
case .apiKey:
WizardStageAPIKeyView(
state: state,
validator: validator,
keychain: keychain,
onAdvance: { state.currentStage = .tcc }
)
case .tcc:
WizardStageTCCView(
state: state,
onAdvance: { state.currentStage = .hotkey },
onGrantInputMonitoring: onGrantInputMonitoring
)
case .hotkey:
WizardStageHotkeyView(state: state, onAdvance: {
state.currentStage = .complete
onComplete()
})
case .complete:
Color.clear
}
}
private var footer: some View {
HStack {
if state.currentStage != .apiKey {
Button("← Back") {
switch state.currentStage {
case .tcc: state.currentStage = .apiKey
case .hotkey: state.currentStage = .tcc
default: break
}
}
}
Spacer()
}
.padding(.horizontal, 48)
.frame(height: 56)
}
}