@lattices/cli 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/app/Info.plist +13 -2
- package/app/Lattices.app/Contents/Info.plist +13 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Sources/AppShell/App.swift +7 -1
- package/app/Sources/AppShell/AppDelegate.swift +60 -1
- package/app/Sources/AppShell/AppShellView.swift +10 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +2 -2
- package/app/Sources/AppShell/MainView.swift +1 -1
- package/app/Sources/AppShell/Preferences.swift +29 -1
- package/app/Sources/AppShell/SettingsView.swift +498 -58
- package/app/Sources/AppShell/SettingsWindow.swift +4 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +23 -7
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +35 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +1 -1
- package/app/Sources/Core/Input/KeyboardRemapConfig.swift +69 -0
- package/app/Sources/Core/Input/KeyboardRemapController.swift +184 -0
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +84 -0
- package/app/Sources/Core/Workspace/SessionManager.swift +1 -1
- package/app/Sources/Core/Workspace/WorkspaceManager.swift +3 -3
- package/bin/lattices-app.ts +11 -0
- package/bin/lattices-dev +11 -0
- package/bin/lattices.ts +57 -17
- package/docs/app.md +30 -2
- package/docs/companion-deck.md +29 -0
- package/docs/concepts.md +5 -5
- package/docs/config.md +34 -9
- package/docs/layers.md +1 -1
- package/docs/overview.md +1 -1
- package/docs/quickstart.md +4 -4
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import SwiftUI
|
|
|
6
6
|
struct SettingsContentView: View {
|
|
7
7
|
private enum SettingsSection: String, CaseIterable, Identifiable {
|
|
8
8
|
case general
|
|
9
|
+
case companion
|
|
9
10
|
case ai
|
|
10
11
|
case search
|
|
11
12
|
case shortcuts
|
|
@@ -15,6 +16,7 @@ struct SettingsContentView: View {
|
|
|
15
16
|
var title: String {
|
|
16
17
|
switch self {
|
|
17
18
|
case .general: return "General"
|
|
19
|
+
case .companion: return "Companion"
|
|
18
20
|
case .ai: return "AI"
|
|
19
21
|
case .search: return "Search & OCR"
|
|
20
22
|
case .shortcuts: return "Shortcuts"
|
|
@@ -24,6 +26,7 @@ struct SettingsContentView: View {
|
|
|
24
26
|
var icon: String {
|
|
25
27
|
switch self {
|
|
26
28
|
case .general: return "slider.horizontal.3"
|
|
29
|
+
case .companion: return "ipad.and.iphone"
|
|
27
30
|
case .ai: return "sparkles"
|
|
28
31
|
case .search: return "text.viewfinder"
|
|
29
32
|
case .shortcuts: return "command"
|
|
@@ -33,6 +36,7 @@ struct SettingsContentView: View {
|
|
|
33
36
|
var eyebrow: String {
|
|
34
37
|
switch self {
|
|
35
38
|
case .general: return "Workspace"
|
|
39
|
+
case .companion: return "Local Bridge"
|
|
36
40
|
case .ai: return "Agents"
|
|
37
41
|
case .search: return "Indexing"
|
|
38
42
|
case .shortcuts: return "Controls"
|
|
@@ -43,6 +47,8 @@ struct SettingsContentView: View {
|
|
|
43
47
|
switch self {
|
|
44
48
|
case .general:
|
|
45
49
|
return "Terminal defaults, scan roots, window snapping, and app updates."
|
|
50
|
+
case .companion:
|
|
51
|
+
return "Local-network pairing, trusted iPad devices, and bridge security."
|
|
46
52
|
case .ai:
|
|
47
53
|
return "Claude CLI detection plus advisor model and spending controls."
|
|
48
54
|
case .search:
|
|
@@ -60,6 +66,7 @@ struct SettingsContentView: View {
|
|
|
60
66
|
@ObservedObject var workspaceManager: WorkspaceManager = .shared
|
|
61
67
|
@ObservedObject var appUpdater: AppUpdater = .shared
|
|
62
68
|
@ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
|
|
69
|
+
@ObservedObject var keyboardRemapStore: KeyboardRemapStore = .shared
|
|
63
70
|
var onBack: (() -> Void)? = nil
|
|
64
71
|
|
|
65
72
|
@State private var selectedTab: SettingsSection = .general
|
|
@@ -78,6 +85,11 @@ struct SettingsContentView: View {
|
|
|
78
85
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
79
86
|
.clipped()
|
|
80
87
|
.background(PanelBackground())
|
|
88
|
+
.onAppear {
|
|
89
|
+
if page == .companionSettings {
|
|
90
|
+
selectedTab = .companion
|
|
91
|
+
}
|
|
92
|
+
}
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
// MARK: - Back Bar
|
|
@@ -96,15 +108,17 @@ struct SettingsContentView: View {
|
|
|
96
108
|
private var backBar: some View {
|
|
97
109
|
VStack(spacing: 0) {
|
|
98
110
|
HStack(spacing: 8) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
if let onBack {
|
|
112
|
+
Button {
|
|
113
|
+
onBack()
|
|
114
|
+
} label: {
|
|
115
|
+
Image(systemName: "chevron.left")
|
|
116
|
+
.font(.system(size: 10, weight: .semibold))
|
|
117
|
+
.foregroundColor(Palette.textMuted)
|
|
118
|
+
}
|
|
119
|
+
.buttonStyle(.plain)
|
|
120
|
+
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
|
|
105
121
|
}
|
|
106
|
-
.buttonStyle(.plain)
|
|
107
|
-
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
|
|
108
122
|
|
|
109
123
|
Text(page == .docs ? "Docs" : currentTabLabel)
|
|
110
124
|
.font(Typo.heading(13))
|
|
@@ -240,6 +254,8 @@ struct SettingsContentView: View {
|
|
|
240
254
|
switch selectedTab {
|
|
241
255
|
case .general:
|
|
242
256
|
generalContent
|
|
257
|
+
case .companion:
|
|
258
|
+
companionContent
|
|
243
259
|
case .ai:
|
|
244
260
|
aiContent
|
|
245
261
|
case .search:
|
|
@@ -590,6 +606,126 @@ struct SettingsContentView: View {
|
|
|
590
606
|
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
591
607
|
}
|
|
592
608
|
}
|
|
609
|
+
|
|
610
|
+
settingsCard {
|
|
611
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
612
|
+
Text("Keyboard remaps")
|
|
613
|
+
.font(Typo.mono(11))
|
|
614
|
+
.foregroundColor(Palette.text)
|
|
615
|
+
|
|
616
|
+
HStack {
|
|
617
|
+
Text("Caps Lock as Hyper")
|
|
618
|
+
.font(Typo.mono(10))
|
|
619
|
+
.foregroundColor(Palette.textDim)
|
|
620
|
+
Spacer()
|
|
621
|
+
Toggle("", isOn: $prefs.keyboardRemapsEnabled)
|
|
622
|
+
.toggleStyle(.switch)
|
|
623
|
+
.controlSize(.small)
|
|
624
|
+
.labelsHidden()
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
Text("Rules live in ~/.lattices/keyboard-remaps.json. The default maps hold Caps Lock to Hyper and tap Caps Lock to Escape, so the existing Hyper shortcuts work on the laptop keyboard.")
|
|
628
|
+
.font(Typo.caption(9))
|
|
629
|
+
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
630
|
+
|
|
631
|
+
cardDivider
|
|
632
|
+
|
|
633
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
634
|
+
Text("Active remaps")
|
|
635
|
+
.font(Typo.mono(10))
|
|
636
|
+
.foregroundColor(Palette.textDim)
|
|
637
|
+
|
|
638
|
+
ForEach(keyboardRemapStore.summaryLines.prefix(4), id: \.self) { line in
|
|
639
|
+
Text(line)
|
|
640
|
+
.font(Typo.caption(9))
|
|
641
|
+
.foregroundColor(Palette.textMuted.opacity(0.78))
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if keyboardRemapStore.summaryLines.isEmpty {
|
|
645
|
+
Text("No active remaps")
|
|
646
|
+
.font(Typo.caption(9))
|
|
647
|
+
.foregroundColor(Palette.textMuted.opacity(0.6))
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
HStack(spacing: 8) {
|
|
652
|
+
Button {
|
|
653
|
+
keyboardRemapStore.openConfiguration()
|
|
654
|
+
} label: {
|
|
655
|
+
Text("Configure...")
|
|
656
|
+
.font(Typo.monoBold(10))
|
|
657
|
+
.foregroundColor(Palette.text)
|
|
658
|
+
.padding(.horizontal, 12)
|
|
659
|
+
.padding(.vertical, 4)
|
|
660
|
+
.background(
|
|
661
|
+
RoundedRectangle(cornerRadius: 4)
|
|
662
|
+
.fill(Palette.surfaceHov)
|
|
663
|
+
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
.buttonStyle(.plain)
|
|
667
|
+
|
|
668
|
+
Button {
|
|
669
|
+
keyboardRemapStore.restoreDefaults()
|
|
670
|
+
} label: {
|
|
671
|
+
Text("Restore Defaults")
|
|
672
|
+
.font(Typo.monoBold(10))
|
|
673
|
+
.foregroundColor(Palette.text)
|
|
674
|
+
.padding(.horizontal, 12)
|
|
675
|
+
.padding(.vertical, 4)
|
|
676
|
+
.background(
|
|
677
|
+
RoundedRectangle(cornerRadius: 4)
|
|
678
|
+
.fill(Palette.surfaceHov)
|
|
679
|
+
.overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
.buttonStyle(.plain)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
.padding(16)
|
|
688
|
+
.frame(maxWidth: 760, alignment: .leading)
|
|
689
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// MARK: - Companion
|
|
694
|
+
|
|
695
|
+
private var companionContent: some View {
|
|
696
|
+
ScrollView {
|
|
697
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
698
|
+
companionBridgeOverviewCard
|
|
699
|
+
companionTrustedDevicesCard
|
|
700
|
+
|
|
701
|
+
settingsCard {
|
|
702
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
703
|
+
Text("Input")
|
|
704
|
+
.font(Typo.mono(11))
|
|
705
|
+
.foregroundColor(Palette.text)
|
|
706
|
+
|
|
707
|
+
HStack(alignment: .top, spacing: 12) {
|
|
708
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
709
|
+
Text("Trackpad proxy")
|
|
710
|
+
.font(Typo.mono(10))
|
|
711
|
+
.foregroundColor(Palette.textDim)
|
|
712
|
+
Text("Allow paired companions with the input.trackpad grant to move the Mac pointer through the encrypted bridge.")
|
|
713
|
+
.font(Typo.caption(9.5))
|
|
714
|
+
.foregroundColor(Palette.textMuted.opacity(0.75))
|
|
715
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
Spacer()
|
|
719
|
+
|
|
720
|
+
Toggle("", isOn: $prefs.companionTrackpadEnabled)
|
|
721
|
+
.toggleStyle(.switch)
|
|
722
|
+
.controlSize(.small)
|
|
723
|
+
.labelsHidden()
|
|
724
|
+
.disabled(!prefs.companionBridgeEnabled)
|
|
725
|
+
.opacity(prefs.companionBridgeEnabled ? 1 : 0.45)
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
593
729
|
}
|
|
594
730
|
.padding(16)
|
|
595
731
|
.frame(maxWidth: 760, alignment: .leading)
|
|
@@ -597,6 +733,328 @@ struct SettingsContentView: View {
|
|
|
597
733
|
}
|
|
598
734
|
}
|
|
599
735
|
|
|
736
|
+
private var companionBridgeOverviewCard: some View {
|
|
737
|
+
settingsCard {
|
|
738
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
739
|
+
HStack(alignment: .top, spacing: 10) {
|
|
740
|
+
RoundedRectangle(cornerRadius: 6)
|
|
741
|
+
.fill(Palette.running.opacity(0.14))
|
|
742
|
+
.overlay(
|
|
743
|
+
Image(systemName: "lock.shield")
|
|
744
|
+
.font(.system(size: 13, weight: .semibold))
|
|
745
|
+
.foregroundColor(Palette.running)
|
|
746
|
+
)
|
|
747
|
+
.frame(width: 30, height: 30)
|
|
748
|
+
|
|
749
|
+
VStack(alignment: .leading, spacing: 3) {
|
|
750
|
+
Text(prefs.companionBridgeEnabled ? "Secure local bridge" : "Local bridge off")
|
|
751
|
+
.font(Typo.mono(12))
|
|
752
|
+
.foregroundColor(Palette.text)
|
|
753
|
+
Text(prefs.companionBridgeEnabled
|
|
754
|
+
? "Bonjour discovery with explicit Mac approval, signed requests, encrypted payloads, and capability grants."
|
|
755
|
+
: "The companion bridge is not listening or advertising on the local network until you turn it on.")
|
|
756
|
+
.font(Typo.caption(10))
|
|
757
|
+
.foregroundColor(Palette.textMuted)
|
|
758
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
Spacer()
|
|
762
|
+
|
|
763
|
+
Toggle("", isOn: $prefs.companionBridgeEnabled)
|
|
764
|
+
.toggleStyle(.switch)
|
|
765
|
+
.controlSize(.small)
|
|
766
|
+
.labelsHidden()
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
cardDivider
|
|
770
|
+
|
|
771
|
+
LazyVGrid(
|
|
772
|
+
columns: [
|
|
773
|
+
GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
|
|
774
|
+
GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
|
|
775
|
+
GridItem(.flexible(minimum: 120), spacing: 10, alignment: .leading),
|
|
776
|
+
],
|
|
777
|
+
alignment: .leading,
|
|
778
|
+
spacing: 10
|
|
779
|
+
) {
|
|
780
|
+
companionBridgeFact(
|
|
781
|
+
label: "Status",
|
|
782
|
+
value: prefs.companionBridgeEnabled ? "enabled" : "off"
|
|
783
|
+
)
|
|
784
|
+
companionBridgeFact(
|
|
785
|
+
label: "Port",
|
|
786
|
+
value: String(LatticesCompanionBridgeServer.defaultPort)
|
|
787
|
+
)
|
|
788
|
+
companionBridgeFact(
|
|
789
|
+
label: "Protocol",
|
|
790
|
+
value: "v\(LatticesCompanionBridgeServer.protocolVersion)"
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
795
|
+
Text("Enable deep link")
|
|
796
|
+
.font(Typo.mono(10))
|
|
797
|
+
.foregroundColor(Palette.textDim)
|
|
798
|
+
Text("lattices://companion/enable")
|
|
799
|
+
.font(Typo.monoBold(12))
|
|
800
|
+
.foregroundColor(Palette.text)
|
|
801
|
+
.textSelection(.enabled)
|
|
802
|
+
}
|
|
803
|
+
.padding(10)
|
|
804
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
805
|
+
.background(shortcutsInsetPanel)
|
|
806
|
+
|
|
807
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
808
|
+
Text("Mac bridge fingerprint")
|
|
809
|
+
.font(Typo.mono(10))
|
|
810
|
+
.foregroundColor(Palette.textDim)
|
|
811
|
+
Text(LatticesCompanionSecurityCoordinator.shared.bridgeFingerprint)
|
|
812
|
+
.font(Typo.monoBold(13))
|
|
813
|
+
.foregroundColor(Palette.text)
|
|
814
|
+
.textSelection(.enabled)
|
|
815
|
+
}
|
|
816
|
+
.padding(10)
|
|
817
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
818
|
+
.background(shortcutsInsetPanel)
|
|
819
|
+
|
|
820
|
+
HStack(spacing: 6) {
|
|
821
|
+
ForEach(DeckBridgeCapability.defaultCompanionCapabilities, id: \.self) { capability in
|
|
822
|
+
companionCapabilityBadge(capability)
|
|
823
|
+
}
|
|
824
|
+
Spacer(minLength: 0)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private var companionTrustedDevicesCard: some View {
|
|
831
|
+
let trustedDevices = companionTrustedDevices(revision: companionTrustRevision)
|
|
832
|
+
|
|
833
|
+
return settingsCard {
|
|
834
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
835
|
+
HStack(alignment: .top, spacing: 12) {
|
|
836
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
837
|
+
Text("Paired devices")
|
|
838
|
+
.font(Typo.mono(12))
|
|
839
|
+
.foregroundColor(Palette.text)
|
|
840
|
+
Text("Only trusted devices can call protected deck and input routes. Pairing grants are listed per device.")
|
|
841
|
+
.font(Typo.caption(10))
|
|
842
|
+
.foregroundColor(Palette.textMuted)
|
|
843
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
Spacer()
|
|
847
|
+
|
|
848
|
+
HStack(spacing: 8) {
|
|
849
|
+
Button {
|
|
850
|
+
companionTrustRevision += 1
|
|
851
|
+
} label: {
|
|
852
|
+
Image(systemName: "arrow.clockwise")
|
|
853
|
+
.font(.system(size: 10, weight: .semibold))
|
|
854
|
+
.foregroundColor(Palette.textDim)
|
|
855
|
+
.frame(width: 24, height: 24)
|
|
856
|
+
.background(
|
|
857
|
+
RoundedRectangle(cornerRadius: 5)
|
|
858
|
+
.fill(Palette.surfaceHov)
|
|
859
|
+
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
.buttonStyle(.plain)
|
|
863
|
+
|
|
864
|
+
if trustedDevices.isEmpty == false {
|
|
865
|
+
Button {
|
|
866
|
+
guard confirmForgetTrustedDevices() else { return }
|
|
867
|
+
LatticesCompanionSecurityCoordinator.shared.clearTrustedDevices()
|
|
868
|
+
companionTrustRevision += 1
|
|
869
|
+
} label: {
|
|
870
|
+
Text("Forget All")
|
|
871
|
+
.font(Typo.monoBold(10))
|
|
872
|
+
.foregroundColor(Palette.kill.opacity(0.9))
|
|
873
|
+
.padding(.horizontal, 10)
|
|
874
|
+
.padding(.vertical, 5)
|
|
875
|
+
.background(
|
|
876
|
+
RoundedRectangle(cornerRadius: 5)
|
|
877
|
+
.fill(Palette.kill.opacity(0.10))
|
|
878
|
+
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.kill.opacity(0.22), lineWidth: 0.5))
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
.buttonStyle(.plain)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if trustedDevices.isEmpty {
|
|
887
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
888
|
+
Image(systemName: "ipad.and.iphone")
|
|
889
|
+
.font(.system(size: 18, weight: .semibold))
|
|
890
|
+
.foregroundColor(Palette.textMuted)
|
|
891
|
+
|
|
892
|
+
Text("No paired iPad or iPhone devices yet.")
|
|
893
|
+
.font(Typo.caption(10.5))
|
|
894
|
+
.foregroundColor(Palette.textMuted)
|
|
895
|
+
|
|
896
|
+
Text("Open the Lattices companion app on your iPad and select this Mac. You’ll approve the pairing prompt here.")
|
|
897
|
+
.font(Typo.caption(9.5))
|
|
898
|
+
.foregroundColor(Palette.textMuted.opacity(0.72))
|
|
899
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
900
|
+
}
|
|
901
|
+
.padding(12)
|
|
902
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
903
|
+
.background(shortcutsInsetPanel)
|
|
904
|
+
} else {
|
|
905
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
906
|
+
ForEach(trustedDevices) { device in
|
|
907
|
+
companionDeviceRow(device)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private func companionBridgeFact(label: String, value: String) -> some View {
|
|
916
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
917
|
+
Text(label.uppercased())
|
|
918
|
+
.font(Typo.pixel(11))
|
|
919
|
+
.foregroundColor(Palette.textDim)
|
|
920
|
+
.tracking(1)
|
|
921
|
+
Text(value)
|
|
922
|
+
.font(Typo.monoBold(11))
|
|
923
|
+
.foregroundColor(Palette.text)
|
|
924
|
+
.lineLimit(1)
|
|
925
|
+
.truncationMode(.middle)
|
|
926
|
+
}
|
|
927
|
+
.padding(10)
|
|
928
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
929
|
+
.background(shortcutsInsetPanel)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private func companionDeviceRow(_ device: DeckTrustedDeviceSummary) -> some View {
|
|
933
|
+
HStack(alignment: .top, spacing: 10) {
|
|
934
|
+
RoundedRectangle(cornerRadius: 6)
|
|
935
|
+
.fill(Palette.surfaceHov)
|
|
936
|
+
.overlay(
|
|
937
|
+
Image(systemName: companionDeviceIcon(for: device.name))
|
|
938
|
+
.font(.system(size: 13, weight: .semibold))
|
|
939
|
+
.foregroundColor(Palette.textDim)
|
|
940
|
+
)
|
|
941
|
+
.frame(width: 30, height: 30)
|
|
942
|
+
|
|
943
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
944
|
+
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
945
|
+
Text(device.name)
|
|
946
|
+
.font(Typo.monoBold(11))
|
|
947
|
+
.foregroundColor(Palette.text)
|
|
948
|
+
.lineLimit(1)
|
|
949
|
+
|
|
950
|
+
Text(device.fingerprint)
|
|
951
|
+
.font(Typo.mono(10))
|
|
952
|
+
.foregroundColor(Palette.textMuted)
|
|
953
|
+
.lineLimit(1)
|
|
954
|
+
|
|
955
|
+
Spacer(minLength: 0)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
HStack(spacing: 10) {
|
|
959
|
+
Text("Paired \(relativeTimestamp(device.pairedAt))")
|
|
960
|
+
Text("Last seen \(relativeTimestamp(device.lastSeenAt))")
|
|
961
|
+
}
|
|
962
|
+
.font(Typo.caption(9.5))
|
|
963
|
+
.foregroundColor(Palette.textMuted.opacity(0.78))
|
|
964
|
+
|
|
965
|
+
HStack(spacing: 6) {
|
|
966
|
+
ForEach(device.capabilities, id: \.self) { capability in
|
|
967
|
+
companionCapabilityBadge(capability)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
Spacer(minLength: 0)
|
|
973
|
+
|
|
974
|
+
Button {
|
|
975
|
+
guard confirmRevokeTrustedDevice(device) else { return }
|
|
976
|
+
LatticesCompanionSecurityCoordinator.shared.revokeTrustedDevice(id: device.id)
|
|
977
|
+
companionTrustRevision += 1
|
|
978
|
+
} label: {
|
|
979
|
+
HStack(spacing: 5) {
|
|
980
|
+
Image(systemName: "xmark.shield")
|
|
981
|
+
.font(.system(size: 10, weight: .semibold))
|
|
982
|
+
Text("Revoke")
|
|
983
|
+
.font(Typo.monoBold(9.5))
|
|
984
|
+
}
|
|
985
|
+
.foregroundColor(Palette.kill.opacity(0.95))
|
|
986
|
+
.padding(.horizontal, 8)
|
|
987
|
+
.padding(.vertical, 5)
|
|
988
|
+
.background(
|
|
989
|
+
RoundedRectangle(cornerRadius: 5)
|
|
990
|
+
.fill(Palette.kill.opacity(0.10))
|
|
991
|
+
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.kill.opacity(0.22), lineWidth: 0.5))
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
.buttonStyle(.plain)
|
|
995
|
+
.help("Revoke this paired device")
|
|
996
|
+
}
|
|
997
|
+
.padding(12)
|
|
998
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
999
|
+
.background(shortcutsInsetPanel)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
private func companionCapabilityBadge(_ capability: String) -> some View {
|
|
1003
|
+
Text(companionCapabilityLabel(capability))
|
|
1004
|
+
.font(Typo.monoBold(9))
|
|
1005
|
+
.foregroundColor(Palette.running.opacity(0.92))
|
|
1006
|
+
.padding(.horizontal, 7)
|
|
1007
|
+
.padding(.vertical, 3)
|
|
1008
|
+
.background(
|
|
1009
|
+
Capsule()
|
|
1010
|
+
.fill(Palette.running.opacity(0.10))
|
|
1011
|
+
.overlay(Capsule().strokeBorder(Palette.running.opacity(0.18), lineWidth: 0.5))
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private func companionCapabilityLabel(_ capability: String) -> String {
|
|
1016
|
+
switch capability {
|
|
1017
|
+
case DeckBridgeCapability.deckRead:
|
|
1018
|
+
return "Deck Read"
|
|
1019
|
+
case DeckBridgeCapability.deckPerform:
|
|
1020
|
+
return "Deck Actions"
|
|
1021
|
+
case DeckBridgeCapability.inputTrackpad:
|
|
1022
|
+
return "Trackpad"
|
|
1023
|
+
default:
|
|
1024
|
+
return capability
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private func companionDeviceIcon(for name: String) -> String {
|
|
1029
|
+
name.localizedCaseInsensitiveContains("ipad") ? "ipad" : "iphone"
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private func confirmForgetTrustedDevices() -> Bool {
|
|
1033
|
+
let alert = NSAlert()
|
|
1034
|
+
alert.alertStyle = .warning
|
|
1035
|
+
alert.messageText = "Forget all paired companion devices?"
|
|
1036
|
+
alert.informativeText = "Your iPad or iPhone will need to pair again before it can control Lattices."
|
|
1037
|
+
alert.addButton(withTitle: "Forget Devices")
|
|
1038
|
+
alert.addButton(withTitle: "Cancel")
|
|
1039
|
+
return alert.runModal() == .alertFirstButtonReturn
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private func confirmRevokeTrustedDevice(_ device: DeckTrustedDeviceSummary) -> Bool {
|
|
1043
|
+
let alert = NSAlert()
|
|
1044
|
+
alert.alertStyle = .warning
|
|
1045
|
+
alert.messageText = "Revoke \(device.name)?"
|
|
1046
|
+
alert.informativeText = """
|
|
1047
|
+
This removes the paired-device trust record for \(device.name).
|
|
1048
|
+
|
|
1049
|
+
Fingerprint: \(device.fingerprint)
|
|
1050
|
+
|
|
1051
|
+
The device will need to pair again before it can control Lattices.
|
|
1052
|
+
"""
|
|
1053
|
+
alert.addButton(withTitle: "Revoke Device")
|
|
1054
|
+
alert.addButton(withTitle: "Cancel")
|
|
1055
|
+
return alert.runModal() == .alertFirstButtonReturn
|
|
1056
|
+
}
|
|
1057
|
+
|
|
600
1058
|
// MARK: - AI
|
|
601
1059
|
|
|
602
1060
|
private var aiContent: some View {
|
|
@@ -1249,7 +1707,7 @@ struct SettingsContentView: View {
|
|
|
1249
1707
|
let layout = LatticesCompanionCockpitCatalog.normalized(prefs.companionCockpitLayout)
|
|
1250
1708
|
let selectedPage = layout.pages.first(where: { $0.id == selectedCompanionCockpitPageID }) ?? layout.pages.first
|
|
1251
1709
|
let categories = LatticesCompanionShortcutCategory.allCases
|
|
1252
|
-
let
|
|
1710
|
+
let trustedDeviceCount = companionTrustedDevices(revision: companionTrustRevision).count
|
|
1253
1711
|
|
|
1254
1712
|
return shortcutSectionCard(
|
|
1255
1713
|
title: "Companion Cockpit",
|
|
@@ -1273,63 +1731,45 @@ struct SettingsContentView: View {
|
|
|
1273
1731
|
Toggle("", isOn: $prefs.companionTrackpadEnabled)
|
|
1274
1732
|
.toggleStyle(.switch)
|
|
1275
1733
|
.labelsHidden()
|
|
1734
|
+
.disabled(!prefs.companionBridgeEnabled)
|
|
1735
|
+
.opacity(prefs.companionBridgeEnabled ? 1 : 0.45)
|
|
1276
1736
|
}
|
|
1277
1737
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
Text("New companions must be approved on the Mac before they can send encrypted bridge requests.")
|
|
1285
|
-
.font(Typo.caption(10.5))
|
|
1286
|
-
.foregroundColor(Palette.textMuted)
|
|
1287
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
Spacer()
|
|
1291
|
-
|
|
1292
|
-
if trustedDevices.isEmpty == false {
|
|
1293
|
-
Button("Forget All") {
|
|
1294
|
-
LatticesCompanionSecurityCoordinator.shared.clearTrustedDevices()
|
|
1295
|
-
companionTrustRevision += 1
|
|
1296
|
-
}
|
|
1297
|
-
.buttonStyle(.plain)
|
|
1738
|
+
HStack(alignment: .center, spacing: 12) {
|
|
1739
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
1740
|
+
Text("Pairing and trust")
|
|
1741
|
+
.font(Typo.monoBold(11))
|
|
1742
|
+
.foregroundColor(Palette.text)
|
|
1743
|
+
Text("\(trustedDeviceCount) paired \(trustedDeviceCount == 1 ? "device" : "devices"). Revoke devices and review bridge grants in Companion settings.")
|
|
1298
1744
|
.font(Typo.caption(10.5))
|
|
1299
|
-
.foregroundColor(Palette.
|
|
1300
|
-
|
|
1745
|
+
.foregroundColor(Palette.textMuted)
|
|
1746
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1301
1747
|
}
|
|
1302
1748
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
.foregroundColor(Palette.textDim)
|
|
1314
|
-
.frame(width: 14)
|
|
1315
|
-
|
|
1316
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
1317
|
-
Text(device.name)
|
|
1318
|
-
.font(Typo.caption(11))
|
|
1319
|
-
.foregroundColor(Palette.text)
|
|
1320
|
-
Text("\(device.fingerprint) · Last seen \(relativeTimestamp(device.lastSeenAt))")
|
|
1321
|
-
.font(Typo.caption(10))
|
|
1322
|
-
.foregroundColor(Palette.textMuted)
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
Spacer(minLength: 0)
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1749
|
+
Spacer()
|
|
1750
|
+
|
|
1751
|
+
Button {
|
|
1752
|
+
selectedTab = .companion
|
|
1753
|
+
} label: {
|
|
1754
|
+
HStack(spacing: 5) {
|
|
1755
|
+
Image(systemName: "ipad.and.iphone")
|
|
1756
|
+
.font(.system(size: 10, weight: .semibold))
|
|
1757
|
+
Text("Manage")
|
|
1758
|
+
.font(Typo.monoBold(10))
|
|
1328
1759
|
}
|
|
1760
|
+
.foregroundColor(Palette.text)
|
|
1761
|
+
.padding(.horizontal, 10)
|
|
1762
|
+
.padding(.vertical, 5)
|
|
1763
|
+
.background(
|
|
1764
|
+
RoundedRectangle(cornerRadius: 5)
|
|
1765
|
+
.fill(Palette.surfaceHov)
|
|
1766
|
+
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Palette.borderLit, lineWidth: 0.5))
|
|
1767
|
+
)
|
|
1329
1768
|
}
|
|
1330
|
-
.
|
|
1331
|
-
.background(shortcutsInsetPanel)
|
|
1769
|
+
.buttonStyle(.plain)
|
|
1332
1770
|
}
|
|
1771
|
+
.padding(12)
|
|
1772
|
+
.background(shortcutsInsetPanel)
|
|
1333
1773
|
|
|
1334
1774
|
if let selectedPage {
|
|
1335
1775
|
Picker("Companion page", selection: $selectedCompanionCockpitPageID) {
|
|
@@ -14,6 +14,10 @@ final class SettingsWindowController {
|
|
|
14
14
|
ScreenMapWindowController.shared.showPage(.settings)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
func showCompanion() {
|
|
18
|
+
ScreenMapWindowController.shared.showPage(.companionSettings)
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
func close() {
|
|
18
22
|
ScreenMapWindowController.shared.close()
|
|
19
23
|
}
|