Repository navigation
fix: join soft-wrapped rows when copying terminal text - #6923
Conversation
|
@mvanhorn is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedWe couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds terminal copy reflow through a new Swift package, configurable ChangesTerminal copy reflow
Transport test timing
Estimated code review effort: 5 (Critical) | ~100 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant GhosttyTerminalView
participant CmuxCopyReflow
participant TerminalPasteboardService
participant SystemClipboard
User->>GhosttyTerminalView: Copy terminal selection
GhosttyTerminalView->>CmuxCopyReflow: Classify and reflow eligible text
CmuxCopyReflow-->>GhosttyTerminalView: Reflowed text
GhosttyTerminalView->>TerminalPasteboardService: Rewrite text representations
TerminalPasteboardService->>SystemClipboard: Publish replacement pasteboard item
User->>GhosttyTerminalView: Invoke Copy Raw
GhosttyTerminalView->>SystemClipboard: Publish verbatim selection
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 3 warnings)
✅ Passed checks (21 passed)
Full details: Description checkExplanation The description includes the required sections and testing details, but its core behavior and setting information conflicts with the changeset. It describes Resolution Update the description to match the implementation. Document ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Greptile SummaryThis PR adds a "clean copy" reflow transform that rejoins hard-wrapped terminal text on Cmd+C, while preserving code fences, tables, blockquotes, lists, and URLs. All reflow logic lives in a new pure, dependency-free
Confidence Score: 5/5Safe to merge. The reflow transform is bounded, runs on the AppKit event thread where copy already occurs, and is guarded by a user-facing setting with a verbatim-copy escape hatch. Localization is complete across all 20 app and web locales. All new production paths are bounded and conservative. The reflow engine lives in a well-tested, dependency-free package with 49 unit tests. The UI integration correctly gates reflow behind setting, line-count, and byte-count checks. No new blocking issues were found beyond what was already addressed in previous review threads. No files require special attention. The most complex new file, CopyReflow.swift, is well-commented and has direct unit-test coverage for its edge cases. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant AppDelegate
participant GhosttyNSView
participant Ghostty as libghostty
participant NSPasteboard
participant ReflowEngine as CmuxCopyReflow
User->>AppDelegate: Cmd+C (copy)
AppDelegate->>GhosttyNSView: copyKeyboardCopyModeSelectionToClipboard / copy(_:)
GhosttyNSView->>GhosttyNSView: activeSelectionShouldBypassCopyReflow()
GhosttyNSView->>Ghostty: ghostty_surface_selection_screen_rows()
Ghostty-->>GhosttyNSView: (topRow, bottomRow) or nil
GhosttyNSView->>Ghostty: performBindingAction(copy_to_clipboard)
Ghostty->>NSPasteboard: write formatted text
GhosttyNSView->>GhosttyNSView: reflowCopySettings.value(for: reflowCopyKey)
alt reflow enabled and bounds present and within limits
GhosttyNSView->>NSPasteboard: string(forType: .string)
NSPasteboard-->>GhosttyNSView: original text
GhosttyNSView->>ReflowEngine: ReflowOptions.default.reflow(text, terminalWidth:)
ReflowEngine-->>GhosttyNSView: reflowed text
GhosttyNSView->>NSPasteboard: rewriteTextRepresentations(reflowed)
else reflow off / raw / rectangular / too large
Note over GhosttyNSView,NSPasteboard: original text left on pasteboard
end
User->>AppDelegate: Cmd+Shift+C (Copy Raw)
AppDelegate->>GhosttyNSView: copyRawSelectionToClipboard()
GhosttyNSView->>Ghostty: performBindingAction(copy_to_clipboard)
Note over GhosttyNSView: reflow=false, pasteboard unchanged
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant AppDelegate
participant GhosttyNSView
participant Ghostty as libghostty
participant NSPasteboard
participant ReflowEngine as CmuxCopyReflow
User->>AppDelegate: Cmd+C (copy)
AppDelegate->>GhosttyNSView: copyKeyboardCopyModeSelectionToClipboard / copy(_:)
GhosttyNSView->>GhosttyNSView: activeSelectionShouldBypassCopyReflow()
GhosttyNSView->>Ghostty: ghostty_surface_selection_screen_rows()
Ghostty-->>GhosttyNSView: (topRow, bottomRow) or nil
GhosttyNSView->>Ghostty: performBindingAction(copy_to_clipboard)
Ghostty->>NSPasteboard: write formatted text
GhosttyNSView->>GhosttyNSView: reflowCopySettings.value(for: reflowCopyKey)
alt reflow enabled and bounds present and within limits
GhosttyNSView->>NSPasteboard: string(forType: .string)
NSPasteboard-->>GhosttyNSView: original text
GhosttyNSView->>ReflowEngine: ReflowOptions.default.reflow(text, terminalWidth:)
ReflowEngine-->>GhosttyNSView: reflowed text
GhosttyNSView->>NSPasteboard: rewriteTextRepresentations(reflowed)
else reflow off / raw / rectangular / too large
Note over GhosttyNSView,NSPasteboard: original text left on pasteboard
end
User->>AppDelegate: Cmd+Shift+C (Copy Raw)
AppDelegate->>GhosttyNSView: copyRawSelectionToClipboard()
GhosttyNSView->>Ghostty: performBindingAction(copy_to_clipboard)
Note over GhosttyNSView: reflow=false, pasteboard unchanged
Reviews (35): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swift`:
- Around line 139-142: The wrap-join logic in CopyReflow should not depend only
on startsLowercaseLetter(content), because that misses common command
continuations like --flag, /path, $VAR, or quoted tokens. Update the canJoin
decision in CopyReflow.swift to add a command/token continuation rule alongside
the existing s1, s3, and s4 checks, and keep the prevEndsTerminator guard
intact. Add a regression test covering a long wrapped command that continues
onto --another-option or /path to verify the hard newline is removed.
🪄 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: 2dd6cc20-140a-42e5-a64e-908e5be17d29
📒 Files selected for processing (3)
Packages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swiftPackages/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ReflowParagraphTests.swiftSources/GhosttyTerminalView.swift
There was a problem hiding this comment.
3 issues found across 24 files
You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Sources/GhosttyTerminalView.swift (2)
4740-4789: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winReflow-decision logic is duplicated across three call sites.
The condition
reflow && !activeSelectionMayBeRectangular && TerminalReflowCopySettings.isEnabled()(or an equivalent) is reimplemented three times: inline in theghosttyWroteFormattedTextbranch viareflowClipboardTextIfEnabled(line 4749/4767), inline again in the raw-selection fallback (line 4757), and a third variant incopyKeyboardCopyModeSelectionToClipboard(line 4781) that intentionally omits theactiveSelectionMayBeRectangularcheck. This is exactly the kind of "same behavior wired separately through multiple surfaces" the repo's shared-behavior policy calls out, and it's already drifted once (the third site has a different rule).Consider extracting a single helper, e.g.:
private func shouldReflow(_ reflow: Bool, considerRectangular: Bool = true) -> Bool { guard reflow, TerminalReflowCopySettings.isEnabled() else { return false } return !considerRectangular || !activeSelectionMayBeRectangular }and call it from all three sites, so future changes to the eligibility rule can't silently diverge between entry points.
Based on learnings: repo's shared behavior policy states "When a behavior is exposed through multiple entrypoints... implement one shared action/model path... Do not patch one surface while leaving the others with duplicated logic."
🤖 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 `@Sources/GhosttyTerminalView.swift` around lines 4740 - 4789, The reflow eligibility check is duplicated and already diverging across copy paths in GhosttyTerminalView. Extract a single shared helper around the copy/reflow decision in copyCurrentGhosttySelectionToClipboard and copyKeyboardCopyModeSelectionToClipboard, then use it everywhere instead of repeating TerminalReflowCopySettings.isEnabled(), reflow, and activeSelectionMayBeRectangular checks inline. Keep the helper as the source of truth for both the formatted pasteboard branch and the raw-selection fallback so all entrypoints follow the same rule.
4740-4774: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve Ghostty’s mixed clipboard payload
Sources/GhosttyTerminalView.swift:4769-4773
copy_to_clipboardwrites.mixedhere, sopasteboard.clearContents()followed bysetString(..., forType: .string)drops the HTML flavor and breaks rich-text paste fidelity. Reflow should rewrite the plain-text item without discarding the other pasteboard types.🤖 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 `@Sources/GhosttyTerminalView.swift` around lines 4740 - 4774, The clipboard reflow path in copyCurrentGhosttySelectionToClipboard / reflowClipboardTextIfEnabled is replacing the whole pasteboard contents, which drops Ghostty’s mixed payload and loses rich-text flavors. Update the reflow logic to rewrite only the plain-text representation on the existing pasteboard item after copy_to_clipboard, preserving any additional types such as HTML instead of calling clearContents and resetting the pasteboard from scratch.
🤖 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/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swift`:
- Around line 117-176: The joiner in CopyReflow’s prose/url handling is using
the paragraph-level isURL flag, which can incorrectly suppress spaces after
URL-led lines that later continue as normal prose. Update the join logic in the
case .prose, .urlLine block to key the joiner off the per-iteration s3 signal
instead of p.isURL, so only true bare-URL fragment continuations use no
separator while s1/s4 prose continuations keep a space; add a regression test
covering a URL-prefixed line followed by multi-word continuation.
In `@Packages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/LineKind.swift`:
- Around line 81-86: The current LineKind.isTableRow heuristic treats any line
containing a pipe as a table row, which causes shell commands with pipes to be
misclassified and hard-broken in CopyReflow.swift. Tighten the table detection
in isTableRow so it only returns true for likely markdown tables, such as when
the line is adjacent to a separator-like row or when the content has multiple
pipe delimiters, and keep shell command lines from being classified as
.tableRow.
In `@Sources/GhosttyTerminalView.swift`:
- Line 3539: The selection reflow logic is still relying on the mouse-driven
activeSelectionMayBeRectangular flag, which can become stale after non-mouse
selection changes. Update GhosttyTerminalView’s copy/reflow path to derive
whether a selection is rectangular from the selection payload’s rectangle shape
instead of shadow-tracking it via mouse modifiers, and make the Cmd+C handling
use that authoritative selection state.
---
Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 4740-4789: The reflow eligibility check is duplicated and already
diverging across copy paths in GhosttyTerminalView. Extract a single shared
helper around the copy/reflow decision in copyCurrentGhosttySelectionToClipboard
and copyKeyboardCopyModeSelectionToClipboard, then use it everywhere instead of
repeating TerminalReflowCopySettings.isEnabled(), reflow, and
activeSelectionMayBeRectangular checks inline. Keep the helper as the source of
truth for both the formatted pasteboard branch and the raw-selection fallback so
all entrypoints follow the same rule.
- Around line 4740-4774: The clipboard reflow path in
copyCurrentGhosttySelectionToClipboard / reflowClipboardTextIfEnabled is
replacing the whole pasteboard contents, which drops Ghostty’s mixed payload and
loses rich-text flavors. Update the reflow logic to rewrite only the plain-text
representation on the existing pasteboard item after copy_to_clipboard,
preserving any additional types such as HTML instead of calling clearContents
and resetting the pasteboard from scratch.
🪄 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: dab798c3-a73a-45c4-be7d-b5e7a8318869
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (34)
Packages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swiftPackages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/LineKind.swiftPackages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/ReflowOptions.swiftPackages/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ClassifierTests.swiftPackages/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ReflowParagraphTests.swiftPackages/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ReflowStructureTests.swiftPackages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swiftPackages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsSearchIndexTests.swiftResources/Localizable.xcstringsSources/CmuxSettingsJSONPathSupport.swiftSources/GhosttyTerminalView.swiftcmux.xcodeproj/project.pbxprojdocs/plans/2026-06-26-001-feat-terminal-copy-reflow-plan.mdweb/data/cmux.schema.jsonweb/messages/ar.jsonweb/messages/bs.jsonweb/messages/da.jsonweb/messages/de.jsonweb/messages/en.jsonweb/messages/es.jsonweb/messages/fr.jsonweb/messages/it.jsonweb/messages/ja.jsonweb/messages/km.jsonweb/messages/ko.jsonweb/messages/no.jsonweb/messages/pl.jsonweb/messages/pt-BR.jsonweb/messages/ru.jsonweb/messages/th.jsonweb/messages/tr.jsonweb/messages/uk.jsonweb/messages/zh-CN.jsonweb/messages/zh-TW.json
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Ping on this one @austinywang - thanks for the earlier look. The red checks (tests / activation-session) look like they just need a rebase, which I'm glad to push. Is there anything about the reflow behavior itself you'd want changed before it can go in? |
|
Deployment failed with the following error: |
|
@mvanhorn I’ve taken this through the maintainer-update pass and pushed the result to your fork branch. The reflow behavior itself is now in the shape I’d want before merge:
All nine inline review threads are resolved. The branch is currently |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Addressing the remaining Greptile summary note: Swift already classifies U+00A0 (NO-BREAK SPACE) as whitespace on this PR's pinned toolchain. Verified directly with: $ swift -e 'print(Character("\u{00A0}").isWhitespace)'
true
|
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
5 similar comments
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swift (1)
269-288: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winAdvance past rows already promoted to avoid quadratic table scanning.
Each separator scans the remaining candidate rows, but those promoted
.tableRowentries are processed again as separators. A 20,000-line separator-shaped selection therefore performs O(n²) work on the interactive copy path.Proposed single-pass fix
- for i in lines.indices { - guard isPromotableTableKind(lineKinds[i]), - lineKindIsMarkdownTableSeparatorRow(lines[i]) else { continue } + var i = lines.startIndex + while i < lines.endIndex { + guard isPromotableTableKind(lineKinds[i]), + lineKindIsMarkdownTableSeparatorRow(lines[i]) else { + i += 1 + continue + } lineKinds[i] = .tableRow ... while bodyIndex < lines.count, isPromotableTableKind(lineKinds[bodyIndex]), lineKindIsMarkdownTableCandidateRow(lines[bodyIndex]) { lineKinds[bodyIndex] = .tableRow bodyIndex += 1 } + i = bodyIndex }Add an adversarial large separator-row regression or benchmark. As per path instructions, prefer single-pass reducers over repeated scans of scalable collections.
🤖 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/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swift` around lines 269 - 288, Update the table-promotion loop around isPromotableTableKind and lineKindIsMarkdownTableSeparatorRow so advancing through a separator’s body skips already-promoted .tableRow entries and does not reprocess them as separators. Preserve promotion of the header and contiguous candidate body rows while making the traversal single-pass, and add an adversarial large separator-row regression or benchmark covering the interactive copy path.Source: Path instructions
🤖 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/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ReflowParagraphTests.swift`:
- Around line 359-375: Remove the wall-clock timing and scaling assertion from
nearLimitSelectionReflowsWithLinearScaling, including its timedReflow, seconds,
and ContinuousClock usage. Replace it with a deterministic large-input
correctness assertion using the existing reflow expectations; move performance
scaling coverage to the benchmark harness rather than this correctness suite.
---
Outside diff comments:
In `@Packages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swift`:
- Around line 269-288: Update the table-promotion loop around
isPromotableTableKind and lineKindIsMarkdownTableSeparatorRow so advancing
through a separator’s body skips already-promoted .tableRow entries and does not
reprocess them as separators. Preserve promotion of the header and contiguous
candidate body rows while making the traversal single-pass, and add an
adversarial large separator-row regression or benchmark covering the interactive
copy path.
🪄 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: 913e0b04-f5b4-47d5-b78f-d6e23b19a3c6
📒 Files selected for processing (18)
.github/workflows/ci.ymlPackages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/CopyReflow.swiftPackages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/LineKind.swiftPackages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/ReflowOptions+ContinuationEvidence.swiftPackages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/TerminalCellWidth.swiftPackages/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ClassifierTests.swiftPackages/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ReflowParagraphTests.swiftPackages/macOS/CmuxCopyReflow/Tests/CmuxCopyReflowTests/ReflowStructureTests.swiftPackages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swiftPackages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swiftPackages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swiftPackages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swiftPackages/macOS/CmuxTerminal/Sources/CmuxTerminal/Services/Pasteboard/TerminalPasteboardService+TextContents.swiftPackages/macOS/CmuxTerminal/Tests/CmuxTerminalTests/Services/TerminalPasteboardServiceTests.swiftResources/Localizable.xcstringsSources/AppDelegate.swiftSources/GhosttyTerminalView.swiftcmux.xcodeproj/project.pbxproj
💤 Files with no reviewable changes (1)
- Packages/macOS/CmuxCopyReflow/Sources/CmuxCopyReflow/LineKind.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
Final review reconciliation on the current branch:
|
|
Appreciate you carrying this all the way to mergeable @austinywang - the continuation/table/CJK coverage covers the edges I was unsure about. Let me know if you need anything from me before it lands. |
|
Thanks for all the work here, and sorry it sat so long. The soft-wrap part of #3096 is a real bug, and people hit it constantly. I'm not going to merge this as-is, for two reasons:
If you're up for it, a smaller PR would be easy to land: just fix soft-wrapped lines (join rows Ghostty marks as wrapped) on the current copy path, with hard-wrap reflow behind a setting that's off by default. Please leave out the plan doc, the |
1516fc0 to
b83f771
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
All contributors have signed the CLA ✍️ ✅ |
|
Thanks @teamleaderleo, took you up on the smaller version and reworked this PR in place rather than opening another (b83f771, one commit on current main). It now only joins rows Ghostty marks as soft-wrapped, on the current copy path, and leaves rectangular selections alone. Hard-wrap reflow is behind a Terminal setting that's off by default. The plan doc, the ci.yml edit and the test reorder are gone. The description has the testing details, including what I couldn't verify locally. |
joinedCopyText read "terminal.reflowHardWrapOnCopy" as a raw UserDefaults string. Read it through TerminalCatalogSection so the catalog stays the one definition of the key, its type, and its default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the soft-wrap join changed the plain text, representationsJoiningSoftWraps dropped every non-plain representation, so a copy that used to paste styled HTML or RTF into Notes, Mail, or a document editor pasted unstyled text instead. Keep the rich representations unchanged and rewrite only text/plain. Their row breaks live inside styled markup and cannot be remapped from the plain text without re-rendering Ghostty's formatter output, so regenerating them is not a small change. This is the least surprising split: plain-text destinations (terminals, editors, chat inputs) get the joined line, and rich destinations paste exactly what they did before this PR rather than losing formatting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add settings.terminal.reflowHardWrapOnCopy, its subtitleOn and subtitleOff variants, and the settings.search.alias.setting.terminal.reflow-hard-wrap-on-copy search alias to Resources/Localizable.xcstrings with all nine required macOS locales, next to the copy-on-select entries they sit beside in Settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for this! I pushed the localization entries plus a few small fixes (catalog key read, keeping rich clipboard formats) to your branch :) |
The package conventions lint rejects all-static namespace enums. The copy builder now carries its hard-wrap settings as stored properties, and the call sites and tests construct it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ghostty's copy already unwraps soft wraps, so a logical line wider than the grid was being joined onto the next line. Also narrow the wide-cell table to East Asian Wide/Fullwidth and emoji so punctuation and box drawing count as one cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pOnCopy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merge receipt for |
|
Merged, thanks @mvanhorn :) |
9bae42b Show one settings section at a time (manaflow-ai#12993) 1dfd0e6 fix: join soft-wrapped rows when copying terminal text (manaflow-ai#6923) 8ae8f01 ci: build the nightly app on cmux15's trusted runner first, Blacksmith as fallback (manaflow-ai#14821) 977148c Add command palette entries for shortcut-only actions (manaflow-ai#14815) 659fc76 Keep Claude NODE_OPTIONS restore preload out of TMPDIR (manaflow-ai#14814) 749a2f8 test(simulator): bound the interactive frame wait by the idle interval, not 250 ms (manaflow-ai#14820) # Conflicts: # .github/workflows/ci-health-report.yml # .github/workflows/ci-owned-pool-rescue.yml # .github/workflows/ci-repo-variables.yml # .github/workflows/nightly.yml
|
Thanks for the review and merge @teamleaderleo! |
Summary
Fixes the soft-wrap part of #3096, reworked into the smaller change @teamleaderleo suggested below. Copying a line Ghostty soft-wrapped now pastes as one line: on the current copy path, both the formatted representations and the plain-text write, rows Ghostty marks as wrapped are joined. Line breaks the program printed are kept. Option-drag rectangular selections are left alone, since joining their rows would merge columns.
Heuristic reflow of hard-wrapped text is behind a new Terminal setting, "Reflow Hard-Wrapped Text on Copy" (
terminal.reflowHardWrapOnCopy), which is off by default.Testing
TerminalSoftWrapCopyTests(new, CmuxTerminalCore): 15/15 viaswift test, covering the soft-wrap join, blank lines, rectangular selections, and the setting on and off.SettingCatalogTests: 10/10 with the new key.GhosttyTerminalView.swifthook), so CI is the compile check there, and I haven't checked it in a tagged build.Demo Video
Checklist
String(localized:)keys with English defaults; catalog entries are not added yetMaintainer note
Resources/Localizable.xcstringswith all nine macOS locales.text/plainis rewritten. HTML and RTF pass through unchanged, since their row breaks can't be remapped without re-rendering Ghostty's styled output, and dropping them lost formatting in rich-text destinations.write_clipboard_cb, which also carries OSC 52 writes from programs and has no reliable way to tell them apart, so joining there could rewrite text an app put on the clipboard. It still copies rows as Ghostty emits them.