@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,275 @@
1
+ import AppKit
2
+ import RemuxKit
3
+
4
+ /// Manages the native macOS menu bar.
5
+ @MainActor
6
+ final class MenuBarManager {
7
+ private weak var state: RemuxState?
8
+
9
+ /// Callback for split operations, wired from AppDelegate.
10
+ var onSplitRight: (() -> Void)?
11
+ var onSplitDown: (() -> Void)?
12
+ var onClosePane: (() -> Void)?
13
+ var onFocusNextPane: (() -> Void)?
14
+ var onFocusPreviousPane: (() -> Void)?
15
+
16
+ /// Callbacks for new features.
17
+ var onNewBrowserPane: (() -> Void)?
18
+ var onNewMarkdownPane: (() -> Void)?
19
+ var onCommandPalette: (() -> Void)?
20
+ var onCopyMode: (() -> Void)?
21
+ var onDetachPane: (() -> Void)?
22
+
23
+ init(state: RemuxState) {
24
+ self.state = state
25
+ setupMenuBar()
26
+ }
27
+
28
+ private func setupMenuBar() {
29
+ let mainMenu = NSMenu()
30
+
31
+ // App menu
32
+ let appMenu = NSMenu()
33
+ appMenu.addItem(withTitle: "About Remux", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
34
+ appMenu.addItem(.separator())
35
+ appMenu.addItem(withTitle: "Settings...", action: #selector(showSettings), keyEquivalent: ",")
36
+ appMenu.addItem(.separator())
37
+ appMenu.addItem(withTitle: "Hide Remux", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
38
+ let hideOthers = appMenu.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
39
+ hideOthers.keyEquivalentModifierMask = [.command, .option]
40
+ appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
41
+ appMenu.addItem(.separator())
42
+ appMenu.addItem(withTitle: "Quit Remux", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
43
+ let appMenuItem = NSMenuItem()
44
+ appMenuItem.submenu = appMenu
45
+ mainMenu.addItem(appMenuItem)
46
+
47
+ // File menu
48
+ let fileMenu = NSMenu(title: "File")
49
+ let newTab = fileMenu.addItem(withTitle: "New Tab", action: #selector(newTab(_:)), keyEquivalent: "t")
50
+ newTab.target = self
51
+ let closeTab = fileMenu.addItem(withTitle: "Close Tab", action: #selector(closeCurrentTab(_:)), keyEquivalent: "w")
52
+ closeTab.target = self
53
+ fileMenu.addItem(.separator())
54
+ let newWindow = fileMenu.addItem(withTitle: "New Window", action: #selector(newWindow(_:)), keyEquivalent: "n")
55
+ newWindow.target = self
56
+ fileMenu.addItem(.separator())
57
+ let newSession = fileMenu.addItem(withTitle: "New Session", action: #selector(newSession(_:)), keyEquivalent: "n")
58
+ newSession.keyEquivalentModifierMask = [.command, .shift]
59
+ newSession.target = self
60
+
61
+ fileMenu.addItem(.separator())
62
+
63
+ // Open in... submenu
64
+ let openInMenu = NSMenu(title: "Open in...")
65
+ for editor in FinderIntegration.installedEditors {
66
+ let item = openInMenu.addItem(withTitle: editor.name, action: #selector(openInEditor(_:)), keyEquivalent: "")
67
+ item.target = self
68
+ item.representedObject = editor
69
+ item.image = NSImage(systemSymbolName: editor.icon, accessibilityDescription: editor.name)
70
+ }
71
+ if openInMenu.items.isEmpty {
72
+ openInMenu.addItem(withTitle: "No Editors Found", action: nil, keyEquivalent: "")
73
+ }
74
+ let openInMenuItem = NSMenuItem(title: "Open in...", action: nil, keyEquivalent: "")
75
+ openInMenuItem.submenu = openInMenu
76
+ fileMenu.addItem(openInMenuItem)
77
+
78
+ let fileMenuItem = NSMenuItem()
79
+ fileMenuItem.submenu = fileMenu
80
+ mainMenu.addItem(fileMenuItem)
81
+
82
+ // Edit menu
83
+ let editMenu = NSMenu(title: "Edit")
84
+ editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
85
+ editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
86
+ editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
87
+ editMenu.addItem(.separator())
88
+ let findItem = editMenu.addItem(withTitle: "Find...", action: #selector(findInTerminal(_:)), keyEquivalent: "f")
89
+ findItem.target = self
90
+ editMenu.addItem(.separator())
91
+ let copyModeItem = editMenu.addItem(withTitle: "Copy Mode", action: #selector(copyModeAction(_:)), keyEquivalent: "c")
92
+ copyModeItem.keyEquivalentModifierMask = [.command, .shift]
93
+ copyModeItem.target = self
94
+ let editMenuItem = NSMenuItem()
95
+ editMenuItem.submenu = editMenu
96
+ mainMenu.addItem(editMenuItem)
97
+
98
+ // View menu
99
+ let viewMenu = NSMenu(title: "View")
100
+ let toggleSidebar = viewMenu.addItem(withTitle: "Toggle Sidebar", action: #selector(toggleSidebar(_:)), keyEquivalent: "s")
101
+ toggleSidebar.keyEquivalentModifierMask = [.command, .control]
102
+ toggleSidebar.target = self
103
+
104
+ viewMenu.addItem(.separator())
105
+
106
+ // Command Palette
107
+ let cmdPalette = viewMenu.addItem(withTitle: "Command Palette", action: #selector(commandPaletteAction(_:)), keyEquivalent: "p")
108
+ cmdPalette.keyEquivalentModifierMask = [.command, .shift]
109
+ cmdPalette.target = self
110
+
111
+ viewMenu.addItem(.separator())
112
+
113
+ // Split pane items
114
+ let splitRight = viewMenu.addItem(withTitle: "Split Right", action: #selector(splitRightAction(_:)), keyEquivalent: "d")
115
+ splitRight.target = self
116
+
117
+ let splitDown = viewMenu.addItem(withTitle: "Split Down", action: #selector(splitDownAction(_:)), keyEquivalent: "d")
118
+ splitDown.keyEquivalentModifierMask = [.command, .shift]
119
+ splitDown.target = self
120
+
121
+ viewMenu.addItem(.separator())
122
+
123
+ // New panel types
124
+ let browserPane = viewMenu.addItem(withTitle: "New Browser Pane", action: #selector(newBrowserPaneAction(_:)), keyEquivalent: "b")
125
+ browserPane.keyEquivalentModifierMask = [.command, .shift]
126
+ browserPane.target = self
127
+
128
+ let markdownPane = viewMenu.addItem(withTitle: "New Markdown Pane", action: #selector(newMarkdownPaneAction(_:)), keyEquivalent: "m")
129
+ markdownPane.keyEquivalentModifierMask = [.command, .shift]
130
+ markdownPane.target = self
131
+
132
+ viewMenu.addItem(.separator())
133
+
134
+ let closePane = viewMenu.addItem(withTitle: "Close Pane", action: #selector(closePaneAction(_:)), keyEquivalent: "w")
135
+ closePane.keyEquivalentModifierMask = [.command, .shift]
136
+ closePane.target = self
137
+
138
+ viewMenu.addItem(.separator())
139
+
140
+ // Focus navigation
141
+ let focusNext = viewMenu.addItem(withTitle: "Focus Next Pane", action: #selector(focusNextAction(_:)), keyEquivalent: "]")
142
+ focusNext.keyEquivalentModifierMask = [.command, .option]
143
+ focusNext.target = self
144
+
145
+ let focusPrev = viewMenu.addItem(withTitle: "Focus Previous Pane", action: #selector(focusPrevAction(_:)), keyEquivalent: "[")
146
+ focusPrev.keyEquivalentModifierMask = [.command, .option]
147
+ focusPrev.target = self
148
+
149
+ let viewMenuItem = NSMenuItem()
150
+ viewMenuItem.submenu = viewMenu
151
+ mainMenu.addItem(viewMenuItem)
152
+
153
+ // Window menu
154
+ let windowMenu = NSMenu(title: "Window")
155
+ windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
156
+ windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
157
+ windowMenu.addItem(.separator())
158
+
159
+ let detachPane = windowMenu.addItem(withTitle: "Detach Pane to Window", action: #selector(detachPaneAction(_:)), keyEquivalent: "\r")
160
+ detachPane.keyEquivalentModifierMask = [.command, .shift]
161
+ detachPane.target = self
162
+
163
+ windowMenu.addItem(.separator())
164
+ windowMenu.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "")
165
+ let windowMenuItem = NSMenuItem()
166
+ windowMenuItem.submenu = windowMenu
167
+ mainMenu.addItem(windowMenuItem)
168
+ NSApp.windowsMenu = windowMenu
169
+
170
+ // Help menu
171
+ let helpMenu = NSMenu(title: "Help")
172
+ let helpMenuItem = NSMenuItem()
173
+ helpMenuItem.submenu = helpMenu
174
+ mainMenu.addItem(helpMenuItem)
175
+ NSApp.helpMenu = helpMenu
176
+
177
+ NSApp.mainMenu = mainMenu
178
+ }
179
+
180
+ @objc private func showSettings(_ sender: Any?) {
181
+ NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
182
+ }
183
+
184
+ @objc private func newWindow(_ sender: Any?) {
185
+ if let appDelegate = NSApp.delegate as? AppDelegate {
186
+ appDelegate.createNewWindow()
187
+ }
188
+ }
189
+
190
+ @objc private func newTab(_ sender: Any?) {
191
+ state?.createTab()
192
+ }
193
+
194
+ @objc private func closeCurrentTab(_ sender: Any?) {
195
+ guard let state, !state.tabs.isEmpty else { return }
196
+ let activeTab = state.tabs.first { $0.index == state.activeTabIndex }
197
+ if let pane = activeTab?.panes.first {
198
+ state.closeTab(id: pane.id)
199
+ }
200
+ }
201
+
202
+ @objc private func newSession(_ sender: Any?) {
203
+ let name = "session-\(Int.random(in: 1000...9999))"
204
+ state?.createSession(name: name)
205
+ }
206
+
207
+ @objc private func toggleSidebar(_ sender: Any?) {
208
+ NSApp.keyWindow?.contentView?.window?.firstResponder?.tryToPerform(
209
+ #selector(NSSplitViewController.toggleSidebar(_:)), with: nil
210
+ )
211
+ }
212
+
213
+ @objc private func findInTerminal(_ sender: Any?) {
214
+ // Search is triggered through the GhosttyNativeView's performKeyEquivalent
215
+ // which intercepts Cmd+F. The menu item provides discoverability.
216
+ }
217
+
218
+ // MARK: - Split pane actions
219
+
220
+ @objc private func splitRightAction(_ sender: Any?) {
221
+ onSplitRight?()
222
+ }
223
+
224
+ @objc private func splitDownAction(_ sender: Any?) {
225
+ onSplitDown?()
226
+ }
227
+
228
+ @objc private func closePaneAction(_ sender: Any?) {
229
+ onClosePane?()
230
+ }
231
+
232
+ @objc private func focusNextAction(_ sender: Any?) {
233
+ onFocusNextPane?()
234
+ }
235
+
236
+ @objc private func focusPrevAction(_ sender: Any?) {
237
+ onFocusPreviousPane?()
238
+ }
239
+
240
+ // MARK: - New panel actions
241
+
242
+ @objc private func newBrowserPaneAction(_ sender: Any?) {
243
+ onNewBrowserPane?()
244
+ }
245
+
246
+ @objc private func newMarkdownPaneAction(_ sender: Any?) {
247
+ onNewMarkdownPane?()
248
+ }
249
+
250
+ @objc private func commandPaletteAction(_ sender: Any?) {
251
+ onCommandPalette?()
252
+ }
253
+
254
+ @objc private func copyModeAction(_ sender: Any?) {
255
+ onCopyMode?()
256
+ }
257
+
258
+ @objc private func detachPaneAction(_ sender: Any?) {
259
+ onDetachPane?()
260
+ }
261
+
262
+ @objc private func openInEditor(_ sender: Any?) {
263
+ guard let item = sender as? NSMenuItem,
264
+ let editor = item.representedObject as? FinderIntegration.ExternalEditor else { return }
265
+
266
+ // Get CWD from the active tab
267
+ guard let state,
268
+ let tab = state.tabs.first(where: { $0.active }),
269
+ let cwd = tab.panes.first?.cwd, !cwd.isEmpty else {
270
+ return
271
+ }
272
+
273
+ FinderIntegration.openInExternalEditor(path: cwd, editor: editor)
274
+ }
275
+ }
@@ -0,0 +1,145 @@
1
+ import AppKit
2
+ import UserNotifications
3
+
4
+ /// Manages terminal notifications with three-level escalation.
5
+ /// Ref: cmux TerminalNotificationStore.swift (design pattern, not code)
6
+ ///
7
+ /// Level 1: Tab badge (red dot + count) — always
8
+ /// Level 2: Sidebar session highlight — always
9
+ /// Level 3: System notification — only when window not focused
10
+ @MainActor
11
+ final class NotificationManager: NSObject {
12
+
13
+ struct TerminalNotification {
14
+ let title: String
15
+ let body: String
16
+ let tabIndex: Int
17
+ let sessionName: String
18
+ }
19
+
20
+ /// Notification counts per tab index
21
+ private(set) var badgeCounts: [Int: Int] = [:]
22
+
23
+ /// Rate limit: max 1 system notification per 30 seconds
24
+ private var lastSystemNotification = Date.distantPast
25
+ private let systemNotificationCooldown: TimeInterval = 30
26
+
27
+ /// Whether UNUserNotificationCenter is available (requires app bundle context)
28
+ private let notificationsAvailable: Bool
29
+
30
+ override init() {
31
+ // UNUserNotificationCenter.current() crashes if not running inside an app bundle
32
+ notificationsAvailable = Bundle.main.bundleIdentifier != nil
33
+ super.init()
34
+ guard notificationsAvailable else { return }
35
+ UNUserNotificationCenter.current().delegate = self
36
+ requestPermission()
37
+ }
38
+
39
+ private func requestPermission() {
40
+ guard notificationsAvailable else { return }
41
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
42
+ }
43
+
44
+ func handleNotification(_ notification: TerminalNotification) {
45
+ // Level 1: Increment badge count
46
+ badgeCounts[notification.tabIndex, default: 0] += 1
47
+
48
+ // Level 3: System notification if window not focused
49
+ let windowFocused = NSApp.isActive && NSApp.keyWindow != nil
50
+ if !windowFocused {
51
+ sendSystemNotification(notification)
52
+ }
53
+ }
54
+
55
+ func clearBadge(forTab tabIndex: Int) {
56
+ badgeCounts[tabIndex] = nil
57
+ }
58
+
59
+ func clearAllBadges() {
60
+ badgeCounts.removeAll()
61
+ }
62
+
63
+ private func sendSystemNotification(_ notification: TerminalNotification) {
64
+ guard notificationsAvailable else { return }
65
+ let now = Date()
66
+ guard now.timeIntervalSince(lastSystemNotification) > systemNotificationCooldown else { return }
67
+ lastSystemNotification = now
68
+
69
+ let content = UNMutableNotificationContent()
70
+ content.title = notification.title
71
+ content.body = notification.body
72
+ content.sound = .default
73
+ content.userInfo = [
74
+ "tabIndex": notification.tabIndex,
75
+ "sessionName": notification.sessionName,
76
+ ]
77
+
78
+ let request = UNNotificationRequest(
79
+ identifier: UUID().uuidString,
80
+ content: content,
81
+ trigger: nil
82
+ )
83
+ UNUserNotificationCenter.current().add(request)
84
+ }
85
+ }
86
+
87
+ extension NotificationManager: @preconcurrency UNUserNotificationCenterDelegate {
88
+ nonisolated func userNotificationCenter(
89
+ _ center: UNUserNotificationCenter,
90
+ didReceive response: UNNotificationResponse,
91
+ withCompletionHandler completionHandler: @escaping () -> Void
92
+ ) {
93
+ DispatchQueue.main.async {
94
+ NSApp.activate(ignoringOtherApps: true)
95
+ NSApp.keyWindow?.makeKeyAndOrderFront(nil)
96
+ }
97
+ completionHandler()
98
+ }
99
+
100
+ nonisolated func userNotificationCenter(
101
+ _ center: UNUserNotificationCenter,
102
+ willPresent notification: UNNotification,
103
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
104
+ ) {
105
+ let options: UNNotificationPresentationOptions = NSApp.isActive ? [] : [.banner, .sound]
106
+ completionHandler(options)
107
+ }
108
+ }
109
+
110
+ // MARK: - OSC Notification Parser
111
+
112
+ /// Parses OSC 9/99/777 notification sequences from PTY data.
113
+ /// Ref: cmux OSC notification detection approach
114
+ struct OSCNotificationParser {
115
+ /// Parse PTY data for notification sequences.
116
+ /// Returns extracted notifications (if any).
117
+ static func parse(_ data: Data) -> [String] {
118
+ guard let text = String(data: data, encoding: .utf8) else { return [] }
119
+ var notifications: [String] = []
120
+
121
+ // OSC 9: iTerm2 notification — ESC ] 9 ; <message> BEL/ST
122
+ let osc9Pattern = "\u{1b}]9;([^\u{07}\u{1b}]+)[\u{07}]"
123
+ if let regex = try? NSRegularExpression(pattern: osc9Pattern) {
124
+ let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
125
+ for match in matches {
126
+ if let range = Range(match.range(at: 1), in: text) {
127
+ notifications.append(String(text[range]))
128
+ }
129
+ }
130
+ }
131
+
132
+ // OSC 777: rxvt-unicode notification — ESC ] 777 ; notify ; <title> ; <body> BEL
133
+ let osc777Pattern = "\u{1b}]777;notify;([^;]*);([^\u{07}\u{1b}]*)[\u{07}]"
134
+ if let regex = try? NSRegularExpression(pattern: osc777Pattern) {
135
+ let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
136
+ for match in matches {
137
+ if let range = Range(match.range(at: 2), in: text) {
138
+ notifications.append(String(text[range]))
139
+ }
140
+ }
141
+ }
142
+
143
+ return notifications
144
+ }
145
+ }
@@ -0,0 +1,152 @@
1
+ import Foundation
2
+ import RemuxKit
3
+
4
+ /// Detects listening ports from server-reported process info.
5
+ /// Displays detected ports in the sidebar and supports opening
6
+ /// in the browser panel (localhost:port).
7
+ ///
8
+ /// Adapted from VS Code port forwarding / detection feature.
9
+ @MainActor
10
+ @Observable
11
+ final class PortScanner {
12
+
13
+ /// A detected listening port.
14
+ struct DetectedPort: Identifiable, Equatable, Hashable, Sendable {
15
+ var id: Int { port }
16
+ let port: Int
17
+ let processName: String
18
+ let pid: Int?
19
+ let detectedAt: Date
20
+
21
+ var localURL: URL? {
22
+ URL(string: "http://localhost:\(port)")
23
+ }
24
+ }
25
+
26
+ /// Currently detected ports.
27
+ private(set) var ports: [DetectedPort] = []
28
+
29
+ /// Known port patterns (for naming).
30
+ private static let knownPorts: [Int: String] = [
31
+ 3000: "Dev Server",
32
+ 3001: "Dev Server",
33
+ 4200: "Angular",
34
+ 5000: "Flask/ASP.NET",
35
+ 5173: "Vite",
36
+ 5174: "Vite",
37
+ 8000: "Django/FastAPI",
38
+ 8080: "HTTP Alt",
39
+ 8443: "HTTPS Alt",
40
+ 8767: "Remux",
41
+ 8888: "Jupyter",
42
+ 9000: "PHP-FPM",
43
+ 9090: "Prometheus",
44
+ ]
45
+
46
+ /// Parse terminal output text for port listening patterns.
47
+ /// Common patterns:
48
+ /// "listening on port 3000"
49
+ /// "running on http://localhost:8080"
50
+ /// "server started at :5173"
51
+ /// "Local: http://localhost:5173/"
52
+ func parseTerminalOutput(_ text: String) {
53
+ let patterns = [
54
+ // "listening on port NNNN" or "listening at port NNNN"
55
+ "(?:listening|started|running|serving)\\s+(?:on|at)\\s+(?:port\\s+)?(\\d{2,5})",
56
+ // "http://localhost:NNNN" or "http://127.0.0.1:NNNN"
57
+ "https?://(?:localhost|127\\.0\\.0\\.1):(\\d{2,5})",
58
+ // ":NNNN" at word boundary (e.g., "server at :3000")
59
+ "(?:at|on)\\s+:(\\d{2,5})",
60
+ ]
61
+
62
+ for pattern in patterns {
63
+ guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
64
+ continue
65
+ }
66
+ let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
67
+ for match in matches {
68
+ if let range = Range(match.range(at: 1), in: text),
69
+ let port = Int(text[range]),
70
+ port >= 1024, port <= 65535 {
71
+ addPort(port)
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ /// Add a detected port if not already tracked.
78
+ func addPort(_ port: Int, processName: String? = nil, pid: Int? = nil) {
79
+ guard !ports.contains(where: { $0.port == port }) else { return }
80
+
81
+ let name = processName ?? Self.knownPorts[port] ?? "Port \(port)"
82
+ let detected = DetectedPort(
83
+ port: port,
84
+ processName: name,
85
+ pid: pid,
86
+ detectedAt: Date()
87
+ )
88
+ ports.append(detected)
89
+ ports.sort { $0.port < $1.port }
90
+ NSLog("[remux] Port detected: %d (%@)", port, name)
91
+ }
92
+
93
+ /// Remove a port from the tracked list.
94
+ func removePort(_ port: Int) {
95
+ ports.removeAll { $0.port == port }
96
+ }
97
+
98
+ /// Clear all detected ports.
99
+ func clearAll() {
100
+ ports.removeAll()
101
+ }
102
+
103
+ /// Check if a port is reachable by attempting a TCP connection.
104
+ func isPortReachable(_ port: Int, timeout: TimeInterval = 1.0) -> Bool {
105
+ let fd = socket(AF_INET, SOCK_STREAM, 0)
106
+ guard fd >= 0 else { return false }
107
+ defer { close(fd) }
108
+
109
+ // Set non-blocking
110
+ var flags = fcntl(fd, F_GETFL, 0)
111
+ flags |= O_NONBLOCK
112
+ fcntl(fd, F_SETFL, flags)
113
+
114
+ var addr = sockaddr_in()
115
+ addr.sin_family = sa_family_t(AF_INET)
116
+ addr.sin_port = UInt16(port).bigEndian
117
+ addr.sin_addr.s_addr = inet_addr("127.0.0.1")
118
+
119
+ let result = withUnsafePointer(to: &addr) { ptr in
120
+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
121
+ connect(fd, sockPtr, socklen_t(MemoryLayout<sockaddr_in>.size))
122
+ }
123
+ }
124
+
125
+ if result == 0 { return true }
126
+ if errno != EINPROGRESS { return false }
127
+
128
+ // Wait for connection with timeout
129
+ var fdSet = fd_set()
130
+ __darwin_fd_zero(&fdSet)
131
+ withUnsafeMutablePointer(to: &fdSet) { ptr in
132
+ let rawPtr = UnsafeMutableRawPointer(ptr)
133
+ let offset = Int(fd / 32)
134
+ let bit = Int(fd % 32)
135
+ rawPtr.advanced(by: offset * MemoryLayout<Int32>.size)
136
+ .assumingMemoryBound(to: Int32.self)
137
+ .pointee |= Int32(1 << bit)
138
+ }
139
+
140
+ var tv = timeval(tv_sec: Int(timeout), tv_usec: 0)
141
+ let selectResult = select(fd + 1, nil, &fdSet, nil, &tv)
142
+ return selectResult > 0
143
+ }
144
+ }
145
+
146
+ // MARK: - fd_set helpers
147
+
148
+ private func __darwin_fd_zero(_ set: inout fd_set) {
149
+ withUnsafeMutableBytes(of: &set) { rawBuf in
150
+ rawBuf.initializeMemory(as: UInt8.self, repeating: 0)
151
+ }
152
+ }
@@ -0,0 +1,13 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ @main
5
+ struct RemuxApp: App {
6
+ @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
7
+
8
+ var body: some Scene {
9
+ Settings {
10
+ SettingsView()
11
+ }
12
+ }
13
+ }