TUI: /goal 4k-char validation skipped when entire `/goal …` command is pasted as one >1000-char chunk
Summary
When a user pastes a single clipboard string of the form "/goal <large objective>" into an empty composer and the whole paste exceeds LARGE_PASTE_CHAR_THRESHOLD (1000 chars), the TUI never recognizes the input as a /goal slash command. The 4,000-character MAX_THREAD_GOAL_OBJECTIVE_CHARS validation is silently skipped: no error appears, and the literal "/goal …" text is submitted to the model as a regular user message. The model then frequently follows up by calling the create_goal function tool with its own summarized objective, so a goal ends up being created anyway — from the user's perspective the 4k limit looks bypassed.
Observed on codex CLI 0.128.0; reproducible against current main (6a225e400).
Reproduction
- Start a TUI session with the goals feature enabled (empty thread).
- Copy a single string like
/goalfollowed by ~4,001x's to the clipboard (anything that pushes the whole paste over 1000 chars and the objective over 4000 chars). - Paste into an empty composer (the textarea now shows
[Pasted Content N chars]) and press Enter.
Expected: the same Goal objective is too long: 4,001 characters. Limit: 4,000 characters. Put longer instructions in a file … error that fires when the user types /goal first and then pastes the long objective. That path is already covered by goal_slash_command_rejects_large_paste_using_expanded_length at codex-rs/tui/src/chatwidget/tests/goal_validation.rs:133.
Actual:
- No validation error is shown.
- The full
"/goal …"text is submitted as a normaluser_message. - The model often calls the
create_goalfunction tool with a summarized objective, so a goal is created without ever passing through the TUI length guard.
I reproduced this with a one-off test in codex-rs/tui/src/chatwidget/tests/goal_validation.rs that does chat.handle_paste(format!("/goal {}", "x".repeat(MAX_THREAD_GOAL_OBJECTIVE_CHARS + 1))) on an empty composer and submits. SetThreadGoalObjective is never emitted, no Goal objective is too long error is rendered, and a plain Op::UserTurn is sent.
Root cause
In codex-rs/tui/src/bottom_pane/chat_composer.rs:
handle_paste(line 949): any paste withchar_count > LARGE_PASTE_CHAR_THRESHOLD(1000) is replaced by a single[Pasted Content N chars]placeholder element in the textarea, with the full text stashed inpending_pastes.try_dispatch_bare_slash_command(line 3034) andtry_dispatch_slash_command_with_args(line 3070) both inspectself.textarea.text()before expansion and callparse_slash_name, which requires a leading/. When the entire/goal …blob was pasted, the textarea contains only[Pasted Content N chars], so both returnNone.- Execution falls through to
prepare_submission_text_with_options(line 2778), which expands the placeholder. At that point the only length check isMAX_USER_INPUT_TEXT_CHARS(~1 MiB), notMAX_THREAD_GOAL_OBJECTIVE_CHARS. handle_submission_with_timethen emitsInputResult::Submittedwith the expanded"/goal …"text. The slash-dispatch path that callsgoal_objective_with_pending_pastes_is_allowed(codex-rs/tui/src/chatwidget/goal_validation.rs:17) is never entered.
Existing paste tests miss this case because they pre-fill "/goal " into the composer before pasting, so the textarea already starts with /:
goal_slash_command_rejects_large_paste_using_expanded_length(line 133)goal_slash_command_giant_paste_uses_goal_specific_error(line 166)
Note: the protocol-side validate_thread_goal_objective (codex-rs/protocol/src/protocol.rs:3551) does enforce 4k on create_goal arguments, but the model may summarize the objective before calling the tool, so the protocol guard often does not trip for this path. It is also not user-visible feedback at the moment of submission.
Why it matters
- Silent failure mode: the user pastes a long
/goal, sees no error, and may not realize the goal that gets set (if any) was paraphrased by the model rather than created from their literal text. - Token waste: the full pasted text is round-tripped through the model even though the TUI guard was meant to short-circuit it.
- Inconsistent with the existing pasted-after-
/goalbehavior, which was the whole point of #20746.
Proposed fix direction
Make the slash-command detector consider the expanded text when the composer leads with a paste placeholder. Two reasonable shapes:
- In
try_dispatch_bare_slash_command/try_dispatch_slash_command_with_args, when the raw textarea starts with a[Pasted Content …]placeholder, parseparse_slash_nameagainst the expansion (or against the first pending-paste content) and, if it resolves to a known builtin, route through the normal slash-dispatch path sogoal_objective_with_pending_pastes_is_allowedruns. - Alternatively, in
prepare_submission_text_with_options, after expansion, if the resulting text starts with a known slash command, redirect into the slash-dispatch path instead of returningInputResult::Submitted.
Either path should also pick up a regression test mirroring goal_slash_command_rejects_large_paste_using_expanded_length but with the entire "/goal " + objective pasted into an empty composer.
Happy to send a PR if a maintainer agrees with one of the directions.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗