@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,32 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Main 5-tab navigation: Now / Inspect / Live / Control / Me
5
+ struct MainTabView: View {
6
+ @Environment(RemuxState.self) private var state
7
+ @State private var selectedTab = 0
8
+
9
+ var body: some View {
10
+ TabView(selection: $selectedTab) {
11
+ NowView()
12
+ .tabItem { Label("Now", systemImage: "house.fill") }
13
+ .tag(0)
14
+
15
+ InspectView()
16
+ .tabItem { Label("Inspect", systemImage: "doc.text.magnifyingglass") }
17
+ .tag(1)
18
+
19
+ LiveTerminalView()
20
+ .tabItem { Label("Live", systemImage: "terminal.fill") }
21
+ .tag(2)
22
+
23
+ ControlView()
24
+ .tabItem { Label("Control", systemImage: "slider.horizontal.3") }
25
+ .tag(3)
26
+
27
+ MeView()
28
+ .tabItem { Label("Me", systemImage: "person.fill") }
29
+ .tag(4)
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>aps-environment</key>
6
+ <string>development</string>
7
+ </dict>
8
+ </plist>
@@ -0,0 +1,14 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ @main
5
+ struct RemuxiOSApp: App {
6
+ @State private var state = RemuxState()
7
+
8
+ var body: some Scene {
9
+ WindowGroup {
10
+ RootView()
11
+ .environment(state)
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,130 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Root view: onboarding → Face ID → main app.
5
+ /// iPad uses NavigationSplitView, iPhone uses TabView.
6
+ struct RootView: View {
7
+ @Environment(RemuxState.self) private var state
8
+ @Environment(\.horizontalSizeClass) private var sizeClass
9
+ @State private var isLocked = true
10
+ @State private var faceIdManager = FaceIDManager()
11
+ private let keychain = KeychainStore()
12
+
13
+ var body: some View {
14
+ Group {
15
+ if isLocked {
16
+ LockedView { unlock() }
17
+ } else if case .connected = state.connectionStatus {
18
+ adaptiveMainView
19
+ } else if case .reconnecting = state.connectionStatus {
20
+ adaptiveMainView
21
+ .overlay(ReconnectingBanner())
22
+ } else {
23
+ OnboardingView()
24
+ }
25
+ }
26
+ .onAppear {
27
+ Task { await checkAuth() }
28
+ }
29
+ .onChange(of: state.connectionStatus) { _, newStatus in
30
+ if case .disconnected = newStatus,
31
+ let snapshot = state.inspectSnapshot,
32
+ let server = state.serverURL?.absoluteString {
33
+ Task {
34
+ await InspectCache.shared.save(
35
+ snapshot: snapshot, server: server, tabIndex: state.activeTabIndex
36
+ )
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ @ViewBuilder
43
+ private var adaptiveMainView: some View {
44
+ if sizeClass == .regular {
45
+ iPadLayout
46
+ } else {
47
+ MainTabView()
48
+ }
49
+ }
50
+
51
+ /// iPad multi-column: sidebar (tabs) + terminal + inspect
52
+ private var iPadLayout: some View {
53
+ NavigationSplitView {
54
+ List {
55
+ Section("Session: \(state.currentSession)") {
56
+ ForEach(state.tabs, id: \.index) { tab in
57
+ Button {
58
+ if let pane = tab.panes.first { state.switchTab(id: pane.id) }
59
+ } label: {
60
+ HStack {
61
+ Image(systemName: tab.active ? "terminal.fill" : "terminal")
62
+ Text(tab.name)
63
+ Spacer()
64
+ if tab.hasBell { Circle().fill(.red).frame(width: 6, height: 6) }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ Section {
70
+ Button { state.createTab() } label: {
71
+ Label("New Tab", systemImage: "plus")
72
+ }
73
+ }
74
+ }
75
+ .navigationTitle("Remux")
76
+ } content: {
77
+ LiveTerminalView()
78
+ } detail: {
79
+ InspectView()
80
+ }
81
+ }
82
+
83
+ private func checkAuth() async {
84
+ let authed = await faceIdManager.authenticateIfNeeded()
85
+ isLocked = !authed
86
+ if authed { tryAutoConnect() }
87
+ }
88
+
89
+ private func unlock() {
90
+ Task {
91
+ let authed = await faceIdManager.authenticateIfNeeded()
92
+ if authed { isLocked = false; tryAutoConnect() }
93
+ }
94
+ }
95
+
96
+ private func tryAutoConnect() {
97
+ let servers = keychain.savedServers()
98
+ if let server = servers.first,
99
+ let token = keychain.loadResumeToken(forServer: server) ?? keychain.loadServerToken(forServer: server),
100
+ let url = URL(string: server) {
101
+ state.connect(url: url, credential: .token(token))
102
+ }
103
+ }
104
+ }
105
+
106
+ struct LockedView: View {
107
+ let onUnlock: () -> Void
108
+ var body: some View {
109
+ VStack(spacing: 20) {
110
+ Image(systemName: "lock.fill").font(.system(size: 48)).foregroundStyle(.secondary)
111
+ Text("Remux is Locked").font(.title2)
112
+ Button("Unlock with Face ID") { onUnlock() }
113
+ .buttonStyle(.borderedProminent)
114
+ }
115
+ }
116
+ }
117
+
118
+ struct ReconnectingBanner: View {
119
+ var body: some View {
120
+ VStack {
121
+ HStack {
122
+ ProgressView().scaleEffect(0.8)
123
+ Text("Reconnecting...").font(.caption).foregroundStyle(.secondary)
124
+ }
125
+ .padding(8).background(.ultraThinMaterial).cornerRadius(8)
126
+ Spacer()
127
+ }
128
+ .padding(.top, 8)
129
+ }
130
+ }
@@ -0,0 +1,102 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Control tab: session/tab management with swipe actions.
5
+ struct ControlView: View {
6
+ @Environment(RemuxState.self) private var state
7
+ @State private var showNewSession = false
8
+
9
+ var body: some View {
10
+ NavigationStack {
11
+ List {
12
+ sessionSection
13
+ tabsSection
14
+ actionsSection
15
+ }
16
+ .navigationTitle("Control")
17
+ .alert("New Session", isPresented: $showNewSession) {
18
+ Button("Create") {
19
+ state.createSession(name: "session-\(Int.random(in: 1000...9999))")
20
+ }
21
+ Button("Cancel", role: .cancel) {}
22
+ }
23
+ }
24
+ }
25
+
26
+ private var sessionSection: some View {
27
+ Section("Session") {
28
+ HStack {
29
+ Image(systemName: "server.rack")
30
+ Text(state.currentSession)
31
+ .font(.headline)
32
+ Spacer()
33
+ }
34
+ }
35
+ }
36
+
37
+ private var tabsSection: some View {
38
+ Section("Tabs (\(state.tabs.count))") {
39
+ ForEach(state.tabs, id: \.index) { tab in
40
+ TabRow(tab: tab, isActive: tab.index == state.activeTabIndex)
41
+ .contentShape(Rectangle())
42
+ .onTapGesture {
43
+ if let pane = tab.panes.first {
44
+ state.switchTab(id: pane.id)
45
+ }
46
+ }
47
+ .swipeActions(edge: .trailing) {
48
+ Button(role: .destructive) {
49
+ if let pane = tab.panes.first {
50
+ state.closeTab(id: pane.id)
51
+ }
52
+ } label: {
53
+ Label("Close", systemImage: "xmark")
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ private var actionsSection: some View {
61
+ Section {
62
+ Button(action: { state.createTab() }) {
63
+ Label("New Tab", systemImage: "plus")
64
+ }
65
+ Button(action: { showNewSession = true }) {
66
+ Label("New Session", systemImage: "plus.rectangle.on.folder")
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ struct TabRow: View {
73
+ let tab: WorkspaceTab
74
+ let isActive: Bool
75
+
76
+ var body: some View {
77
+ HStack {
78
+ Image(systemName: isActive ? "terminal.fill" : "terminal")
79
+ .foregroundStyle(isActive ? Color.accentColor : Color.secondary)
80
+ VStack(alignment: .leading) {
81
+ Text(tab.name)
82
+ if let pane = tab.panes.first {
83
+ Text("\(pane.cols)×\(pane.rows)")
84
+ .font(.caption)
85
+ .foregroundStyle(.tertiary)
86
+ }
87
+ }
88
+ Spacer()
89
+ if isActive {
90
+ Text("Active")
91
+ .font(.caption2)
92
+ .padding(.horizontal, 6)
93
+ .padding(.vertical, 2)
94
+ .background(.tint.opacity(0.15))
95
+ .cornerRadius(4)
96
+ }
97
+ if tab.hasBell {
98
+ Circle().fill(.red).frame(width: 6, height: 6)
99
+ }
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,98 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Inspect tab: readable terminal content with badges, search, pagination.
5
+ struct InspectView: View {
6
+ @Environment(RemuxState.self) private var state
7
+ @State private var searchQuery = ""
8
+
9
+ var body: some View {
10
+ NavigationStack {
11
+ VStack(spacing: 0) {
12
+ // Tab selector
13
+ ScrollView(.horizontal, showsIndicators: false) {
14
+ HStack(spacing: 8) {
15
+ ForEach(state.tabs, id: \.index) { tab in
16
+ Button {
17
+ state.requestInspect(tabIndex: tab.index)
18
+ } label: {
19
+ Text(tab.name)
20
+ .font(.caption)
21
+ .padding(.horizontal, 12)
22
+ .padding(.vertical, 6)
23
+ .background(tab.index == state.activeTabIndex ? Color.accentColor : Color.secondary.opacity(0.15))
24
+ .foregroundStyle(tab.index == state.activeTabIndex ? .white : .primary)
25
+ .cornerRadius(16)
26
+ }
27
+ .buttonStyle(.plain)
28
+ }
29
+ }
30
+ .padding(.horizontal)
31
+ .padding(.vertical, 8)
32
+ }
33
+
34
+ // Descriptor badges
35
+ if let snapshot = state.inspectSnapshot {
36
+ HStack(spacing: 6) {
37
+ Badge(text: snapshot.descriptor.source, color: .blue)
38
+ Badge(text: snapshot.descriptor.precision, color: snapshot.descriptor.precision == "precise" ? .green : .yellow)
39
+ Badge(text: snapshot.descriptor.staleness, color: snapshot.descriptor.staleness == "fresh" ? .green : .orange)
40
+ Spacer()
41
+ Text(snapshot.descriptor.capturedAt.prefix(19).replacingOccurrences(of: "T", with: " "))
42
+ .font(.caption2)
43
+ .foregroundStyle(.tertiary)
44
+ }
45
+ .padding(.horizontal)
46
+ .padding(.bottom, 4)
47
+ }
48
+
49
+ Divider()
50
+
51
+ // Content
52
+ if let snapshot = state.inspectSnapshot {
53
+ List {
54
+ ForEach(Array(snapshot.items.enumerated()), id: \.offset) { _, item in
55
+ Text(item.content)
56
+ .font(.system(.caption, design: .monospaced))
57
+ .foregroundStyle(item.type == "output" ? .primary : .secondary)
58
+ .listRowInsets(EdgeInsets(top: 1, leading: 12, bottom: 1, trailing: 12))
59
+ }
60
+ }
61
+ .listStyle(.plain)
62
+ } else {
63
+ ContentUnavailableView(
64
+ "No Inspect Data",
65
+ systemImage: "doc.text",
66
+ description: Text("Pull to refresh or switch tabs above.")
67
+ )
68
+ }
69
+ }
70
+ .navigationTitle("Inspect")
71
+ .searchable(text: $searchQuery, prompt: "Search terminal content")
72
+ .onSubmit(of: .search) {
73
+ state.requestInspect(tabIndex: state.activeTabIndex, query: searchQuery.isEmpty ? nil : searchQuery)
74
+ }
75
+ .refreshable {
76
+ state.requestInspect(tabIndex: state.activeTabIndex, query: searchQuery.isEmpty ? nil : searchQuery)
77
+ }
78
+ .onAppear {
79
+ state.requestInspect(tabIndex: state.activeTabIndex)
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ struct Badge: View {
86
+ let text: String
87
+ let color: Color
88
+
89
+ var body: some View {
90
+ Text(text)
91
+ .font(.system(size: 10))
92
+ .padding(.horizontal, 6)
93
+ .padding(.vertical, 2)
94
+ .background(color.opacity(0.15))
95
+ .foregroundStyle(color)
96
+ .cornerRadius(4)
97
+ }
98
+ }
@@ -0,0 +1,132 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Live terminal tab: full-screen ghostty-web terminal via WKWebView.
5
+ struct LiveTerminalView: View {
6
+ @Environment(RemuxState.self) private var state
7
+ @State private var bridge: GhosttyBridge?
8
+ @State private var showKeyboardAccessory = true
9
+
10
+ var body: some View {
11
+ ZStack {
12
+ // Terminal
13
+ if let bridge {
14
+ GhosttyTerminalView(bridge: bridge)
15
+ .ignoresSafeArea(.keyboard)
16
+ .onReceive(NotificationCenter.default.publisher(for: .remuxTerminalData)) { notification in
17
+ if let data = notification.userInfo?["data"] as? Data {
18
+ bridge.writeToTerminal(data: data)
19
+ }
20
+ }
21
+ }
22
+
23
+ // Connection status overlay
24
+ if case .disconnected = state.connectionStatus {
25
+ Color.black.opacity(0.7)
26
+ .ignoresSafeArea()
27
+ VStack {
28
+ Image(systemName: "wifi.slash")
29
+ .font(.largeTitle)
30
+ .foregroundStyle(.white)
31
+ Text("Disconnected")
32
+ .foregroundStyle(.white)
33
+ }
34
+ }
35
+ }
36
+ .safeAreaInset(edge: .bottom) {
37
+ if showKeyboardAccessory {
38
+ TerminalKeyboardAccessory(bridge: bridge)
39
+ }
40
+ }
41
+ .onAppear { setupBridge() }
42
+ .navigationBarHidden(true)
43
+ }
44
+
45
+ private func setupBridge() {
46
+ let b = GhosttyBridge()
47
+ b.onInput = { input in
48
+ if let data = input.data(using: .utf8) {
49
+ state.sendTerminalData(data)
50
+ }
51
+ }
52
+ b.onResize = { cols, rows in
53
+ state.sendJSON(["type": "resize", "cols": cols, "rows": rows])
54
+ }
55
+ b.onReady = {
56
+ // Terminal initialized — attach to current tab
57
+ }
58
+ b.onBell = {
59
+ UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
60
+ }
61
+ bridge = b
62
+ }
63
+ }
64
+
65
+ /// Keyboard accessory bar with Esc, Tab, Ctrl, arrows.
66
+ /// Design ref: Termius / Blink terminal keyboard accessory.
67
+ struct TerminalKeyboardAccessory: View {
68
+ let bridge: GhosttyBridge?
69
+ @State private var ctrlActive = false
70
+
71
+ var body: some View {
72
+ ScrollView(.horizontal, showsIndicators: false) {
73
+ HStack(spacing: 8) {
74
+ AccessoryButton("Esc") { send("\u{1b}") }
75
+ AccessoryButton("Tab") { send("\t") }
76
+
77
+ AccessoryButton(ctrlActive ? "Ctrl ●" : "Ctrl") {
78
+ ctrlActive.toggle()
79
+ }
80
+
81
+ Divider().frame(height: 20)
82
+
83
+ AccessoryButton("↑") { send("\u{1b}[A") }
84
+ AccessoryButton("↓") { send("\u{1b}[B") }
85
+ AccessoryButton("←") { send("\u{1b}[D") }
86
+ AccessoryButton("→") { send("\u{1b}[C") }
87
+
88
+ Divider().frame(height: 20)
89
+
90
+ AccessoryButton("Ctrl+C") { send("\u{03}") }
91
+ AccessoryButton("Ctrl+D") { send("\u{04}") }
92
+ AccessoryButton("Ctrl+Z") { send("\u{1a}") }
93
+ }
94
+ .padding(.horizontal, 8)
95
+ .padding(.vertical, 4)
96
+ }
97
+ .background(.bar)
98
+ }
99
+
100
+ private func send(_ text: String) {
101
+ if ctrlActive, text.count == 1, let ascii = text.first?.asciiValue {
102
+ // Ctrl+key: send control character (ascii - 64)
103
+ let ctrl = Character(UnicodeScalar(ascii &- 64))
104
+ bridge?.writeString(String(ctrl))
105
+ ctrlActive = false
106
+ } else {
107
+ bridge?.writeString(text)
108
+ }
109
+ }
110
+ }
111
+
112
+ struct AccessoryButton: View {
113
+ let title: String
114
+ let action: () -> Void
115
+
116
+ init(_ title: String, action: @escaping () -> Void) {
117
+ self.title = title
118
+ self.action = action
119
+ }
120
+
121
+ var body: some View {
122
+ Button(action: action) {
123
+ Text(title)
124
+ .font(.system(size: 13, weight: .medium, design: .monospaced))
125
+ .padding(.horizontal, 10)
126
+ .padding(.vertical, 6)
127
+ .background(.quaternary)
128
+ .cornerRadius(6)
129
+ }
130
+ .buttonStyle(.plain)
131
+ }
132
+ }
@@ -0,0 +1,173 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Now tab: shows current status, active session, recent activity.
5
+ /// Design ref: hapi's Now first screen — focus on the 3 most important things.
6
+ struct NowView: View {
7
+ @Environment(RemuxState.self) private var state
8
+
9
+ var body: some View {
10
+ NavigationStack {
11
+ ScrollView {
12
+ VStack(spacing: 16) {
13
+ // Connection status card
14
+ ConnectionStatusCard()
15
+
16
+ // Active session card
17
+ if !state.currentSession.isEmpty {
18
+ SessionCard(
19
+ sessionName: state.currentSession,
20
+ tabCount: state.tabs.count,
21
+ activeTab: state.tabs.first { $0.index == state.activeTabIndex }
22
+ )
23
+ }
24
+
25
+ // Client role
26
+ HStack {
27
+ Image(systemName: state.clientRole == "active" ? "hand.raised.fill" : "eye.fill")
28
+ Text(state.clientRole == "active" ? "Active" : "Observer")
29
+ .font(.subheadline)
30
+ Spacer()
31
+ if state.clientRole == "observer" {
32
+ Button("Request Control") {
33
+ state.requestControl()
34
+ }
35
+ .buttonStyle(.bordered)
36
+ .controlSize(.small)
37
+ }
38
+ }
39
+ .padding()
40
+ .background(.regularMaterial)
41
+ .cornerRadius(12)
42
+
43
+ // Quick actions
44
+ QuickActionsSection()
45
+ }
46
+ .padding()
47
+ }
48
+ .navigationTitle("Now")
49
+ .refreshable {
50
+ state.requestInspect(tabIndex: state.activeTabIndex)
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ struct ConnectionStatusCard: View {
57
+ @Environment(RemuxState.self) private var state
58
+
59
+ var body: some View {
60
+ HStack {
61
+ Circle()
62
+ .fill(statusColor)
63
+ .frame(width: 10, height: 10)
64
+ Text(statusText)
65
+ .font(.subheadline.bold())
66
+ Spacer()
67
+ if let url = state.serverURL {
68
+ Text(url.host ?? "")
69
+ .font(.caption)
70
+ .foregroundStyle(.secondary)
71
+ }
72
+ }
73
+ .padding()
74
+ .background(.regularMaterial)
75
+ .cornerRadius(12)
76
+ }
77
+
78
+ private var statusColor: Color {
79
+ switch state.connectionStatus {
80
+ case .connected: .green
81
+ case .reconnecting: .yellow
82
+ case .connecting, .authenticating: .orange
83
+ case .disconnected: .red
84
+ }
85
+ }
86
+
87
+ private var statusText: String {
88
+ switch state.connectionStatus {
89
+ case .connected: "Connected"
90
+ case .reconnecting(let n): "Reconnecting (\(n))..."
91
+ case .connecting: "Connecting..."
92
+ case .authenticating: "Authenticating..."
93
+ case .disconnected: "Disconnected"
94
+ }
95
+ }
96
+ }
97
+
98
+ struct SessionCard: View {
99
+ let sessionName: String
100
+ let tabCount: Int
101
+ let activeTab: WorkspaceTab?
102
+
103
+ var body: some View {
104
+ VStack(alignment: .leading, spacing: 8) {
105
+ HStack {
106
+ Image(systemName: "terminal")
107
+ Text(sessionName)
108
+ .font(.headline)
109
+ Spacer()
110
+ Text("\(tabCount) tab\(tabCount == 1 ? "" : "s")")
111
+ .font(.caption)
112
+ .foregroundStyle(.secondary)
113
+ }
114
+ if let tab = activeTab {
115
+ HStack {
116
+ Text("Active: \(tab.name)")
117
+ .font(.subheadline)
118
+ .foregroundStyle(.secondary)
119
+ if let cwd = tab.panes.first?.cwd {
120
+ Text(cwd)
121
+ .font(.caption)
122
+ .foregroundStyle(.tertiary)
123
+ .lineLimit(1)
124
+ }
125
+ }
126
+ }
127
+ }
128
+ .padding()
129
+ .background(.regularMaterial)
130
+ .cornerRadius(12)
131
+ }
132
+ }
133
+
134
+ struct QuickActionsSection: View {
135
+ @Environment(RemuxState.self) private var state
136
+
137
+ var body: some View {
138
+ VStack(alignment: .leading, spacing: 8) {
139
+ Text("Quick Actions")
140
+ .font(.caption)
141
+ .foregroundStyle(.secondary)
142
+
143
+ LazyVGrid(columns: [.init(.flexible()), .init(.flexible())], spacing: 8) {
144
+ QuickActionButton(icon: "plus", title: "New Tab") {
145
+ state.createTab()
146
+ }
147
+ QuickActionButton(icon: "doc.text.magnifyingglass", title: "Inspect") {
148
+ // Switch to inspect tab — handled by parent TabView
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ struct QuickActionButton: View {
156
+ let icon: String
157
+ let title: String
158
+ let action: () -> Void
159
+
160
+ var body: some View {
161
+ Button(action: action) {
162
+ VStack(spacing: 6) {
163
+ Image(systemName: icon)
164
+ .font(.title3)
165
+ Text(title)
166
+ .font(.caption)
167
+ }
168
+ .frame(maxWidth: .infinity)
169
+ .padding(.vertical, 12)
170
+ }
171
+ .buttonStyle(.bordered)
172
+ }
173
+ }