feat: add MoriRemote iOS app with SSH terminal#30
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9381274e7c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| do { | ||
| // PTY is allocated by the SSH channel, providing a proper terminal. | ||
| // Use full path to avoid PATH issues in exec channels. | ||
| let command = "/opt/homebrew/bin/tmux -C new-session -A -s \(Self.shellQuote(trimmedName))" |
There was a problem hiding this comment.
Use a portable tmux command path for remote attach
Constructing the control-mode command with a hardcoded /opt/homebrew/bin/tmux path makes session attach fail on most non-Homebrew hosts (for example Linux servers where tmux is usually /usr/bin/tmux or only on PATH). In those environments openExecChannel succeeds but tmux immediately exits with command-not-found, so users cannot attach at all; this should invoke tmux via PATH or probe multiple locations before launching.
Useful? React with 👍 / 👎.
| // Write the command — the actor suspends here, allowing the read | ||
| // task to process incoming data and deliver the response. | ||
| let data = Data((command + "\n").utf8) | ||
| try? await transport.write(data) |
There was a problem hiding this comment.
Propagate write failures when sending tmux commands
The command write error is being swallowed with try?, so sendCommand continues waiting on the response stream even when the outbound write failed. If the transport remains open but drops the write, callers can block until a later disconnect instead of receiving an immediate failure, which breaks command/error semantics and makes reconnect logic unreliable. This write should throw and clear pending response state immediately.
Useful? React with 👍 / 👎.
| try? await Task.sleep(for: .milliseconds(500)) | ||
| markReady() |
There was a problem hiding this comment.
Gate readiness on handshake completion, not a fixed delay
waitForReady() marks the client ready after a fixed 500ms sleep instead of observing actual tmux handshake completion. On slower or high-latency connections, startup control lines can arrive after this timeout and then be mis-correlated with the first user command once awaitingResponse is true, causing wrong command output or spurious failures. Readiness should be driven by protocol events rather than elapsed time.
Useful? React with 👍 / 👎.
2390208 to
06e30f9
Compare
Keep TmuxSSHConfig and TmuxError cross-platform (used by TmuxControlling). Gate SendableResumeGuard and TmuxCommandRunner actor which use Foundation.Process.
TmuxBackend depends on TmuxCommandRunner (macOS-only Process-based runner).
TCP connect via ClientBootstrap, SSH handshake, password auth, exec channel creation. Accept-all host keys (spike only). Uses MultiThreadedEventLoopGroup for both macOS and iOS.
Tests cover SSHAuthMethod construction, SSHError descriptions, SSHConnectionManager initial state and disconnected exec behavior. Uses RunLoop-based async test runner for Swift 6 compatibility.
- Use concrete PasswordAuthDelegate type to avoid protocol existential Sendable warning in channelInitializer closure capture - PasswordAuthDelegate now offers credentials once (matches SimplePasswordDelegate pattern) - Remove unused SSHChannelDataHandler (ExecHandler handles data in SSHConnectionManager)
…rage - connect() now waits for UserAuthSuccessEvent before returning; auth failures surface as SSHError.authenticationFailed at connect time - openExecChannel() now waits for ChannelSuccessEvent (exec accepted) before returning SSHChannel; ChannelFailureEvent rejects cleanly - Added AuthCompletionHandler for parent-channel auth event tracking - ExecHandler now handles ChannelSuccessEvent/ChannelFailureEvent - SSHChannel.init made public for testability - Tests expanded: 18 → 32 assertions covering inbound stream (single/multi chunk, error propagation), write to inactive channel, close idempotency, publicKey auth rejection, unreachable host connection failure
Parser tests: %output with octal escapes (\012, \134), high-bit bytes, %begin/%end/%error blocks, notifications, plainLine, unknown, malformed. Client tests: command success/error, line split across chunks, multi-line chunk, EOF cancellation, paneOutput routing, notification routing, command newline termination.
- Register pendingContinuation BEFORE writing to transport so fast %begin/%end responses are correctly correlated - Write to transport via fire-and-forget Task; write failures handled by transport EOF → failPending() path - Added FastResponseMockTransport + testClientFastResponseRace regression test that feeds %begin/%end immediately during write (no delay) - MoriTmux: 242 → 243 assertions
- Send LANG=en_US.UTF-8 via EnvironmentRequest before shell request so tmux and other tools produce valid UTF-8 escape sequences - Set explicit nativeBackgroundColor (.black) and nativeForegroundColor on SwiftTerm's TerminalView — the default .clear background caused old content to show through on redraws (clear, scrollback, tmux)
Replace SwiftTerm's default input accessory with a two-row custom bar: - Customizable key bar: esc, ctrl, tab, symbols (~|/-), arrows - Tmux popup menu: tap "tmux" button for New Tab, Split, Pane nav, Zoom, Close, Detach — uses real tmux CLI commands via SSH exec channel, works with any prefix configuration - Gear button opens customize sheet to toggle keys on/off - Key layout persisted in UserDefaults with version migration - Scroll fade gradient hints at off-screen content - Expose SwiftTermRenderer.swiftTermView for accessory wiring
- Add NoPTYExecHandler to MoriSSH for lightweight exec without PTY allocation - Add runCommandNoPTY/openExecChannelNoPTY public API for background queries - Re-enable tmux state polling (5s interval) using NoPTY exec channels - Send tmux action commands through shell channel (not exec) for correct client/pane targeting — works with custom keymaps and any tmux config - Add TmuxSidebarView: expandable session groups with window rows, swipe-to-close, new window/session, switch session, kill session - Add SidebarContainer: slide-from-left overlay with edge swipe gesture - Add observable tmux state on ShellCoordinator for reactive UI - Add sidebar button (top-left) visible when tmux is active - Add Identifiable conformance and sessionName to TmuxSession/TmuxWindow
- Add server card with name, status, Switch Host / Disconnect buttons - Flat uppercase session labels (always expanded) with attached badge - Window rows with 3px teal left accent bar for active window - Long-press context menus on sessions (Switch/Rename/Kill) and windows (Switch/New After/Close) - Footer bar with + Window / + Session buttons - Empty state with + New Session action button - Rename session support via alert dialog - Simplify TmuxBarView to single status pill (session › window) - Tapping tmux pill opens sidebar - Fix: only highlight active window in attached session - Fix: use switch-client for cross-session window selection - Extract into smaller components: ServerCardView, TmuxSessionHeader, TmuxWindowRow, TmuxSidebarFooter - Make SidebarContainer generic over sidebar content
- Remove TmuxBarView row from accessory bar (single key bar only) - Remove ellipsis menu button and toolbar overlay - Disconnect and server info now accessible via sidebar - Cleaner terminal screen with only sidebar button overlay
- Auto-attach: switch-client when inside tmux, attach-session via shell when not attached (enables sidebar use without being in tmux) - Show pane_current_path below each window name in sidebar - TmuxWindow.shortPath shows last 2 path components with …/ prefix - Fix sidebar scroll: ignore mostly-vertical drags in SidebarContainer so ScrollView receives them instead of the close gesture
Split single DragGesture on ZStack into two separate gestures: - Edge-open gesture on content area only - Close gesture on dimming overlay only - Sidebar panel has no gesture, so ScrollView works freely
- Add 1024x1024 app icon to iOS asset catalog (was empty) - Add UIRequiredDeviceCapabilities, UISupportedInterfaceOrientations, ITSAppUsesNonExemptEncryption to Info.plist - Use env vars for DEVELOPMENT_TEAM/MARKETING_VERSION/BUILD_NUMBER in project.yml with fallback defaults - Add archive scheme config to project.yml - Add mise tasks: ios:archive, ios:export, ios:upload - Add ExportOptions.plist for App Store Connect distribution - Add ios-build job to ci.yml (simulator build) - Add release-ios.yml workflow (archive, export, TestFlight upload) triggered by ios-v* tags or manual dispatch
- replace hardcoded team and version values with build-setting defaults - route sidebar host switching through the existing disconnect handler
When tmux (or any app using alternate screen + mouse mode) is attached, UIScrollView has no scrollback to scroll. This adds a UIPanGestureRecognizer that translates vertical swipes into mouse wheel events (button 4/5), enabling proper scroll behavior in tmux with `set -g mouse on`. Three-part approach: - syncScrollEnabled(): disables UIScrollView bounce in alternate screen - alternateScrollGesture: sends mouse wheel events per cell-height of drag - shouldBeRequiredToFailBy: prioritizes scroll over click-drag gestures
- macOS: dev.mori.app → com.vaayne.mori - macOS shared suite: dev.mori.shared → com.vaayne.mori.shared - macOS SSH keychain: dev.mori.app.ssh → com.vaayne.mori.ssh - iOS: com.vaayne.mori → com.vaayne.mori.remote - iOS keychain: com.vaayne.mori.servers → com.vaayne.mori.remote.servers - Homebrew zap: keep old IDs for cleanup, add new ones
- project.yml: DEVELOPMENT_TEAM from env var (no fallback) - release-ios.yml: pass APPLE_TEAM_ID secret as DEVELOPMENT_TEAM - Local builds use Xcode automatic signing settings
Replace App Store Connect API key auth with Apple ID + app-specific password (APPLE_ID / APPLE_APP_PASSWORD secrets already exist). No new secrets needed for iOS release pipeline.
com.vaayne.mori.remote → com.vaayne.mori-remote (hyphen, not dot) to match the registered App Store Connect bundle ID.
763157e to
58ea585
Compare
- Drop custom iOS embedding commit (using SwiftTerm now) - Pin to upstream release tag instead of floating commit - Add GhosttyKit stub in ios-build CI for SPM resolution
- Split Debug (automatic) / Release (manual) signing in project.yml - Install provisioning profile by UUID on CI runner - Remove alpha channel from iOS app icon (App Store requirement) - Update ExportOptions.plist with manual signing + profile mapping - Tested locally: archive + export both succeed
Summary
TmuxControlParser,TmuxControlClient, andTmuxTransportprotocol for managing remote tmux sessions over SSH#if os(macOS)MoriRemote iOS App
ServerStore(JSON in app documents)ShellCoordinator(direct shell) andSpikeCoordinator(tmux control-mode)Infrastructure
SSHConnectionManageractor,SSHChannelwith async inbound stream, key-based + password authTest plan
mise run test— all existing tests passmise run test:tmux— new control-mode parser and client tests passmise run build:release)