Wait for Codex's live composer before pasting linked issue drafts (#25779)

* Wait for Codex's live composer before pasting launch drafts

* Keep timed-out Codex draft delivery consumed across remounts

* Document the checked terminal fixtures used by the remount test

* Distinguish Codex's reserved footer row from early multiline input

* Complete transcript baselines and suppress hidden terminal strings

* Align watcher scan budget with linked-directory reconciliation

* Drop unused snapshots after main removed the readiness census
This commit is contained in:
Neil
2026-10-05 23:38:28 -07:00
committed by GitHub
parent eaaae0196f
commit ac3a25ca37
21 changed files with 462 additions and 128 deletions
@@ -74,6 +74,11 @@ const KNOWN_PREEXISTING_I2_FAILURES: Record<string, number> = {
'codex-0-158-0-approval': 12,
'codex-0-158-0-timed-turn': 20,
'codex-0-158-0-trustprompt': 36,
// Fullscreen startup captures diverge identically with the base b58f8197dc36 serializer.
'codex-fullscreen-custom-footer': 18,
'codex-fullscreen-early-input': 4,
'codex-fullscreen-multiline-early-input': 10,
'codex-fullscreen-startup': 14,
'claude-dialog-trust-workspace-answered': 13,
// DSH-TUI's whale intro paints whole rows of 24-bit background, and every one of this
// transcript's divergences is the same shape: `visible-grid row=0`, a true-colour
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-10-06T04:19:41.191Z",
"platform": "darwin",
"command": ["codex", "--no-daemon", "-c", "tui.status_line=[\"model-name\"]"],
"cols": 120,
"rows": 40,
"note": "Codex CLI 0.160.1; single model-name status item",
"exitCode": 0
}
@@ -0,0 +1 @@
[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[?2026h[?25l>_OpenAI Codex (v0.160.1)loadingWhatarewepokingwithametaphoricalstick?⣀⣤⣤⣤⣀⣀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄⣀⣾⣿⣿⠃⣀⣴⣾⣿⣿⡿⠟⠋⣀⡀⠈⠙⢿⣿⣿⣧⣀⣶⣿⣿⣿⣿⡇⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇⣰⣿⣿⡿⢿⣿⣿⡇⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷⢰⣿⣿⡟⠁⢸⣿⣿⡇⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀⣾⣿⣿⠃⢸⣿⣿⡇⢸⣿⣿⠋⠁⠈⠙⢻⣿⣿⣿⣶⣤⡀⠹⢿⣿⣷⡄⢸⣿⣿⣇⠸⣿⣿⣷⣦⣼⣿⣿⢸⣿⣿⠻⢿⣿⣿⡇⠘⣿⣿⣿⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀⢀⣠⣼⣿⣿⣿⣿⡇⣿⣿⣿⠇⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⡇⣠⣿⣿⣿⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿⣿⣿⣷⣴⣿⣿⣿⠃⠘⣿⣿⣷⠈⠛⢿⣿⣿⣿⠿⠋⠉⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠻⣿⣿⣿⣤⣀⠈⠉⢀⣤⣶⣿⣿⣿⠿⠟⠁⣰⣿⣿⡟⠋⠁⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁⣠⣼⣿⣿⡟⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏⠈⠙⠛⠿⠿⠿⠿⠛⠉›Ask Codex to do anything?forshortcuts[0 q [?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx permissions: YOLO modeWhatarewepokingwithametaphoricalstick?[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25lGPT-6.1-Sol[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace-xxxxxxxxxxxxxx]0;⠼ workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠦ workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace-xxxxxxxxxxxxxx]0;⠇ workspace-xxxxxxxxxxxxxx]0;⠏ workspace-xxxxxxxxxxxxxx]0;⠋ workspace-xxxxxxxxxxxxxx]0;workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25h[?2026l
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-10-06T04:15:29.384Z",
"platform": "darwin",
"command": ["codex", "--no-daemon"],
"cols": 120,
"rows": 40,
"note": "Codex CLI 0.160.1; typing before live footer",
"exitCode": 0
}
@@ -0,0 +1 @@
[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[?2026h[?25l>_OpenAI Codex (v0.160.1)loadingMaythesourcebewithyou.⣀⣤⣤⣤⣀⣀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄⣀⣾⣿⣿⠃⣀⣴⣾⣿⣿⡿⠟⠋⣀⡀⠈⠙⢿⣿⣿⣧⣀⣶⣿⣿⣿⣿⡇⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇⣰⣿⣿⡿⢿⣿⣿⡇⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷⢰⣿⣿⡟⠁⢸⣿⣿⡇⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀⣾⣿⣿⠃⢸⣿⣿⡇⢸⣿⣿⠋⠁⠈⠙⢻⣿⣿⣿⣶⣤⡀⠹⢿⣿⣷⡄⢸⣿⣿⣇⠸⣿⣿⣷⣦⣼⣿⣿⢸⣿⣿⠻⢿⣿⣿⡇⠘⣿⣿⣿⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀⢀⣠⣼⣿⣿⣿⣿⡇⣿⣿⣿⠇⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⡇⣠⣿⣿⣿⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿⣿⣿⣷⣴⣿⣿⣿⠃⠘⣿⣿⣷⠈⠛⢿⣿⣿⣿⠿⠋⠉⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠻⣿⣿⣿⣤⣀⠈⠉⢀⣤⣶⣿⣿⣿⠿⠟⠁⣰⣿⣿⡟⠋⠁⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁⣠⣼⣿⣿⡟⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏⠈⠙⠛⠿⠿⠿⠿⠛⠉›Ask Codex to do anything?forshortcuts[0 q [?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx permissions: YOLO modeMaythesourcebewithyou.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l early note[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25lGPT-6.1-Soldefault·~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx·smarter-codex-issue-paste-detectio…[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace-xxxxxxxxxxxxxx]0;⠦ workspace-xxxxxxxxxxxxxx[?2026h[?25lhigh · ~/orca/workspaces/orca/smarte-codex-issue-paste-dtecion · smarte-codex-issue-paste-dtecion ·tabtoqueuemessage[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠧ workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l]0;⠇ workspace-xxxxxxxxxxxxxx]0;⠏ workspace-xxxxxxxxxxxxxx]0;⠋ workspace-xxxxxxxxxxxxxx]0;workspace-xxxxxxxxxxxxxx[?2026h[?25l[?25h[?2026l[?2026h[?25h[?2026l
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-10-06T05:13:09.417Z",
"platform": "darwin",
"command": ["codex", "--no-daemon", "-c", "check_for_update_on_startup=false"],
"cols": 120,
"rows": 40,
"note": "Codex CLI 0.160.1; bracketed multiline input followed by Up on first composer frame",
"exitCode": 0
}
@@ -0,0 +1 @@
[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[?2026h[?25l>_OpenAI Codex (v0.160.1)loadingAh,theterminal.Aclassicmeetingspot.⣀⣤⣤⣤⣀⣀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄⣀⣾⣿⣿⠃⣀⣴⣾⣿⣿⡿⠟⠋⣀⡀⠈⠙⢿⣿⣿⣧⣀⣶⣿⣿⣿⣿⡇⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇⣰⣿⣿⡿⢿⣿⣿⡇⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷⢰⣿⣿⡟⠁⢸⣿⣿⡇⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀⣾⣿⣿⠃⢸⣿⣿⡇⢸⣿⣿⠋⠁⠈⠙⢻⣿⣿⣿⣶⣤⡀⠹⢿⣿⣷⡄⢸⣿⣿⣇⠸⣿⣿⣷⣦⣼⣿⣿⢸⣿⣿⠻⢿⣿⣿⡇⠘⣿⣿⣿⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀⢀⣠⣼⣿⣿⣿⣿⡇⣿⣿⣿⠇⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⡇⣠⣿⣿⣿⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿⣿⣿⣷⣴⣿⣿⣿⠃⠘⣿⣿⣷⠈⠛⢿⣿⣿⣿⠿⠋⠉⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠻⣿⣿⣿⣤⣀⠈⠉⢀⣤⣶⣿⣿⣿⠿⠟⠁⣰⣿⣿⡟⠋⠁⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁⣠⣼⣿⣿⡟⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏⠈⠙⠛⠿⠿⠿⠿⠛⠉›Ask Codex to do anything?forshortcuts[0 q [?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25l›firstline second · line[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx permissions: YOLO modeAh,theterminal.Aclassicmeetingspot.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25lGPT-6.1-Soldefault·..........................................................·...................................[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25lhigh · ~/o.................................................... · ................................ ·tabtoqueuemessage[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25l[?25h[?2026l[?2026h[?25h[?2026l
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-10-06T04:12:15.320Z",
"platform": "darwin",
"command": ["codex", "--no-daemon"],
"cols": 120,
"rows": 40,
"note": "Codex CLI 0.160.1; startup without input",
"exitCode": 0
}
@@ -0,0 +1 @@
[?2004h[>4;0m[>7u[?1004h]10;?\]11;?\[?u[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[?2026h[?25l>_OpenAI Codex (v0.160.1)loadingComeonin.There’sroomforanidea.⣀⣤⣤⣤⣀⣀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄⣀⣾⣿⣿⠃⣀⣴⣾⣿⣿⡿⠟⠋⣀⡀⠈⠙⢿⣿⣿⣧⣀⣶⣿⣿⣿⣿⡇⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇⣰⣿⣿⡿⢿⣿⣿⡇⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷⢰⣿⣿⡟⠁⢸⣿⣿⡇⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀⣾⣿⣿⠃⢸⣿⣿⡇⢸⣿⣿⠋⠁⠈⠙⢻⣿⣿⣿⣶⣤⡀⠹⢿⣿⣷⡄⢸⣿⣿⣇⠸⣿⣿⣷⣦⣼⣿⣿⢸⣿⣿⠻⢿⣿⣿⡇⠘⣿⣿⣿⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀⢀⣠⣼⣿⣿⣿⣿⡇⣿⣿⣿⠇⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⡇⣠⣿⣿⣿⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿⣿⣿⣷⣴⣿⣿⣿⠃⠘⣿⣿⣷⠈⠛⢿⣿⣿⣿⠿⠋⠉⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠻⣿⣿⣿⣤⣀⠈⠉⢀⣤⣶⣿⣿⣿⠿⠟⠁⣰⣿⣿⡟⠋⠁⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁⣠⣼⣿⣿⡟⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏⠈⠙⠛⠿⠿⠿⠿⠛⠉›Ask Codex to do anything?forshortcuts[0 q [?25h[?2026l[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25l~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx permissions: YOLO modeComeonin.There’sroomforanidea.[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;workspace-xxxxxxxxxxxxxx[?2026h[?25lGPT-6.1-Soldefault·~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx·smarter-codex-issue-paste-detectio…[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠹ workspace-xxxxxxxxxxxxxx[?2026h[?25lhigh · ~/orca/workspaces/orca/smarte-codex-issue-paste-dtecion · smarte-codex-issue-paste-dtecion ·[?25h[?2026l[?2026h[?25h[?2026l]0;⠸ workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠼ workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25h[?2026l[?2026h[?25h[?2026l]0;⠴ workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l]0;⠦ workspace-xxxxxxxxxxxxxx]0;⠧ workspace-xxxxxxxxxxxxxx]0;⠇ workspace-xxxxxxxxxxxxxx]0;⠏ workspace-xxxxxxxxxxxxxx]0;workspace-xxxxxxxxxxxxxx[?2026h[?25h[?2026l[?2026h[?25h[?2026l
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { createDraftPasteReadyScanner } from '../../shared/draft-paste-ready-scanner'
import { readRuntimeFixture, replayTranscript } from './agent-transcript-replay-test-harness'
describe('Codex launch draft readiness from captured PTY output', () => {
it.each([
'codex-fullscreen-startup',
'codex-fullscreen-early-input',
'codex-fullscreen-multiline-early-input',
'codex-fullscreen-custom-footer'
])('%s: waits through the provisional composer and resolves on the live footer', async (name) => {
const data = readRuntimeFixture(name)
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
let sawProvisionalComposer = false
let sawReady = false
let offset = 0
for await (const frame of replayTranscript(data, 120, 40)) {
const result = scanner.observe(data.slice(offset, offset + 64))
offset += 64
const hasFooter = frame.screenLines.some((line) => line.includes('GPT-6.1-Sol'))
const hasComposer = frame.screenLines.some((line) => line.trimStart().startsWith('›'))
if (hasComposer && !hasFooter) {
sawProvisionalComposer = true
expect(result.ready).toBe(false)
}
if (result.ready) {
expect(hasFooter).toBe(true)
sawReady = true
}
}
expect(sawProvisionalComposer).toBe(true)
expect(sawReady).toBe(true)
})
it.each(['codex-0-158-0-trustprompt', 'codex-0158-update-available-dialog'])(
'%s: its selection glyph never opens the fullscreen paste gate',
(name) => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
const data = readRuntimeFixture(name)
for (let offset = 0; offset < data.length; offset += 17) {
expect(scanner.observe(data.slice(offset, offset + 17)).ready).toBe(false)
}
}
)
it.each([1, 7, 64, 1024, Infinity])('handles PTY chunks of %s characters', (size) => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
const data = readRuntimeFixture('codex-fullscreen-early-input')
let ready = false
for (let offset = 0; offset < data.length; offset += size) {
ready ||= scanner.observe(data.slice(offset, offset + size)).ready
}
expect(ready).toBe(true)
})
})
@@ -365,7 +365,7 @@ describe('processFileExplorerFsPayload update reconciliation', () => {
expect(refreshDir).toHaveBeenCalledTimes(2)
})
it('purges distinct cached directory renames with one bounded cache scan', () => {
it('purges distinct cached directory renames with bounded batch cache scans', () => {
const root = '/repo'
const worktreeId = 'watch-reconcile-perf'
const entries: Record<string, DirCache> = { [root]: cacheWithChildren([]) }
@@ -431,9 +431,8 @@ describe('processFileExplorerFsPayload update reconciliation', () => {
}
expect(setDirCache).toHaveBeenCalledOnce()
// One scan, in purgeDirCacheSubtrees. The casing-fallback index stays unbuilt because every
// lookup here hits `dirPath in cache` directly.
expect(keyVisits).toBe(entryCount)
// One index scan for linked-directory detection, then one purge scan for the entire batch.
expect(keyVisits).toBe(entryCount * 2)
expect(expandedPathReads).toBe(expandedPaths.length)
expect(remainingExpanded).toEqual(new Set())
})
@@ -228,9 +228,11 @@ describe('connectPanePty', () => {
)
})
it('waits past 8s for a cold Codex composer and preserves input ordering', async () => {
it.each([10_000, 20_000])('keeps Codex draft delivery bounded after %d ms', async (waitMs) => {
vi.useFakeTimers()
const { connectPanePty } = await import('./pty-connection')
const { beginAgentStartupDeliveryAttempt: claimStartupDelivery } =
await import('@/lib/agent-startup-delayed-delivery')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
const transport = createMockTransport('pty-codex')
@@ -259,7 +261,8 @@ describe('connectPanePty', () => {
})
vi.mocked(window.api.pty.getForegroundProcess).mockResolvedValue('codex')
connectPanePty(pane as never, manager as never, deps as never)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the connection fields exercised here without a real terminal or DOM.
const binding = connectPanePty(pane as never, manager as never, deps as never)
await vi.advanceTimersByTimeAsync(VISIBLE_PTY_SETTLE_MS)
await flushAsyncTicks()
expect(capturedDataCallback.current).not.toBeNull()
@@ -276,11 +279,25 @@ describe('connectPanePty', () => {
}
).mock.calls[0]?.[0]('USER_DRAFT')
;(mockStoreState.recordTerminalInput as ReturnType<typeof vi.fn>).mockClear()
await vi.advanceTimersByTimeAsync(10_000)
await vi.advanceTimersByTimeAsync(waitMs)
expect(transport.sendInputAccepted).not.toHaveBeenCalled()
capturedDataCallback.current?.('\x1b[?2004h\x1b[2K› ')
await flushAsyncTicks()
if (waitMs === 20_000) {
expect(transport.sendInputAccepted).not.toHaveBeenCalled()
expect(transport.sendInput.mock.calls.map(([data]) => data)).toEqual(['\x1b[I', 'USER_DRAFT'])
binding.dispose()
expect(
claimStartupDelivery({
worktreeId: 'wt-1',
tabId: 'tab-1',
launchToken: 'launch-token-1'
})
).toBe(false)
return
}
expect(transport.sendInputAccepted).toHaveBeenCalledWith(
'\x1b[200~https://github.com/stablyai/orca/issues/42\x1b[201~',
'launch'
@@ -9,6 +9,7 @@ import { resolveDraftPasteReadyTimeoutMs } from '../../../../../shared/draft-pas
import { createDraftPasteReadyScanner } from '../../../../../shared/draft-paste-ready-scanner'
import { sendAgentDraftPasteContent } from '@/lib/agent-draft-paste-content'
import { writeTerminalPastePtyInput } from '../terminal-pty-paste-writer'
import { createPasteReadinessTimeoutNotice } from '@/lib/launch-agent-paste-timeout-notice'
import { STARTUP_DRAFT_PASTE_QUIET_MS } from './pty-connect-limits'
import { isRemoteRuntimePtyId } from './paired-parked-terminal-restore'
@@ -174,6 +175,19 @@ export function bindSettlePaneSerializer(session: ConnectPanePtySession): void {
}
startupDraftHardTimer = setTimeout(() => {
startupDraftHardTimer = null
if (session.startupDraftAgent === 'codex') {
startupDraftPasteSettled = true
// A timed-out launch stays consumed across pane disposal and remount.
session.startupDraftPasteAttempted = true
session.cleanupStartupDraftPasteTimers()
createPasteReadinessTimeoutNotice({
worktreeId: session.deps.worktreeId,
tabId: session.deps.tabId,
agent: 'codex',
submitted: false
}).onTimeout()
return
}
void deliverStartupDraftIfAgentOwnsPty()
}, resolveDraftPasteReadyTimeoutMs(session.startupDraftAgent))
}
+28 -4
View File
@@ -432,18 +432,18 @@ describe('pasteDraftWhenAgentReady', () => {
it('best-effort pastes when the ready escape was missed but the agent process is running', async () => {
testState.inspectRuntimeTerminalProcess.mockResolvedValue({
foregroundProcess: 'codex',
foregroundProcess: 'gemini',
hasChildProcesses: false
})
const promise = pasteDraftWhenAgentReady({
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'codex'
agent: 'gemini'
})
await flushMicrotasks()
await vi.advanceTimersByTimeAsync(20000)
await vi.advanceTimersByTimeAsync(8000)
await expect(promise).resolves.toBe(true)
expect(testState.sendRuntimePtyInputVerified).toHaveBeenCalledWith(
@@ -539,7 +539,31 @@ describe('pasteDraftWhenAgentReady', () => {
)
})
it('honors the fallback inspection deadline for pty-bound draft paste', async () => {
it.each(['tab', 'pty'] as const)(
'never pastes late into a Codex process without composer readiness (%s)',
async (target) => {
const onTimeout = vi.fn()
testState.inspectRuntimeTerminalProcess.mockResolvedValue({ foregroundProcess: 'codex' })
const args = {
tabId: 'tab-1',
content: ISSUE_URL,
agent: 'codex' as const,
timeoutMs: 1,
onTimeout
}
const promise =
target === 'pty'
? pasteDraftToAgentPtyWhenReady({ ...args, ptyId: 'pty-1' })
: pasteDraftWhenAgentReady(args)
await flushMicrotasks()
await vi.advanceTimersByTimeAsync(20_000)
await expect(promise).resolves.toBe(false)
expect(onTimeout).toHaveBeenCalledTimes(1)
expect(testState.sendRuntimePtyInputVerified).not.toHaveBeenCalled()
}
)
it('ends the Codex readiness wait without a fallback inspection', async () => {
const onTimeout = vi.fn()
testState.inspectRuntimeTerminalProcess.mockReturnValue(new Promise(() => {}))
+10 -8
View File
@@ -73,8 +73,7 @@ export function getSettingsForAgentTabRuntimeOwner(
* 1. `\x1b[?2004h` (DECSET 2004 — bracketed-paste-enable) on the PTY
* output. This is the protocol-level "I accept bracketed paste"
* handshake.
* 2. Either ≥`BRACKETED_PASTE_QUIET_MS` of silence after the last byte of
* the post-handshake render burst, or Codex's composer prompt glyph.
* 2. The agent's composer-ready signal, or its configured quiet window.
*/
export async function pasteDraftWhenAgentReady(args: {
tabId: string
@@ -121,9 +120,11 @@ export async function pasteDraftWhenAgentReady(args: {
// this sidecar subscription attaches. If process/title inspection says the
// launched agent owns the PTY, fall back to a best-effort paste instead of
// silently dropping generated prompts.
const fallbackReady = agentConfig
? await waitForAgentReady(tabId, agentConfig.expectedProcess, { timeoutMs: 1000 })
: { ready: false }
// A running Codex can still own a startup dialog or discard provisional input.
const fallbackReady =
agentConfig && agent !== 'codex'
? await waitForAgentReady(tabId, agentConfig.expectedProcess, { timeoutMs: 1000 })
: { ready: false }
if (!fallbackReady.ready) {
onTimeout?.()
return false
@@ -178,9 +179,10 @@ export async function pasteDraftToAgentPtyWhenReady(args: {
const budget = resolveDraftPasteReadyTimeoutMs(agent, timeoutMs)
const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings)
if (!ready) {
const fallbackReady = agentConfig
? await waitForExpectedAgentOnPty(ptyId, agentConfig.expectedProcess, 1000, settings)
: false
const fallbackReady =
agentConfig && agent !== 'codex'
? await waitForExpectedAgentOnPty(ptyId, agentConfig.expectedProcess, 1000, settings)
: false
if (!fallbackReady) {
onTimeout?.()
return false
@@ -3,11 +3,18 @@
// startup paste, or the draft helper that waits for the agent. Both must write it as launch input.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { TerminalInputKind } from '../../../shared/terminal-input-kind'
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
import { resolveDraftPasteReadyTimeoutMs } from '../../../shared/draft-paste-ready-timeout'
import { bindSettlePaneSerializer } from '@/components/terminal-pane/pty-connection/pane-serializer-settle'
import type { ConnectPanePtySession } from '@/components/terminal-pane/pty-connection/connect-pane-pty-session'
import { STARTUP_DRAFT_PASTE_QUIET_MS } from '@/components/terminal-pane/pty-connection/pty-connect-limits'
import { pasteDraftToAgentPtyWhenReady, submitPromptToAgentPty } from './agent-paste-draft'
const { onTimeout } = vi.hoisted(() => ({ onTimeout: vi.fn() }))
vi.mock('./launch-agent-paste-timeout-notice', () => ({
createPasteReadinessTimeoutNotice: () => ({ onTimeout })
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({ settings: {}, terminalLayoutsByTabId: {} }),
@@ -38,8 +45,7 @@ function stubPtyApi(): { kinds: TerminalInputKind[] } {
return { kinds }
}
/** The pane route: the pane's transport pastes the draft once the agent's composer is ready. */
async function paneRouteKinds(): Promise<TerminalInputKind[]> {
function createPaneDraftSession(agent?: 'codex') {
const { kinds } = stubPtyApi()
const transport = {
getPtyId: () => 'pty-1',
@@ -56,12 +62,20 @@ async function paneRouteKinds(): Promise<TerminalInputKind[]> {
ownsStartupDraftPaste: true,
shouldDeliverStartupViaTerminalPaste: false,
startupDraftPrompt: DRAFT,
startupDraftAgent: agent,
startupDraftAgentConfig: agent ? TUI_AGENT_CONFIG[agent] : undefined,
recordTerminalInputForHibernation: () => {},
deps: { worktreeId: 'wt-1', paneTransportsRef: { current: new Map([[pane.id, transport]]) } }
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the session is an any-typed bag; the draft path reads only the fields above.
const session = bag as unknown as ConnectPanePtySession
bindSettlePaneSerializer(session)
return { session, kinds }
}
/** The pane route: the pane's transport pastes the draft once the agent's composer is ready. */
async function paneRouteKinds(): Promise<TerminalInputKind[]> {
const { session, kinds } = createPaneDraftSession()
session.observeStartupDraftPasteReadiness('\x1b[?2004h')
await vi.advanceTimersByTimeAsync(STARTUP_DRAFT_PASTE_QUIET_MS)
await vi.runAllTimersAsync()
@@ -85,6 +99,7 @@ async function helperRouteKinds(): Promise<TerminalInputKind[]> {
describe('a created worktree’s startup draft', () => {
beforeEach(() => {
vi.useFakeTimers()
onTimeout.mockClear()
})
afterEach(() => {
@@ -110,4 +125,17 @@ describe('a created worktree’s startup draft', () => {
await expect(submitted).resolves.toBe(true)
expect(new Set(kinds)).toEqual(new Set(['driving']))
})
it('never delivers a pane-owned Codex draft after readiness times out', async () => {
const { session, kinds } = createPaneDraftSession('codex')
session.observeStartupDraftPasteReadiness('\x1b[?2004hWaiting for Codex...')
await vi.advanceTimersByTimeAsync(resolveDraftPasteReadyTimeoutMs('codex'))
expect(onTimeout).toHaveBeenCalledOnce()
expect(kinds).toEqual([])
session.observeStartupDraftPasteReadiness('\x1b[2K› ')
await vi.runAllTimersAsync()
expect(kinds).toEqual([])
expect(onTimeout).toHaveBeenCalledOnce()
})
})
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest'
import { createDraftPasteReadyScanner } from './draft-paste-ready-scanner'
const MODES = '\x1b[?2004h\x1b[?1049h'
const BEGIN = '\x1b[?2026h\x1b[?25l'
const END = '\x1b[37;3H\x1b[?25h\x1b[?2026l'
// Geometry from the 0.160.1 startup capture, with synthetic labels.
const PROVISIONAL = `${MODES}${BEGIN}\x1b[37;1H›\x1b[40;3H? for shortcuts${END}`
const LIVE = `${BEGIN}\x1b[39;3HModel high · folder${END}`
describe('Codex fullscreen composer paste gate', () => {
it('waits for the live footer, then accepts cursor-only redraws', () => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
expect(scanner.observe(PROVISIONAL).ready).toBe(false)
expect(scanner.observe(LIVE).ready).toBe(true)
expect(scanner.observe(`${BEGIN}${END}`).ready).toBe(true)
})
it('does not resolve in the middle of a synchronized frame', () => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
scanner.observe(PROVISIONAL)
expect(scanner.observe(`${BEGIN}\x1b[39;3HModel · folder`).ready).toBe(false)
expect(scanner.observe(END).ready).toBe(true)
})
it.each([']', 'P', 'X', '^', '_'])('ignores a footer inside ESC %s control strings', (kind) => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
scanner.observe(PROVISIONAL)
expect(scanner.observe(`\x1b${kind}0;\x1b[39;3HModel · folder\x1b\\`).ready).toBe(false)
expect(scanner.observe(LIVE).ready).toBe(true)
})
it.each([']', 'P', 'X', '^', '_'])('keeps split ESC %s control strings hidden', (kind) => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
scanner.observe(PROVISIONAL)
const payload = `\x1b${kind}${'x'.repeat(1024)}\x1b[39;3HModel · folder\x1b\\`
for (const char of payload) {
expect(scanner.observe(char).ready).toBe(false)
}
expect(scanner.observe(LIVE).ready).toBe(true)
})
it.each(['\x1b[?2004l', '\x1b[?1049l'])(
'withdraws the composer when its terminal mode ends (%s)',
(leave) => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
expect(scanner.observe(PROVISIONAL + LIVE + leave).ready).toBe(false)
expect(scanner.observe('› shell prompt\x1b[?25h').ready).toBe(false)
}
)
it('does not mistake the shortcut hint for a footer after multiline early input', () => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
scanner.observe(PROVISIONAL)
expect(
scanner.observe(`${BEGIN}\x1b[38;3Hcontinued note\x1b[38;17H\x1b[?25h\x1b[?2026l`).ready
).toBe(false)
})
it('keeps the reserved footer row when early multiline input contains footer glyphs', () => {
const scanner = createDraftPasteReadyScanner('codex-composer-prompt')
scanner.observe(PROVISIONAL)
expect(
scanner.observe(
`${BEGIN}\x1b[35;1H› first line\x1b[36;3H? second line\x1b[37;3Hthird · line\x1b[35;3H\x1b[?25h\x1b[?2026l`
).ready
).toBe(false)
expect(scanner.observe(LIVE).ready).toBe(true)
})
})
@@ -0,0 +1,96 @@
import { createTerminalEscapeScanner } from './terminal-escape-scanner'
/** The fullscreen loading frame has a composer too; only the live chat paints its footer. */
export function createCodexComposerReadyScanner(): {
observe: (data: string) => { ready: boolean }
} {
let bracketedPaste = false
let altScreen = false
let leftAltScreen = false
let synchronized = false
let sawSynchronizedFrame = false
let promptSeen = false
let promptRow: number | null = null
let footerRow: number | null = null
let hintRow: number | null = null
let row: number | null = null
let cursorShown = false
const paintedRows = new Set<number>()
const resetLayout = (): void => {
promptSeen = false
promptRow = null
footerRow = null
hintRow = null
row = null
cursorShown = false
paintedRows.clear()
}
const scanner = createTerminalEscapeScanner({
onCsi: (privateMarker, params, final) => {
if (privateMarker === '?' && (final === 'h' || final === 'l')) {
const set = final === 'h'
for (const mode of params.split(';')) {
if (mode === '2004') {
bracketedPaste = set
if (!set) {
resetLayout()
}
} else if (mode === '1049') {
altScreen = set
leftAltScreen = !set
resetLayout()
} else if (mode === '2026') {
synchronized = set
sawSynchronizedFrame = true
} else if (mode === '25') {
cursorShown = set
}
}
} else if (final === 'H' || final === 'f') {
row = Math.max(1, Number(params.split(';')[0] || '1'))
} else if (final === 'J' && params === '2') {
resetLayout()
}
},
onText: (text) => {
for (const char of text) {
if (row !== null && !/\s/.test(char) && paintedRows.size < 128) {
paintedRows.add(row)
}
if (char === '\n' && row !== null) {
row += 1
} else if (char === '›' && (bracketedPaste || altScreen)) {
promptSeen = true
promptRow = row
} else if (char === '·' && promptRow !== null && row !== null && row > promptRow) {
footerRow = row
} else if (char === '?' && promptRow !== null && row !== null && row > promptRow) {
hintRow = Math.max(hintRow ?? row, row)
}
}
}
})
return {
observe: (data) => {
scanner.observe(data)
const liveFullscreenComposer =
altScreen &&
!synchronized &&
cursorShown &&
promptRow !== null &&
row !== null &&
row >= promptRow &&
hintRow !== null &&
// Single-item status lines have no separator; their reserved row is below the input padding.
((footerRow !== null && footerRow === hintRow - 1 && row < footerRow) ||
(row + 2 < hintRow && paintedRows.has(hintRow - 1)))
// Older inline builds do not paint synchronized fullscreen frames.
const legacyComposer = promptSeen && !leftAltScreen && (!altScreen || !sawSynchronizedFrame)
return {
ready: bracketedPaste && (liveFullscreenComposer || legacyComposer)
}
}
}
}
+8 -54
View File
@@ -1,12 +1,12 @@
import type { DraftPasteReadySignal, TuiAgentConfig } from './tui-agent-config'
import { createOpenCodeAgentRowScanner } from './opencode-agent-row-scanner'
import { createCodexComposerReadyScanner } from './codex-composer-ready-scanner'
// Why: agents enable bracketed paste (DECSET 2004) before their composer is
// actually mounted/focused. These markers let the scanner detect the real
// "input is ready" moment per agent instead of guessing from output silence.
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
const DECRST_BRACKETED_PASTE = '\x1b[?2004l'
const CODEX_COMPOSER_PROMPT = '›'
// Why: opencode emits the DECTCEM show-cursor only once the composer row is
// mounted and the text cursor is placed in it — a "composer ready" signal,
// analogous to Codex's prompt glyph. It fires ~2s after bracketed paste is
@@ -42,23 +42,18 @@ type DraftPasteReadySignalSpec = {
}
// Signals that read the screen's structure, not one marker; a Record so none lacks its scanner.
type StructuralSignal = 'opencode-agent-row'
type StructuralSignal = 'opencode-agent-row' | 'codex-composer-prompt'
const STRUCTURAL_SCANNERS: Record<
StructuralSignal,
() => { observe: (data: string) => { ready: boolean; readyAfterMs: number | null } }
() => { observe: (data: string) => { ready: boolean; readyAfterMs?: number | null } }
> = {
'opencode-agent-row': createOpenCodeAgentRowScanner
'opencode-agent-row': createOpenCodeAgentRowScanner,
'codex-composer-prompt': createCodexComposerReadyScanner
}
type SingleSignal = Exclude<DraftPasteReadySignal, StructuralSignal>
const DRAFT_PASTE_READY_SIGNALS: Record<SingleSignal, DraftPasteReadySignalSpec> = {
'codex-composer-prompt': {
markerAnchor: DECSET_BRACKETED_PASTE,
markerAnchorEnd: null,
marker: CODEX_COMPOSER_PROMPT,
quietAnchor: null
},
'render-cursor-after-bracketed-paste': {
markerAnchor: DECSET_BRACKETED_PASTE,
// Why: the launching shell's prompt turns bracketed paste on and back off before exec
@@ -115,7 +110,7 @@ const ANCHOR_CARRY_CHARS = 7
/** Whether the signal has a composer marker, rather than only a quiet window after its anchor. */
export function draftPasteReadySignalHasMarker(readySignal: DraftPasteReadySignal): boolean {
return DRAFT_PASTE_READY_SIGNALS[readySignal].marker !== null
return !isSingleSignal(readySignal) || DRAFT_PASTE_READY_SIGNALS[readySignal].marker !== null
}
export type DraftPasteReadyScanResult = {
@@ -149,9 +144,8 @@ export function resolvePasteReadySignal(
* and return types differ.
*
* Per agent signal:
* - `codex-composer-prompt`: ready when the `›` glyph renders after DECSET
* 2004, or when DECSET follows a glyph rendered while Codex owns the
* alternate screen; never arms the quiet window.
* - `codex-composer-prompt`: fullscreen waits for the live footer and composer
* cursor at a completed frame; older inline builds keep their glyph signal.
* - `render-cursor-after-bracketed-paste`: ready when DECTCEM show-cursor
* (`\x1b[?25h`) renders while DECSET 2004 is held; `\x1b[?2004l` revokes it.
* Like Codex it does NOT arm the quiet window: opencode stays silent for up
@@ -217,11 +211,8 @@ function createSingleSignalScanner(readySignal: SingleSignal): {
let recent = ''
let postAnchorRecent = ''
let anchorCarry = ''
let codexCarry = ''
let sawMarkerAnchor = false
let sawQuietAnchor = false
let codexAltScreen = false
let sawCodexPromptInAltScreen = false
const {
markerAnchor,
@@ -274,37 +265,6 @@ function createSingleSignalScanner(readySignal: SingleSignal): {
return false
}
const scanCodexPreAnchorPrompt = (data: string): void => {
const window = codexCarry + data
codexCarry = window.slice(-ANCHOR_CARRY_CHARS)
let cursor = 0
while (cursor < window.length) {
const enterIndex = window.indexOf(DECSET_ALT_SCREEN, cursor)
const leaveIndex = window.indexOf(DECRST_ALT_SCREEN, cursor)
const promptIndex = window.indexOf(CODEX_COMPOSER_PROMPT, cursor)
const nextIndex = Math.min(
...[enterIndex, leaveIndex, promptIndex].filter((index) => index !== -1)
)
if (!Number.isFinite(nextIndex)) {
return
}
if (nextIndex === enterIndex) {
codexAltScreen = true
sawCodexPromptInAltScreen = false
cursor = nextIndex + DECSET_ALT_SCREEN.length
} else if (nextIndex === leaveIndex) {
codexAltScreen = false
sawCodexPromptInAltScreen = false
cursor = nextIndex + DECRST_ALT_SCREEN.length
} else {
if (codexAltScreen) {
sawCodexPromptInAltScreen = true
}
cursor = nextIndex + CODEX_COMPOSER_PROMPT.length
}
}
}
return {
observe(data: string): DraftPasteReadyScanResult {
const combined = recent + data
@@ -312,9 +272,6 @@ function createSingleSignalScanner(readySignal: SingleSignal): {
if (!sawQuietAnchor && quietAnchor !== null && combined.includes(quietAnchor)) {
sawQuietAnchor = true
}
if (readySignal === 'codex-composer-prompt' && !sawMarkerAnchor) {
scanCodexPreAnchorPrompt(data)
}
if (signalMarker !== null && markerAnchor !== null) {
if (markerAnchorEnd !== null) {
// Why: carry only the bytes an anchor could straddle, so already-scanned
@@ -329,9 +286,6 @@ function createSingleSignalScanner(readySignal: SingleSignal): {
const anchorIndex = combined.indexOf(markerAnchor)
if (anchorIndex !== -1) {
sawMarkerAnchor = true
if (readySignal === 'codex-composer-prompt' && sawCodexPromptInAltScreen) {
return { ready: true, armQuietTimer: false }
}
const postAnchorChunk = combined.slice(anchorIndex + markerAnchor.length)
if (postAnchorChunk.includes(signalMarker)) {
return { ready: true, armQuietTimer: false }
+5 -53
View File
@@ -1,3 +1,5 @@
import { createTerminalEscapeScanner } from './terminal-escape-scanner'
/**
* OpenCode's submit readiness: the `<agent> · <model>` row along the bottom of its input box is
* painted only once its agent list has loaded from its server, and until then it drops an Enter.
@@ -14,25 +16,10 @@
* screen or turning bracketed paste off withdraws what it established.
*/
const ESC = '\x1b'
const AGENT_ROW_SEPARATOR = '\u00b7'
const BOX_BOTTOM_LEFT = '\u2579'
// Why: a narrow pane never paints the row (OpenCode 2 drops the agent below 44 columns), and nor
// does a terminal that does not forward the alternate screen; a theme whose raised background is
// transparent paints no `╹`, so its starts always wait out the grace. Once the box's cursor has shown, the
// earlier box rule takes over after this grace, so those starts still get their task, as before.
// 5 s because the row trailed the box by at most 1.8 s in every recorded start, loaded or not.
// Narrow panes and transparent themes omit the row; retain the earlier box signal after a grace.
export const OPENCODE_AGENT_ROW_GRACE_MS = 5_000
// Longest unfinished escape carried into the next read; anything longer is noise, not a split.
const MAX_CARRIED_ESCAPE_CHARS = 512
/* oxlint-disable no-control-regex -- these match terminal escape sequences, which start with ESC */
const CSI_RE = /^\x1b\[([?>=<]?)([0-9;]*)([ -/]*[@-~])/
const UNFINISHED_CSI_RE = /^\x1b\[[?>=<]?[0-9;]*[ -/]*$/
// OSC and DCS strings (titles, queries) end at BEL or ST; their text is never painted.
const STRING_RE = /^\x1b[\]P][^\x07\x1b]*(?:\x07|\x1b\\)/
const STRING_START_RE = /^\x1b[\]P]/
const STRING_END_RE = /\x07|\x1b\\/
export type OpenCodeAgentRowScan = {
/** The row exists and the box's cursor is shown: Enter will be taken. */
@@ -44,7 +31,6 @@ export type OpenCodeAgentRowScan = {
export function createOpenCodeAgentRowScanner(): {
observe: (data: string) => OpenCodeAgentRowScan
} {
let carry = ''
let altScreen = false
// Once a full-screen app has left, a box cursor outside the alternate screen is the shell's.
let leftAltScreen = false
@@ -118,33 +104,10 @@ export function createOpenCodeAgentRowScanner(): {
}
}
const scanner = createTerminalEscapeScanner({ onCsi, onText })
return {
observe(data: string): OpenCodeAgentRowScan {
const input = carry + data
carry = ''
let index = 0
while (index < input.length) {
const escapeAt = input.indexOf(ESC, index)
if (escapeAt === -1) {
onText(input.slice(index))
break
}
onText(input.slice(index, escapeAt))
const rest = input.slice(escapeAt)
const sequence = CSI_RE.exec(rest) ?? STRING_RE.exec(rest)
if (sequence) {
if (sequence[0][1] === '[') {
onCsi(sequence[1], sequence[2], sequence[3])
}
index = escapeAt + sequence[0].length
continue
}
if (isUnfinishedEscape(rest)) {
carry = rest
break
}
index = escapeAt + 2
}
scanner.observe(data)
// Leaving the alternate screen clears rowPainted, so it only holds while OpenCode is drawn.
const graceApplies = boxCursorShown && !rowPainted && (altScreen || !leftAltScreen)
return {
@@ -154,14 +117,3 @@ export function createOpenCodeAgentRowScanner(): {
}
}
}
function isUnfinishedEscape(rest: string): boolean {
if (rest.length > MAX_CARRIED_ESCAPE_CHARS) {
return false
}
return (
rest === ESC ||
UNFINISHED_CSI_RE.test(rest) ||
(STRING_START_RE.test(rest) && !STRING_END_RE.test(rest))
)
}
+78
View File
@@ -0,0 +1,78 @@
const ESC = '\x1b'
// Longest unfinished escape carried into the next read; anything longer is noise, not a split.
const MAX_CARRIED_ESCAPE_CHARS = 512
/* oxlint-disable no-control-regex -- these match terminal escape sequences, which start with ESC */
const CSI_RE = /^\x1b\[([?>=<]?)([0-9;]*)([ -/]*[@-~])/
const UNFINISHED_CSI_RE = /^\x1b\[[?>=<]?[0-9;]*[ -/]*$/
// Terminal control strings (titles, graphics, queries) never paint their payload text.
const STRING_RE = /^\x1b[\]PX^_][\s\S]*?(?:\x07|\x1b\\)/
const STRING_START_RE = /^\x1b[\]PX^_]/
const STRING_END_RE = /\x07|\x1b\\/
type TerminalEscapeObserver = {
onCsi: (privateMarker: string, params: string, final: string) => void
onText: (text: string) => void
}
/** Incremental painted-text/CSI reader; terminal string payloads are never painted text. */
export function createTerminalEscapeScanner(observer: TerminalEscapeObserver): {
observe: (data: string) => void
} {
let carry = ''
let stringPending = false
return {
observe(data: string): void {
const input = carry + data
carry = ''
let index = 0
if (stringPending) {
const end = STRING_END_RE.exec(input)
if (!end) {
carry = input.endsWith(ESC) ? ESC : ''
return
}
stringPending = false
index = end.index + end[0].length
}
while (index < input.length) {
const escapeAt = input.indexOf(ESC, index)
if (escapeAt === -1) {
observer.onText(input.slice(index))
break
}
observer.onText(input.slice(index, escapeAt))
const rest = input.slice(escapeAt)
const sequence = CSI_RE.exec(rest) ?? STRING_RE.exec(rest)
if (sequence) {
if (sequence[0][1] === '[') {
observer.onCsi(sequence[1], sequence[2], sequence[3])
}
index = escapeAt + sequence[0].length
continue
}
if (STRING_START_RE.test(rest)) {
stringPending = true
carry = rest.endsWith(ESC) ? ESC : ''
break
}
if (isUnfinishedEscape(rest)) {
carry = rest
break
}
index = escapeAt + 2
}
}
}
}
function isUnfinishedEscape(rest: string): boolean {
if (rest.length > MAX_CARRIED_ESCAPE_CHARS) {
return false
}
return (
rest === ESC ||
UNFINISHED_CSI_RE.test(rest) ||
(STRING_START_RE.test(rest) && !STRING_END_RE.test(rest))
)
}