@lattices/cli 0.4.14 → 0.6.0

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 (181) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +4 -4
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  19. package/docs/reference/dewey.config.ts +2 -2
  20. package/docs/release.md +171 -0
  21. package/docs/repo-structure.md +5 -5
  22. package/docs/voice.md +11 -27
  23. package/package.json +11 -10
  24. package/apps/mac/Package.swift +0 -27
  25. package/apps/mac/Sources/AppShell/App.swift +0 -26
  26. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  27. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  28. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  29. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  30. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  31. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  32. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  33. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  34. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  35. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  36. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  37. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  38. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  39. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  41. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  42. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  43. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  44. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  45. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  46. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  47. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  48. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  49. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  50. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  51. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  52. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  53. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  54. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  55. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  56. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  57. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  58. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  59. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  60. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  61. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  62. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  63. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  64. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  65. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  66. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  70. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  71. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  72. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  73. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  74. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  75. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  76. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  77. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  78. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  79. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  80. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  81. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  82. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  83. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  84. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  85. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  86. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  87. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  88. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  90. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  91. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  92. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  93. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  94. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  95. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  98. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  99. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  100. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  101. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  102. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  103. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  104. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  105. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  106. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  107. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  110. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  112. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  113. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  120. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  121. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  122. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  125. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  126. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  129. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  130. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  131. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  132. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  133. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  134. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  135. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  136. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  137. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  138. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  139. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  140. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  141. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  142. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  143. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  144. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  145. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  146. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  147. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  148. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  149. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  150. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  151. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  152. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  153. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  154. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  155. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  156. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  158. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  160. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  161. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  162. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  163. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  164. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  165. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  166. package/apps/mac/Sources/UI/Theme.swift +0 -164
  167. package/apps/mac/Tests/StageDragTests.swift +0 -333
  168. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  169. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  170. package/apps/mac/Tests/StageTileTests.swift +0 -353
  171. package/swift/Package.swift +0 -20
  172. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  173. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  174. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  175. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  176. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  177. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  178. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  179. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  180. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  181. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,259 +0,0 @@
1
- import AppKit
2
-
3
- enum Terminal: String, CaseIterable, Identifiable {
4
- case terminal = "Terminal"
5
- case iterm2 = "iTerm2"
6
- case warp = "Warp"
7
- case ghostty = "Ghostty"
8
- case kitty = "Kitty"
9
- case alacritty = "Alacritty"
10
-
11
- var id: String { rawValue }
12
-
13
- var bundleId: String {
14
- switch self {
15
- case .terminal: return "com.apple.Terminal"
16
- case .iterm2: return "com.googlecode.iterm2"
17
- case .warp: return "dev.warp.Warp-Stable"
18
- case .ghostty: return "com.mitchellh.ghostty"
19
- case .kitty: return "net.kovidgoyal.kitty"
20
- case .alacritty: return "org.alacritty"
21
- }
22
- }
23
-
24
- var isInstalled: Bool {
25
- NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) != nil
26
- }
27
-
28
- static var installed: [Terminal] {
29
- allCases.filter(\.isInstalled)
30
- }
31
-
32
- /// Launch a command in this terminal
33
- func launch(command: String, in directory: String) {
34
- // Use single quotes for the shell command to avoid AppleScript escaping issues
35
- let dir = directory.replacingOccurrences(of: "'", with: "'\\''")
36
- let cmd = command.replacingOccurrences(of: "'", with: "'\\''")
37
- let fullCmd = "cd '\(dir)' && \(cmd)"
38
-
39
- switch self {
40
- case .terminal:
41
- runOsascript(
42
- "tell application \"Terminal\"",
43
- "activate",
44
- "do script \"\(fullCmd)\"",
45
- "end tell"
46
- )
47
-
48
- case .iterm2:
49
- runOsascript(
50
- "tell application \"iTerm2\"",
51
- "activate",
52
- "set newWindow to (create window with default profile)",
53
- "tell current session of newWindow",
54
- "write text \"\(fullCmd)\"",
55
- "end tell",
56
- "end tell"
57
- )
58
-
59
- case .warp:
60
- let task = Process()
61
- task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
62
- task.arguments = ["-a", "Warp", directory]
63
- try? task.run()
64
- task.waitUntilExit()
65
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
66
- runOsascript(
67
- "tell application \"System Events\"",
68
- "tell process \"Warp\"",
69
- "keystroke \"\(cmd)\"",
70
- "keystroke return",
71
- "end tell",
72
- "end tell"
73
- )
74
- }
75
-
76
- case .ghostty:
77
- let task = Process()
78
- task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
79
- task.arguments = ["-a", "Ghostty"]
80
- task.environment = ["GHOSTTY_SHELL_COMMAND": fullCmd]
81
- try? task.run()
82
-
83
- case .kitty:
84
- if let appUrl = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
85
- let kittyBin = appUrl.appendingPathComponent("Contents/MacOS/kitty").path
86
- let task = Process()
87
- task.executableURL = URL(fileURLWithPath: kittyBin)
88
- task.arguments = ["--single-instance", "--directory", directory, "sh", "-c", command]
89
- try? task.run()
90
- }
91
-
92
- case .alacritty:
93
- if let appUrl = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
94
- let bin = appUrl.appendingPathComponent("Contents/MacOS/alacritty").path
95
- let task = Process()
96
- task.executableURL = URL(fileURLWithPath: bin)
97
- task.arguments = ["--working-directory", directory, "-e", "sh", "-c", command]
98
- try? task.run()
99
- }
100
- }
101
- }
102
-
103
- /// Launch a command in a new tab of the current terminal window
104
- func launchTab(command: String, in directory: String, tabName: String? = nil) {
105
- let dir = directory.replacingOccurrences(of: "'", with: "'\\''")
106
- let cmd = command.replacingOccurrences(of: "'", with: "'\\''")
107
- let fullCmd = "cd '\(dir)' && \(cmd)"
108
-
109
- switch self {
110
- case .iterm2:
111
- var lines = [
112
- "tell application \"iTerm2\"",
113
- "activate",
114
- "if (count of windows) = 0 then",
115
- " create window with default profile",
116
- "else",
117
- " tell current window to create tab with default profile",
118
- "end if",
119
- "tell current session of current tab of current window",
120
- " write text \"\(fullCmd)\"",
121
- ]
122
- if let name = tabName {
123
- let escaped = name.replacingOccurrences(of: "\"", with: "\\\"")
124
- lines.append(" set name to \"\(escaped)\"")
125
- }
126
- lines.append("end tell")
127
- lines.append("end tell")
128
- runOsascriptLines(lines)
129
-
130
- case .terminal:
131
- var lines = [
132
- "tell application \"Terminal\"",
133
- "activate",
134
- "if (count of windows) = 0 then",
135
- " do script \"\(fullCmd)\"",
136
- "else",
137
- " do script \"\(fullCmd)\" in front window",
138
- "end if",
139
- ]
140
- if let name = tabName {
141
- let escaped = name.replacingOccurrences(of: "\"", with: "\\\"")
142
- lines.append("set custom title of selected tab of front window to \"\(escaped)\"")
143
- }
144
- lines.append("end tell")
145
- runOsascriptLines(lines)
146
-
147
- default:
148
- // Terminals without AppleScript tab support: fall back to new window
149
- launch(command: command, in: directory)
150
- }
151
- }
152
-
153
- /// Rename the current/frontmost tab in this terminal
154
- func nameTab(_ name: String) {
155
- let escaped = name.replacingOccurrences(of: "\"", with: "\\\"")
156
- switch self {
157
- case .iterm2:
158
- runOsascript(
159
- "tell application \"iTerm2\"",
160
- "tell current session of current tab of current window",
161
- "set name to \"\(escaped)\"",
162
- "end tell",
163
- "end tell"
164
- )
165
- case .terminal:
166
- runOsascript(
167
- "tell application \"Terminal\"",
168
- "set custom title of selected tab of front window to \"\(escaped)\"",
169
- "end tell"
170
- )
171
- default:
172
- break
173
- }
174
- }
175
-
176
- /// The tag we put in the terminal window title via tmux set-titles
177
- static func windowTag(for session: String) -> String {
178
- "[lattices:\(session)]"
179
- }
180
-
181
- /// Find and focus the existing terminal window by its [lattices:name] tag, or open a new attach
182
- func focusOrAttach(session: String) {
183
- let tag = Terminal.windowTag(for: session)
184
-
185
- switch self {
186
- case .terminal:
187
- runOsascript(
188
- "tell application \"Terminal\"",
189
- "activate",
190
- "set found to false",
191
- "repeat with w in windows",
192
- " if name of w contains \"\(tag)\" then",
193
- " set index of w to 1",
194
- " set found to true",
195
- " exit repeat",
196
- " end if",
197
- "end repeat",
198
- "if not found then do script \"tmux attach -t \(session)\"",
199
- "end tell"
200
- )
201
-
202
- case .iterm2:
203
- // Search through all sessions in all tabs of all windows
204
- runOsascript(
205
- "tell application \"iTerm2\"",
206
- "activate",
207
- "set found to false",
208
- "repeat with w in windows",
209
- " repeat with t in tabs of w",
210
- " repeat with s in sessions of t",
211
- " if name of s contains \"\(tag)\" then",
212
- " select w",
213
- " tell w to set current tab to t",
214
- " set found to true",
215
- " exit repeat",
216
- " end if",
217
- " end repeat",
218
- " if found then exit repeat",
219
- " end repeat",
220
- " if found then exit repeat",
221
- "end repeat",
222
- "if not found then",
223
- " if (count of windows) = 0 then",
224
- " create window with default profile",
225
- " else",
226
- " tell current window to create tab with default profile",
227
- " end if",
228
- " tell current session of current tab of current window",
229
- " write text \"tmux attach -t \(session)\"",
230
- " end tell",
231
- "end if",
232
- "end tell"
233
- )
234
-
235
- default:
236
- // For terminals without good AppleScript support, just activate and attach
237
- let task = Process()
238
- task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
239
- task.arguments = ["-a", rawValue]
240
- try? task.run()
241
- }
242
- }
243
- }
244
-
245
- /// Run an AppleScript by joining lines into a single -e script block
246
- private func runOsascript(_ lines: String...) {
247
- runOsascriptLines(lines)
248
- }
249
-
250
- /// Run an AppleScript from an array of lines
251
- private func runOsascriptLines(_ lines: [String]) {
252
- let script = lines.joined(separator: "\n")
253
- let task = Process()
254
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
255
- task.arguments = ["-e", script]
256
- task.standardOutput = FileHandle.nullDevice
257
- task.standardError = FileHandle.nullDevice
258
- try? task.run()
259
- }
@@ -1,156 +0,0 @@
1
- import AppKit
2
-
3
- // MARK: - Data Model
4
-
5
- struct TerminalTab {
6
- let app: Terminal
7
- let windowIndex: Int
8
- let tabIndex: Int
9
- let tty: String // normalized: "ttys003"
10
- let isActiveTab: Bool
11
- let title: String
12
- let sessionId: String? // iTerm2 unique ID only
13
- }
14
-
15
- // MARK: - Query
16
-
17
- enum TerminalQuery {
18
-
19
- /// Normalize TTY strings: strip "/dev/" prefix if present.
20
- /// iTerm2 returns "/dev/ttys003", Terminal.app and `ps` return "ttys003".
21
- static func normalizeTTY(_ raw: String) -> String {
22
- if raw.hasPrefix("/dev/") {
23
- return String(raw.dropFirst(5))
24
- }
25
- return raw
26
- }
27
-
28
- /// Query all running terminal emulators for tab info.
29
- /// Only queries apps that are currently running (won't auto-launch).
30
- static func queryAll() -> [TerminalTab] {
31
- var results: [TerminalTab] = []
32
- if isAppRunning("iTerm2") {
33
- results.append(contentsOf: queryITerm2())
34
- }
35
- if isAppRunning("Terminal") {
36
- results.append(contentsOf: queryTerminalApp())
37
- }
38
- // Future: queryWarp(), queryGhostty(), etc.
39
- return results
40
- }
41
-
42
- // MARK: - iTerm2
43
-
44
- static func queryITerm2() -> [TerminalTab] {
45
- let script = """
46
- tell application "iTerm2"
47
- set output to ""
48
- set winIdx to 0
49
- repeat with w in windows
50
- set tabIdx to 0
51
- repeat with t in tabs of w
52
- repeat with s in sessions of t
53
- set output to output & winIdx & "\t" & tabIdx & "\t" & (tty of s) & "\t" & (name of s) & "\t" & (unique ID of s) & linefeed
54
- end repeat
55
- set tabIdx to tabIdx + 1
56
- end repeat
57
- set winIdx to winIdx + 1
58
- end repeat
59
- return output
60
- end tell
61
- """
62
-
63
- let raw = osascript(script)
64
- guard !raw.isEmpty else { return [] }
65
-
66
- var tabs: [TerminalTab] = []
67
- for line in raw.split(separator: "\n", omittingEmptySubsequences: true) {
68
- let cols = line.split(separator: "\t", maxSplits: 4, omittingEmptySubsequences: false)
69
- guard cols.count >= 5 else { continue }
70
-
71
- guard let winIdx = Int(cols[0]),
72
- let tabIdx = Int(cols[1]) else { continue }
73
-
74
- let tty = normalizeTTY(String(cols[2]))
75
- guard tty.hasPrefix("ttys") else { continue }
76
-
77
- let title = String(cols[3])
78
- let sessionId = String(cols[4])
79
-
80
- tabs.append(TerminalTab(
81
- app: .iterm2,
82
- windowIndex: winIdx,
83
- tabIndex: tabIdx,
84
- tty: tty,
85
- isActiveTab: false, // iTerm2 doesn't expose this easily in a single call
86
- title: title,
87
- sessionId: sessionId
88
- ))
89
- }
90
- return tabs
91
- }
92
-
93
- // MARK: - Terminal.app
94
-
95
- static func queryTerminalApp() -> [TerminalTab] {
96
- let script = """
97
- tell application "Terminal"
98
- set output to ""
99
- set winIdx to 0
100
- repeat with w in windows
101
- set selTab to selected tab of w
102
- set tabIdx to 0
103
- repeat with t in tabs of w
104
- set isSel to (t = selTab)
105
- set output to output & winIdx & "\t" & tabIdx & "\t" & (tty of t) & "\t" & (custom title of t) & "\t" & isSel & linefeed
106
- set tabIdx to tabIdx + 1
107
- end repeat
108
- set winIdx to winIdx + 1
109
- end repeat
110
- return output
111
- end tell
112
- """
113
-
114
- let raw = osascript(script)
115
- guard !raw.isEmpty else { return [] }
116
-
117
- var tabs: [TerminalTab] = []
118
- for line in raw.split(separator: "\n", omittingEmptySubsequences: true) {
119
- let cols = line.split(separator: "\t", maxSplits: 4, omittingEmptySubsequences: false)
120
- guard cols.count >= 5 else { continue }
121
-
122
- guard let winIdx = Int(cols[0]),
123
- let tabIdx = Int(cols[1]) else { continue }
124
-
125
- let tty = normalizeTTY(String(cols[2]))
126
- guard tty.hasPrefix("ttys") else { continue }
127
-
128
- let title = String(cols[3])
129
- let isActive = String(cols[4]).lowercased() == "true"
130
-
131
- tabs.append(TerminalTab(
132
- app: .terminal,
133
- windowIndex: winIdx,
134
- tabIndex: tabIdx,
135
- tty: tty,
136
- isActiveTab: isActive,
137
- title: title,
138
- sessionId: nil
139
- ))
140
- }
141
- return tabs
142
- }
143
-
144
- // MARK: - Helpers
145
-
146
- /// Check if a named app is already running (prevents AppleScript from auto-launching it).
147
- private static func isAppRunning(_ name: String) -> Bool {
148
- NSWorkspace.shared.runningApplications.contains { $0.localizedName == name }
149
- }
150
-
151
- /// Run an AppleScript and capture stdout.
152
- /// Uses ProcessQuery.shell to avoid Process.waitUntilExit() deadlocks on macOS 26.
153
- private static func osascript(_ source: String) -> String {
154
- ProcessQuery.shell(["/usr/bin/osascript", "-e", source])
155
- }
156
- }
@@ -1,200 +0,0 @@
1
- import Foundation
2
-
3
- // MARK: - Unified Output
4
-
5
- struct TerminalInstance {
6
- // Join key
7
- let tty: String
8
-
9
- // Tab info (from AppleScript)
10
- let app: Terminal?
11
- let windowIndex: Int?
12
- let tabIndex: Int?
13
- let isActiveTab: Bool
14
- let tabTitle: String?
15
- let terminalSessionId: String? // iTerm2 unique ID
16
-
17
- // Process info (from ps)
18
- let processes: [ProcessEntry]
19
- let shellPid: Int?
20
- let cwd: String?
21
-
22
- // Tmux info
23
- let tmuxSession: String?
24
- let tmuxPaneId: String?
25
-
26
- // Window info (from CGWindowList)
27
- let windowId: UInt32?
28
- let windowTitle: String?
29
-
30
- // Computed
31
- var hasClaude: Bool {
32
- processes.contains { $0.comm == "claude" }
33
- }
34
-
35
- var displayName: String {
36
- if let session = tmuxSession { return session }
37
- if let title = tabTitle, !title.isEmpty { return title }
38
- if let title = windowTitle, !title.isEmpty { return title }
39
- return tty
40
- }
41
- }
42
-
43
- // MARK: - Synthesizer
44
-
45
- enum TerminalSynthesizer {
46
-
47
- /// Pure-function merge: joins 5 slices by TTY into unified TerminalInstances.
48
- ///
49
- /// - Parameters:
50
- /// - processTable: Full process table from ProcessQuery.snapshot()
51
- /// - interesting: Filtered interesting processes
52
- /// - tmuxSessions: Current tmux sessions with panes
53
- /// - terminalTabs: AppleScript-enumerated tabs
54
- /// - windows: CGWindowList entries
55
- static func synthesize(
56
- processTable: [Int: ProcessEntry],
57
- interesting: [ProcessEntry],
58
- tmuxSessions: [TmuxSession],
59
- terminalTabs: [TerminalTab],
60
- windows: [UInt32: WindowEntry]
61
- ) -> [TerminalInstance] {
62
-
63
- // 1. Single pass: index ALL processes by normalized TTY
64
- // This avoids O(TTYs × processes) re-scans later.
65
- var allProcessesByTTY: [String: [ProcessEntry]] = [:]
66
- for entry in processTable.values {
67
- let tty = TerminalQuery.normalizeTTY(entry.tty)
68
- guard tty != "??" else { continue }
69
- allProcessesByTTY[tty, default: []].append(entry)
70
- }
71
-
72
- // 2. Group interesting processes by TTY (subset of above)
73
- var interestingByTTY: [String: [ProcessEntry]] = [:]
74
- for entry in interesting {
75
- let tty = TerminalQuery.normalizeTTY(entry.tty)
76
- guard tty != "??" else { continue }
77
- interestingByTTY[tty, default: []].append(entry)
78
- }
79
-
80
- // 3. Build tmux pane → TTY lookup
81
- var tmuxByTTY: [String: (session: String, paneId: String)] = [:]
82
- for session in tmuxSessions {
83
- for pane in session.panes {
84
- if let entry = processTable[pane.pid] {
85
- let tty = TerminalQuery.normalizeTTY(entry.tty)
86
- if tty != "??" {
87
- tmuxByTTY[tty] = (session: session.name, paneId: pane.id)
88
- }
89
- }
90
- }
91
- }
92
-
93
- // 4. Index terminal tabs by TTY
94
- var tabByTTY: [String: TerminalTab] = [:]
95
- for tab in terminalTabs {
96
- tabByTTY[tab.tty] = tab
97
- }
98
-
99
- // 5. Collect all known TTYs (union of all maps)
100
- var allTTYs = Set(interestingByTTY.keys)
101
- allTTYs.formUnion(tmuxByTTY.keys)
102
- allTTYs.formUnion(tabByTTY.keys)
103
-
104
- // 6. Build window lookup for positional matching
105
- let windowsByApp = buildWindowsByApp(windows)
106
-
107
- // 7. For each TTY, merge all slices
108
- var instances: [TerminalInstance] = []
109
- for tty in allTTYs {
110
- let procs = interestingByTTY[tty] ?? []
111
- let tab = tabByTTY[tty]
112
- let tmux = tmuxByTTY[tty]
113
-
114
- // Shell PID: process on this TTY whose parent is NOT on this TTY
115
- let ttyProcs = allProcessesByTTY[tty] ?? []
116
- let ttyPids = Set(ttyProcs.map(\.pid))
117
- let shellPid = ttyProcs.first { !ttyPids.contains($0.ppid) }?.pid
118
-
119
- // CWD: deepest interesting process's cwd, or shell's cwd
120
- let cwd = procs.last(where: { $0.cwd != nil })?.cwd
121
- ?? (shellPid.flatMap { processTable[$0]?.cwd })
122
-
123
- // Window: try lattices tag match first, then positional
124
- let windowMatch = resolveWindow(
125
- tmuxSession: tmux?.session,
126
- tab: tab,
127
- windowsByApp: windowsByApp,
128
- allWindows: windows
129
- )
130
-
131
- instances.append(TerminalInstance(
132
- tty: tty,
133
- app: tab?.app,
134
- windowIndex: tab?.windowIndex,
135
- tabIndex: tab?.tabIndex,
136
- isActiveTab: tab?.isActiveTab ?? false,
137
- tabTitle: tab?.title,
138
- terminalSessionId: tab?.sessionId,
139
- processes: procs,
140
- shellPid: shellPid,
141
- cwd: cwd,
142
- tmuxSession: tmux?.session,
143
- tmuxPaneId: tmux?.paneId,
144
- windowId: windowMatch?.wid,
145
- windowTitle: windowMatch?.title
146
- ))
147
- }
148
-
149
- // 7. Sort: Claude first, active tabs first, then by TTY
150
- instances.sort { a, b in
151
- if a.hasClaude != b.hasClaude { return a.hasClaude }
152
- if a.isActiveTab != b.isActiveTab { return a.isActiveTab }
153
- return a.tty < b.tty
154
- }
155
-
156
- return instances
157
- }
158
-
159
- // MARK: - Private Helpers
160
-
161
- /// Group windows by app name for positional matching.
162
- private static func buildWindowsByApp(_ windows: [UInt32: WindowEntry]) -> [String: [WindowEntry]] {
163
- var result: [String: [WindowEntry]] = [:]
164
- for w in windows.values {
165
- result[w.app, default: []].append(w)
166
- }
167
- // Sort each app's windows for consistent positional matching
168
- for key in result.keys {
169
- result[key]?.sort { $0.wid < $1.wid }
170
- }
171
- return result
172
- }
173
-
174
- /// Resolve a window for this TTY. Try lattices tag match first, then positional.
175
- private static func resolveWindow(
176
- tmuxSession: String?,
177
- tab: TerminalTab?,
178
- windowsByApp: [String: [WindowEntry]],
179
- allWindows: [UInt32: WindowEntry]
180
- ) -> WindowEntry? {
181
- // Strategy 1: lattices session tag match
182
- if let session = tmuxSession {
183
- let tag = Terminal.windowTag(for: session)
184
- if let match = allWindows.values.first(where: { $0.title.contains(tag) }) {
185
- return match
186
- }
187
- }
188
-
189
- // Strategy 2: positional match by app + window index
190
- if let tab = tab {
191
- let appName = tab.app.rawValue
192
- if let appWindows = windowsByApp[appName],
193
- tab.windowIndex < appWindows.count {
194
- return appWindows[tab.windowIndex]
195
- }
196
- }
197
-
198
- return nil
199
- }
200
- }
@@ -1,60 +0,0 @@
1
- import Foundation
2
-
3
- final class TmuxModel: ObservableObject {
4
- static let shared = TmuxModel()
5
-
6
- @Published private(set) var sessions: [TmuxSession] = []
7
- @Published private(set) var isAvailable: Bool = TmuxQuery.isAvailable
8
- private var timer: Timer?
9
-
10
- func start(interval: TimeInterval = 3.0) {
11
- guard timer == nil else { return }
12
-
13
- if !isAvailable {
14
- DiagnosticLog.shared.warn("TmuxModel: tmux not found — session features disabled")
15
- return
16
- }
17
-
18
- DiagnosticLog.shared.info("TmuxModel: starting (interval=\(interval)s)")
19
- poll()
20
- timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
21
- self?.poll()
22
- }
23
- }
24
-
25
- func stop() {
26
- timer?.invalidate()
27
- timer = nil
28
- }
29
-
30
- func poll() {
31
- let fresh = TmuxQuery.listSessions()
32
- let changed = sessionsChanged(old: sessions, new: fresh)
33
-
34
- DispatchQueue.main.async {
35
- self.sessions = fresh
36
- }
37
-
38
- if changed {
39
- EventBus.shared.post(.tmuxChanged(sessions: fresh))
40
- }
41
- }
42
-
43
- func isRunning(_ name: String) -> Bool {
44
- sessions.contains { $0.name == name }
45
- }
46
-
47
- private func sessionsChanged(old: [TmuxSession], new: [TmuxSession]) -> Bool {
48
- guard old.count == new.count else { return true }
49
- let oldNames = Set(old.map(\.name))
50
- let newNames = Set(new.map(\.name))
51
- if oldNames != newNames { return true }
52
- // Check pane counts changed
53
- for newSession in new {
54
- guard let oldSession = old.first(where: { $0.name == newSession.name }) else { return true }
55
- if oldSession.panes.count != newSession.panes.count { return true }
56
- if oldSession.attached != newSession.attached { return true }
57
- }
58
- return false
59
- }
60
- }