Skip to content

iOS: hidden native text input for working hold-to-repeat backspace + dictation (supersedes the hasText approach) - #5596

Closed
lawrencecchen wants to merge 5 commits into
mainfrom
feat-ios-hidden-native-input
Closed

lawrencecchen wants to merge 5 commits into
mainfrom
feat-ios-hidden-native-input

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Why

PR #5573 tried to give the iOS terminal native hold-to-repeat backspace and system dictation by overriding hasText to return true on the existing TerminalInputTextView (a UITextView subclass), copying iSH's "it's always ok to send a delete" trick. It shipped but on a real device neither hold-to-repeat backspace nor dictation worked. (Its other half, multi-line paste via terminal.paste, does work and is kept.)

Real reason hasText = true failed

TerminalInputTextView was a UITextView that forced hasText = true but cleared its document to "" after every committed keystroke (the buffer-mirror / TerminalTextInputPipeline round-trip). A UITextView drives backspace auto-repeat and dictation off its internal text storage and selection, not off UIKeyInput.hasText:

  • Hold-to-repeat stopped after one delete. With an empty document and a collapsed selection at offset 0, UIKit had nothing to repeat-delete, and a UITextView never consults the overridden hasText for repeat. (The hasText override is only honored by a raw UIKeyInput responder, which is what iSH actually is.)
  • Dictation never landed. Dictation inserts a placeholder into the document then replaces it with the recognized text, but the per-keystroke text = "" clear nuked the placeholder mid-recognition.

iSH's TerminalView is not a UITextView at all: it is a bare UIView conforming to UIKeyInput + UITextInput with no real document. That is why both features work there.

Fix: documentless UIKeyInput + UITextInput responder

TerminalInputTextView is now UIView, UIKeyInput, UITextInput with no editable document, the same construction iSH ships. On a raw responder the keyboard honors hasText for repeat, and the explicit dictation-placeholder methods on a real (non-cleared) UITextInput conformer let recognized text arrive through insertText as one block.

Why this produces native repeat + dictation on device where the old approach did not: the auto-repeat timer fires deleteBackward as long as a raw first responder reports hasText == true (now a constant), and there is no document/selection state machine fighting it; dictation's placeholder hook returns a token and the recognized text comes back through insertText with nothing to clear it mid-flight.

How input is forwarded to the terminal (byte semantics unchanged from #5573):

  • Single typed character / Return: insertText -> onText -> terminal.input (CR for Return, control bytes preserved).
  • Backspace (incl. hold-to-repeat): deleteBackward -> onBackspace -> \u{7F}. Ctrl/Alt/Cmd+Backspace ladders intact (Cmd+Backspace = Ctrl+U 0x15, etc.).
  • Dictation / autocorrect-style block / emoji / keyboard-clipboard insert: multi-character insertText (or replace(_:withText:)) -> onPasteText -> the existing terminal.paste bracketed-paste RPC, so embedded newlines do not CR-fragment.
  • Cmd+V / edit-menu Paste: re-added explicitly (a bare UIView does not inherit UITextView's paste), routed through the same clipboard handling as the toolbar Paste button.
  • Tab 0x09, Shift-Tab ESC [ Z, arrows, control/alt/cmd ladders, and the toolbar/modifier state machine are unchanged.

Marked text (IME composition) is hand-rolled (markedText string + opaque UITextRange/UITextPosition sentinels). Marked-text mutations are bracketed with the UITextInputDelegate textWillChange/textDidChange callbacks so the keyboard/dictation machinery stays synchronized.

Known limitation (documented, by design)

A documentless view cannot decompose a CJK syllable itself. Composition edits are normally driven by the IME through setMarkedText (the IME owns its floating candidate bar; this invisible view's markedText just mirrors it). In the rare case where an IME routes composition-backspace through deleteBackward instead, this view cancels the in-progress composition rather than corrupting it (modeling 한 as a Swift Character and dropping it would nuke the whole syllable). Autocorrect/predictive text stay disabled and cannot be enabled: they require retaining the in-progress word, which is incompatible with forwarding every keystroke to a remote terminal. Emoji and IME composition still work.

Relationship to #5573

This supersedes #5573's input-conformance part (the hasText override) and keeps #5573's terminal.paste RPC and bracketed-paste routing. It is built on #5573's branch, so this PR carries those commits too; merging this delivers working repeat + dictation + paste together.

Verification

iOS simulator Debug build is clean (arm64). The simulator cannot exercise hold-to-repeat or dictation, so those are device-verified by the dogfood checklist below. Autoreview converged clean (documentless responder is internally coherent; paste routes restored; marked-text delegate callbacks corrected).

Device dogfood checklist

  • Hold backspace in a terminal: it should auto-repeat (delete many chars), not stop after one.
  • Tap the keyboard mic and dictate a phrase: it should land as one block (one bracketed paste), not drop.
  • Emoji from the emoji keyboard: inserts normally.
  • Cmd+V (hardware keyboard) and edit-menu Paste: pastes clipboard into the terminal.
  • IME (Korean/CJK/Japanese): compose and commit normally. Compose Korean (ㅎ -> 하 -> 한) and backspace mid-syllable: if your IME drives it via setMarkedText it decomposes; if it routes through deleteBackward the composition cancels. Report if cancel is disruptive.
  • Confirm the composer, toolbar shortcuts, modifier ladders, and multi-line paste still work.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes the core remote terminal input path and adds a new Mac RPC handler; risk is mitigated by capability detection, per-key fallback on old hosts, and unchanged single-character/modifier semantics.

Overview
Replaces the iOS terminal keyboard proxy TerminalInputTextView from a UITextView that cleared its document after every keystroke with a documentless UIView conforming to UIKeyInput + UITextInput (iSH-style). That change is what makes hold-to-repeat backspace honor hasText and lets dictation complete via placeholder/insertText without mid-flight clears. IME composition is tracked only in transient markedText with sentinel UITextPosition/UITextRange types; Cmd+V and toolbar paste are re-wired on the bare view.

Multi-character commits (dictation, clipboard paste, etc.) are classified by new TerminalCommitRouter (>1 Character → paste) and forwarded through onPasteText → submitTerminalPasteText, which calls the Mac terminal.paste RPC when terminal.paste.v1 is advertised (otherwise newline-normalized terminal.input). The Mac adds v2MobileTerminalPaste, advertises the capability in mobile.host.status, and includes paste in attach-ticket terminal auth; RPC client auth routing treats paste like other terminal methods.

Per-keystroke typing, modifiers, and image paste paths are unchanged aside from routing clipboard text through bracketed paste instead of per-character input.

Reviewed by Cursor Bugbot for commit 98e54b7. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Enables native hold-to-repeat backspace and system dictation on iOS by replacing the terminal input with a hidden, documentless UIView that implements UIKeyInput/UITextInput. Multi-character commits (dictation, autocorrect, keyboard paste) now go through a new terminal.paste RPC with capability gating and safe fallback.

  • New Features

    • Hold-to-repeat backspace works by reporting hasText = true on a raw responder.
    • Dictation uses UITextInput placeholder hooks and commits as one block.
    • Bracketed paste via terminal.paste RPC; capability terminal.paste.v1 is consumed. Falls back to terminal.input with newline→CR when unsupported.
    • GhosttySurfaceView adds didPasteText; accessory Paste and Cmd+V route through the same path.
    • Existing modifier ladders, arrows, Tab/Shift-Tab, and image paste remain unchanged.
  • Refactors

    • TerminalInputTextView → documentless UIView conforming to UIKeyInput/UITextInput; no local buffer; IME marked text stored; custom TerminalInputTextRange/TerminalInputTextPosition.
    • Routing: single chars/Return via onText; multi-char blocks via onPasteText using TerminalCommitRouter (in CmuxMobileTerminalKit) with tests.
    • Host/client wiring for terminal.paste: RPC allowlist in MobileCoreRPCClient/MobileHostService, handler in TerminalController, capability reset on reconnect.
    • Input view removed from accessibility tree; keyboard traits disable autocorrect/predictive.

Written for commit 98e54b7. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

New Features

  • Added dedicated terminal paste functionality with automatic capability detection for paired Macs
  • Multi-character text pastes now route through optimized bracketed paste delivery to preserve formatting and reduce input buffering issues
  • Enhanced terminal input system to properly handle paste events from dictation, autocorrect, and clipboard operations with appropriate newline normalization

lawrencecchen and others added 5 commits June 7, 2026 05:41
…xt-input conformance)

Make the iOS terminal input view behave like a real text input so the system
software keyboard drives backspace auto-repeat and dictation natively, the way
iSH's terminal does, without introducing local-echo drift (keystrokes still
forward to the Mac).

iSH technique (app/TerminalView.m): its `hasText` returns YES unconditionally
("it's always ok to send a delete"), so the keyboard's auto-repeat timer keeps
firing `deleteBackward` while backspace is held. cmux kept a perpetually-empty
document, so the inherited `UITextView.hasText` was false and the repeat stopped
after one delete.

- Override `hasText` -> true so the keyboard auto-repeats backspace.
- Decouple `deleteBackward` routing from `hasText` (now a constant): key off
  `markedTextRange` (IME composition) instead, so backspace and the
  Ctrl/Alt/Cmd+Backspace ladders still reach the Mac.
- Route committed multi-character blocks (dictation, autocorrect, keyboard
  clipboard insert) through a new bracketed-paste path so embedded newlines are
  not CR-fragmented. Single chars and Return stay on per-key `terminal.input`.
- Add `terminal.paste` text RPC (Mac side routes to `sendText` ->
  `ghostty_surface_text`, the bracketed-paste path) wired through every gate:
  TerminalController dispatch + handler, MobileHostService ticket auth,
  capabilities, and the iOS RPC client allowlist + MobileShellComposite.
- Extract a pure `TerminalCommitRouter` (input-vs-paste) into
  CmuxMobileTerminalKit with unit tests.

Dictation note: iSH hand-rolls `insertDictationResultPlaceholder` /
`removeDictationResultPlaceholder` only because it is a raw UIView conforming to
UITextInput. This view is a UITextView, which already satisfies those UITextInput
requirements as framework witnesses that are not exposed for override (the
compiler rejects `override` on them). So we inherit iSH's placeholder plumbing
for free; recognized dictation text arrives via insertText/textViewDidChange and
routes through the bracketed-paste sink.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…through it

Address two autoreview P1 findings:

- Consume the new `terminal.paste.v1` capability from `mobile.host.status` in
  MobileShellComposite (supportsTerminalPaste) and fall back to per-key
  `terminal.input` (CR-normalized) when a new client pairs with an older Mac, so
  dictation/predictive/clipboard text is never dropped with method_not_found.
- Route the visible accessory Paste button's clipboard text through the same
  bracketed-paste sink (onPasteText) instead of onText, so multi-line clipboard
  text no longer fragments into per-line Returns. One shared paste entrypoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address autoreview P2: resetTerminalOutputTracking() (fired from
remoteClient.didSet when the client clears, on every reconnect) now also clears
supportsTerminalPaste, so reconnecting from a paste-capable host to an older one
cannot send terminal.paste on a stale true before the next status probe lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Aziz documentation policy: expand the default GhosttySurfaceViewDelegate
didPasteText extension to a proper Swift-DocC comment (summary + parameters).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… working hold-to-repeat backspace + dictation

Supersedes the hasText-override approach from PR #5573, which shipped but did
not deliver native hold-to-repeat backspace or system dictation on device. Keeps
#5573's terminal.paste RPC and bracketed-paste routing intact.

Root cause of the prior failure: TerminalInputTextView was a UITextView that
forced hasText=true but cleared its document to "" after every keystroke. A
UITextView drives backspace auto-repeat and dictation off its internal text
storage/selection, not off UIKeyInput.hasText, so:
- hold-to-repeat stopped after one delete (empty doc, collapsed selection at
  offset 0 = nothing to repeat-delete; UITextView never consults the overridden
  hasText for repeat),
- dictation never landed (the per-keystroke text="" clear nuked the dictation
  placeholder mid-recognition).

Fix: make the view a bare UIView conforming to UIKeyInput + UITextInput directly
with no document, the same construction iSH's TerminalView ships for this exact
purpose. A raw responder honors hasText for repeat, and the explicit dictation
placeholder methods on a real (non-cleared) UITextInput conformer let recognized
text arrive via insertText as one block.

- TerminalInputTextView: UITextView -> UIView, UIKeyInput, UITextInput.
- hasText -> true; insertText routes straight to the terminal (single char via
  onText, multi-char block via the existing bracketed-paste onPasteText);
  deleteBackward keeps the Ctrl/Alt/Cmd ladder + onBackspace (0x7F).
- Hand-rolled marked-text storage (markedText string) with
  setMarkedText/unmarkText/markedTextRange; IME routing keys off markedText.
- Full UITextInput stub conformance (neutral offsets/rects) + dictation
  placeholder hooks; keyboard traits as computed UITextInputTraits.
- Opaque TerminalInputTextPosition / TerminalInputTextRange sentinels (one file
  each per file-organization policy); identity-compared by UIKit.
- Removed the buffer-mirror/TerminalTextInputPipeline round-trip and the
  UITextViewDelegate; adapted the test seam.
- Autocorrect/predictive stay disabled (incompatible with per-keystroke remote
  forwarding). Emoji and IME composition still work via insertText.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 8, 2026 2:43am
cmux-staging Building Building Preview, Comment Jun 8, 2026 2:43am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements bracketed-paste support for terminal text input across the iOS and Mac RPC layers. The change refactors TerminalInputTextView from UITextView to a documentless UIView, adds text routing logic to classify commits as per-character input or multi-character paste, and exposes a new terminal.paste RPC endpoint with capability detection and fallback for older hosts.

Changes

Terminal bracketed-paste flow

Layer / File(s) Summary
Text routing classification
Packages/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/TerminalCommitRoute.swift, TerminalCommitRouter.swift, Tests/CmuxMobileTerminalKitTests/TerminalCommitRouterTests.swift
TerminalCommitRoute enum and TerminalCommitRouter classifier determine whether committed text routes as single-character .input or multi-character .paste based on grapheme cluster count.
UITextInput documentless foundation
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextPosition.swift, TerminalInputTextRange.swift
Sentinel UITextPosition and UITextRange implementations support the documentless text input model without traditional document offsets or geometry.
TerminalInputTextView documentless refactor
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
Refactors from UITextView to a UIView implementing UIKeyInput and UITextInput directly; maintains transient IME markedText state and routes all committed text via centralized emitUnmodifiedText using TerminalCommitRouter to classify single vs. multi-character blocks.
GhosttySurfaceView paste delegate hook
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
Extends GhosttySurfaceViewDelegate with ghosttySurfaceView(_:didPasteText:) callback; default implementation normalizes newlines to CR and forwards through existing didProduceInput path. Wires TerminalInputTextView.onPasteText to the new delegate method.
MobileShellComposite terminal paste orchestration
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Detects terminal.paste.v1 capability and maintains supportsTerminalPaste flag; implements submitTerminalPasteText public API with fallback to newline-normalized raw input when host doesn't support the new RPC; adds sendRemoteTerminalPasteText RPC wiring with response handling.
Shell UI paste text forwarding
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
Coordinator delegate method ghosttySurfaceView(_:didPasteText:) forwards committed text to store?.submitTerminalPasteText via @MainActor task.
RPC backend authorization and dispatch
Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift, Sources/Mobile/MobileHostService.swift, Sources/TerminalController.swift
MobileCoreRPCClient includes paste methods in auth-fallback routing; MobileHostService advertises terminal.paste.v1 capability and exempts paste requests from ticket authorization; TerminalController routes terminal.paste RPC to v2MobileTerminalPaste handler, which validates parameters, resolves terminal surface, and injects pasted text via sendText path with queue-full error handling.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5546: Both PRs update MobileCoreRPCClient.requestNeedsStackAuthFallback to exempt paste-related RPCs from attach-ticket auth-fallback logic; this PR handles text paste while the related PR targets image paste.

🐰 A view once typed all alone,
Now pastes text in bracketed tone,
With routes and routers made so clean,
And sentinels in between,
The terminal hops with typing thrown! 📱✨


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error Nested full-collection scans in hot paste path: submitTerminalRawInput and submitTerminalPasteText use first(where:) + contains(where:), O(W*T), called from UI delegate on every paste/dictation event. Add surfaceID→(workspace,terminal) index maintained during workspace/terminal updates for O(1) lookup.
Cmux Swift File And Package Boundaries ❌ Error TerminalInputTextView grew from ~907 to 1181 lines (+274 net, +363 added) violating the rule against adding >250 lines to existing files over 800 lines without shrinking by 200+. Extract UITextInput protocol, IME state, or accessory toolbar UI to new package to reduce TerminalInputTextView below the 800-line threshold or satisfy the >200-line shrink exception.
Docstring Coverage ⚠️ Warning Docstring coverage is 27.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: refactoring TerminalInputTextView to a documentless UIView responder to enable hold-to-repeat backspace and dictation, superseding the failed hasText approach.
Description check ✅ Passed The description is comprehensive and well-structured, covering the problem, solution, implementation details, known limitations, device verification, and dogfood checklist. It exceeds template requirements with thorough technical context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Blocking Runtime ✅ Passed Core iOS terminal input files introduce no blocking patterns. GhosttySurfaceView's NSLock and DispatchSemaphore have documented carve-outs; Task.sleep uses proper async timeout racing.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift iOS code. Check scope is TypeScript/JavaScript/shell; Swift sleeps excluded per separate rule. No non-Swift runtime code affected.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns. Fire-and-forget Task is only in UIKit delegate callback boundary, which is an allowed exception per rules.
Cmux Swift @Concurrent ✅ Passed New async methods follow established codebase pattern (like scrollTerminal) for fire-and-forget UI action forwarding, intentionally bounded to @MainActor via explicit Task wrapping in delegates.
Cmux Swift Logging ✅ Passed PR adds no print/NSLog/debugPrint/dump in production code. Only DEBUG-guarded cmuxDebugLog used in v2MobileTerminalPaste. No new Logger violations or secrets exposure.
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages in the PR comply with user-facing error privacy rules: generic, safe, no vendor names, credentials, or implementation details exposed.
Cmux Full Internationalization ✅ Passed New user-facing text uses String(localized:) with complete translations (en+ja) in catalog. New internal files have no user-facing text. Backend changes use literal protocol tokens.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations found. MobileShellComposite uses modern @Observable with plain Bool properties. GhosttySurfaceRepresentable is UIKit bridge (allowed). Most changes are UIKit, not SwiftUI.
Cmux Architecture Rethink ✅ Passed No timing repairs, locks, or split lifecycle. Documentless UIView owned by GhosttySurfaceView, centralized paste routing, pure router, single capability gate point.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies iOS UIView/UIKeyInput code only; no NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup declarations are added or changed.
Cmux Source Artifacts ✅ Passed All 12 changed files are hand-written Swift source/test files in standard package directories with no artifact patterns; consistent with PR objectives for terminal input feature.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-hidden-native-input
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat-ios-hidden-native-input

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 98e54b7. Configure here.

guard let composing = markedText else { return }
withMarkedTextChange { markedText = nil }
emitCommittedText(composing, source: "unmarkText")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unmarkText emits duplicate IME

High Severity

unmarkText() forwards the marked composition to the remote terminal via emitCommittedText, but UIKit treats unmarkText as clearing IME state only; committed text is delivered separately through insertText or replace. That can duplicate characters on commit or send in-progress syllables when the user cancels composition.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 98e54b7. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the UITextView-based TerminalInputTextView with a documentless UIView + UIKeyInput + UITextInput responder, mirroring iSH's architecture, to fix hold-to-repeat backspace and system dictation on real iOS devices. It also wires a new terminal.paste RPC end-to-end (iOS → Mac) so multi-character input from dictation, autocorrect, and Cmd+V arrives as a single bracketed paste rather than fragmented per-key input.

  • Documentless responder (TerminalInputTextView): UITextView subclass dropped; bare UIView + UIKeyInput + hand-rolled UITextInput conformance added. hasText returns true unconditionally so the keyboard's auto-repeat timer keeps firing; IME composition is tracked via a markedText string and two opaque sentinel ranges. Dictation placeholder hooks are implemented so recognized text arrives through insertText as one block.
  • terminal.paste RPC: TerminalCommitRouter classifies committed text (single char → terminal.input, multi-char block → terminal.paste); MobileShellComposite.submitTerminalPasteText adds a capability-gated call to the new terminal.paste RPC with a terminal.input fallback for older Mac hosts; TerminalController.v2MobileTerminalPaste on the Mac side routes through sendText/ghostty_surface_text for native bracketed-paste framing.

Confidence Score: 4/5

The core input-responder refactor is architecturally sound and closely follows the iSH pattern. All new RPC paths have capability-gated fallbacks for older Mac hosts. The two findings are non-blocking: the file-size overage is a house-rule cleanup, and the composition-not-cleared edge in replace(_:withText:) requires two mutually exclusive input modes to be active simultaneously and is practically unreachable given the disabled autocorrect/predictive-text configuration.

The documentless responder is internally coherent — hasText, markedText, sentinel ranges, and the delegate callbacks are wired consistently. The RPC capability flag is correctly reset on disconnect. The replace(_:withText:) gap is a theoretical path (autocorrect off, predictive off, simultaneous IME + accessibility text injection) that won't surface in normal use. The only concrete rule violation is the file size, which is addressable post-merge.

TerminalInputTextView.swift warrants a second look for the replace(_:withText:) composition-clearing gap and for the 800-line rule overage; all other files are straightforward additions or small wiring changes.

Important Files Changed

Filename Overview
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift Core refactor: UITextView subclass replaced by UIView + UIKeyInput + UITextInput documentless responder. The new design correctly enables hold-to-repeat backspace and dictation, but the file grows to 1181 lines (over the 800-line rule), and replace(_:withText:) leaves composition state set when replacing selectedTextRange during active IME.
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextPosition.swift New sentinel UITextPosition subclass. Empty class used only for UITextInput identity comparisons; well-documented and correctly sized.
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextRange.swift New sentinel UITextRange subclass with identity-based dispatch. Both start/end return the same position sentinel and isEmpty is always true, consistent with offset(from:to:) returning 0 and the documentless design.
Packages/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/TerminalCommitRouter.swift New pure transform deciding input vs. paste routing by grapheme cluster count. Well-isolated, testable without UIKit, and correctly treats emoji/flag as single graphemes.
Packages/CmuxMobileTerminalKit/Tests/CmuxMobileTerminalKitTests/TerminalCommitRouterTests.swift New test suite for TerminalCommitRouter. Good coverage of single char, Return, empty string, word, multiline, single emoji, and two-emoji cases.
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds supportsTerminalPaste capability flag and submitTerminalPasteText/sendRemoteTerminalPasteText methods. Flag is reset on disconnect to prevent stale-capability sends to older hosts. Logging uses proper OSLog with privacy levels.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift Adds ghosttySurfaceView(_:didPasteText:) delegate forward. Consistent with existing coordinator pattern; Task {@mainactor [weak store]} usage matches surrounding handlers.
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift Adds onPasteText callback and optional protocol extension with a per-character fallback. Default implementation is backward-compatible for conformers that don't implement the bracketed-paste path.
Sources/TerminalController.swift Adds v2MobileTerminalPaste handler and registers mobile.terminal.paste/terminal.paste in the RPC dispatch table and requiredRPCMethods. Routes through sendText (ghostty_surface_text) for native bracketed-paste framing.
Sources/Mobile/MobileHostService.swift Adds terminal.paste.v1 to mobileHostCapabilities and terminal.paste to the RPC routing table. Symmetrical with existing method registrations.
Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift Adds terminal.paste to the requiresActiveTerminal RPC list, consistent with terminal.input and paste_image.
Packages/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/TerminalCommitRoute.swift New Sendable enum with .input and .paste cases. Minimal, correct, well-documented.

Sequence Diagram

sequenceDiagram
    participant KB as iOS Keyboard
    participant TV as TerminalInputTextView
    participant GS as GhosttySurfaceView
    participant MSC as MobileShellComposite
    participant RPC as MobileCoreRPCClient
    participant TC as TerminalController (Mac)

    Note over KB,TV: Single character typed
    KB->>TV: insertText("a")
    TV->>GS: onText("a") → didProduceInput
    GS->>MSC: submitTerminalRawInput
    MSC->>RPC: terminal.input

    Note over KB,TV: Hold backspace (auto-repeat)
    KB->>TV: "deleteBackward() [repeating, hasText==true]"
    TV->>GS: onBackspace → didProduceInput(0x7F)
    GS->>MSC: submitTerminalRawInput

    Note over KB,TV: Dictation / multi-char block
    KB->>TV: insertDictationResultPlaceholder()
    TV-->>KB: "" (token)
    KB->>TV: insertText("recognized phrase")
    TV->>GS: onPasteText → didPasteText
    GS->>MSC: submitTerminalPasteText
    alt supportsTerminalPaste
        MSC->>RPC: terminal.paste
        RPC->>TC: v2MobileTerminalPaste
        TC->>TC: sendText (ghostty_surface_text)
    else fallback (old Mac host)
        MSC->>RPC: terminal.input (newlines→CR)
    end

    Note over KB,TV: IME composition (CJK)
    KB->>TV: setMarkedText("한", ...)
    TV->>TV: "markedText = "한""
    KB->>TV: insertText("한")
    TV->>TV: clear markedText
    TV->>GS: onText("한") [single char]
Loading

Reviews (1): Last reviewed commit: "iOS terminal input: documentless UIKeyIn..." | Re-trigger Greptile

Comment on lines +1068 to 1181
// MARK: - UITextInput (documentless conformance)

// This view owns no editable document. It implements `UITextInput` to unlock two
// keyboard features that a bare `UIKeyInput` view does not get — system
// dictation (the mic key) and IME marked-text composition — exactly the way
// iSH's `TerminalView` does. Recognized dictation text and committed IME
// candidates both arrive through ``insertText(_:)`` and route to the terminal;
// the geometry/offset methods return neutral values because there is nothing to
// measure. UIKit compares the marked/selected ranges by object identity.
extension TerminalInputTextView {
var markedTextRange: UITextRange? { markedText != nil ? markedTextRangeSentinel : nil }

var selectedTextRange: UITextRange? {
get { selectedTextRangeSentinel }
set {}
}

func textViewDidChange(_ textView: UITextView) {
handleTextChange(
currentText: textView.text ?? "",
isComposing: textView.markedTextRange != nil
)
var markedTextStyle: [NSAttributedString.Key: Any]? {
get { nil }
set {}
}

var beginningOfDocument: UITextPosition { TerminalInputTextPosition() }
var endOfDocument: UITextPosition { TerminalInputTextPosition() }

/// The IME hands a candidate string in; hold it as the marked composition so
/// ``markedTextRange`` reports active composition. Nothing is sent to the
/// terminal until the candidate commits via ``insertText(_:)`` (driven by
/// the keyboard) or ``unmarkText()``.
///
/// Mutating ``markedText`` changes the string the view exposes through
/// ``text(in:)``/``markedTextRange``, so it is a *text* change in the
/// ``UITextInputDelegate`` contract: it is bracketed with
/// `textWillChange`/`textDidChange` (via ``withMarkedTextChange(_:)``) so the
/// IME and dictation machinery keep their composition state synchronized.
func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
TerminalInputDebugLog.log("proxy.setMarkedText text=\(TerminalInputDebugLog.textSummary(markedText ?? "")) ")
withMarkedTextChange {
self.markedText = (markedText?.isEmpty == true) ? nil : markedText
}
}

/// Brackets a mutation of ``markedText`` with the `UITextInputDelegate`
/// text-change callbacks.
///
/// The marked composition is the only text this view exposes, so any change
/// to it (set by the IME, committed by ``insertText(_:)``/``unmarkText()``,
/// or canceled by ``replace(_:withText:)``) is a text change UIKit must be
/// told about with `textWillChange`/`textDidChange`. Selection-only callbacks
/// would leave the keyboard observing stale composition state.
private func withMarkedTextChange(_ mutate: () -> Void) {
inputDelegate?.textWillChange(self)
mutate()
inputDelegate?.textDidChange(self)
}

/// Commit the in-progress IME composition. Forwards the held candidate to the
/// terminal as one block (multi-character commits route to bracketed paste).
func unmarkText() {
guard let composing = markedText else { return }
withMarkedTextChange { markedText = nil }
emitCommittedText(composing, source: "unmarkText")
}

func text(in range: UITextRange) -> String? {
if range === markedTextRangeSentinel { return markedText }
if range === selectedTextRangeSentinel { return "" }
return nil
}

/// Commit text delivered through a range replacement.
///
/// Most committed input arrives via ``insertText(_:)``, but some system
/// paths (text replacement, certain dictation/suggestion commits) deliver it
/// by replacing ``selectedTextRange`` or ``markedTextRange`` instead. The
/// view holds no addressable document, so the range itself is ignored, but
/// the *text* must still reach the terminal — route it through the same
/// commit path as ``insertText(_:)`` rather than dropping it. A replacement
/// of the marked region also supersedes the in-progress IME composition, so
/// clear it first. An empty replacement is a pure deletion of the marked
/// composition (no committed text to send).
func replace(_ range: UITextRange, withText text: String) {
TerminalInputDebugLog.log("proxy.replace text=\(TerminalInputDebugLog.textSummary(text)) marked=\(range === markedTextRangeSentinel)")
if range === markedTextRangeSentinel, markedText != nil {
withMarkedTextChange { markedText = nil }
}
guard !text.isEmpty else { return }
emitCommittedText(text, source: "replace")
}
func textRange(from fromPosition: UITextPosition, to toPosition: UITextPosition) -> UITextRange? { nil }
func position(from position: UITextPosition, offset: Int) -> UITextPosition? { nil }
func position(from position: UITextPosition, in direction: UITextLayoutDirection, offset: Int) -> UITextPosition? { nil }
func compare(_ position: UITextPosition, to other: UITextPosition) -> ComparisonResult { .orderedSame }
func offset(from: UITextPosition, to toPosition: UITextPosition) -> Int { 0 }
func position(within range: UITextRange, farthestIn direction: UITextLayoutDirection) -> UITextPosition? { nil }
func characterRange(byExtending position: UITextPosition, in direction: UITextLayoutDirection) -> UITextRange? { nil }
func baseWritingDirection(for position: UITextPosition, in direction: UITextStorageDirection) -> NSWritingDirection { .leftToRight }
func setBaseWritingDirection(_ writingDirection: NSWritingDirection, for range: UITextRange) {}
func firstRect(for range: UITextRange) -> CGRect { .zero }
func caretRect(for position: UITextPosition) -> CGRect { .zero }
func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { [] }
func closestPosition(to point: CGPoint) -> UITextPosition? { nil }
func closestPosition(to point: CGPoint, within range: UITextRange) -> UITextPosition? { nil }
func characterRange(at point: CGPoint) -> UITextRange? { nil }

// MARK: Dictation placeholder hooks
//
// UIKit calls these when the mic is tapped. Returning a placeholder (an
// empty token; iSH does the same) is what tells the framework this view
// accepts dictation; the recognized text then arrives via `insertText`. The
// remove hook is a no-op because there is no document placeholder to strip.
func insertDictationResultPlaceholder() -> Any { "" }
func removeDictationResultPlaceholder(_ placeholder: Any, willInsertResult: Bool) {}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 File exceeds 800-line limit

TerminalInputTextView.swift is now 1181 lines, against a prior baseline of ~908 lines, and both are over the 800-line threshold flagged by the file-package-boundaries rule. The UITextInput documentless conformance section (lines 1068–1181) and the UITextInputTraits extension (lines 1050–1066) are independently comprehensible units with no private member access that can't be exposed through internal. Splitting them into TerminalInputTextView+UITextInput.swift and TerminalInputTextView+UITextInputTraits.swift would bring the core responder file under the limit without any logic change.

Rule Used: Flag Swift changes that add too much unrelated res... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +1149 to 1156
func replace(_ range: UITextRange, withText text: String) {
TerminalInputDebugLog.log("proxy.replace text=\(TerminalInputDebugLog.textSummary(text)) marked=\(range === markedTextRangeSentinel)")
if range === markedTextRangeSentinel, markedText != nil {
withMarkedTextChange { markedText = nil }
}
guard !text.isEmpty else { return }
emitCommittedText(text, source: "replace")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Composition not cleared when replacing selectedTextRange during IME

replace(_:withText:) correctly clears markedText when range === markedTextRangeSentinel, but not when range === selectedTextRangeSentinel while markedText != nil. If a system path (e.g., an accessibility "type text" action) calls replace(selectedTextRange, withText:) during active IME composition, emitCommittedText fires immediately while markedText remains set. The IME will later commit its candidate a second time via insertText or unmarkText, producing a double-send to the terminal. In practice this requires simultaneous use of two input modes and is very unlikely given autocorrectionType = .no, but a defensive if markedText != nil { withMarkedTextChange { markedText = nil } } before the emitCommittedText call would make the behavior symmetric with the markedTextRangeSentinel branch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift (1)

5-1181: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Split TerminalInputTextView.swift; the touched production file violates the repository size policy.

This file is now well past the hard 800-line threshold and carries multiple responsibilities (input transport policy, toolbar UI wiring, modifier-state application, paste/image flows, and UITextInput protocol surface) in one unit, which materially increases regression risk and review cost.

As per coding guidelines, "Flag Swift production files that exceed 400 lines without a clear single responsibility, or exceed 800 lines even with mostly coherent responsibility."

🤖 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 5 - 1181, The file TerminalInputTextView.swift is >800 lines and
mixes UI, input protocol conformance, accessory toolbar, modifier handling, and
paste logic; split it into focused files: keep the core responder class
TerminalInputTextView with only high-level wiring and methods like
insertText(_:), deleteBackward(), emitCommittedText(_:source:), and modifier
toggles, then move accessory toolbar construction (terminalAccessoryToolbar,
populateAccessoryActions(), makeAccessoryButton(...),
applyModifierPresentation(), updateAccessoryLayoutInsets()) into a new
TerminalAccessoryToolbar (or TerminalInputToolbar) file/class, move paste
handling (handlePasteAction()) into TerminalInputPasteHandler, and extract
UITextInput conformance plumbing (markedText, setMarkedText(_:selectedRange:),
withMarkedTextChange(_:), unmarkText(), replace(_:withText:), text(in:) and
dictation hooks) into TerminalInputTextView+UITextInput.swift; ensure shared
helpers (AccessoryActionButton usage, onPasteText/onPasteImage/onEscapeSequence
callbacks, modifierState references like modifierState.tap/consumeIfNotSticky
and methods toggleControlModifier/toggleAlternateModifier) remain accessible
(adjust visibility or add internal APIs) and update Notification observer
registration to the initializer in the core class. Keep all symbol names
unchanged so callers (e.g. handleAccessoryButton(_:), handleAccessoryAction(_:),
applyModifierPresentation(), emitCommittedText(_:source:), insertText(_:),
setKeyboardShown(_:)) still link; run a build and fix access control/imports
after the refactor.

Source: Coding guidelines

🤖 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 `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1462-1464: The current fallback branch in MobileShellComposite
(guard supportsTerminalPaste -> submitTerminalRawInput) naively replaces "\n"
with "\r" causing Windows CRLF ("\r\n") to become "\r\r"; first normalize CRLF
by replacing occurrences of "\r\n" with a single "\r", then replace any
remaining "\n" with "\r" before calling submitTerminalRawInput (refer to
symbols: supportsTerminalPaste, text, normalized, submitTerminalRawInput,
workspace.id, terminalID).

---

Outside diff comments:
In
`@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift`:
- Around line 5-1181: The file TerminalInputTextView.swift is >800 lines and
mixes UI, input protocol conformance, accessory toolbar, modifier handling, and
paste logic; split it into focused files: keep the core responder class
TerminalInputTextView with only high-level wiring and methods like
insertText(_:), deleteBackward(), emitCommittedText(_:source:), and modifier
toggles, then move accessory toolbar construction (terminalAccessoryToolbar,
populateAccessoryActions(), makeAccessoryButton(...),
applyModifierPresentation(), updateAccessoryLayoutInsets()) into a new
TerminalAccessoryToolbar (or TerminalInputToolbar) file/class, move paste
handling (handlePasteAction()) into TerminalInputPasteHandler, and extract
UITextInput conformance plumbing (markedText, setMarkedText(_:selectedRange:),
withMarkedTextChange(_:), unmarkText(), replace(_:withText:), text(in:) and
dictation hooks) into TerminalInputTextView+UITextInput.swift; ensure shared
helpers (AccessoryActionButton usage, onPasteText/onPasteImage/onEscapeSequence
callbacks, modifierState references like modifierState.tap/consumeIfNotSticky
and methods toggleControlModifier/toggleAlternateModifier) remain accessible
(adjust visibility or add internal APIs) and update Notification observer
registration to the initializer in the core class. Keep all symbol names
unchanged so callers (e.g. handleAccessoryButton(_:), handleAccessoryAction(_:),
applyModifierPresentation(), emitCommittedText(_:source:), insertText(_:),
setKeyboardShown(_:)) still link; run a build and fix access control/imports
after the refactor.
🪄 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: b2762df3-39d7-41ca-9d8c-14f3a1be5872

📥 Commits

Reviewing files that changed from the base of the PR and between 8d0ed95 and 98e54b7.

📒 Files selected for processing (12)
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextPosition.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextRange.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
  • Packages/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/TerminalCommitRoute.swift
  • Packages/CmuxMobileTerminalKit/Sources/CmuxMobileTerminalKit/TerminalCommitRouter.swift
  • Packages/CmuxMobileTerminalKit/Tests/CmuxMobileTerminalKitTests/TerminalCommitRouterTests.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/TerminalController.swift

Comment on lines +1462 to +1464
guard supportsTerminalPaste else {
let normalized = text.replacingOccurrences(of: "\n", with: "\r")
await submitTerminalRawInput(normalized, workspaceID: workspace.id, terminalID: terminalID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize CRLF before CR fallback conversion.

Line 1463 replaces \n with \r directly, so Windows-style \r\n becomes \r\r on old-host fallback and can inject extra Returns.

Suggested fix
-            let normalized = text.replacingOccurrences(of: "\n", with: "\r")
+            let normalized = text
+                .replacingOccurrences(of: "\r\n", with: "\n")
+                .replacingOccurrences(of: "\r", with: "\n")
+                .replacingOccurrences(of: "\n", with: "\r")
             await submitTerminalRawInput(normalized, workspaceID: workspace.id, terminalID: terminalID)
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 1462 - 1464, The current fallback branch in MobileShellComposite
(guard supportsTerminalPaste -> submitTerminalRawInput) naively replaces "\n"
with "\r" causing Windows CRLF ("\r\n") to become "\r\r"; first normalize CRLF
by replacing occurrences of "\r\n" with a single "\r", then replace any
remaining "\n" with "\r" before calling submitTerminalRawInput (refer to
symbols: supportsTerminalPaste, text, normalized, submitTerminalRawInput,
workspace.id, terminalID).

lawrencecchen added a commit that referenced this pull request Jun 8, 2026
…extInput input, supersedes #5573 hasText) into dog bundle
lawrencecchen added a commit that referenced this pull request Jun 11, 2026
Reset to origin/main (composer #5876 landed), then merge old dog HEAD
a300868 to preserve every feature not yet on main: notifications
dismiss-sync (#5568), multi-Mac switcher hardening (#5545), foreground
repaint (#5571), image paste too-large toast (#5572), hidden native
input (#5596), workspace groups (#5625), wslist round-10 snapshot,
scroll-to-bottom hysteresis, DEV dogfood pane, attachments button,
arrow toolbar keys, terminal.paste capability gating.

Conflict policy: main's reviewed composer-land form wins for composer
core (keyed focus handshake, draft FIFO coalescing, paste submit
partial-success), dog wins for unlanded feature surface. ghostty pinned
to dog 34cbf18 (descendant of main's e5c962a).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 12, 2026
Carries unlanded: multi-Mac #5545, notif-sync #5568, foreground-repaint #5571,
image-paste toast #5572, hidden-input #5596, groups (iOS side) #5625,
scroll-hysteresis, dogfood pane, capabilities superset.
Main's reviewed forms win: TerminalController decomposition (Control*Context),
notif-tap-deeplink #5927, mobile.terminal.* routing.
lawrencecchen added a commit that referenced this pull request Jun 13, 2026
…#5596/#5625/#5628) over current main

Beta queue (#5876/#5872/#5869/#5875/#5927/#5912/#5726/#5776/#5916) is now on
main; conflicts resolved by taking main as authoritative for the merged
workspace-list/notifications/read-state/close surface, while preserving the
carry-set: terminal.paste capability (#5572), hidden-input strings (#5596),
smooth-scroll/scroll-to-bottom (#5628), and the live notifications feed
(notificationsStore + mobile.notifications.list/mark_read dispatch). Dropped
the superseded mute design. Capability flags unified onto main's computed
supportedHostCapabilities set (added computed supportsTerminalPaste +
DEBUG supportsDogfoodChecklist). xcstrings merged (HEAD-precedence union,
mute keys dropped). pbxproj took HEAD consistently; budget regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 15, 2026
Reconcile the branch's per-Bool capability flags onto main's
supportedHostCapabilities Set model: add a computed supportsTerminalPaste
property + terminalPasteCapability constant, advertise terminal.paste.v1
from the Mac capabilities extension, and keep main's identity-recovery
on status-probe failure. deleteBackward keeps both the shift one-shot
consumption (markedText == nil) and the branch's marked-text composition
cancel.
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 98e54b70 Deployed Jun 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants