Repository navigation
iOS: reorderable built-in toolbar items (Ctrl/Option/Cmd/Esc/Tab) in toolbar settings - #5579
Conversation
📝 WalkthroughWalkthroughThis PR expands toolbar action configurability to include modifiers and zoom, introduces a v3 storage schema with automated v2→v3 migration, refactors accessory bar population to render user-configured items in order, unifies button styling, and adds comprehensive migration and configuration tests. ChangesTerminal Accessory Toolbar Configurability and v3 Schema Migration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (3 errors, 1 warning)
✅ Passed checks (15 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
aa39bf7 to
19f5ee7
Compare
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 19f5ee766d
ℹ️ 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".
| for id in newLeading + newTrailing where seen.insert(id).inserted { | ||
| enabledSet.append(id) |
There was a problem hiding this comment.
Force-enable already-present pinned IDs
When a legacy v1/v2 order already contains one of the IDs being widened (the helper explicitly preserves that case), this loop only appends newLeading + newTrailing, so a forced built-in that was present in order but omitted from enabled remains hidden after migration. That violates the v3 upgrade contract that previously pinned controls are force-shown and can make an upgrading toolbar drop a modifier/zoom/paste item for partially migrated or beta defaults; iterate over all leading + trailing IDs when force-enabling, while still avoiding duplicates.
Useful? React with 👍 / 👎.
19f5ee7 to
c37047b
Compare
Greptile SummaryThis PR folds the previously pinned modifier keys (⌃ ⌥ ⌘), paste, and zoom controls into the unified reorderable toolbar region, so every iOS terminal bar button can be dragged, hidden, or reordered in settings alongside shortcuts and custom actions. A v3 UserDefaults schema and
Confidence Score: 4/5Safe to merge after fixing one defect in the new reconcile helper: shift armed via hardware keyboard is silently cleared on every bar rebuild. The migration logic, UIKit builder refactor, and localization changes are all well-structured and well-tested. The one concrete defect is in Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift — specifically the Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[App Launch / TerminalAccessoryConfiguration.init] --> B{v3 keys present?}
B -- Yes --> C[Load as-is, v3 enabled set is authoritative]
B -- No --> D{v2 keys present?}
D -- Yes --> E[widenedToV3: prepend leading, append trailing, force-enable]
D -- No --> F{v1 keys present?}
F -- Yes --> G[migratedOrder/migratedEnabled then widenedToV3]
F -- No --> H[Fresh install: empty saved → defaultConfigurableOrder]
C --> I[reducer.load → displayOrder + enabledSet]
E --> I
G --> I
H --> I
I --> J[persist under v3 keys — migration runs once]
J --> K[TerminalInputTextView.populateAccessoryActions]
K --> L[enabledItems in saved order]
L --> M{action == .command?}
M -- not isMacRemote --> N[skip — stays in saved order unrendered]
M -- isMacRemote --> O[addArrangedSubview]
L --> O
K --> P[reconcileArmedModifierVisibility]
P --> Q{armedModifier present AND accessoryAction returns non-nil?}
Q -- button not in renderedActions --> R[disarmAll + refreshStyles]
Q -- button rendered or nil --> S[no-op]
Reviews (2): Last reviewed commit: "iOS: reorderable built-in toolbar items ..." | Re-trigger Greptile |
…oolbar items) into dog bundle # Conflicts: # Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift # Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
Render the input accessory bar buttons (modifiers ⌃⌥⌘⇧, Esc/Tab/arrows/ $/@//, zoom, agent launchers) with real iOS 26 Liquid Glass via UIButton.Configuration .glass() (resting) and .prominentGlass() (armed/sticky), keeping the prior flat gray/blue fill as the < iOS 26 fallback. Button styling and the armed/sticky restyle now flow through one configuration builder. Also pin single-glyph modifiers (⌃⌥⌘⇧) and icon buttons (zoom) to a fixed width so they stay uniform: their glyph metrics differ and the glass capsule under a greater-than-or-equal min-width let Ctrl/Option grow wider than Cmd. Variable-text buttons (Claude/Codex/etc.) still size to content. Independent of the iOS composer work (PR #5511); this improves the existing terminal accessory bar and can land on its own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The data-driven terminal accessory bar's default arrangement put Esc seven positions after Tab. Move Esc so the two most common terminal keys sit adjacent (Tab, Esc, ^C, ...). Only changes the first-launch / Restore Default order; a device with a saved v2 order keeps its arrangement until reset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…toolbar settings The terminal accessory bar's modifier keys (⌃ ⌥ ⌘), zoom controls, and paste were structurally pinned outside the configurable region, so only the insertable shortcuts and custom actions could be reordered/hidden. Fold those built-ins into the same configurable region so every bar button (except ⇧, which is never surfaced, and the fixed "customize" control) can be dragged to any position or hidden in toolbar settings. - Widen `TerminalInputAccessoryAction.isUserConfigurable` to include modifiers (minus shift), zoom, and paste; add `defaultLeadingActions`/`defaultTrailingActions` and rebuild `defaultConfigurableOrder` so a fresh install shows modifiers/paste leading, zoom trailing, and Esc right after Tab. - Bump the persisted schema to v3 and add `ToolbarLayoutMigration.widenedToV3` (pure, in CmuxMobileTerminalKit): v1/v2 → v3 force-enables the now-configurable built-ins and inserts them at their old fixed positions so an upgrading user's bar looks unchanged. Force-enable is confined to the upgrade boundary; a v3 config's enabled set is authoritative, so hiding a modifier persists. - Render the configurable region inline in `populateAccessoryActions` (no more pinned leading/trailing lists). ⌘ stays in the saved order but only renders for a Mac remote; `updateModifierLabels` repopulates on the flip. Liquid Glass styling (#5536) applies to every item regardless of position. - Settings editor already lists `displayItems` and reorders via `onMove`, so the built-ins now appear there too; updated footer + per-action display names. Tests: v3 widening transform (Kit, swift test) and the live config model (fresh-install default order, reorder/hide round-trips, v2→v3 migration with a partial enabled subset; cmuxFeatureTests, simulator). Stacks on #5536 (feat-ios-accessory-glass): sequence #5536 → this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c37047b to
1ac9735
Compare
| private static func accessoryAction(for modifier: TerminalInputModifier) -> TerminalInputAccessoryAction? { | ||
| switch modifier { | ||
| case .control: return .control | ||
| case .alternate: return .alternate | ||
| case .command: return .command | ||
| case .shift: return .shift | ||
| } | ||
| } |
There was a problem hiding this comment.
shift case silently disarms on every bar rebuild
accessoryAction(for: .shift) returns .shift rather than nil, so the early-exit guard let action = Self.accessoryAction(for: armed) else { return } never fires when shift is armed. Because .shift is intentionally not in enabledItems (isUserConfigurable is false), its button is never added to renderedActions — so guard !renderedActions.contains(action) is always true, and every call to populateAccessoryActions() (triggered by hiding any item, toggling an enabled flag, or a Mac-remote switch) calls modifierState.disarmAll() while shift is active, silently clearing it. Returning nil for .shift makes the guard exit without touching modifier state, which is the correct invariant since shift has no bar button to reconcile against.
| private static func accessoryAction(for modifier: TerminalInputModifier) -> TerminalInputAccessoryAction? { | |
| switch modifier { | |
| case .control: return .control | |
| case .alternate: return .alternate | |
| case .command: return .command | |
| case .shift: return .shift | |
| } | |
| } | |
| private static func accessoryAction(for modifier: TerminalInputModifier) -> TerminalInputAccessoryAction? { | |
| switch modifier { | |
| case .control: return .control | |
| case .alternate: return .alternate | |
| case .command: return .command | |
| case .shift: return nil | |
| } | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@ios/cmuxPackage/Tests/cmuxFeatureTests/TerminalAccessoryConfigurationTests.swift`:
- Around line 29-50: The test freshInstallDefaultOrder currently only checks
displayOrder (which excludes .shift) so add a runtime assertion against the
actual rendered accessory toolbar to catch regressions: after creating let
config = TerminalAccessoryConfiguration(defaults: freshDefaults()), instantiate
or request the real accessory view/controller that uses that config (the same
codepath used to render the input accessory) and collect its visible toolbar
item identifiers, then assert that the visible items do not contain id(.shift);
keep the existing displayOrder checks and use TerminalAccessoryConfiguration,
displayOrder, TerminalInputAccessoryAction.configurableActions and
config.isEnabled references to locate the test and the configuration.
In
`@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift`:
- Around line 201-223: populateAccessoryActions currently rebuilds the bar only
from TerminalAccessoryConfiguration.shared.enabledItems and the trailing
customize button, which drops the fixed Shift control; update
populateAccessoryActions to always add the fixed Shift accessory (use the
existing maker, e.g. makeShiftAccessoryButton or the Shift-specific view
factory) to the stack (in its fixed position) when rebuilding the
arrangedSubviews, then add the enabledItems (.builtin via makeAccessoryButton
and .custom via makeCustomAccessoryButton) and finally the settings button;
ensure you don't duplicate the Shift view if it's already present and that the
Shift button remains non-configurable and rendered regardless of enabledItems.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 245d4901-b925-4c90-a601-1865e358a53e
📒 Files selected for processing (8)
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalShortcutsSettingsView.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalAccessoryConfiguration.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swiftPackages/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/ToolbarLayoutMigration.swiftPackages/CmuxMobileTerminalKit/Tests/CmuxMobileTerminalKitTests/ToolbarCustomizationTests.swiftios/cmux/Resources/Localizable.xcstringsios/cmuxPackage/Tests/cmuxFeatureTests/TerminalAccessoryConfigurationTests.swift
| @Test("fresh install puts modifiers at the front and zoom at the back, all shown") | ||
| func freshInstallDefaultOrder() throws { | ||
| let config = TerminalAccessoryConfiguration(defaults: freshDefaults()) | ||
| let order = config.displayOrder | ||
|
|
||
| // Leading region: ⌃ ⌥ ⌘ then paste, in that order. | ||
| #expect(Array(order.prefix(4)) == [ | ||
| id(.control), id(.alternate), id(.command), id(.paste), | ||
| ]) | ||
| // Trailing region: the two zoom controls, in that order. | ||
| #expect(Array(order.suffix(2)) == [id(.zoomOut), id(.zoomIn)]) | ||
| // Esc sits right after Tab in the redesigned default. | ||
| let tabIndex = try #require(order.firstIndex(of: id(.tab))) | ||
| #expect(order[tabIndex + 1] == id(.escape)) | ||
| // Everything is shown on a fresh install, including the now-configurable | ||
| // modifiers/zoom/paste. | ||
| for action in TerminalInputAccessoryAction.configurableActions { | ||
| #expect(config.isEnabled(action.itemID)) | ||
| } | ||
| // Shift is never surfaced as a bar button. | ||
| #expect(!order.contains(id(.shift))) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add a behavior test for the fixed Shift key.
These assertions stop at displayOrder, which intentionally excludes .shift, so they would still pass if the accessory bar forgot to render the pinned Shift button. Please add a runtime check against the actual toolbar contents so this regression is caught next time.
As per coding guidelines, "When tests miss a bug, add or adjust behavior-level coverage around the exact repro path before claiming the fix is complete."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@ios/cmuxPackage/Tests/cmuxFeatureTests/TerminalAccessoryConfigurationTests.swift`
around lines 29 - 50, The test freshInstallDefaultOrder currently only checks
displayOrder (which excludes .shift) so add a runtime assertion against the
actual rendered accessory toolbar to catch regressions: after creating let
config = TerminalAccessoryConfiguration(defaults: freshDefaults()), instantiate
or request the real accessory view/controller that uses that config (the same
codepath used to render the input accessory) and collect its visible toolbar
item identifiers, then assert that the visible items do not contain id(.shift);
keep the existing displayOrder checks and use TerminalAccessoryConfiguration,
displayOrder, TerminalInputAccessoryAction.configurableActions and
config.isEnabled references to locate the test and the configuration.
Source: Coding guidelines
| private func populateAccessoryActions() { | ||
| guard let stack = accessoryStackView else { return } | ||
| for view in stack.arrangedSubviews { | ||
| stack.removeArrangedSubview(view) | ||
| view.removeFromSuperview() | ||
| } | ||
| commandAccessoryButton?.removeFromSuperview() | ||
| commandAccessoryButton = nil | ||
|
|
||
| // Pinned leading modifier controls, in fixed order. | ||
| for action in Self.pinnedLeadingActions { | ||
| let button = makeAccessoryButton(for: action) | ||
| // Command is Mac-only; kept out of the stack and inserted by | ||
| // applyModifierPresentation() when driving a Mac remote. | ||
| if action == .command { | ||
| commandAccessoryButton = button | ||
| } else { | ||
| stack.addArrangedSubview(button) | ||
| } | ||
| } | ||
| // The user-configurable region: built-in shortcuts and custom actions in | ||
| // the user's saved order. | ||
| // The user-configurable region: built-in shortcuts/modifiers/zoom/paste | ||
| // and custom actions, all in the user's saved order. | ||
| for item in TerminalAccessoryConfiguration.shared.enabledItems { | ||
| switch item { | ||
| case let .builtin(action): | ||
| // ⌘ only makes sense against a Mac remote; skip it otherwise | ||
| // (it stays in the saved order, just unrendered, so flipping the | ||
| // remote re-shows it in place). | ||
| if action == .command && !isMacRemote { continue } | ||
| stack.addArrangedSubview(makeAccessoryButton(for: action)) | ||
| case let .custom(custom): | ||
| stack.addArrangedSubview(makeCustomAccessoryButton(for: custom)) | ||
| } | ||
| } | ||
| // Pinned trailing zoom controls, after the configurable shortcuts (the | ||
| // redesigned bar moved zoom here from the leading region). | ||
| for action in Self.pinnedTrailingActions { | ||
| stack.addArrangedSubview(makeAccessoryButton(for: action)) | ||
| } | ||
| // The "customize" button pinned at the very end of the bar. | ||
| stack.addArrangedSubview(makeToolbarSettingsButton()) |
There was a problem hiding this comment.
Keep the fixed Shift button in the rebuilt bar.
populateAccessoryActions() now renders only enabledItems plus the trailing customize control. Because .shift is intentionally outside the configurable order, this rebuild drops the Shift key from the accessory bar entirely instead of keeping it as the fixed non-configurable control for this feature.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift`
around lines 201 - 223, populateAccessoryActions currently rebuilds the bar only
from TerminalAccessoryConfiguration.shared.enabledItems and the trailing
customize button, which drops the fixed Shift control; update
populateAccessoryActions to always add the fixed Shift accessory (use the
existing maker, e.g. makeShiftAccessoryButton or the Shift-specific view
factory) to the stack (in its fixed position) when rebuilding the
arrangedSubviews, then add the enabledItems (.builtin via makeAccessoryButton
and .custom via makeCustomAccessoryButton) and finally the settings button;
ensure you don't duplicate the Shift view if it's already present and that the
Shift button remains non-configurable and rendered regardless of enabledItems.
Every terminal accessory-bar button now sizes to its intrinsic content plus one shared compact horizontal inset, instead of being pinned wide. Width: - Tab/Esc/^C/^D and other text buttons were floored at accessoryButtonMinWidth (44pt via greaterThanOrEqualToConstant), so the short labels could not get narrower than 44pt. Modifier (⌃⌥⌘) and icon buttons (zoom/paste) were truly fixed at 44pt (equalToConstant). - All three creation paths (makeAccessoryButton, makeCustomAccessoryButton, makeToolbarSettingsButton) now use a single greaterThanOrEqualToConstant floor, lowered 44 -> 34 (tap-target minimum), so every button hugs its content and only floors when the glyph is tiny. - accessoryButtonContentInsets leading/trailing 10 -> 8 so the bar reads tight and uniform; this is what shrinks the already-wide "Tab". Icons: unify all SF Symbols under one shared symbol config. The keyboard toggle dropped from a private 16pt config to the shared 14pt config, and the arrow keys (↑↓←→) became arrow.up/down/left/right SF Symbols so they render at the same size as paste/zoom instead of as oversized Unicode glyphs. Arrow accessibility labels added (image-only buttons need them). Liquid Glass styling (#5536) and reorderable built-ins (#5579) are intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
The terminal accessory bar's modifier keys (⌃ ⌥ ⌘), zoom controls, and paste were structurally pinned outside the configurable region, so only the insertable shortcuts and custom actions could be reordered or hidden in toolbar settings. This folds those built-ins into the same reorderable region, so every bar button can be dragged to any position or hidden, alongside custom actions. (⇧ stays off the bar as before, and the trailing "customize" control is a fixed affordance.)
Liquid Glass styling (#5536) applies to every item regardless of position.
Stacking
Stacks on #5536 (
feat-ios-accessory-glass). Review/merge sequence: #5536 → this. The PR base is set tofeat-ios-accessory-glassso the diff is only this change. The "Esc right of Tab" default already landed on the base; this PR keeps it (and only adds the leading/trailing built-ins around it).How the data-driven toolbar + persistence works
PR #5510 made the bar data-driven: a unified
ToolbarItemID(.builtin(rawValue)|.custom(uuid)), a pureTerminalAccessoryLayoutReducer(order/enable/move over opaque ids), andTerminalAccessoryConfiguration(@Observable,UserDefaults-backed, postsdidChangeNotificationso the UIKit bar rebuilds live). The settings list (TerminalShortcutsSettingsView) already listsdisplayItemsand reorders via SwiftUIonMove. The only thing keeping modifiers/zoom/paste out was theisUserConfigurablepredicate plus fixedpinnedLeadingActions/pinnedTrailingActionslists in the UIKit builder.Model + migration
isUserConfigurableto include every action exceptshift. AdddefaultLeadingActions(⌃ ⌥ ⌘, paste) anddefaultTrailingActions(zoom), and rebuilddefaultConfigurableOrderso a fresh install renders modifiers/paste leading, zoom trailing.ToolbarLayoutMigration.widenedToV3(pure, inCmuxMobileTerminalKit). On a v1/v2 → v3 upgrade it force-enables the now-configurable built-ins and inserts them at their old fixed positions, so an upgrading user's bar looks unchanged. Force-enable is confined to the upgrade boundary: a v3 config's enabled set is authoritative, so hiding a modifier then persists. Init order: v3 (load as-is) → v2 (widen) → v1 (relabel + widen) → fresh.UIKit bar
populateAccessoryActionsrenders the configurable region inline (no pinned lists).⌘stays in the saved order but only renders for a Mac remote;updateModifierLabelsrepopulates on the flip.reconcileArmedModifierVisibilitydisarms any armed/sticky modifier whose button is no longer on the bar, so hiding an active modifier can't leave it invisibly modifying every keystroke.Tests
CmuxMobileTerminalKit(swift test, 77 pass): v3 widening transform — inserts/force-enables forced built-ins, preserves a partially-hidden shortcut set, handlesnilenabled, no duplicate when a forced id is already present, a custom action keeps its slot while modifiers fold in around it, and round-trips through the reducer.cmuxFeatureTests(simulator, CI): liveTerminalAccessoryConfiguration— fresh-install default order (modifiers front, zoom back, Esc after Tab, all shown), reorder + hide round-trips across reload, v2 → v3 migration with a partial enabled subset, migration of a config carrying a custom action (the custom keeps its slot while modifiers/zoom fold in), and that the upgraded config re-persists under v3 keys so hiding a modifier survives.Verification
iOS simulator app builds clean (
ios/scripts/reload.sh --tag tbarord). Kit tests pass locally;cmuxFeatureTestsrun in CI (local iOS test runs are disallowed).Localization
Footer (
mobile.shortcuts.footer) updated en+ja. New settings labels for the modifiers useterminal.shortcut.name.control/.alternate/.command(added en+ja); zoom reuses the existing localizedterminal.input_accessory.zoom_in/zoom_out; paste reusesterminal.input_accessory.paste. All resolve viaBundle.main(the app catalog), matching the package's existingString(localized:)pattern.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Touches persisted toolbar layout (v3 migration), live UIKit bar rebuild, and armed-modifier state when users hide keys—user-facing input behavior with migration edge cases covered by tests.
Overview
Makes every iOS terminal keyboard-bar button reorderable and hideable, including ⌃ ⌥ ⌘, paste, and zoom—not only insertable shortcuts and custom actions. The settings list and persisted layout now treat the whole bar as one region (⇧ and the trailing customize control stay fixed).
Persistence moves to UserDefaults v3 with
ToolbarLayoutMigration.widenedToV3: upgrading from v1/v2 force-enables and inserts previously pinned leading/trailing built-ins so the on-screen bar matches pre-upgrade, then v3 keys make hide/reorder authoritative.UIKit bar drops separate pinned leading/trailing stacks; it renders
TerminalAccessoryConfiguration.enabledItemsin user order (⌘ still omitted unless Mac remote).reconcileArmedModifierVisibilitydisarms modifiers whose buttons are hidden or not rendered so sticky modifiers cannot affect typing invisibly. Accessory button styling is centralized viaUIButton.Configuration(including Liquid Glass on iOS 26).Tests and copy: v3 migration and configuration behavior tests; settings footer and modifier labels localized (en/ja).
Reviewed by Cursor Bugbot for commit 1ac9735. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Make every iOS terminal accessory bar button reorderable and hideable, including ⌃ ⌥ ⌘, Esc/Tab, Paste, and Zoom. Also adopts real iOS 26 Liquid Glass styling for buttons, with a solid-fill fallback on earlier iOS.
New Features
Migration
CmuxMobileTerminal/CmuxMobileTerminalKit.Written for commit 1ac9735. Summary will update on new commits.
Summary by CodeRabbit
New Features
Changes
Improvements