Skip to content

fix: prevent terminal flickering during layout changes (e.g., new-workspace) - #1689

Closed
BillionClaw wants to merge 1 commit into
manaflow-ai:mainfrom
BillionClaw:clawoss/fix/terminal-flicker-layout-changes
Closed

BillionClaw wants to merge 1 commit into
manaflow-ai:mainfrom
BillionClaw:clawoss/fix/terminal-flicker-layout-changes

Conversation

@BillionClaw

@BillionClaw BillionClaw commented Mar 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #1625 - All terminal content flickers when CLI commands trigger layout changes.

When using cmux CLI commands that modify the workspace/layout state (e.g., cmux new-workspace), all visible terminal surfaces would flicker — not just the affected workspace. The entire terminal content would briefly disappear and redraw.

Root Cause

When a new workspace was added, the SwiftUI view hierarchy would update, causing existing terminal views to be temporarily detached and re-attached. This triggered expensive layout and surface update operations in viewDidMoveToWindow, resulting in visible flicker.

Changes

  1. ContentView.swift: Added .id(tab.id) to WorkspaceContentView to ensure stable view identity across mountedWorkspaces changes. This prevents SwiftUI from unnecessarily re-creating terminal views during layout transitions.

  2. GhosttyTerminalView.swift: Added window tracking to detect when a view is being re-parented within the same window (rather than moving to a different window). When re-parenting within the same window, we now skip expensive layout and surface updates that were causing the flicker.

Testing

  • Verified that new-workspace commands no longer cause existing terminals to flicker
  • Verified that terminal surfaces still properly resize and update when actually changing windows
  • Verified that text input coordinates are still invalidated on re-parenting (needed for correct IME positioning)

Related


Summary by cubic

Prevents terminal flicker when workspace layout changes. Keeps existing terminals stable during CLI actions like cmux new-workspace.

  • Bug Fixes
    • ContentView: add .id(tab.id) to WorkspaceContentView to keep stable view identity and avoid unnecessary recreation.
    • GhosttyTerminalView: track the last attached window to detect same-window re-parenting; skip expensive layout/surface updates in that case while still invalidating text input coordinates.

Written for commit aeab5e0. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Significantly reduced terminal display flicker that occurred during workspace transitions and when switching between multiple mounted workspaces
    • Improved overall rendering stability, responsiveness, and visual consistency by preventing unnecessary view recreation during dynamic layout changes
    • Optimized window attachment and re-attachment operations to efficiently handle common workspace management scenarios without triggering expensive layout recalculations

…kspace)

This commit addresses issue manaflow-ai#1625 where all terminal content flickers when
CLI commands trigger layout changes like new-workspace.

Changes made:
1. Added stable identity (.id(tab.id)) to WorkspaceContentView in ContentView.swift
   to prevent view recreation when mountedWorkspaces changes.

2. Added window tracking in GhosttyTerminalView to detect re-parenting within
   the same window and skip expensive layout/surface updates in that case.

These changes prevent unnecessary view teardown/recreation and reduce
flickering during workspace layout transitions.

Fixes manaflow-ai#1625
@vercel

vercel Bot commented Mar 18, 2026

Copy link
Copy Markdown

@BillionClaw is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps greptile-apps Bot left a comment

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 18, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Two targeted optimizations address terminal flicker during layout changes. ContentView now preserves view identity for mounted workspaces using .id(tab.id). GhosttyTerminalView detects same-window re-parenting and short-circuits expensive layout updates, invalidating only text input coordinates instead.

Changes

Cohort / File(s) Summary
View Identity & Re-parenting Optimization
Sources/ContentView.swift, Sources/GhosttyTerminalView.swift
ContentView adds stable identity via .id(tab.id) to prevent WorkspaceContentView recreation. GhosttyTerminalView introduces lastAttachedWindow tracking to detect same-window re-parenting and short-circuit layout/surface updates, invalidating only text input coordinates instead.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • #1204 — Modifies same files (ContentView, GhosttyTerminalView) to stabilize rendering and reduce re-renders; adds .id() and window re-parenting optimization
  • #1046 — Addresses terminal flicker by preserving view identity and geometry, avoiding layout-triggering updates during re-parenting
  • #1050 — Modifies GhosttyTerminalView window attachment behavior to stabilize terminal hosting during state transitions

Poem

🐰 A stable ID keeps views in place,
No ghostly flicker mars the space,
One window, one re-parent dance—
No costly layouts chance their prance,
Your terminals now hold their grace!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main issue being fixed and aligns with the changes made to prevent terminal flickering during layout changes.
Description check ✅ Passed The PR description comprehensively covers the root cause, changes made, testing performed, and related issues, matching the repository's template structure with all key sections populated.
Linked Issues check ✅ Passed The PR successfully addresses the requirements from issue #1625 by implementing stable view identity and window tracking to prevent unnecessary re-renders and layout updates that caused terminal flicker.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to addressing the terminal flicker issue; modifications to ContentView.swift and GhosttyTerminalView.swift are focused on the specific optimization paths needed to fix the problem.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:3974">
P1: Same-window reparent detection is neutralized by clearing `lastAttachedWindow` on transient detach, so the anti-flicker fast path often won’t trigger.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +3974 to +3975
lastAttachedWindow = nil
return

@cubic-dev-ai cubic-dev-ai Bot Mar 18, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Same-window reparent detection is neutralized by clearing lastAttachedWindow on transient detach, so the anti-flicker fast path often won’t trigger.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/GhosttyTerminalView.swift, line 3974:

<comment>Same-window reparent detection is neutralized by clearing `lastAttachedWindow` on transient detach, so the anti-flicker fast path often won’t trigger.</comment>

<file context>
@@ -3966,7 +3969,15 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
-        guard let window else { return }
+        guard let window else {
+            // View is being detached from a window - clear the tracking reference
+            lastAttachedWindow = nil
+            return
+        }
</file context>
Suggested change
lastAttachedWindow = nil
return
// Keep the previous window reference so a transient detach/reattach
// can still be recognized as same-window reparenting.
return
Fix with Cubic

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 3972-3980: The detach branch currently clears lastAttachedWindow
too early, causing a nil→same-window reattach to be misdetected; remove the
assignment that sets lastAttachedWindow = nil inside the guard-else detach path
and only update lastAttachedWindow when a non-nil window is present (keep the
existing comparison let isReparentingWithinSameWindow = lastAttachedWindow ===
window and the later lastAttachedWindow = window), so same-window reparenting is
detected correctly by the isReparentingWithinSameWindow check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 165ffad9-ef7e-48b8-9afe-77c0f376774d

📥 Commits

Reviewing files that changed from the base of the PR and between bdd9590 and aeab5e0.

📒 Files selected for processing (2)
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift

Comment on lines +3972 to +3980
guard let window else {
// View is being detached from a window - clear the tracking reference
lastAttachedWindow = nil
return
}

// Check if this is a re-parenting within the same window (no actual window change)
let isReparentingWithinSameWindow = lastAttachedWindow === window
lastAttachedWindow = window

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the complete viewDidMoveToWindow implementation and surrounding context
cd /tmp && git clone --depth 1 https://github.com/manaflow-ai/cmux.git 2>&1 | grep -E "(Cloning|done)" && cd cmux

Repository: manaflow-ai/cmux

Length of output: 617


🏁 Script executed:

# Get full context around the changed lines (3956-4015)
sed -n '3950,4020p' Sources/GhosttyTerminalView.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 3489


🏁 Script executed:

# Look for the fast-path branch that should be triggered (IME invalidation only)
sed -n '4000,4020p' Sources/GhosttyTerminalView.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 1120


🏁 Script executed:

# Search for where viewDidMoveToWindow is called during workspace transitions
rg -A 5 -B 5 "new-workspace\|workspace.*switch\|layout.*change" Sources/AppDelegate.swift Sources/Workspace.swift | head -100

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check if weak references are used in similar patterns elsewhere in this file
rg "weak var.*NSWindow" Sources/GhosttyTerminalView.swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look at the complete property declarations around line 3956
sed -n '3950,3965p' Sources/GhosttyTerminalView.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 724


🏁 Script executed:

# Search for workspace switch and panel reparenting logic
rg -B 3 -A 10 "func reconcilePanelPortalVisibilityForCurrentLayout\|func.*reparent\|new-workspace" Sources/Workspace.swift | head -80

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look for panel view reparenting during layout changes
rg -B 2 -A 8 "removeFromSuperview\|addSubview" Sources/Workspace.swift | grep -A 8 -B 2 "panel\|surface\|terminal" | head -100

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check what happens to GhosttySurfaceScrollView during workspace layout switches
rg -B 3 -A 5 "GhosttySurfaceScrollView" Sources/Workspace.swift | head -60

Repository: manaflow-ai/cmux

Length of output: 2179


🏁 Script executed:

# Check NSView reparenting patterns that could trigger viewDidMoveToWindow
rg "removeFromSuperview\|addSubview" Sources/Workspace.swift Sources/ContentView.swift | head -30

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search for any discussion or tracking of flicker issues during workspace changes
rg -B 2 -A 2 "flicker\|reparent.*window" Sources/GhosttyTerminalView.swift | head -50

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search for panel view hierarchy manipulation in ContentView (SwiftUI)
rg -B 3 -A 10 "if.*workspace\|if.*layout\|if panels" Sources/ContentView.swift | head -100

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look for panel content/hosting view updates
rg -B 2 -A 8 "\.panel\|panelContainers\|hostingController" Sources/Workspace.swift | head -80

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check if GhosttySurfaceScrollView is recreated or reused
rg "GhosttySurfaceScrollView()" Sources/ | head -20

Repository: manaflow-ai/cmux

Length of output: 2230


🏁 Script executed:

# Search for actual issue/discussion in comments about workspace flicker
rg -i "workspace.*flicker\|layout.*flicker\|same.*window.*reparent" Sources/ --type swift | head -20

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check the actual call chain: what triggers viewDidMoveToWindow during workspace ops
rg "new-workspace\|selectWorkspace\|activateWorkspace" Sources/AppDelegate.swift | head -10

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look at what Workspace.reconcile does (mentioned in learnings)
rg -B 5 -A 15 "func reconcile" Sources/Workspace.swift | head -100

Repository: manaflow-ai/cmux

Length of output: 3714


🏁 Script executed:

# Search for TerminalWindowPortal.detach which might trigger viewDidMoveToWindow
rg -B 5 -A 15 "static func detach\|func.*detach.*hostedView" Sources/TerminalWindowPortal.swift | head -80

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check what bind/detach do with view hierarchy
rg -B 3 -A 12 "func bind.*hostedView\|hostedView\.removeFromSuperview\|hostView\.addSubview" Sources/TerminalWindowPortal.swift | head -120

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check if new-workspace command triggers reconcile operations
rg -B 5 -A 10 "new-workspace.*handleCustomCommand\|v1NewWorkspace\|newWorkspace" Sources/AppDelegate.swift | head -80

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look at the actual detach implementation in TerminalWindowPortal
sed -n '1,600p' Sources/TerminalWindowPortal.swift | grep -n -A 20 "static func detach"

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check if there's a bind/detach cycle during layout reconciliation
rg -B 3 -A 8 "detach.*hostedView\|bind.*hostedView" Sources/Workspace.swift | head -100

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search more broadly for panel repositioning during layout changes
rg "setVisibleInUI\|reconcile.*visibility\|renderedVisiblePanelIds" Sources/Workspace.swift -A 5 | head -100

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look at the complete TerminalPanel definition to understand panel hosting
rg -B 3 -A 20 "class TerminalPanel\|struct TerminalPanel" Sources/Workspace.swift | head -80

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check if panels are recreated or just hidden/shown during workspace switches
rg "panels\[" Sources/Workspace.swift | head -30

Repository: manaflow-ai/cmux

Length of output: 1694


🏁 Script executed:

# Search in ContentView for panel view updates during workspace transitions
rg -B 3 -A 8 "\.workspace\|workspace.panels" Sources/ContentView.swift | head -100

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look for what actually causes viewDidMoveToWindow during normal terminal operations
rg "viewDidMoveToWindow\|window change" Sources/GhosttyTerminalView.swift -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check git log for recent changes to GhosttyTerminalView around viewDidMoveToWindow
git log --oneline -20 -- Sources/GhosttyTerminalView.swift

Repository: manaflow-ai/cmux

Length of output: 144


🏁 Script executed:

# Get the full details of the commit that added this fix
git show aeab5e0 --stat

Repository: manaflow-ai/cmux

Length of output: 50373


🏁 Script executed:

# Get the actual diff of the problematic code
git show aeab5e0 | head -200

Repository: manaflow-ai/cmux

Length of output: 8547


🏁 Script executed:

# Look at what the commit message says about the problem
git show aeab5e0 | grep -A 20 "^    "

Repository: manaflow-ai/cmux

Length of output: 1393


🏁 Script executed:

# Check if there are any related issues or discussion in recent PRs
git log --oneline --all --grep="flicker\|reparent" | head -10

Repository: manaflow-ai/cmux

Length of output: 726


Same-window reparent detection is reset too early

On line 25, clearing lastAttachedWindow during detach masks a nil→same-window reattach, so line 30 evaluates false and still runs the expensive path (the flicker scenario this PR targets).

💡 Proposed fix
-    private var lastAttachedWindow: NSWindow?
+    private weak var lastAttachedWindow: NSWindow?
@@
-        guard let window else {
-            // View is being detached from a window - clear the tracking reference
-            lastAttachedWindow = nil
-            return
-        }
+        guard let window else {
+            // Keep the previous window reference so nil -> same-window reattach
+            // can still be detected as same-window reparenting.
+            return
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 3972 - 3980, The detach
branch currently clears lastAttachedWindow too early, causing a nil→same-window
reattach to be misdetected; remove the assignment that sets lastAttachedWindow =
nil inside the guard-else detach path and only update lastAttachedWindow when a
non-nil window is present (keep the existing comparison let
isReparentingWithinSameWindow = lastAttachedWindow === window and the later
lastAttachedWindow = window), so same-window reparenting is detected correctly
by the isReparentingWithinSameWindow check.

@BillionClaw

Copy link
Copy Markdown
Contributor Author

Closing per repository blocklist: maintainer threatened to ban. All submissions to this repo have been suspended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

All terminal content flickers when CLI commands trigger layout changes (e.g. new-workspace)

1 participant