From da09166367cd9dccf40675ebe2067fcfdce90201 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:47:24 -0700 Subject: [PATCH 1/4] test: cover unchanged TextBox binding sync --- .../AppDelegateShortcutRoutingTests.swift | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index a491cad4c5..0b90dd052a 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -10098,6 +10098,73 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { XCTAssertEqual(pendingWriteCount, 0) } + func testTextBoxContentSyncSkipsUnchangedSwiftUIBindings() { + var text = "same" + var attachments: [TextBoxAttachment] = [] + var height: CGFloat = TextBoxLayout.minimumTextHeight + var hasPendingAttachmentUpload = false + var textWriteCount = 0 + var attachmentWriteCount = 0 + var pendingWriteCount = 0 + var contentChangeCount = 0 + + let inputView = TextBoxInputView( + text: Binding( + get: { text }, + set: { newValue in + textWriteCount += 1 + text = newValue + } + ), + attachments: Binding( + get: { attachments }, + set: { newValue in + attachmentWriteCount += 1 + attachments = newValue + } + ), + textViewHeight: Binding(get: { height }, set: { height = $0 }), + hasPendingAttachmentUpload: Binding( + get: { hasPendingAttachmentUpload }, + set: { newValue in + pendingWriteCount += 1 + hasPendingAttachmentUpload = newValue + } + ), + font: NSFont.systemFont(ofSize: 14), + backgroundColor: .textBackgroundColor, + foregroundColor: .labelColor, + terminalTitle: "codex", + completionRootDirectory: nil, + onSubmit: {}, + onEscape: {}, + onFocusTextBox: {}, + onToggleFocus: {}, + onForwardText: { _, _ in }, + onForwardKey: { _ in }, + onForwardControl: { _ in }, + onPaste: { _, _ in false }, + onInsertFileURLs: { _, _ in false }, + onChooseFiles: {}, + onContentChanged: { contentChangeCount += 1 }, + onTextViewCreated: { _ in }, + onTextViewMovedToWindow: { _ in }, + onTextViewDismantled: { _ in } + ) + let coordinator = TextBoxInputView.Coordinator(parent: inputView) + let textView = TextBoxInputTextView(frame: NSRect(x: 0, y: 0, width: 320, height: 30)) + textView.font = NSFont.systemFont(ofSize: 14) + textView.textColor = .labelColor + textView.string = "same" + + coordinator.textDidChange(Notification(name: NSText.didChangeNotification, object: textView)) + + XCTAssertEqual(textWriteCount, 0) + XCTAssertEqual(attachmentWriteCount, 0) + XCTAssertEqual(pendingWriteCount, 0) + XCTAssertEqual(contentChangeCount, 0) + } + func testTextBoxPendingAttachmentUploadPreservesOriginalInsertionPoint() throws { let originalURL = try makeTemporaryPNGFile(named: "moon.png") let originalAttachment = TextBoxAttachment( From fda59be505b8cff67ff76a3ba29258927946dec2 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:04:35 -0700 Subject: [PATCH 2/4] fix: keep TextBox sync test self-contained --- cmuxTests/AppDelegateShortcutRoutingTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 0b90dd052a..2b123cc1f2 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -10101,7 +10101,7 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { func testTextBoxContentSyncSkipsUnchangedSwiftUIBindings() { var text = "same" var attachments: [TextBoxAttachment] = [] - var height: CGFloat = TextBoxLayout.minimumTextHeight + var height: CGFloat = 24 var hasPendingAttachmentUpload = false var textWriteCount = 0 var attachmentWriteCount = 0 From 034b95dfb6f5f051990a9044c92ddbdd5916d7bf Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:42:12 -0700 Subject: [PATCH 3/4] Use Swift Testing for TextBox content sync regression --- cmux.xcodeproj/project.pbxproj | 4 + .../AppDelegateShortcutRoutingTests.swift | 67 ---------------- cmuxTests/TextBoxContentSyncTests.swift | 79 +++++++++++++++++++ 3 files changed, 83 insertions(+), 67 deletions(-) create mode 100644 cmuxTests/TextBoxContentSyncTests.swift diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 82cabb06bb..983163c832 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -530,6 +530,7 @@ C13519000000000000000001 /* TerminalStartupEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13519000000000000000002 /* TerminalStartupEnvironment.swift */; }; A5001532 /* TerminalWindowPortal.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001531 /* TerminalWindowPortal.swift */; }; D0B10006A1B2C3D4E5F60001 /* TerminalWindowPortalDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10007A1B2C3D4E5F60001 /* TerminalWindowPortalDebug.swift */; }; + C5345001A1B2C3D4E5F60719 /* TextBoxContentSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5345002A1B2C3D4E5F60719 /* TextBoxContentSyncTests.swift */; }; C0DE7B100000000000000001 /* TextBoxInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE7B100000000000000002 /* TextBoxInput.swift */; }; C0DE7B310000000000000001 /* TextBoxMentionCachedIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE7B310000000000000002 /* TextBoxMentionCachedIndex.swift */; }; C0DE7B250000000000000001 /* TextBoxMentionCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE7B250000000000000002 /* TextBoxMentionCandidate.swift */; }; @@ -1169,6 +1170,7 @@ C13519000000000000000002 /* TerminalStartupEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalStartupEnvironment.swift; sourceTree = ""; }; A5001531 /* TerminalWindowPortal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindowPortal.swift; sourceTree = ""; }; D0B10007A1B2C3D4E5F60001 /* TerminalWindowPortalDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindowPortalDebug.swift; sourceTree = ""; }; + C5345002A1B2C3D4E5F60719 /* TextBoxContentSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBoxContentSyncTests.swift; sourceTree = ""; }; C0DE7B100000000000000002 /* TextBoxInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBoxInput.swift; sourceTree = ""; }; C0DE7B310000000000000002 /* TextBoxMentionCachedIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBoxMentionCachedIndex.swift; sourceTree = ""; }; C0DE7B250000000000000002 /* TextBoxMentionCandidate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBoxMentionCandidate.swift; sourceTree = ""; }; @@ -1849,6 +1851,7 @@ F5310001A1B2C3D4E5F60718 /* RovoDevHookConfigTests.swift */, FA100001A1B2C3D4E5F60718 /* BrowserImportMappingTests.swift */, F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */, + C5345002A1B2C3D4E5F60719 /* TextBoxContentSyncTests.swift */, C0DE7B300000000000000002 /* TextBoxMentionCompletionTests.swift */, C1713005C1713005C1713005 /* CommandPaletteShortcutCustomizationTests.swift */, 17FCD4CC61D54A2F8F2F463D /* AppDelegateBareSpaceShortcutRoutingTests.swift */, @@ -2881,6 +2884,7 @@ A5A5A503A1B2C3D4E5F60718 /* TerminalNotificationQueueTests.swift in Sources */, A5E380700000000000000001 /* TerminalNotificationSocketActionTests.swift in Sources */, C0DE53360000000000000001 /* TerminalSearchOverlayMouseReleaseTests.swift in Sources */, + C5345001A1B2C3D4E5F60719 /* TextBoxContentSyncTests.swift in Sources */, C0DE7B300000000000000001 /* TextBoxMentionCompletionTests.swift in Sources */, F50030040000000000000001 /* TitlebarInteractiveControlTests.swift in Sources */, D3284001A1B2C3D4E5F60718 /* TraditionalChineseIMENumpadRegressionTests.swift in Sources */, diff --git a/cmuxTests/AppDelegateShortcutRoutingTests.swift b/cmuxTests/AppDelegateShortcutRoutingTests.swift index 2b123cc1f2..a491cad4c5 100644 --- a/cmuxTests/AppDelegateShortcutRoutingTests.swift +++ b/cmuxTests/AppDelegateShortcutRoutingTests.swift @@ -10098,73 +10098,6 @@ final class AppDelegateShortcutRoutingTests: XCTestCase { XCTAssertEqual(pendingWriteCount, 0) } - func testTextBoxContentSyncSkipsUnchangedSwiftUIBindings() { - var text = "same" - var attachments: [TextBoxAttachment] = [] - var height: CGFloat = 24 - var hasPendingAttachmentUpload = false - var textWriteCount = 0 - var attachmentWriteCount = 0 - var pendingWriteCount = 0 - var contentChangeCount = 0 - - let inputView = TextBoxInputView( - text: Binding( - get: { text }, - set: { newValue in - textWriteCount += 1 - text = newValue - } - ), - attachments: Binding( - get: { attachments }, - set: { newValue in - attachmentWriteCount += 1 - attachments = newValue - } - ), - textViewHeight: Binding(get: { height }, set: { height = $0 }), - hasPendingAttachmentUpload: Binding( - get: { hasPendingAttachmentUpload }, - set: { newValue in - pendingWriteCount += 1 - hasPendingAttachmentUpload = newValue - } - ), - font: NSFont.systemFont(ofSize: 14), - backgroundColor: .textBackgroundColor, - foregroundColor: .labelColor, - terminalTitle: "codex", - completionRootDirectory: nil, - onSubmit: {}, - onEscape: {}, - onFocusTextBox: {}, - onToggleFocus: {}, - onForwardText: { _, _ in }, - onForwardKey: { _ in }, - onForwardControl: { _ in }, - onPaste: { _, _ in false }, - onInsertFileURLs: { _, _ in false }, - onChooseFiles: {}, - onContentChanged: { contentChangeCount += 1 }, - onTextViewCreated: { _ in }, - onTextViewMovedToWindow: { _ in }, - onTextViewDismantled: { _ in } - ) - let coordinator = TextBoxInputView.Coordinator(parent: inputView) - let textView = TextBoxInputTextView(frame: NSRect(x: 0, y: 0, width: 320, height: 30)) - textView.font = NSFont.systemFont(ofSize: 14) - textView.textColor = .labelColor - textView.string = "same" - - coordinator.textDidChange(Notification(name: NSText.didChangeNotification, object: textView)) - - XCTAssertEqual(textWriteCount, 0) - XCTAssertEqual(attachmentWriteCount, 0) - XCTAssertEqual(pendingWriteCount, 0) - XCTAssertEqual(contentChangeCount, 0) - } - func testTextBoxPendingAttachmentUploadPreservesOriginalInsertionPoint() throws { let originalURL = try makeTemporaryPNGFile(named: "moon.png") let originalAttachment = TextBoxAttachment( diff --git a/cmuxTests/TextBoxContentSyncTests.swift b/cmuxTests/TextBoxContentSyncTests.swift new file mode 100644 index 0000000000..129f67e507 --- /dev/null +++ b/cmuxTests/TextBoxContentSyncTests.swift @@ -0,0 +1,79 @@ +import AppKit +import SwiftUI +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +struct TextBoxContentSyncTests { + @Test func contentSyncSkipsUnchangedSwiftUIBindings() { + var text = "same" + var attachments: [TextBoxAttachment] = [] + var height: CGFloat = 24 + var hasPendingAttachmentUpload = false + var textWriteCount = 0 + var attachmentWriteCount = 0 + var pendingWriteCount = 0 + var contentChangeCount = 0 + + let inputView = TextBoxInputView( + text: Binding( + get: { text }, + set: { newValue in + textWriteCount += 1 + text = newValue + } + ), + attachments: Binding( + get: { attachments }, + set: { newValue in + attachmentWriteCount += 1 + attachments = newValue + } + ), + textViewHeight: Binding(get: { height }, set: { height = $0 }), + hasPendingAttachmentUpload: Binding( + get: { hasPendingAttachmentUpload }, + set: { newValue in + pendingWriteCount += 1 + hasPendingAttachmentUpload = newValue + } + ), + font: NSFont.systemFont(ofSize: 14), + backgroundColor: .textBackgroundColor, + foregroundColor: .labelColor, + terminalTitle: "codex", + completionRootDirectory: nil, + onSubmit: {}, + onEscape: {}, + onFocusTextBox: {}, + onToggleFocus: {}, + onForwardText: { _, _ in }, + onForwardKey: { _ in }, + onForwardControl: { _ in }, + onPaste: { _, _ in false }, + onInsertFileURLs: { _, _ in false }, + onChooseFiles: {}, + onContentChanged: { contentChangeCount += 1 }, + onTextViewCreated: { _ in }, + onTextViewMovedToWindow: { _ in }, + onTextViewDismantled: { _ in } + ) + let coordinator = TextBoxInputView.Coordinator(parent: inputView) + let textView = TextBoxInputTextView(frame: NSRect(x: 0, y: 0, width: 320, height: 30)) + textView.font = NSFont.systemFont(ofSize: 14) + textView.textColor = .labelColor + textView.string = "same" + + coordinator.textDidChange(Notification(name: NSText.didChangeNotification, object: textView)) + + #expect(textWriteCount == 0) + #expect(attachmentWriteCount == 0) + #expect(pendingWriteCount == 0) + #expect(contentChangeCount == 0) + } +} From 2e58388e1aa3f2eab175dcf771bc7c6bfc7a145b Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:47:42 -0700 Subject: [PATCH 4/4] fix: skip unchanged TextBox binding writes --- Sources/TextBoxInput.swift | 46 +++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/Sources/TextBoxInput.swift b/Sources/TextBoxInput.swift index d2d6309817..9f1a5fc41a 100644 --- a/Sources/TextBoxInput.swift +++ b/Sources/TextBoxInput.swift @@ -3420,17 +3420,51 @@ struct TextBoxInputView: NSViewRepresentable { let nextText = textView.plainText() let nextAttachments = textView.inlineAttachments() let nextHasPendingAttachmentUpload = textView.hasPendingAttachmentUploadPlaceholder() - let contentChanged = parent.text != nextText - || parent.attachments.map(\.id) != nextAttachments.map(\.id) - || parent.hasPendingAttachmentUpload != nextHasPendingAttachmentUpload - parent.text = nextText - parent.attachments = nextAttachments - parent.hasPendingAttachmentUpload = nextHasPendingAttachmentUpload + let didChangeText = parent.text != nextText + let didChangeAttachments = Self.attachmentsDiffer(parent.attachments, nextAttachments) + let didChangePendingUpload = parent.hasPendingAttachmentUpload != nextHasPendingAttachmentUpload + let contentChanged = didChangeText || didChangeAttachments || didChangePendingUpload + if didChangeText { + parent.text = nextText + } + if didChangeAttachments { + parent.attachments = nextAttachments + } + if didChangePendingUpload { + parent.hasPendingAttachmentUpload = nextHasPendingAttachmentUpload + } if contentChanged { parent.onContentChanged() } } + private static func attachmentsDiffer( + _ currentAttachments: [TextBoxAttachment], + _ nextAttachments: [TextBoxAttachment] + ) -> Bool { + guard currentAttachments.count == nextAttachments.count else { return true } + return zip(currentAttachments, nextAttachments).contains { current, next in + current.id != next.id + || current.displayName != next.displayName + || current.submissionText != next.submissionText + || current.submissionPath != next.submissionPath + || current.localURL != next.localURL + || current.cleanupLocalURLWhenDisposed != next.cleanupLocalURLWhenDisposed + || !sameThumbnail(current.thumbnail, next.thumbnail) + } + } + + private static func sameThumbnail(_ currentThumbnail: NSImage?, _ nextThumbnail: NSImage?) -> Bool { + switch (currentThumbnail, nextThumbnail) { + case (nil, nil): + true + case let (current?, next?): + current === next + default: + false + } + } + func recalculateHeight(_ textView: NSTextView) { guard let layoutManager = textView.layoutManager, let textContainer = textView.textContainer else { return }