Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions clients/macos/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ let package = Package(
exclude: ["Resources/Info.plist"],
resources: [
.process("Resources/Assets.xcassets"),
.process("Resources/dino.webp"),
.process("Resources/egg.jpg"),
.process("Resources/egg_svg.svg"),
.process("Resources/meadow.svg"),
.process("Resources/Fonts"),
.copy("Resources/Recipes")
.copy("Resources/Recipes"),
.process("Resources/Onboarding")
],
linkerSettings: [
.linkedFramework("ApplicationServices"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ enum Meadow {
// Pixel scaling factor
static let pixelScale: CGFloat = 2.0

// Art pixel size — each pixel-art cell renders as this many points
static let artPixelSize: CGFloat = 5.0

// Interview palette
static let avatarGradientStart = Violet._600
static let avatarGradientEnd = Violet._400
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,83 +6,86 @@ struct AccessibilityPermissionStepView: View {
@State private var showContent = false
@State private var permissionGranted = false
@State private var pollTimer: Timer?

private static let reactions = [
"Sound! I can hear everything \u{2014} this is wild.",
"Wait\u{2026} is that your voice? I can hear you!",
"Oh \u{2014} so *that\u{2019}s* what the world sounds like.",
]
@State private var pollCount = 0

var body: some View {
VStack(spacing: VSpacing.xxl) {
if permissionGranted {
ReactionBubble(text: "I can take action now.", delay: 0)
} else {
ReactionBubble(text: Self.reactions.randomElement()!)
}

VStack(spacing: VSpacing.xl) {
VStack(spacing: VSpacing.md) {
Text("Now teach me to act.")
Text("Now teach me to act")
.font(VFont.onboardingTitle)
.foregroundColor(VColor.textPrimary)

Text("I can hear you, but I can\u{2019}t do anything yet. Let me control your Mac so I can take action on what you ask.")
.font(VFont.onboardingSubtitle)
.foregroundColor(VColor.textSecondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 320)
.frame(maxWidth: 400)
}
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 8)

VStack(spacing: VSpacing.xl) {
Text("\u{1F932}")
.font(VFont.cardEmoji)

Text("Give me hands")
.font(VFont.cardTitle)
// Compact permission info card
VStack(alignment: .leading, spacing: VSpacing.sm) {
Text("Accessibility")
.font(VFont.bodyMedium)
.foregroundColor(VColor.textPrimary)

Text("Accessibility access lets \(state.assistantName) click, type, and navigate your Mac for you. macOS will ask you to flip a switch in System Settings.")
.font(VFont.caption)
.foregroundColor(VColor.textSecondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 260)

if permissionGranted {
HStack(spacing: VSpacing.md) {
HStack(spacing: VSpacing.sm) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(VColor.success)
Text("I can take action now")
Text("Permission granted")
.foregroundColor(VColor.success)
.font(VFont.bodyMedium)
.font(VFont.caption)
}
.transition(.scale.combined(with: .opacity))
} else {
VStack(spacing: VSpacing.md) {
OnboardingButton(title: "Let me help", style: .primary) {
requestAccessibilityPermission()
}

Text("You\u{2019}ll be sent to System Settings \u{2014} come back here after.")
.font(VFont.small)
.foregroundColor(VColor.textMuted)
}
Text("Allows the assistant to interact with apps on your behalf \u{2014} clicking, typing, and navigating. All actions are performed locally and can be revoked at any time.")
.font(VFont.caption)
.foregroundColor(VColor.textMuted)
}
}
.padding(VSpacing.xxl)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(VSpacing.lg)
.background(
RoundedRectangle(cornerRadius: VRadius.lg)
.fill(VColor.surface.opacity(0.4))
RoundedRectangle(cornerRadius: VRadius.md)
.fill(VColor.surface.opacity(0.3))
.overlay(
RoundedRectangle(cornerRadius: VRadius.lg)
.stroke(VColor.onboardingAccent.opacity(0.3), lineWidth: 1)
RoundedRectangle(cornerRadius: VRadius.md)
.stroke(VColor.surfaceBorder.opacity(0.4), lineWidth: 1)
)
)
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 12)

if !permissionGranted {
VStack(spacing: VSpacing.md) {
OnboardingButton(title: "Continue", style: .primary) {
requestAccessibilityPermission()
}

HStack(spacing: VSpacing.lg) {
Button("Skip for now") {
state.advance()
}
.buttonStyle(.plain)
.font(VFont.small)
.foregroundColor(VColor.textMuted)

if pollCount >= 8 {
Button("I\u{2019}ve already granted it") {
grantPermission()
}
.buttonStyle(.plain)
.font(VFont.small)
.foregroundColor(VColor.accent)
.transition(.opacity)
}
}
}
.opacity(showContent ? 1 : 0)
}
}
.animation(.easeOut(duration: 0.5), value: permissionGranted)
.animation(.easeOut(duration: 0.4), value: permissionGranted)
.animation(.easeOut(duration: 0.3), value: pollCount)
.onAppear {
if state.skipPermissionChecks {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
Expand All @@ -96,7 +99,7 @@ struct AccessibilityPermissionStepView: View {
}
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.easeOut(duration: 0.5)) {
showContent = true
}
Expand All @@ -115,10 +118,12 @@ struct AccessibilityPermissionStepView: View {

private func startPolling() {
pollTimer?.invalidate()
pollTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
let status = PermissionManager.accessibilityStatus(prompt: false)
if status == .granted {
DispatchQueue.main.async {
pollCount = 0
pollTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [self] _ in
DispatchQueue.main.async {
pollCount += 1
let status = PermissionManager.accessibilityStatus(prompt: false)
if status == .granted {
grantPermission()
}
}
Expand All @@ -129,21 +134,22 @@ struct AccessibilityPermissionStepView: View {
pollTimer?.invalidate()
permissionGranted = true
state.accessibilityGranted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
state.advance()
}
}
}

#Preview {
ZStack {
MeadowBackground()
VColor.background
AccessibilityPermissionStepView(state: {
let s = OnboardingState()
s.assistantName = "Vellum"
s.currentStep = 4
return s
}())
.frame(maxWidth: 500)
}
.frame(width: 1366, height: 849)
.frame(width: 640, height: 500)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct AliveStepView: View {
}

var body: some View {
VStack(spacing: VSpacing.xxxl) {
VStack(spacing: VSpacing.xl) {
VStack(spacing: VSpacing.md) {
Text("\(state.assistantName.isEmpty ? "It" : state.assistantName) has hatched.")
.font(VFont.onboardingTitle)
Expand Down Expand Up @@ -46,41 +46,25 @@ struct AliveStepView: View {
}
}

VStack(spacing: VSpacing.xl) {
VStack(spacing: VSpacing.md) {
OnboardingButton(
title: "Start using \(state.assistantName.isEmpty ? "your agent" : state.assistantName)",
style: .primary
) {
onComplete()
}
Comment on lines 50 to 55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore a path from step 6 to the interview step

At this point in the flow, the only actions are onComplete() and onOpenSettings(), and neither advances onboarding state. Since OnboardingFlowView only shows InterviewStepView when currentStep > 6, users can no longer reach step 7 through normal onboarding, so the interview/profile setup path is effectively unreachable unless state is manually restored.

Useful? React with 👍 / 👎.

.font(VFont.cardTitle)

VStack(spacing: VSpacing.md) {
Button {
state.advance()
} label: {
Text("Say hi to \(state.assistantName.isEmpty ? "your agent" : state.assistantName) first")
.font(VFont.caption)
.foregroundColor(VColor.textSecondary)
}
.buttonStyle(.plain)
.onHover { hovering in
NSCursor.pointingHand.set()
if !hovering { NSCursor.arrow.set() }
}

Button {
onOpenSettings()
} label: {
Text("Open Settings first")
.font(VFont.caption)
.foregroundColor(VColor.textMuted)
}
.buttonStyle(.plain)
.onHover { hovering in
NSCursor.pointingHand.set()
if !hovering { NSCursor.arrow.set() }
}
Button {
onOpenSettings()
} label: {
Text("Open Settings first")
.font(VFont.caption)
.foregroundColor(VColor.textMuted)
}
.buttonStyle(.plain)
.onHover { hovering in
NSCursor.pointingHand.set()
if !hovering { NSCursor.arrow.set() }
}
}
.opacity(showButtons ? 1 : 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,14 @@ struct FnKeyStepView: View {
@State private var highlightedKey: ActivationKey?
@State private var wrongKeyHint: String?
@State private var eventMonitor: Any?
@State private var nameReaction: String = ""

private let keyOptions: [(key: ActivationKey, label: String)] = [
(.fn, "\u{1F310} fn"),
(.fn, "fn"),
(.ctrl, "ctrl"),
]

private static let nameReactions = [
"%@\u{2026} that feels like mine.",
"%@. I\u{2019}ll grow into it.",
"%@ \u{2014} I already like the sound of it.",
]

var body: some View {
VStack(spacing: VSpacing.xxl) {
ReactionBubble(text: nameReaction)

VStack(spacing: VSpacing.md) {
Text("Let\u{2019}s find your voice.")
.font(VFont.onboardingTitle)
Expand All @@ -33,7 +24,7 @@ struct FnKeyStepView: View {
.font(VFont.onboardingSubtitle)
.foregroundColor(VColor.textSecondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 320)
.frame(maxWidth: 400)
}
.opacity(showButtons ? 1 : 0)

Expand Down Expand Up @@ -61,9 +52,6 @@ struct FnKeyStepView: View {
.animation(.easeOut(duration: 0.3), value: wrongKeyHint)
.animation(.easeOut(duration: 0.3), value: highlightedKey)
.onAppear {
let format = Self.nameReactions.randomElement()!
nameReaction = String(format: format, state.assistantName)

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
withAnimation(.easeOut(duration: 0.5)) {
showButtons = true
Expand All @@ -82,11 +70,12 @@ struct FnKeyStepView: View {
} label: {
Text(label)
.font(VFont.mono)
.foregroundColor(highlightedKey == key ? VColor.background : VColor.textPrimary.opacity(0.85))
.frame(width: 64, height: 44)
.foregroundColor(highlightedKey == key ? .white : VColor.textPrimary.opacity(0.85))
.frame(maxWidth: .infinity)
.frame(height: 44)
.background(
RoundedRectangle(cornerRadius: VRadius.md)
.fill(highlightedKey == key ? VColor.onboardingAccent : VColor.surface.opacity(0.5))
.fill(highlightedKey == key ? VColor.accent : VColor.surface.opacity(0.5))
)
.overlay(
RoundedRectangle(cornerRadius: VRadius.md)
Expand Down Expand Up @@ -142,13 +131,13 @@ struct FnKeyStepView: View {

#Preview {
ZStack {
MeadowBackground()
VColor.background
FnKeyStepView(state: {
let s = OnboardingState()
s.assistantName = "Alex"
s.currentStep = 2
return s
}())
}
.frame(width: 1366, height: 849)
.frame(width: 520, height: 400)
}
Loading