@wangyaoshen/remux 0.3.8-dev.29e114b
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,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,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
|
+
}
|