Skip to content

Fix glued prompt after scrollback restore (#2823) - #5853

Closed
grantland wants to merge 2 commits into
manaflow-ai:mainfrom
grantland:cmux-2823
Closed

grantland wants to merge 2 commits into
manaflow-ai:mainfrom
grantland:cmux-2823

Conversation

@grantland

@grantland grantland commented Jun 10, 2026 •

Copy link
Copy Markdown

Fixes #2823 (the scrollback-restore portion).

Summary

  • What changed?

    • Fix: SessionScrollbackReplayStore.normalizedScrollback now guarantees a
      single trailing newline on the replayed scrollback. Captured scrollback ends
      at the prompt line where the cursor sat, which has no trailing newline, so a
      bare replay (cat) glued the freshly-restored live prompt onto the end of the
      old prompt line (…$ …$). The newline is added when the replay file is written
      at terminal spawn, so it applies to both bash and zsh and even to scrollback
      saved by older builds.
    • Docs: Added docs/shell-integration.md explaining how cmux's shell
      integration loads (bash via an exported PROMPT_COMMAND bootstrap; zsh via
      ZDOTDIR), how a dotfile that assigns PROMPT_COMMAND silently wipes it out
      (breaking new-tab cwd inheritance, scrollback restore, and shell-state
      reporting), the append / direct-source fixes, the analogous (rarer) zsh
      hook-array risk, and a troubleshooting check.
  • Why?

    • While investigating Sessions are lost after app restart #2823 ("sessions/terminal contents lost after restart"),
      the root cause for affected bash users turned out to be a dotfile overwriting
      PROMPT_COMMAND, which prevents cmux's shell integration from ever loading —
      so scrollback was never persisted and never replayed. Once the integration
      loads, restore works, which surfaced a separate, long-latent cosmetic bug: the
      restored prompt and the live prompt rendered on the same line. This PR fixes
      that cosmetic bug and documents the integration-loading pitfall so others can
      self-diagnose the underlying issue.

Testing

  • How did you test this change?
    • Built a tagged Debug app (CMUX_SKIP_ZIG_BUILD=1 ./scripts/reload.sh --tag cmux-2823)
      and exercised the real save → quit → relaunch → replay path on macOS.
  • What did you verify manually?
    • Before the fix: the restored (old) prompt and the new live prompt rendered on
      the same line (…$ …$).
    • After the fix: the restored prompt sits on its own line with the live prompt
      below it.
    • Verified end-to-end that the newline survives the shell side:
      _cmux_restore_scrollback_once uses a direct /bin/cat -- "$path" (not
      $(cat)), so the trailing newline written by writeReplayFile is preserved.
    • Separately confirmed the documented root cause: with the integration loaded,
      type -t _cmux_restore_scrollback_once returns function and scrollback
      restores; with PROMPT_COMMAND overwritten it does not.

Demo Video

For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).

  • Video URL or attachment: N/A — before/after is a one-line terminal rendering difference; can attach a screenshot of the restored prompt on its own line if preferred.

Review Trigger (Copy/Paste as PR comment)

​text @codex review @coderabbitai review @greptile-apps review @cubic-dev-ai review ​

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes — intentionally omitted: the newline is only observable through real shell replay, which isn't exercisable in a unit test today; asserting a trailing \n on the private helper would be a shape test, not behavior (per the repo's test-quality policy). Happy to add a runtime seam in a follow-up if desired.
  • I updated docs/changelog if needed — added docs/shell-integration.md.
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

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


Summary by cubic

Fixes the glued prompt after scrollback restore by guaranteeing a single trailing newline and adding it before ANSI wrapping, so the live prompt starts on its own line without extra blank lines. Adds docs/shell-integration.md explaining how integration loads (bash via PROMPT_COMMAND bootstrap; zsh via ZDOTDIR), common clobbers, and troubleshooting for #2823.

Written for commit 04e7377. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Fixed scrollback replay so restored output no longer concatenates the live prompt onto the final captured line.
  • Documentation

    • Added a shell integration guide covering automatic cwd inheritance, scrollback restoration after quit/relaunch, shell activity reporting for persistence/close-confirmation UX, common failure modes, and troubleshooting steps.

Captured scrollback ends at the prompt line where the cursor sat, with no
trailing newline, so a bare replay glued the freshly-restored live prompt onto
the end of the old prompt line. Guarantee a single trailing newline in
normalizedScrollback so the live shell's first prompt starts on its own line.
Applied at replay time, so it fixes both bash and zsh and even scrollback saved
by older builds.

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

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ab4399a4-7750-434d-9b3d-baad3eb3d958

📥 Commits

Reviewing files that changed from the base of the PR and between 150dc90 and 04e7377.

📒 Files selected for processing (1)
  • Sources/SessionPersistence.swift

📝 Walkthrough

Walkthrough

This PR ensures truncated, ANSI-safe scrollback ends with a trailing newline before replay to avoid prompt concatenation, and adds documentation describing cmux’s automatic shell integration for bash and zsh, its failure modes, and troubleshooting steps.

Changes

Session Persistence and Restoration

Layer / File(s) Summary
Scrollback replay normalization
Sources/SessionPersistence.swift
SessionScrollbackReplayStore.normalizedScrollback now appends a trailing newline when missing, then passes the newline-normalized truncated ANSI-safe scrollback into ansiSafeReplayText, preventing restored live prompt concatenation onto the final captured line.
Shell integration documentation
docs/shell-integration.md
Documents cmux automatic shell integration for bash and zsh: load mechanisms (bash PROMPT_COMMAND bootstrap, zsh ZDOTDIR wrapper and add-zsh-hook), common failure modes (notably PROMPT_COMMAND clobbering), remediation examples, and troubleshooting verification steps.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • manaflow-ai/cmux#5175: Both PRs modify SessionScrollbackReplayStore.normalizedScrollback to post-process restored scrollback (one ensures a trailing newline; the other strips terminal-color OSC sequences).

Poem

🐰 I nudge the scrollback, tuck a newline in tight,

so prompts won't leap forward in the soft morning light.
Docs map the hooks where shells may meet,
restoring our sessions so relaunches are sweet.


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Source Artifacts ❌ Error .claude/scheduled_tasks.lock contains runtime lock data (sessionId, pid, timestamps) from Claude IDE, violating the "local tool output" artifact rule. Similar paths like .claude/worktrees/ are alre... Remove .claude/scheduled_tasks.lock from the commit, or add a broader .claude/ pattern to .gitignore (or keep only the documented .claude/commands/ subdirectory as config).
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 (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing a visual bug where restored scrollback prompt and live prompt render on the same line.
Description check ✅ Passed The description comprehensively covers what changed and why, with detailed testing methodology and manual verification steps, though bot review requests were not completed.
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 Actor Isolation ✅ Passed Production change modifies SessionScrollbackReplayStore.normalizedScrollback() to add newline handling using only immutable String values in a static utility enum; no actor isolation issues introdu...
Cmux Swift Blocking Runtime ✅ Passed Pull request introduces only non-blocking string manipulation in SessionPersistence.swift and documentation changes; no semaphores, waits, sleeps, or manual locks detected.
Cmux Expensive Synchronous Load ✅ Passed PR changes only string manipulation in SessionPersistence.swift for scrollback restoration (adding trailing newline before ANSI processing) and adds documentation. No expensive synchronous loaders...
Cmux Cache Substitution Correctness ✅ Passed The PR modifies normalizedScrollback in a persistence path but makes no cache substitutions. It adds a trailing newline transformation to fresh input before ANSI processing—no caching involved.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code and documentation changes; rule applies exclusively to TypeScript, JavaScript, shell, and build/runtime scripts, which are absent from this PR.
Cmux Algorithmic Complexity ✅ Passed PR adds trailing newline check to scrollback restoration before ANSI wrapping; no nested loops, repeated scans, or algorithmic complexity violations introduced; string size explicitly bounded to 40...
Cmux Swift Concurrency ✅ Passed The PR changes only modify the normalizedScrollback function to add trailing newline handling—a synchronous string operation. No legacy concurrency patterns (DispatchQueue, Combine, completion hand...
Cmux Swift @Concurrent ✅ Passed No async or concurrent annotation violations found. SessionScrollbackReplayStore methods are all synchronous (non-async) helper functions doing lightweight string manipulation and file I/O. Per rul...
Cmux Swift File And Package Boundaries ✅ Passed The PR makes a focused bug fix adding 9 lines to SessionScrollbackReplayStore.normalizedScrollback in an existing file (2142 lines total). While the file is large, it maintains clear persistence/sn...
Cmux Swift Logging ✅ Passed PR introduces only string operations in normalizedScrollback; no print, NSLog, debugPrint, dump, or ad hoc logging added to production Swift code.
Cmux User-Facing Error Privacy ✅ Passed PR adds code comment (developer-only) and documentation (allowed case). No user-facing errors expose vendor names, provider details, credentials, or sensitive information per the rules.
Cmux Full Internationalization ✅ Passed PR contains only a technical code fix (trailing newline handling) and operational docs not shown to end users; no user-facing text, string catalogs, or web UI changes requiring internationalization.
Cmux Swiftui State Layout ✅ Passed PR changes only SessionPersistence.swift (backend data model with no SwiftUI state) and documentation; no ObservableObject, @Published, @Observable, GeometryReader, lazy containers, or render-time...
Cmux Architecture Rethink ✅ Passed The PR's Swift change is a small local correctness fix (moving trailing newline insertion before ansiSafeReplayText wrapping) with clear ownership and invariant, matching the allowed case in swift-...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR changes SessionPersistence.swift (scrollback fix) and shell-integration.md docs; no NSWindow/NSPanel/WindowGroup declarations, no cmux.* window identifiers, and linting script passes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the cosmetic "glued prompt" bug (#2823) where replayed scrollback rendered its last line and the fresh live prompt on the same line. The fix adds a trailing newline to the captured buffer before ANSI wrapping so the reset character always trails the newline rather than defeating the suffix check.

  • Sources/SessionPersistence.swift: normalizedScrollback now guarantees exactly one trailing \"\ \" on truncated before calling ansiSafeReplayText; this ensures the ANSI reset wraps cleanly over the content without introducing a second blank line.
  • docs/shell-integration.md: New developer doc explaining how bash/zsh integration loads, the PROMPT_COMMAND-clobber failure mode that silently breaks scrollback restore, two safe alternatives, and a type -t / typeset -f troubleshooting check.

Confidence Score: 5/5

Safe to merge — the change is a focused two-line fix to a private helper with no effect on any other code path.

The newline is added before ANSI wrapping, so the reset character always trails the newline and the live prompt starts on its own line. Both the 'no trailing newline' (common) and 'already ends in newline' (rare) cases are handled correctly by the hasSuffix guard. The docs file is additive and operational-only.

No files require special attention.

Important Files Changed

Filename Overview
Sources/SessionPersistence.swift Adds a single trailing newline to scrollback before ANSI-wrapping, fixing the glued-prompt cosmetic bug; the ordering (before not after ansiSafeReplayText) is correct and the hasSuffix guard prevents double newlines.
docs/shell-integration.md New operational docs file explaining bash/zsh integration loading, PROMPT_COMMAND clobber failure mode, and troubleshooting steps; no user-facing UI text, so no i18n concern.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SC as Captured Scrollback
    participant NS as normalizedScrollback()
    participant AN as ansiSafeReplayText()
    participant RF as writeReplayFile()
    participant SH as Shell (cat replay)

    SC->>NS: raw scrollback string
    NS->>NS: strip theme OSC sequences
    NS->>NS: truncate per policy
    NS->>NS: hasSuffix("\\n") ? keep : append "\\n"
    NS->>AN: withTrailingNewline
    AN->>AN: wrap with ESC[0m if ANSI escapes present
    AN-->>NS: "ESC[0m…content\\nESC[0m"
    NS->>RF: normalized text
    RF-->>SH: replay file path (env var)
    SH->>SH: /bin/cat -- "$path" → cursor lands on new line
    Note over SH: Live prompt renders below restored content
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 SC as Captured Scrollback
    participant NS as normalizedScrollback()
    participant AN as ansiSafeReplayText()
    participant RF as writeReplayFile()
    participant SH as Shell (cat replay)

    SC->>NS: raw scrollback string
    NS->>NS: strip theme OSC sequences
    NS->>NS: truncate per policy
    NS->>NS: hasSuffix("\\n") ? keep : append "\\n"
    NS->>AN: withTrailingNewline
    AN->>AN: wrap with ESC[0m if ANSI escapes present
    AN-->>NS: "ESC[0m…content\\nESC[0m"
    NS->>RF: normalized text
    RF-->>SH: replay file path (env var)
    SH->>SH: /bin/cat -- "$path" → cursor lands on new line
    Note over SH: Live prompt renders below restored content
Loading

Reviews (4): Last reviewed commit: "Move trailing-newline insertion before a..." | Re-trigger Greptile

Comment thread Sources/SessionPersistence.swift Outdated
Comment on lines +2008 to +2014
let safe = ansiSafeReplayText(truncated)
// The captured scrollback ends at the prompt line where the cursor sat,
// which has no trailing newline. A bare replay (`cat`) would then glue the
// freshly-restored live prompt onto the end of that old prompt line
// ("…$ …$"). Guarantee a trailing newline so the live shell's first prompt
// starts on its own line (https://github.com/manaflow-ai/cmux/issues/2823).
return safe.hasSuffix("\n") ? safe : safe + "\n"

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 When ansiSafeReplayText runs on scrollback that already ends in " ", it appends ansiReset after that newline (because hasSuffix(ansiReset) is false), yielding … \u{001B}[0m. The newline check then sees hasSuffix(" ") as false and appends another " ", producing … \u{001B}[0m . On replay this writes an extra blank line between the restored prompt and the live one — the opposite cosmetic artifact from the original glue bug. The case is rare (the PR notes captured scrollback typically has no trailing newline), but adding the newline before calling ansiSafeReplayText avoids the interaction entirely: the reset wraps the content and the newline is always the final character.

Suggested change
let safe = ansiSafeReplayText(truncated)
// The captured scrollback ends at the prompt line where the cursor sat,
// which has no trailing newline. A bare replay (`cat`) would then glue the
// freshly-restored live prompt onto the end of that old prompt line
// ("…$ …$"). Guarantee a trailing newline so the live shell's first prompt
// starts on its own line (https://github.com/manaflow-ai/cmux/issues/2823).
return safe.hasSuffix("\n") ? safe : safe + "\n"
// The captured scrollback ends at the prompt line where the cursor sat,
// which has no trailing newline. A bare replay (`cat`) would then glue the
// freshly-restored live prompt onto the end of that old prompt line
// ("…$ …$"). Guarantee a trailing newline so the live shell's first prompt
// starts on its own line (https://github.com/manaflow-ai/cmux/issues/2823).
let newlineTerminated = truncated.hasSuffix("\n") ? truncated : truncated + "\n"
return ansiSafeReplayText(newlineTerminated)

…w-ai#5853 review)

When the captured scrollback both contains ANSI and already ended in a newline,
the previous two-step approach (wrap, then append \n) produced …\n␛[0m\n — an
extra blank line between the restored content and the live prompt, the inverse of
the original glue bug. Append the newline before wrapping so the cursor-neutral
ANSI reset trails it and exactly one newline is ever emitted.

Co-Authored-By: Claude <noreply@anthropic.com>
@grantland

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 22, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@grantland I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,260 of the 240,000 allowed lines of code this month. Reviews resume on 1 July 2026 (in 9 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the PR diff
gh pr diff 5853 --repo manaflow-ai/cmux

echo "=== PR FILES ==="
gh pr view 5853 --repo manaflow-ai/cmux --json files --jq '.files[].path'

Length of output: 7093



@grantland — reviewed! Here's my full assessment:


Sources/SessionPersistence.swift

The core fix is clean and correct.

Logic is sound: hasSuffix("\n") guards against inserting a second newline when the captured buffer already ends with one, and placing the insertion before ansiSafeReplayText is the right call — the ANSI reset that function appends then naturally follows the newline rather than interposing itself and defeating the check.

One minor edge case worth acknowledging: if truncated is non-nil but empty (e.g., the scrollback was stripped down to nothing by strippingTerminalColorOSCSequences + truncatedScrollback), it will become "\n" and a single bare-newline replay is written. That's probably fine behaviour — better than nil which would skip replay entirely — but it's a silent path.

// Optional: make the intent explicit
let withTrailingNewline: String
if truncated.isEmpty {
    return nil  // or ansiSafeReplayText(truncated) — preserve existing behaviour
} else {
    withTrailingNewline = truncated.hasSuffix("\n") ? truncated : truncated + "\n"
}
return ansiSafeReplayText(withTrailingNewline)

Not blocking — just worth a conscious decision either way.


docs/shell-integration.md

Well-structured and fills a real gap. A couple of observations:

1. CMUX_SHELL_INTEGRATION=0 appears only in the zsh section

It's listed as one of the zsh-specific ways to break the integration, but it almost certainly applies to both shells. Bash users who stumble on this may not realise the override flag exists. Consider moving it to a shared note or the troubleshooting section.

2. Bash append example ordering

PROMPT_COMMAND="history -a${PROMPT_COMMAND:+; $PROMPT_COMMAND}"

This places history -a before the cmux bootstrap in the command list. The bootstrap still runs (it's at the end), and after it self-replaces on the first prompt the hook is prepended correctly — so it's functionally fine. But it's the opposite of the "prepend cmux, append yours" mental model. The more conventional append form is:

PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }history -a"

Either works; the current example is just slightly non-idiomatic and might confuse readers who try to reason about ordering.

3. Troubleshooting function check is necessary but not sufficient

type -t _cmux_restore_scrollback_once confirms the integration loaded, but not that scrollback is actually being captured/replayed (e.g., a user whose dotfiles wiped the hook after the bootstrap ran would pass this check but still get no scrollback). Could add a secondary check like verifying a recent scrollback file exists, or noting the limitation of the function-presence test.


Overall: the fix is minimal and well-motivated, the comment explains the non-obvious ordering constraint clearly, and the documentation covers the problem space thoroughly. The items above are all minor — nothing blocking.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

You had this first. The scrollback newline fix and shell-integration guide are now on main in #15152 and #15116, so I’m closing this landed-elsewhere PR. Thank you :)

#15152
#15116

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.

Sessions are lost after app restart

2 participants