Repository navigation
Re-inject OSC 133 prompt mark into replayed scrollback (#6691) - #6908
austinywang wants to merge 44 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR re-injects OSC 133 semantic prompt-start markers into replayed scrollback on session restore, fixing broken
Confidence Score: 5/5Safe 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
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]
%%{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]
Reviews (23): Last reviewed commit: "Reject colored blockquote prompt echoes" | Re-trigger Greptile |
| if visible.contains(needle) { | ||
| targetIndex = index | ||
| break | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Good catch — addressed in dae1b66. The matcher no longer does an unconstrained contains substring scan. It now:
- 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. - 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.
- 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.
There was a problem hiding this comment.
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
…rompt-bar-last-user-message
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>
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughSession 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. ChangesSemantic prompt replay restoration
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 25✅ Passed checks (25 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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>
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (3)
Sources/SessionPersistence.swiftSources/Workspace.swiftcmuxTests/SessionPersistenceTests.swift
…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>
This comment has been minimized.
This comment has been minimized.
…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
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@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
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (1)
Sources/Workspace.swift
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis 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
recordResumeIntenton actual resumability.Lines 1292-1294 treat any
restorableAgentsnapshot as enough to rebound the transcript, so Lines 1419-1427 will mark the session live/.idleeven 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 deriveresumeReboundSessiononly from an explicit resumable signal (for example, an agent-hook binding or arestorableAgentwith non-nil resume metadata), and otherwise leave itnil.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
| @@ -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 | |||
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
…rompt-bar-last-user-message # Conflicts: # .github/swift-file-length-budget.tsv
…rompt-bar-last-user-message
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 +
resumerestores them.Root cause (confirmed by reading the code)
cmux captures session scrollback via Ghostty's
write_screen_file:copy,vtexport (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 atsemantic_prompt == .none, sojump_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
ghosttysubmodule and would require republishing the GhosttyKit xcframework, so this fixes it at the cmux replay layer instead.SessionScrollbackReplayStore.reinjectingLastPromptMarkfinds the restored last user message's prompt row and prepends anOSC 133 ; A ; cl=linemarker (the same form cmux's shell integration emits), re-marking that row as a prompt. The row finder (promptRowIndex):>,❯,›,»,▶,│,┃,$,%) — 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…");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 throughreplayEnvironmentat 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, thescrollbackContainsPromptRowcapture gate, a snapshot→restore→replay-file integration test, and an end-to-endreplayEnvironmentround-trip.Note on test framework: the new cases extend the existing
SessionScrollbackReplayStoreXCTest suite already living inSessionPersistenceTests.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