@lattices/cli 0.4.6 → 0.4.7

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/app/Info.plist CHANGED
@@ -15,9 +15,9 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>APPL</string>
17
17
  <key>CFBundleVersion</key>
18
- <string>0.4.6</string>
18
+ <string>0.4.7</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.6</string>
20
+ <string>0.4.7</string>
21
21
  <key>LSMinimumSystemVersion</key>
22
22
  <string>13.0</string>
23
23
  <key>LSUIElement</key>
@@ -15,9 +15,9 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>APPL</string>
17
17
  <key>CFBundleVersion</key>
18
- <string>0.4.6</string>
18
+ <string>0.4.7</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.6</string>
20
+ <string>0.4.7</string>
21
21
  <key>LSMinimumSystemVersion</key>
22
22
  <string>13.0</string>
23
23
  <key>LSUIElement</key>
@@ -162,6 +162,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
162
162
  }
163
163
  store.register(action: .tileDistribute) { WindowTiler.distributeVisible(reactivateLattices: false) }
164
164
  store.register(action: .tileTypeGrid) { WindowTiler.distributeVisibleByFrontmostType(reactivateLattices: false) }
165
+ store.register(action: .tileOrganize) {
166
+ let appName = DesktopModel.shared.frontmostWindow()?.app
167
+ ?? NSWorkspace.shared.frontmostApplication?.localizedName
168
+ CommandModeWindow.shared.show(launchMode: .organize(appName: appName))
169
+ }
165
170
 
166
171
  // Onboarding on first launch; otherwise just check permissions
167
172
  if !OnboardingWindowController.shared.showIfNeeded() {
@@ -34,7 +34,7 @@ struct KeyRecorderView: View {
34
34
  .frame(minWidth: 80, alignment: .leading)
35
35
  } else if let binding = binding {
36
36
  HStack(spacing: 4) {
37
- ForEach(binding.displayParts, id: \.self) { part in
37
+ ForEach(binding.compactDisplayParts, id: \.self) { part in
38
38
  keyBadge(part)
39
39
  }
40
40
  }
@@ -37,7 +37,6 @@ class Preferences: ObservableObject {
37
37
  @Published var companionCockpitLayout: LatticesCompanionCockpitLayout {
38
38
  didSet { persistCompanionCockpitLayout() }
39
39
  }
40
-
41
40
  @Published var mouseGesturesEnabled: Bool {
42
41
  didSet { UserDefaults.standard.set(mouseGesturesEnabled, forKey: "mouseGestures.enabled") }
43
42
  }
@@ -163,7 +162,6 @@ class Preferences: ObservableObject {
163
162
  }
164
163
 
165
164
  self.companionCockpitLayout = Self.loadCompanionCockpitLayout()
166
-
167
165
  if UserDefaults.standard.object(forKey: "mouseGestures.enabled") != nil {
168
166
  self.mouseGesturesEnabled = UserDefaults.standard.bool(forKey: "mouseGestures.enabled")
169
167
  } else {
@@ -57,6 +57,7 @@ struct SettingsContentView: View {
57
57
  @ObservedObject var prefs: Preferences
58
58
  @ObservedObject var scanner: ProjectScanner
59
59
  @ObservedObject var hotkeyStore: HotkeyStore = .shared
60
+ @ObservedObject var workspaceManager: WorkspaceManager = .shared
60
61
  @ObservedObject var appUpdater: AppUpdater = .shared
61
62
  @ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
62
63
  var onBack: (() -> Void)? = nil
@@ -85,6 +86,13 @@ struct SettingsContentView: View {
85
86
  page == .docs ? "Docs" : selectedTab.title
86
87
  }
87
88
 
89
+ private var snapModifierBinding: Binding<SnapModifierKey> {
90
+ Binding(
91
+ get: { workspaceManager.snapZonesConfig.modifier ?? .command },
92
+ set: { workspaceManager.updateSnapModifier($0) }
93
+ )
94
+ }
95
+
88
96
  private var backBar: some View {
89
97
  VStack(spacing: 0) {
90
98
  HStack(spacing: 8) {
@@ -459,13 +467,28 @@ struct SettingsContentView: View {
459
467
  .labelsHidden()
460
468
  }
461
469
 
462
- Text("Hold the configured snap modifier while dragging to reveal landing targets and a live preview, then release it to go back to a free drag. Default: Command.")
470
+ HStack {
471
+ Text("Snap modifier")
472
+ .font(Typo.mono(10))
473
+ .foregroundColor(Palette.textDim)
474
+ Spacer()
475
+ Picker("", selection: snapModifierBinding) {
476
+ ForEach(SnapModifierKey.allCases) { modifier in
477
+ Text(modifier.shortLabel).tag(modifier)
478
+ }
479
+ }
480
+ .pickerStyle(.segmented)
481
+ .labelsHidden()
482
+ .frame(width: 220)
483
+ }
484
+
485
+ 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
486
  .font(Typo.caption(9))
464
487
  .foregroundColor(Palette.textMuted.opacity(0.7))
465
488
 
466
489
  cardDivider
467
490
 
468
- Text("Agent-editable rules live in ~/.lattices/snap-zones.json. Changes are picked up on the next drag.")
491
+ Text("Advanced landing-zone rules still live in ~/.lattices/snap-zones.json. Modifier changes here take effect on the next drag.")
469
492
  .font(Typo.caption(9))
470
493
  .foregroundColor(Palette.textMuted.opacity(0.7))
471
494
  }
@@ -1541,6 +1564,8 @@ struct SettingsContentView: View {
1541
1564
  .foregroundColor(Palette.textMuted)
1542
1565
  .fixedSize(horizontal: false, vertical: true)
1543
1566
  }
1567
+
1568
+ compactKeyRecorder(action: .tileOrganize)
1544
1569
  }
1545
1570
  }
1546
1571
 
@@ -31,7 +31,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
31
31
  // Tiling
32
32
  case tileLeft, tileRight, tileMaximize, tileCenter
33
33
  case tileTopLeft, tileTopRight, tileBottomLeft, tileBottomRight
34
- case tileTop, tileBottom, tileDistribute, tileTypeGrid
34
+ case tileTop, tileBottom, tileDistribute, tileTypeGrid, tileOrganize
35
35
  case tileLeftThird, tileCenterThird, tileRightThird
36
36
 
37
37
  var label: String {
@@ -71,6 +71,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
71
71
  case .tileBottom: return "Bottom Half"
72
72
  case .tileDistribute: return "Distribute"
73
73
  case .tileTypeGrid: return "Grid Type"
74
+ case .tileOrganize: return "Organize Windows"
74
75
  case .tileLeftThird: return "Left Third"
75
76
  case .tileCenterThird: return "Center Third"
76
77
  case .tileRightThird: return "Right Third"
@@ -127,6 +128,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
127
128
  case .tileLeftThird: return 311
128
129
  case .tileCenterThird: return 312
129
130
  case .tileRightThird: return 313
131
+ case .tileOrganize: return 315
130
132
  }
131
133
  }
132
134
 
@@ -142,6 +144,15 @@ struct KeyBinding: Codable, Equatable {
142
144
  let carbonModifiers: UInt32
143
145
  var displayParts: [String]
144
146
 
147
+ var compactDisplayParts: [String] {
148
+ guard let key = displayParts.last else { return [] }
149
+ let modifiers = Set(displayParts.dropLast())
150
+ if modifiers == Set(["Ctrl", "Option", "Shift", "Cmd"]) {
151
+ return ["Hyper", key]
152
+ }
153
+ return displayParts
154
+ }
155
+
145
156
  static func carbonModifiers(from flags: NSEvent.ModifierFlags) -> UInt32 {
146
157
  var mods: UInt32 = 0
147
158
  if flags.contains(.command) { mods |= UInt32(cmdKey) }
@@ -266,6 +277,7 @@ class HotkeyStore: ObservableObject {
266
277
  bind(.tileLeftThird, 18, ctrlOpt) // Ctrl+Opt+1
267
278
  bind(.tileCenterThird, 19, ctrlOpt) // Ctrl+Opt+2
268
279
  bind(.tileRightThird, 20, ctrlOpt) // Ctrl+Opt+3
280
+ bind(.tileOrganize, 31, ctrlOpt) // Ctrl+Opt+O
269
281
 
270
282
  return d
271
283
  }()
@@ -2118,7 +2118,6 @@ enum WindowTiler {
2118
2118
  }
2119
2119
  return nil
2120
2120
  }
2121
-
2122
2121
  private static func displaySpaces(containing cgPoint: CGPoint) -> DisplaySpaces? {
2123
2122
  guard let screenIndex = screenIndex(for: cgPoint) else { return nil }
2124
2123
  return getDisplaySpaces().first(where: { $0.displayIndex == screenIndex })
@@ -2134,7 +2133,6 @@ enum WindowTiler {
2134
2133
  private static func formatCGPoint(_ point: CGPoint) -> String {
2135
2134
  "\(Int(point.x)),\(Int(point.y))"
2136
2135
  }
2137
-
2138
2136
  private static func screenIndex(for cgPoint: CGPoint) -> Int? {
2139
2137
  let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
2140
2138
  let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
@@ -45,6 +45,11 @@ enum DesktopInventoryMode: Equatable {
45
45
  case screenMap // m → interactive screen map editor
46
46
  }
47
47
 
48
+ enum CommandModeLaunchMode: Equatable {
49
+ case normal
50
+ case organize(appName: String?)
51
+ }
52
+
48
53
  // DisplayGeometry, ScreenMapWindowEntry, ScreenMapEditorState, ScreenMapActionLog
49
54
  // are defined in ScreenMapState.swift
50
55
  // MARK: - Filter Presets
@@ -132,10 +137,43 @@ final class CommandModeState: ObservableObject {
132
137
 
133
138
  var onDismiss: (() -> Void)?
134
139
  var onPanelResize: ((_ width: CGFloat, _ height: CGFloat) -> Void)?
140
+ private let launchMode: CommandModeLaunchMode
135
141
 
136
142
  /// Tracks the last item navigated to, for consistent Shift+arrow multi-select
137
143
  private var cursorWindowId: UInt32?
138
144
 
145
+ init(launchMode: CommandModeLaunchMode = .normal) {
146
+ self.launchMode = launchMode
147
+ }
148
+
149
+ var isOrganizeFlow: Bool {
150
+ if case .organize = launchMode { return true }
151
+ return false
152
+ }
153
+
154
+ var organizeSeedAppName: String? {
155
+ if case .organize(let appName) = launchMode { return appName }
156
+ return nil
157
+ }
158
+
159
+ var organizeSelectionSummary: String {
160
+ let count = selectedWindowIds.count
161
+ if let appName = organizeSeedAppName, !appName.isEmpty {
162
+ return "\(count) \(appName) window\(count == 1 ? "" : "s") selected"
163
+ }
164
+ return "\(count) window\(count == 1 ? "" : "s") selected"
165
+ }
166
+
167
+ var organizeGuidance: String {
168
+ if selectedWindowIds.count > 1 {
169
+ return "Press D to organize. Cmd-click adds or removes windows. Shift-click extends the selection."
170
+ }
171
+ if selectedWindowIds.count == 1 {
172
+ return "Cmd-click another window to add it, then press D to organize the set."
173
+ }
174
+ return "Click windows to select them. Cmd-click adds or removes windows, and D organizes the selection."
175
+ }
176
+
139
177
  // MARK: - Selection Helpers
140
178
 
141
179
  /// Backwards-compat: returns single selected ID (first element)
@@ -369,6 +407,7 @@ final class CommandModeState: ObservableObject {
369
407
  desktopMode = .browsing
370
408
  gridPreviewPlacement = nil
371
409
  phase = .desktopInventory
410
+ configureLaunchMode()
372
411
  // Don't call onPanelResize here — caller handles initial sizing
373
412
  }
374
413
 
@@ -576,6 +615,20 @@ final class CommandModeState: ObservableObject {
576
615
  }
577
616
  return true
578
617
 
618
+ case 2: // d → distribute selected
619
+ if isSearching && selectedWindowIds.isEmpty { return false }
620
+ if isSearching { deactivateSearch() }
621
+ guard !selectedWindowIds.isEmpty else {
622
+ flash("Select 2+ windows to organize")
623
+ return true
624
+ }
625
+ guard selectedWindowIds.count > 1 else {
626
+ flash("Add another window, then press D to organize")
627
+ return true
628
+ }
629
+ distributeSelected()
630
+ return true
631
+
579
632
  case 46: // m → screen map editor (standalone window)
580
633
  if isSearching { deactivateSearch() }
581
634
  ScreenMapWindowController.shared.show()
@@ -1050,6 +1103,54 @@ final class CommandModeState: ObservableObject {
1050
1103
  }
1051
1104
  }
1052
1105
 
1106
+ private func configureLaunchMode() {
1107
+ switch launchMode {
1108
+ case .normal:
1109
+ return
1110
+ case .organize(let appName):
1111
+ activePreset = .currentSpace
1112
+ seedSelectionForOrganization(appName: appName)
1113
+ }
1114
+ }
1115
+
1116
+ private func seedSelectionForOrganization(appName: String?) {
1117
+ let visibleWindows = flatWindowList.filter(\.isOnScreen)
1118
+ let targetApp = appName ?? visibleWindows.first?.appName
1119
+ let initialSelection = visibleWindows.filter { window in
1120
+ guard let name = window.appName, let targetApp else { return false }
1121
+ return name.localizedCaseInsensitiveCompare(targetApp) == .orderedSame
1122
+ }
1123
+
1124
+ if initialSelection.isEmpty {
1125
+ flash("Select windows to organize. Cmd-click adds or removes windows; D distributes.")
1126
+ return
1127
+ }
1128
+
1129
+ selectedWindowIds = Set(initialSelection.map(\.id))
1130
+ cursorWindowId = initialSelection.first?.id
1131
+
1132
+ if let targetApp {
1133
+ if initialSelection.count > 1 {
1134
+ flash("Selected \(initialSelection.count) \(targetApp) windows. Press D to organize.")
1135
+ } else {
1136
+ flash("Selected the \(targetApp) window. Cmd-click more windows, then press D.")
1137
+ }
1138
+ } else if initialSelection.count > 1 {
1139
+ flash("Selected \(initialSelection.count) windows. Press D to organize.")
1140
+ } else {
1141
+ flash("Selected 1 window. Cmd-click more windows, then press D.")
1142
+ }
1143
+
1144
+ DispatchQueue.main.async { [weak self] in
1145
+ guard let self = self else { return }
1146
+ if self.selectedWindowIds.count > 1 {
1147
+ self.highlightAllSelected()
1148
+ } else {
1149
+ self.highlightSelectedWindow()
1150
+ }
1151
+ }
1152
+ }
1153
+
1053
1154
  // MARK: - Inventory Builder
1054
1155
 
1055
1156
  private func buildInventory() -> CommandModeInventory {
@@ -200,10 +200,17 @@ struct CommandModeView: View {
200
200
 
201
201
  private var header: some View {
202
202
  HStack {
203
- Text(isDesktopInventory ? "DESKTOP INVENTORY" : "COMMAND MODE")
203
+ Text(isDesktopInventory ? (state.isOrganizeFlow ? "ORGANIZE WINDOWS" : "DESKTOP INVENTORY") : "COMMAND MODE")
204
204
  .font(Typo.monoBold(11))
205
205
  .foregroundColor(Palette.text)
206
206
 
207
+ if isDesktopInventory && state.isOrganizeFlow {
208
+ bannerBadge("Current Space", tone: .neutral)
209
+ if let appName = state.organizeSeedAppName, !appName.isEmpty {
210
+ bannerBadge(appName, tone: .accent)
211
+ }
212
+ }
213
+
207
214
  if isDesktopInventory {
208
215
  Button(action: { state.copyInventoryToClipboard() }) {
209
216
  HStack(spacing: 3) {
@@ -275,6 +282,11 @@ struct CommandModeView: View {
275
282
 
276
283
  private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
277
284
  VStack(spacing: 0) {
285
+ if state.isOrganizeFlow {
286
+ organizeBanner
287
+ divider
288
+ }
289
+
278
290
  if state.isSearching {
279
291
  searchBar
280
292
  } else {
@@ -358,6 +370,33 @@ struct CommandModeView: View {
358
370
  }
359
371
  }
360
372
 
373
+ private var organizeBanner: some View {
374
+ VStack(alignment: .leading, spacing: 6) {
375
+ HStack(alignment: .center, spacing: 8) {
376
+ Image(systemName: "rectangle.3.group")
377
+ .font(.system(size: 11, weight: .medium))
378
+ .foregroundColor(Palette.running)
379
+ Text(state.organizeSelectionSummary)
380
+ .font(Typo.monoBold(10))
381
+ .foregroundColor(Palette.text)
382
+ Spacer()
383
+ if state.selectedWindowIds.count > 1 {
384
+ bannerBadge("Ready", tone: .accent)
385
+ } else {
386
+ bannerBadge("Add More", tone: .neutral)
387
+ }
388
+ }
389
+
390
+ Text(state.organizeGuidance)
391
+ .font(Typo.mono(10))
392
+ .foregroundColor(Palette.textDim)
393
+ .lineLimit(2)
394
+ }
395
+ .padding(.horizontal, 14)
396
+ .padding(.vertical, 8)
397
+ .background(Palette.running.opacity(0.06))
398
+ }
399
+
361
400
  private var filterPillBar: some View {
362
401
  HStack(spacing: 6) {
363
402
  ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
@@ -835,9 +874,17 @@ struct CommandModeView: View {
835
874
  if indented {
836
875
  Spacer().frame(width: 8)
837
876
  }
838
- Text(isLattices ? "●" : "•")
839
- .font(.system(size: 7))
840
- .foregroundColor(isLattices ? Palette.running : (isSelected ? Palette.text : Palette.textDim))
877
+ Group {
878
+ if isSelected {
879
+ Image(systemName: "checkmark.circle.fill")
880
+ .font(.system(size: 10, weight: .semibold))
881
+ .foregroundColor(Palette.running)
882
+ } else {
883
+ Text(isLattices ? "●" : "•")
884
+ .font(.system(size: 7))
885
+ .foregroundColor(isLattices ? Palette.running : Palette.textDim)
886
+ }
887
+ }
841
888
  if let app = appLabel {
842
889
  Text(app)
843
890
  .font(Typo.monoBold(10))
@@ -1242,6 +1289,9 @@ struct CommandModeView: View {
1242
1289
  chordHint(key: "↩", label: "select & front")
1243
1290
  chordHint(key: "⌘A", label: "select all")
1244
1291
  chordHint(key: "⇧↑↓", label: "multi-select")
1292
+ if state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
1293
+ chordHint(key: "d", label: "organize")
1294
+ }
1245
1295
  if !state.selectedWindowIds.isEmpty {
1246
1296
  chordHint(key: "t", label: "tile")
1247
1297
  }
@@ -1279,9 +1329,42 @@ struct CommandModeView: View {
1279
1329
  .foregroundColor(Palette.running)
1280
1330
  }
1281
1331
  }
1332
+ } else if isDesktopInventory && state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
1333
+ HStack(spacing: 12) {
1334
+ chordHint(key: "d", label: "organize")
1335
+ chordHint(key: "⌘-click", label: "add/remove")
1336
+ chordHint(key: "⇧-click", label: "range")
1337
+ chordHint(key: "↩", label: "front")
1338
+ chordHint(key: "esc", label: "cancel")
1339
+ Spacer()
1340
+ Text("\(state.selectedWindowIds.count) selected")
1341
+ .font(Typo.mono(9))
1342
+ .foregroundColor(Palette.running)
1343
+ }
1344
+ } else if isDesktopInventory && state.isOrganizeFlow && !state.selectedWindowIds.isEmpty {
1345
+ HStack(spacing: 12) {
1346
+ chordHint(key: "⌘-click", label: "add more")
1347
+ chordHint(key: "d", label: "need 2+")
1348
+ chordHint(key: "↩", label: "front")
1349
+ chordHint(key: "esc", label: "cancel")
1350
+ Spacer()
1351
+ Text(state.organizeSelectionSummary)
1352
+ .font(Typo.mono(9))
1353
+ .foregroundColor(Palette.textDim)
1354
+ }
1355
+ } else if isDesktopInventory && state.isOrganizeFlow {
1356
+ HStack(spacing: 12) {
1357
+ chordHint(key: "click", label: "select")
1358
+ chordHint(key: "⌘-click", label: "add/remove")
1359
+ chordHint(key: "/", label: "search")
1360
+ chordHint(key: "esc", label: "cancel")
1361
+ Spacer()
1362
+ }
1282
1363
  } else if isDesktopInventory && state.selectedWindowIds.count > 1 {
1283
1364
  // Multi-selection active
1284
1365
  HStack(spacing: 12) {
1366
+ chordHint(key: "s", label: "grid preview")
1367
+ chordHint(key: "d", label: "distribute")
1285
1368
  chordHint(key: "s", label: "grid preview")
1286
1369
  chordHint(key: "↩", label: "front")
1287
1370
  chordHint(key: "t", label: "grid region")
@@ -1302,6 +1385,7 @@ struct CommandModeView: View {
1302
1385
  } else if isDesktopInventory && !state.selectedWindowIds.isEmpty {
1303
1386
  // Single selection active — browsing hints with direct shortcuts
1304
1387
  HStack(spacing: 12) {
1388
+ chordHint(key: "d", label: "organize")
1305
1389
  chordHint(key: "s", label: "show")
1306
1390
  chordHint(key: "↩", label: "front")
1307
1391
  chordHint(key: "f", label: "focus+close")
@@ -1367,6 +1451,31 @@ struct CommandModeView: View {
1367
1451
  }
1368
1452
  }
1369
1453
 
1454
+ private enum BannerTone {
1455
+ case neutral
1456
+ case accent
1457
+ }
1458
+
1459
+ private func bannerBadge(_ text: String, tone: BannerTone) -> some View {
1460
+ let foreground = tone == .accent ? Palette.running : Palette.textDim
1461
+ let fill = tone == .accent ? Palette.running.opacity(0.10) : Palette.surface
1462
+ let stroke = tone == .accent ? Palette.running.opacity(0.30) : Palette.border
1463
+
1464
+ return Text(text)
1465
+ .font(Typo.mono(8))
1466
+ .foregroundColor(foreground)
1467
+ .padding(.horizontal, 6)
1468
+ .padding(.vertical, 3)
1469
+ .background(
1470
+ RoundedRectangle(cornerRadius: 8)
1471
+ .fill(fill)
1472
+ .overlay(
1473
+ RoundedRectangle(cornerRadius: 8)
1474
+ .strokeBorder(stroke, lineWidth: 0.5)
1475
+ )
1476
+ )
1477
+ }
1478
+
1370
1479
  private func actionButton(key: String, label: String, action: @escaping () -> Void) -> some View {
1371
1480
  Button(action: action) {
1372
1481
  HStack(spacing: 4) {
@@ -43,15 +43,19 @@ final class CommandModeWindow {
43
43
 
44
44
  var isVisible: Bool { isOpen }
45
45
 
46
- func toggle() {
46
+ func toggle(launchMode: CommandModeLaunchMode = .normal) {
47
47
  if isOpen {
48
- dismiss()
48
+ if launchMode == .normal {
49
+ dismiss()
50
+ } else {
51
+ show(launchMode: launchMode)
52
+ }
49
53
  } else {
50
- show()
54
+ show(launchMode: launchMode)
51
55
  }
52
56
  }
53
57
 
54
- func show() {
58
+ func show(launchMode: CommandModeLaunchMode = .normal) {
55
59
  // Always rebuild for fresh state
56
60
  dismiss()
57
61
  isOpen = true
@@ -61,7 +65,7 @@ final class CommandModeWindow {
61
65
  CommandPaletteWindow.shared.dismiss()
62
66
  }
63
67
 
64
- let state = CommandModeState()
68
+ let state = CommandModeState(launchMode: launchMode)
65
69
  state.onDismiss = { [weak self] in
66
70
  self?.dismiss()
67
71
  }
@@ -255,6 +255,7 @@ struct CheatSheetView: View {
255
255
  shortcutRow(action: .tileCenter)
256
256
  shortcutRow(action: .tileDistribute)
257
257
  shortcutRow(action: .tileTypeGrid)
258
+ shortcutRow(action: .tileOrganize)
258
259
 
259
260
  // Hovered shortcut detail
260
261
  if let hovered = hoveredAction, let binding = hotkeyStore.bindings[hovered] {
@@ -2035,7 +2035,6 @@ final class ScreenMapController: ObservableObject {
2035
2035
 
2036
2036
  func handleKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
2037
2037
  let diag = DiagnosticLog.shared
2038
- diag.info("[ScreenMap] key: \(keyCode)")
2039
2038
 
2040
2039
  // Tiling mode intercepts keys before anything else
2041
2040
  if editor?.isTilingMode == true {
@@ -1805,6 +1805,7 @@ struct ScreenMapView: View {
1805
1805
  let displays = editor?.displays ?? []
1806
1806
  let zoomLevel = editor?.zoomLevel ?? 1.0
1807
1807
  let panOffset = editor?.panOffset ?? .zero
1808
+ let canvasShape = RoundedRectangle(cornerRadius: 6, style: .continuous)
1808
1809
 
1809
1810
  return GeometryReader { geo in
1810
1811
  let metrics = CanvasMetrics(editor: editor, displays: displays, viewportSize: geo.size)
@@ -1852,12 +1853,14 @@ struct ScreenMapView: View {
1852
1853
  }
1853
1854
  .padding(8)
1854
1855
  .frame(maxWidth: .infinity, maxHeight: .infinity)
1856
+ .contentShape(canvasShape)
1857
+ .clipShape(canvasShape)
1855
1858
  .clipped()
1856
1859
  .background(
1857
1860
  ZStack {
1858
- RoundedRectangle(cornerRadius: 6)
1861
+ canvasShape
1859
1862
  .fill(Color.black.opacity(0.25))
1860
- RoundedRectangle(cornerRadius: 6)
1863
+ canvasShape
1861
1864
  .strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
1862
1865
  Canvas { context, size in
1863
1866
  let spacing: CGFloat = 20
@@ -2742,11 +2745,14 @@ struct ScreenMapView: View {
2742
2745
  }
2743
2746
  // Track space key for canvas drag-to-pan
2744
2747
  if event.keyCode == 49 && !controller.isSearchActive {
2745
- if event.type == .keyDown && !event.isARepeat {
2746
- isSpaceHeld = true
2747
- NSCursor.openHand.push()
2748
+ if event.type == .keyDown {
2749
+ if !isSpaceHeld {
2750
+ isSpaceHeld = true
2751
+ NSCursor.openHand.push()
2752
+ }
2748
2753
  return nil
2749
2754
  } else if event.type == .keyUp {
2755
+ guard isSpaceHeld else { return nil }
2750
2756
  isSpaceHeld = false
2751
2757
  spaceDragStart = nil
2752
2758
  NSCursor.pop()
@@ -2788,16 +2794,18 @@ struct ScreenMapView: View {
2788
2794
  mouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { event in
2789
2795
  guard let eventWindow = event.window,
2790
2796
  eventWindow === ScreenMapWindowController.shared.nsWindow else { return event }
2797
+ let flippedPt = flippedScreenPoint(event)
2791
2798
 
2792
2799
  // Space+click → begin canvas pan
2793
- if isSpaceHeld, let editor = controller.editor {
2800
+ if isSpaceHeld,
2801
+ isCanvasPoint(flippedPt),
2802
+ let editor = controller.editor {
2794
2803
  spaceDragStart = event.locationInWindow
2795
2804
  spaceDragPanStart = editor.panOffset
2796
2805
  NSCursor.closedHand.push()
2797
2806
  return nil
2798
2807
  }
2799
2808
 
2800
- let flippedPt = flippedScreenPoint(event)
2801
2809
  if let editor = controller.editor,
2802
2810
  let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor),
2803
2811
  hoveredWindowId == hit.id {
@@ -3042,6 +3050,7 @@ struct ScreenMapView: View {
3042
3050
  // MARK: - Hit Test / Coordinate Conversion
3043
3051
 
3044
3052
  private func canvasHit(flippedScreenPt: CGPoint, editor: ScreenMapEditorState) -> CanvasHit? {
3053
+ guard isCanvasPoint(flippedScreenPt) else { return nil }
3045
3054
  let projection = CanvasProjection(editor: editor)
3046
3055
  guard projection.scale > 0 else { return nil }
3047
3056
  let canvasLocal = CGPoint(
@@ -3059,6 +3068,10 @@ struct ScreenMapView: View {
3059
3068
  return nil
3060
3069
  }
3061
3070
 
3071
+ private func isCanvasPoint(_ point: CGPoint) -> Bool {
3072
+ CGRect(origin: screenMapCanvasOrigin, size: screenMapCanvasSize).contains(point)
3073
+ }
3074
+
3062
3075
  private func detectDragMode(mapPoint: CGPoint, windowMapRect: CGRect) -> CanvasDragMode {
3063
3076
  let w = windowMapRect.width
3064
3077
  let h = windowMapRect.height
@@ -66,12 +66,40 @@ struct GridFile: Codable {
66
66
  let snapZones: SnapZonesConfig?
67
67
  }
68
68
 
69
- enum SnapModifierKey: String, Codable, Equatable {
69
+ enum SnapModifierKey: String, Codable, Equatable, CaseIterable, Identifiable {
70
70
  case command
71
71
  case option
72
72
  case control
73
73
  case shift
74
74
 
75
+ var id: String { rawValue }
76
+
77
+ var label: String {
78
+ switch self {
79
+ case .command:
80
+ return "Command"
81
+ case .option:
82
+ return "Option"
83
+ case .control:
84
+ return "Control"
85
+ case .shift:
86
+ return "Shift"
87
+ }
88
+ }
89
+
90
+ var shortLabel: String {
91
+ switch self {
92
+ case .command:
93
+ return "Cmd"
94
+ case .option:
95
+ return "Opt"
96
+ case .control:
97
+ return "Ctrl"
98
+ case .shift:
99
+ return "Shift"
100
+ }
101
+ }
102
+
75
103
  var eventFlags: NSEvent.ModifierFlags {
76
104
  switch self {
77
105
  case .command:
@@ -98,9 +126,6 @@ enum SnapModifierKey: String, Codable, Equatable {
98
126
  }
99
127
  }
100
128
 
101
- var label: String {
102
- rawValue.capitalized
103
- }
104
129
  }
105
130
 
106
131
  enum SnapZoneTriggerSpec: Codable, Equatable {
@@ -459,6 +484,36 @@ class WorkspaceManager: ObservableObject {
459
484
  self.snapZonesConfig = snapZones
460
485
  }
461
486
 
487
+ func updateSnapModifier(_ modifier: SnapModifierKey) {
488
+ let updated = SnapZonesConfig(
489
+ enabled: snapZonesConfig.enabled,
490
+ modifier: modifier,
491
+ zoneOpacity: snapZonesConfig.zoneOpacity,
492
+ highlightOpacity: snapZonesConfig.highlightOpacity,
493
+ previewOpacity: snapZonesConfig.previewOpacity,
494
+ cornerRadius: snapZonesConfig.cornerRadius,
495
+ rules: snapZonesConfig.rules
496
+ )
497
+
498
+ do {
499
+ let url = URL(fileURLWithPath: snapZonesConfigPath)
500
+ try FileManager.default.createDirectory(
501
+ at: url.deletingLastPathComponent(),
502
+ withIntermediateDirectories: true
503
+ )
504
+
505
+ let encoder = JSONEncoder()
506
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
507
+ let data = try encoder.encode(updated)
508
+ try data.write(to: url, options: .atomic)
509
+
510
+ loadGridConfig()
511
+ DiagnosticLog.shared.info("WorkspaceManager: updated snap modifier to \(modifier.rawValue)")
512
+ } catch {
513
+ DiagnosticLog.shared.error("WorkspaceManager: failed to write snap-zones.json — \(error.localizedDescription)")
514
+ }
515
+ }
516
+
462
517
  /// Resolve a tile string to fractions: check user presets first, then built-in TilePosition
463
518
  func resolveTileFractions(_ tile: String) -> (CGFloat, CGFloat, CGFloat, CGFloat)? {
464
519
  resolvePlacement(tile)?.fractions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lattices/cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Agentic window manager for macOS — programmable workspace, smart layouts, managed tmux sessions, and a 35+-method agent API",
5
5
  "bin": {
6
6
  "lattices": "./bin/lattices.ts",