@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/deploy.yml +65 -0
- package/.github/workflows/publish.yml +312 -0
- package/.github/workflows/release-please.yml +21 -0
- package/.gitmodules +3 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +3 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/apps/ios/Config/signing.xcconfig +4 -0
- package/apps/ios/Package.swift +26 -0
- package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
- package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
- package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
- package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
- package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
- package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
- package/apps/ios/Sources/Remux/RootView.swift +130 -0
- package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
- package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
- package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
- package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
- package/apps/macos/Package.swift +37 -0
- package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
- package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
- package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
- package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
- package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
- package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
- package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
- package/apps/macos/Resources/terminfo/67/ghostty +0 -0
- package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
- package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
- package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
- package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
- package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
- package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
- package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
- package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
- package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
- package/apps/macos/Sources/Remux/SocketController.swift +258 -0
- package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
- package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
- package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
- package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
- package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
- package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
- package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
- package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
- package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
- package/build.mjs +33 -0
- package/native/android/DecodeGoldenPayloads.kt +487 -0
- package/native/android/ProtocolModels.kt +188 -0
- package/native/ios/DecodeGoldenPayloads.swift +711 -0
- package/native/ios/ProtocolModels.swift +200 -0
- package/package.json +45 -0
- package/packages/RemuxKit/Package.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
- package/playwright.config.ts +17 -0
- package/pnpm-lock.yaml +1588 -0
- package/pty-daemon.js +303 -0
- package/release-please-config.json +14 -0
- package/scripts/auto-deploy.sh +46 -0
- package/scripts/build-dmg.sh +121 -0
- package/scripts/build-ghostty-kit.sh +43 -0
- package/scripts/check-active-terminology.mjs +132 -0
- package/scripts/setup-ci-secrets.sh +80 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/scripts/upload-testflight.sh +100 -0
- package/server.js +7074 -0
- package/src/adapters/agent-events.ts +246 -0
- package/src/adapters/claude-code.ts +158 -0
- package/src/adapters/codex.ts +210 -0
- package/src/adapters/generic-shell.ts +58 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +99 -0
- package/src/adapters/types.ts +41 -0
- package/src/auth.ts +174 -0
- package/src/e2ee.ts +236 -0
- package/src/git-service.ts +168 -0
- package/src/message-buffer.ts +137 -0
- package/src/pty-daemon.ts +357 -0
- package/src/push.ts +127 -0
- package/src/renderers.ts +455 -0
- package/src/server.ts +2407 -0
- package/src/service.ts +226 -0
- package/src/session.ts +978 -0
- package/src/store.ts +1422 -0
- package/src/team.ts +123 -0
- package/src/tunnel.ts +126 -0
- package/src/types.d.ts +50 -0
- package/src/vt-tracker.ts +188 -0
- package/src/workspace-head.ts +144 -0
- package/src/workspace.ts +153 -0
- package/src/ws-handler.ts +1526 -0
- package/start.ps1 +83 -0
- package/tests/adapters.test.js +171 -0
- package/tests/auth.test.js +243 -0
- package/tests/codex-adapter.test.js +535 -0
- package/tests/durable-stream.test.js +153 -0
- package/tests/e2e/app.spec.js +530 -0
- package/tests/e2ee.test.js +325 -0
- package/tests/message-buffer.test.js +245 -0
- package/tests/message-routing.test.js +305 -0
- package/tests/pty-daemon.test.js +346 -0
- package/tests/push.test.js +281 -0
- package/tests/renderers.test.js +391 -0
- package/tests/search-shell.test.js +499 -0
- package/tests/server.test.js +882 -0
- package/tests/service.test.js +267 -0
- package/tests/store.test.js +369 -0
- package/tests/tunnel.test.js +67 -0
- package/tests/workspace-head.test.js +116 -0
- package/tests/workspace.test.js +417 -0
- package/tsconfig.backend.json +11 -0
- package/tsconfig.json +15 -0
- package/tui/client/client_test.go +125 -0
- package/tui/client/connection.go +342 -0
- package/tui/client/host_manager.go +141 -0
- package/tui/config/cache.go +81 -0
- package/tui/config/config.go +53 -0
- package/tui/config/config_test.go +89 -0
- package/tui/go.mod +32 -0
- package/tui/go.sum +50 -0
- package/tui/main.go +261 -0
- package/tui/tests/integration_test.go +283 -0
- package/tui/ui/model.go +310 -0
- 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
|
+
}
|