Parcourir la Source

Improve free-tier Home UX and fix paywall pricing card layout.

Show free users how many AI job search replies remain under the Home chat
input (hidden for Pro). Replace generic “All premium features” bullets with
concrete Pro benefits on every plan card. Fix paywall layout so long feature
lists no longer stretch cards off-screen: pin the footer (Continue with free
plan, Restore, legal links), scroll the card area, and cap each card’s feature
list with an internal scroll view and wrapping labels.

Co-authored-by: Cursor <cursoragent@cursor.com>
Uzair Tahir il y a 2 semaines
Parent
commit
15f6e0a333

+ 131 - 41
App for Indeed/Controllers/PremiumPlansWindowController.swift

@@ -483,8 +483,11 @@ private final class PremiumPlansViewController: NSViewController {
483
     }
483
     }
484
 
484
 
485
     private enum FeatureListMetrics {
485
     private enum FeatureListMetrics {
486
-        static let spacing = CGFloat(10)
487
-        static let edgeInsets = NSEdgeInsets(top: 21, left: 37, bottom: 21, right: 0)
486
+        static let rowSpacing = CGFloat(8)
487
+        static let iconLabelSpacing = CGFloat(8)
488
+        static let edgeInsets = NSEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
489
+        /// Caps feature list height so pricing cards do not push the footer off-screen.
490
+        static let maxScrollHeight = CGFloat(168)
488
     }
491
     }
489
 
492
 
490
     private let subscriptionStore = SubscriptionStore.shared
493
     private let subscriptionStore = SubscriptionStore.shared
@@ -495,6 +498,14 @@ private final class PremiumPlansViewController: NSViewController {
495
     private var subscriptionStatusObservation: NSObjectProtocol?
498
     private var subscriptionStatusObservation: NSObjectProtocol?
496
     private var appearanceObserver: NSObjectProtocol?
499
     private var appearanceObserver: NSObjectProtocol?
497
 
500
 
501
+    /// Core Pro capabilities shown on every pricing card (replaces generic “All premium features”).
502
+    private static let proCapabilityFeatures = [
503
+        "Unlimited AI job search on Home",
504
+        "Save jobs & open listings in-app",
505
+        "CV Maker, profiles & PDF export",
506
+        "Role, company & skill shortcuts"
507
+    ]
508
+
498
     private let plans: [Plan] = [
509
     private let plans: [Plan] = [
499
         Plan(
510
         Plan(
500
             id: "weekly",
511
             id: "weekly",
@@ -505,9 +516,8 @@ private final class PremiumPlansViewController: NSViewController {
505
             billedLine: "",
516
             billedLine: "",
506
             crossedPrice: nil,
517
             crossedPrice: nil,
507
             savingsText: nil,
518
             savingsText: nil,
508
-            features: [
509
-                "All premium features",
510
-                "Perfect for short-term goals",
519
+            features: proCapabilityFeatures + [
520
+                "Perfect for short-term job hunts",
511
                 "Cancel anytime"
521
                 "Cancel anytime"
512
             ],
522
             ],
513
             iconName: "paperplane.fill",
523
             iconName: "paperplane.fill",
@@ -523,9 +533,8 @@ private final class PremiumPlansViewController: NSViewController {
523
             billedLine: "",
533
             billedLine: "",
524
             crossedPrice: nil,
534
             crossedPrice: nil,
525
             savingsText: nil,
535
             savingsText: nil,
526
-            features: [
527
-                "All premium features",
528
-                "Best value for regular users",
536
+            features: proCapabilityFeatures + [
537
+                "Best for regular job seekers",
529
                 "Priority support"
538
                 "Priority support"
530
             ],
539
             ],
531
             iconName: "bolt.fill",
540
             iconName: "bolt.fill",
@@ -541,8 +550,7 @@ private final class PremiumPlansViewController: NSViewController {
541
             billedLine: "",
550
             billedLine: "",
542
             crossedPrice: nil,
551
             crossedPrice: nil,
543
             savingsText: nil,
552
             savingsText: nil,
544
-            features: [
545
-                "All premium features",
553
+            features: proCapabilityFeatures + [
546
                 "Lowest effective monthly cost",
554
                 "Lowest effective monthly cost",
547
                 "Ideal for long-term use"
555
                 "Ideal for long-term use"
548
             ],
556
             ],
@@ -638,34 +646,73 @@ private final class PremiumPlansViewController: NSViewController {
638
         cardsRow.alignment = .top
646
         cardsRow.alignment = .top
639
         cardsRow.distribution = .fillEqually
647
         cardsRow.distribution = .fillEqually
640
         cardsRow.translatesAutoresizingMaskIntoConstraints = false
648
         cardsRow.translatesAutoresizingMaskIntoConstraints = false
641
-        for card in cardsRow.arrangedSubviews {
642
-            card.heightAnchor.constraint(equalTo: cardsRow.heightAnchor).isActive = true
643
-        }
644
 
649
 
645
         let trustRow = makeTrustRow()
650
         let trustRow = makeTrustRow()
646
         let footerRow = makeFooterRow()
651
         let footerRow = makeFooterRow()
647
 
652
 
648
-        let root = NSStackView(views: [crownIcon, title, subtitle, cardsRow, trustRow, footerRow])
649
-        root.orientation = .vertical
650
-        root.spacing = 18
651
-        root.alignment = .centerX
652
-        root.translatesAutoresizingMaskIntoConstraints = false
653
-
654
-        view.addSubview(root)
653
+        let headerStack = NSStackView(views: [crownIcon, title, subtitle])
654
+        headerStack.orientation = .vertical
655
+        headerStack.spacing = 10
656
+        headerStack.alignment = .centerX
657
+        headerStack.translatesAutoresizingMaskIntoConstraints = false
658
+
659
+        let bodyStack = NSStackView(views: [cardsRow, trustRow])
660
+        bodyStack.orientation = .vertical
661
+        bodyStack.spacing = 16
662
+        bodyStack.alignment = .centerX
663
+        bodyStack.translatesAutoresizingMaskIntoConstraints = false
664
+
665
+        let scrollView = NSScrollView()
666
+        scrollView.hasVerticalScroller = true
667
+        scrollView.autohidesScrollers = true
668
+        scrollView.drawsBackground = false
669
+        scrollView.borderType = .noBorder
670
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
671
+
672
+        let scrollDocument = NSView()
673
+        scrollDocument.translatesAutoresizingMaskIntoConstraints = false
674
+        scrollView.documentView = scrollDocument
675
+        scrollDocument.addSubview(bodyStack)
676
+
677
+        view.addSubview(headerStack)
678
+        view.addSubview(scrollView)
679
+        view.addSubview(footerRow)
655
         view.addSubview(closeButton)
680
         view.addSubview(closeButton)
681
+
682
+        let scrollClip = scrollView.contentView
656
         NSLayoutConstraint.activate([
683
         NSLayoutConstraint.activate([
657
-            root.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
658
-            root.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
659
-            root.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
660
-            root.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
684
+            headerStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
685
+            headerStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
686
+            headerStack.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
687
+
688
+            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
689
+            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
690
+            scrollView.topAnchor.constraint(equalTo: headerStack.bottomAnchor, constant: 12),
691
+            scrollView.bottomAnchor.constraint(equalTo: footerRow.topAnchor, constant: -12),
692
+
693
+            footerRow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
694
+            footerRow.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
695
+            footerRow.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
696
+            footerRow.heightAnchor.constraint(greaterThanOrEqualToConstant: 32),
697
+
698
+            scrollDocument.leadingAnchor.constraint(equalTo: scrollClip.leadingAnchor),
699
+            scrollDocument.trailingAnchor.constraint(equalTo: scrollClip.trailingAnchor),
700
+            scrollDocument.topAnchor.constraint(equalTo: scrollClip.topAnchor),
701
+            scrollDocument.widthAnchor.constraint(equalTo: scrollClip.widthAnchor),
702
+            scrollDocument.bottomAnchor.constraint(equalTo: bodyStack.bottomAnchor, constant: 8),
703
+
704
+            bodyStack.leadingAnchor.constraint(equalTo: scrollDocument.leadingAnchor),
705
+            bodyStack.trailingAnchor.constraint(equalTo: scrollDocument.trailingAnchor),
706
+            bodyStack.topAnchor.constraint(equalTo: scrollDocument.topAnchor, constant: 4),
707
+            bodyStack.widthAnchor.constraint(equalTo: scrollDocument.widthAnchor),
708
+
709
+            cardsRow.widthAnchor.constraint(equalTo: bodyStack.widthAnchor),
710
+            trustRow.widthAnchor.constraint(equalTo: bodyStack.widthAnchor),
711
+
661
             closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 14),
712
             closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 14),
662
             closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -14),
713
             closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -14),
663
             closeButton.widthAnchor.constraint(equalToConstant: 30),
714
             closeButton.widthAnchor.constraint(equalToConstant: 30),
664
             closeButton.heightAnchor.constraint(equalToConstant: 30),
715
             closeButton.heightAnchor.constraint(equalToConstant: 30),
665
-            cardsRow.widthAnchor.constraint(equalTo: root.widthAnchor),
666
-            cardsRow.heightAnchor.constraint(equalToConstant: 420),
667
-            trustRow.widthAnchor.constraint(equalTo: root.widthAnchor),
668
-            footerRow.widthAnchor.constraint(equalTo: root.widthAnchor),
669
             crownIcon.heightAnchor.constraint(equalToConstant: 20)
716
             crownIcon.heightAnchor.constraint(equalToConstant: 20)
670
         ])
717
         ])
671
         premiumCloseButton = closeButton
718
         premiumCloseButton = closeButton
@@ -741,9 +788,15 @@ private final class PremiumPlansViewController: NSViewController {
741
         let featureRows = plan.features.map(makeFeatureRow(_:))
788
         let featureRows = plan.features.map(makeFeatureRow(_:))
742
         let featuresStack = NSStackView(views: featureRows)
789
         let featuresStack = NSStackView(views: featureRows)
743
         featuresStack.orientation = .vertical
790
         featuresStack.orientation = .vertical
744
-        featuresStack.spacing = FeatureListMetrics.spacing
791
+        featuresStack.spacing = FeatureListMetrics.rowSpacing
745
         featuresStack.alignment = .leading
792
         featuresStack.alignment = .leading
746
         featuresStack.edgeInsets = FeatureListMetrics.edgeInsets
793
         featuresStack.edgeInsets = FeatureListMetrics.edgeInsets
794
+        featuresStack.setContentCompressionResistancePriority(.required, for: .vertical)
795
+        featuresStack.setContentHuggingPriority(.defaultHigh, for: .vertical)
796
+        for row in featureRows {
797
+            row.setContentCompressionResistancePriority(.required, for: .vertical)
798
+        }
799
+        let featuresScroll = makeFeatureListScroll(featuresStack: featuresStack)
747
 
800
 
748
         let selectButton = PlanPurchaseHoverButton(
801
         let selectButton = PlanPurchaseHoverButton(
749
             planId: plan.id,
802
             planId: plan.id,
@@ -788,14 +841,9 @@ private final class PremiumPlansViewController: NSViewController {
788
         if plan.crossedPrice != nil, plan.savingsText != nil {
841
         if plan.crossedPrice != nil, plan.savingsText != nil {
789
             contentViews.append(inlinePriceInfo)
842
             contentViews.append(inlinePriceInfo)
790
         }
843
         }
791
-        contentViews.append(contentsOf: [divider, featuresStack])
844
+        contentViews.append(contentsOf: [divider, featuresScroll])
792
 
845
 
793
-        let verticalFlex = NSView()
794
-        verticalFlex.translatesAutoresizingMaskIntoConstraints = false
795
-        verticalFlex.setContentHuggingPriority(.defaultLow, for: .vertical)
796
-        verticalFlex.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
797
-
798
-        let column = NSStackView(views: contentViews + [verticalFlex, selectButton])
846
+        let column = NSStackView(views: contentViews + [selectButton])
799
         column.orientation = .vertical
847
         column.orientation = .vertical
800
         column.spacing = 10
848
         column.spacing = 10
801
         column.alignment = .centerX
849
         column.alignment = .centerX
@@ -806,7 +854,7 @@ private final class PremiumPlansViewController: NSViewController {
806
 
854
 
807
         NSLayoutConstraint.activate([
855
         NSLayoutConstraint.activate([
808
             divider.widthAnchor.constraint(equalTo: column.widthAnchor),
856
             divider.widthAnchor.constraint(equalTo: column.widthAnchor),
809
-            featuresStack.widthAnchor.constraint(equalTo: column.widthAnchor),
857
+            featuresScroll.widthAnchor.constraint(equalTo: column.widthAnchor),
810
             column.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18),
858
             column.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18),
811
             column.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18),
859
             column.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18),
812
             column.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
860
             column.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
@@ -819,6 +867,35 @@ private final class PremiumPlansViewController: NSViewController {
819
         return card
867
         return card
820
     }
868
     }
821
 
869
 
870
+    private func makeFeatureListScroll(featuresStack: NSStackView) -> NSScrollView {
871
+        let scroll = NSScrollView()
872
+        scroll.hasVerticalScroller = true
873
+        scroll.autohidesScrollers = true
874
+        scroll.drawsBackground = false
875
+        scroll.borderType = .noBorder
876
+        scroll.translatesAutoresizingMaskIntoConstraints = false
877
+
878
+        let document = NSView()
879
+        document.translatesAutoresizingMaskIntoConstraints = false
880
+        featuresStack.translatesAutoresizingMaskIntoConstraints = false
881
+        scroll.documentView = document
882
+        document.addSubview(featuresStack)
883
+
884
+        NSLayoutConstraint.activate([
885
+            scroll.heightAnchor.constraint(equalToConstant: FeatureListMetrics.maxScrollHeight),
886
+            document.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
887
+            document.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
888
+            document.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
889
+            document.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor),
890
+            document.bottomAnchor.constraint(equalTo: featuresStack.bottomAnchor, constant: FeatureListMetrics.edgeInsets.bottom),
891
+            featuresStack.leadingAnchor.constraint(equalTo: document.leadingAnchor, constant: FeatureListMetrics.edgeInsets.left),
892
+            featuresStack.trailingAnchor.constraint(equalTo: document.trailingAnchor, constant: -FeatureListMetrics.edgeInsets.right),
893
+            featuresStack.topAnchor.constraint(equalTo: document.topAnchor, constant: FeatureListMetrics.edgeInsets.top),
894
+            featuresStack.widthAnchor.constraint(equalTo: document.widthAnchor, constant: -(FeatureListMetrics.edgeInsets.left + FeatureListMetrics.edgeInsets.right))
895
+        ])
896
+        return scroll
897
+    }
898
+
822
     private func makeFeatureRow(_ text: String) -> NSView {
899
     private func makeFeatureRow(_ text: String) -> NSView {
823
         let icon = NSImageView()
900
         let icon = NSImageView()
824
         icon.translatesAutoresizingMaskIntoConstraints = false
901
         icon.translatesAutoresizingMaskIntoConstraints = false
@@ -826,16 +903,29 @@ private final class PremiumPlansViewController: NSViewController {
826
         icon.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil)
903
         icon.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil)
827
         icon.contentTintColor = Theme.iconTint
904
         icon.contentTintColor = Theme.iconTint
828
         icon.widthAnchor.constraint(equalToConstant: 14).isActive = true
905
         icon.widthAnchor.constraint(equalToConstant: 14).isActive = true
906
+        icon.heightAnchor.constraint(equalToConstant: 14).isActive = true
907
+        icon.setContentHuggingPriority(.required, for: .horizontal)
908
+        icon.setContentHuggingPriority(.required, for: .vertical)
829
 
909
 
830
-        let label = NSTextField(labelWithString: text)
831
-        label.font = .systemFont(ofSize: 14, weight: .medium)
910
+        let label = NSTextField(wrappingLabelWithString: text)
911
+        label.font = .systemFont(ofSize: 13, weight: .medium)
832
         label.textColor = Theme.primaryText
912
         label.textColor = Theme.primaryText
913
+        label.alignment = .left
914
+        label.lineBreakMode = .byWordWrapping
915
+        label.maximumNumberOfLines = 0
916
+        label.cell?.wraps = true
917
+        label.cell?.isScrollable = false
918
+        label.setContentCompressionResistancePriority(.required, for: .vertical)
919
+        label.setContentHuggingPriority(.required, for: .vertical)
920
+        label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
921
+        label.setContentHuggingPriority(.defaultLow, for: .horizontal)
833
 
922
 
834
         let row = NSStackView(views: [icon, label])
923
         let row = NSStackView(views: [icon, label])
835
         row.orientation = .horizontal
924
         row.orientation = .horizontal
836
-        row.spacing = FeatureListMetrics.spacing
837
-        row.alignment = .centerY
925
+        row.spacing = FeatureListMetrics.iconLabelSpacing
926
+        row.alignment = .top
838
         row.distribution = .fill
927
         row.distribution = .fill
928
+        row.translatesAutoresizingMaskIntoConstraints = false
839
         return row
929
         return row
840
     }
930
     }
841
 
931
 

+ 46 - 2
App for Indeed/Views/DashboardView.swift

@@ -37,6 +37,11 @@ private enum FreeTierJobSearchQuota {
37
         guard !isProActive else { return }
37
         guard !isProActive else { return }
38
         userMessageCount += 1
38
         userMessageCount += 1
39
     }
39
     }
40
+
41
+    static func remainingUserMessages(isProActive: Bool) -> Int {
42
+        guard !isProActive else { return maxUserMessages }
43
+        return max(0, maxUserMessages - userMessageCount)
44
+    }
40
 }
45
 }
41
 
46
 
42
 private enum SettingsAppearanceID {
47
 private enum SettingsAppearanceID {
@@ -106,7 +111,9 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
106
     private let mainOverlay = NSStackView()
111
     private let mainOverlay = NSStackView()
107
     private let greetingLabel = NSTextField(labelWithString: "")
112
     private let greetingLabel = NSTextField(labelWithString: "")
108
     private let subtitleLabel = NSTextField(labelWithString: "")
113
     private let subtitleLabel = NSTextField(labelWithString: "")
114
+    private let searchBarColumn = NSStackView()
109
     private let searchBarShadowHost = NSView()
115
     private let searchBarShadowHost = NSView()
116
+    private let freeJobSearchQuotaLabel = NSTextField(labelWithString: "")
110
     private let searchCard = HoverableView()
117
     private let searchCard = HoverableView()
111
     private let jobSearchIcon = NSImageView()
118
     private let jobSearchIcon = NSImageView()
112
     private let jobKeywordsField = NSTextField()
119
     private let jobKeywordsField = NSTextField()
@@ -306,6 +313,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
306
         findJobsCTAPill.layer?.backgroundColor = (ctaHovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
313
         findJobsCTAPill.layer?.backgroundColor = (ctaHovering ? Theme.brandBlueHover : Theme.brandBlue).cgColor
307
         sendIconView.contentTintColor = Theme.proCTAText
314
         sendIconView.contentTintColor = Theme.proCTAText
308
         sendLabel.textColor = Theme.proCTAText
315
         sendLabel.textColor = Theme.proCTAText
316
+        freeJobSearchQuotaLabel.textColor = Theme.secondaryText
309
 
317
 
310
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
318
         appearanceModeSegment?.selectedSegment = AppAppearanceManager.shared.mode.segmentIndex
311
         cvMakerPageView.applyCurrentAppearance()
319
         cvMakerPageView.applyCurrentAppearance()
@@ -318,6 +326,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
318
         reloadSavedJobsListings()
326
         reloadSavedJobsListings()
319
         rebuildChatUI()
327
         rebuildChatUI()
320
         applyProSubscriptionToSidebar()
328
         applyProSubscriptionToSidebar()
329
+        updateFreeJobSearchQuotaLabel()
321
         needsLayout = true
330
         needsLayout = true
322
     }
331
     }
323
 
332
 
@@ -514,7 +523,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
514
         mainOverlay.addArrangedSubview(chatHeaderRow)
523
         mainOverlay.addArrangedSubview(chatHeaderRow)
515
         mainOverlay.addArrangedSubview(chatScrollView)
524
         mainOverlay.addArrangedSubview(chatScrollView)
516
         mainOverlay.addArrangedSubview(chatBottomSpacer)
525
         mainOverlay.addArrangedSubview(chatBottomSpacer)
517
-        mainOverlay.addArrangedSubview(searchBarShadowHost)
526
+        mainOverlay.addArrangedSubview(searchBarColumn)
518
 
527
 
519
         panelsRow.addSubview(sidebar)
528
         panelsRow.addSubview(sidebar)
520
         panelsRow.addSubview(mainHost)
529
         panelsRow.addSubview(mainHost)
@@ -556,7 +565,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
556
             indeedJobBrowserHost.topAnchor.constraint(equalTo: mainHost.topAnchor),
565
             indeedJobBrowserHost.topAnchor.constraint(equalTo: mainHost.topAnchor),
557
             indeedJobBrowserHost.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
566
             indeedJobBrowserHost.bottomAnchor.constraint(equalTo: mainHost.bottomAnchor, constant: -24),
558
 
567
 
559
-            searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
568
+            searchBarColumn.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
560
             featureCardsRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
569
             featureCardsRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
561
             chatHeaderRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
570
             chatHeaderRow.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
562
             chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
571
             chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
@@ -605,6 +614,21 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
605
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
614
             upgradeDescription.preferredMaxLayoutWidth = descriptionWidth
606
             upgradeButton.title = "Try Pro"
615
             upgradeButton.title = "Try Pro"
607
         }
616
         }
617
+        updateFreeJobSearchQuotaLabel()
618
+    }
619
+
620
+    private func updateFreeJobSearchQuotaLabel() {
621
+        let isPro = SubscriptionStore.shared.isProActive
622
+        if isPro {
623
+            freeJobSearchQuotaLabel.isHidden = true
624
+            freeJobSearchQuotaLabel.stringValue = ""
625
+            return
626
+        }
627
+        let remaining = FreeTierJobSearchQuota.remainingUserMessages(isProActive: false)
628
+        freeJobSearchQuotaLabel.isHidden = false
629
+        freeJobSearchQuotaLabel.stringValue = remaining == 1
630
+            ? "1 reply left"
631
+            : "\(remaining) replies left"
608
     }
632
     }
609
 
633
 
610
     /// Returns `false` and presents the paywall when the user does not have an active Pro subscription.
634
     /// Returns `false` and presents the paywall when the user does not have an active Pro subscription.
@@ -1209,6 +1233,24 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1209
         let pillCorner: CGFloat = 27
1233
         let pillCorner: CGFloat = 27
1210
         let barHeight: CGFloat = 54
1234
         let barHeight: CGFloat = 54
1211
 
1235
 
1236
+        searchBarColumn.orientation = .vertical
1237
+        searchBarColumn.spacing = 6
1238
+        searchBarColumn.alignment = .width
1239
+        searchBarColumn.distribution = .fill
1240
+        searchBarColumn.translatesAutoresizingMaskIntoConstraints = false
1241
+        searchBarColumn.setContentHuggingPriority(.defaultHigh, for: .vertical)
1242
+
1243
+        freeJobSearchQuotaLabel.font = .systemFont(ofSize: 11, weight: .medium)
1244
+        freeJobSearchQuotaLabel.textColor = Theme.secondaryText
1245
+        freeJobSearchQuotaLabel.alignment = .center
1246
+        freeJobSearchQuotaLabel.lineBreakMode = .byTruncatingTail
1247
+        freeJobSearchQuotaLabel.maximumNumberOfLines = 1
1248
+        freeJobSearchQuotaLabel.setContentHuggingPriority(.required, for: .vertical)
1249
+        freeJobSearchQuotaLabel.setContentCompressionResistancePriority(.required, for: .vertical)
1250
+
1251
+        searchBarColumn.addArrangedSubview(searchBarShadowHost)
1252
+        searchBarColumn.addArrangedSubview(freeJobSearchQuotaLabel)
1253
+
1212
         searchBarShadowHost.translatesAutoresizingMaskIntoConstraints = false
1254
         searchBarShadowHost.translatesAutoresizingMaskIntoConstraints = false
1213
         searchBarShadowHost.wantsLayer = true
1255
         searchBarShadowHost.wantsLayer = true
1214
         searchBarShadowHost.layer?.masksToBounds = false
1256
         searchBarShadowHost.layer?.masksToBounds = false
@@ -1384,6 +1426,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1384
             findJobsCTAHost.widthAnchor.constraint(greaterThanOrEqualTo: sendContentStack.widthAnchor, constant: sendContentPadding * 2)
1426
             findJobsCTAHost.widthAnchor.constraint(greaterThanOrEqualTo: sendContentStack.widthAnchor, constant: sendContentPadding * 2)
1385
         ])
1427
         ])
1386
         searchCard.hoverHandler = nil
1428
         searchCard.hoverHandler = nil
1429
+        updateFreeJobSearchQuotaLabel()
1387
     }
1430
     }
1388
 
1431
 
1389
     private func updateFindJobsCTAShadowPath() {
1432
     private func updateFindJobsCTAShadowPath() {
@@ -2168,6 +2211,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2168
 
2211
 
2169
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
2212
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
2170
         FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
2213
         FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
2214
+        updateFreeJobSearchQuotaLabel()
2171
         isAwaitingResponse = true
2215
         isAwaitingResponse = true
2172
         addInlineChatThinkingRow()
2216
         addInlineChatThinkingRow()
2173
         setInputEnabled(false)
2217
         setInputEnabled(false)