@wangyaoshen/remux 0.3.8-dev.a8ceb0c

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 (183) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
  4. package/.github/dependabot.yml +33 -0
  5. package/.github/workflows/ci.yml +65 -0
  6. package/.github/workflows/deploy.yml +65 -0
  7. package/.github/workflows/publish.yml +312 -0
  8. package/.github/workflows/release-please.yml +21 -0
  9. package/.gitmodules +3 -0
  10. package/.nvmrc +1 -0
  11. package/.release-please-manifest.json +3 -0
  12. package/CLAUDE.md +104 -0
  13. package/Dockerfile +23 -0
  14. package/LICENSE +21 -0
  15. package/README.md +120 -0
  16. package/apps/ios/Config/signing.xcconfig +4 -0
  17. package/apps/ios/Package.swift +26 -0
  18. package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
  19. package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  20. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
  21. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
  22. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
  23. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
  24. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
  25. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
  26. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
  27. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
  28. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
  29. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
  30. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
  31. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
  32. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
  33. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
  34. package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
  35. package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
  36. package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
  37. package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
  38. package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
  39. package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
  40. package/apps/ios/Sources/Remux/RootView.swift +130 -0
  41. package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
  42. package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
  43. package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
  44. package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
  45. package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
  46. package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
  47. package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
  48. package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
  49. package/apps/macos/Package.swift +37 -0
  50. package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
  51. package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
  52. package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
  53. package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
  54. package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
  55. package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
  56. package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
  57. package/apps/macos/Resources/terminfo/67/ghostty +0 -0
  58. package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
  59. package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
  60. package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
  61. package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
  62. package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
  63. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
  64. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
  65. package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
  66. package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
  67. package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
  68. package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
  69. package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
  70. package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
  71. package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
  72. package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
  73. package/apps/macos/Sources/Remux/SocketController.swift +258 -0
  74. package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
  75. package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
  76. package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
  77. package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
  78. package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
  79. package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
  80. package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
  81. package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
  82. package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
  83. package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
  84. package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
  85. package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
  86. package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
  87. package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
  88. package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
  89. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
  90. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
  91. package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
  92. package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
  93. package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
  94. package/build.mjs +33 -0
  95. package/native/android/DecodeGoldenPayloads.kt +487 -0
  96. package/native/android/ProtocolModels.kt +188 -0
  97. package/native/ios/DecodeGoldenPayloads.swift +711 -0
  98. package/native/ios/ProtocolModels.swift +200 -0
  99. package/package.json +45 -0
  100. package/packages/RemuxKit/Package.swift +27 -0
  101. package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
  102. package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
  103. package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
  104. package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
  105. package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
  106. package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
  107. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
  108. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
  109. package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
  110. package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
  111. package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
  112. package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
  113. package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
  114. package/playwright.config.ts +17 -0
  115. package/pnpm-lock.yaml +1588 -0
  116. package/pty-daemon.js +303 -0
  117. package/release-please-config.json +14 -0
  118. package/scripts/auto-deploy.sh +46 -0
  119. package/scripts/build-dmg.sh +121 -0
  120. package/scripts/build-ghostty-kit.sh +43 -0
  121. package/scripts/check-active-terminology.mjs +132 -0
  122. package/scripts/setup-ci-secrets.sh +80 -0
  123. package/scripts/sync-ghostty-web.sh +28 -0
  124. package/scripts/upload-testflight.sh +100 -0
  125. package/server.js +7074 -0
  126. package/src/adapters/agent-events.ts +246 -0
  127. package/src/adapters/claude-code.ts +158 -0
  128. package/src/adapters/codex.ts +210 -0
  129. package/src/adapters/generic-shell.ts +58 -0
  130. package/src/adapters/index.ts +15 -0
  131. package/src/adapters/registry.ts +99 -0
  132. package/src/adapters/types.ts +41 -0
  133. package/src/auth.ts +174 -0
  134. package/src/e2ee.ts +236 -0
  135. package/src/git-service.ts +168 -0
  136. package/src/message-buffer.ts +137 -0
  137. package/src/pty-daemon.ts +357 -0
  138. package/src/push.ts +127 -0
  139. package/src/renderers.ts +455 -0
  140. package/src/server.ts +2407 -0
  141. package/src/service.ts +226 -0
  142. package/src/session.ts +978 -0
  143. package/src/store.ts +1422 -0
  144. package/src/team.ts +123 -0
  145. package/src/tunnel.ts +126 -0
  146. package/src/types.d.ts +50 -0
  147. package/src/vt-tracker.ts +188 -0
  148. package/src/workspace-head.ts +144 -0
  149. package/src/workspace.ts +153 -0
  150. package/src/ws-handler.ts +1526 -0
  151. package/start.ps1 +83 -0
  152. package/tests/adapters.test.js +171 -0
  153. package/tests/auth.test.js +243 -0
  154. package/tests/codex-adapter.test.js +535 -0
  155. package/tests/durable-stream.test.js +153 -0
  156. package/tests/e2e/app.spec.js +530 -0
  157. package/tests/e2ee.test.js +325 -0
  158. package/tests/message-buffer.test.js +245 -0
  159. package/tests/message-routing.test.js +305 -0
  160. package/tests/pty-daemon.test.js +346 -0
  161. package/tests/push.test.js +281 -0
  162. package/tests/renderers.test.js +391 -0
  163. package/tests/search-shell.test.js +499 -0
  164. package/tests/server.test.js +882 -0
  165. package/tests/service.test.js +267 -0
  166. package/tests/store.test.js +369 -0
  167. package/tests/tunnel.test.js +67 -0
  168. package/tests/workspace-head.test.js +116 -0
  169. package/tests/workspace.test.js +417 -0
  170. package/tsconfig.backend.json +11 -0
  171. package/tsconfig.json +15 -0
  172. package/tui/client/client_test.go +125 -0
  173. package/tui/client/connection.go +342 -0
  174. package/tui/client/host_manager.go +141 -0
  175. package/tui/config/cache.go +81 -0
  176. package/tui/config/config.go +53 -0
  177. package/tui/config/config_test.go +89 -0
  178. package/tui/go.mod +32 -0
  179. package/tui/go.sum +50 -0
  180. package/tui/main.go +261 -0
  181. package/tui/tests/integration_test.go +283 -0
  182. package/tui/ui/model.go +310 -0
  183. package/vitest.config.js +10 -0
@@ -0,0 +1,257 @@
1
+ import AppKit
2
+ import SwiftUI
3
+ import RemuxKit
4
+
5
+ /// Main application delegate. Manages windows, tray, and global state.
6
+ /// Architecture ref: cmux AppDelegate.swift
7
+ @MainActor
8
+ final class AppDelegate: NSObject, NSApplicationDelegate {
9
+ private var mainWindow: NSWindow?
10
+ private var additionalWindows: [NSWindow] = []
11
+ private var detachedWindows: [UUID: NSWindow] = [:]
12
+ private var statusItem: NSStatusItem?
13
+ private var menuBarManager: MenuBarManager?
14
+ private(set) var notificationManager: NotificationManager?
15
+ private var socketController: SocketController?
16
+ private var finderIntegration: FinderIntegration?
17
+
18
+ let state = RemuxState()
19
+
20
+ func applicationDidFinishLaunching(_ notification: Notification) {
21
+ // Install crash reporter first
22
+ CrashReporter.shared.install()
23
+
24
+ // Load saved session (if any)
25
+ let savedSession = SessionPersistence.shared.load()
26
+
27
+ setupMainWindow(savedSession: savedSession)
28
+ setupStatusItem()
29
+ menuBarManager = MenuBarManager(state: state)
30
+ notificationManager = NotificationManager()
31
+ setupGlobalShortcut()
32
+
33
+ // Start socket controller for CLI scripting
34
+ socketController = SocketController(state: state)
35
+ socketController?.start()
36
+
37
+ // Setup Finder integration
38
+ finderIntegration = FinderIntegration(state: state)
39
+ finderIntegration?.registerServices()
40
+
41
+ // Start autosave
42
+ SessionPersistence.shared.startAutosave { [weak self] in
43
+ guard let self else {
44
+ return AppSession()
45
+ }
46
+ return self.buildCurrentSession()
47
+ }
48
+
49
+ // Auto-connect after a brief delay to ensure UI is ready
50
+ Task {
51
+ try? await Task.sleep(for: .milliseconds(500))
52
+ self.autoConnectIfConfigured(savedSession: savedSession)
53
+ }
54
+
55
+ // Check for crash reports from previous launch
56
+ Task {
57
+ try? await Task.sleep(for: .seconds(1))
58
+ CrashReporter.shared.checkForPendingReports()
59
+ }
60
+ }
61
+
62
+ func applicationWillTerminate(_ notification: Notification) {
63
+ // Stop socket controller
64
+ socketController?.stop()
65
+
66
+ // Save session on quit (already on @MainActor via class annotation)
67
+ let session = buildCurrentSession()
68
+ SessionPersistence.shared.save(session)
69
+ SessionPersistence.shared.stopAutosave()
70
+ }
71
+
72
+ /// Build the current session state for persistence.
73
+ private func buildCurrentSession() -> AppSession {
74
+ var session = AppSession()
75
+
76
+ // Server URL
77
+ if case .connected = state.connectionStatus {
78
+ session.serverURL = state.serverURL?.absoluteString
79
+ }
80
+
81
+ // Window frame
82
+ if let frame = mainWindow?.frame {
83
+ session.windowFrame = CodableRect(rect: frame)
84
+ }
85
+
86
+ // Split layout — for now, save a single leaf; the MainContentView
87
+ // snapshot would need to be plumbed here for full persistence
88
+ session.splitLayout = .leaf(tabIndex: state.activeTabIndex)
89
+ session.sidebarCollapsed = false
90
+
91
+ return session
92
+ }
93
+
94
+ /// Auto-connect if REMUX_URL and REMUX_TOKEN environment variables are set,
95
+ /// or if a saved session has a server URL.
96
+ private func autoConnectIfConfigured(savedSession: AppSession?) {
97
+ // Priority 1: Environment variables
98
+ if let urlStr = ProcessInfo.processInfo.environment["REMUX_URL"],
99
+ let token = ProcessInfo.processInfo.environment["REMUX_TOKEN"],
100
+ let url = URL(string: urlStr) {
101
+ NSLog("[remux] Auto-connecting to %@ (env vars)", urlStr)
102
+ state.connect(url: url, credential: .token(token))
103
+ return
104
+ }
105
+
106
+ // Priority 2: Saved session with server URL
107
+ if let urlStr = savedSession?.serverURL,
108
+ let _ = URL(string: urlStr) {
109
+ NSLog("[remux] Saved session has server URL: %@, showing connection UI", urlStr)
110
+ // Don't auto-connect without credentials — just pre-fill the URL
111
+ }
112
+
113
+ NSLog("[remux] No REMUX_URL/REMUX_TOKEN env vars, showing connection UI")
114
+ }
115
+
116
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
117
+ false // Keep running in tray
118
+ }
119
+
120
+ // MARK: - Main Window
121
+
122
+ private func setupMainWindow(savedSession: AppSession? = nil) {
123
+ let contentView = MainContentView()
124
+ .environment(state)
125
+
126
+ let defaultFrame = NSRect(x: 0, y: 0, width: 1280, height: 800)
127
+ let windowFrame = savedSession?.windowCGRect ?? defaultFrame
128
+
129
+ let window = NSWindow(
130
+ contentRect: windowFrame,
131
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
132
+ backing: .buffered,
133
+ defer: false
134
+ )
135
+ window.minSize = NSSize(width: 800, height: 500)
136
+ window.title = "Remux"
137
+ window.contentView = NSHostingView(rootView: contentView)
138
+
139
+ // Restore position or center
140
+ if savedSession?.windowCGRect != nil {
141
+ // Position is already set from windowFrame
142
+ } else {
143
+ window.center()
144
+ }
145
+
146
+ window.setFrameAutosaveName("RemuxMainWindow")
147
+ window.makeKeyAndOrderFront(nil)
148
+
149
+ mainWindow = window
150
+ }
151
+
152
+ // MARK: - System Tray
153
+
154
+ private func setupStatusItem() {
155
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
156
+ if let button = statusItem?.button {
157
+ button.image = NSImage(systemSymbolName: "terminal", accessibilityDescription: "Remux")
158
+ button.action = #selector(toggleWindow)
159
+ button.target = self
160
+ }
161
+
162
+ let menu = NSMenu()
163
+ menu.addItem(NSMenuItem(title: "Show/Hide Window", action: #selector(toggleWindow), keyEquivalent: ""))
164
+ menu.addItem(NSMenuItem.separator())
165
+ menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate), keyEquivalent: "q"))
166
+ statusItem?.menu = menu
167
+ }
168
+
169
+ // MARK: - Global Shortcut (Cmd+Shift+R)
170
+
171
+ private func setupGlobalShortcut() {
172
+ NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
173
+ // Cmd+Shift+R
174
+ if event.modifierFlags.contains([.command, .shift]),
175
+ event.charactersIgnoringModifiers == "r" {
176
+ Task { @MainActor in
177
+ if let appDelegate = NSApp.delegate as? AppDelegate {
178
+ appDelegate.toggleWindow()
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ /// Create a new window sharing the same RemuxState connection.
186
+ func createNewWindow() {
187
+ let contentView = MainContentView()
188
+ .environment(state)
189
+
190
+ let window = NSWindow(
191
+ contentRect: NSRect(x: 0, y: 0, width: 1280, height: 800),
192
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
193
+ backing: .buffered,
194
+ defer: false
195
+ )
196
+ window.minSize = NSSize(width: 800, height: 500)
197
+ window.title = "Remux"
198
+ window.contentView = NSHostingView(rootView: contentView)
199
+ window.center()
200
+ window.makeKeyAndOrderFront(nil)
201
+ additionalWindows.append(window)
202
+ }
203
+
204
+ @objc private func toggleWindow() {
205
+ if let window = mainWindow {
206
+ if window.isVisible {
207
+ window.orderOut(nil)
208
+ } else {
209
+ window.makeKeyAndOrderFront(nil)
210
+ NSApp.activate(ignoringOtherApps: true)
211
+ }
212
+ }
213
+ }
214
+
215
+ // MARK: - Window Portal (Detach Pane to Window)
216
+
217
+ /// Detach a terminal panel to its own standalone window.
218
+ /// The panel is displayed in a new NSWindow.
219
+ func detachPaneToWindow() {
220
+ let panelID = UUID()
221
+ let contentView = TerminalContainerView()
222
+ .environment(state)
223
+
224
+ let window = NSWindow(
225
+ contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
226
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
227
+ backing: .buffered,
228
+ defer: false
229
+ )
230
+ window.minSize = NSSize(width: 400, height: 300)
231
+ window.title = "Remux (Detached)"
232
+ window.contentView = NSHostingView(rootView: contentView)
233
+ window.center()
234
+ window.makeKeyAndOrderFront(nil)
235
+
236
+ detachedWindows[panelID] = window
237
+
238
+ // When the window closes, clean up
239
+ NotificationCenter.default.addObserver(
240
+ forName: NSWindow.willCloseNotification,
241
+ object: window,
242
+ queue: .main
243
+ ) { [weak self] _ in
244
+ Task { @MainActor in
245
+ self?.detachedWindows.removeValue(forKey: panelID)
246
+ }
247
+ }
248
+ }
249
+
250
+ /// Close all detached windows and return their panels to the main window.
251
+ func attachAllBack() {
252
+ for (_, window) in detachedWindows {
253
+ window.close()
254
+ }
255
+ detachedWindows.removeAll()
256
+ }
257
+ }
@@ -0,0 +1,210 @@
1
+ import AppKit
2
+ import Foundation
3
+
4
+ /// Lightweight crash reporter: saves crash info to disk and offers to
5
+ /// copy the report on next launch. No external SDK dependency.
6
+ ///
7
+ /// Crash files: ~/Library/Application Support/com.remux/crashes/
8
+ ///
9
+ /// Adapted from PLCrashReporter conceptual design (without the framework).
10
+ final class CrashReporter: @unchecked Sendable {
11
+
12
+ static let shared = CrashReporter()
13
+
14
+ private let fileManager = FileManager.default
15
+
16
+ private var crashDirectory: URL {
17
+ let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
18
+ return appSupport
19
+ .appendingPathComponent("com.remux", isDirectory: true)
20
+ .appendingPathComponent("crashes", isDirectory: true)
21
+ }
22
+
23
+ private init() {}
24
+
25
+ // MARK: - Install handler
26
+
27
+ /// Install the uncaught exception handler. Call once at app launch.
28
+ func install() {
29
+ // Ensure crash directory exists
30
+ try? fileManager.createDirectory(at: crashDirectory, withIntermediateDirectories: true)
31
+
32
+ NSSetUncaughtExceptionHandler { exception in
33
+ CrashReporter.shared.handleException(exception)
34
+ }
35
+
36
+ // Also handle POSIX signals for non-exception crashes
37
+ for sig: Int32 in [SIGABRT, SIGBUS, SIGSEGV, SIGFPE, SIGILL, SIGTRAP] {
38
+ signal(sig) { signalNumber in
39
+ CrashReporter.shared.handleSignal(signalNumber)
40
+ }
41
+ }
42
+
43
+ NSLog("[remux] CrashReporter installed")
44
+ }
45
+
46
+ // MARK: - Check for previous crash
47
+
48
+ /// Check if there are unsent crash reports from a previous launch.
49
+ /// If found, shows an alert offering to copy the report.
50
+ @MainActor
51
+ func checkForPendingReports() {
52
+ let reports = pendingReports()
53
+ guard let latest = reports.last else { return }
54
+
55
+ guard let content = try? String(contentsOf: latest, encoding: .utf8) else { return }
56
+
57
+ let alert = NSAlert()
58
+ alert.messageText = "Remux Crash Report"
59
+ alert.informativeText = "Remux crashed during the last session. Would you like to copy the crash report to your clipboard?"
60
+ alert.alertStyle = .warning
61
+ alert.addButton(withTitle: "Copy to Clipboard")
62
+ alert.addButton(withTitle: "Dismiss")
63
+ alert.addButton(withTitle: "Delete All Reports")
64
+
65
+ let response = alert.runModal()
66
+
67
+ switch response {
68
+ case .alertFirstButtonReturn:
69
+ // Copy to clipboard
70
+ NSPasteboard.general.clearContents()
71
+ NSPasteboard.general.setString(content, forType: .string)
72
+ NSLog("[remux] Crash report copied to clipboard")
73
+ // Mark as sent by renaming
74
+ let sentURL = latest.deletingPathExtension().appendingPathExtension("sent")
75
+ try? fileManager.moveItem(at: latest, to: sentURL)
76
+
77
+ case .alertThirdButtonReturn:
78
+ // Delete all reports
79
+ deleteAllReports()
80
+
81
+ default:
82
+ // Dismiss: rename to .dismissed so we don't ask again
83
+ let dismissedURL = latest.deletingPathExtension().appendingPathExtension("dismissed")
84
+ try? fileManager.moveItem(at: latest, to: dismissedURL)
85
+ }
86
+ }
87
+
88
+ // MARK: - Report listing
89
+
90
+ /// List pending (unsent) crash report files.
91
+ func pendingReports() -> [URL] {
92
+ guard let files = try? fileManager.contentsOfDirectory(
93
+ at: crashDirectory,
94
+ includingPropertiesForKeys: [.creationDateKey],
95
+ options: .skipsHiddenFiles
96
+ ) else { return [] }
97
+
98
+ return files
99
+ .filter { $0.pathExtension == "crash" }
100
+ .sorted { a, b in
101
+ let aDate = (try? a.resourceValues(forKeys: [.creationDateKey]).creationDate) ?? .distantPast
102
+ let bDate = (try? b.resourceValues(forKeys: [.creationDateKey]).creationDate) ?? .distantPast
103
+ return aDate < bDate
104
+ }
105
+ }
106
+
107
+ /// Delete all crash report files.
108
+ func deleteAllReports() {
109
+ guard let files = try? fileManager.contentsOfDirectory(
110
+ at: crashDirectory,
111
+ includingPropertiesForKeys: nil,
112
+ options: .skipsHiddenFiles
113
+ ) else { return }
114
+
115
+ for file in files {
116
+ try? fileManager.removeItem(at: file)
117
+ }
118
+ NSLog("[remux] All crash reports deleted")
119
+ }
120
+
121
+ // MARK: - Crash handling (called from signal/exception handler)
122
+
123
+ private func handleException(_ exception: NSException) {
124
+ let report = buildReport(
125
+ kind: "NSException",
126
+ name: exception.name.rawValue,
127
+ reason: exception.reason ?? "Unknown",
128
+ stackTrace: exception.callStackSymbols
129
+ )
130
+ writeReport(report)
131
+ }
132
+
133
+ private func handleSignal(_ signal: Int32) {
134
+ let signalName: String
135
+ switch signal {
136
+ case SIGABRT: signalName = "SIGABRT"
137
+ case SIGBUS: signalName = "SIGBUS"
138
+ case SIGSEGV: signalName = "SIGSEGV"
139
+ case SIGFPE: signalName = "SIGFPE"
140
+ case SIGILL: signalName = "SIGILL"
141
+ case SIGTRAP: signalName = "SIGTRAP"
142
+ default: signalName = "SIG\(signal)"
143
+ }
144
+
145
+ // Capture call stack (limited in signal context)
146
+ var symbols: [String] = []
147
+ var callstack = [UnsafeMutableRawPointer?](repeating: nil, count: 128)
148
+ let frames = backtrace(&callstack, Int32(callstack.count))
149
+ if frames > 0 {
150
+ if let strs = backtrace_symbols(&callstack, frames) {
151
+ for i in 0..<Int(frames) {
152
+ if let sym = strs[i] {
153
+ symbols.append(String(cString: sym))
154
+ }
155
+ }
156
+ free(strs)
157
+ }
158
+ }
159
+
160
+ let report = buildReport(
161
+ kind: "Signal",
162
+ name: signalName,
163
+ reason: "Process received \(signalName)",
164
+ stackTrace: symbols
165
+ )
166
+ writeReport(report)
167
+
168
+ // Re-raise the signal to get the default behavior (crash)
169
+ Darwin.signal(signal, SIG_DFL)
170
+ raise(signal)
171
+ }
172
+
173
+ private func buildReport(kind: String, name: String, reason: String, stackTrace: [String]) -> String {
174
+ let date = ISO8601DateFormatter().string(from: Date())
175
+ // Read version from bundle directly (safe from any context)
176
+ let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
177
+ ?? ProcessInfo.processInfo.environment["REMUX_VERSION"]
178
+ ?? "0.0.0"
179
+ let os = ProcessInfo.processInfo.operatingSystemVersionString
180
+
181
+ var lines = [
182
+ "Remux Crash Report",
183
+ "==================",
184
+ "Date: \(date)",
185
+ "Version: \(version)",
186
+ "OS: macOS \(os)",
187
+ "Type: \(kind)",
188
+ "Name: \(name)",
189
+ "Reason: \(reason)",
190
+ "",
191
+ "Stack Trace:",
192
+ "------------",
193
+ ]
194
+ lines.append(contentsOf: stackTrace)
195
+
196
+ return lines.joined(separator: "\n")
197
+ }
198
+
199
+ private func writeReport(_ report: String) {
200
+ let timestamp = ISO8601DateFormatter().string(from: Date())
201
+ .replacingOccurrences(of: ":", with: "-")
202
+ let filename = "crash-\(timestamp).crash"
203
+ let fileURL = crashDirectory.appendingPathComponent(filename)
204
+
205
+ try? report.write(to: fileURL, atomically: true, encoding: .utf8)
206
+
207
+ // Also write to stderr for debugging
208
+ fputs("[remux] Crash report saved: \(fileURL.path)\n", stderr)
209
+ }
210
+ }
@@ -0,0 +1,117 @@
1
+ import AppKit
2
+ import RemuxKit
3
+
4
+ /// Finder integration: "Open in Remux" service and external editor launcher.
5
+ /// When invoked from Finder (or via Services menu), creates a new tab with
6
+ /// the selected folder's CWD.
7
+ ///
8
+ /// Also provides quick-launch for popular external editors.
9
+ /// Adapted from iTerm2 / Warp Finder integration patterns.
10
+ @MainActor
11
+ final class FinderIntegration: NSObject {
12
+
13
+ private weak var state: RemuxState?
14
+
15
+ /// External editor definitions.
16
+ struct ExternalEditor: Identifiable {
17
+ let id: String
18
+ let name: String
19
+ let bundleID: String
20
+ let icon: String // SF Symbol
21
+
22
+ /// Check if this editor is installed.
23
+ var isInstalled: Bool {
24
+ NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) != nil
25
+ }
26
+ }
27
+
28
+ /// Known external editors.
29
+ static let editors: [ExternalEditor] = [
30
+ ExternalEditor(id: "vscode", name: "VS Code", bundleID: "com.microsoft.VSCode", icon: "chevron.left.forwardslash.chevron.right"),
31
+ ExternalEditor(id: "cursor", name: "Cursor", bundleID: "com.todesktop.230313mzl4w4u92", icon: "cursorarrow.rays"),
32
+ ExternalEditor(id: "zed", name: "Zed", bundleID: "dev.zed.Zed", icon: "bolt.fill"),
33
+ ExternalEditor(id: "xcode", name: "Xcode", bundleID: "com.apple.dt.Xcode", icon: "hammer.fill"),
34
+ ExternalEditor(id: "sublime", name: "Sublime Text", bundleID: "com.sublimetext.4", icon: "text.alignleft"),
35
+ ]
36
+
37
+ init(state: RemuxState) {
38
+ self.state = state
39
+ super.init()
40
+ }
41
+
42
+ // MARK: - Open folder in Remux (create tab with CWD)
43
+
44
+ /// Handle "Open in Remux" service invocation.
45
+ /// Sends a new_tab request; the server will create a tab.
46
+ /// CWD is set by the server based on the default shell profile,
47
+ /// but we include the path hint in the request.
48
+ func openFolderInRemux(_ folderPath: String) {
49
+ guard let state else { return }
50
+
51
+ // Send a create-tab request with the folder path as a hint.
52
+ // The server can use this to set the initial CWD.
53
+ state.sendJSON([
54
+ "type": "new_tab",
55
+ "cwd": folderPath,
56
+ ])
57
+
58
+ // Bring Remux to front
59
+ NSApp.activate(ignoringOtherApps: true)
60
+ NSApp.keyWindow?.makeKeyAndOrderFront(nil)
61
+
62
+ NSLog("[remux] Open in Remux: %@", folderPath)
63
+ }
64
+
65
+ // MARK: - Open in external editor
66
+
67
+ /// Open a file or directory in the specified external editor.
68
+ static func openInExternalEditor(path: String, editor: ExternalEditor) {
69
+ guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: editor.bundleID) else {
70
+ NSLog("[remux] Editor not found: %@", editor.name)
71
+ return
72
+ }
73
+
74
+ let fileURL = URL(fileURLWithPath: path)
75
+ let config = NSWorkspace.OpenConfiguration()
76
+ config.activates = true
77
+
78
+ NSWorkspace.shared.open(
79
+ [fileURL],
80
+ withApplicationAt: appURL,
81
+ configuration: config
82
+ ) { _, error in
83
+ if let error {
84
+ NSLog("[remux] Failed to open in %@: %@", editor.name, error.localizedDescription)
85
+ }
86
+ }
87
+ }
88
+
89
+ /// Get list of installed editors (for building menu).
90
+ static var installedEditors: [ExternalEditor] {
91
+ editors.filter { $0.isInstalled }
92
+ }
93
+
94
+ // MARK: - NSServices provider
95
+
96
+ /// Register as services provider. Call from AppDelegate.
97
+ /// For full Finder integration, the app's Info.plist needs NSServices entries.
98
+ func registerServices() {
99
+ NSApp.servicesProvider = self
100
+ }
101
+
102
+ /// Handle the "Open in Remux" service invocation from Finder.
103
+ @objc func openInRemux(_ pboard: NSPasteboard, userData: String, error: AutoreleasingUnsafeMutablePointer<NSString>) {
104
+ guard let urls = pboard.readObjects(forClasses: [NSURL.self], options: [
105
+ .urlReadingFileURLsOnly: true,
106
+ ]) as? [URL] else { return }
107
+
108
+ for url in urls {
109
+ if url.hasDirectoryPath {
110
+ openFolderInRemux(url.path)
111
+ } else {
112
+ // For files, open the parent directory
113
+ openFolderInRemux(url.deletingLastPathComponent().path)
114
+ }
115
+ }
116
+ }
117
+ }