Skip to content

Re-inject OSC 133 prompt mark into replayed scrollback (#6691) - #6908

Closed
austinywang wants to merge 44 commits into
mainfrom
issue-6691-semantic-prompt-bar-last-user-message
Closed

austinywang wants to merge 44 commits into
mainfrom
issue-6691-semantic-prompt-bar-last-user-message

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6691

Problem

The semantic prompt affordances on a coding-agent tab (jump-to-prompt, click-to-move, prompt-boundary selection, the orange semantic-prompt overlay) disappear after an auto-resume rebuild and don't come back when sending new messages — only a full agent exit + resume restores them.

Root cause (confirmed by reading the code)

cmux captures session scrollback via Ghostty's write_screen_file:copy,vt export (ghostty/src/terminal/formatter.zig). That exporter emits OSC 4/7/8/10/11 but never re-emits OSC 133 semantic-prompt markers — they're per-row/per-cell metadata the VT formatter drops. So saved scrollback is "plain" as far as semantic prompts go.

On the scrollback-replay restore path (SessionScrollbackReplayStore → CMUX_RESTORE_SCROLLBACK_FILE → shell-integration cat), the rebuilt screen therefore has every row at semantic_prompt == .none, so jump_to_prompt / cursor-click-to-move / prompt-boundary selection (all Ghostty per-row consumers) silently no-op. Agents like Claude Code emit the prompt-start mark only once at process startup, so it never returns on its own.

Fix

The proper fix — preserving OSC 133 in Ghostty's VT exporter — lives in the pinned ghostty submodule and would require republishing the GhosttyKit xcframework, so this fixes it at the cmux replay layer instead.

SessionScrollbackReplayStore.reinjectingLastPromptMark finds the restored last user message's prompt row and prepends an OSC 133 ; A ; cl=line marker (the same form cmux's shell integration emits), re-marking that row as a prompt. The row finder (promptRowIndex):

  • anchors to the row start allowing only a known prompt sigil (>, ❯, ›, », ▶, │, ┃, $, %) — never an unconstrained substring scan — so it excludes agent output that echoes the user's words mid-sentence ("I'll refactor the login flow", "Summary: …") and Markdown list/heading echoes ("- refactor…", "# Refactor…");
  • prefers a sigil-prefixed candidate over a bare one (a real prompt leads with a sigil; a bare line opening with the words is more likely agent prose);
  • matches whitespace-stripped, so soft- and hard- (mid-word, narrow-grid) wrapped and multiline prompts still match across captured rows;
  • keeps the bottom-most of the chosen kind so a prompt resubmitted across turns restores to the latest turn;
  • no-ops safely when the message is unknown or unmatched, so plain shells and non-agent terminals are untouched.

The workspace's most recent submitted prompt is persisted on the terminal snapshot (SessionTerminalPanelSnapshot.lastUserMessage) only into the snapshot of the terminal whose saved scrollback actually contains that prompt row (scrollbackContainsPromptRow) — so it's never copied into an unrelated panel's snapshot and never persists any input the saved scrollback doesn't already carry. It's threaded back through replayEnvironment at restore.

Scope

Targets the cmux scrollback-replay rebuild path. A true restorable agent that redraws via its own --resume (no cmux scrollback replay) is a separate mechanism; this fix activates wherever cmux replays scrollback that contains the prompt.

Tests

Two-commit red/green so CI proves the test catches the bug (commit 1 = failing test + no-op stub; commit 2 = implementation). Coverage in cmuxTests/SessionPersistenceTests.swift: basic injection, nil/blank no-op, repeated-prompt most-recent selection, mid-sentence/Markdown-bullet/heading/bare agent-echo exclusion, soft-wrapped + multiline prompts, the scrollbackContainsPromptRow capture gate, a snapshot→restore→replay-file integration test, and an end-to-end replayEnvironment round-trip.

Note on test framework: the new cases extend the existing SessionScrollbackReplayStore XCTest suite already living in SessionPersistenceTests.swift; keeping them there avoids splitting one behavior suite across XCTest and Swift Testing (per the documented cmux test-policy exception). No new test file → no pbxproj wiring needed.

Localization

No user-facing strings added or changed — the only new literals are OSC 133 control bytes (terminal protocol, not UI). No Localizable.xcstrings / web message catalog changes required.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Terminal session restore now preserves the most recent prompt position more reliably by carrying a lightweight prompt-match hint with terminal snapshots.
  • Bug Fixes
    • Scrollback replay reinjects the semantic prompt-start marker into the correct restored prompt row, with safeguards against duplicates and incorrect placement (including wrapped and multiline prompts).
  • Tests
    • Added regression tests covering marker reinjection behavior, snapshot Codable round-trips, replay file contents, and prompt-match key generation/bounds.

cmux and others added 2 commits June 26, 2026 04:51
cmux captures session scrollback via Ghostty's `write_screen_file:copy,vt`
export, which emits OSC 4/7/8/10/11 but NOT OSC 133 semantic-prompt marks.
So replayed scrollback loses the per-row prompt metadata that drives
jump-to-prompt and click-to-move. Agents such as Claude Code emit the
prompt-start mark only once at process startup, so after an auto-resume
rebuild the affordance never returns.

This commit adds a regression test asserting that the replay store
re-injects an OSC 133 ; A marker before the restored last user message,
plus a `reinjectingLastPromptMark` stub that returns the scrollback
unchanged so the test fails (red). The implementation follows in the next
commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ghostty's `write_screen_file:copy,vt` export (used to capture session
scrollback) emits OSC 4/7/8/10/11 but drops OSC 133 semantic-prompt marks.
So when cmux rebuilds an agent terminal via app-restart auto-resume
(`terminal.autoResumeAgentSessions`) and replays the saved scrollback, the
new screen has no per-row semantic-prompt metadata. Agents such as Claude
Code emit the prompt-start mark only once at process startup, so the marks
never come back and prompt-navigation affordances (jump-to-prompt,
click-to-move, prompt-boundary selection) stay broken until a fresh resume.

Fix at the replay layer (the proper fix — preserving OSC 133 in Ghostty's
VT exporter — would require republishing the pinned GhosttyKit xcframework):

- `SessionScrollbackReplayStore.reinjectingLastPromptMark` locates the last
  occurrence of the restored last user message in the replayed scrollback
  (ANSI/OSC-stripped, whitespace-collapsed, prefix match so soft-wrapped and
  SGR-interleaved rows still match) and prepends an `OSC 133 ; A ; cl=line`
  marker so the rebuilt row is marked as a prompt. No-ops safely when the
  message is unknown or not found, so plain shells are untouched.
- Persist the workspace's most recent submitted prompt on the terminal
  snapshot (`lastUserMessage`, agent panels only) and thread it through
  `replayEnvironment` at restore.

Implements the regression test from the previous commit (now green) plus
matching/round-trip coverage.

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 Canceled Canceled Jul 5, 2026 1:57am
cmux-staging Building Building Preview, Comment Jul 5, 2026 1:57am

@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR re-injects OSC 133 semantic prompt-start markers into replayed scrollback on session restore, fixing broken jump-to-prompt, click-to-move, and prompt-boundary affordances after an auto-resume rebuild. Because Ghostty's write_screen_file:copy,vt exporter silently drops OSC 133 per-row metadata, the fix operates at the cmux replay layer by identifying and marking the last user prompt row before writing the scrollback replay file.

  • Prompt row detection (promptRowIndex) is sigil-anchored (requires >, ❯, $, etc.) and rejects plain Markdown blockquotes and bare agent-echo lines; it returns nil on ambiguity so misidentification is impossible at the cost of occasionally not injecting.
  • Ownership gate (sessionPromptMarkKeyForSnapshot) ensures the bounded ≤48-character match key is persisted only into the snapshot of the terminal panel that actually submitted the prompt and whose scrollback contains the matching row — never into unrelated panels.
  • surfaceId threading through handlePromptSubmit → recordSubmittedMessage provides a reliable event-sourced panel identity rather than relying solely on focusedPanelId at snapshot time.

Confidence Score: 5/5

Safe to merge; all changes are additive and fail-closed — the injection no-ops when no unambiguous sigil-prefixed match is found, and the scrollback gate prevents the key from leaking into unrelated panels.

The matching logic is carefully anchored to known prompt sigils and validated with a thorough test suite covering agent-echo exclusion, blockquote disambiguation, wrapped/multiline prompts, snapshot round-trips, and end-to-end replay file contents. The previous review concern about contains over-matching has been addressed by replacing it with sigil-anchored prefix matching and an ambiguity guard. No blocking issues remain.

No files require special attention. Sources/SessionPersistence.swift contains all the new matching and injection logic and is the natural place for a second look, but the tests cover its behavior comprehensively.

Important Files Changed

Filename Overview
Sources/SessionPersistence.swift Adds ~210 lines of OSC 133 re-injection logic: reinjectingLastPromptMark, promptRowIndex (sigil-anchored, ambiguity-guarded row finder), persistablePromptMatchKey, strippingEscapeSequences, and rawLineHasStyledAnglePromptSigil. Also adds lastPromptMarkKey to SessionTerminalPanelSnapshot.
Sources/Workspace.swift Adds latestSubmittedPanelId, restoredPromptMarkKeysByPanelId, and sessionPromptMarkKeyForSnapshot to thread the prompt-owner panel through snapshot/restore. Correctly clears both dictionaries on session clean and panel purge.
Sources/WorkspacePromptSubmit.swift Threads surfaceId: UUID? through handlePromptSubmit and handleConversationMessage so the submitting terminal's identity is reliably passed to recordSubmittedMessage.
Sources/TerminalController.swift One-line change: passes v2UUIDAny(event.surfaceId) into handlePromptSubmit so the event-sourced surface ID is used instead of relying on focused panel.
cmuxTests/SessionPromptMarkReplayTests.swift New Swift Testing suite (341 lines) covering injection, no-op, styled-vs-plain blockquote disambiguation, agent-echo exclusion, wrapped/multiline prompts, scrollback gate, snapshot round-trip, and end-to-end replay file verification.
cmuxTests/WorkspacePromptSubmitTests.swift Adds two integration tests: panel ownership gate (only the submitted panel gets the key) and key survival across a re-snapshot without a new submit.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Prompt submitted by user] --> B[recordSubmittedMessage with panelId / focusedPanelId]
    B --> C[latestSubmittedPanelId set]
    D[sessionSnapshot called] --> E{includeScrollback?}
    E -- No --> F[lastPromptMarkKey = nil]
    E -- Yes --> G{panel == latestSubmittedPanelId?}
    G -- Yes --> H[persistablePromptMatchKey scrollback gate check]
    G -- No --> I{restoredPromptMarkKeysByPanelId has key for panel?}
    I -- Yes --> H
    I -- No --> F
    H -- match found --> J[Store bounded 48-char key in snapshot.lastPromptMarkKey]
    H -- no match --> F
    K[restoreSessionSnapshot] --> L[Read snapshot.terminal.lastPromptMarkKey]
    L --> M[replayEnvironment for scrollback lastUserMessage = key]
    M --> N[reinjectingLastPromptMark into scrollback text]
    N --> O{promptRowIndex finds unambiguous sigil row?}
    O -- nil: none or ambiguous --> P[scrollback unchanged]
    O -- Some index --> Q{Row already has OSC 133;A marker?}
    Q -- Yes --> P
    Q -- No --> R[Prepend semanticPromptStartMark to that row]
    R --> S[Write marked text to replay file]
    S --> T[CMUX_RESTORE_SCROLLBACK_FILE env set]
    T --> U[Ghostty regains OSC 133 row - prompt navigation restored]
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"}}}%%
flowchart TD
    A[Prompt submitted by user] --> B[recordSubmittedMessage with panelId / focusedPanelId]
    B --> C[latestSubmittedPanelId set]
    D[sessionSnapshot called] --> E{includeScrollback?}
    E -- No --> F[lastPromptMarkKey = nil]
    E -- Yes --> G{panel == latestSubmittedPanelId?}
    G -- Yes --> H[persistablePromptMatchKey scrollback gate check]
    G -- No --> I{restoredPromptMarkKeysByPanelId has key for panel?}
    I -- Yes --> H
    I -- No --> F
    H -- match found --> J[Store bounded 48-char key in snapshot.lastPromptMarkKey]
    H -- no match --> F
    K[restoreSessionSnapshot] --> L[Read snapshot.terminal.lastPromptMarkKey]
    L --> M[replayEnvironment for scrollback lastUserMessage = key]
    M --> N[reinjectingLastPromptMark into scrollback text]
    N --> O{promptRowIndex finds unambiguous sigil row?}
    O -- nil: none or ambiguous --> P[scrollback unchanged]
    O -- Some index --> Q{Row already has OSC 133;A marker?}
    Q -- Yes --> P
    Q -- No --> R[Prepend semanticPromptStartMark to that row]
    R --> S[Write marked text to replay file]
    S --> T[CMUX_RESTORE_SCROLLBACK_FILE env set]
    T --> U[Ghostty regains OSC 133 row - prompt navigation restored]
Loading

Reviews (23): Last reviewed commit: "Reject colored blockquote prompt echoes" | Re-trigger Greptile

Comment thread Sources/SessionPersistence.swift Outdated
Comment on lines +2100 to +2103
if visible.contains(needle) {
targetIndex = index
break
}

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 contains can match agent output that echoes the user's message

The scan walks the scrollback from bottom to top and takes the first line whose stripped visible text contains(needle). Because agent responses frequently reference the user's request — e.g. "I'll refactor the login flow", "Completed: fix the flaky test", "Here's how I'll add a dark mode toggle" — a line of agent output appearing after the user's prompt in the scrollback will be found first, and that agent output row gets the OSC 133 mark instead of the actual prompt row.

The PR description calls the match "prefix-based," which accurately describes how needle is constructed (first 48 chars of the message), but the search is an unconstrained substring check anywhere in the stripped line. In practice, a short or common user message (like those in the tests) will match agent-emitted text that contains those words, so jump-to-prompt lands in the middle of agent output rather than at the prompt boundary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — addressed in dae1b66. The matcher no longer does an unconstrained contains substring scan. It now:

  1. Anchors to the row start. A row only matches if it begins with the message text (after a short ≤4-char prompt sigil like > /❯ /│ > ). Agent prose that echoes the words mid-sentence ("I'll refactor the login flow", "Summary: …", "Confirmed: …") is no longer matched, because the message isn't at the row start.
  2. Picks the first (top-most) anchored row, not the last. The user's prompt always precedes the agent's response for a turn, so the earliest anchored row is the prompt — never a later agent line that quotes it.
  3. Compares with whitespace stripped, so soft- and hard-wrapped (mid-word, narrow-grid) prompts still match across captured rows.

Added a regression test (testScrollbackReplayDoesNotMarkAgentOutputEchoingTheMessage) that asserts an agent row echoing the message is left unmarked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in current HEAD. The matcher now requires a known prompt sigil and a compacted prefix match via promptRowIndex/compactPrefixMatches, and fails closed on ambiguous matches rather than scanning with contains. Tests cover agent-output echoes, bare echoes, and Markdown bullet/heading echoes.

— Claude Code

cmux and others added 2 commits June 26, 2026 05:16
SessionPersistence.swift (+OSC 133 re-injection) and SessionPersistenceTests.swift
(+regression tests) grew past their budget. Regenerated with
`python3 scripts/swift_file_length_budget.py --write-budget` per CLAUDE.md
(never hand-edited).

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

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 509216a0-ed05-43ad-b3f6-51347c1f607b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Session snapshots now persist a bounded prompt match key, restore passes it into scrollback replay, and replayed scrollback reinjects OSC 133 prompt-start markers before the matching prompt row.

Changes

Semantic prompt replay restoration

Layer / File(s) Summary
Persist prompt match key
Sources/SessionPersistence.swift, Sources/Workspace.swift
SessionTerminalPanelSnapshot adds lastPromptMarkKey, and workspace snapshotting stores latestSubmittedMessage only when replayable scrollback exists.
Reinject marker during replay
Sources/SessionPersistence.swift, Sources/Workspace.swift
SessionScrollbackReplayStore.replayEnvironment accepts lastUserMessage, reinjects semanticPromptStartMark into replayed scrollback, and restore wiring passes the replayable key into that call.
Prompt row matching
Sources/SessionPersistence.swift
persistablePromptMatchKey, promptRowIndex, and helper functions collapse visible text, strip escape sequences, and require a unique prompt row before persisting or reinjecting.
Reinjection edge cases
cmuxTests/SessionPersistenceTests.swift
Tests cover missing and blank messages, ambiguous echoes, wrapped prompts, ANSI-interleaved prompts, multiline prompts, and prompt-sigil matching for reinjectingLastPromptMark.
Replay and snapshot roundtrip tests
cmuxTests/SessionPersistenceTests.swift
Tests cover replay file output, lastPromptMarkKey Codable persistence, persistablePromptMatchKey gating and bounds, and the nil key case when a terminal snapshot omits scrollback.

Sequence Diagram(s)

sequenceDiagram
  participant Workspace
  participant SessionScrollbackReplayStore
  participant ReplayFile
  Workspace->>SessionScrollbackReplayStore: restore scrollback with prompt key
  SessionScrollbackReplayStore->>SessionScrollbackReplayStore: reinject semanticPromptStartMark
  SessionScrollbackReplayStore->>ReplayFile: write replayed scrollback
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop, hop, the prompt mark stayed in place,
Through replay scrollback, time and space.
OSC 133 now finds its nest,
And last-user prompts can rest.

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: re-injecting OSC 133 prompt marks into replayed scrollback.
Description check ✅ Passed The description is detailed and covers the problem, fix, scope, and testing, though some template sections are missing.
Linked Issues check ✅ Passed The change matches #6691 by restoring OSC 133 prompt markers during scrollback replay and adding tests for the rebuild path.
Out of Scope Changes check ✅ Passed The changes stay focused on scrollback replay, prompt-marker persistence, restore plumbing, and tests; no unrelated work is evident.
Docstring Coverage ✅ Passed Docstring coverage is 81.48% which is sufficient. The required threshold is 80.00%.
Cmux Swift Actor Isolation ✅ Passed Workspace is @MainActor, and the new prompt-key capture/restore helpers are pure static utilities or tests, with no new shared mutable Sendable state.
Cmux Swift Blocking Runtime ✅ Passed No new production waits/locks/sleeps were added; the new replay code is pure parsing/file-writing, and semaphore use is confined to tests/existing helpers.
Cmux Browser Automation Off-Main ✅ Passed PR only changes session replay/persistence and tests; no browser.* routing, worker-lane, or WebKit/AppKit wait paths were touched.
Cmux Expensive Synchronous Load ✅ Passed The diff only threads a prompt-mark key through snapshot/restore and does pure string matching; no new sync agent-history/JSON load is added to main-thread or interactive paths.
Cmux Cache Substitution Correctness ✅ Passed The persisted prompt key is freshness-checked against current scrollback and only used as a graceful no-op restore hint; no authoritative read was blindly replaced.
Cmux No Hacky Sleeps ✅ Passed PR only adds synchronous Swift prompt-replay plumbing; no new fixed sleeps/polling in production runtime paths, and remaining waits are pre-existing or test-only.
Cmux Algorithmic Complexity ✅ Passed Prompt matching scans capped scrollback linearly (4k lines/400k chars) with a fixed 48-char needle; no nested full-collection rescans or hot-path sorts were added.
Cmux Swift Concurrency ✅ Passed The diff adds only synchronous snapshot/replay helpers; no new DispatchQueue, Task, Combine, or completion-handler async patterns appear in added lines.
Cmux Swift @Concurrent ✅ Passed Diff adds only synchronous helpers/call sites; no new @concurrent, nonisolated async, or actor-isolation violations appear in the changed Swift files.
Cmux Swift File And Package Boundaries ✅ Passed Focused 221-line addition to an existing 2297-line persistence file; no new oversized file, no package-boundary expansion, and responsibilities stay in session persistence/replay.
Cmux Swiftpm Lockfiles ✅ Passed Cmux-owned .gitignore files don't ignore Package.resolved; package URL deps have matching lockfiles, and the root Xcode Package.resolved is present. Vendor omission is allowed.
Cmux Swift Logging ✅ Passed PASS: The patch adds no new print/debugPrint/dump/NSLog or Logger usage in the changed production code; existing NSLog calls are untouched, and test output is allowed.
Cmux User-Facing Error Privacy ✅ Passed The PR only adds internal replay/snapshot logic and tests; no new user-facing errors, alerts, or recovery copy expose vendor or secret details.
Cmux Full Internationalization ✅ Passed Touched production code only adds internal OSC 133/persistence plumbing and comments; no new user-facing Swift text, catalogs, or web locale files were introduced.
Cmux Swiftui State Layout ✅ Passed The PR only changes snapshot/restore utilities; it adds no new SwiftUI state/layout patterns, and the existing ObservableObject/@published in Workspace.swift are legacy, untouched state.
Cmux Architecture Rethink ✅ Passed PASS: This is a local snapshot→restore correctness fix with a clear owner/invariant; no sleeps, polling, observers, or duplicate UI lifecycle wiring were added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Touched Swift files only change session snapshot/replay data; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes were introduced.
Cmux Source Artifacts ✅ Passed Changed paths are source/tests only; no artifact files, caches, logs, or temp outputs were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR adds prompt-replay plumbing (lastPromptMarkKey, reinjection helpers) with production callers; no new debug*/ForTesting or test-guarded accessor in Sources/.
Cmux No Ambient Global State ✅ Passed No new top-level funcs, mutable globals, or singletons were added; the new helpers live inside an existing static namespace enum and only add static let constants.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6691-semantic-prompt-bar-last-user-message

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.

cmux and others added 10 commits June 26, 2026 05:23
…prompts (#6691)

Autoreview findings:

- [P1 privacy] The persisted `lastUserMessage` was written for every restorable
  agent panel even when the snapshot intentionally omits scrollback
  (`includeScrollback: false`, or scrollback persistence disabled by policy),
  creating a new on-disk copy of sensitive input. Gate it on `resolvedScrollback
  != nil` — the value only exists to mark replayed scrollback, so without
  scrollback it is both useless and a leak.

- [P2 matching] A fixed 48-char single-row needle missed prompts that soft-wrap
  on a narrow grid or contain newlines (the prefix spans multiple captured rows,
  so no single row contains it). Replace single-row matching with a
  whitespace-collapsed cross-row stream that records each character's source
  row; the last match maps back to the row where the prompt begins, which is the
  row marked. Added wrapped + multiline regression tests.

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

Greptile flagged that an unanchored `contains(needle)` substring scan can land
on agent output that echoes the user's words ("I'll refactor the login flow")
instead of the actual prompt row. Anchor the match to the start of a row
(allowing a short ≤4-char prompt sigil like "> "/"❯ "/"│ > "), and compare with
whitespace removed so it stays robust to soft and hard (mid-word) wraps. Among
anchored rows we keep the bottom-most, i.e. the most recent prompt. Added a
regression test asserting agent output echoing the message is not marked.

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

Both reviewers (Greptile, Codex) noted that selecting a later occurrence risks
marking agent output that quotes the user's message. Since the user's prompt
always precedes the agent's response for a turn, take the FIRST anchored match
top-to-bottom instead of the last — the earliest anchored row is the prompt,
never a later agent line. Updated the multi-row test accordingly.

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

Resolve the tension both review passes probed from opposite sides:
- mid-sentence agent echoes ("I'll refactor…", "Summary: …") are excluded
  because the match is anchored to the row start, not an arbitrary substring;
- so among the remaining anchored prompt rows we can safely keep the BOTTOM-most
  one, restoring jump-to-prompt to the user's MOST RECENT prompt when the same
  text was submitted across multiple turns.

Test renamed/updated to assert the most-recent of two repeated prompt rows is
marked; the agent-echo regression test still asserts mid-sentence echoes stay
unmarked.

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

Codex review caught that the prompt-marker plumbing was dead: persisting
`lastUserMessage` was gated on `effectiveRestorableAgent != nil`, but cmux only
saves replayable scrollback when there is NO restorable agent
(`shouldReplaySessionScrollback` returns `!hasRestorableAgent && …`). The two
conditions are mutually exclusive, so `lastUserMessage` was always nil and the
OSC 133 re-injection never fired through the real snapshot path — exactly the
"replayed scrollback" path the issue is about.

Persist `lastUserMessage` whenever replayable scrollback is saved (still gated on
`resolvedScrollback != nil`, so no input is written for snapshots that omit
terminal contents). `latestSubmittedMessage` is nil for workspaces without agent
prompts, so plain shells persist nothing.

Added an integration test across the persistence boundary (snapshot Codable
round trip → restore → replay file carries the mark) plus a no-scrollback /
nil-lastUserMessage test.

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

@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: 2

🤖 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 `@cmuxTests/SessionPersistenceTests.swift`:
- Around line 902-910: The current test only verifies the default
encoding/decoding behavior of SessionTerminalPanelSnapshot, so it does not cover
the real capture path. Update
testTerminalSnapshotWithoutScrollbackDecodesNilLastUserMessage to build the
snapshot through Workspace.sessionSnapshot(includeScrollback: false) (or the
equivalent session capture API), then decode the terminal snapshot and assert
lastUserMessage is omitted/nil there. Keep the assertions focused on the actual
Workspace/sessionSnapshot flow and SessionTerminalPanelSnapshot decoding.

In `@Sources/Workspace.swift`:
- Around line 529-541: The `lastUserMessage` assignment in `Workspace.swift` is
using the workspace-wide `latestSubmittedMessage` for every replayable terminal,
which can persist the wrong prompt into unrelated panels. Update the snapshot
logic around this `lastUserMessage` field to use a per-panel/per-session
association (or verify the target scrollback actually contains the prompt row)
instead of the workspace-global value, so prompt replay state only persists when
the specific terminal can reliably own it and otherwise fails closed.
🪄 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: 388b120e-5169-4061-8202-c79148cdaa40

📥 Commits

Reviewing files that changed from the base of the PR and between 6d6c701 and d9ee53c.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (3)
  • Sources/SessionPersistence.swift
  • Sources/Workspace.swift
  • cmuxTests/SessionPersistenceTests.swift

Comment thread cmuxTests/SessionPersistenceTests.swift Outdated
Comment thread Sources/Workspace.swift Outdated
cmux and others added 6 commits June 26, 2026 06:16
…ed (#6691)

Codex flagged that allowing up to four arbitrary non-alphanumeric sigil
characters let agent plan output that restates the request as a Markdown bullet
("- refactor the login flow") or heading ("# Refactor the login flow") anchor and
win the bottom-most match. Restrict the leading sigil to a whitelist of actual
shell/agent prompt characters (>, ❯, ›, », ▶, │, ┃, $, %) — Markdown list and
heading markers are excluded, so those echo rows no longer look like the prompt.
Added a regression test with a bullet and heading restating the message below the
real prompt.

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

Codex flagged that `latestSubmittedMessage` is workspace-scoped, so writing it
into every terminal snapshot with replayable scrollback could serialize one
agent terminal's prompt into a sibling panel's session JSON (privacy) and let
restore match against unrelated scrollback.

Gate persistence on `SessionScrollbackReplayStore.scrollbackContainsPromptRow`:
the message is persisted only into the snapshot of the terminal whose saved
scrollback actually contains the prompt row. This ties it to the right panel and
never persists any input the saved scrollback does not already carry. Refactored
the shared row-finding into `promptRowIndex` (used by both the capture gate and
re-injection); added a gate test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex flagged that allowing the message to match at offset 0 (no sigil consumed)
let a bare agent-output line that opens with the same words ("refactor the login
flow is done…") anchor and, being bottom-most, steal the marker from the real
prompt. Track sigil-prefixed and bare candidates separately and prefer the
sigil-prefixed one (a real prompt almost always leads with a sigil), falling back
to a bare match only when no sigil row matched. Added a regression test.

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

This comment has been minimized.

cmux and others added 2 commits June 26, 2026 19:17
…rompt-bar-last-user-message

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

Codex flagged that the capture gate matched on the ≤48-char needle but then
persisted the full (up to 240-char) `latestSubmittedMessage`, so a long prompt —
or a row sharing the same 48-char prefix — could write prompt text into the
session snapshot beyond what the saved scrollback actually contains.

Persist only the exact match key needed for re-injection:
- Replace `scrollbackContainsPromptRow` (Bool) with `persistablePromptMatchKey`,
  which returns the collapsed ≤48-char needle IFF the saved scrollback already
  contains the matching prompt row, else nil.
- Rename the snapshot field `lastUserMessage` -> `lastPromptMarkKey` to make the
  bounded-key semantics explicit; capture stores that key, restore threads it
  back into `replayEnvironment` (re-injection re-derives the same needle, so
  behavior is unchanged).
- Tests assert the gate yields a bounded key only on a real match and never the
  full long message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rompt-bar-last-user-message

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

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Workspace.swift`:
- Around line 1291-1304: `recordResumeIntent` is currently driven by
`resumeReboundSession` in `Workspace`, but it can be set from any
`restorableAgent` snapshot even when there is no actual resumable path. Update
the `resumeReboundSession` construction to fail closed: only return a value when
there is an explicit resume signal such as a valid agent-hook `resumeBinding`
with non-empty checkpoint/kind, or a `restorableAgent` that exposes real resume
metadata, and otherwise keep it nil. Keep the logic localized around the
`resumeReboundSession` closure so the downstream live/.idle transition only
happens for truly resumable sessions.
🪄 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: 42517297-7202-4aa8-9787-5b5e58145605

📥 Commits

Reviewing files that changed from the base of the PR and between 7d0af58 and f2f9da8.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (1)
  • Sources/Workspace.swift

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

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 `@Sources/Workspace.swift`:
- Around line 1291-1304: `recordResumeIntent` is currently driven by
`resumeReboundSession` in `Workspace`, but it can be set from any
`restorableAgent` snapshot even when there is no actual resumable path. Update
the `resumeReboundSession` construction to fail closed: only return a value when
there is an explicit resume signal such as a valid agent-hook `resumeBinding`
with non-empty checkpoint/kind, or a `restorableAgent` that exposes real resume
metadata, and otherwise keep it nil. Keep the logic localized around the
`resumeReboundSession` closure so the downstream live/.idle transition only
happens for truly resumable sessions.
🪄 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: 42517297-7202-4aa8-9787-5b5e58145605

📥 Commits

Reviewing files that changed from the base of the PR and between 7d0af58 and f2f9da8.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (1)
  • Sources/Workspace.swift
🛑 Comments failed to post (1)
Sources/Workspace.swift (1)

1291-1304: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate recordResumeIntent on actual resumability.

Lines 1292-1294 treat any restorableAgent snapshot as enough to rebound the transcript, so Lines 1419-1427 will mark the session live/.idle even when the restored snapshot has no real resume path. That makes lifecycle state fail open and can surface a dead session as editable after restore. Please derive resumeReboundSession only from an explicit resumable signal (for example, an agent-hook binding or a restorableAgent with non-nil resume metadata), and otherwise leave it nil.

As per path instructions, "For correctness-critical detection/identity ... prefer fail closed when the reliable signal is missing."

Also applies to: 1419-1427

🤖 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/Workspace.swift` around lines 1291 - 1304, `recordResumeIntent` is
currently driven by `resumeReboundSession` in `Workspace`, but it can be set
from any `restorableAgent` snapshot even when there is no actual resumable path.
Update the `resumeReboundSession` construction to fail closed: only return a
value when there is an explicit resume signal such as a valid agent-hook
`resumeBinding` with non-empty checkpoint/kind, or a `restorableAgent` that
exposes real resume metadata, and otherwise keep it nil. Keep the logic
localized around the `resumeReboundSession` closure so the downstream live/.idle
transition only happens for truly resumable sessions.

Source: Path instructions

…rompt-bar-last-user-message

# Conflicts:
#	.github/swift-file-length-budget.tsv
Comment thread .claude/scheduled_tasks.lock Outdated
@@ -0,0 +1 @@
{"sessionId":"5c2a62d0-99c2-42c0-9452-f67b6fbaacda","pid":75304,"procStart":"Thu Jun 4 23:34:54 2026","acquiredAt":1780616564932} No newline at end of file

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.

P1 Runtime lock file committed to source control

.claude/scheduled_tasks.lock is a machine-local runtime artifact generated by Claude Code's scheduled-tasks feature. It encodes a machine-specific PID (75304), process-start timestamp, and session UUID that are meaningless on any other checkout. Any developer who clones the repo, checks out this branch, and runs Claude Code's scheduler will inherit a stale lock pointing at a dead process — potentially blocking the scheduler from acquiring the lock at all, or causing confusing behavior.

This file should be added to .gitignore (under .claude/) and removed from the tree.

Rule Used: Flag local tool output, generated logs, screenshot... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 14ec262. Removed the tracked .claude/scheduled_tasks.lock artifact from the tree; .gitignore already contains .claude/scheduled_tasks.lock so it will not be re-added.

— Claude Code

…rompt-bar-last-user-message

# Conflicts:
#	.github/swift-file-length-budget.tsv
…rompt-bar-last-user-message

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

This branch was successfully deployed

1 active deployment
Preview – cmux — e5b4bd7c 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.

Semantic prompt bar (last user message) permanently disappears after auto-resume / hibernation rebuild — replayed scrollback drops OSC 133 marks

3 participants