@lattices/cli 0.4.6 → 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 +65 -1
- package/app/Sources/AppShell/AppShellView.swift +10 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +2 -2
- package/app/Sources/AppShell/KeyRecorderView.swift +1 -1
- package/app/Sources/AppShell/MainView.swift +1 -1
- package/app/Sources/AppShell/Preferences.swift +29 -3
- package/app/Sources/AppShell/SettingsView.swift +525 -60
- package/app/Sources/AppShell/SettingsWindow.swift +4 -0
- package/app/Sources/Core/Actions/HotkeyStore.swift +13 -1
- 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/Desktop/WindowTiler.swift +0 -2
- 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/Overlays/CommandMode/CommandModeState.swift +101 -0
- package/app/Sources/Core/Overlays/CommandMode/CommandModeView.swift +113 -4
- package/app/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +9 -5
- package/app/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +1 -0
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -1
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +20 -7
- package/app/Sources/Core/Workspace/SessionManager.swift +1 -1
- package/app/Sources/Core/Workspace/WorkspaceManager.swift +62 -7
- 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:
|
|
@@ -57,8 +63,10 @@ struct SettingsContentView: View {
|
|
|
57
63
|
@ObservedObject var prefs: Preferences
|
|
58
64
|
@ObservedObject var scanner: ProjectScanner
|
|
59
65
|
@ObservedObject var hotkeyStore: HotkeyStore = .shared
|
|
66
|
+
@ObservedObject var workspaceManager: WorkspaceManager = .shared
|
|
60
67
|
@ObservedObject var appUpdater: AppUpdater = .shared
|
|
61
68
|
@ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
|
|
69
|
+
@ObservedObject var keyboardRemapStore: KeyboardRemapStore = .shared
|
|
62
70
|
var onBack: (() -> Void)? = nil
|
|
63
71
|
|
|
64
72
|
@State private var selectedTab: SettingsSection = .general
|
|
@@ -77,6 +85,11 @@ struct SettingsContentView: View {
|
|
|
77
85
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
78
86
|
.clipped()
|
|
79
87
|
.background(PanelBackground())
|
|
88
|
+
.onAppear {
|
|
89
|
+
if page == .companionSettings {
|
|
90
|
+
selectedTab = .companion
|
|
91
|
+
}
|
|
92
|
+
}
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
// MARK: - Back Bar
|
|
@@ -85,18 +98,27 @@ struct SettingsContentView: View {
|
|
|
85
98
|
page == .docs ? "Docs" : selectedTab.title
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
private var snapModifierBinding: Binding<SnapModifierKey> {
|
|
102
|
+
Binding(
|
|
103
|
+
get: { workspaceManager.snapZonesConfig.modifier ?? .command },
|
|
104
|
+
set: { workspaceManager.updateSnapModifier($0) }
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
private var backBar: some View {
|
|
89
109
|
VStack(spacing: 0) {
|
|
90
110
|
HStack(spacing: 8) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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() } }
|
|
97
121
|
}
|
|
98
|
-
.buttonStyle(.plain)
|
|
99
|
-
.onHover { h in if h { NSCursor.pointingHand.push() } else { NSCursor.pop() } }
|
|
100
122
|
|
|
101
123
|
Text(page == .docs ? "Docs" : currentTabLabel)
|
|
102
124
|
.font(Typo.heading(13))
|
|
@@ -232,6 +254,8 @@ struct SettingsContentView: View {
|
|
|
232
254
|
switch selectedTab {
|
|
233
255
|
case .general:
|
|
234
256
|
generalContent
|
|
257
|
+
case .companion:
|
|
258
|
+
companionContent
|
|
235
259
|
case .ai:
|
|
236
260
|
aiContent
|
|
237
261
|
case .search:
|
|
@@ -459,13 +483,28 @@ struct SettingsContentView: View {
|
|
|
459
483
|
.labelsHidden()
|
|
460
484
|
}
|
|
461
485
|
|
|
462
|
-
|
|
486
|
+
HStack {
|
|
487
|
+
Text("Snap modifier")
|
|
488
|
+
.font(Typo.mono(10))
|
|
489
|
+
.foregroundColor(Palette.textDim)
|
|
490
|
+
Spacer()
|
|
491
|
+
Picker("", selection: snapModifierBinding) {
|
|
492
|
+
ForEach(SnapModifierKey.allCases) { modifier in
|
|
493
|
+
Text(modifier.shortLabel).tag(modifier)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
.pickerStyle(.segmented)
|
|
497
|
+
.labelsHidden()
|
|
498
|
+
.frame(width: 220)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
Text("Dragging stays normal until you hold \(snapModifierBinding.wrappedValue.label). While that key is down, Lattices reveals snap targets and a live preview for the window you’re moving.")
|
|
463
502
|
.font(Typo.caption(9))
|
|
464
503
|
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
465
504
|
|
|
466
505
|
cardDivider
|
|
467
506
|
|
|
468
|
-
Text("
|
|
507
|
+
Text("Advanced landing-zone rules still live in ~/.lattices/snap-zones.json. Modifier changes here take effect on the next drag.")
|
|
469
508
|
.font(Typo.caption(9))
|
|
470
509
|
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
471
510
|
}
|
|
@@ -567,6 +606,83 @@ struct SettingsContentView: View {
|
|
|
567
606
|
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
568
607
|
}
|
|
569
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
|
+
}
|
|
570
686
|
}
|
|
571
687
|
.padding(16)
|
|
572
688
|
.frame(maxWidth: 760, alignment: .leading)
|
|
@@ -574,6 +690,371 @@ struct SettingsContentView: View {
|
|
|
574
690
|
}
|
|
575
691
|
}
|
|
576
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
|
+
}
|
|
729
|
+
}
|
|
730
|
+
.padding(16)
|
|
731
|
+
.frame(maxWidth: 760, alignment: .leading)
|
|
732
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
733
|
+
}
|
|
734
|
+
}
|
|
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
|
+
|
|
577
1058
|
// MARK: - AI
|
|
578
1059
|
|
|
579
1060
|
private var aiContent: some View {
|
|
@@ -1226,7 +1707,7 @@ struct SettingsContentView: View {
|
|
|
1226
1707
|
let layout = LatticesCompanionCockpitCatalog.normalized(prefs.companionCockpitLayout)
|
|
1227
1708
|
let selectedPage = layout.pages.first(where: { $0.id == selectedCompanionCockpitPageID }) ?? layout.pages.first
|
|
1228
1709
|
let categories = LatticesCompanionShortcutCategory.allCases
|
|
1229
|
-
let
|
|
1710
|
+
let trustedDeviceCount = companionTrustedDevices(revision: companionTrustRevision).count
|
|
1230
1711
|
|
|
1231
1712
|
return shortcutSectionCard(
|
|
1232
1713
|
title: "Companion Cockpit",
|
|
@@ -1250,63 +1731,45 @@ struct SettingsContentView: View {
|
|
|
1250
1731
|
Toggle("", isOn: $prefs.companionTrackpadEnabled)
|
|
1251
1732
|
.toggleStyle(.switch)
|
|
1252
1733
|
.labelsHidden()
|
|
1734
|
+
.disabled(!prefs.companionBridgeEnabled)
|
|
1735
|
+
.opacity(prefs.companionBridgeEnabled ? 1 : 0.45)
|
|
1253
1736
|
}
|
|
1254
1737
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
Text("New companions must be approved on the Mac before they can send encrypted bridge requests.")
|
|
1262
|
-
.font(Typo.caption(10.5))
|
|
1263
|
-
.foregroundColor(Palette.textMuted)
|
|
1264
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
Spacer()
|
|
1268
|
-
|
|
1269
|
-
if trustedDevices.isEmpty == false {
|
|
1270
|
-
Button("Forget All") {
|
|
1271
|
-
LatticesCompanionSecurityCoordinator.shared.clearTrustedDevices()
|
|
1272
|
-
companionTrustRevision += 1
|
|
1273
|
-
}
|
|
1274
|
-
.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.")
|
|
1275
1744
|
.font(Typo.caption(10.5))
|
|
1276
|
-
.foregroundColor(Palette.
|
|
1277
|
-
|
|
1745
|
+
.foregroundColor(Palette.textMuted)
|
|
1746
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
1278
1747
|
}
|
|
1279
1748
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
.foregroundColor(Palette.textDim)
|
|
1291
|
-
.frame(width: 14)
|
|
1292
|
-
|
|
1293
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
1294
|
-
Text(device.name)
|
|
1295
|
-
.font(Typo.caption(11))
|
|
1296
|
-
.foregroundColor(Palette.text)
|
|
1297
|
-
Text("\(device.fingerprint) · Last seen \(relativeTimestamp(device.lastSeenAt))")
|
|
1298
|
-
.font(Typo.caption(10))
|
|
1299
|
-
.foregroundColor(Palette.textMuted)
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
Spacer(minLength: 0)
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
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))
|
|
1305
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
|
+
)
|
|
1306
1768
|
}
|
|
1307
|
-
.
|
|
1308
|
-
.background(shortcutsInsetPanel)
|
|
1769
|
+
.buttonStyle(.plain)
|
|
1309
1770
|
}
|
|
1771
|
+
.padding(12)
|
|
1772
|
+
.background(shortcutsInsetPanel)
|
|
1310
1773
|
|
|
1311
1774
|
if let selectedPage {
|
|
1312
1775
|
Picker("Companion page", selection: $selectedCompanionCockpitPageID) {
|
|
@@ -1541,6 +2004,8 @@ struct SettingsContentView: View {
|
|
|
1541
2004
|
.foregroundColor(Palette.textMuted)
|
|
1542
2005
|
.fixedSize(horizontal: false, vertical: true)
|
|
1543
2006
|
}
|
|
2007
|
+
|
|
2008
|
+
compactKeyRecorder(action: .tileOrganize)
|
|
1544
2009
|
}
|
|
1545
2010
|
}
|
|
1546
2011
|
|