@lattices/cli 0.4.14 → 0.5.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 (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  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/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. 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
- }