Skip to content

Fix sidebar freeze from spinner terminal titles (#6291) - #6840

Closed
austinywang wants to merge 12 commits into
mainfrom
issue-6291-sidebar-becomes-unclickable-when-cli-tools
Closed

austinywang wants to merge 12 commits into
mainfrom
issue-6291-sidebar-becomes-unclickable-when-cli-tools

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6291

Problem

CLI tools like pnpm, npm, and cargo animate a progress spinner by rewriting the terminal title on every animation frame with a leading Unicode braille glyph (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏). cmux forwards every title change through .ghosttyDidSetTitle into the workspace panel-title coalescer and the toolbar command-text updater.

Because every raw frame is a distinct string (only the spinner glyph differs), none of the existing dedup paths caught them: each frame mutated the workspace title, posted .workspaceTitleDidChange, and re-rendered the sidebar rows. The sheer volume starved main-thread hit-testing, so sidebar/tab clicks were swallowed until the spinner-emitting command finished — the user had to fall back to cmd+1/cmd+2 to navigate.

Fix

Collapse spinner frames to one stable title at every layer of the title pipeline:

  1. TerminalSurface.stableTerminalNotificationTitle(_:) — strips spinner glyphs (the full Braille Patterns block U+2800…U+28FF plus common non-braille rotating glyphs) and collapses the whitespace they bordered. Titles that contain no spinner glyph take a fast path and are returned byte-for-byte unchanged, so ordinary titles keep their exact text.
  2. TerminalSurface.publishableTerminalTitle(forRawTitle:) — dedupes at the source: the GHOSTTY_ACTION_SET_TITLE handler drops a post whose normalized title equals the last one published for that surface. A spinner storm now produces zero notifications, so the coalescer, sidebar layout, and toolbar updater are never woken.
  3. TabManager.enqueuePanelTitleUpdate — normalizes through the same shared algorithm and early-returns when the same stable title is already queued, or when Workspace.alreadyReflectsPanelTitleUpdate(panelId:title:) reports the workspace UI already shows it. This is the safety net for any post that bypasses the source-level dedup. A different pending title still falls through, so last-write-wins correctness is preserved.

The toolbar command-text path is already gated to the selected tab via shouldScheduleRawTitleRefresh, so it benefits from the reduced post volume with no further change.

A nice side effect: the sidebar/panel title now shows a stable pnpm install instead of a flickering ⠋ pnpm install.

Tests

Two-commit red/green structure:

  • Commit 1 (red): spinnerTerminalTitleFramesCollapseToStablePanelTitle posts the 10 braille spinner frames of pnpm install through the real .ghosttyDidSetTitle → TabManager path and asserts they collapse to one stable, spinner-free panel title with a single .workspaceTitleDidChange. Fails on main (panel title retains the last glyph; the workspace title changes once per frame).
  • Commit 2 (green): the fix, plus stableTerminalNotificationTitleStripsSpinnerGlyphs, a pure-algorithm unit test covering leading/embedded/trailing spinner glyphs, whitespace collapse, and the unchanged fast path.

Both tests live in the already-wired cmuxTests/TabManagerTitleUpdateTests.swift.

Localization

No user-facing string literals were added — terminal titles are dynamic content from the shell/CLI, not localizable literals. Localization audit: nothing to update.

🤖 Generated with Claude Code


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


Summary by cubic

Fixes sidebar and tab freezes caused by spinner-driven terminal titles by collapsing animation frames to one stable title and deduping updates end-to-end. Keeps the UI responsive during commands like pnpm install, npm install, and cargo build (fixes #6291).

  • Bug Fixes
    • Normalize titles via TerminalSurface.stableTerminalNotificationTitle: drop only standalone spinner tokens (full Braille block + common rotating glyphs) and one adjacent whitespace run; keep Braille scalars inside words; titles without spinners return unchanged.
    • Dedup at the source with TerminalSurface.publishableTerminalTitle; GhosttyTerminalView posts only the stable title to .ghosttyDidSetTitle; reset the dedupe state on workspace rebind so a moved surface can publish again.
    • TabManager.enqueuePanelTitleUpdate: use the source surface’s normalization; skip when the same stable title is already pending from the same surface; refresh the pending source when a surface is replaced but the stable title is unchanged.
    • Result: no notification floods or flicker; titles show “pnpm install”; clicks stay responsive. Tests cover frame collapse, standalone-token stripping, spacing preservation, replacement-surface refresh, and dedupe reset on rebind.
    • CI: Hardened bash integration cleanup to terminate background jobs and descendants on exit, reducing test flakiness.

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

Review in cubic

cmux and others added 2 commits June 26, 2026 00:48
CLI tools like pnpm/npm/cargo animate a spinner by rewriting the terminal
title every frame with a leading braille glyph. Each raw frame is a distinct
string, so every frame mutates the workspace title and posts
.workspaceTitleDidChange, thrashing the sidebar's main-thread layout until
clicks are starved.

This test posts the braille spinner frames of "pnpm install" and asserts they
collapse to one stable, spinner-free panel title with a single workspace-title
change. It fails on current main (the panel title retains the last spinner
glyph and the workspace title changes once per frame).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLI tools (pnpm, npm, cargo, …) animate a spinner by rewriting the terminal
title on every frame with a leading braille glyph (⠋⠙⠹…). cmux forwarded every
title through .ghosttyDidSetTitle, and because each raw frame is a distinct
string, no existing dedup caught them: each frame mutated the workspace title,
posted .workspaceTitleDidChange, and re-rendered the sidebar rows. The volume
starved main-thread hit-testing, so sidebar/tab clicks were swallowed until the
spinner-emitting command finished.

Collapse spinner frames to one stable title at every layer:

- TerminalSurface.stableTerminalNotificationTitle strips spinner glyphs (the
  full Braille Patterns block plus common rotating glyphs) and collapses the
  whitespace they bordered; titles without a spinner glyph are returned verbatim.
- TerminalSurface.publishableTerminalTitle dedupes at the source: the SET_TITLE
  handler drops a post whose normalized title equals the last one published for
  that surface, so a spinner storm produces no notifications at all.
- TabManager.enqueuePanelTitleUpdate normalizes via the shared algorithm and
  early-returns when the same title is already queued or the workspace already
  shows it (Workspace.alreadyReflectsPanelTitleUpdate), so any post that bypasses
  the source dedup still cannot thrash the sidebar.

The toolbar command-text path is already gated to the selected tab via
shouldScheduleRawTitleRefresh, so it benefits from the reduced post volume
without further change.

Adds a pure-algorithm unit test and makes the behavior regression test from the
previous commit pass (spinner frames collapse to one stable panel title with a
single workspace-title change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 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 Jul 5, 2026 12:15am
cmux-staging Building Building Preview, Comment Jul 5, 2026 12:15am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@austinywang, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1663066e-1c13-449c-bb21-b9f581819fb9

📥 Commits

Reviewing files that changed from the base of the PR and between 9c91710 and e131580.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • cmuxTests/TabManagerTitleUpdateTests.swift
  • tests/test_bash_integration_no_done_notifications.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6291-sidebar-becomes-unclickable-when-cli-tools

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.

@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a main-thread starvation issue (#6291) where CLI tools like pnpm, npm, and cargo animate a progress spinner by rewriting the terminal title on every frame — each distinct string bypassed existing dedup and posted .workspaceTitleDidChange per frame, flooding the sidebar layout until clicks were swallowed. The fix collapses spinner frames to one stable title at both the source layer and the TabManager safety net layer, using a shared normalization algorithm.

  • TerminalSurface.stableTerminalNotificationTitle strips whitespace-delimited pure-spinner tokens (the full Braille Patterns block U+2800–U+28FF plus common non-braille rotating glyphs), with a fast path for titles containing no spinner glyphs. Embedded braille in mixed words is correctly preserved.
  • TerminalSurface.publishableTerminalTitle(forRawTitle:) deduplicates at the source: the GHOSTTY_ACTION_SET_TITLE handler drops notifications whose normalized title equals the last one published for that surface.
  • TabManager.enqueuePanelTitleUpdate applies the same normalization as a safety net and early-returns when the same stable title is already pending from the same surface, with correct pass-through for replacement surfaces.
  • CI teardown in test_bash_integration_no_done_notifications.py is hardened to kill child process descendants and avoid leaked background jobs between test runs.

Confidence Score: 5/5

Safe to merge — the normalization algorithm is correct, dedup logic is layered and well-tested, and no existing title update paths are regressed.

The spinner-stripping algorithm correctly handles all described edge cases (embedded Braille preserved, multi-position standalone tokens, multi-token titles, whitespace collapsing). The three-layer dedup is sound and the replacement-surface test directly exercises the trickiest interaction. No correctness or isolation regressions were found.

No files require special attention.

Important Files Changed

Filename Overview
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift Adds stableTerminalNotificationTitle, publishableTerminalTitle, terminalTitleSpinnerScalars, and lastPublishedTerminalTitle. Algorithm is correct; embedded braille preserved; fast path avoids allocation on non-spinner titles.
Sources/GhosttyTerminalView.swift Gates GHOSTTY_ACTION_SET_TITLE notification post on publishableTerminalTitle returning non-nil. Spinner-storm frames now produce zero notifications.
Sources/TabManager.swift Adds normalization via stableTerminalNotificationTitle and early-return guard for same-title same-surface pending updates, with correct fall-through for replacement surfaces.
cmuxTests/TabManagerTitleUpdateTests.swift Adds four new tests covering spinner collapse, replacement-surface refresh, stableTerminalNotificationTitle algorithm, and publishableTerminalTitle reset-on-rebind.
tests/test_bash_integration_no_done_notifications.py Hardens teardown with _kill_descendants (SIGTERM then SIGKILL), shell-side job-kill loop, and shlex.quote for PS1. Reduces test flakiness.
.github/swift-file-length-budget.tsv Updates line-count budget for changed files; all within policy limits.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI as CLI Tool
    participant GH as GhosttyTerminalView
    participant TS as TerminalSurface
    participant NC as NotificationCenter
    participant TM as TabManager
    participant WS as Workspace

    CLI->>GH: frame 1 title
    GH->>TS: publishableTerminalTitle
    TS-->>GH: stable title (publish)
    GH->>NC: post .ghosttyDidSetTitle
    NC->>TM: enqueuePanelTitleUpdate
    TM->>WS: flush panel title

    CLI->>GH: frame 2 title
    GH->>TS: publishableTerminalTitle
    TS-->>GH: nil (same stable title, drop)
    Note over GH,WS: Zero additional mutations
Loading
%%{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 CLI as CLI Tool
    participant GH as GhosttyTerminalView
    participant TS as TerminalSurface
    participant NC as NotificationCenter
    participant TM as TabManager
    participant WS as Workspace

    CLI->>GH: frame 1 title
    GH->>TS: publishableTerminalTitle
    TS-->>GH: stable title (publish)
    GH->>NC: post .ghosttyDidSetTitle
    NC->>TM: enqueuePanelTitleUpdate
    TM->>WS: flush panel title

    CLI->>GH: frame 2 title
    GH->>TS: publishableTerminalTitle
    TS-->>GH: nil (same stable title, drop)
    Note over GH,WS: Zero additional mutations
Loading

Reviews (9): Last reviewed commit: "Reset terminal title dedupe on workspace..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
Comment thread Sources/TabManager.swift Outdated
cmux and others added 3 commits June 26, 2026 02:14
…6291)

Codex review (P3): the previous normalization deleted every spinner scalar
anywhere in the title, which would corrupt a legitimate title that merely
contains a Braille scalar inside a word (e.g. a path like ~/work/⠋-project).
Rework stableTerminalNotificationTitle to drop only whitespace-delimited tokens
that are made up ENTIRELY of spinner glyphs (a standalone animation frame at any
position); every other token is preserved verbatim. Add unit coverage proving a
Braille scalar inside a path component survives even when a leading spinner is
stripped.

Greptile review: remove the redundant Workspace.alreadyReflectsPanelTitleUpdate
predicate (it shadowed the mutation conditions of updatePanelTitle/
applyProcessTitle and was effectively dead in production — the source-level
publishableTerminalTitle dedup already prevents the flood, and updatePanelTitle
is idempotent). Inline the one-call-site stableTerminalPanelTitle wrapper. The
TabManager safety-net keeps the cheap, non-shadowing 'same title already pending'
guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerated via scripts/swift_file_length_budget.py --write-budget after merging
origin/main. Raises budgets for the four files this PR grows (TerminalSurface,
TabManager, GhosttyTerminalView, TabManagerTitleUpdateTests) and ratchets down
files that shrank on main since the budget was last written.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…comes-unclickable-when-cli-tools

# Conflicts:
#	.github/swift-file-length-budget.tsv
@Solarrixs

Copy link
Copy Markdown
Contributor

Same story - the SET_TITLE integration point this targets is gone on main (your TabManager hunks still apply, but the load-bearing part conflicts). Details here: #6291 (comment)

@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 — e131580d Deployed Jul 5, 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.

Sidebar becomes unclickable when CLI tools emit frame-by-frame spinner characters in terminal titles

4 participants