Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions crates/daemon/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1838,6 +1838,7 @@
};

const LARGE_TEXT_PASTE_CHARS = 16 * 1024;
const CODEX_COMPOSER_SUBMIT_DELAY_MS = 150;
const WEB_PTY_ENGAGEMENT_MS = 3000;
const WEB_PTY_RESIZE_DEBOUNCE_MS = 120;
const COMPOSER_RESIZE_SUPPRESS_PTY_RESIZE_MS = 350;
Expand Down Expand Up @@ -5244,6 +5245,10 @@
return result.reference || `[#file:${result.path}]`;
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function insertComposerText(text) {
const start = inputEl.selectionStart ?? inputEl.value.length;
const end = inputEl.selectionEnd ?? start;
Expand All @@ -5267,9 +5272,17 @@
return btoa(binary);
}

function currentSession() {
return state.sessions.find((x) => x.id === state.currentId) || null;
}

function currentSessionHasPty() {
const s = state.sessions.find((x) => x.id === state.currentId);
return isTerminalSession(s);
return isTerminalSession(currentSession());
}

function currentSessionHarness() {
const s = currentSession();
return String((s && s.harness) || "").toLowerCase();
}

function restoreComposerText(text, sessionId) {
Expand All @@ -5294,10 +5307,18 @@
inputEl.value = "";
resetInputHeightPreservingTerminalScroll();
const pty = currentSessionHasPty();
const harness = currentSessionHarness();
if (pty) {
try {
if (text) {
await rpc("session.pty_input", { session_id: sid, data: encodeBase64Utf8(text) });
if (enter && harness === "codex") {
// Codex classifies rapid terminal input as paste-like and suppresses
// Enter for a short window so pasted newlines stay in the draft. The
// web composer sends the whole prompt as one PTY write, so give Codex
// time to flush that burst before sending the submit Enter.
await sleep(CODEX_COMPOSER_SUBMIT_DELAY_MS);
}
}
if (enter) {
await rpc("session.pty_input", { session_id: sid, data: encodeBase64Utf8("\r") });
Expand Down
43 changes: 43 additions & 0 deletions crates/e2e/tests/web_smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,49 @@ async fn web_client_loads_and_websocket_connects() {
"Insert should still send PTY input: {fit_scroll:?}"
);

let codex_submit_delay: serde_json::Value = page
.evaluate(
r#"
(async () => {
const oldCurrent = state.currentId;
const oldSessions = state.sessions;
const oldRpc = rpc;
const oldSleep = sleep;
const input = document.getElementById('input');
const calls = [];
rpc = async (method, params) => {
calls.push({ method, data: atob(params.data) });
return null;
};
sleep = async (ms) => {
calls.push({ sleep: ms });
};
state.currentId = 's-codex';
state.sessions = [{ id: 's-codex', harness: 'codex', has_pty: true, mode: 'interactive' }];
input.value = 'hello codex';
await submitComposer({ enter: true });
rpc = oldRpc;
sleep = oldSleep;
state.currentId = oldCurrent;
state.sessions = oldSessions;
return calls;
})()
"#,
)
.await
.expect("evaluate codex submit delay")
.into_value::<serde_json::Value>()
.expect("json array");
assert_eq!(
codex_submit_delay,
serde_json::json!([
{ "method": "session.pty_input", "data": "hello codex" },
{ "sleep": 150 },
{ "method": "session.pty_input", "data": "\r" },
]),
"Codex composer submit should let Codex flush paste-burst detection before Enter: {codex_submit_delay:?}"
);

// Reconnect regression: mobile keyboard show/hide can churn the browser's
// viewport and, on some devices, the websocket. Reconnect must not hydrate
// the selected terminal session through transcript/PTY replay, because that
Expand Down
Loading