mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 08:02:21 +00:00
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:
@@ -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[6n]10;?\]11;?\[?u[c[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[1;1H[J[?2026h[?25l[2;3H>_[2;6H[1mOpenAI Codex[22m[2m[2m (v0.160.1)[3;6Hloading[5;3H[22mWhat[5;8Hare[5;12Hwe[5;15Hpoking[5;22Hwith[5;27Ha[5;29Hmetaphorical[5;42Hstick?[12;56H[2m⣀⣤⣤⣤⣀⣀[13;53H⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀[14;51H⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄[15;50H⣀⣾⣿⣿⠃[15;56H⣀⣴⣾⣿⣿⡿⠟⠋[15;65H⣀⡀[15;68H⠈⠙⢿⣿⣿⣧[16;47H⣀⣶⣿⣿⣿⣿⡇[16;55H⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇[17;46H⣰⣿⣿⡿⢿⣿⣿⡇[17;55H⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷[18;45H⢰⣿⣿⡟⠁⢸⣿⣿⡇[18;55H⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀[19;45H⣾⣿⣿⠃[19;50H⢸⣿⣿⡇[19;55H⢸⣿⣿⠋⠁[19;62H⠈⠙⢻⣿⣿⣿⣶⣤⡀[19;72H⠹⢿⣿⣷⡄[20;45H⢸⣿⣿⣇[20;50H⠸⣿⣿⣷⣦⣼⣿⣿[20;64H⢸⣿⣿⠻⢿⣿⣿⡇[20;73H⠘⣿⣿⣿[21;45H⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀[21;62H⢀⣠⣼⣿⣿[21;69H⣿⣿⡇[21;74H⣿⣿⣿⠇[22;47H⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿[22;69H⣿⣿⡇[22;73H⣠⣿⣿⣿[23;47H⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿[23;69H⣿⣿⣷⣴⣿⣿⣿⠃[24;47H⠘⣿⣿⣷[24;52H⠈⠛⢿⣿⣿⣿⠿⠋⠉[24;62H⣠⣴⣿⣿⣿[24;69H⣿⣿⣿⣿⣿⠟⠁[25;48H⠻⣿⣿⣿⣤⣀[25;55H⠈⠉[25;58H⢀⣤⣶⣿⣿⣿⠿⠟⠁[25;68H⣰⣿⣿⡟⠋⠁[26;49H⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁[26;66H⣠⣼⣿⣿⡟[27;52H⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏[28;59H⠈⠙⠛⠿⠿⠿⠿⠛⠉[37;1H[22m[1m›[37;3H[22m[2m[2mAsk Codex to do anything[40;3H[22m[1m?[40;5H[22mfor[40;9Hshortcuts[m[m[0m[1;1H[0 q[1;1H [m[m[0m[37;3H[?25h[?2026l[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[?25l[5;2H[0m[m[K[3;6H[2m~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx[4;1H permissions: [22m[1mYOLO mode[6;3H[22mWhat[6;8Hare[6;12Hwe[6;15Hpoking[6;22Hwith[6;27Ha[6;29Hmetaphorical[6;42Hstick?[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[?25l[39;3HGPT-6.1-Sol[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠸ workspace-xxxxxxxxxxxxxx]0;⠼ workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠴ workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠦ workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠧ workspace-xxxxxxxxxxxxxx]0;⠇ workspace-xxxxxxxxxxxxxx]0;⠏ workspace-xxxxxxxxxxxxxx]0;⠋ workspace-xxxxxxxxxxxxxx]0;workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?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[6n]10;?\]11;?\[?u[c[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[1;1H[J[?2026h[?25l[2;3H>_[2;6H[1mOpenAI Codex[22m[2m[2m (v0.160.1)[3;6Hloading[5;3H[22mMay[5;7Hthe[5;11Hsource[5;18Hbe[5;21Hwith[5;26Hyou.[12;56H[2m⣀⣤⣤⣤⣀⣀[13;53H⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀[14;51H⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄[15;50H⣀⣾⣿⣿⠃[15;56H⣀⣴⣾⣿⣿⡿⠟⠋[15;65H⣀⡀[15;68H⠈⠙⢿⣿⣿⣧[16;47H⣀⣶⣿⣿⣿⣿⡇[16;55H⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇[17;46H⣰⣿⣿⡿⢿⣿⣿⡇[17;55H⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷[18;45H⢰⣿⣿⡟⠁⢸⣿⣿⡇[18;55H⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀[19;45H⣾⣿⣿⠃[19;50H⢸⣿⣿⡇[19;55H⢸⣿⣿⠋⠁[19;62H⠈⠙⢻⣿⣿⣿⣶⣤⡀[19;72H⠹⢿⣿⣷⡄[20;45H⢸⣿⣿⣇[20;50H⠸⣿⣿⣷⣦⣼⣿⣿[20;64H⢸⣿⣿⠻⢿⣿⣿⡇[20;73H⠘⣿⣿⣿[21;45H⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀[21;62H⢀⣠⣼⣿⣿[21;69H⣿⣿⡇[21;74H⣿⣿⣿⠇[22;47H⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿[22;69H⣿⣿⡇[22;73H⣠⣿⣿⣿[23;47H⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿[23;69H⣿⣿⣷⣴⣿⣿⣿⠃[24;47H⠘⣿⣿⣷[24;52H⠈⠛⢿⣿⣿⣿⠿⠋⠉[24;62H⣠⣴⣿⣿⣿[24;69H⣿⣿⣿⣿⣿⠟⠁[25;48H⠻⣿⣿⣿⣤⣀[25;55H⠈⠉[25;58H⢀⣤⣶⣿⣿⣿⠿⠟⠁[25;68H⣰⣿⣿⡟⠋⠁[26;49H⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁[26;66H⣠⣼⣿⣿⡟[27;52H⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏[28;59H⠈⠙⠛⠿⠿⠿⠿⠛⠉[37;1H[22m[1m›[37;3H[22m[2m[2mAsk Codex to do anything[40;3H[22m[1m?[40;5H[22mfor[40;9Hshortcuts[m[m[0m[1;1H[0 q[1;1H [m[m[0m[37;3H[?25h[?2026l[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[?25l[5;2H[0m[m[K[3;6H[2m~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx[4;1H permissions: [22m[1mYOLO mode[6;3H[22mMay[6;7Hthe[6;11Hsource[6;18Hbe[6;21Hwith[6;26Hyou.[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[?25l[12;2H[0m[m[K[13;2H[0m[m[K[14;2H[0m[m[K[15;2H[0m[m[K[16;2H[0m[m[K[17;2H[0m[m[K[18;2H[0m[m[K[19;2H[0m[m[K[20;2H[0m[m[K[21;2H[0m[m[K[22;2H[0m[m[K[23;2H[0m[m[K[24;2H[0m[m[K[25;2H[0m[m[K[26;2H[0m[m[K[27;2H[0m[m[K[28;2H[0m[m[K[40;2H[0m[m[K[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[?25l[37;14H[0m[m[K[37;3H early note[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l]0;workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[?25l[39;3HGPT-6.1-Sol[39;15Hdefault[39;23H·[39;25H~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx[39;84H·[39;86Hsmarter-codex-issue-paste-detectio…[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l]0;⠸ workspace-xxxxxxxxxxxxxx]0;⠦ workspace-xxxxxxxxxxxxxx[?2026h[?25l[39;15Hhigh · ~/orca/workspaces/orca/smarte[39;52H-codex-issue-paste-d[39;73Htec[39;77Hion · smarte[39;90H-codex-issue-paste-d[39;111Htec[39;115Hion ·[40;3H[1mtab[40;7H[22mto[40;10Hqueue[40;16Hmessage[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?25h[?2026l]0;⠧ workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;14H[?25h[?2026l]0;⠇ workspace-xxxxxxxxxxxxxx]0;⠏ workspace-xxxxxxxxxxxxxx]0;⠋ workspace-xxxxxxxxxxxxxx]0;workspace-xxxxxxxxxxxxxx[?2026h[?25l[40;2H[0m[m[K[m[m[0m[37;14H[?25h[?2026l[?2026h[m[m[0m[37;14H[?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[6n]10;?\]11;?\[?u[c[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[1;1H[J[?2026h[?25l[2;3H>_[2;6H[1mOpenAI Codex[22m[2m[2m (v0.160.1)[3;6Hloading[5;3H[22mAh,[5;7Hthe[5;11Hterminal.[5;21HA[5;23Hclassic[5;31Hmeeting[5;39Hspot.[12;56H[2m⣀⣤⣤⣤⣀⣀[13;53H⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀[14;51H⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄[15;50H⣀⣾⣿⣿⠃[15;56H⣀⣴⣾⣿⣿⡿⠟⠋[15;65H⣀⡀[15;68H⠈⠙⢿⣿⣿⣧[16;47H⣀⣶⣿⣿⣿⣿⡇[16;55H⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇[17;46H⣰⣿⣿⡿⢿⣿⣿⡇[17;55H⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷[18;45H⢰⣿⣿⡟⠁⢸⣿⣿⡇[18;55H⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀[19;45H⣾⣿⣿⠃[19;50H⢸⣿⣿⡇[19;55H⢸⣿⣿⠋⠁[19;62H⠈⠙⢻⣿⣿⣿⣶⣤⡀[19;72H⠹⢿⣿⣷⡄[20;45H⢸⣿⣿⣇[20;50H⠸⣿⣿⣷⣦⣼⣿⣿[20;64H⢸⣿⣿⠻⢿⣿⣿⡇[20;73H⠘⣿⣿⣿[21;45H⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀[21;62H⢀⣠⣼⣿⣿[21;69H⣿⣿⡇[21;74H⣿⣿⣿⠇[22;47H⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿[22;69H⣿⣿⡇[22;73H⣠⣿⣿⣿[23;47H⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿[23;69H⣿⣿⣷⣴⣿⣿⣿⠃[24;47H⠘⣿⣿⣷[24;52H⠈⠛⢿⣿⣿⣿⠿⠋⠉[24;62H⣠⣴⣿⣿⣿[24;69H⣿⣿⣿⣿⣿⠟⠁[25;48H⠻⣿⣿⣿⣤⣀[25;55H⠈⠉[25;58H⢀⣤⣶⣿⣿⣿⠿⠟⠁[25;68H⣰⣿⣿⡟⠋⠁[26;49H⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁[26;66H⣠⣼⣿⣿⡟[27;52H⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏[28;59H⠈⠙⠛⠿⠿⠿⠿⠛⠉[37;1H[22m[1m›[37;3H[22m[2m[2mAsk Codex to do anything[40;3H[22m[1m?[40;5H[22mfor[40;9Hshortcuts[m[m[0m[1;1H[0 q[1;1H [m[m[0m[37;3H[?25h[?2026l[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[?25l[12;2H[0m[m[K[13;2H[0m[m[K[14;2H[0m[m[K[15;2H[0m[m[K[16;2H[0m[m[K[17;2H[0m[m[K[18;2H[0m[m[K[19;2H[0m[m[K[20;2H[0m[m[K[21;2H[0m[m[K[22;2H[0m[m[K[23;2H[0m[m[K[24;2H[0m[m[K[25;2H[0m[m[K[26;2H[0m[m[K[27;2H[0m[m[K[28;2H[0m[m[K[37;16H[0m[m[K[40;2H[0m[m[K[36;1H[1m›[36;3H[22mfirst[36;9Hline[37;1H [37;3Hsecond · line[m[m[0m[37;16H[?25h[?2026l[?2026h[m[m[0m[37;16H[?25h[?2026l[?2026h[m[m[0m[37;16H[?25h[?2026l[?2026h[m[m[0m[37;16H[?25h[?2026l[?2026h[m[m[0m[37;16H[?25h[?2026l[?2026h[?25l[5;2H[0m[m[K[3;6H[2m~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx[4;1H permissions: [22m[1mYOLO mode[6;3H[22mAh,[6;7Hthe[6;11Hterminal.[6;21HA[6;23Hclassic[6;31Hmeeting[6;39Hspot.[m[m[0m[37;16H[?25h[?2026l[?2026h[m[m[0m[37;16H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l]0;XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25l[39;3HGPT-6.1-Sol[39;15Hdefault[39;23H·[39;25H..........................................................[39;84H·[39;86H...................................[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25l[39;15Hhigh · ~/o..........................[39;52H....................[39;73H...[39;77H... · ......[39;90H....................[39;111H...[39;115H... ·[40;3H[1mtab[40;7H[22mto[40;10Hqueue[40;16Hmessage[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[m[m[0m[36;13H[?25h[?2026l]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;X XXXXXXXXXXXXXXXXXXXXXXXX]0;XXXXXXXXXXXXXXXXXXXXXXXX[?2026h[?25l[40;2H[0m[m[K[m[m[0m[36;13H[?25h[?2026l[?2026h[m[m[0m[36;13H[?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[6n]10;?\]11;?\[?u[c[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1003h[?1006h[?25l[1;1H[J[?2026h[?25l[2;3H>_[2;6H[1mOpenAI Codex[22m[2m[2m (v0.160.1)[3;6Hloading[5;3H[22mCome[5;8Hon[5;11Hin.[5;15HThere’s[5;23Hroom[5;28Hfor[5;32Han[5;35Hidea.[12;56H[2m⣀⣤⣤⣤⣀⣀[13;53H⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀[14;51H⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄[15;50H⣀⣾⣿⣿⠃[15;56H⣀⣴⣾⣿⣿⡿⠟⠋[15;65H⣀⡀[15;68H⠈⠙⢿⣿⣿⣧[16;47H⣀⣶⣿⣿⣿⣿⡇[16;55H⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇[17;46H⣰⣿⣿⡿⢿⣿⣿⡇[17;55H⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷[18;45H⢰⣿⣿⡟⠁⢸⣿⣿⡇[18;55H⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀[19;45H⣾⣿⣿⠃[19;50H⢸⣿⣿⡇[19;55H⢸⣿⣿⠋⠁[19;62H⠈⠙⢻⣿⣿⣿⣶⣤⡀[19;72H⠹⢿⣿⣷⡄[20;45H⢸⣿⣿⣇[20;50H⠸⣿⣿⣷⣦⣼⣿⣿[20;64H⢸⣿⣿⠻⢿⣿⣿⡇[20;73H⠘⣿⣿⣿[21;45H⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀[21;62H⢀⣠⣼⣿⣿[21;69H⣿⣿⡇[21;74H⣿⣿⣿⠇[22;47H⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿[22;69H⣿⣿⡇[22;73H⣠⣿⣿⣿[23;47H⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿[23;69H⣿⣿⣷⣴⣿⣿⣿⠃[24;47H⠘⣿⣿⣷[24;52H⠈⠛⢿⣿⣿⣿⠿⠋⠉[24;62H⣠⣴⣿⣿⣿[24;69H⣿⣿⣿⣿⣿⠟⠁[25;48H⠻⣿⣿⣿⣤⣀[25;55H⠈⠉[25;58H⢀⣤⣶⣿⣿⣿⠿⠟⠁[25;68H⣰⣿⣿⡟⠋⠁[26;49H⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁[26;66H⣠⣼⣿⣿⡟[27;52H⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏[28;59H⠈⠙⠛⠿⠿⠿⠿⠛⠉[37;1H[22m[1m›[37;3H[22m[2m[2mAsk Codex to do anything[40;3H[22m[1m?[40;5H[22mfor[40;9Hshortcuts[m[m[0m[1;1H[0 q[1;1H [m[m[0m[37;3H[?25h[?2026l[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[?25l[5;2H[0m[m[K[3;6H[2m~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx[4;1H permissions: [22m[1mYOLO mode[6;3H[22mCome[6;8Hon[6;11Hin.[6;15HThere’s[6;23Hroom[6;28Hfor[6;32Han[6;35Hidea.[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;workspace-xxxxxxxxxxxxxx[?2026h[?25l[39;3HGPT-6.1-Sol[39;15Hdefault[39;23H·[39;25H~/orca/workspaces/orca/workspace-xxxxxxxxxxxxxxxxxxxxxxxxx[39;84H·[39;86Hsmarter-codex-issue-paste-detectio…[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠹ workspace-xxxxxxxxxxxxxx[?2026h[?25l[39;15Hhigh · ~/orca/workspaces/orca/smarte[39;52H-codex-issue-paste-d[39;73Htec[39;77Hion · smarte[39;90H-codex-issue-paste-d[39;111Htec[39;115Hion ·[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠸ workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠼ workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠴ workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l]0;⠦ workspace-xxxxxxxxxxxxxx]0;⠧ workspace-xxxxxxxxxxxxxx]0;⠇ workspace-xxxxxxxxxxxxxx]0;⠏ workspace-xxxxxxxxxxxxxx]0;workspace-xxxxxxxxxxxxxx[?2026h[m[m[0m[37;3H[?25h[?2026l[?2026h[m[m[0m[37;3H[?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())
|
||||
})
|
||||
|
||||
+20
-3
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(() => {}))
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user