feat: add AwaitPrompt command for shell prompt detection#708
feat: add AwaitPrompt command for shell prompt detection#708MattieTK wants to merge 2 commits intocharmbracelet:mainfrom
Conversation
b564fd7 to
96cc4ad
Compare
aymanbagabas
left a comment
There was a problem hiding this comment.
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. |
I don't see any new commits. Did you push your changes? 😄 |
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>
e98aad8 to
cdfce19
Compare
Sorry @aymanbagabas! I of course left this locally and then promptly went on holiday. Merged now and rebased. |
Summary
Adds a new
AwaitPromptcommand 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
Relationship to
WaitPlain
Wait(without+Lineor+Screen) already does something similar – it defaults toWait+Linewith 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.
Waitmatches visible text in the terminal buffer, which means:>(compilation output, markdown,git log, etc.),Waitreturns early before the command has actually finished>, plainWaitstops working unless the user also setsWaitPatternWait+Screenhas a known bug with scrolled content (Wait+Screenregex fails to match text in the viewport after scrolling #659);Wait+Lineis less affected but still reads from the bufferAwaitPromptavoids 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
Waitbe reworked instead?Rather than adding a separate command, the OSC 133 marker mechanism could potentially be folded into
Waititself – making plainWait(with no regex) use prompt detection by default, whileWait /pattern/,Wait+Line, andWait+Screencontinue to use regex matching as they do today.This would mean existing tapes that use bare
Waitwould 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
AwaitPromptas a distinct command to make the behaviour explicit and avoid changing existingWaitsemantics.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_COMMANDin cassette_deck.The mechanism:
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.term.write()term.write()scans for\x1b]133;Ain 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.AwaitPromptrecords 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:
\e]133;A\ainPS1\[...\]to mark as non-printing\e]133;A\ainPROMPT%{...%}to mark as non-printingprintf '\e]133;A\a'infish_prompt[Console]::Write(ESC + ']133;A' + BEL)promptfunction$E]133;A$E\inpromptESC \) instead of BEL, since cmd.exe'spromptcommand has no BEL codeprint -nwith marker inPROMPT_COMMANDPS1formatPROMPT-DflagA test (
TestShellPromptMarker) verifies that all 9 shells include the133marker. 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
PROMPT_COMMANDapproachWaitpartially addressed this but requires knowing the expected output patternPROMPT_COMMANDleaking into VHS recordingsTest plan
AwaitPrompt, with@timeout(seconds, milliseconds, minutes)go test ./...)Note
This PR was authored with AI assistance (Claude Code).