Skip to content

feat: add AwaitPrompt command for shell prompt detection#708

Open
MattieTK wants to merge 2 commits intocharmbracelet:mainfrom
MattieTK:feat/await-prompt
Open

feat: add AwaitPrompt command for shell prompt detection#708
MattieTK wants to merge 2 commits intocharmbracelet:mainfrom
MattieTK:feat/await-prompt

Conversation

@MattieTK
Copy link
Copy Markdown

@MattieTK MattieTK commented Feb 20, 2026

Summary

Adds a new AwaitPrompt command that waits for the shell to render a new prompt before proceeding. This provides a reliable, timing-independent way to synchronise tape playback with command completion.

Syntax

AwaitPrompt          # wait using default WaitTimeout (15s)
AwaitPrompt@30s      # wait with explicit timeout
AwaitPrompt@2m       # longer timeout for slow commands

Relationship to Wait

Plain Wait (without +Line or +Screen) already does something similar – it defaults to Wait+Line with the pattern />$/, which matches VHS's default > prompt. For the common case of "run a command and wait for the shell to be ready", both commands achieve the same result.

The difference is in reliability. Wait matches visible text in the terminal buffer, which means:

  • False positives: if a command outputs a line ending in > (compilation output, markdown, git log, etc.), Wait returns early before the command has actually finished
  • Custom prompts: if the prompt doesn't end with >, plain Wait stops working unless the user also sets WaitPattern
  • Buffer issues: Wait+Screen has a known bug with scrolled content (Wait+Screen regex fails to match text in the viewport after scrolling #659); Wait+Line is less affected but still reads from the buffer

AwaitPrompt avoids these problems by detecting an invisible marker in the raw terminal stream rather than matching visible text. It works regardless of what the prompt looks like or what the command outputs.

Could Wait be reworked instead?

Rather than adding a separate command, the OSC 133 marker mechanism could potentially be folded into Wait itself – making plain Wait (with no regex) use prompt detection by default, while Wait /pattern/, Wait+Line, and Wait+Screen continue to use regex matching as they do today.

This would mean existing tapes that use bare Wait would silently become more reliable, with no syntax changes needed. We're happy to rework the implementation in that direction if the maintainers prefer it.

For now, this PR introduces AwaitPrompt as a distinct command to make the behaviour explicit and avoid changing existing Wait semantics.

How it works

A central challenge discussed in #70 is that ttyd has no API to expose the state of child processes (confirmed by the ttyd maintainer in tsl0922/ttyd#1009). VHS communicates with ttyd through a browser – keystrokes go in, rendered canvas frames come out. There is no side channel for process lifecycle information.

This implementation sidesteps that limitation entirely. Rather than querying ttyd about process state, it reads the raw terminal output stream that already flows from ttyd through xterm.js. The approach follows the direction suggested by @normanr in #70 (using shell prompt detection via escape sequences) and is similar to what @kotborealis implemented with PROMPT_COMMAND in cassette_deck.

The mechanism:

  1. An invisible FinalTerm shell integration sequence (ESC ] 133;A BEL – "prompt start") is embedded in each shell's prompt string. OSC 133 is a recognised standard supported by iTerm2, WezTerm, Kitty, Ghostty, VS Code terminal, and others.
  2. When a command finishes and the shell renders a new prompt, this marker flows through: shell → ttyd → websocket → xterm.js → term.write()
  3. A JavaScript hook on term.write() scans for \x1b]133;A in the raw data before xterm.js processes it, incrementing a counter each time. The match is on the prefix only, so both BEL- and ST-terminated variants (cmd.exe) are detected.
  4. AwaitPrompt records the current counter and polls until it increases (or the timeout expires)

Because the marker is emitted by the shell's own prompt rendering, it fires precisely when the shell is ready for the next command – regardless of how long the previous command took.

Cross-platform shell support

Every shell supported by VHS embeds the OSC 133;A marker in its prompt:

Shell Marker mechanism Notes
bash \e]133;A\a in PS1 Uses \[...\] to mark as non-printing
zsh \e]133;A\a in PROMPT Uses %{...%} to mark as non-printing
fish printf '\e]133;A\a' in fish_prompt Emitted before the visible prompt
PowerShell [Console]::Write(ESC + ']133;A' + BEL) Direct console write in prompt function
pwsh Same as PowerShell Same mechanism
cmd.exe $E]133;A$E\ in prompt Uses ST (ESC \) instead of BEL, since cmd.exe's prompt command has no BEL code
nushell print -n with marker in PROMPT_COMMAND Raw bytes embedded by Go; nushell passes them through verbatim
osh Same as bash Shares bash's PS1 format
xonsh Raw marker bytes in PROMPT Raw bytes embedded by Go via -D flag

A test (TestShellPromptMarker) verifies that all 9 shells include the 133 marker. Both BEL and ST are valid OSC terminators per the spec and are accepted by all major terminal emulators.

The remaining FinalTerm markers – B (prompt end), C (command start), D (command finished with exit code) – are natural follow-ups if richer shell integration is needed later.

Motivation / Related discussions

Test plan

  • Parser tests: bare AwaitPrompt, with @timeout (seconds, milliseconds, minutes)
  • Command count test updated for the new command
  • Shell prompt marker test: all 9 shells verified to include OSC 133;A
  • Full test suite passes (go test ./...)
  • Manual testing with real tape files across available shells

Note

This PR was authored with AI assistance (Claude Code).

@MattieTK MattieTK marked this pull request as ready for review February 20, 2026 07:53
@MattieTK MattieTK requested a review from a team as a code owner February 20, 2026 07:53
@MattieTK MattieTK requested review from andreynering and aymanbagabas and removed request for a team February 20, 2026 07:53
Copy link
Copy Markdown
Member

@aymanbagabas aymanbagabas left a comment

Choose a reason for hiding this comment

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

Thank you, @MattieTK, for your work here. I wonder if instead of using our own OSC7777 sequences, we could use FinalTerm shell integration sequences OSC133 to indicate prompt begin, end, command begin, and command end 🤔

@MattieTK
Copy link
Copy Markdown
Author

Thank you, @MattieTK, for your work here. I wonder if instead of using our own OSC7777 sequences, we could use FinalTerm shell integration sequences OSC133 to indicate prompt begin, end, command begin, and command end 🤔

Great idea, I've swapped the custom string for 133;A which is 'new prompt' and practically the same thing as far as we're concerned in this case.

@aymanbagabas
Copy link
Copy Markdown
Member

Great idea, I've swapped the custom string for 133;A which is 'new prompt' and practically the same thing as far as we're concerned in this case.

I don't see any new commits. Did you push your changes? 😄

MattieTK and others added 2 commits March 22, 2026 09:42
Add AwaitPrompt, a command that waits for the shell to emit a new
prompt rather than matching terminal content. Each shell's prompt
configuration embeds an invisible OSC 7777 marker; a JavaScript
write hook in xterm.js counts these markers as they flow through
the terminal stream.

Syntax:
  AwaitPrompt        # wait using default WaitTimeout
  AwaitPrompt@30s    # wait up to 30 seconds

Supported shells: bash, zsh, fish, powershell, pwsh, cmd.exe,
nushell, osh, xonsh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use the standardised FinalTerm "prompt start" marker (OSC 133;A)
instead of a custom OSC 7777 sequence for prompt detection. OSC 133
is supported by iTerm2, WezTerm, Kitty, Ghostty, VS Code terminal,
and others, so this aligns VHS with the wider terminal ecosystem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@MattieTK MattieTK force-pushed the feat/await-prompt branch from e98aad8 to cdfce19 Compare March 22, 2026 09:43
@MattieTK
Copy link
Copy Markdown
Author

Great idea, I've swapped the custom string for 133;A which is 'new prompt' and practically the same thing as far as we're concerned in this case.

I don't see any new commits. Did you push your changes? 😄

Sorry @aymanbagabas! I of course left this locally and then promptly went on holiday.

Merged now and rebased.

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.

2 participants