Files
codex/codex-rs/tui/src/chatwidget/goal_validation.rs
T
Eric Traut f09e1936e0 Validate /goal objective length in TUI (#20746)
## Why

Long `/goal` definitions currently reach lower-level goal validation and
can produce an opaque failure. This bug was reported by a user. Pasted
instruction blocks are especially confusing because the composer can
still contain a paste placeholder before expansion, which may otherwise
fall into the generic prompt-size error path.

There was also a related paste edge case where `/goal ` followed by a
multiline block whose first pasted line was blank looked like a bare
`/goal` command. That showed the goal usage/summary instead of setting
the pasted objective.

## What Changed

This adds TUI-side preflight validation for `/goal <objective>` using
the shared `MAX_THREAD_GOAL_OBJECTIVE_CHARS` limit. Oversized typed,
queued, and pasted goal objectives now fail locally with a goal-specific
message that recommends putting longer instructions in a file and
referencing that file from the goal.

The TUI now also lets inline-argument slash commands consume later-line
arguments before treating the first line as a bare command, so `/goal `
followed by blank lines and then objective text sets the goal instead of
opening the bare `/goal` flow.

## Manual Testing

1. Start the TUI with goals enabled and an active session.
2. Submit `/goal ` followed by exactly 4,000 objective characters. It
should continue through the normal goal-setting path.
3. Submit `/goal ` followed by 4,001 objective characters. It should not
set a goal, and should show `Goal objective is too long: 4,001
characters. Limit: 4,000 characters.` followed by the guidance to put
longer instructions in a file and reference that file from the goal.
4. Type `/goal `, paste a large block that becomes a `[Pasted Content
... chars]` placeholder, then submit. It should validate the expanded
pasted text and show the goal-specific file guidance rather than the
generic prompt-size error.
5. Type `/goal `, paste a multiline block whose first line is blank,
then submit. It should set the objective from the non-blank pasted
content instead of showing `Usage: /goal <objective>` or the bare goal
summary.
6. While a turn is running, queue an oversized `/goal` command. When the
queue drains, it should show the same goal-specific error and should not
emit a goal-setting request.
2026-05-05 09:55:07 -07:00

65 lines
2.3 KiB
Rust

//! Validation helpers for `/goal` objective text.
use super::*;
use crate::bottom_pane::ChatComposer;
use codex_protocol::num_format::format_with_separators;
use codex_protocol::protocol::MAX_THREAD_GOAL_OBJECTIVE_CHARS;
const GOAL_TOO_LONG_FILE_HINT: &str = "Put longer instructions in a file and refer to that file in the goal, for example: /goal follow the instructions in docs/goal.md.";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum GoalObjectiveValidationSource {
Live,
Queued,
}
impl ChatWidget {
pub(super) fn goal_objective_with_pending_pastes_is_allowed(
&mut self,
args: &str,
text_elements: &[TextElement],
) -> bool {
let pending_pastes = self.bottom_pane.composer_pending_pastes();
let objective_chars = if pending_pastes.is_empty() {
args.trim().chars().count()
} else {
let (expanded, _) =
ChatComposer::expand_pending_pastes(args, text_elements.to_vec(), &pending_pastes);
expanded.trim().chars().count()
};
self.goal_objective_char_count_is_allowed(
objective_chars,
GoalObjectiveValidationSource::Live,
)
}
pub(super) fn goal_objective_is_allowed(
&mut self,
objective: &str,
source: GoalObjectiveValidationSource,
) -> bool {
self.goal_objective_char_count_is_allowed(objective.chars().count(), source)
}
fn goal_objective_char_count_is_allowed(
&mut self,
actual_chars: usize,
source: GoalObjectiveValidationSource,
) -> bool {
if actual_chars <= MAX_THREAD_GOAL_OBJECTIVE_CHARS {
return true;
}
let actual_chars = format_with_separators(actual_chars as i64);
let max_chars = format_with_separators(MAX_THREAD_GOAL_OBJECTIVE_CHARS as i64);
self.add_error_message(format!(
"Goal objective is too long: {actual_chars} characters. Limit: {max_chars} characters. {GOAL_TOO_LONG_FILE_HINT}"
));
if source == GoalObjectiveValidationSource::Live {
self.bottom_pane
.set_composer_text(String::new(), Vec::new(), Vec::new());
self.bottom_pane.drain_pending_submission_state();
}
false
}
}