@lattices/cli 0.4.7 → 0.4.9

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.
Files changed (32) hide show
  1. package/README.md +8 -6
  2. package/app/Info.plist +13 -2
  3. package/app/Lattices.app/Contents/Info.plist +13 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Sources/AppShell/App.swift +7 -1
  6. package/app/Sources/AppShell/AppDelegate.swift +64 -1
  7. package/app/Sources/AppShell/AppShellView.swift +10 -0
  8. package/app/Sources/AppShell/AppUpdater.swift +216 -4
  9. package/app/Sources/AppShell/CliActionLauncher.swift +2 -2
  10. package/app/Sources/AppShell/MainView.swift +1 -1
  11. package/app/Sources/AppShell/Preferences.swift +29 -1
  12. package/app/Sources/AppShell/SettingsView.swift +576 -61
  13. package/app/Sources/AppShell/SettingsWindow.swift +4 -0
  14. package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +23 -7
  15. package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +35 -0
  16. package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +1 -1
  17. package/app/Sources/Core/Input/KeyboardRemapConfig.swift +69 -0
  18. package/app/Sources/Core/Input/KeyboardRemapController.swift +184 -0
  19. package/app/Sources/Core/Input/KeyboardRemapStore.swift +84 -0
  20. package/app/Sources/Core/Workspace/SessionManager.swift +1 -1
  21. package/app/Sources/Core/Workspace/WorkspaceManager.swift +3 -3
  22. package/bin/lattices-app.ts +11 -0
  23. package/bin/lattices-dev +11 -0
  24. package/bin/lattices.ts +57 -17
  25. package/docs/app.md +30 -2
  26. package/docs/companion-deck.md +29 -0
  27. package/docs/concepts.md +5 -5
  28. package/docs/config.md +34 -9
  29. package/docs/layers.md +1 -1
  30. package/docs/overview.md +1 -1
  31. package/docs/quickstart.md +4 -4
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -81,11 +81,11 @@ Close your laptop, reboot, come back a week later — your editor, dev
81
81
  server, and test runner are exactly where you left them.
82
82
 
83
83
  ```sh
84
- cd my-project && lattices
84
+ cd my-project && lattices start
85
85
  ```
86
86
 
87
- No config? It reads your `package.json` and picks the right dev command
88
- automatically.
87
+ No config? It opens a shell in the project and, when it can, starts your
88
+ detected dev command in a second pane.
89
89
 
90
90
  ### Configuration
91
91
 
@@ -95,7 +95,7 @@ Drop a `.lattices.json` in your project root:
95
95
  {
96
96
  "ensure": true,
97
97
  "panes": [
98
- { "name": "claude", "cmd": "claude", "size": 60 },
98
+ { "name": "shell", "size": 60 },
99
99
  { "name": "server", "cmd": "pnpm dev" },
100
100
  { "name": "tests", "cmd": "pnpm test --watch" }
101
101
  ]
@@ -108,7 +108,7 @@ Drop a `.lattices.json` in your project root:
108
108
  2 panes 3+ panes
109
109
 
110
110
  ┌──────────┬───────┐ ┌──────────┬───────┐
111
- claude │server │ │ claude │server │
111
+ shell │server │ │ shell │server │
112
112
  │ (60%) │(40%) │ │ (60%) ├───────┤
113
113
  └──────────┴───────┘ │ │tests │
114
114
  └──────────┴───────┘
@@ -193,7 +193,9 @@ desktop the same way you do.
193
193
  ## CLI
194
194
 
195
195
  ```
196
- lattices Create or reattach to session
196
+ lattices Show workspace status and common commands
197
+ lattices start Create or reattach to current project session
198
+ lattices tmux Alias for lattices start
197
199
  lattices init Generate .lattices.json
198
200
  lattices ls List active sessions
199
201
  lattices kill [name] Kill a session
package/app/Info.plist CHANGED
@@ -14,10 +14,21 @@
14
14
  <string>AppIcon</string>
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>APPL</string>
17
+ <key>CFBundleURLTypes</key>
18
+ <array>
19
+ <dict>
20
+ <key>CFBundleURLName</key>
21
+ <string>com.arach.lattices</string>
22
+ <key>CFBundleURLSchemes</key>
23
+ <array>
24
+ <string>lattices</string>
25
+ </array>
26
+ </dict>
27
+ </array>
17
28
  <key>CFBundleVersion</key>
18
- <string>0.4.7</string>
29
+ <string>0.4.8</string>
19
30
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.7</string>
31
+ <string>0.4.8</string>
21
32
  <key>LSMinimumSystemVersion</key>
22
33
  <string>13.0</string>
23
34
  <key>LSUIElement</key>
@@ -14,10 +14,21 @@
14
14
  <string>AppIcon</string>
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>APPL</string>
17
+ <key>CFBundleURLTypes</key>
18
+ <array>
19
+ <dict>
20
+ <key>CFBundleURLName</key>
21
+ <string>com.arach.lattices</string>
22
+ <key>CFBundleURLSchemes</key>
23
+ <array>
24
+ <string>lattices</string>
25
+ </array>
26
+ </dict>
27
+ </array>
17
28
  <key>CFBundleVersion</key>
18
- <string>0.4.7</string>
29
+ <string>0.4.9</string>
19
30
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.7</string>
31
+ <string>0.4.9</string>
21
32
  <key>LSMinimumSystemVersion</key>
22
33
  <string>13.0</string>
23
34
  <key>LSUIElement</key>
@@ -5,7 +5,13 @@ struct LatticesApp: App {
5
5
  @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
6
6
 
7
7
  var body: some Scene {
8
- Settings { EmptyView() }
8
+ Settings {
9
+ SettingsContentView(
10
+ prefs: Preferences.shared,
11
+ scanner: ProjectScanner.shared
12
+ )
13
+ .frame(width: 900, height: 640)
14
+ }
9
15
  .commands {
10
16
  CommandGroup(after: .appInfo) {
11
17
  Button("Update Lattices…") {
@@ -1,4 +1,5 @@
1
1
  import AppKit
2
+ import Carbon
2
3
  import SwiftUI
3
4
 
4
5
  extension Notification.Name {
@@ -69,6 +70,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
69
70
  Self.shared = self
70
71
  NSApp.setActivationPolicy(.accessory)
71
72
  NSApp.appearance = NSAppearance(named: .darkAqua)
73
+ registerDeepLinkHandler()
72
74
 
73
75
  // --- Status item ---
74
76
  statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
@@ -124,6 +126,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
124
126
  store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
125
127
  WindowDragSnapController.shared.start()
126
128
  MouseGestureController.shared.start()
129
+ KeyboardRemapController.shared.start()
127
130
 
128
131
  // Session layer cycling
129
132
  store.register(action: .layerNext) { SessionLayerStore.shared.cycleNext() }
@@ -183,10 +186,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
183
186
  ProcessModel.shared.start()
184
187
  LatticesApi.setup()
185
188
  DaemonServer.shared.start()
186
- LatticesCompanionBridgeServer.shared.start()
189
+ if Preferences.shared.companionBridgeEnabled {
190
+ LatticesCompanionBridgeServer.shared.start()
191
+ } else {
192
+ diag.info("CompanionBridge: disabled by preference")
193
+ }
187
194
  AgentPool.shared.start()
188
195
  diag.finish(tBoot)
189
196
 
197
+ Task {
198
+ await AppUpdater.shared.checkIfNeeded()
199
+ }
200
+
190
201
  // --diagnostics flag: auto-open diagnostics panel on launch
191
202
  if CommandLine.arguments.contains("--diagnostics") {
192
203
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -203,6 +214,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
203
214
  }
204
215
 
205
216
  func applicationWillTerminate(_ notification: Notification) {
217
+ KeyboardRemapController.shared.stop()
206
218
  LatticesCompanionBridgeServer.shared.stop()
207
219
  DaemonServer.shared.stop()
208
220
  }
@@ -333,6 +345,57 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
333
345
  @objc private func menuSettings() { SettingsWindowController.shared.show() }
334
346
  @objc private func menuQuit() { NSApp.terminate(nil) }
335
347
 
348
+ // MARK: - Deep Links
349
+
350
+ private func registerDeepLinkHandler() {
351
+ NSAppleEventManager.shared().setEventHandler(
352
+ self,
353
+ andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)),
354
+ forEventClass: AEEventClass(kInternetEventClass),
355
+ andEventID: AEEventID(kAEGetURL)
356
+ )
357
+ }
358
+
359
+ @objc private func handleGetURLEvent(
360
+ _ event: NSAppleEventDescriptor,
361
+ withReplyEvent replyEvent: NSAppleEventDescriptor
362
+ ) {
363
+ guard
364
+ let value = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
365
+ let url = URL(string: value)
366
+ else {
367
+ return
368
+ }
369
+ handleDeepLink(url)
370
+ }
371
+
372
+ private func handleDeepLink(_ url: URL) {
373
+ guard url.scheme?.localizedCaseInsensitiveCompare("lattices") == .orderedSame else {
374
+ return
375
+ }
376
+
377
+ let host = url.host?.lowercased()
378
+ let action = url.pathComponents
379
+ .first { $0 != "/" }?
380
+ .lowercased()
381
+
382
+ guard host == "companion" else {
383
+ SettingsWindowController.shared.show()
384
+ return
385
+ }
386
+
387
+ switch action {
388
+ case "enable", "start":
389
+ Preferences.shared.companionBridgeEnabled = true
390
+ SettingsWindowController.shared.showCompanion()
391
+ case "disable", "stop":
392
+ Preferences.shared.companionBridgeEnabled = false
393
+ SettingsWindowController.shared.showCompanion()
394
+ default:
395
+ SettingsWindowController.shared.showCompanion()
396
+ }
397
+ }
398
+
336
399
  private static func showWorkspaceInspector() {
337
400
  guard let entry = DesktopModel.shared.frontmostWindow(),
338
401
  entry.app != "Lattices" else {
@@ -8,6 +8,7 @@ enum AppPage: String, CaseIterable {
8
8
  case desktopInventory
9
9
  case pi
10
10
  case settings
11
+ case companionSettings
11
12
  case docs
12
13
 
13
14
  var label: String {
@@ -17,6 +18,7 @@ enum AppPage: String, CaseIterable {
17
18
  case .desktopInventory: return "Desktop Inventory"
18
19
  case .pi: return "Pi"
19
20
  case .settings: return "Settings"
21
+ case .companionSettings:return "Settings"
20
22
  case .docs: return "Docs"
21
23
  }
22
24
  }
@@ -28,6 +30,7 @@ enum AppPage: String, CaseIterable {
28
30
  case .desktopInventory: return "macwindow.on.rectangle"
29
31
  case .pi: return "terminal"
30
32
  case .settings: return "gearshape"
33
+ case .companionSettings:return "ipad.and.iphone"
31
34
  case .docs: return "book"
32
35
  }
33
36
  }
@@ -129,6 +132,13 @@ struct AppShellView: View {
129
132
  scanner: ProjectScanner.shared,
130
133
  onBack: { windowController.activePage = .screenMap; controller.enter() }
131
134
  )
135
+ case .companionSettings:
136
+ SettingsContentView(
137
+ page: .companionSettings,
138
+ prefs: Preferences.shared,
139
+ scanner: ProjectScanner.shared,
140
+ onBack: { windowController.activePage = .screenMap; controller.enter() }
141
+ )
132
142
  case .docs:
133
143
  SettingsContentView(
134
144
  page: .docs,
@@ -1,6 +1,15 @@
1
1
  import AppKit
2
2
  import Combine
3
3
  import Foundation
4
+ import SwiftUI
5
+
6
+ struct LatticesUpdateInfo: Equatable {
7
+ let version: String
8
+ let downloadURL: URL
9
+ let releaseNotes: String
10
+ let publishedAt: Date
11
+ let htmlURL: URL
12
+ }
4
13
 
5
14
  @MainActor
6
15
  final class AppUpdater: ObservableObject {
@@ -8,6 +17,16 @@ final class AppUpdater: ObservableObject {
8
17
 
9
18
  @Published private(set) var isUpdating = false
10
19
  @Published private(set) var statusMessage: String?
20
+ @Published private(set) var availableUpdate: LatticesUpdateInfo?
21
+ @Published private(set) var isChecking = false
22
+ @Published private(set) var lastChecked: Date?
23
+ @Published private(set) var lastError: String?
24
+
25
+ @AppStorage("appUpdater.autoCheck") var autoCheckEnabled = true
26
+ @AppStorage("appUpdater.lastCheckTime") private var lastCheckTimeInterval: Double = 0
27
+ @AppStorage("appUpdater.skippedVersion") private var skippedVersion = ""
28
+
29
+ private let checkInterval: TimeInterval = 24 * 60 * 60
11
30
 
12
31
  private init() {}
13
32
 
@@ -27,6 +46,62 @@ final class AppUpdater: ObservableObject {
27
46
  return nil
28
47
  }
29
48
 
49
+ func checkIfNeeded() async {
50
+ guard autoCheckEnabled else { return }
51
+
52
+ let now = Date()
53
+ let lastCheck = Date(timeIntervalSince1970: lastCheckTimeInterval)
54
+ if now.timeIntervalSince(lastCheck) < checkInterval { return }
55
+
56
+ await check()
57
+ }
58
+
59
+ func check() async {
60
+ guard !isChecking else { return }
61
+
62
+ isChecking = true
63
+ lastError = nil
64
+
65
+ defer {
66
+ isChecking = false
67
+ lastChecked = Date()
68
+ lastCheckTimeInterval = Date().timeIntervalSince1970
69
+ }
70
+
71
+ do {
72
+ let release = try await fetchLatestRelease()
73
+ guard let update = parseRelease(release), isNewerVersion(update.version) else {
74
+ availableUpdate = nil
75
+ return
76
+ }
77
+
78
+ if update.version == skippedVersion {
79
+ availableUpdate = nil
80
+ return
81
+ }
82
+
83
+ availableUpdate = update
84
+ } catch UpdateCheckError.noRelease {
85
+ availableUpdate = nil
86
+ } catch {
87
+ lastError = error.localizedDescription
88
+ }
89
+ }
90
+
91
+ func skipCurrentUpdate() {
92
+ guard let update = availableUpdate else { return }
93
+ skippedVersion = update.version
94
+ availableUpdate = nil
95
+ }
96
+
97
+ func viewCurrentRelease() {
98
+ if let update = availableUpdate {
99
+ NSWorkspace.shared.open(update.htmlURL)
100
+ } else if let url = URL(string: "https://github.com/arach/lattices/releases/latest") {
101
+ NSWorkspace.shared.open(url)
102
+ }
103
+ }
104
+
30
105
  func promptForUpdate() {
31
106
  guard canUpdate else {
32
107
  presentAlert(
@@ -36,11 +111,50 @@ final class AppUpdater: ObservableObject {
36
111
  return
37
112
  }
38
113
 
114
+ guard availableUpdate != nil else {
115
+ Task {
116
+ await check()
117
+ if availableUpdate != nil {
118
+ presentUpdateConfirmation()
119
+ } else if let error = lastError {
120
+ presentAlert(
121
+ title: "Could Not Check for Updates",
122
+ message: error
123
+ )
124
+ } else {
125
+ presentAlert(
126
+ title: "Lattices Is Up to Date",
127
+ message: "You’re running \(currentVersion), which is the latest published release."
128
+ )
129
+ }
130
+ }
131
+ return
132
+ }
133
+
134
+ presentUpdateConfirmation()
135
+ }
136
+
137
+ private func presentUpdateConfirmation() {
39
138
  let alert = NSAlert()
40
139
  alert.alertStyle = .informational
41
- alert.messageText = "Install the latest Lattices app update?"
42
- alert.informativeText = "Lattices will download the latest released app bundle, close, and relaunch when the update is ready."
43
- alert.addButton(withTitle: "Update")
140
+ if let update = availableUpdate {
141
+ alert.messageText = "Update Lattices?"
142
+ alert.informativeText = """
143
+ Current version: \(currentVersion)
144
+ New version: \(update.version)
145
+
146
+ Lattices will download the signed release, quit briefly, replace the app, and relaunch when the update is ready.
147
+ """
148
+ } else {
149
+ alert.messageText = "Check and update Lattices?"
150
+ alert.informativeText = """
151
+ Current version: \(currentVersion)
152
+ New version: latest published release
153
+
154
+ Lattices will download the signed release, quit briefly, replace the app, and relaunch when the update is ready.
155
+ """
156
+ }
157
+ alert.addButton(withTitle: availableUpdate == nil ? "Check & Update" : "Update")
44
158
  alert.addButton(withTitle: "Cancel")
45
159
 
46
160
  guard alert.runModal() == .alertFirstButtonReturn else { return }
@@ -72,7 +186,11 @@ final class AppUpdater: ObservableObject {
72
186
  do {
73
187
  try proc.run()
74
188
  isUpdating = true
75
- statusMessage = "Updating to the latest release. Lattices will relaunch when it's ready."
189
+ if let update = availableUpdate {
190
+ statusMessage = "Preparing Lattices \(update.version). The app will relaunch when the update is ready."
191
+ } else {
192
+ statusMessage = "Preparing the latest Lattices release. The app will relaunch when the update is ready."
193
+ }
76
194
  } catch {
77
195
  presentAlert(
78
196
  title: "Update Failed",
@@ -89,4 +207,98 @@ final class AppUpdater: ObservableObject {
89
207
  alert.addButton(withTitle: "OK")
90
208
  alert.runModal()
91
209
  }
210
+
211
+ private func fetchLatestRelease() async throws -> GitHubRelease {
212
+ let url = URL(string: "https://api.github.com/repos/arach/lattices/releases/latest")!
213
+ var request = URLRequest(url: url)
214
+ request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
215
+ request.setValue("Lattices/\(currentVersion)", forHTTPHeaderField: "User-Agent")
216
+
217
+ let (data, response) = try await URLSession.shared.data(for: request)
218
+ guard let http = response as? HTTPURLResponse else {
219
+ throw UpdateCheckError.invalidResponse
220
+ }
221
+
222
+ switch http.statusCode {
223
+ case 200:
224
+ let decoder = JSONDecoder()
225
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
226
+ decoder.dateDecodingStrategy = .iso8601
227
+ return try decoder.decode(GitHubRelease.self, from: data)
228
+ case 404:
229
+ throw UpdateCheckError.noRelease
230
+ case 403:
231
+ throw UpdateCheckError.rateLimited
232
+ default:
233
+ throw UpdateCheckError.httpError(http.statusCode)
234
+ }
235
+ }
236
+
237
+ private func parseRelease(_ release: GitHubRelease) -> LatticesUpdateInfo? {
238
+ guard !release.draft, !release.prerelease else { return nil }
239
+
240
+ let asset = release.assets.first { asset in
241
+ asset.name == "Lattices.dmg" ||
242
+ (asset.name.hasPrefix("Lattices") && asset.name.hasSuffix(".dmg"))
243
+ }
244
+
245
+ guard let asset,
246
+ let downloadURL = URL(string: asset.browserDownloadUrl),
247
+ let htmlURL = URL(string: release.htmlUrl) else {
248
+ return nil
249
+ }
250
+
251
+ let version = release.tagName.hasPrefix("v")
252
+ ? String(release.tagName.dropFirst())
253
+ : release.tagName
254
+
255
+ return LatticesUpdateInfo(
256
+ version: version,
257
+ downloadURL: downloadURL,
258
+ releaseNotes: release.body ?? "",
259
+ publishedAt: release.publishedAt,
260
+ htmlURL: htmlURL
261
+ )
262
+ }
263
+
264
+ private func isNewerVersion(_ remoteVersion: String) -> Bool {
265
+ guard currentVersion != "unknown" else { return false }
266
+ return remoteVersion.compare(currentVersion, options: .numeric) == .orderedDescending
267
+ }
268
+ }
269
+
270
+ private struct GitHubRelease: Decodable {
271
+ let tagName: String
272
+ let name: String
273
+ let body: String?
274
+ let htmlUrl: String
275
+ let publishedAt: Date
276
+ let assets: [GitHubAsset]
277
+ let prerelease: Bool
278
+ let draft: Bool
279
+ }
280
+
281
+ private struct GitHubAsset: Decodable {
282
+ let name: String
283
+ let browserDownloadUrl: String
284
+ }
285
+
286
+ private enum UpdateCheckError: LocalizedError {
287
+ case invalidResponse
288
+ case noRelease
289
+ case rateLimited
290
+ case httpError(Int)
291
+
292
+ var errorDescription: String? {
293
+ switch self {
294
+ case .invalidResponse:
295
+ return "Invalid response from GitHub."
296
+ case .noRelease:
297
+ return "No published release found."
298
+ case .rateLimited:
299
+ return "GitHub rate limited the update check."
300
+ case .httpError(let code):
301
+ return "GitHub returned HTTP \(code)."
302
+ }
303
+ }
92
304
  }
@@ -24,7 +24,7 @@ enum CliActionLauncher {
24
24
  ) else { return }
25
25
 
26
26
  Preferences.shared.terminal.launch(
27
- command: "lattices init && lattices",
27
+ command: "lattices init && lattices start",
28
28
  in: directory
29
29
  )
30
30
  }
@@ -36,7 +36,7 @@ enum CliActionLauncher {
36
36
  ) else { return }
37
37
 
38
38
  Preferences.shared.terminal.launch(
39
- command: "lattices",
39
+ command: "lattices start",
40
40
  in: directory
41
41
  )
42
42
  }
@@ -490,7 +490,7 @@ struct MainView: View {
490
490
  .buttonStyle(.plain)
491
491
  }
492
492
 
493
- Text("Initialize runs lattices init && lattices in the folder you choose.")
493
+ Text("Initialize runs lattices init && lattices start in the folder you choose.")
494
494
  .font(Typo.mono(9))
495
495
  .foregroundColor(Palette.textMuted)
496
496
  .multilineTextAlignment(.center)
@@ -10,6 +10,7 @@ class Preferences: ObservableObject {
10
10
  static let shared = Preferences()
11
11
 
12
12
  private enum CompanionDefaultsKey {
13
+ static let bridgeEnabled = "companion.bridge.enabled"
13
14
  static let trackpadEnabled = "companion.trackpad.enabled"
14
15
  static let cockpitLayout = "companion.cockpit.layout"
15
16
  }
@@ -30,6 +31,17 @@ class Preferences: ObservableObject {
30
31
  didSet { UserDefaults.standard.set(dragSnapEnabled, forKey: "windowSnap.enabled") }
31
32
  }
32
33
 
34
+ @Published var companionBridgeEnabled: Bool {
35
+ didSet {
36
+ UserDefaults.standard.set(companionBridgeEnabled, forKey: CompanionDefaultsKey.bridgeEnabled)
37
+ if companionBridgeEnabled {
38
+ LatticesCompanionBridgeServer.shared.start()
39
+ } else {
40
+ LatticesCompanionBridgeServer.shared.stop()
41
+ }
42
+ }
43
+ }
44
+
33
45
  @Published var companionTrackpadEnabled: Bool {
34
46
  didSet { UserDefaults.standard.set(companionTrackpadEnabled, forKey: CompanionDefaultsKey.trackpadEnabled) }
35
47
  }
@@ -41,6 +53,10 @@ class Preferences: ObservableObject {
41
53
  didSet { UserDefaults.standard.set(mouseGesturesEnabled, forKey: "mouseGestures.enabled") }
42
54
  }
43
55
 
56
+ @Published var keyboardRemapsEnabled: Bool {
57
+ didSet { UserDefaults.standard.set(keyboardRemapsEnabled, forKey: "keyboardRemaps.enabled") }
58
+ }
59
+
44
60
  // MARK: - AI / Claude
45
61
 
46
62
  @Published var claudePath: String {
@@ -155,10 +171,16 @@ class Preferences: ObservableObject {
155
171
  self.dragSnapEnabled = true
156
172
  }
157
173
 
174
+ if UserDefaults.standard.object(forKey: CompanionDefaultsKey.bridgeEnabled) != nil {
175
+ self.companionBridgeEnabled = UserDefaults.standard.bool(forKey: CompanionDefaultsKey.bridgeEnabled)
176
+ } else {
177
+ self.companionBridgeEnabled = false
178
+ }
179
+
158
180
  if UserDefaults.standard.object(forKey: CompanionDefaultsKey.trackpadEnabled) != nil {
159
181
  self.companionTrackpadEnabled = UserDefaults.standard.bool(forKey: CompanionDefaultsKey.trackpadEnabled)
160
182
  } else {
161
- self.companionTrackpadEnabled = true
183
+ self.companionTrackpadEnabled = false
162
184
  }
163
185
 
164
186
  self.companionCockpitLayout = Self.loadCompanionCockpitLayout()
@@ -167,6 +189,12 @@ class Preferences: ObservableObject {
167
189
  } else {
168
190
  self.mouseGesturesEnabled = false
169
191
  }
192
+
193
+ if UserDefaults.standard.object(forKey: "keyboardRemaps.enabled") != nil {
194
+ self.keyboardRemapsEnabled = UserDefaults.standard.bool(forKey: "keyboardRemaps.enabled")
195
+ } else {
196
+ self.keyboardRemapsEnabled = true
197
+ }
170
198
  // AI / Claude
171
199
  self.claudePath = UserDefaults.standard.string(forKey: "claude.path") ?? ""
172
200
  self.advisorModel = UserDefaults.standard.string(forKey: "claude.advisorModel") ?? "haiku"